From a6928abf481831facd87b6d137b203bddf0bf43d Mon Sep 17 00:00:00 2001 From: bherbruck Date: Wed, 14 Oct 2026 10:36:56 -0400 Subject: [PATCH] Tell people when a newer release is out (#1100) At start, and at most once a day, a background thread asks GitHub for the latest stable release (one HTTPS GET to releases/latest, nothing else sent; 15 s timeout; silent when offline; GitHub's rate limit respected). When it is newer than this build, an Update button appears next to Report a bug. It opens a window with the version, the release notes and Download (the asset that fits this installation), Skip this version and Remind me later. - Versions compare as semver; a nightly counts as the release it was built after, so it is offered only a newer stable release. - The installation is detected (AppImage, Flatpak, Snap from the store or a file, deb, rpm, tar.gz, Windows MSI/portable/exe, macOS app); Download picks the fixed-name asset. Development builds and the Flatpak (no network permission) never check. - Preferences > General > Updates: Check for updates automatically (on) and Check now. - HTTP via ureq + rustls with the operating system's certificates (desktop app only). - docs: Updates and privacy page. Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 228 ++++- Cargo.toml | 1 + THIRD-PARTY-LICENSES.txt | 1265 +++++++++++++++++++++++++++- apps/solidcraft/Cargo.toml | 3 + apps/solidcraft/src/main.rs | 40 + assets/icons/ui/source/icons.rs | 6 + assets/locales/de.json | 26 +- assets/locales/en.json | 28 +- assets/locales/ja.json | 28 +- assets/locales/ko.json | 26 +- assets/locales/zh.json | 28 +- crates/ui-egui/Cargo.toml | 1 + crates/ui-egui/src/lib.rs | 8 + crates/ui-egui/src/prefs.rs | 11 + crates/ui-egui/src/toolbar.rs | 6 +- crates/ui-egui/src/update.rs | 959 +++++++++++++++++++++ crates/ui-egui/src/update/tests.rs | 281 ++++++ docs/book/src/SUMMARY.md | 1 + docs/book/src/guide/updates.md | 69 ++ docs/control-protocol.md | 2 +- docs/releasing.md | 3 +- 21 files changed, 2969 insertions(+), 51 deletions(-) create mode 100644 crates/ui-egui/src/update.rs create mode 100644 crates/ui-egui/src/update/tests.rs create mode 100644 docs/book/src/guide/updates.md diff --git a/Cargo.lock b/Cargo.lock index 2906e54d..ac5740e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.10.0" @@ -710,6 +716,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -723,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -736,7 +752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -1578,6 +1594,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2449,6 +2481,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "orbclient" version = "0.3.55" @@ -2949,6 +2987,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "robust" version = "1.2.0" @@ -3016,6 +3068,80 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -3031,6 +3157,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3043,6 +3178,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.3.0" @@ -3295,9 +3453,11 @@ dependencies = [ "objc2-foundation 0.3.2", "quick-xml", "rfd", + "rustls", "serde_json", "solidcraft-engine", "solidcraft-ui-egui", + "ureq", "winit", ] @@ -3433,6 +3593,7 @@ dependencies = [ "image", "log", "pollster 0.4.0", + "semver", "serde", "serde_json", "solidcraft-engine", @@ -3490,6 +3651,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -3912,6 +4079,40 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -3924,6 +4125,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4246,6 +4453,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.1.12" @@ -4734,7 +4950,7 @@ dependencies = [ "calloop 0.13.0", "cfg_aliases", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics", "cursor-icon", "dpi", @@ -5042,6 +5258,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + [[package]] name = "zerotrie" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 8faed3d7..f89f2ddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ serde = { version = "1", features = ["derive", "rc"] } serde_json = { version = "1", features = ["float_roundtrip"] } log = "0.4" sha2 = "0.10" +semver = "1" # geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it. truck-modeling = "0.6" diff --git a/THIRD-PARTY-LICENSES.txt b/THIRD-PARTY-LICENSES.txt index 98a4bf3e..c46f8b12 100644 --- a/THIRD-PARTY-LICENSES.txt +++ b/THIRD-PARTY-LICENSES.txt @@ -1,6 +1,6 @@ Third-party software in SolidCraft -SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 453 crates below. +SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 476 crates below. Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files. SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE. @@ -40,6 +40,7 @@ atomic-waker 1.1.2 (Apache-2.0 OR MIT) atspi 0.29.0 (Apache-2.0 OR MIT) atspi-common 0.13.0 (Apache-2.0 OR MIT) atspi-proxies 0.13.0 (Apache-2.0 OR MIT) +base64 0.23.1 (MIT OR Apache-2.0) bit-set 0.10.0 (Apache-2.0 OR MIT) bit-vec 0.9.1 (Apache-2.0 OR MIT) bitflags 1.3.2 (MIT/Apache-2.0) @@ -68,6 +69,7 @@ color 0.3.3 (Apache-2.0 OR MIT) combine 4.6.8 (MIT) concurrent-queue 2.5.0 (Apache-2.0 OR MIT) convert_case 0.4.0 (MIT) +core-foundation 0.10.1 (MIT OR Apache-2.0) core-foundation 0.9.4 (MIT OR Apache-2.0) core-foundation-sys 0.8.7 (MIT OR Apache-2.0) core-graphics 0.23.2 (MIT OR Apache-2.0) @@ -148,6 +150,8 @@ hashbrown 0.17.1 (MIT OR Apache-2.0) hermit-abi 0.5.3 (MIT OR Apache-2.0) hex 0.4.3 (MIT OR Apache-2.0) hidapi 2.6.7 (MIT) +http 1.5.0 (MIT OR Apache-2.0) +httparse 1.10.1 (MIT OR Apache-2.0) iana-time-zone 0.1.65 (MIT OR Apache-2.0) iana-time-zone-haiku 0.1.2 (MIT OR Apache-2.0) icu_collections 2.3.0 (Unicode-3.0) @@ -228,6 +232,7 @@ objc2-ui-kit 0.3.2 (Zlib OR Apache-2.0 OR MIT) objc2-uniform-type-identifiers 0.2.2 (MIT) objc2-user-notifications 0.2.2 (MIT) once_cell 1.21.4 (MIT OR Apache-2.0) +openssl-probe 0.2.1 (MIT OR Apache-2.0) orbclient 0.3.55 (MIT) ordered-float 5.5.0 (MIT) ordered-stream 0.2.0 (MIT OR Apache-2.0) @@ -281,16 +286,27 @@ redox_syscall 0.5.18 (MIT) redox_syscall 0.9.4 (MIT) renderdoc-sys 1.1.0 (MIT OR Apache-2.0) rfd 0.17.2 (MIT) +ring 0.17.14 (Apache-2.0 AND ISC) robust 1.2.0 (MIT OR Apache-2.0) ron 0.12.2 (MIT OR Apache-2.0) rustc-hash 1.1.0 (Apache-2.0/MIT) rustc-hash 2.1.3 (Apache-2.0 OR MIT) rustix 0.38.44 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) rustix 1.1.5 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) +rustls 0.23.46 (Apache-2.0 OR ISC OR MIT) +rustls-native-certs 0.8.5 (Apache-2.0 OR ISC OR MIT) +rustls-pki-types 1.15.1 (MIT OR Apache-2.0) +rustls-platform-verifier 0.7.1 (MIT OR Apache-2.0) +rustls-platform-verifier-android 0.2.0 (MIT OR Apache-2.0) +rustls-webpki 0.103.15 (ISC) rustversion 1.0.23 (MIT OR Apache-2.0) +schannel 0.1.29 (MIT) scoped-tls 1.0.1 (MIT/Apache-2.0) scopeguard 1.2.0 (MIT OR Apache-2.0) +security-framework 3.7.0 (MIT OR Apache-2.0) +security-framework-sys 2.17.0 (MIT OR Apache-2.0) self_cell 1.3.0 (Apache-2.0 OR GPL-2.0-only) +semver 1.0.28 (MIT OR Apache-2.0) serde 1.0.229 (MIT OR Apache-2.0) serde_core 1.0.229 (MIT OR Apache-2.0) serde_derive 1.0.229 (MIT OR Apache-2.0) @@ -314,6 +330,7 @@ spade 2.15.1 (MIT OR Apache-2.0) spirv 0.4.0+sdk-1.4.341.0 (Apache-2.0) stable_deref_trait 1.2.1 (MIT OR Apache-2.0) static_assertions 1.1.0 (MIT OR Apache-2.0) +subtle 2.6.1 (BSD-3-Clause) syn 1.0.109 (MIT OR Apache-2.0) syn 2.0.119 (MIT OR Apache-2.0) syn 3.0.6 (MIT OR Apache-2.0) @@ -352,7 +369,11 @@ unicode-general-category 1.1.0 (Apache-2.0) unicode-ident 1.0.26 ((MIT OR Apache-2.0) AND Unicode-3.0) unicode-segmentation 1.13.3 (MIT OR Apache-2.0) unicode-width 0.2.2 (MIT OR Apache-2.0) +untrusted 0.9.0 (ISC) +ureq 3.4.2 (MIT OR Apache-2.0) +ureq-proto 0.6.4 (MIT OR Apache-2.0) url 2.5.8 (MIT OR Apache-2.0) +utf8-zero 0.8.1 (MIT OR Apache-2.0) utf8_iter 1.0.4 (Apache-2.0 OR MIT) uuid 1.27.0 (Apache-2.0 OR MIT) vello_common 0.1.0 (Apache-2.0 OR MIT) @@ -378,6 +399,7 @@ wayland-sys 0.31.11 (MIT) web-sys 0.3.106 (MIT OR Apache-2.0) web-time 1.1.0 (MIT OR Apache-2.0) webbrowser 1.2.4 (MIT OR Apache-2.0) +webpki-root-certs 1.0.9 (CDLA-Permissive-2.0) weezl 0.1.12 (MIT OR Apache-2.0) wgpu 30.0.1 (MIT OR Apache-2.0) wgpu-core 30.0.1 (MIT OR Apache-2.0) @@ -445,6 +467,7 @@ zerocopy 0.8.61 (BSD-2-Clause OR Apache-2.0 OR MIT) zerocopy-derive 0.8.61 (BSD-2-Clause OR Apache-2.0 OR MIT) zerofrom 0.1.8 (Unicode-3.0) zerofrom-derive 0.1.7 (Unicode-3.0) +zeroize 1.9.1 (Apache-2.0 OR MIT) zerotrie 0.2.5 (Unicode-3.0) zerovec 0.11.8 (Unicode-3.0) zerovec-derive 0.11.6 (Unicode-3.0) @@ -485,12 +508,12 @@ futures-lite 2.6.1, glow 0.17.0, hermit-abi 0.5.3, itoa 1.0.18, khronos-egl 6.0. ordered-stream 0.2.0, parking 2.2.1, pin-project 1.1.13, pin-project-internal 1.1.13, pin-project-lite 0.2.17, piper 0.2.5, polling 3.11.0, portable-atomic 1.15.0, portable-atomic-util 0.2.8, proc-macro-crate 3.5.0, proc-macro2 1.0.107, quote 1.0.47, rustc-hash 1.1.0, rustc-hash -2.1.3, rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, serde 1.0.229, serde_core 1.0.229, -serde_derive 1.0.229, serde_json 1.0.151, serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn -1.0.109, syn 2.0.119, syn 3.0.6, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, -thiserror-impl 2.0.21, typeid 1.0.3, unicode-ident 1.0.26, wasi 0.11.1+wasi-snapshot-preview1, -wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, zbus-lockstep 0.5.2, -zbus-lockstep-macros 0.5.2, zmij 1.0.23, zvariant_utils 4.2.0 +2.1.3, rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, serde 1.0.229, serde_core +1.0.229, serde_derive 1.0.229, serde_json 1.0.151, serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str +0.2.2, syn 1.0.109, syn 2.0.119, syn 3.0.6, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl +1.0.69, thiserror-impl 2.0.21, typeid 1.0.3, unicode-ident 1.0.26, utf8-zero 0.8.1, wasi +0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, +zbus-lockstep 0.5.2, zbus-lockstep-macros 0.5.2, zmij 1.0.23, zvariant_utils 4.2.0 ================================================================================ Permission is hereby granted, free of charge, to any @@ -756,25 +779,26 @@ DEALINGS IN THE SOFTWARE. ================================================================================ ahash 0.8.12, arbitrary 1.5.0, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-io 2.6.0, async-lock 3.4.2, async-process 2.5.0, async-recursion 1.2.0, async-signal 0.2.14, async-task -4.7.1, atomic-waker 1.1.2, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo 3.20.3, cfg-if -1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.9.4, core-foundation-sys 0.8.7, -core-graphics 0.23.2, core-graphics-types 0.1.3, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, -crossbeam-utils 0.8.23, derive_arbitrary 1.5.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, -errno 0.3.14, euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, -flate2 1.1.10, form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, glow 0.17.0, hashbrown -0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, -itertools 0.13.0, itertools 0.15.0, js-sys 0.3.106, khronos-egl 6.0.0, linux-raw-sys 0.12.1, -linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, num-complex 0.4.6, num-traits 0.2.19, once_cell -1.21.4, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, -percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, -pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, -rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, scoped-tls 1.0.1, scopeguard 1.2.0, -signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait -1.2.1, syn 1.0.109, tempfile 3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width -0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, -wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129, -wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12, -wit-bindgen 0.57.1 +4.7.1, atomic-waker 1.1.2, base64 0.23.1, bitflags 1.3.2, bitflags 2.13.2, blocking 1.7.0, bumpalo +3.20.3, cfg-if 1.0.5, cgl 0.3.2, concurrent-queue 2.5.0, core-foundation 0.10.1, core-foundation +0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, crossbeam-deque +0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils 0.8.23, derive_arbitrary 1.5.0, displaydoc 0.2.7, +either 1.19.0, equivalent 1.0.2, errno 0.3.14, euclid 0.22.14, event-listener 5.4.2, +event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10, form_urlencoded 1.2.2, futures-lite +2.6.1, gethostname 1.1.0, glow 0.17.0, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, +httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.13.0, itertools +0.15.0, js-sys 0.3.106, khronos-egl 6.0.0, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api +0.4.14, log 0.4.34, num-complex 0.4.6, num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, +ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding +2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon +1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, rustc-hash 1.1.0, rustix +0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard +1.2.0, security-framework 3.7.0, security-framework-sys 2.17.0, signal-hook-registry 1.4.8, +simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait 1.2.1, syn 1.0.109, tempfile +3.27.0, ttf-parser 0.25.1, unicode-segmentation 1.13.3, unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, +wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, +wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, +wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1 ================================================================================ Apache License @@ -982,9 +1006,9 @@ limitations under the License. ================================================================================ allocator-api2 0.2.21, async-trait 0.1.92, enumn 0.1.14, glifo 0.2.0, itoa 1.0.18, libc 0.2.190, litrs 1.0.0, num_enum 0.7.6, num_enum_derive 0.7.6, proc-macro2 1.0.107, quote 1.0.47, rustc-hash -2.1.3, rustversion 1.0.23, serde 1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_json -1.0.151, serde_repr 0.1.21, syn 2.0.119, syn 3.0.6, thiserror 1.0.69, thiserror 2.0.21, -thiserror-impl 1.0.69, thiserror-impl 2.0.21, typeid 1.0.3, unicode-ident 1.0.26 +2.1.3, rustversion 1.0.23, semver 1.0.28, serde 1.0.229, serde_core 1.0.229, serde_derive 1.0.229, +serde_json 1.0.151, serde_repr 0.1.21, syn 2.0.119, syn 3.0.6, thiserror 1.0.69, thiserror 2.0.21, +thiserror-impl 1.0.69, thiserror-impl 2.0.21, typeid 1.0.3, unicode-ident 1.0.26, utf8-zero 0.8.1 ================================================================================ Apache License @@ -1477,8 +1501,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ approx 0.4.0, as-raw-xcb-connection 1.0.1, atspi 0.29.0, atspi-common 0.13.0, atspi-proxies 0.13.0, cgmath 0.18.0, codespan-reporting 0.13.1, kurbo 0.13.1, nohash-hasher 0.2.0, peniko 0.6.1, polycool -0.4.0, raw-window-metal 1.1.0, self_cell 1.3.0, siphasher 1.0.4, spade 2.15.1, static_assertions -1.1.0, utf8_iter 1.0.4, x11rb 0.13.2, x11rb-protocol 0.13.2, zune-core 0.5.3, zune-jpeg 0.5.15 +0.4.0, raw-window-metal 1.1.0, rustls-platform-verifier 0.7.1, self_cell 1.3.0, siphasher 1.0.4, +spade 2.15.1, static_assertions 1.1.0, ureq 3.4.2, ureq-proto 0.6.4, utf8_iter 1.0.4, x11rb 0.13.2, +x11rb-protocol 0.13.2, zeroize 1.9.1, zune-core 0.5.3, zune-jpeg 0.5.15 ================================================================================ Apache License @@ -2198,6 +2223,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +base64 0.23.1 +================================================================================ + +The MIT License (MIT) + +Copyright (c) 2025 Alice Maz, Marshall Pierce + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ================================================================================ bit-set 0.10.0 ================================================================================ @@ -3016,9 +3067,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -cfg-if 1.0.5, js-sys 0.3.106, scoped-tls 1.0.1, wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, -wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys -0.3.106 +cfg-if 1.0.5, js-sys 0.3.106, openssl-probe 0.2.1, scoped-tls 1.0.1, wasm-bindgen 0.2.129, +wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support 0.2.129, +wasm-bindgen-shared 0.2.129, web-sys 0.3.106 ================================================================================ Copyright (c) 2014 Alex Crichton @@ -3048,8 +3099,8 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -cgl 0.3.2, core-foundation 0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, -core-graphics-types 0.1.3, euclid 0.22.14 +cgl 0.3.2, core-foundation 0.10.1, core-foundation 0.9.4, core-foundation-sys 0.8.7, core-graphics +0.23.2, core-graphics-types 0.1.3, euclid 0.22.14 ================================================================================ Copyright (c) 2012-2013 Mozilla Foundation @@ -6069,6 +6120,266 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FO OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +http 1.5.0 +================================================================================ + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2017 http-rs authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +================================================================================ +http 1.5.0 +================================================================================ + +Copyright (c) 2017 http-rs authors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +================================================================================ +httparse 1.10.1 +================================================================================ + +Copyright (c) 2015-2025 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + ================================================================================ iana-time-zone 0.1.65, iana-time-zone-haiku 0.1.2 ================================================================================ @@ -9377,6 +9688,314 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +ring 0.17.14 +================================================================================ + +*ring* uses an "ISC" license, like BoringSSL used to use, for new code +files. See LICENSE-other-bits for the text of that license. + +See LICENSE-BoringSSL for code that was sourced from BoringSSL under the +Apache 2.0 license. Some code that was sourced from BoringSSL under the ISC +license. In each case, the license info is at the top of the file. + +See src/polyfill/once_cell/LICENSE-APACHE and src/polyfill/once_cell/LICENSE-MIT +for the license to code that was sourced from the once_cell project. + +================================================================================ +ring 0.17.14 +================================================================================ + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +================================================================================ +ring 0.17.14 +================================================================================ + +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + ================================================================================ robust 1.2.0 ================================================================================ @@ -9472,6 +10091,368 @@ is licensed under: at your option. +================================================================================ +rustls 0.23.46, rustls-native-certs 0.8.5 +================================================================================ + +Copyright (c) 2016 Joseph Birr-Pixton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +================================================================================ +rustls 0.23.46, rustls-native-certs 0.8.5 +================================================================================ + +ISC License (ISC) +Copyright (c) 2016, Joseph Birr-Pixton + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +================================================================================ +rustls-native-certs 0.8.5 +================================================================================ + +Rustls is distributed under the following three licenses: + +- Apache License version 2.0. +- MIT license. +- ISC license. + +These are included as LICENSE-APACHE, LICENSE-MIT and LICENSE-ISC +respectively. You may use this software under the terms of any +of these licenses, at your option. + +================================================================================ +rustls-pki-types 1.15.1 +================================================================================ + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2023 Dirkjan Ochtman + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +================================================================================ +rustls-pki-types 1.15.1 +================================================================================ + +Copyright (c) 2023 Dirkjan Ochtman + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +================================================================================ +rustls-platform-verifier 0.7.1 +================================================================================ + +MIT License + +Copyright (c) 2022 1Password + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ +rustls-webpki 0.103.15 +================================================================================ + +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + +================================================================================ +schannel 0.1.29 +================================================================================ + +Copyright (c) 2015 steffengy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ================================================================================ scopeguard 1.2.0 ================================================================================ @@ -9502,6 +10483,31 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +security-framework 3.7.0, security-framework-sys 2.17.0 +================================================================================ + +The MIT License (MIT) + +Copyright (c) 2015 Steven Fackler + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ================================================================================ self_cell 1.3.0 ================================================================================ @@ -10140,6 +11146,40 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +subtle 2.6.1 +================================================================================ + +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + ================================================================================ synstructure 0.13.2 ================================================================================ @@ -11362,6 +12402,62 @@ at your option. All files in the project carrying such notice may not be copied, modified, or distributed except according to those terms. +================================================================================ +untrusted 0.9.0 +================================================================================ + +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +================================================================================ +ureq 3.4.2 +================================================================================ + +MIT License + +Copyright (c) 2019 Martin Algesten + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ +ureq-proto 0.6.4 +================================================================================ + +Copyright 2022 Martin Algesten + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ================================================================================ utf8_iter 1.0.4 ================================================================================ @@ -11985,6 +13081,72 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +webpki-root-certs 1.0.9 +================================================================================ + +# Community Data License Agreement - Permissive - Version 2.0 + +This is the Community Data License Agreement - Permissive, Version +2.0 (the "agreement"). Data Provider(s) and Data Recipient(s) agree +as follows: + +## 1. Provision of the Data + +1.1. A Data Recipient may use, modify, and share the Data made +available by Data Provider(s) under this agreement if that Data +Recipient follows the terms of this agreement. + +1.2. This agreement does not impose any restriction on a Data +Recipient's use, modification, or sharing of any portions of the +Data that are in the public domain or that may be used, modified, +or shared under any other legal exception or limitation. + +## 2. Conditions for Sharing Data + +2.1. A Data Recipient may share Data, with or without modifications, so +long as the Data Recipient makes available the text of this agreement +with the shared Data. + +## 3. No Restrictions on Results + +3.1. This agreement does not impose any restriction or obligations +with respect to the use, modification, or sharing of Results. + +## 4. No Warranty; Limitation of Liability + +4.1. All Data Recipients receive the Data subject to the following +terms: + +THE DATA IS PROVIDED ON AN "AS IS" BASIS, WITHOUT REPRESENTATIONS, +WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +NO DATA PROVIDER SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE DATA OR RESULTS, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +## 5. Definitions + +5.1. "Data" means the material received by a Data Recipient under +this agreement. + +5.2. "Data Provider" means any person who is the source of Data +provided under this agreement and in reliance on a Data Recipient's +agreement to its terms. + +5.3. "Data Recipient" means any person who receives Data directly +or indirectly from a Data Provider and agrees to the terms of this +agreement. + +5.4. "Results" means any outcome obtained by computational analysis +of Data, including for example machine learning models and models' +insights. + ================================================================================ weezl 0.1.12 ================================================================================ @@ -12962,6 +14124,36 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +================================================================================ +zeroize 1.9.1 +================================================================================ + +Copyright (c) 2018-2026 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + ================================================================================ zip 3.0.0 ================================================================================ @@ -13336,6 +14528,7 @@ objc2-user-notifications 0.2.2: MIT (https://github.com/madsmtm/objc2) profiling 1.0.18: MIT OR Apache-2.0 (https://github.com/aclysma/profiling) r-efi 5.3.0: MIT OR Apache-2.0 OR LGPL-2.1-or-later (https://github.com/r-efi/r-efi) r-efi 6.0.0: MIT OR Apache-2.0 OR LGPL-2.1-or-later (https://github.com/r-efi/r-efi) +rustls-platform-verifier-android 0.2.0: MIT OR Apache-2.0 (https://github.com/rustls/rustls-platform-verifier) spirv 0.4.0+sdk-1.4.341.0: Apache-2.0 (https://github.com/gfx-rs/rspirv) truck-base 0.5.0: Apache-2.0 (https://github.com/ricosjp/truck) truck-derivers 0.1.0: Apache-2.0 (https://github.com/ricosjp/truck) diff --git a/apps/solidcraft/Cargo.toml b/apps/solidcraft/Cargo.toml index 229cbc83..ff332a5a 100644 --- a/apps/solidcraft/Cargo.toml +++ b/apps/solidcraft/Cargo.toml @@ -28,6 +28,9 @@ serde_json = { workspace = true } log = { workspace = true } rfd = { workspace = true } getrandom = "0.3" +# The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system. +ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] } +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] } [target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] quick-xml = { workspace = true } diff --git a/apps/solidcraft/src/main.rs b/apps/solidcraft/src/main.rs index 0ff292dc..8b925c08 100644 --- a/apps/solidcraft/src/main.rs +++ b/apps/solidcraft/src/main.rs @@ -113,6 +113,7 @@ fn services() -> Services { dialog.save_file().map(|p| p.to_string_lossy().to_string()) })), graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()), + http_get: Some(std::sync::Arc::new(http_get)), show_in_folder: Some(Box::new(|path| { let folder = std::path::Path::new(path).parent().unwrap_or_else(|| std::path::Path::new(".")); let mut command = std::process::Command::new(if cfg!(target_os = "windows") { @@ -133,6 +134,45 @@ fn services() -> Services { } } +/// HTTPS GET for the update check (docs/updates.md): certificates from the operating system, a +/// 15 s limit, at most 1 MB, and no headers beyond what GitHub requires (a User-Agent without +/// the version or anything about this machine, and the API's media type). +fn http_get(url: &str) -> Result, solidcraft_ui_egui::update::FetchError> { + use solidcraft_ui_egui::update::FetchError; + use std::time::Duration; + let tls = ureq::tls::TlsConfig::builder() + .root_certs(ureq::tls::RootCerts::PlatformVerifier) + .unversioned_rustls_crypto_provider(std::sync::Arc::new(rustls::crypto::ring::default_provider())) + .build(); + let agent: ureq::Agent = ureq::Agent::config_builder() + .tls_config(tls) + .https_only(true) + .http_status_as_error(false) + .timeout_global(Some(Duration::from_secs(15))) + .timeout_connect(Some(Duration::from_secs(5))) + .user_agent("SolidCraft-update-check") + .max_redirects(3) + .build() + .into(); + let mut response = agent + .get(url) + .header("Accept", "application/vnd.github+json") + .header("X-GitHub-Api-Version", "2022-11-28") + .call() + .map_err(|e| FetchError::Failed(e.to_string()))?; + let status = response.status().as_u16(); + let header = |k: &str| response.headers().get(k).and_then(|v| v.to_str().ok()).and_then(|v| v.trim().parse::().ok()); + if status == 429 || (status == 403 && (header("x-ratelimit-remaining") == Some(0) || header("retry-after").is_some())) { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + let retry_after_s = header("retry-after").or_else(|| header("x-ratelimit-reset").map(|reset| reset.saturating_sub(now))).unwrap_or(3600); + return Err(FetchError::RateLimited { retry_after_s }); + } + if status != 200 { + return Err(FetchError::Failed(format!("GitHub answered HTTP {status}"))); + } + response.body_mut().with_config().limit(1 << 20).read_to_vec().map_err(|e| FetchError::Failed(e.to_string())) +} + /// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference. fn settings_dir() -> Option { eframe::storage_dir("SolidCraft") diff --git a/assets/icons/ui/source/icons.rs b/assets/icons/ui/source/icons.rs index 83cfbde4..25df0e9d 100644 --- a/assets/icons/ui/source/icons.rs +++ b/assets/icons/ui/source/icons.rs @@ -266,6 +266,12 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce pen.line(&[(17.0, y), (21.0, y)], ink); } } + // Update: an arrow down onto a tray (a newer release can be downloaded). + "update" => { + pen.line(&[(12.0, 3.5), (12.0, 14.5)], ink); + pen.line(&[(7.5, 10.0), (12.0, 14.5), (16.5, 10.0)], ink); + pen.line(&[(4.0, 15.0), (4.0, 20.0), (20.0, 20.0), (20.0, 15.0)], ink); + } // Help: a question mark in a ring. "help" => { pen.circle(12.0, 12.0, 9.0, Color32::TRANSPARENT, ink); diff --git a/assets/locales/de.json b/assets/locales/de.json index d5db4939..d741a6ff 100644 --- a/assets/locales/de.json +++ b/assets/locales/de.json @@ -701,5 +701,29 @@ "sketch_text.align_right": "Rechts", "sketch_text.path_hint": "Eine Skizzenkurve, der der Text folgt: hier klicken, dann die Kurve", "sketch_text.path_clear": "Text von der Kurve lösen", - "sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage" + "sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage", + "update.button": "Update", + "update.button_tip": "SolidCraft {version} ist verfügbar", + "update.title": "Update verfügbar", + "update.available": "SolidCraft {version} ist verfügbar", + "update.you_have": "Installiert ist {version}.", + "update.no_notes": "Zu diesem Release gibt es keine Hinweise.", + "update.system_hint": "Diese Installation ist ein {kind}. Aktualisieren mit:", + "update.package_hint": "Diese Installation stammt aus einem {kind}. Neues Paket herunterladen und installieren mit:", + "update.copy": "Kopieren", + "update.file": "Herunterladen holt {file}.", + "update.download": "Herunterladen", + "update.release_page": "Release-Seite öffnen", + "update.skip": "Diese Version überspringen", + "update.later": "Später erinnern", + "update.prefs_label": "Updates", + "update.check_automatically": "Automatisch nach Updates suchen", + "update.check_now": "Jetzt suchen", + "update.checking": "Suche läuft…", + "update.failed": "Suche fehlgeschlagen: {error}", + "update.up_to_date": "SolidCraft ist aktuell.", + "update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.", + "update.prefs_dev": "Dies ist ein Entwicklungs-Build ({version}): er sucht nicht nach Updates.", + "update.show": "Update auf {version} anzeigen", + "update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst." } diff --git a/assets/locales/en.json b/assets/locales/en.json index e328e9f9..64374f71 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -959,6 +959,30 @@ "Line Spacing": "Line Spacing", "Path": "Path", "sketch_text.on_path": "The text follows its path: the curve places it", - "cmd.material_properties_validate":"Validate Physical Material Data", - "cmd.material_properties_directional":"Directional Elastic Modulus" + "cmd.material_properties_validate": "Validate Physical Material Data", + "cmd.material_properties_directional": "Directional Elastic Modulus", + "update.button": "Update", + "update.button_tip": "SolidCraft {version} is available", + "update.title": "Update available", + "update.available": "SolidCraft {version} is available", + "update.you_have": "You have {version}.", + "update.no_notes": "This release has no notes.", + "update.system_hint": "This copy is a {kind}. Update it with:", + "update.package_hint": "This copy came from a {kind}. Download the new package, then install it with:", + "update.copy": "Copy", + "update.file": "Download gets {file}.", + "update.download": "Download", + "update.release_page": "Open release page", + "update.skip": "Skip this version", + "update.later": "Remind me later", + "update.prefs_label": "Updates", + "update.check_automatically": "Check for updates automatically", + "update.check_now": "Check now", + "update.checking": "Checking…", + "update.failed": "Couldn't check: {error}", + "update.up_to_date": "SolidCraft is up to date.", + "update.prefs_note": "This is SolidCraft {version} ({kind}). A check asks GitHub for the latest release and sends nothing else.", + "update.prefs_dev": "This is a development build ({version}): it doesn't check for updates.", + "update.show": "Show the update to {version}", + "update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software center. SolidCraft doesn't check." } diff --git a/assets/locales/ja.json b/assets/locales/ja.json index aa3011b0..fcb6d4b9 100644 --- a/assets/locales/ja.json +++ b/assets/locales/ja.json @@ -959,6 +959,30 @@ "Line Spacing": "行間", "Path": "パス", "sketch_text.on_path": "文字はパスに沿います: 位置は曲線で決まります", - "cmd.material_properties_validate":"物理材料データを検証", - "cmd.material_properties_directional":"方向別弾性率" + "cmd.material_properties_validate": "物理材料データを検証", + "cmd.material_properties_directional": "方向別弾性率", + "update.button": "アップデート", + "update.button_tip": "SolidCraft {version} が利用できます", + "update.title": "アップデートがあります", + "update.available": "SolidCraft {version} が利用できます", + "update.you_have": "現在のバージョンは {version} です。", + "update.no_notes": "このリリースにはリリースノートがありません。", + "update.system_hint": "このコピーは {kind} です。次のコマンドで更新します:", + "update.package_hint": "このコピーは {kind} からインストールされました。新しいパッケージをダウンロードし、次のコマンドでインストールします:", + "update.copy": "コピー", + "update.file": "ダウンロードするファイル: {file}", + "update.download": "ダウンロード", + "update.release_page": "リリースページを開く", + "update.skip": "このバージョンをスキップ", + "update.later": "後で通知", + "update.prefs_label": "アップデート", + "update.check_automatically": "アップデートを自動的に確認", + "update.check_now": "今すぐ確認", + "update.checking": "確認中…", + "update.failed": "確認できませんでした: {error}", + "update.up_to_date": "SolidCraft は最新です。", + "update.prefs_note": "これは SolidCraft {version} ({kind}) です。確認では GitHub に最新リリースを問い合わせるだけで、ほかには何も送信しません。", + "update.prefs_dev": "これは開発ビルド ({version}) のため、アップデートを確認しません。", + "update.show": "{version} へのアップデートを表示", + "update.prefs_flatpak": "このコピー ({version}) は Flatpak が更新します: {command} を実行するか、ソフトウェアセンターを使ってください。SolidCraft 自身は確認しません。" } diff --git a/assets/locales/ko.json b/assets/locales/ko.json index 05231f34..50087f0a 100644 --- a/assets/locales/ko.json +++ b/assets/locales/ko.json @@ -701,5 +701,29 @@ "sketch_text.align_right": "오른쪽", "sketch_text.path_hint": "텍스트가 따라갈 스케치 곡선: 여기를 클릭한 다음 곡선을 클릭", "sketch_text.path_clear": "곡선에서 텍스트 떼기", - "sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다" + "sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다", + "update.button": "업데이트", + "update.button_tip": "SolidCraft {version} 사용 가능", + "update.title": "업데이트 사용 가능", + "update.available": "SolidCraft {version} 사용 가능", + "update.you_have": "현재 버전은 {version}입니다.", + "update.no_notes": "이 릴리스에는 릴리스 노트가 없습니다.", + "update.system_hint": "이 사본은 {kind}입니다. 다음 명령으로 업데이트하세요:", + "update.package_hint": "이 사본은 {kind}(으)로 설치되었습니다. 새 패키지를 내려받은 뒤 다음 명령으로 설치하세요:", + "update.copy": "복사", + "update.file": "내려받을 파일: {file}", + "update.download": "다운로드", + "update.release_page": "릴리스 페이지 열기", + "update.skip": "이 버전 건너뛰기", + "update.later": "나중에 알림", + "update.prefs_label": "업데이트", + "update.check_automatically": "업데이트 자동 확인", + "update.check_now": "지금 확인", + "update.checking": "확인 중…", + "update.failed": "확인할 수 없습니다: {error}", + "update.up_to_date": "SolidCraft가 최신 버전입니다.", + "update.prefs_note": "SolidCraft {version} ({kind})입니다. 확인 시 GitHub에 최신 릴리스만 요청하며 다른 정보는 보내지 않습니다.", + "update.prefs_dev": "개발 빌드({version})이므로 업데이트를 확인하지 않습니다.", + "update.show": "{version} 업데이트 보기", + "update.prefs_flatpak": "이 사본({version})은 Flatpak이 업데이트합니다: {command}를 실행하거나 소프트웨어 센터를 사용하세요. SolidCraft는 직접 확인하지 않습니다." } diff --git a/assets/locales/zh.json b/assets/locales/zh.json index 48517771..42dc0bf9 100644 --- a/assets/locales/zh.json +++ b/assets/locales/zh.json @@ -959,6 +959,30 @@ "sketch_text.on_path": "文字沿路径排列:由曲线放置", "ui.analysis": "分析", "ui.edit_section_analysis": "编辑截面分析", - "cmd.material_properties_validate":"验证物理材料数据", - "cmd.material_properties_directional":"方向弹性模量" + "cmd.material_properties_validate": "验证物理材料数据", + "cmd.material_properties_directional": "方向弹性模量", + "update.button": "更新", + "update.button_tip": "SolidCraft {version} 已发布", + "update.title": "有可用更新", + "update.available": "SolidCraft {version} 已发布", + "update.you_have": "当前版本为 {version}。", + "update.no_notes": "此版本没有发布说明。", + "update.system_hint": "此副本为 {kind}。用以下命令更新:", + "update.package_hint": "此副本来自 {kind}。下载新软件包后用以下命令安装:", + "update.copy": "复制", + "update.file": "将下载 {file}。", + "update.download": "下载", + "update.release_page": "打开发布页面", + "update.skip": "跳过此版本", + "update.later": "稍后提醒", + "update.prefs_label": "更新", + "update.check_automatically": "自动检查更新", + "update.check_now": "立即检查", + "update.checking": "正在检查…", + "update.failed": "无法检查: {error}", + "update.up_to_date": "SolidCraft 已是最新版本。", + "update.prefs_note": "当前为 SolidCraft {version}({kind})。检查时只向 GitHub 查询最新版本,不发送其他任何数据。", + "update.prefs_dev": "这是开发版本({version}),不检查更新。", + "update.show": "显示 {version} 更新", + "update.prefs_flatpak": "此副本({version})由 Flatpak 更新:运行 {command} 或使用软件中心。SolidCraft 不会自行检查。" } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 8a2f84e3..f0da304f 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -19,6 +19,7 @@ log = { workspace = true } web-time = { workspace = true } image = { workspace = true, features = ["jpeg"] } ttf-parser = { workspace = true } +semver = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] # The offscreen viewport render test opens a device without a window. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c54662b7..1a9aa3ce 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -131,6 +131,7 @@ pub mod titlebar; pub mod toolbar; pub mod toolbar_custom; pub mod tools; +pub mod update; pub mod viewport; mod viewport_layout; pub mod workspace; @@ -212,6 +213,8 @@ pub struct Services { pub show_in_folder: Option>, /// Where the host reads the anti-aliasing preference at start (a small JSON file). pub graphics_path: Option, + /// HTTPS GET for the update check (`update`); None: never check (browser, tests). + pub http_get: Option, } pub struct SolveApp { @@ -248,6 +251,8 @@ pub struct SolveApp { pub issue_report: issue_report::ReportState, /// The Crash Reports window. pub crashes: crash_ui::CrashUi, + /// The update check and its window (`update`). + pub updater: update::Updater, /// Toolbar buttons the user pinned, removed or reordered. pub toolbar_custom: toolbar_custom::ToolbarCustom, pub sketch_choices: sketch_families::Choices, @@ -328,6 +333,7 @@ impl SolveApp { help: help::HelpState::default(), issue_report: issue_report::ReportState::default(), crashes: crash_ui::CrashUi::default(), + updater: update::Updater::default(), toolbar_custom: toolbar_custom::ToolbarCustom::default(), sketch_choices: sketch_families::Choices::default(), preferences: prefs::Prefs::default(), @@ -1029,6 +1035,7 @@ impl SolveApp { toolbar::shortcuts(self, ctx); } recovery_ui::tick(self); + update::tick(self, ctx); if self.issue_report.draft.is_none() { for f in ctx.input(|i| i.raw.dropped_files.clone()) { let p = f.path().to_string_lossy().to_string(); @@ -1092,6 +1099,7 @@ impl SolveApp { issue_report::CadInput::block(self, ui, report_input); file_load::show(self, ui.ctx()); documents::prompt(self, ui.ctx()); + update::show(self, ui.ctx()); if self.home.open { home::show(self, ui); if self.home.open { diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 1bb1f250..ffe8ddb6 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -245,6 +245,10 @@ pub struct Prefs { pub offer_stand_up: bool, /// What a middle-button double-click in the view does: `home`, `fit` or `off`. pub middle_double_click: String, + /// Check for updates at start and once a day (docs/updates.md). + pub check_updates: bool, + /// When the last update check ran, and the versions skipped or put off (`update::Memo`). + pub update: crate::update::Memo, } /// What a middle-button double-click in the view does. @@ -303,6 +307,8 @@ impl Default for Prefs { up_axis: "z".into(), offer_stand_up: true, middle_double_click: "home".into(), + check_updates: true, + update: crate::update::Memo::default(), } } } @@ -359,7 +365,10 @@ impl Snapshot { } fn restore(self, app: &mut SolveApp) { let msaa = app.preferences.msaa; + // Update bookkeeping isn't a setting: a check that finished while the dialog was open stays. + let update = std::mem::take(&mut app.preferences.update); app.preferences = self.prefs; + app.preferences.update = update; app.ui.dark = self.dark; app.ui.show_grid = self.grid; app.ui.ground_shadow = self.shadow; @@ -392,6 +401,7 @@ fn defaults(app: &mut SolveApp, page: usize) { p.default_units = d.default_units; p.length_decimals = d.length_decimals; p.angle_decimals = d.angle_decimals; + p.check_updates = d.check_updates; app.ui.dark = true; app.ui.agent_cursor = Default::default(); app.autosave_minutes = 5.0; @@ -901,6 +911,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { ui.label(""); ui.add_enabled(s.show, egui::Checkbox::new(&mut s.follow_camera, crate::i18n::tr_label("Follow the camera (Look At sketches)"))); ui.end_row(); + crate::update::prefs_rows(app, ui, t); } 1 => { ui.label(app.tr_label("Pan, zoom, orbit")); diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 36561fea..f48251a1 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -250,7 +250,9 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { let captions = if app.custom_titlebar { crate::titlebar::WIDTH } else { 0.0 }; let tabs_start = x + 16.0; let (report, report_label) = report_rect(r, captions, tabs_start); - let tabs_end = report.left() - 8.0; + // A newer release: its Update button, left of Report a bug (`update`). + let update = crate::update::button(app, ui, report.left() - 6.0, report.top(), report_label); + let tabs_end = update.map_or(report.left(), |u| u.left()) - 8.0; // Narrow title bars can fit only part of the tab strip. Its hit regions must not // extend into the persistent report and window controls. let tabs_rect = Rect::from_min_max(pos2(tabs_start.min(tabs_end), r.top()), pos2(tabs_end, r.bottom())); @@ -296,7 +298,7 @@ pub fn app_bar(app: &mut SolveApp, ui: &mut egui::Ui) { ui.painter().text( pos2(r.right() - 12.0 - captions, r.center().y), Align2::RIGHT_CENTER, - format!("v{}", env!("CARGO_PKG_VERSION")), + format!("v{}", crate::update::current_version()), FontId::proportional(11.0), Color32::from_rgb(160, 168, 182), ); diff --git a/crates/ui-egui/src/update.rs b/crates/ui-egui/src/update.rs new file mode 100644 index 00000000..121d507f --- /dev/null +++ b/crates/ui-egui/src/update.rs @@ -0,0 +1,959 @@ +//! Update notifications (docs/updates.md). At start, and at most once a day after that, a +//! background thread asks GitHub for the latest stable release (`releases/latest`; nightlies and +//! release candidates are never "latest"). When it is newer than this build, an Update button +//! appears in the application bar beside Report a bug; it opens a small window with the version, +//! the release notes and Download, Skip this version and Remind me later. +//! +//! The request is the only thing sent: no identifiers, no usage data. A short timeout keeps it +//! from hanging, a failed check (offline, rate limited) stays silent and is retried later, and +//! development builds never check. Turning off Preferences ▸ General ▸ Check for updates +//! automatically stops the automatic checks; Check now still works. +//! +//! The host provides the HTTP client (`Services::http_get`), so this crate stays portable and +//! the browser build never checks (it updates itself through its service worker). + +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::mpsc::{Receiver, TryRecvError}; + +use egui::{Color32, FontId, RichText, text::LayoutJob}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::SolveApp; +use crate::theme::Tokens; + +/// The repository releases come from. +pub const REPO: &str = "bherbruck/solidcraft"; +/// The one request an update check makes. +pub const LATEST_API: &str = "https://api.github.com/repos/bherbruck/solidcraft/releases/latest"; +/// Where Download goes when no package fits this installation. +pub const RELEASES_PAGE: &str = "https://github.com/bherbruck/solidcraft/releases/latest"; +/// Release pages and assets must be on GitHub, under this repository. +const RELEASE_PREFIX: &str = "https://github.com/bherbruck/solidcraft/releases/"; + +/// Automatic checks: at most one a day. +pub const CHECK_INTERVAL_S: f64 = 24.0 * 3600.0; +/// Remind me later hides the button for three days. +pub const REMIND_DELAY_S: f64 = 3.0 * 24.0 * 3600.0; +/// Check now can't be pressed more often than this (GitHub allows 60 anonymous requests an hour). +pub const MANUAL_COOLDOWN_S: f64 = 30.0; +/// After a failed automatic check, try again after this long (an hour), not every start. +pub const RETRY_AFTER_FAILURE_S: f64 = 3600.0; +/// Caps on what a release response may hold (it comes from the network). +const MAX_NOTES: usize = 64 * 1024; +const MAX_ASSETS: usize = 400; +/// The Update window's width, and its release notes' wrap width. +const WINDOW_W: f32 = 460.0; +const NOTES_W: f32 = WINDOW_W - 30.0; + +/// The Linux app ID (Flatpak, desktop files). +pub const APP_ID: &str = "ai.storyteller.solidcraft"; + +/// Why a check failed. Automatic checks never show it; Check now does. +#[derive(Clone, Debug, PartialEq)] +pub enum FetchError { + /// GitHub answered 403/429 with a rate limit: don't ask again for this many seconds. + RateLimited { retry_after_s: u64 }, + /// Offline, timed out, a bad answer, ... + Failed(String), +} + +impl std::fmt::Display for FetchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FetchError::RateLimited { retry_after_s } => { + write!(f, "GitHub's rate limit was reached; try again in {} min", retry_after_s.div_ceil(60)) + } + FetchError::Failed(e) => f.write_str(e), + } + } +} + +/// The host's HTTP GET (HTTPS, short timeout): the body of a 200 answer, or why not. +pub type HttpGet = Arc Result, FetchError> + Send + Sync>; + +// ---- versions --------------------------------------------------------------------------------- + +/// A version as SolidCraft's tags and builds spell it: `0.10.0`, `v0.11.0-rc.1`, +/// `0.9.0-nightly.20261014.36.1`. +pub fn parse_version(v: &str) -> Option { + let v = v.trim(); + semver::Version::parse(v.strip_prefix('v').unwrap_or(v)).ok() +} + +/// What a build is compared as. A nightly is built from `main` after the release it is +/// numbered from (`0.9.0-nightly.…` contains everything in 0.9.0 and more), so it counts as that +/// release, not as something before it: it is offered only a strictly newer stable release. +pub fn comparable(v: &semver::Version) -> semver::Version { + let mut v = v.clone(); + if v.pre.as_str().starts_with("nightly") { + v.pre = semver::Prerelease::EMPTY; + } + v.build = semver::BuildMetadata::EMPTY; + v +} + +/// `latest` (a release tag) is a stable release newer than the `current` build. +pub fn is_newer(latest: &str, current: &str) -> bool { + let (Some(latest), Some(current)) = (parse_version(latest), parse_version(current)) else { return false }; + latest.pre.is_empty() && latest > comparable(¤t) +} + +/// This build's version: the release workflow's `SOLIDCRAFT_VERSION` (with any nightly suffix) +/// when it built this binary, else the crate version. +pub fn current_version() -> &'static str { + option_env!("SOLIDCRAFT_VERSION").filter(|v| !v.is_empty()).unwrap_or(env!("CARGO_PKG_VERSION")) +} + +/// The release workflow built this binary (`SOLIDCRAFT_VERSION` is set there). Anything else is a +/// development build, which never checks for updates. +pub fn release_build() -> bool { + option_env!("SOLIDCRAFT_VERSION").is_some_and(|v| !v.is_empty()) && !cfg!(debug_assertions) +} + +// ---- installations ---------------------------------------------------------------------------- + +/// How this copy of SolidCraft was installed: decides which file Download fetches and whether +/// the system's package manager is the way to update. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum InstallKind { + /// Built from source (or by something other than the release workflow): never checks. + Dev, + /// The browser app (updates itself). + Web, + /// An AppImage, at this path. + AppImage(PathBuf), + /// A Flatpak, with this app ID. It has no network access and Flatpak updates it: never checks. + Flatpak(String), + /// A Snap: from the Snap Store (`store`), or a downloaded `.snap` installed with `--dangerous`. + Snap { + store: bool, + }, + Deb, + Rpm, + /// The plain `.tar.gz`, unpacked somewhere. + LinuxTarball, + /// Installed by the `.msi` (under Program Files). + WindowsMsi, + /// The portable `.zip`, unpacked somewhere. + WindowsPortable, + /// The lone `SolidCraft--windows-x64.exe`. + WindowsExe, + /// `SolidCraft.app`, at this path (the bundle). + MacApp(PathBuf), + /// A release build in a place none of the above explains (FreeBSD, a copied binary). + Other, +} + +/// What [`detect`] looks at, so tests can describe any machine. +pub struct Probe<'a> { + /// `std::env::consts::OS`. + pub os: &'a str, + pub release_build: bool, + pub env: &'a dyn Fn(&str) -> Option, + /// The running executable. + pub exe: Option, + pub exists: &'a dyn Fn(&Path) -> bool, +} + +pub fn detect(p: &Probe) -> InstallKind { + if p.os == "unknown" || cfg!(target_arch = "wasm32") { + return InstallKind::Web; + } + if !p.release_build { + return InstallKind::Dev; + } + let env = |k: &str| (p.env)(k).filter(|v| !v.trim().is_empty()); + let Some(exe) = p.exe.clone() else { return InstallKind::Other }; + match p.os { + "linux" => { + if let Some(id) = env("FLATPAK_ID") { + return InstallKind::Flatpak(id); + } + if env("SNAP").is_some() && env("SNAP_NAME").is_none_or(|n| n.starts_with("solidcraft")) { + // Locally installed (unasserted) snaps have revisions x1, x2, … + return InstallKind::Snap { store: !env("SNAP_REVISION").is_some_and(|r| r.starts_with('x')) }; + } + if let Some(image) = env("APPIMAGE") { + return InstallKind::AppImage(PathBuf::from(image)); + } + if exe.starts_with("/usr") { + // nfpm installs both packages under /usr; dpkg keeps a file list per package. + if (p.exists)(Path::new("/var/lib/dpkg/info/solidcraft.list")) { + return InstallKind::Deb; + } + if (p.exists)(Path::new("/var/lib/rpm")) || (p.exists)(Path::new("/usr/lib/sysimage/rpm")) { + return InstallKind::Rpm; + } + return InstallKind::Other; + } + InstallKind::LinuxTarball + } + "windows" => { + let under = |k: &str| env(k).is_some_and(|dir| starts_with_ci(&exe, Path::new(&dir))); + if under("ProgramFiles") || under("ProgramW6432") || under("ProgramFiles(x86)") { + return InstallKind::WindowsMsi; + } + if exe.parent().is_some_and(|d| (p.exists)(&d.join("solidcraft-cli.exe"))) { + return InstallKind::WindowsPortable; + } + InstallKind::WindowsExe + } + "macos" => app_bundle(&exe).map_or(InstallKind::Other, InstallKind::MacApp), + _ => InstallKind::Other, + } +} + +/// `path` is inside `dir` (Windows paths: case-insensitive). +fn starts_with_ci(path: &Path, dir: &Path) -> bool { + let (p, d) = (path.to_string_lossy().to_lowercase().replace('/', "\\"), dir.to_string_lossy().to_lowercase().replace('/', "\\")); + let d = d.trim_end_matches('\\'); + !d.is_empty() && p.strip_prefix(d).is_some_and(|rest| rest.starts_with('\\')) +} + +/// The `.app` bundle an executable at `…/X.app/Contents/MacOS/x` belongs to. +fn app_bundle(exe: &Path) -> Option { + let macos = exe.parent()?; + let contents = macos.parent()?; + let bundle = contents.parent()?; + (macos.file_name()? == "MacOS" && contents.file_name()? == "Contents" && bundle.extension()? == "app").then(|| bundle.to_path_buf()) +} + +/// This machine's installation, looked up once. +pub fn this_install() -> InstallKind { + #[cfg(target_arch = "wasm32")] + { + InstallKind::Web + } + #[cfg(not(target_arch = "wasm32"))] + { + let env = |k: &str| std::env::var(k).ok(); + let exists = |p: &Path| p.exists(); + detect(&Probe { os: std::env::consts::OS, release_build: release_build(), env: &env, exe: std::env::current_exe().ok(), exists: &exists }) + } +} + +impl InstallKind { + /// The package manager updates it: Download isn't the way (the window shows the command). + pub fn system_managed(&self) -> bool { + matches!(self, InstallKind::Flatpak(_) | InstallKind::Snap { store: true }) + } + + /// A short name for Preferences and docs. + pub fn label(&self) -> &'static str { + match self { + InstallKind::Dev => "development build", + InstallKind::Web => "browser app", + InstallKind::AppImage(_) => "AppImage", + InstallKind::Flatpak(_) => "Flatpak", + InstallKind::Snap { store: true } => "Snap", + InstallKind::Snap { store: false } => "Snap (installed from a file)", + InstallKind::Deb => ".deb package", + InstallKind::Rpm => ".rpm package", + InstallKind::LinuxTarball => ".tar.gz", + InstallKind::WindowsMsi => "Windows installer (.msi)", + InstallKind::WindowsPortable => "portable .zip", + InstallKind::WindowsExe => "standalone .exe", + InstallKind::MacApp(_) => "macOS app", + InstallKind::Other => "other", + } + } + + /// Release builds check; development builds, the browser app and the Flatpak (no network + /// permission; Flatpak updates it) don't. + pub fn checks(&self) -> bool { + !matches!(self, InstallKind::Dev | InstallKind::Web | InstallKind::Flatpak(_)) + } + + /// The command that updates a package-managed installation, if there is one. + pub fn update_command(&self, asset: Option<&str>) -> Option { + match self { + InstallKind::Flatpak(id) => Some(format!("flatpak update {id}")), + InstallKind::Snap { store: true } => Some("sudo snap refresh solidcraft".into()), + InstallKind::Snap { store: false } => { + Some(format!("sudo snap install --dangerous ./{}", asset.unwrap_or("solidcraft-linux-x86_64.snap"))) + } + InstallKind::Deb => Some(format!("sudo apt install ./{}", asset.unwrap_or("solidcraft-linux-x86_64.deb"))), + InstallKind::Rpm => Some(format!("sudo dnf install ./{}", asset.unwrap_or("solidcraft-linux-x86_64.rpm"))), + _ => None, + } + } +} + +/// The release asset that installs or replaces this installation, by its fixed name +/// (docs/releasing.md: `solidcraft-windows-x64.msi`, `solidcraft-linux-x86_64.AppImage`, …), or +/// the versioned name for the packages without one. `arch` is `std::env::consts::ARCH`. +pub fn asset_name(kind: &InstallKind, arch: &str, version: &str) -> Option { + let linux = match arch { + "x86_64" | "aarch64" => Some(arch), + _ => None, + }; + let windows = match arch { + "x86_64" => Some("x64"), + "aarch64" => Some("arm64"), + "x86" => Some("x86"), + _ => None, + }; + let version = version.trim_start_matches('v'); + Some(match kind { + InstallKind::AppImage(_) => format!("solidcraft-linux-{}.AppImage", linux?), + InstallKind::Flatpak(_) => format!("solidcraft-linux-{}.flatpak", linux?), + InstallKind::Snap { .. } => format!("solidcraft-linux-{}.snap", linux?), + InstallKind::Deb => format!("solidcraft-linux-{}.deb", linux?), + InstallKind::Rpm => format!("solidcraft-linux-{}.rpm", linux?), + InstallKind::LinuxTarball => format!("solidcraft-{version}-linux-{}.tar.gz", linux?), + InstallKind::WindowsMsi => format!("solidcraft-windows-{}.msi", windows?), + InstallKind::WindowsPortable => format!("solidcraft-windows-{}-portable.zip", windows?), + // Only x64 has the lone .exe; elsewhere the portable zip is the same thing. + InstallKind::WindowsExe if windows == Some("x64") => format!("SolidCraft-{version}-windows-x64.exe"), + InstallKind::WindowsExe => format!("solidcraft-windows-{}-portable.zip", windows?), + InstallKind::MacApp(_) => "solidcraft-macos-universal.dmg".into(), + InstallKind::Dev | InstallKind::Web | InstallKind::Other => return None, + }) +} + +// ---- releases --------------------------------------------------------------------------------- + +#[derive(Clone, Debug, PartialEq)] +pub struct Asset { + pub name: String, + pub url: String, + pub size: u64, +} + +/// The parts of a GitHub release an update needs. +#[derive(Clone, Debug, PartialEq)] +pub struct Release { + pub tag: String, + pub name: String, + /// Release notes (Markdown). + pub notes: String, + /// The release's page. + pub page: String, + pub assets: Vec, +} + +impl Release { + /// A `releases/latest` answer. Drafts and prereleases are refused (GitHub never returns them + /// there, but the answer comes from the network). + pub fn parse(json: &[u8]) -> Result { + let v: Value = serde_json::from_slice(json).map_err(|e| FetchError::Failed(format!("unexpected answer from GitHub: {e}")))?; + let s = |k: &str| v.get(k).and_then(Value::as_str).unwrap_or_default().to_string(); + if v.get("draft").and_then(Value::as_bool).unwrap_or(false) || v.get("prerelease").and_then(Value::as_bool).unwrap_or(false) { + return Err(FetchError::Failed("the latest release is a prerelease".into())); + } + let tag = s("tag_name"); + if parse_version(&tag).is_none() { + return Err(FetchError::Failed(format!("the latest release's tag `{}` isn't a version", tag.chars().take(40).collect::()))); + } + let mut notes = s("body"); + if notes.len() > MAX_NOTES { + let mut cut = MAX_NOTES; + while !notes.is_char_boundary(cut) { + cut -= 1; + } + notes.truncate(cut); + notes.push_str("\n\n…"); + } + let page = Some(s("html_url")).filter(|u| u.starts_with(RELEASE_PREFIX)).unwrap_or_else(|| RELEASES_PAGE.to_string()); + let assets = v + .get("assets") + .and_then(Value::as_array) + .into_iter() + .flatten() + .take(MAX_ASSETS) + .filter_map(|a| { + let name = a.get("name")?.as_str()?.to_string(); + let url = a.get("browser_download_url")?.as_str()?.to_string(); + url.starts_with(RELEASE_PREFIX).then(|| Asset { name, url, size: a.get("size").and_then(Value::as_u64).unwrap_or(0) }) + }) + .collect(); + Ok(Release { + name: Some(s("name")).filter(|n| !n.trim().is_empty()).unwrap_or_else(|| format!("SolidCraft {tag}")), + tag, + notes, + page, + assets, + }) + } + + /// The version without the `v`. + pub fn version(&self) -> &str { + self.tag.trim_start_matches('v') + } + + /// The asset that fits `kind` on `arch`, if this release has it. + pub fn asset_for(&self, kind: &InstallKind, arch: &str) -> Option<&Asset> { + let name = asset_name(kind, arch, self.version())?; + self.assets.iter().find(|a| a.name == name) + } + + /// What Download opens: the fitting file, else the release page. Package-managed + /// installations get the page (their package manager does the update). + pub fn download_url(&self, kind: &InstallKind, arch: &str) -> &str { + if kind.system_managed() { + return &self.page; + } + self.asset_for(kind, arch).map_or(self.page.as_str(), |a| a.url.as_str()) + } +} + +// ---- what is remembered ----------------------------------------------------------------------- + +/// Update bookkeeping, kept with the preferences (not design data). Times are Unix seconds. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(default)] +pub struct Memo { + /// When the last check finished (successfully or not). + pub last_check: f64, + /// The next automatic check: a day after a successful one, an hour after a failed one. + pub next_check: f64, + /// GitHub's rate limit was reached: no request at all before this, not even Check now. + pub rate_limited_until: f64, + /// Skip this version: its button never shows (a later version's does). + pub skipped: String, + /// Remind me later: no button before this time. + pub remind_after: f64, +} + +impl Memo { + /// An automatic check is due (a clock set back by more than a day counts as due). + pub fn due(&self, now: f64) -> bool { + now >= self.rate_limited_until && (now >= self.next_check || self.next_check - now > CHECK_INTERVAL_S) + } + + /// The Update button shows for `release`. + pub fn offers(&self, release: &Release, current: &str, now: f64) -> bool { + is_newer(&release.tag, current) && !same_version(&self.skipped, &release.tag) && now >= self.remind_after + } + + pub fn skip(&mut self, release: &Release) { + self.skipped = release.version().to_string(); + } + + pub fn remind_later(&mut self, now: f64) { + self.remind_after = now + REMIND_DELAY_S; + } + + /// A check finished: the next one is a day later after a success, an hour after a failure, + /// and after a rate limit as long as GitHub said (at least a minute, at most a day). + pub fn checked(&mut self, now: f64, result: &Result) { + self.last_check = now; + self.next_check = now + + match result { + Ok(_) => CHECK_INTERVAL_S, + Err(FetchError::Failed(_)) => RETRY_AFTER_FAILURE_S, + Err(FetchError::RateLimited { retry_after_s }) => (*retry_after_s as f64).clamp(60.0, CHECK_INTERVAL_S), + }; + if let Err(FetchError::RateLimited { .. }) = result { + self.rate_limited_until = self.next_check; + } + } +} + +fn same_version(a: &str, b: &str) -> bool { + !a.is_empty() && a.trim_start_matches('v') == b.trim_start_matches('v') +} + +/// Now, in Unix seconds (works in the browser too). +pub fn unix_now() -> f64 { + web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map_or(0.0, |d| d.as_secs_f64()) +} + +// ---- the running state ------------------------------------------------------------------------ + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum Status { + #[default] + Idle, + Checking, + UpToDate, + Failed(String), +} + +#[derive(Default)] +pub struct Updater { + /// The installation, looked up at the first frame. + pub kind: Option, + pub status: Status, + /// The newest release found (whether or not it is offered). + pub release: Option, + /// The Update window is open. + pub open: bool, + /// The check in flight, and whether Check now started it. + rx: Option<(Receiver>, bool)>, + started: bool, + /// When Check now was last pressed (Unix seconds). + last_manual: f64, + /// A release given by `SOLIDCRAFT_UPDATE_FEED` (a file), for tests and screenshots. + feed: Option, +} + +impl Updater { + /// The installation (looked up on first use). + pub fn kind(&mut self) -> &InstallKind { + self.kind.get_or_insert_with(this_install) + } +} + +/// The Update button shows. +pub fn offered(app: &SolveApp) -> Option<&Release> { + let r = app.updater.release.as_ref()?; + app.preferences.update.offers(r, current_version(), unix_now()).then_some(r) +} + +/// Checks may run here: a release build with the host's HTTP client, or a test feed. +pub fn available(app: &mut SolveApp) -> bool { + app.updater.feed.is_some() || (app.services.http_get.is_some() && app.updater.kind().checks()) +} + +/// Per frame: the first frame starts a due automatic check; a finished check is collected. +pub fn tick(app: &mut SolveApp, ctx: &egui::Context) { + if !app.updater.started { + app.updater.started = true; + #[cfg(not(target_arch = "wasm32"))] + { + if std::env::var("SOLIDCRAFT_UPDATE_CHECK").is_ok_and(|v| v == "0") { + return; + } + app.updater.feed = std::env::var_os("SOLIDCRAFT_UPDATE_FEED").map(PathBuf::from); + } + } + poll(app); + let now = unix_now(); + if app.updater.rx.is_none() && app.preferences.check_updates && app.preferences.update.due(now) && available(app) { + start(app, ctx, false); + } +} + +/// Check now (Preferences): ignores the daily interval, not GitHub's rate limit. +pub fn check_now(app: &mut SolveApp, ctx: &egui::Context) { + let now = unix_now(); + if app.updater.rx.is_some() || now - app.updater.last_manual < MANUAL_COOLDOWN_S || !available(app) { + return; + } + if now < app.preferences.update.rate_limited_until { + let wait = (app.preferences.update.rate_limited_until - now).max(0.0) as u64; + app.updater.status = Status::Failed(FetchError::RateLimited { retry_after_s: wait }.to_string()); + return; + } + app.updater.last_manual = now; + start(app, ctx, true); +} + +fn start(app: &mut SolveApp, ctx: &egui::Context, manual: bool) { + let (tx, rx) = std::sync::mpsc::channel(); + app.updater.status = Status::Checking; + app.updater.rx = Some((rx, manual)); + let feed = app.updater.feed.clone(); + let http = app.services.http_get.clone(); + let fetch = move || -> Result { + let body = match (&feed, &http) { + (Some(path), _) => std::fs::read(path).map_err(|e| FetchError::Failed(format!("{}: {e}", path.display())))?, + (None, Some(get)) => get(LATEST_API)?, + (None, None) => return Err(FetchError::Failed("no network client".into())), + }; + Release::parse(&body) + }; + #[cfg(not(target_arch = "wasm32"))] + { + let ctx = ctx.clone(); + let spawned = std::thread::Builder::new().name("update-check".into()).spawn(move || { + let _ = tx.send(fetch()); + ctx.request_repaint(); + }); + if spawned.is_err() { + app.updater.rx = None; + app.updater.status = Status::Idle; + } + } + #[cfg(target_arch = "wasm32")] + { + let _ = (ctx, tx, fetch); + app.updater.rx = None; + app.updater.status = Status::Idle; + } +} + +fn poll(app: &mut SolveApp) { + let Some((rx, manual)) = &app.updater.rx else { return }; + let manual = *manual; + let result = match rx.try_recv() { + Ok(r) => r, + Err(TryRecvError::Empty) => return, + Err(TryRecvError::Disconnected) => Err(FetchError::Failed("the check stopped".into())), + }; + app.updater.rx = None; + app.preferences.update.checked(unix_now(), &result); + match result { + Ok(release) => { + let newer = is_newer(&release.tag, current_version()); + app.updater.status = if newer { Status::Idle } else { Status::UpToDate }; + if newer && manual { + // Asked for: show it even if it was skipped or put off. + app.preferences.update.skipped.clear(); + app.preferences.update.remind_after = 0.0; + app.updater.open = true; + } + app.updater.release = Some(release); + } + Err(e) => { + log::info!("update check: {e}"); + app.updater.status = if manual { Status::Failed(e.to_string()) } else { Status::Idle }; + } + } +} + +// ---- user interface --------------------------------------------------------------------------- + +/// The application bar's Update button, right of `right`; returns its rect (None: not shown). +pub fn button(app: &mut SolveApp, ui: &mut egui::Ui, right: f32, top: f32, labeled: bool) -> Option { + offered(app)?; + let t = Tokens::get(); + let text = crate::i18n::tr("update.button"); + let width = if labeled { + 30.0 + ui.fonts_mut(|f| f.layout_no_wrap(text.to_string(), FontId::proportional(13.0), Color32::WHITE).size().x) + 10.0 + } else { + 28.0 + }; + let rect = egui::Rect::from_min_size(egui::pos2(right - width, top), egui::vec2(width, 26.0)); + let response = ui.interact(rect, ui.id().with("sc_update_button"), egui::Sense::click()); + response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), text)); + crate::scenario::publish_handle("titlebar:update", rect.center()); + let fill = if response.hovered() || response.has_focus() { t.hover } else { t.app_bar }; + ui.painter().rect_filled(rect, 4.0, fill); + ui.painter().rect_stroke(rect, 4.0, egui::Stroke::new(1.0, t.accent), egui::StrokeKind::Inside); + let icon = egui::Rect::from_center_size(egui::pos2(rect.left() + 14.0, rect.center().y), egui::vec2(18.0, 18.0)); + crate::icons::paint(ui.painter(), icon, "update", t.accent, t.accent, t.app_bar); + if labeled { + ui.painter().text( + egui::pos2(rect.left() + 29.0, rect.center().y), + egui::Align2::LEFT_CENTER, + text, + FontId::proportional(13.0), + t.app_bar_text, + ); + } + let version = app.updater.release.as_ref().map(|r| r.version().to_string()).unwrap_or_default(); + if response.on_hover_text(crate::i18n::format("update.button_tip", &[("version", &version)])).clicked() { + app.updater.open = !app.updater.open; + } + Some(rect) +} + +/// The Update window. +pub fn show(app: &mut SolveApp, ctx: &egui::Context) { + if !app.updater.open { + return; + } + let Some(release) = app.updater.release.clone() else { + app.updater.open = false; + return; + }; + let kind = app.updater.kind().clone(); + let arch = std::env::consts::ARCH; + let t = Tokens::get(); + let mut open = true; + let mut action = None; + let shown = crate::frame::window(ctx, crate::i18n::tr("update.title"), crate::frame::Width::Wide) + .id(egui::Id::new("sc_update")) + .open(&mut open) + .resizable(false) + .show(ctx, |ui| { + ui.set_width(WINDOW_W); + ui.set_max_width(WINDOW_W); + ui.label(RichText::new(crate::i18n::format("update.available", &[("version", release.version())])).strong().size(15.0)); + ui.label(RichText::new(crate::i18n::format("update.you_have", &[("version", current_version())])).color(t.text_dim)); + ui.add_space(6.0); + egui::Frame::group(ui.style()).show(ui, |ui| { + ui.set_width(ui.available_width()); + ui.set_max_width(NOTES_W + 12.0); + egui::ScrollArea::vertical().max_height(260.0).auto_shrink([false, true]).show(ui, |ui| { + if release.notes.trim().is_empty() { + ui.label(RichText::new(crate::i18n::tr("update.no_notes")).color(t.text_dim)); + } else { + markdown(ui, &release.notes, &t); + } + }); + }); + ui.add_space(6.0); + let asset = release.asset_for(&kind, arch).map(|a| a.name.clone()); + if let Some(cmd) = kind.update_command(asset.as_deref()) { + let how = match kind { + InstallKind::Deb | InstallKind::Rpm | InstallKind::Snap { store: false } => "update.package_hint", + _ => "update.system_hint", + }; + ui.add(egui::Label::new(crate::i18n::format(how, &[("kind", kind.label())])).wrap()); + ui.horizontal(|ui| { + ui.add(egui::Label::new(RichText::new(&cmd).monospace()).selectable(true)); + if ui.small_button(crate::i18n::tr("update.copy")).clicked() { + ui.ctx().copy_text(cmd.clone()); + } + }); + ui.add_space(4.0); + } else if let Some(name) = &asset { + ui.label(RichText::new(crate::i18n::format("update.file", &[("file", name)])).size(11.0).color(t.text_dim)); + } + ui.horizontal(|ui| { + let download = if kind.system_managed() { crate::i18n::tr("update.release_page") } else { crate::i18n::tr("update.download") }; + let r = ui.add(egui::Button::new(RichText::new(download).strong()).fill(t.accent)); + crate::scenario::publish_rect("update:download", r.rect); + if r.clicked() { + action = Some("download"); + } + if ui.button(crate::i18n::tr("update.skip")).clicked() { + action = Some("skip"); + } + if ui.button(crate::i18n::tr("update.later")).clicked() { + action = Some("later"); + } + }); + }); + if let Some(w) = &shown { + crate::scenario::publish_rect("update:window", w.response.rect); + } + let now = unix_now(); + match action { + Some("download") => { + let url = release.download_url(&kind, arch).to_string(); + ctx.open_url(egui::OpenUrl::new_tab(url)); + app.updater.open = false; + } + Some("skip") => { + app.preferences.update.skip(&release); + app.updater.open = false; + } + Some("later") => { + app.preferences.update.remind_later(now); + app.updater.open = false; + } + _ if !open => app.updater.open = false, + _ => {} + } +} + +/// Preferences ▸ General: the automatic check and Check now. +pub fn prefs_rows(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) { + let can = available(app); + let kind = app.updater.kind().clone(); + ui.label(crate::i18n::tr("update.prefs_label")); + ui.add_enabled(can, egui::Checkbox::new(&mut app.preferences.check_updates, crate::i18n::tr("update.check_automatically"))); + ui.end_row(); + ui.label(""); + ui.horizontal(|ui| { + let busy = app.updater.status == Status::Checking; + let r = ui.add_enabled(can && !busy, egui::Button::new(crate::i18n::tr("update.check_now"))); + crate::scenario::publish_rect("prefs:check_now", r.rect); + if r.clicked() { + let ctx = ui.ctx().clone(); + check_now(app, &ctx); + } + let status = match (&app.updater.status, offered_or_newer(app)) { + _ if !can => String::new(), + (Status::Checking, _) => crate::i18n::tr("update.checking").to_string(), + (Status::Failed(e), _) => crate::i18n::format("update.failed", &[("error", e)]), + (_, Some(v)) => crate::i18n::format("update.available", &[("version", &v)]), + (Status::UpToDate, None) => crate::i18n::tr("update.up_to_date").to_string(), + (Status::Idle, None) => String::new(), + }; + if !status.is_empty() { + ui.label(RichText::new(status).color(t.text_dim)); + } + }); + ui.end_row(); + ui.label(""); + let note = if can { + crate::i18n::format("update.prefs_note", &[("version", current_version()), ("kind", kind.label())]) + } else if let InstallKind::Flatpak(id) = &kind { + crate::i18n::format("update.prefs_flatpak", &[("version", current_version()), ("command", &format!("flatpak update {id}"))]) + } else { + crate::i18n::format("update.prefs_dev", &[("version", current_version())]) + }; + ui.add(egui::Label::new(RichText::new(note).size(11.0).color(t.text_dim)).wrap()); + ui.end_row(); + if let Some(v) = offered_or_newer(app) + && offered(app).is_none() + && ui.link(crate::i18n::format("update.show", &[("version", &v)])).clicked() + { + app.updater.open = true; + } +} + +/// The newer version found, if any (offered or skipped). +fn offered_or_newer(app: &SolveApp) -> Option { + app.updater.release.as_ref().filter(|r| is_newer(&r.tag, current_version())).map(|r| r.version().to_string()) +} + +// ---- release notes ---------------------------------------------------------------------------- + +/// A span of inline Markdown. +#[derive(Clone, Debug, PartialEq)] +pub enum Span { + Text(String), + Strong(String), + Code(String), + /// A link's text (the target is dropped: the release page has them). + Link(String), +} + +/// A block of the release notes. +#[derive(Clone, Debug, PartialEq)] +pub enum Block { + Heading(Vec), + Paragraph(Vec), + /// A list item, at a nesting depth. + Item(usize, Vec), +} + +/// The Markdown GitHub release notes use, enough to read them: headings, paragraphs (soft line +/// breaks joined), `-`/`*` lists, `**strong**`, `` `code` ``, `[links](…)` and bare pull-request +/// URLs (shown as `#123`). Anything else shows as written. +pub fn parse_markdown(src: &str) -> Vec { + let mut blocks = Vec::new(); + let mut para = String::new(); + let mut item: Option<(usize, String)> = None; + let flush = |blocks: &mut Vec, para: &mut String, item: &mut Option<(usize, String)>| { + if !para.trim().is_empty() { + blocks.push(Block::Paragraph(inline(para.trim()))); + } + para.clear(); + if let Some((depth, text)) = item.take() { + blocks.push(Block::Item(depth, inline(text.trim()))); + } + }; + for line in src.lines() { + let trimmed = line.trim_start(); + let indent = line.len() - trimmed.len(); + if trimmed.is_empty() { + flush(&mut blocks, &mut para, &mut item); + } else if let Some(h) = trimmed.strip_prefix('#') { + flush(&mut blocks, &mut para, &mut item); + blocks.push(Block::Heading(inline(h.trim_start_matches('#').trim()))); + } else if let Some(rest) = trimmed.strip_prefix("- ").or_else(|| trimmed.strip_prefix("* ")).or_else(|| trimmed.strip_prefix("+ ")) { + flush(&mut blocks, &mut para, &mut item); + item = Some((indent / 2, rest.to_string())); + } else if let Some((_, text)) = &mut item { + text.push(' '); + text.push_str(trimmed); + } else { + if !para.is_empty() { + para.push(' '); + } + para.push_str(trimmed); + } + } + flush(&mut blocks, &mut para, &mut item); + blocks +} + +/// Inline spans of one block's text. +pub fn inline(text: &str) -> Vec { + let mut out: Vec = Vec::new(); + let mut plain = String::new(); + let mut rest = text; + let push_plain = |out: &mut Vec, plain: &mut String| { + if !plain.is_empty() { + out.push(Span::Text(std::mem::take(plain))); + } + }; + while let Some(c) = rest.chars().next() { + if let Some(after) = rest.strip_prefix("**") + && let Some(end) = after.find("**") + && end > 0 + { + push_plain(&mut out, &mut plain); + let inner = after.get(..end).unwrap_or_default(); + out.push(Span::Strong(strip_links(inner))); + rest = after.get(end + 2..).unwrap_or_default(); + } else if let Some(after) = rest.strip_prefix('`') + && let Some(end) = after.find('`') + { + push_plain(&mut out, &mut plain); + out.push(Span::Code(after.get(..end).unwrap_or_default().to_string())); + rest = after.get(end + 1..).unwrap_or_default(); + } else if let Some(after) = rest.strip_prefix('[') + && let Some(close) = after.find("](") + && let Some(end) = after.get(close + 2..).and_then(|s| s.find(')')) + { + push_plain(&mut out, &mut plain); + out.push(Span::Link(strip_links(after.get(..close).unwrap_or_default()))); + rest = after.get(close + 2 + end + 1..).unwrap_or_default(); + } else if rest.starts_with("https://github.com/") { + let end = rest.find(char::is_whitespace).unwrap_or(rest.len()); + let url = rest.get(..end).unwrap_or_default(); + let tail = url.trim_end_matches(['.', ',', ')', ';']); + let number = tail.rsplit('/').next().filter(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit())); + let kind = tail.rsplit('/').nth(1); + match (number, kind) { + (Some(n), Some("pull" | "issues")) => { + push_plain(&mut out, &mut plain); + out.push(Span::Link(format!("#{n}"))); + rest = rest.get(tail.len()..).unwrap_or_default(); + } + _ => { + plain.push_str(url); + rest = rest.get(end..).unwrap_or_default(); + } + } + } else { + plain.push(c); + rest = rest.get(c.len_utf8()..).unwrap_or_default(); + } + } + push_plain(&mut out, &mut plain); + out +} + +/// Link text inside bold: `**[x](y)**` → `x`. +fn strip_links(s: &str) -> String { + inline(s) + .into_iter() + .map(|span| match span { + Span::Text(t) | Span::Strong(t) | Span::Code(t) | Span::Link(t) => t, + }) + .collect() +} + +fn markdown(ui: &mut egui::Ui, src: &str, t: &Tokens) { + let body = ui.visuals().text_color(); + let job = |spans: &[Span], size: f32, strong: bool, wrap: f32| { + let mut job = LayoutJob::default(); + job.wrap.max_width = wrap; + for span in spans { + let (text, font, color) = match span { + Span::Text(s) => (s.as_str(), FontId::proportional(size), if strong { t.text } else { body }), + Span::Strong(s) => (s.as_str(), FontId::proportional(size), t.text), + Span::Code(s) => (s.as_str(), FontId::monospace(size - 1.0), body), + Span::Link(s) => (s.as_str(), FontId::proportional(size), t.accent), + }; + job.append(text, 0.0, egui::TextFormat { font_id: font, color, ..Default::default() }); + } + job + }; + // Fixed widths: the notes never widen the window, whatever their lines. + let width = NOTES_W; + for block in parse_markdown(src) { + match block { + Block::Heading(spans) => { + ui.add_space(4.0); + ui.add(egui::Label::new(job(&spans, 14.0, true, width)).wrap()); + } + Block::Paragraph(spans) => { + ui.add(egui::Label::new(job(&spans, 12.5, false, width)).wrap()); + ui.add_space(3.0); + } + Block::Item(depth, spans) => { + let indent = 6.0 + 12.0 * depth.min(4) as f32; + ui.horizontal_top(|ui| { + ui.add_space(indent); + ui.label(RichText::new("•").size(12.5)); + ui.add(egui::Label::new(job(&spans, 12.5, false, width - indent - 14.0)).wrap()); + }); + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/update/tests.rs b/crates/ui-egui/src/update/tests.rs new file mode 100644 index 00000000..94eb05f3 --- /dev/null +++ b/crates/ui-egui/src/update/tests.rs @@ -0,0 +1,281 @@ +use super::*; +use serde_json::json; +use std::collections::BTreeMap; + +#[test] +fn versions_compare_as_semver() { + assert!(is_newer("v0.10.0", "0.9.0")); + assert!(is_newer("v0.10.0", "0.9.12"), "0.10 is after 0.9.12, not before it as text would sort"); + assert!(is_newer("v1.0.0", "0.10.0")); + assert!(!is_newer("v0.10.0", "0.10.0")); + assert!(!is_newer("v0.9.0", "0.10.0")); + // A release candidate is older than its release; the release is offered to it. + assert!(is_newer("v0.11.0", "0.11.0-rc.2")); + // A prerelease is never offered (releases/latest never returns one anyway). + assert!(!is_newer("v0.12.0-rc.1", "0.11.0")); + assert!(!is_newer("garbage", "0.9.0")); + assert!(!is_newer("v0.10.0", "not a version")); +} + +#[test] +fn nightlies_count_as_the_release_they_follow() { + // Built from main after 0.10.0: not nagged about 0.10.0 itself. + assert!(!is_newer("v0.10.0", "0.10.0-nightly.20261015.37.1")); + assert!(!is_newer("v0.9.0", "0.10.0-nightly.20261015.37.1")); + // A newer stable release is offered. + assert!(is_newer("v0.11.0", "0.10.0-nightly.20261015.37.1")); + assert!(is_newer("v0.10.0", "0.9.0-nightly.20261014.36.1")); +} + +fn probe(os: &str, release: bool, env: &[(&str, &str)], exe: &str, files: &[&str]) -> InstallKind { + let env: BTreeMap = env.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(); + let files: Vec = files.iter().map(PathBuf::from).collect(); + let get = |k: &str| env.get(k).cloned(); + let exists = |p: &Path| files.iter().any(|f| f == p); + detect(&Probe { os, release_build: release, env: &get, exe: Some(PathBuf::from(exe)), exists: &exists }) +} + +#[test] +fn install_kinds_are_told_apart() { + let home = "/home/me/Apps/solidcraft"; + assert_eq!(probe("linux", false, &[("APPIMAGE", "/x.AppImage")], home, &[]), InstallKind::Dev, "dev builds never check"); + assert_eq!(probe("linux", true, &[("FLATPAK_ID", APP_ID)], "/app/bin/solidcraft", &[]), InstallKind::Flatpak(APP_ID.into())); + let snap = |rev: &str| { + probe( + "linux", + true, + &[("SNAP", "/snap/solidcraft/12"), ("SNAP_NAME", "solidcraft"), ("SNAP_REVISION", rev)], + "/snap/solidcraft/12/usr/bin/solidcraft", + &[], + ) + }; + assert_eq!(snap("12"), InstallKind::Snap { store: true }); + assert_eq!(snap("x3"), InstallKind::Snap { store: false }, "installed from a file with --dangerous"); + assert_eq!( + probe("linux", true, &[("APPIMAGE", "/home/me/SolidCraft.AppImage")], "/tmp/.mount_SolidXYZ/usr/bin/solidcraft", &[]), + InstallKind::AppImage("/home/me/SolidCraft.AppImage".into()) + ); + // An empty variable doesn't count. + assert_eq!(probe("linux", true, &[("APPIMAGE", "")], "/opt/sc/bin/solidcraft", &[]), InstallKind::LinuxTarball); + assert_eq!(probe("linux", true, &[], "/usr/bin/solidcraft", &["/var/lib/dpkg/info/solidcraft.list", "/var/lib/rpm"]), InstallKind::Deb); + assert_eq!(probe("linux", true, &[], "/usr/bin/solidcraft", &["/var/lib/rpm"]), InstallKind::Rpm); + assert_eq!(probe("linux", true, &[], "/usr/bin/solidcraft", &["/usr/lib/sysimage/rpm"]), InstallKind::Rpm); + assert_eq!(probe("linux", true, &[], "/usr/bin/solidcraft", &[]), InstallKind::Other); + assert_eq!(probe("linux", true, &[], home, &[]), InstallKind::LinuxTarball); + + let pf = [("ProgramFiles", "C:\\Program Files"), ("ProgramFiles(x86)", "C:\\Program Files (x86)")]; + assert_eq!(probe("windows", true, &pf, "C:\\Program Files\\SolidCraft\\solidcraft.exe", &[]), InstallKind::WindowsMsi); + assert_eq!(probe("windows", true, &pf, "c:\\program files\\SolidCraft\\solidcraft.exe", &[]), InstallKind::WindowsMsi, "case-insensitive"); + // "C:\Program Files Extra" is not under "C:\Program Files". + assert_eq!(probe("windows", true, &pf, "C:\\Program Files Extra\\solidcraft.exe", &[]), InstallKind::WindowsExe); + let portable = "C:\\Users\\me\\solidcraft-0.10.0-windows-x64-portable\\solidcraft.exe"; + let cli = "C:\\Users\\me\\solidcraft-0.10.0-windows-x64-portable\\solidcraft-cli.exe"; + // Path::parent splits on `\` only on Windows; this test describes the layout either way. + if cfg!(windows) { + assert_eq!(probe("windows", true, &pf, portable, &[cli]), InstallKind::WindowsPortable); + } + assert_eq!(probe("windows", true, &pf, "C:\\Users\\me\\Downloads\\SolidCraft-0.10.0-windows-x64.exe", &[]), InstallKind::WindowsExe); + + assert_eq!( + probe("macos", true, &[], "/Applications/SolidCraft.app/Contents/MacOS/solidcraft", &[]), + InstallKind::MacApp("/Applications/SolidCraft.app".into()) + ); + assert_eq!(probe("macos", true, &[], "/usr/local/bin/solidcraft", &[]), InstallKind::Other); + assert_eq!(probe("macos", false, &[], "/Applications/SolidCraft.app/Contents/MacOS/solidcraft", &[]), InstallKind::Dev); + assert_eq!(probe("freebsd", true, &[], "/usr/local/bin/solidcraft", &[]), InstallKind::Other); + assert!(!InstallKind::Dev.checks() && !InstallKind::Web.checks() && InstallKind::Snap { store: true }.checks()); + assert!(!InstallKind::Flatpak(APP_ID.into()).checks(), "the Flatpak has no network permission; Flatpak updates it"); +} + +#[test] +fn portable_windows_layout_is_recognised() { + let exe = PathBuf::from("/portable/solidcraft.exe"); + let get = |k: &str| (k == "ProgramFiles").then(|| "C:\\Program Files".to_string()); + let exists = |p: &Path| p == Path::new("/portable/solidcraft-cli.exe"); + assert_eq!(detect(&Probe { os: "windows", release_build: true, env: &get, exe: Some(exe), exists: &exists }), InstallKind::WindowsPortable); +} + +/// The assets of v0.10.0, as published. +fn release() -> Release { + let names = [ + "SHA256SUMS.txt", + "solidcraft-0.10.0-linux-x86_64.AppImage", + "solidcraft-0.10.0-linux-x86_64.AppImage.zsync", + "solidcraft-0.10.0-linux-x86_64.tar.gz", + "solidcraft-0.10.0-linux-aarch64.tar.gz", + "SolidCraft-0.10.0-windows-x64.exe", + "solidcraft-linux-aarch64.AppImage", + "solidcraft-linux-aarch64.deb", + "solidcraft-linux-x86_64.AppImage", + "solidcraft-linux-x86_64.deb", + "solidcraft-linux-x86_64.flatpak", + "solidcraft-linux-x86_64.rpm", + "solidcraft-linux-x86_64.snap", + "solidcraft-macos-universal.dmg", + "solidcraft-windows-arm64-portable.zip", + "solidcraft-windows-arm64.msi", + "solidcraft-windows-x64-portable.zip", + "solidcraft-windows-x64.msi", + "solidcraft-windows-x86-portable.zip", + "solidcraft-windows-x86.msi", + ]; + let assets: Vec = names + .iter() + .map(|n| json!({"name": n, "size": 10, "browser_download_url": format!("https://github.com/bherbruck/solidcraft/releases/download/v0.10.0/{n}")})) + .collect(); + let body = json!({ + "tag_name": "v0.10.0", "name": "SolidCraft v0.10.0", "body": "**What's new**\n\n- Surface tools.", + "html_url": "https://github.com/bherbruck/solidcraft/releases/tag/v0.10.0", "draft": false, "prerelease": false, "assets": assets, + }); + Release::parse(body.to_string().as_bytes()).unwrap() +} + +#[test] +fn each_installation_gets_its_own_file() { + let r = release(); + let name = |k: InstallKind, arch: &str| r.asset_for(&k, arch).map(|a| a.name.clone()); + assert_eq!(name(InstallKind::AppImage("/a".into()), "x86_64").as_deref(), Some("solidcraft-linux-x86_64.AppImage")); + assert_eq!(name(InstallKind::AppImage("/a".into()), "aarch64").as_deref(), Some("solidcraft-linux-aarch64.AppImage")); + assert_eq!(name(InstallKind::Deb, "x86_64").as_deref(), Some("solidcraft-linux-x86_64.deb")); + assert_eq!(name(InstallKind::Rpm, "x86_64").as_deref(), Some("solidcraft-linux-x86_64.rpm")); + assert_eq!(name(InstallKind::LinuxTarball, "aarch64").as_deref(), Some("solidcraft-0.10.0-linux-aarch64.tar.gz")); + assert_eq!(name(InstallKind::WindowsMsi, "x86_64").as_deref(), Some("solidcraft-windows-x64.msi")); + assert_eq!(name(InstallKind::WindowsMsi, "aarch64").as_deref(), Some("solidcraft-windows-arm64.msi")); + assert_eq!(name(InstallKind::WindowsMsi, "x86").as_deref(), Some("solidcraft-windows-x86.msi")); + assert_eq!(name(InstallKind::WindowsPortable, "x86_64").as_deref(), Some("solidcraft-windows-x64-portable.zip")); + assert_eq!(name(InstallKind::WindowsExe, "x86_64").as_deref(), Some("SolidCraft-0.10.0-windows-x64.exe")); + assert_eq!(name(InstallKind::WindowsExe, "aarch64").as_deref(), Some("solidcraft-windows-arm64-portable.zip")); + assert_eq!(name(InstallKind::MacApp("/A.app".into()), "aarch64").as_deref(), Some("solidcraft-macos-universal.dmg")); + assert_eq!(name(InstallKind::Other, "x86_64"), None); + assert_eq!(name(InstallKind::Deb, "riscv64"), None); + // Download: the file, or the page when there is none or a package manager updates it. + assert_eq!( + r.download_url(&InstallKind::WindowsMsi, "x86_64"), + "https://github.com/bherbruck/solidcraft/releases/download/v0.10.0/solidcraft-windows-x64.msi" + ); + assert_eq!(r.download_url(&InstallKind::Other, "x86_64"), "https://github.com/bherbruck/solidcraft/releases/tag/v0.10.0"); + assert_eq!(r.download_url(&InstallKind::Flatpak(APP_ID.into()), "x86_64"), r.page); + assert_eq!(r.download_url(&InstallKind::Snap { store: true }, "x86_64"), r.page); + assert_eq!( + r.download_url(&InstallKind::Snap { store: false }, "x86_64"), + "https://github.com/bherbruck/solidcraft/releases/download/v0.10.0/solidcraft-linux-x86_64.snap" + ); + assert_eq!( + InstallKind::Snap { store: false }.update_command(Some("solidcraft-linux-x86_64.snap")).as_deref(), + Some("sudo snap install --dangerous ./solidcraft-linux-x86_64.snap") + ); + assert_eq!(InstallKind::Flatpak(APP_ID.into()).update_command(None).as_deref(), Some("flatpak update ai.storyteller.solidcraft")); + assert_eq!( + InstallKind::Deb.update_command(Some("solidcraft-linux-x86_64.deb")).as_deref(), + Some("sudo apt install ./solidcraft-linux-x86_64.deb") + ); + assert_eq!(InstallKind::WindowsMsi.update_command(None), None); +} + +#[test] +fn release_answers_are_checked() { + let r = release(); + assert_eq!((r.tag.as_str(), r.version(), r.assets.len()), ("v0.10.0", "0.10.0", 20)); + let bad = |v: Value| Release::parse(v.to_string().as_bytes()).is_err(); + assert!(bad(json!({"tag_name": "v0.11.0-rc.1", "prerelease": true}))); + assert!(bad(json!({"tag_name": "v0.11.0", "draft": true}))); + assert!(bad(json!({"tag_name": "nightly"}))); + assert!(Release::parse(b"").is_err()); + // Links elsewhere are dropped: the page falls back to the releases page, foreign assets go. + let r = Release::parse( + json!({"tag_name": "v1.0.0", "html_url": "https://evil.example/x", "assets": [ + {"name": "solidcraft-windows-x64.msi", "browser_download_url": "https://evil.example/a.msi"}, + {"name": "solidcraft-windows-x86.msi", "browser_download_url": "https://github.com/bherbruck/solidcraft/releases/download/v1.0.0/solidcraft-windows-x86.msi"}, + ]}) + .to_string() + .as_bytes(), + ) + .unwrap(); + assert_eq!(r.page, RELEASES_PAGE); + assert_eq!(r.assets.len(), 1); + assert_eq!(r.name, "SolidCraft v1.0.0"); + // Huge notes are cut (on a character boundary). + let r = Release::parse(json!({"tag_name": "v1.0.0", "body": "é".repeat(MAX_NOTES)}).to_string().as_bytes()).unwrap(); + assert!(r.notes.len() <= MAX_NOTES + 8 && r.notes.ends_with('…')); +} + +#[test] +fn skip_and_remind_later() { + let r = release(); + let mut m = Memo::default(); + let now = 1_800_000_000.0; + assert!(m.offers(&r, "0.9.0", now)); + assert!(!m.offers(&r, "0.10.0", now)); + m.remind_later(now); + assert!(!m.offers(&r, "0.9.0", now + 3600.0)); + assert!(m.offers(&r, "0.9.0", now + REMIND_DELAY_S + 1.0)); + m.skip(&r); + assert!(!m.offers(&r, "0.9.0", now + REMIND_DELAY_S + 1.0), "a skipped version stays hidden"); + let mut next = r.clone(); + next.tag = "v0.11.0".into(); + assert!(m.offers(&next, "0.9.0", now + REMIND_DELAY_S + 1.0), "a later version shows again"); +} + +#[test] +fn checks_run_daily_and_back_off() { + let mut m = Memo::default(); + let now = 1_800_000_000.0; + assert!(m.due(now), "the first start checks"); + m.checked(now, &Ok(release())); + assert!(!m.due(now + 3600.0)); + assert!(m.due(now + CHECK_INTERVAL_S)); + assert!(m.due(now - 2.0 * CHECK_INTERVAL_S), "a clock set back doesn't block checks for days"); + m.checked(now, &Err(FetchError::Failed("offline".into()))); + assert!(!m.due(now + 60.0)); + assert!(m.due(now + RETRY_AFTER_FAILURE_S)); + m.checked(now, &Err(FetchError::RateLimited { retry_after_s: 1800 })); + assert!(!m.due(now + 1700.0)); + assert!(m.due(now + 1800.0)); + assert_eq!(m.rate_limited_until, now + 1800.0); + // Absurd waits are capped at a day. + m.checked(now, &Err(FetchError::RateLimited { retry_after_s: u64::MAX })); + assert!(m.due(now + CHECK_INTERVAL_S)); +} + +#[test] +fn the_memo_survives_with_the_preferences() { + let mut p = crate::prefs::Prefs::default(); + assert!(p.check_updates); + p.update.skipped = "0.10.0".into(); + p.update.remind_after = 5.0; + let back: crate::prefs::Prefs = serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap(); + assert_eq!(back, p); + // Older preferences without these fields load with the defaults. + let old: crate::prefs::Prefs = serde_json::from_str(r#"{"theme":"light"}"#).unwrap(); + assert!(old.check_updates && old.update == Memo::default()); +} + +#[test] +fn release_notes_read_as_markdown() { + let md = "## What's new\n\n**Why 0.10:** a minor\nbump.\n\n- **Surface tools.** Revolve and `solidcraft-cli mcp`\n continued ([#925](https://x/925)).\n - nested\n* Fix by @a in https://github.com/bherbruck/solidcraft/pull/1064\n\nSee https://example.com/x."; + let b = parse_markdown(md); + assert_eq!(b.first(), Some(&Block::Heading(vec![Span::Text("What's new".into())]))); + assert_eq!(b.get(1), Some(&Block::Paragraph(vec![Span::Strong("Why 0.10:".into()), Span::Text(" a minor bump.".into())]))); + assert_eq!( + b.get(2), + Some(&Block::Item( + 0, + vec![ + Span::Strong("Surface tools.".into()), + Span::Text(" Revolve and ".into()), + Span::Code("solidcraft-cli mcp".into()), + Span::Text(" continued (".into()), + Span::Link("#925".into()), + Span::Text(").".into()), + ] + )) + ); + assert_eq!(b.get(3), Some(&Block::Item(1, vec![Span::Text("nested".into())]))); + assert_eq!(b.get(4), Some(&Block::Item(0, vec![Span::Text("Fix by @a in ".into()), Span::Link("#1064".into())]))); + assert_eq!(b.get(5), Some(&Block::Paragraph(vec![Span::Text("See https://example.com/x.".into())]))); + // Unclosed markers and odd input show as written, without panicking. + for s in ["**open", "`tick", "[a](b", "[a]", "#", "- ", "é**", "https://github.com/", "**[x](y)**"] { + let _ = parse_markdown(s); + } + assert_eq!(inline("**[x](y)**"), vec![Span::Strong("x".into())]); +} diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index f77cd2f7..3ed2352d 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -14,6 +14,7 @@ - [Plastic parts](guide/plastic.md) - [Import and export](guide/files.md) - [Keyboard shortcuts](guide/shortcuts.md) +- [Updates and privacy](guide/updates.md) - [Driving SolidCraft with an AI agent (MCP)](guide/mcp.md) # Developer guide diff --git a/docs/book/src/guide/updates.md b/docs/book/src/guide/updates.md new file mode 100644 index 00000000..0babbabd --- /dev/null +++ b/docs/book/src/guide/updates.md @@ -0,0 +1,69 @@ +# Updates and privacy + +SolidCraft tells you when a newer release is out. It never updates itself without asking, and +the check sends nothing about you or your designs. + +## What you see + +When a newer stable release exists, an **Update** button appears in the title bar next to +**Report a bug**. It opens a small window with the new version, its release notes and three +choices: + +- **Download** opens the file that fits your installation (see the table below) in your + browser, or the release page when there isn't one. +- **Skip this version** hides the button until an even newer release comes out. +- **Remind me later** hides it for three days. + +**Preferences ▸ General ▸ Updates** has **Check for updates automatically** (on by default) and +**Check now**, which checks right away and shows the window even for a version you skipped. + +## What is sent + +One HTTPS request, at start and at most once a day while SolidCraft runs: + +```text +GET https://api.github.com/repos/bherbruck/solidcraft/releases/latest +User-Agent: SolidCraft-update-check +Accept: application/vnd.github+json +X-GitHub-Api-Version: 2022-11-28 +``` + +That's all: no version number, no machine or user identifier, no usage data, no cookies. GitHub +sees the request like any other visit to the API (your IP address, the time); see +[GitHub's privacy statement](https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement). +SolidCraft compares the answer with its own version on your computer. + +- Offline, or GitHub doesn't answer within 15 seconds: nothing is shown, and the check is tried + again an hour later. +- GitHub's rate limit for anonymous requests (60 an hour per address) is respected: when GitHub + says to wait, SolidCraft waits that long, Check now included. +- Turning off **Check for updates automatically** stops the checks; only **Check now** asks. +- Setting the environment variable `SOLIDCRAFT_UPDATE_CHECK=0` turns all checks off, for + example on managed machines. + +Builds you compile yourself, the Flatpak and the browser app (which updates itself) never check. + +## Which file Download gets + +Releases attach the main packages under fixed names (see [releasing](https://github.com/bherbruck/solidcraft/blob/main/docs/releasing.md)), +and SolidCraft picks the one matching how it was installed: + +| Installed as | Detected by | Download | How to update | +|---|---|---|---| +| AppImage | `APPIMAGE` is set | `solidcraft-linux-.AppImage` | Replace the old AppImage with the new one | +| Flatpak | `FLATPAK_ID` is set | doesn't check (see below) | `flatpak update ai.storyteller.solidcraft` | +| Snap from the Snap Store | `SNAP` is set | release page | `sudo snap refresh solidcraft` (snapd also refreshes by itself) | +| Snap from a downloaded file | `SNAP_REVISION` starts with `x` | `solidcraft-linux-.snap` | `sudo snap install --dangerous ./solidcraft-linux-.snap` | +| `.deb` | under `/usr`, dpkg knows `solidcraft` | `solidcraft-linux-.deb` | `sudo apt install ./solidcraft-linux-.deb` | +| `.rpm` | under `/usr`, an rpm database exists | `solidcraft-linux-.rpm` | `sudo dnf install ./solidcraft-linux-.rpm` | +| `.tar.gz` | anywhere else on Linux | `solidcraft--linux-.tar.gz` | Unpack over the old copy | +| Windows installer | under Program Files | `solidcraft-windows-.msi` | Run the installer | +| Windows portable | `solidcraft-cli.exe` beside it | `solidcraft-windows--portable.zip` | Unzip over the old copy | +| Windows `.exe` | anywhere else on Windows | `SolidCraft--windows-x64.exe` | Replace the old `.exe` | +| macOS | inside `SolidCraft.app` | `solidcraft-macos-universal.dmg` | Drag the new app to Applications | + +The window shows the command for Snap, `.deb` and `.rpm` installations, with a Copy button. The +Flatpak has no network permission and Flatpak itself (or your software center) offers its +updates, so it never checks. Release builds compare versions as [semver](https://semver.org): a nightly build counts +as the release it was built after, so it's offered only a newer stable release, and release +candidates are offered their final release. diff --git a/docs/control-protocol.md b/docs/control-protocol.md index e0cf0ec4..f0be4f3a 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -123,7 +123,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o | `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs | | `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) | | `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (copies diagnostics), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text | -| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `ko`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`; `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | +| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `ko`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | | `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it | diff --git a/docs/releasing.md b/docs/releasing.md index 92e904e0..3276d2fa 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -20,7 +20,8 @@ also attached without the version in their names, for example `solidcraft-window `solidcraft-linux-x86_64.AppImage`, and listed in `SHA256SUMS.txt`. The README's Download section links to `https://github.com/bherbruck/solidcraft/releases/latest/download/`, which follows the release marked latest; nightlies never are. Keep these names stable: renaming one breaks the -README links. +README links and the in-app update check, which picks them by name (`crates/ui-egui/src/update.rs`; +[updates and privacy](book/src/guide/updates.md)). Project Pages publishes the documentation, browser app and published-release Flatpak repository under `/solidcraft/`. Do not add a separate owner-site publisher or permanent