diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1f18b1..ae5ea0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,8 +49,13 @@ jobs: echo "apparmor userns restriction: $(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null || echo none)" echo "If the first is 0 or the second is 1, the mount tests below skipped." + # `--locked` was the gap: the release workflow and the PKGBUILD both + # build locked, so a Cargo.lock that no longer agreed with Cargo.toml + # passed every check and then failed the release, where it is expensive + # and public. It is asserted here rather than by a second, fat-LTO + # release build, which would cost minutes to learn the same thing. - name: Test - run: cargo test --verbose + run: cargo test --verbose --locked # The org rule is that every dependency sits on its latest release. This makes # drift visible in a diff rather than leaving it to be discovered later. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe52e90..2df198c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,14 +21,70 @@ jobs: - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 id: release with: - release-type: rust + # The workspace is a virtual manifest: one `[workspace.package]` + # version that both crates inherit, and no `[package]` at the root at + # all. That is why the config uses `release-type: simple` and names + # the version line outright - the `rust` type assumes a Cargo package + # lives here and throws on the manifest that does not have one, so no + # release pull request is opened at all. The type is set in the config + # file rather than here, so there is one place to read it. config-file: release-please-config.json manifest-file: .release-please-manifest.json + # `release-type: simple` rewrites Cargo.toml and nothing else, but + # Cargo.lock pins every workspace member by version and this workflow + # builds with `--locked`. A release pull request that bumped the manifest + # alone would fail the moment it merged, so the lock is brought along. + # + # The condition is the branch existing, not an output of the action above: + # conditioning on `prs_created` silently skips this, and a step that skips + # looks exactly like a step that had nothing to do. That lesson is Eidos's, + # whose release pipeline this mirrors. + - name: Keep Cargo.lock in step with the version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + url="https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + # Matched by prefix rather than named outright. Under `release-type: + # rust` the branch carried a component read from the Cargo package + # name - `…--components--raven` - and `simple` has no package to read + # one from, so the suffix is not ours to predict. One open release + # branch is the only case there is; more than one would be a + # release-please misconfiguration worth failing on. + branch=$(git ls-remote --heads "$url" \ + "release-please--branches--${GITHUB_REF_NAME}*" \ + | sed 's#.*refs/heads/##') + if [ -z "$branch" ]; then + echo "no release branch open - nothing to sync" + exit 0 + fi + if [ "$(printf '%s\n' "$branch" | wc -l)" -gt 1 ]; then + echo "more than one release branch open, refusing to guess:" + printf ' %s\n' $branch + exit 1 + fi + git clone --branch "$branch" --depth 1 "$url" pr + cd pr + # Rewrites the workspace members' versions in the lock and nothing + # else: no external dependency is touched and nothing is compiled. + # `cargo metadata --no-deps` does NOT do this - skipping resolution is + # the whole point of that flag, so the lock is never rewritten. + cargo update --workspace + if git diff --quiet Cargo.lock; then + echo "Cargo.lock already agrees with Cargo.toml" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: sync Cargo.lock with the release version" Cargo.lock + git push + echo "Cargo.lock synced onto $branch" + # Raven is Linux-only: overlayfs and binfmt_misc do not exist elsewhere, # so the asset matrix is one row, not four. build: - name: Build raven-linux + name: Build raven and raven-gui needs: release-please if: ${{ needs.release-please.outputs.release_created }} runs-on: ubuntu-latest @@ -47,14 +103,18 @@ jobs: - name: Build run: cargo build --release --locked --target x86_64-unknown-linux-gnu - - name: Stage binary - run: cp target/x86_64-unknown-linux-gnu/release/raven raven-linux + - name: Stage binaries + run: | + cp target/x86_64-unknown-linux-gnu/release/raven raven-linux + cp target/x86_64-unknown-linux-gnu/release/raven-gui raven-gui-linux - - name: Upload binary to GitHub Release + - name: Upload binaries to GitHub Release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: tag_name: ${{ needs.release-please.outputs.tag_name }} - files: raven-linux + files: | + raven-linux + raven-gui-linux # Sign every release asset with the Project-Colony org key (ed25519). # Colony >= 0.8.0 verifies these signatures on install and REFUSES a diff --git a/Cargo.lock b/Cargo.lock index e7c7dc7..86dc187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,69 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.20", +] + +[[package]] +name = "android-build" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fc9904ad2ad097c3c1cfe2eacaaf0fc24710936fa9ed941cb310b7c6ed2ab7" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_system_properties" version = "0.1.6" @@ -47,7 +110,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,7 +121,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -67,30 +130,329 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cc" version = "1.4.4" @@ -98,6 +460,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -107,6 +471,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.45" @@ -161,390 +531,486 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "colorchoice" -version = "1.0.5" +name = "clipboard-win" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "clipboard_macos" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "clipboard_wayland" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" dependencies = [ - "cfg-if", + "smithay-clipboard", ] [[package]] -name = "enumn" -version = "0.1.14" +name = "clipboard_x11" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror 2.0.20", + "x11rb", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "codespan-reporting" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] [[package]] -name = "errno" -version = "0.3.14" +name = "colony-ui" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "9005873dd9a2d7b63d226eeaa7191d595e42e312caba03d193b3d473a25ccea8" dependencies = [ - "libc", - "windows-sys", + "dirs", + "iced", + "serde_json", ] [[package]] -name = "find-msvc-tools" -version = "0.1.11" +name = "colorchoice" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] -name = "futures-core" -version = "0.3.34" +name = "combine" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] [[package]] -name = "futures-task" -version = "0.3.34" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "futures-util" -version = "0.3.34" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "core-foundation-sys", + "libc", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "heck" -version = "0.5.0" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "core-graphics" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "core-graphics-types" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "cc", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", ] [[package]] -name = "indexmap" -version = "2.14.1" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "equivalent", - "hashbrown", + "bitflags 2.13.1", + "core-foundation 0.10.1", + "libc", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "core_maths" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] [[package]] -name = "js-sys" -version = "0.3.104" +name = "cosmic-text" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "173852283a9a57a3cbe365d86e74dc428a09c50421477d5ad6fe9d9509e37737" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "bitflags 2.13.1", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 1.1.0", + "self_cell", + "skrifa 0.37.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", ] [[package]] -name = "libc" -version = "0.2.189" +name = "crossbeam-utils" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] -name = "log" -version = "0.4.34" +name = "cryoglyph" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +checksum = "08bc795bdbccdbd461736fb163930a009da6597b226d6f6fce33e7a8eb6ec519" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.3", + "wgpu", +] [[package]] -name = "memoffset" -version = "0.9.1" +name = "ctor" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" dependencies = [ - "autocfg", + "dtor", ] [[package]] -name = "nt-hive" -version = "0.3.0" +name = "cursor-icon" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f2d1dac9c1dd47ef51de60ff6794e2e163272124489bc93ec7b6a63348bbe9" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "bitflags", - "enumn", - "memoffset", - "thiserror 2.0.20", - "zerocopy", + "dirs-sys", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "dirs-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ - "autocfg", + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "dispatch" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "dlib" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] [[package]] -name = "proc-macro2" -version = "1.0.107" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "unicode-ident", + "litrs", ] [[package]] -name = "quote" -version = "1.0.47" +name = "downcast-rs" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dtor" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "proc-macro2", + "cfg-if", ] [[package]] -name = "raven" -version = "0.3.0" +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "anyhow", - "clap", - "nt-hive", - "regf", - "rustix", + "enumflags2_derive", "serde", - "thiserror 2.0.20", - "toml", ] [[package]] -name = "regf" -version = "0.1.0" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c58f645b9512345197fd9509bdd4a0c0843eeb8e2980e70a828f018f52bc7c4" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "bitflags", - "byteorder", - "chrono", - "encoding_rs", - "thiserror 1.0.69", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "enumn" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "bitflags", - "errno", "libc", - "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] -name = "rustversion" -version = "1.0.23" +name = "error-code" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" [[package]] -name = "serde" -version = "1.0.229" +name = "etagere" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" dependencies = [ - "serde_core", - "serde_derive", + "euclid", + "svg_fmt", ] [[package]] -name = "serde_core" -version = "1.0.229" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ - "serde_derive", + "num-traits", ] [[package]] -name = "serde_derive" -version = "1.0.229" +name = "event-listener" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", + "parking", + "pin-project-lite", ] [[package]] -name = "serde_spanned" -version = "1.1.1" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "serde_core", + "event-listener", + "pin-project-lite", ] [[package]] -name = "shlex" -version = "2.0.1" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "slab" -version = "0.4.12" +name = "find-msvc-tools" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] -name = "strsim" -version = "0.11.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "syn" -version = "2.0.119" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bytemuck", ] [[package]] -name = "syn" -version = "3.0.4" +name = "font-types" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "e64eb721ca85a34323425f4041adc5d82704d3782d5f8f03793bc012419dce23" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bytemuck", ] [[package]] -name = "thiserror" -version = "1.0.69" +name = "fontconfig-parser" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ - "thiserror-impl 1.0.69", + "roxmltree", ] [[package]] -name = "thiserror" -version = "2.0.20" +name = "fontdb" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ - "thiserror-impl 2.0.20", + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "foreign-types" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "foreign-types-macros", + "foreign-types-shared", ] [[package]] -name = "thiserror-impl" -version = "2.0.20" +name = "foreign-types-macros" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", @@ -552,99 +1018,2819 @@ dependencies = [ ] [[package]] -name = "toml" -version = "1.1.4+spec-1.1.0" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "futures" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ - "serde_core", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" +name = "futures-channel" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ - "winnow", + "futures-core", + "futures-sink", ] [[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "core_maths", + "read-fonts 0.35.0", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "iced" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000e01026c93ba643f8357a3db3ada0e6555265a377f6f9291c472f6dd701fb3" +dependencies = [ + "iced_core", + "iced_debug", + "iced_futures", + "iced_renderer", + "iced_runtime", + "iced_widget", + "iced_winit", + "thiserror 2.0.20", +] + +[[package]] +name = "iced_core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ab1937d699403e7e69252ae743a902bcee9f4ab2052cc4c9a46fcf34729d85" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "glam", + "lilt", + "log", + "num-traits", + "rustc-hash 2.1.3", + "smol_str", + "thiserror 2.0.20", + "web-time", +] + +[[package]] +name = "iced_debug" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25035ab0215a620e53f4103e36fc4e59a1fb2817e4bfc38a30ad27b4202ea0be" +dependencies = [ + "iced_core", + "iced_futures", + "log", +] + +[[package]] +name = "iced_futures" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0c85ccad42dfbec7293c36c018af0ea0dbcc52d137a4a9a0b0f6822a3fdf0a" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.3", + "tokio", + "wasm-bindgen-futures", + "wasmtimer", +] + +[[package]] +name = "iced_graphics" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234ca1c2cec4155055f68fa5fad1b5242c496ac8238d80a259bca382fb44a102" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "log", + "raw-window-handle", + "rustc-hash 2.1.3", + "thiserror 2.0.20", + "unicode-segmentation", +] + +[[package]] +name = "iced_program" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfafec2947cda688d8eb00dac337ba11aa60f9ef6335aed343e189d26e4a673" +dependencies = [ + "iced_graphics", + "iced_runtime", +] + +[[package]] +name = "iced_renderer" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250cc0802408e8c077986ec56c7d07c65f423ee658a4b9fd795a1f2aae5dac05" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 2.0.20", +] + +[[package]] +name = "iced_runtime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1889b819ce4c06674183242e336c8d49465665441396914dc07cc86f44fa8d4" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 2.0.20", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0acf8b75a3bc914aff5f2329fdffc1b36eeaea29dda0e4bd232f1c62e9cc3d" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_debug", + "iced_graphics", + "kurbo", + "log", + "rustc-hash 2.1.3", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "cryoglyph", + "futures", + "glam", + "guillotiere", + "iced_debug", + "iced_graphics", + "log", + "rustc-hash 2.1.3", + "thiserror 2.0.20", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1596afa0d3109c2618e8bc12bae6c11d3064df8f95c42dfce570397dbe957ab" +dependencies = [ + "iced_renderer", + "log", + "num-traits", + "rustc-hash 2.1.3", + "thiserror 2.0.20", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7dbedc47562d1de3b9707d939f678b88c382004b7ab5a18f7a7dd723162d75" +dependencies = [ + "iced_debug", + "iced_program", + "log", + "mundy", + "rustc-hash 2.1.3", + "thiserror 2.0.20", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "window_clipboard", + "winit", +] + +[[package]] +name = "indexmap" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.20", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.3", +] + +[[package]] +name = "lilt" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337d4c256f7d9f2dbd633891d48ace853efa5b1554122d9220b172ad9c03c3a9" +dependencies = [ + "web-time", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mundy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32eb0db40f2df2bcfb05c93b8f73938d4c26ce9ac8881f1df0c8d3296921a73" +dependencies = [ + "android-build", + "async-io", + "cfg-if", + "dispatch", + "futures-channel", + "futures-lite", + "jni", + "ndk-context", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "pin-project-lite", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", + "zbus", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.20", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nt-hive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f2d1dac9c1dd47ef51de60ff6794e2e163272124489bc93ec7b6a63348bbe9" +dependencies = [ + "bitflags 2.13.1", + "enumn", + "memoffset", + "thiserror 2.0.20", + "zerocopy", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c860fd3227ca4ac3cc032e2cd20cba3f02ccdf4b610538f8ee6584d56bb62e96" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "raven" +version = "0.3.0" +dependencies = [ + "anyhow", + "clap", + "nt-hive", + "regf", + "rustix 1.1.4", + "serde", + "thiserror 2.0.20", + "toml", +] + +[[package]] +name = "raven-gui" +version = "0.3.0" +dependencies = [ + "colony-ui", + "iced", + "raven", + "tokio", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.4", + "once_cell", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "regf" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c58f645b9512345197fd9509bdd4a0c0843eeb8e2980e70a828f018f52bc7c4" +dependencies = [ + "bitflags 2.13.1", + "byteorder", + "chrono", + "encoding_rs", + "thiserror 1.0.69", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.20", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.1", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.20", + "web-sys", +] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] -name = "utf8parse" -version = "0.2.2" +name = "window_clipboard" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "d5654226305eaf2dde8853fb482861d28e5dcecbbd40cb88e8393d94bb80d733" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 2.0.20", +] [[package]] -name = "wasm-bindgen" -version = "0.2.127" +name = "windows" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "windows-core 0.58.0", + "windows-targets", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.127" +name = "windows" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.127" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", + "windows-core 0.62.2", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.127" +name = "windows-core" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "unicode-ident", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets", ] [[package]] @@ -653,11 +3839,33 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.2", + "windows-interface 0.59.3", "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -671,6 +3879,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -688,6 +3907,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -697,6 +3935,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets", +] + [[package]] name = "windows-strings" version = "0.5.1" @@ -706,6 +3954,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -715,11 +3981,290 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" @@ -740,3 +4285,49 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 0.7.15", +] diff --git a/Cargo.toml b/Cargo.toml index 100e437..1dd90bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,26 +1,26 @@ -[package] -name = "raven" -description = "Run Windows programs on Linux against a real Windows installation mounted as C:" +[workspace] +resolver = "3" +members = ["crates/raven", "crates/raven-gui"] + +# Shared by both crates so they cannot drift apart: the GUI is unusable +# against a library of a different age, and release-please gives them one +# version. +[workspace.package] version = "0.3.0" # x-release-please-version license = "GPL-3.0-or-later" repository = "https://github.com/Project-Colony/Raven" edition = "2024" rust-version = "1.85" -[dependencies] -anyhow = "1.0.104" -clap = { version = "4.6.6", features = ["derive"] } -nt-hive = "0.3.0" -rustix = { version = "1.1.4", features = ["mount", "thread", "process", "fs"] } -serde = { version = "1.0.229", features = ["derive"] } -thiserror = "2.0.20" -toml = "1.1.4" - -[dev-dependencies] -regf = "0.1.0" - -# raven runs once per .exe launch, so what matters is start-up and size, both -# measured before and after (numbers in the commit that added this). +# Two binaries share this profile now, and they are paid for by different +# people. `raven` runs once per .exe launch, so start-up and size are all that +# matter, and both were measured before and after (numbers in the commit that +# added this). `raven-gui` gains nothing measurable from either setting - it is +# opened by hand, once, and then stays open - and pays for them in compile +# time, since it links wgpu and iced under one codegen unit. The settings are +# kept anyway: they are here for the launcher, which is the binary on the hot +# path, and a second profile for the window would buy a faster build at the +# cost of two release binaries built two different ways. # `panic = "abort"` is deliberately not set: it trades backtraces away for # little, and that decision is recorded in docs/project/consolidation.md. [profile.release] diff --git a/README.md b/README.md index 39b330d..e0f05a0 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,18 @@ that physically cannot be. > an official ISO, mounts as C:, a real installer wrote 256 MB into an > environment without touching a byte of the base — and the game it installs > runs from a double-click in a file manager to its title screen. The registry -> projection carries 1 894 keys from the real hives, process spawn costs 1.19× -> plain Wine (135 ms against 113, down from 2× after the fonts discovery), and -> the shadow set has two entries, each backed by a measurement. +> projection carries 1 894 keys from the real hives, a launch into an +> already-running session costs about 2 ms of Raven's own overhead, and the +> shadow set is down to a single entry — the fonts mask that bought the older +> spawn figures was withdrawn, because a Windows declaring 961 fonts and having +> none is not the real thing. > -> What that does *not* mean: one game, 2D and software-rendered, which turned out to -> render through GDI and never touch Direct3D at all; DXVK now installs and has -> been shown to initialise and enumerate the GPU, but no game has rendered a -> frame through it; one installer framework exercised; programs that keep -> their strings in `.mui` files run mute. The honest ledger, including five -> performance theories that measurement destroyed, is in +> What that does *not* mean: two games, the first 2D and software-rendered, +> which turned out to render through GDI and never touch Direct3D at all, the +> second drawing on the GPU through DXVK and Direct3D 11; vkd3d-proton installs, +> but no Direct3D 12 title has been tried; one installer framework exercised; +> programs that keep their strings in `.mui` files run mute. The honest ledger, +> including five performance theories that measurement destroyed, is in > [docs/project/status.md](docs/project/status.md) and > [docs/internals/performance.md](docs/internals/performance.md). @@ -93,10 +95,10 @@ Raven's position is the one nobody occupies: ## Installation -On Arch, the package in [packaging/](packaging/) installs the binary, registers -`.exe` files with the kernel, and masks Wine's competing registration — be -aware that **installing changes what every `.exe` on the machine does**, and -uninstalling reverses it: +On Arch, the package in [packaging/](packaging/) installs both binaries, +registers `.exe` files with the kernel, and masks Wine's competing +registration — be aware that **installing changes what every `.exe` on the +machine does**, and uninstalling reverses it: ```bash git clone https://github.com/Project-Colony/Raven @@ -104,6 +106,10 @@ cd Raven/packaging makepkg -si ``` +There are two binaries because there are two front ends. `raven` is the command +line and the primary interface; `raven-gui` is a window over the same library — +environments, bases and diagnostics — and nothing here requires it. + Everywhere else, build from source — short, but the `.exe` registration is then yours to install (`raven binfmt` prints it): diff --git a/assets/brand/README.md b/assets/brand/README.md index 901fd3d..7ddacb4 100644 --- a/assets/brand/README.md +++ b/assets/brand/README.md @@ -51,14 +51,22 @@ Three things have to agree or the icon silently does not appear: | | | |---|---| -| `Icon=` in `packaging/raven.desktop` | `raven` | -| the installed desktop file | `raven.desktop` | +| `Icon=` in both desktop entries | `raven` | +| the installed desktop files | `raven.desktop` and `raven-gui.desktop` | | the installed icon | `hicolor//apps/raven.png` | `packaging/PKGBUILD` installs all three. Before that it installed only the first two, which is why the desktop entry has always asked for an icon that was not there. +A *window* needs a fourth. A compositor is not told which desktop file a running +window came from - it matches the application id the window reports against a +desktop file's basename - so `raven-gui` sets its `application_id` to +`raven-gui`, the basename of `raven-gui.desktop`, in +`crates/raven-gui/src/main.rs`. iced leaves that empty by default, which matches +nothing: the menu entry would keep its icon while the window it launched showed +the compositor's blank fallback, with nothing to say the two were one program. + ## Colours | role | dark | light | diff --git a/crates/raven-gui/Cargo.toml b/crates/raven-gui/Cargo.toml new file mode 100644 index 0000000..830edea --- /dev/null +++ b/crates/raven-gui/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "raven-gui" +description = "Raven's administration window" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +raven = { path = "../raven" } +colony-ui = "0.1.4" +iced = { version = "0.14.0", features = ["tokio", "advanced"] } +tokio = { version = "1", features = ["rt"] } diff --git a/crates/raven-gui/src/deploy.rs b/crates/raven-gui/src/deploy.rs new file mode 100644 index 0000000..a21141c --- /dev/null +++ b/crates/raven-gui/src/deploy.rs @@ -0,0 +1,281 @@ +//! Deploying a base, and reading how far it has got. +//! +//! `base deploy` is the only long operation in Raven - minutes over 143 886 +//! files - so the GUI spawns `raven base deploy` rather than calling the +//! library, and reads wimlib's progress out of the child's output. +//! +//! No library change was needed for this. `.status()` inherits the parent's +//! stdio, so a piped stdout reaches `wimlib-imagex` unchanged, and wimlib does +//! not suppress progress when its output is not a terminal - verified before +//! this was written. +//! +//! The two shapes below are wimlib's own format strings, read from the +//! `wimlib-imagex` binary: +//! +//! ```text +//! Creating files: %lu of %lu (%u%%) done +//! Extracting file data: %lu %s of %lu %s (%u%%) done +//! ``` + +use std::io::Read; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use iced::futures::channel::mpsc; +use iced::futures::{SinkExt, Stream}; + +use crate::Message; + +/// How far a deployment has got. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Progress { + /// 0 to 100. + pub percent: u8, + /// Which phase wimlib is in, as it names it. + pub what: String, +} + +/// Reads one line of wimlib output. `None` for anything that is not progress. +/// +/// Matched by shape rather than by regex: the phase name, then a bracketed +/// percentage, then the word `done`. A stray percentage elsewhere in a line is +/// not progress and must not be read as any. +pub fn parse_progress(line: &str) -> Option { + let line = line.trim(); + let rest = line.strip_suffix(" done")?; + let (head, pct) = rest.rsplit_once(" (")?; + let percent: u8 = pct.strip_suffix("%)")?.parse().ok()?; + let what = head.split_once(':')?.0; + if what.is_empty() { + return None; + } + Some(Progress { + percent: percent.min(100), + what: what.to_string(), + }) +} + +/// The one line of a failed child's error output worth putting in a banner. +/// +/// Raven's own failures reach stderr through anyhow, which prints the +/// sentence first and its cause underneath: +/// +/// ```text +/// Error: could not read the image at /mnt/win.wim +/// +/// Caused by: +/// No such file or directory (os error 2) +/// ``` +/// +/// The last line there is the errno, which says nothing about which file or +/// what Raven was doing - so the `Error:` line wins when there is one. Only +/// when there is not, as for `wimlib-imagex`'s own diagnostics, is the last +/// line the one that says why. Empty pieces are skipped either way: wimlib +/// redraws progress with `\r`, so a killed child can leave only whitespace. +pub fn last_meaningful_line(stderr: &str) -> Option { + let lines = || stderr.split(['\r', '\n']).map(str::trim); + if let Some(sentence) = lines().find_map(|l| l.strip_prefix("Error: ")) { + if !sentence.is_empty() { + return Some(sentence.to_owned()); + } + } + lines().rfind(|line| !line.is_empty()).map(str::to_owned) +} + +/// What `raven base deploy` needs, gathered from the bases screen's three +/// text fields since this batch has no file dialog. +/// +/// Doubles as the identity `Subscription::run_with` hashes on: while the same +/// deployment is in flight, the stream survives repeated `view` calls rather +/// than being torn down and restarted. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Args { + pub image: PathBuf, + pub edition: u32, + pub name: String, +} + +/// Spawns `raven base deploy` and turns its stdout into progress messages. +/// +/// The intended target of `Subscription::run_with(args, deploy::run)`. Runs +/// the child on a blocking worker - spawning, reading a pipe and waiting are +/// all blocking calls - and forwards what it reads back through `output`. +/// +/// `+ use<>` tells the 2024 capture rules the returned stream does not borrow +/// `args` - it is cloned on the next line - so the opaque type stays a plain +/// `S`, which is what `Subscription::run_with`'s `fn(&D) -> S` needs; without +/// it the type is `for<'a> fn(&'a Args) -> S<'a>`, which cannot coerce there. +pub fn run(args: &Args) -> impl Stream + use<> { + let args = args.clone(); + iced::stream::channel(16, async move |mut output| { + let outcome = { + let output = output.clone(); + tokio::task::spawn_blocking(move || pump(&args, output)) + .await + .expect("the blocking task panicked") + }; + // The outcome comes back as a value rather than being pushed down the + // channel with the progress updates, so that it can be `await`ed here. + // Dropping a progress update is right - they arrive faster than anyone + // reads them. Dropping this one is not: `deploying` and `deploy_job` + // would stay `Some` for ever, freezing the bar at whatever percentage + // it had reached with the fields and the Deploy button disabled and no + // way out of it. Awaiting waits for room instead of giving up. + let _ = output.send(Message::DeployDone(outcome)).await; + }) +} + +/// Runs the child to completion, reporting wimlib's progress as it goes, and +/// returns why it failed if it did. +/// +/// wimlib redraws its progress line in place with `\r` rather than emitting +/// one `\n`-terminated line per update, so each chunk read off the pipe is +/// split on both and only the last complete reading is reported - the ones +/// before it were already stale by the time the read returned. +/// +/// stderr is piped rather than inherited. A GUI started from an application +/// menu has nowhere to inherit it *to*, so the reason a deployment failed - +/// the one operation here that costs minutes - would go to no terminal at all +/// and the window would have nothing to say but "try it again by hand". +/// +/// `Command::new("raven")` resolves through `$PATH`, not to this workspace's +/// own build. That's the right binary to run in the packaged case the GUI +/// ships for: the package installs the GUI and the CLI together at the same +/// version (see `packaging/PKGBUILD`), so whichever `raven` `$PATH` finds is +/// guaranteed to match. It is the wrong binary under `cargo run -p +/// raven-gui`: PATH still resolves to the system's installed `/usr/bin/raven` +/// rather than `target/debug/raven`, so a developer testing a CLI change +/// against a freshly built GUI silently deploys with the old, already- +/// installed CLI and sees no sign that happened. +fn pump(args: &Args, mut output: mpsc::Sender) -> Result<(), String> { + let child = Command::new("raven") + .arg("base") + .arg("deploy") + .arg("--image") + .arg(&args.image) + .arg("--edition") + .arg(args.edition.to_string()) + .arg("--name") + .arg(&args.name) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn(); + + let mut child = match child { + Ok(child) => child, + Err(e) => return Err(format!("`raven base deploy` would not start: {e}")), + }; + + // stderr is drained by its own thread. The child writes to both pipes, and + // a single reader parked on stdout would deadlock the moment stderr's pipe + // filled - which is exactly what a failing deployment does. + let mut stderr = child.stderr.take().expect("stderr was requested piped"); + let errors = std::thread::spawn(move || { + let mut said = String::new(); + let _ = stderr.read_to_string(&mut said); + said + }); + + let mut stdout = child.stdout.take().expect("stdout was requested piped"); + let mut buf = [0u8; 4096]; + loop { + let n = match stdout.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(n) => n, + }; + let chunk = String::from_utf8_lossy(&buf[..n]); + if let Some(progress) = chunk + .split(['\r', '\n']) + .filter_map(parse_progress) + .next_back() + { + // Losing one of these is fine and intended: they arrive faster than + // the window redraws, and the next one is already truer. + let _ = output.try_send(Message::DeployProgress(progress)); + } + } + + let success = child.wait().map(|status| status.success()).unwrap_or(false); + let said = errors.join().unwrap_or_default(); + if success { + Ok(()) + } else { + Err(last_meaningful_line(&said) + .unwrap_or_else(|| "It gave no reason before it stopped.".to_owned())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_two_shapes_wimlib_emits_are_both_understood() { + // Captured from `wimlib-imagex apply` with its output piped, which is + // how the GUI will always see it. + let extracting = parse_progress("Extracting file data: 3886 KiB of 3906 KiB (99%) done"); + assert_eq!( + extracting, + Some(Progress { + percent: 99, + what: "Extracting file data".into() + }) + ); + let creating = parse_progress("Creating files: 12345 of 143886 (8%) done"); + assert_eq!( + creating, + Some(Progress { + percent: 8, + what: "Creating files".into() + }) + ); + } + + #[test] + fn anything_else_is_not_progress() { + assert_eq!(parse_progress(""), None); + assert_eq!(parse_progress("Applying image 1 to /home/x/base"), None); + // A percentage that is not wimlib's shape must not be mistaken for one. + assert_eq!(parse_progress("almost (50%) there"), None); + } + + #[test] + fn a_failure_is_reported_with_the_line_that_says_why() { + // Raven's own wording, as `base deploy` writes it to stderr, followed + // by the blank line a terminating newline leaves behind. + let said = "Applying image 1 to /home/x/.local/share/raven/bases/win11\n\ + error: no space left on device while writing Windows/System32\n"; + assert_eq!( + last_meaningful_line(said).as_deref(), + Some("error: no space left on device while writing Windows/System32") + ); + } + + #[test] + fn ravens_own_failure_is_reported_by_its_sentence_and_not_its_errno() { + // anyhow prints the context first and the cause underneath, so the + // last line is "No such file or directory" - true of a thousand + // failures and a description of none of them. + let said = "Error: could not read the image at /mnt/win.wim\n\n\ + Caused by:\n No such file or directory (os error 2)\n"; + assert_eq!( + last_meaningful_line(said).as_deref(), + Some("could not read the image at /mnt/win.wim") + ); + } + + #[test] + fn a_child_that_said_nothing_yields_nothing_to_show() { + assert_eq!(last_meaningful_line(""), None); + // wimlib redraws with \r, so a killed child can leave only whitespace. + assert_eq!(last_meaningful_line("\r\n \r\n"), None); + } + + #[test] + fn a_carriage_return_stream_yields_the_last_complete_line() { + // wimlib redraws in place with \r rather than emitting new lines. + let chunk = "Extracting file data: 1 KiB of 10 KiB (10%) done\rExtracting file data: 5 KiB of 10 KiB (50%) done\r"; + let last = chunk.split('\r').filter_map(parse_progress).next_back(); + assert_eq!(last.map(|p| p.percent), Some(50)); + } +} diff --git a/crates/raven-gui/src/errors.rs b/crates/raven-gui/src/errors.rs new file mode 100644 index 0000000..c7a61e9 --- /dev/null +++ b/crates/raven-gui/src/errors.rs @@ -0,0 +1,202 @@ +//! Turning a library error into something a window can offer. +//! +//! Raven's messages are written for a terminal and several carry a command to +//! run. Telling someone using a window to open a terminal is an admission of +//! failure, so the errors with an obvious action become that action. The rest +//! keep their own words: the text is good, and a GUI-flavoured paraphrase +//! would lose the part that explains *why*. + +use raven::Error; + +/// What the window offers to do about an error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Action { + /// Release Raven's own session anchor. Nothing of the user's is running: + /// the library raises `SessionHolds` only when every holder is Raven. + StopSession(String), + /// Terminate every process holding the environment, the user's programs + /// included - `stop()` signals holders without asking who they are. Kept + /// apart from `StopSession` because the button has to say so: the window + /// uses "session" for the anchor, and "Stop the session" over a running + /// game would read as harmless. + StopHolders(String), +} + +impl Action { + /// The button's words. Here rather than in the view so the test that + /// pins the distinction can read them. + pub fn label(&self) -> &'static str { + match self { + Action::StopSession(_) => "Stop the session", + Action::StopHolders(_) => "Stop everything using it", + } + } + + /// What pressing it asks for. + /// + /// Only `StopSession` goes through the stop that checks again first: its + /// label promises nothing of the user's is running, and the banner + /// carrying it outlives the poll, so that promise can go stale. + /// `StopHolders` has already said it will end the programs it lists, and + /// routing it through the same check made it refuse in exactly the case + /// it exists for - a button that re-raised its own banner for ever. + pub fn message(&self) -> crate::Message { + match self { + Action::StopSession(name) => crate::Message::StopSession(name.clone()), + Action::StopHolders(name) => crate::Message::Stop(name.clone()), + } + } +} + +/// What raised the banner, which decides both how it is painted and what is +/// allowed to take it away. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// Reading the machine's state failed. The next successful read means the + /// condition is gone, so that read clears it. + LoadError, + /// An action failed. A read must never clear this: the environments screen + /// reloads every two seconds, and an error that survives for one poll + /// interval is an error nobody gets to act on. + ActionError, + /// Guidance, not a failure - what to type for something the window cannot + /// do yet. Painted neutrally, because `error` ink on `error_bg` would tell + /// the user something went wrong when nothing did. + Notice, +} + +/// A message to show, and optionally a button to show beside it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Offer { + pub kind: Kind, + pub message: String, + pub action: Option, +} + +impl Offer { + pub fn load_error(message: String) -> Self { + Self { + kind: Kind::LoadError, + message, + action: None, + } + } + + pub fn action_error(message: String) -> Self { + Self { + kind: Kind::ActionError, + message, + action: None, + } + } + + pub fn notice(message: String) -> Self { + Self { + kind: Kind::Notice, + message, + action: None, + } + } +} + +pub fn explain(error: &Error) -> Offer { + match error { + Error::SessionHolds(name) => Offer { + kind: Kind::ActionError, + message: format!( + "A session is holding {name}'s C:, which is what makes launches fast." + ), + action: Some(Action::StopSession(name.clone())), + }, + Error::EnvironmentBusy { name, holders } => Offer { + kind: Kind::ActionError, + message: format!("{name} is still in use by {holders}. Stopping it ends them."), + action: Some(Action::StopHolders(name.clone())), + }, + other => Offer::action_error(other.to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use raven::Error; + + #[test] + fn a_session_holding_an_environment_offers_to_stop_it() { + let offer = explain(&Error::SessionHolds("games".into())); + assert_eq!(offer.action, Some(Action::StopSession("games".into()))); + assert!( + !offer.message.contains("raven env stop"), + "the button replaces the command; repeating it is noise: {}", + offer.message + ); + } + + #[test] + fn an_environment_in_use_by_other_programs_says_that_stopping_ends_them() { + // The library raises this when something other than Raven's own + // anchor holds the mount - a game, typically - and `stop()` will + // SIGTERM and then SIGKILL all of it. The window's own vocabulary + // uses "session" for the anchor, so a button called "Stop the + // session" here would read as harmless while ending the game. + let offer = explain(&Error::EnvironmentBusy { + name: "games".into(), + holders: "1234 (ShineHill.exe)".into(), + }); + let action = offer.action.clone().expect("stopping is the remedy"); + assert_eq!(action, Action::StopHolders("games".into())); + assert_ne!( + action.label(), + Action::StopSession("games".into()).label(), + "ending the user's programs must not wear the anchor's label" + ); + assert!( + offer.message.contains("1234 (ShineHill.exe)") && offer.message.contains("ends"), + "must say what it will end: {}", + offer.message + ); + } + + #[test] + fn the_stop_that_ends_the_users_programs_does_not_ask_permission_first() { + // The guarded stop refuses when anything but Raven's anchor holds the + // environment - which is exactly when this banner is raised. Sending + // it there made the button a loop: it re-raised its own banner and + // released nothing, and every card's Stop went with it, because Wine's + // services hold the mount from the moment Start warms them. + let busy = explain(&Error::EnvironmentBusy { + name: "games".into(), + holders: "1234 (ShineHill.exe)".into(), + }); + assert!( + matches!(busy.action.unwrap().message(), crate::Message::Stop(n) if n == "games"), + "a stop that names what it ends must go straight through" + ); + let session = explain(&Error::SessionHolds("games".into())); + assert!( + matches!(session.action.unwrap().message(), crate::Message::StopSession(n) if n == "games"), + "and the one promising nothing is running must look again" + ); + } + + #[test] + fn what_an_action_raises_is_marked_as_such_so_a_refresh_cannot_erase_it() { + // The environments screen reloads every two seconds. If these came back + // as `LoadError`, the next poll would take the message and its button + // away before anyone had read either. + assert_eq!( + explain(&Error::SessionHolds("games".into())).kind, + Kind::ActionError + ); + assert_eq!(explain(&Error::NoWine).kind, Kind::ActionError); + } + + #[test] + fn an_error_with_no_obvious_action_keeps_its_own_words() { + let e = Error::NoWine; + let offer = explain(&e); + assert_eq!(offer.action, None); + assert_eq!(offer.message, e.to_string()); + } +} diff --git a/crates/raven-gui/src/load.rs b/crates/raven-gui/src/load.rs new file mode 100644 index 0000000..27d65af --- /dev/null +++ b/crates/raven-gui/src/load.rs @@ -0,0 +1,78 @@ +//! Calling the library without freezing the window. +//! +//! Reading one environment's state opens `/proc` for every process on the +//! machine. Doing that on the interface thread stutters the window, so every +//! read goes through this module, and the actions take the same blocking +//! route through `act` in `main.rs`. + +use iced::Task; + +use crate::Message; +use crate::model::{self, BaseRow, Check, EnvRow}; + +/// Loads every environment's state on a blocking worker. +pub fn environments() -> Task { + Task::perform( + async { + tokio::task::spawn_blocking(|| { + raven::env::Environment::list() + .map(model::env_rows) + .map_err(|e| e.to_string()) + }) + .await + .unwrap_or_else(|e| Err(e.to_string())) + }, + Message::Environments, + ) +} + +/// Loads every base, and the base id of every environment so each count is +/// right. The ids come from the manifests alone, so unlike `environments` +/// this never touches `/proc`; it stays off the interface thread only +/// because it reads the disk. +pub fn bases() -> Task { + Task::perform( + async { + tokio::task::spawn_blocking(|| -> Loaded> { + let bases = raven::base::Base::list().map_err(|e| e.to_string())?; + let env_bases: Vec = raven::env::Environment::list() + .map_err(|e| e.to_string())? + .into_iter() + .map(|e| e.manifest.base) + .collect(); + Ok(model::base_rows(bases, &env_bases)) + }) + .await + .unwrap_or_else(|e| Err(e.to_string())) + }, + Message::Bases, + ) +} + +/// Loads every diagnostic on a blocking worker. `checks` shells out to `wine +/// --version`, which is enough of a subprocess call that it must not run on +/// the interface thread either. Unlike `environments` and `bases`, `checks` +/// cannot fail - it turns absence into a judgement rather than an error - so +/// there is no `Err` case to carry. +pub fn doctor() -> Task { + Task::perform( + async { + tokio::task::spawn_blocking(model::checks) + .await + .unwrap_or_default() + }, + Message::Doctor, + ) +} + +/// The shape every loader returns: the data, or a message already fit to show. +pub type Loaded = Result; + +/// Named so the signature above reads: `Task` carrying `Loaded>`. +pub type EnvRows = Loaded>; + +/// Named so the signature above reads: `Task` carrying `Loaded>`. +pub type BaseRows = Loaded>; + +/// Named so the signature above reads: `Task` carrying `Vec`. +pub type Checks = Vec; diff --git a/crates/raven-gui/src/main.rs b/crates/raven-gui/src/main.rs new file mode 100644 index 0000000..7e2caa4 --- /dev/null +++ b/crates/raven-gui/src/main.rs @@ -0,0 +1,435 @@ +//! Raven's administration window. +//! +//! A second caller of the same library the command line uses - see +//! docs/superpowers/specs/2026-09-01-raven-gui-design.md. Nothing here decides +//! anything about environments; it draws what the library reports and asks the +//! library to act. + +mod deploy; +mod errors; +mod load; +mod model; +mod theme; +mod view; + +use std::path::PathBuf; +use std::time::Duration; + +use iced::{Element, Subscription, Task}; + +use errors::Offer; +use model::{BaseRow, Check, EnvRow}; + +/// How often the environments screen asks who holds a session, per the design's +/// Data flow section. The one clock in the program, and it runs only while that +/// screen is showing. +const POLL: Duration = Duration::from_secs(2); + +/// Which screen is showing. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum Screen { + #[default] + Environments, + Detail(String), + Bases, + Doctor, +} + +/// The bases screen's three text fields, since this batch has no file dialog: +/// an image path typed or pasted, an edition index, and a name. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct DeployForm { + pub image: String, + pub edition: String, + pub name: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + Go(Screen), + Environments(load::EnvRows), + Bases(load::BaseRows), + Doctor(load::Checks), + Refresh, + Start(String), + Stop(String), + /// A stop offered on the promise that only Raven's anchor holds the + /// environment. Distinct from `Stop` because that promise is checked + /// again before it acts; see `errors::Action::message`. + StopSession(String), + // `raven::Error` holds `std::io::Error` in some variants, so it cannot be + // `Clone` - and iced's widgets (`Button::on_press` among them) require + // `Message: Clone`. `Arc` is `Clone` regardless of what it wraps, so it + // carries the error across that boundary without the library changing. + Acted(Result<(), std::sync::Arc>), + Open(String), + InstallD3d { + env: String, + vkd3d: bool, + }, + RemoveD3d { + env: String, + vkd3d: bool, + }, + Detach { + env: String, + letter: char, + }, + Reproject(String), + DeployImageChanged(String), + DeployEditionChanged(String), + DeployNameChanged(String), + DeployStart, + DeployProgress(deploy::Progress), + // `Err` carries the last thing the child said before it gave up, so a + // failure that took minutes does not have to be reproduced in a terminal + // to be understood. + DeployDone(Result<(), String>), +} + +#[derive(Default)] +pub struct App { + pub(crate) screen: Screen, + pub(crate) envs: Vec, + pub(crate) bases: Vec, + pub(crate) checks: Vec, + pub(crate) offer: Option, + /// Whether an environments load is in flight. The poll fires on a clock + /// that knows nothing about how long `/proc` takes, so without this a slow + /// scan would have a second one queued behind it every two seconds. + loading_envs: bool, + pub(crate) deploy_form: DeployForm, + /// The bar the bases screen draws, when a deployment is running. + pub(crate) deploying: Option, + /// What the running deployment was started with - kept so `subscription` + /// can keep pointing `Subscription::run_with` at the same job, and `None` + /// once it's over. Distinct from `deploying`: that one is the drawn + /// state, cleared to show the bar at all; this one is the stream's + /// identity, and would restart the job if it changed mid-flight. + deploy_job: Option, +} + +impl App { + /// The window and the first load. `reload_environments` rather than + /// `load::environments` so `loading_envs` is true from the first frame: + /// the boot load and the first poll must not both be in flight. + fn boot() -> (Self, Task) { + let mut app = Self::default(); + let task = app.reload_environments(); + (app, task) + } + + /// Reads every environment's state, and records that it is being read. + fn reload_environments(&mut self) -> Task { + self.loading_envs = true; + load::environments() + } + + /// Forgets a banner the user has navigated away from. An error raised on + /// one screen means nothing above another, and leaving it there makes the + /// new screen look broken. + fn dismiss_offer(&mut self) { + self.offer = None; + } + + /// Forgets a banner a *load* raised, and only that. A load succeeding says + /// the read works again; it says nothing about the action that failed, and + /// taking that message and its button away is what a two-second poll would + /// otherwise do to every error in the program. + fn dismiss_stale_load_error(&mut self) { + if matches!( + self.offer, + Some(Offer { + kind: errors::Kind::LoadError, + .. + }) + ) { + self.offer = None; + } + } + + fn update(&mut self, message: Message) -> Task { + match message { + Message::Go(screen) => { + self.dismiss_offer(); + // The design asks for a reload on arriving at the environments + // screen: the poll only runs while it is showing, so without + // this the first two seconds show whatever was true when the + // user last left it. + let task = match &screen { + Screen::Environments | Screen::Detail(_) => self.reload_environments(), + Screen::Bases => load::bases(), + Screen::Doctor => load::doctor(), + }; + self.screen = screen; + task + } + // Opening a card is navigating to its detail, and gets the same + // treatment rather than a second, quietly different one. + Message::Open(name) => self.update(Message::Go(Screen::Detail(name))), + Message::Environments(Ok(rows)) => { + self.envs = rows; + self.loading_envs = false; + self.dismiss_stale_load_error(); + Task::none() + } + Message::Environments(Err(e)) => { + self.loading_envs = false; + self.offer = Some(Offer::load_error(e)); + Task::none() + } + Message::Bases(Ok(rows)) => { + self.bases = rows; + self.dismiss_stale_load_error(); + Task::none() + } + Message::Bases(Err(e)) => { + self.offer = Some(Offer::load_error(e)); + Task::none() + } + Message::Doctor(rows) => { + self.checks = rows; + Task::none() + } + // A poll that arrives while the last one is still reading `/proc` + // is dropped rather than queued: the answer it would fetch is the + // answer already on its way. + Message::Refresh => { + if self.loading_envs { + Task::none() + } else { + self.reload_environments() + } + } + Message::Start(name) => act(name, |e| { + // Mounting is the cheap half. The card's Start exists because + // `env start` is what makes launches instant, and what makes + // them instant is Wine's services already standing - so this + // waits for them too, rather than flipping the card to + // "running" and leaving the first launch to pay the seconds + // the button was pressed to avoid. Their failure is not this + // action's failure: the session is up either way. + e.ensure_session()?; + e.warm_up(); + Ok(()) + }), + // What every Stop control the user can see asks for: the card's, + // the detail screen's, and the banner that already says it will + // end the programs it lists. Wine's services hold the mount from + // the moment an environment is started, so a stop that refused + // while anything but the anchor held it would refuse always. + Message::Stop(name) => act(name, |e| e.stop().map(|_| ())), + Message::StopSession(name) => act(name, |e| { + // This one is offered on the library's judgement that only + // Raven's own anchor holds the environment, and the banner + // carrying it is deliberately long-lived - exempt from the + // poll that clears the others. A program started in between + // would be killed by a button whose words promised it would + // not be, so the judgement is taken again; when it has + // changed, the error relabels the banner honestly. + match e.ensure_not_running() { + Err(busy @ raven::Error::EnvironmentBusy { .. }) => Err(busy), + _ => e.stop().map(|_| ()), + } + }), + Message::Acted(Ok(())) => { + self.dismiss_offer(); + self.reload_environments() + } + // The reload still happens: whatever the action did before it + // failed is part of the state. It no longer takes the banner with + // it - that is what `dismiss_stale_load_error` is careful about. + Message::Acted(Err(e)) => { + self.offer = Some(errors::explain(&e)); + self.reload_environments() + } + Message::InstallD3d { env, vkd3d } => { + let which = if vkd3d { "vkd3d" } else { "dxvk" }; + self.offer = Some(Offer::notice(format!( + "Install it from a build you already have: raven env {which} {env} --from " + ))); + Task::none() + } + Message::RemoveD3d { env, vkd3d } => act(env, move |e| { + let rt = if vkd3d { + &raven::d3d::VKD3D + } else { + &raven::d3d::DXVK + }; + e.remove_d3d(rt).map(|_| ()) + }), + Message::Detach { env, letter } => act(env, move |e| e.detach(letter)), + Message::Reproject(env) => act(env, |e| e.project_registry().map(|_| ())), + Message::DeployImageChanged(image) => { + self.deploy_form.image = image; + Task::none() + } + Message::DeployEditionChanged(edition) => { + self.deploy_form.edition = edition; + Task::none() + } + Message::DeployNameChanged(name) => { + self.deploy_form.name = name; + Task::none() + } + Message::DeployStart => { + let form = &self.deploy_form; + match form.edition.trim().parse::() { + Ok(edition) + if !form.image.trim().is_empty() && !form.name.trim().is_empty() => + { + self.deploy_job = Some(deploy::Args { + image: PathBuf::from(form.image.trim()), + edition, + name: form.name.trim().to_owned(), + }); + self.deploying = Some(deploy::Progress { + percent: 0, + what: "Starting".into(), + }); + self.dismiss_offer(); + } + _ => { + self.offer = Some(Offer::action_error( + "Fill in the image path, a numeric edition, and a name.".into(), + )); + } + } + Task::none() + } + Message::DeployProgress(progress) => { + self.deploying = Some(progress); + Task::none() + } + Message::DeployDone(outcome) => { + self.deploy_job = None; + self.deploying = None; + match outcome { + Ok(()) => { + self.deploy_form = DeployForm::default(); + load::bases() + } + Err(reason) => { + self.offer = + Some(Offer::action_error(format!("Deployment failed. {reason}"))); + Task::none() + } + } + } + } + } + + /// The two things that run without being asked. + /// + /// A deployment's stream, while one is running: `deploy_job` - not + /// `deploying` - is the identity `Subscription::run_with` hashes on, so + /// editing the progress text alone can never be mistaken for a new job. + /// + /// And the design's one clock. Who holds a session changes without the + /// window doing anything, so the environments screen and its details poll + /// for it - and nothing else does, because reading `/proc` for every + /// process on the machine on behalf of a screen nobody is looking at is + /// exactly the waste the design refuses. + fn subscription(&self) -> Subscription { + let deploying = match &self.deploy_job { + Some(args) => Subscription::run_with(args.clone(), deploy::run), + None => Subscription::none(), + }; + let polling = match self.screen { + Screen::Environments | Screen::Detail(_) => { + iced::time::every(POLL).map(|_| Message::Refresh) + } + Screen::Bases | Screen::Doctor => Subscription::none(), + }; + Subscription::batch([deploying, polling]) + } + + fn view(&self) -> Element<'_, Message> { + view::shell(self) + } +} + +/// Opens the environment on a blocking thread, runs one action against it, +/// and reports the outcome as `Message::Acted`. +/// +/// Every action in the window has this shape - open, one library call, the +/// `Arc` bridge for the error - and writing it once means a sixth action +/// cannot quietly do any of it differently. Off the interface thread for the +/// reason `load` gives: the calls behind these read `/proc` and start +/// processes. +fn act( + env: String, + f: impl FnOnce(&raven::env::Environment) -> Result<(), raven::Error> + Send + 'static, +) -> Task { + Task::perform( + async move { + tokio::task::spawn_blocking(move || f(&raven::env::Environment::open(&env)?)) + .await + .expect("the blocking task panicked") + .map_err(std::sync::Arc::new) + }, + Message::Acted, + ) +} + +/// Answers a command line rather than opening a window, and never returns. +/// +/// The library re-runs a Raven binary for the two things it cannot do in the +/// calling process - holding a session open, and mounting for a registry +/// import - and it prefers the `raven` beside us for both. Should it ever +/// fall back to this binary, falling through to iced would put a second, +/// unexplained window on the user's screen and, worse, hand the caller the +/// exit status of a window they closed: a registry import that projected +/// nothing would report success. So anything that is not a window is +/// answered here, before iced and tokio exist - which `session-anchor` needs +/// anyway, since its `unshare(CLONE_NEWUSER)` is refused to a process that +/// already has threads. +/// +/// Everything is said on stdout: that is the stream the library reads. +fn answer(verb: &str) -> ! { + match verb { + "session-anchor" => match std::env::args().nth(2) { + Some(name) => raven::session::anchor(&name), + None => { + println!("session-anchor needs an environment name"); + std::process::exit(1); + } + }, + "--help" | "-h" => { + println!("raven-gui - Raven's administration window. It takes no arguments."); + println!("The command line is `raven`; run `raven --help` for it."); + std::process::exit(0); + } + "--version" | "-V" => { + println!("raven-gui {}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } + other => { + println!("raven-gui does not answer {other:?}; run `raven {other}` instead."); + std::process::exit(1); + } + } +} + +fn main() -> iced::Result { + if let Some(verb) = std::env::args().nth(1) { + answer(&verb); + } + iced::application(App::boot, App::update, App::view) + .subscription(App::subscription) + .title("Raven") + .default_font(theme::APP_FONT) + // A Wayland compositor identifies a window by the application id it + // reports and looks for a desktop file with that basename; iced leaves + // it empty, so nothing would ever match `raven-gui.desktop` and the + // `Icon=raven` it carries would never reach a taskbar. The three-way + // agreement this belongs to is written down in assets/brand/README.md. + .window(iced::window::Settings { + platform_specific: iced::window::settings::PlatformSpecific { + application_id: "raven-gui".to_owned(), + ..Default::default() + }, + ..Default::default() + }) + .run() +} diff --git a/crates/raven-gui/src/model.rs b/crates/raven-gui/src/model.rs new file mode 100644 index 0000000..4289ef0 --- /dev/null +++ b/crates/raven-gui/src/model.rs @@ -0,0 +1,283 @@ +//! Plain data the screens draw. +//! +//! Kept apart from the drawing so it can be tested: an iced widget tree cannot +//! be meaningfully asserted on, and pretending otherwise produces tests that +//! pass whatever the window looks like. + +use std::path::PathBuf; + +use raven::d3d::{DXVK, VKD3D}; +use raven::env::Environment; + +/// One environment, as a card draws it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnvRow { + pub name: String, + pub base: String, + /// The session anchor's pid, when one is live. + pub session: Option, + /// How many processes hold the mount, the anchor included. + pub holders: usize, + /// The installed build, as the release named itself. + pub dxvk: Option, + pub vkd3d: Option, + /// Drive letter and the device behind it. + pub attachments: Vec<(char, PathBuf)>, +} + +impl EnvRow { + pub fn is_running(&self) -> bool { + self.session.is_some() + } + + /// The line under the environment's name. Written here rather than in the + /// view so it can be tested, and because "1 process" is not "1 processes". + pub fn status_line(&self) -> String { + if !self.is_running() { + return "not running".into(); + } + let plural = if self.holders == 1 { + "process" + } else { + "processes" + }; + format!("running - {} {plural}", self.holders) + } +} + +/// Reads every environment's state. Touches `/proc` for each process on the +/// machine and stats dozens of files, so this must not run on the interface +/// thread - `load::environments` is the only intended caller. +pub fn env_rows(envs: Vec) -> Vec { + envs.into_iter() + .map(|e| EnvRow { + name: e.name.clone(), + base: e.manifest.base.clone(), + session: e.session(), + holders: e.holders().len(), + dxvk: e.d3d_build(&DXVK), + vkd3d: e.d3d_build(&VKD3D), + attachments: e + .attachments() + .into_iter() + .map(|a| (a.letter, a.device)) + .collect(), + }) + .collect() +} + +/// One deployed Windows, as the bases screen draws it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaseRow { + pub id: String, + /// How many environments run against it, so destroying one is informed. + pub environments: usize, +} + +/// `env_bases` is one base id per environment, straight from the manifests. +/// A count needs nothing more, and reading an environment's state to get it +/// would walk `/proc` for every process on the machine, twice over. +pub fn base_rows(bases: Vec, env_bases: &[String]) -> Vec { + bases + .into_iter() + .map(|b| BaseRow { + environments: env_bases.iter().filter(|base| **base == b.id).count(), + id: b.id, + }) + .collect() +} + +/// One line of the diagnostics screen. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Check { + pub label: String, + pub ok: bool, + /// What it means, not just whether it passed. `raven doctor` reports + /// "absent - Wine falls back to wineserver for NT synchronization" rather + /// than "no", and the window keeps that. + pub detail: String, +} + +/// The same judgements `raven doctor` prints, from the same functions - a +/// difference between the two would be a bug here, not a second opinion. +pub fn checks() -> Vec { + use raven::mount::MountBackend as _; + let userns = raven::mount::UserNsOverlay::is_available(); + let wine = raven::prefix::wine_available(); + let ntsync = std::path::Path::new("/dev/ntsync").exists(); + let media = raven::prefix::media_decoders(); + + vec![ + exe_handler(), + Check { + label: "Unprivileged user namespaces".into(), + ok: userns, + detail: if userns { + "available - Raven can mount without root".into() + } else { + "this kernel restricts them, and Raven's only mount backend needs them".into() + }, + }, + Check { + label: "Wine".into(), + ok: wine, + detail: if wine { + "found".into() + } else { + "missing - Raven cannot run anything without it".into() + }, + }, + Check { + label: "ntsync".into(), + ok: ntsync, + detail: if ntsync { + "present".into() + } else { + "absent - Wine falls back to wineserver for NT synchronization".into() + }, + }, + Check { + label: "Media playback".into(), + ok: media.is_none(), + detail: match media { + None => "GStreamer decoders present".into(), + Some(missing) => format!( + "incomplete: {}. Games will run with their cutscenes and music silently absent.", + missing.join("; ") + ), + }, + }, + ] +} + +/// Who the kernel hands a double-clicked `.exe` to. +/// +/// The check `raven doctor` closes with, and the one the window most needs: +/// Wine registers a handler for the same `MZ` magic, the kernel silently +/// picks the most recently registered, and losing that race looks exactly +/// like Raven losing its prefix. Someone using the window rather than a +/// terminal is precisely who would never find that out. +fn exe_handler() -> Check { + let label = "Double-clicked .exe".to_string(); + if !std::path::Path::new("/proc/sys/fs/binfmt_misc").exists() { + return Check { + label, + ok: false, + detail: "binfmt_misc is not mounted - a double-clicked .exe cannot run".into(), + }; + } + let handlers = raven::launch::exe_handlers(); + match handlers.iter().find(|h| h.enabled) { + None if handlers.is_empty() => Check { + label, + ok: false, + detail: "nothing claims .exe files - run `raven binfmt` to see what to register".into(), + }, + None => Check { + label, + ok: false, + detail: "every handler is disabled - a double-clicked .exe will not run".into(), + }, + Some(w) if w.name == raven::launch::BINFMT_NAME => Check { + label, + ok: true, + detail: format!("handled by {} -> {}", w.name, w.interpreter.display()), + }, + Some(w) => Check { + label, + ok: false, + detail: format!( + "{} claims it first, not Raven: a double-clicked .exe runs against {}", + w.name, + w.interpreter.display() + ), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn an_environment_with_a_session_is_marked_running() { + let row = EnvRow { + name: "games".into(), + base: "win11-26200-pro".into(), + session: Some(4242), + holders: 9, + dxvk: Some("dxvk-3.1".into()), + vkd3d: None, + attachments: vec![], + }; + assert!(row.is_running()); + assert_eq!(row.status_line(), "running - 9 processes"); + } + + #[test] + fn an_environment_without_one_says_so_in_the_singular_when_it_should() { + let mut row = EnvRow { + name: "games".into(), + base: "b".into(), + session: None, + holders: 0, + dxvk: None, + vkd3d: None, + attachments: vec![], + }; + assert!(!row.is_running()); + assert_eq!(row.status_line(), "not running"); + row.session = Some(1); + row.holders = 1; + assert_eq!(row.status_line(), "running - 1 process"); + } + + #[test] + fn a_base_counts_the_environments_built_on_it_from_their_ids_alone() { + // Counting must not cost a read of each environment's state: that + // walks /proc for every process on the machine, and the id is in the + // manifest already. + let bases = vec![ + raven::base::Base { + id: "win11".into(), + path: PathBuf::new(), + }, + raven::base::Base { + id: "win10".into(), + path: PathBuf::new(), + }, + ]; + let rows = base_rows(bases, &["win11".into(), "win11".into()]); + assert_eq!(rows[0].environments, 2); + assert_eq!(rows[1].environments, 0); + } + + #[test] + fn every_check_carries_its_consequence_and_not_just_a_no() { + // `checks()` reads the machine, so which of them pass differs between + // machines and cannot be asserted on. What holds everywhere is the + // shape: four judgements, each one saying what it costs. Building a + // `Check` here and asserting on the string just typed would run none + // of that. + let checks = checks(); + assert!( + checks.len() >= 5, + "the window shows what raven doctor judges, the .exe handler included" + ); + assert!( + checks.iter().any(|c| c.label.contains(".exe")), + "the handler race is the failure that looks like Raven losing its prefix" + ); + for c in &checks { + assert!(!c.label.is_empty()); + // A bare verdict is what `raven doctor` refuses to print, and the + // window shows whatever this returns, so it must refuse it too. + assert!( + !matches!(c.detail.as_str(), "" | "yes" | "no" | "ok"), + "{} said only {:?}, which is the green tick this screen exists to avoid", + c.label, + c.detail + ); + } + } +} diff --git a/crates/raven-gui/src/theme.rs b/crates/raven-gui/src/theme.rs new file mode 100644 index 0000000..14aaa26 --- /dev/null +++ b/crates/raven-gui/src/theme.rs @@ -0,0 +1,64 @@ +//! What colony-ui needs to know about this program, and what it gives back. +//! +//! Every colour and every font size in the window comes through here. The org +//! rule is that a size is never a raw number: two independent user +//! preferences, Appearance and Accessibility, multiply into one scale, so the +//! layout has to survive 0.7225x to 1.68x, and `sz` is the only thing that +//! knows it. + +use colony_ui::{ThemePalette, Typography}; +use iced::Font; +use iced::widget::button; + +/// The org's application font. JetBrainsMono Nerd Font per +/// Project-Colony-Resources/design/typography.md. +pub const APP_FONT: Font = Font::with_name("JetBrainsMono Nerd Font"); + +/// The current palette, whichever theme the user picked. +pub fn palette() -> ThemePalette { + colony_ui::active_palette() +} + +/// The look of an ordinary button, for every ordinary button in the window. +/// +/// A button left without `.style()` does not fall back to nothing - it falls +/// back to iced's own theme, which paints its blue on colony-ui's ground. One +/// helper rather than a copy per call site, so the three button colours the +/// palette carries are used everywhere or nowhere. +/// +/// `Disabled` keeps the resting fill and dims the label: the palette has no +/// disabled token, and a button that vanishes when it cannot be pressed is +/// harder to find again than one that greys out. +pub fn button_style(p: ThemePalette) -> impl Fn(&iced::Theme, button::Status) -> button::Style { + move |_, status| button::Style { + background: Some( + match status { + button::Status::Hovered => p.btn_hover, + button::Status::Pressed => p.btn_pressed, + button::Status::Active | button::Status::Disabled => p.btn_default, + } + .into(), + ), + text_color: match status { + button::Status::Disabled => p.text_muted, + _ => p.text_primary, + }, + border: iced::Border { + radius: 4.0.into(), + ..Default::default() + }, + ..Default::default() + } +} + +/// The type scale. `scale` is 1.0 until the settings screen exists to change +/// it; the accessor is used everywhere from the start so adding that screen +/// later changes one function rather than every call site. +pub fn typography() -> Typography { + Typography { + scale: 1.0, + regular: APP_FONT, + medium: APP_FONT, + bold: APP_FONT, + } +} diff --git a/crates/raven-gui/src/view/bases.rs b/crates/raven-gui/src/view/bases.rs new file mode 100644 index 0000000..947b65e --- /dev/null +++ b/crates/raven-gui/src/view/bases.rs @@ -0,0 +1,179 @@ +//! Deployed bases, and the form and progress bar for deploying one more. +//! +//! Deploying is the only long operation in Raven - minutes of silence over +//! 143 886 files in the CLI - so this is the one screen with a +//! `progress_bar`: real feedback for the one wait a user cannot otherwise +//! tell from a hang. + +use iced::widget::{ + Space, button, column, container, progress_bar, row, scrollable, text, text_input, +}; +use iced::{Element, Length}; + +use crate::deploy::Progress; +use crate::model::BaseRow; +use crate::theme; +use crate::{DeployForm, Message}; + +pub fn screen<'a>( + bases: &'a [BaseRow], + deploying: Option<&'a Progress>, + form: &'a DeployForm, +) -> Element<'a, Message> { + let t = theme::typography(); + let p = theme::palette(); + + let list: Element<'_, Message> = if bases.is_empty() { + text("No bases yet") + .size(t.sz(13)) + .color(p.text_muted) + .into() + } else { + let cards = bases + .iter() + .fold(column![].spacing(t.sz(10)), |acc, b| acc.push(card(b))); + scrollable(cards).height(Length::Fill).into() + }; + + container( + column![ + text("Bases").size(t.sz(22)).color(p.text_primary), + Space::new().height(t.sz(8)), + list, + Space::new().height(t.sz(16)), + deploy_form(deploying, form), + ] + .spacing(t.sz(4)), + ) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn card(b: &BaseRow) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + let plural = if b.environments == 1 { + "environment" + } else { + "environments" + }; + + container( + column![ + text(b.id.clone()).size(t.sz(16)).color(p.text_primary), + text(format!("{} {plural}", b.environments)) + .size(t.sz(12)) + .color(p.text_muted), + ] + .spacing(t.sz(3)), + ) + .padding(t.sz(12) as u16) + .width(Length::Fill) + .style(move |_| container::Style { + background: Some(p.bg_card.into()), + border: iced::Border { + color: p.border_subtle, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() +} + +/// The three fields a deployment needs, and the Deploy button. Fields are +/// disabled while a deployment is running: `Subscription::run_with` keys on +/// them, so editing one mid-flight would restart the job rather than change +/// it. +fn deploy_form<'a>(deploying: Option<&'a Progress>, form: &'a DeployForm) -> Element<'a, Message> { + let t = theme::typography(); + let p = theme::palette(); + let busy = deploying.is_some(); + + let field = + move |placeholder: &'static str, value: &'a str, on_input: fn(String) -> Message| { + text_input(placeholder, value) + .size(t.sz(13)) + .padding(t.sz(8) as u16) + .on_input_maybe((!busy).then_some(on_input)) + .style(move |_, _| text_input::Style { + background: p.bg_input.into(), + border: iced::Border { + color: p.border_subtle, + width: 1.0, + radius: 4.0.into(), + }, + icon: p.text_muted, + placeholder: p.text_placeholder, + value: p.text_primary, + selection: p.accent_blue, + }) + }; + + let inputs = row![ + field( + "Path to install.wim", + &form.image, + Message::DeployImageChanged + ) + .width(Length::FillPortion(3)), + field( + "Edition index", + &form.edition, + Message::DeployEditionChanged + ) + .width(Length::FillPortion(1)), + field("Name", &form.name, Message::DeployNameChanged).width(Length::FillPortion(1)), + ] + .spacing(t.sz(8)); + + let deploy_button = button(text("Deploy").size(t.sz(13))).style(theme::button_style(p)); + let deploy_button = if busy { + deploy_button + } else { + deploy_button.on_press(Message::DeployStart) + }; + + let mut section = column![ + text("Deploy a base").size(t.sz(14)).color(p.text_secondary), + inputs, + deploy_button, + ] + .spacing(t.sz(8)); + + if let Some(progress) = deploying { + section = section.push( + row![ + progress_bar(0.0..=100.0, progress.percent as f32) + .girth(t.sz(10)) + .length(Length::FillPortion(4)) + .style(move |_| progress_bar::Style { + background: p.bg_progress.into(), + bar: p.accent_progress.into(), + border: iced::Border::default(), + }), + text(format!("{} - {}%", progress.what, progress.percent)) + .size(t.sz(12)) + .color(p.text_muted), + ] + .spacing(t.sz(8)) + .align_y(iced::Alignment::Center), + ); + } + + container(section) + .padding(t.sz(12) as u16) + .width(Length::Fill) + .style(move |_| container::Style { + background: Some(p.bg_card.into()), + border: iced::Border { + color: p.border_subtle, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() +} diff --git a/crates/raven-gui/src/view/detail.rs b/crates/raven-gui/src/view/detail.rs new file mode 100644 index 0000000..b946abf --- /dev/null +++ b/crates/raven-gui/src/view/detail.rs @@ -0,0 +1,127 @@ +//! One environment, in full: its session, its Direct3D runtimes, its devices +//! and its registry rules. + +use iced::widget::{Space, button, column, container, row, text}; +use iced::{Element, Length}; + +use crate::Message; +use crate::model::EnvRow; +use crate::theme; + +pub fn screen(e: &EnvRow) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + let heading = |s: &'static str| text(s).size(t.sz(14)).color(p.text_secondary); + + let session = column![ + heading("Session"), + text(e.status_line()).size(t.sz(13)).color(p.text_primary), + if e.is_running() { + button(text("Stop").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::Stop(e.name.clone())) + } else { + button(text("Start").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::Start(e.name.clone())) + }, + ] + .spacing(t.sz(6)); + + let runtime = |label: &'static str, build: &Option, vkd3d: bool| { + let name = e.name.clone(); + match build { + Some(v) => row![ + text(format!("{label}: {v}")) + .size(t.sz(13)) + .color(p.text_primary), + button(text("Remove").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::RemoveD3d { env: name, vkd3d }), + ], + None => row![ + text(format!("{label}: not installed")) + .size(t.sz(13)) + .color(p.text_muted), + button(text("Install…").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::InstallD3d { env: name, vkd3d }), + ], + } + .spacing(t.sz(8)) + .align_y(iced::Alignment::Center) + }; + + let d3d = column![ + heading("Direct3D"), + runtime("DXVK (D3D 8-11)", &e.dxvk, false), + runtime("vkd3d-proton (D3D 12)", &e.vkd3d, true), + text("They are separate runtimes, not versions of one.") + .size(t.sz(11)) + .color(p.text_muted), + ] + .spacing(t.sz(6)); + + let mut devices = column![heading("Devices")].spacing(t.sz(6)); + if e.attachments.is_empty() { + devices = devices.push( + text("None attached. Attach one with: raven env attach /dev/sdX") + .size(t.sz(12)) + .color(p.text_muted), + ); + } else { + for (letter, device) in &e.attachments { + devices = devices.push( + row![ + text(format!("{letter}: {}", device.display())) + .size(t.sz(13)) + .color(p.text_primary), + button(text("Detach").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::Detach { + env: e.name.clone(), + letter: *letter, + }), + ] + .spacing(t.sz(8)) + .align_y(iced::Alignment::Center), + ); + } + } + + let registry = column![ + heading("Registry"), + text("Re-run the projection after editing registry-rules.toml.") + .size(t.sz(12)) + .color(p.text_muted), + button(text("Reproject").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::Reproject(e.name.clone())), + ] + .spacing(t.sz(6)); + + container( + column![ + row![ + button(text("← Environments").size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(Message::Go(crate::Screen::Environments)), + Space::new().width(Length::Fill), + ], + text(e.name.clone()).size(t.sz(22)).color(p.text_primary), + text(e.base.clone()).size(t.sz(12)).color(p.text_muted), + Space::new().height(t.sz(12)), + session, + Space::new().height(t.sz(12)), + d3d, + Space::new().height(t.sz(12)), + devices, + Space::new().height(t.sz(12)), + registry, + ] + .spacing(t.sz(4)), + ) + .width(Length::Fill) + .into() +} diff --git a/crates/raven-gui/src/view/doctor.rs b/crates/raven-gui/src/view/doctor.rs new file mode 100644 index 0000000..d60dd53 --- /dev/null +++ b/crates/raven-gui/src/view/doctor.rs @@ -0,0 +1,68 @@ +//! Diagnostics: the same four judgements `raven doctor` prints, from the same +//! functions. A green tick and a red cross would throw away the only useful +//! half of what the CLI says - "absent - Wine falls back to wineserver for NT +//! synchronization" rather than "no" - so every row, passing or not, keeps +//! its consequence in the detail line. + +use iced::widget::{Space, column, container, scrollable, text}; +use iced::{Element, Length}; + +use crate::Message; +use crate::model::Check; +use crate::theme; + +pub fn screen(checks: &[Check]) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + let list: Element<'_, Message> = if checks.is_empty() { + text("No diagnostics yet") + .size(t.sz(13)) + .color(p.text_muted) + .into() + } else { + let cards = checks + .iter() + .fold(column![].spacing(t.sz(10)), |acc, c| acc.push(card(c))); + scrollable(cards).height(Length::Fill).into() + }; + + container( + column![ + text("Diagnostics").size(t.sz(22)).color(p.text_primary), + Space::new().height(t.sz(8)), + list, + ] + .spacing(t.sz(4)), + ) + .width(Length::Fill) + .height(Length::Fill) + .into() +} + +fn card(c: &Check) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + container( + column![ + text(c.label.clone()).size(t.sz(16)).color(p.text_primary), + text(c.detail.clone()) + .size(t.sz(12)) + .color(if c.ok { p.success } else { p.error }), + ] + .spacing(t.sz(3)), + ) + .padding(t.sz(12) as u16) + .width(Length::Fill) + .style(move |_| container::Style { + background: Some(p.bg_card.into()), + border: iced::Border { + color: p.border_subtle, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() +} diff --git a/crates/raven-gui/src/view/environments.rs b/crates/raven-gui/src/view/environments.rs new file mode 100644 index 0000000..7a0f5af --- /dev/null +++ b/crates/raven-gui/src/view/environments.rs @@ -0,0 +1,107 @@ +//! One card per environment. Start and Stop sit on the card because starting +//! is the most frequent action and `env start` is what makes launches instant. + +use iced::widget::{Space, button, column, container, row, scrollable, text}; +use iced::{Element, Length}; + +use crate::Message; +use crate::model::EnvRow; +use crate::theme; + +pub fn screen(envs: &[EnvRow]) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + if envs.is_empty() { + return column![ + text("No environments yet") + .size(t.sz(22)) + .color(p.text_primary), + Space::new().height(t.sz(8)), + text("Create one with: raven env create --base ") + .size(t.sz(13)) + .color(p.text_muted), + ] + .into(); + } + + let cards = envs + .iter() + .fold(column![].spacing(t.sz(10)), |acc, e| acc.push(card(e))); + + scrollable(cards).height(Length::Fill).into() +} + +fn card(e: &EnvRow) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + let action = if e.is_running() { + button(text("Stop").size(t.sz(13))) + .style(theme::button_style(p)) + .on_press(Message::Stop(e.name.clone())) + } else { + button(text("Start").size(t.sz(13))) + .style(theme::button_style(p)) + .on_press(Message::Start(e.name.clone())) + }; + + let mut runtimes = row![].spacing(t.sz(8)); + if let Some(v) = &e.dxvk { + runtimes = runtimes.push(text(v.clone()).size(t.sz(11)).color(p.text_muted)); + } + if let Some(v) = &e.vkd3d { + runtimes = runtimes.push(text(v.clone()).size(t.sz(11)).color(p.text_muted)); + } + for (letter, device) in &e.attachments { + runtimes = runtimes.push( + text(format!("{letter}: {}", device.display())) + .size(t.sz(11)) + .color(p.text_muted), + ); + } + + // The name is the card's own heading, not a control beside it, so it is + // drawn as text and only behaves like a button. No fill, and its colour is + // stated rather than left to `Style::default()`, which is black. + let name = button(text(e.name.clone()).size(t.sz(16)).color(p.text_primary)) + .style(move |_, _| button::Style { + background: None, + text_color: p.text_primary, + ..Default::default() + }) + .padding(0.0) + .on_press(Message::Open(e.name.clone())); + + container( + row![ + column![ + name, + text(e.base.clone()).size(t.sz(11)).color(p.text_muted), + text(e.status_line()) + .size(t.sz(12)) + .color(if e.is_running() { + p.success + } else { + p.text_muted + }), + runtimes, + ] + .spacing(t.sz(3)) + .width(Length::Fill), + action, + ] + .align_y(iced::Alignment::Center), + ) + .padding(t.sz(12) as u16) + .style(move |_| container::Style { + background: Some(p.bg_card.into()), + border: iced::Border { + color: p.border_subtle, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() +} diff --git a/crates/raven-gui/src/view/mod.rs b/crates/raven-gui/src/view/mod.rs new file mode 100644 index 0000000..3ee1b97 --- /dev/null +++ b/crates/raven-gui/src/view/mod.rs @@ -0,0 +1,129 @@ +//! Drawing. Nothing in here decides anything - `update` does that. + +pub mod bases; +pub mod detail; +pub mod doctor; +pub mod environments; + +use iced::widget::{Space, button, column, container, row, text}; +use iced::{Element, Length}; + +use crate::theme; +use crate::{App, Message, Screen}; + +/// The sidebar and whichever screen is selected. +pub fn shell(app: &App) -> Element<'_, Message> { + let t = theme::typography(); + let p = theme::palette(); + + // `Screen::Detail` carries a `String`, so `Screen` is no longer `Copy`; + // `current` is taken by reference so the same `&app.screen` can feed all + // three calls below, and `selected` is settled before the `move` closure + // so `screen` stays available afterward for `on_press`. + let item = |label: &'static str, screen: Screen, current: &Screen| { + // An environment's detail is the Environments screen one level down: + // `Open` is `Go(Detail)` and its back button returns here. So that + // entry stays lit while a detail shows, rather than the whole sidebar + // going dark on the most common navigation in the window. + let selected = match (&screen, current) { + (Screen::Environments, Screen::Detail(_)) => true, + _ => screen == *current, + }; + button(text(label).size(t.sz(13))) + .width(Length::Fill) + .style(move |_, _| button::Style { + background: Some( + if selected { + p.bg_selected + } else { + p.bg_sidebar + } + .into(), + ), + text_color: if selected { + p.accent_blue + } else { + p.text_secondary + }, + ..Default::default() + }) + .on_press(Message::Go(screen)) + }; + + let sidebar = container( + column![ + text("Raven").size(t.sz(20)).color(p.text_primary), + Space::new().height(t.sz(12)), + item("Environments", Screen::Environments, &app.screen), + item("Bases", Screen::Bases, &app.screen), + item("Diagnostics", Screen::Doctor, &app.screen), + ] + .spacing(t.sz(4)), + ) + .width(Length::Fixed(t.sz(180))) + .height(Length::Fill) + .padding(t.sz(12) as u16) + .style(move |_| container::Style { + background: Some(p.bg_sidebar.into()), + ..Default::default() + }); + + let body = match &app.screen { + Screen::Environments => environments::screen(&app.envs), + Screen::Detail(name) => match app.envs.iter().find(|e| &e.name == name) { + Some(e) => detail::screen(e), + // The environment was destroyed from elsewhere between the click and + // the draw. Falling back is better than an empty page. + None => environments::screen(&app.envs), + }, + Screen::Bases => bases::screen(&app.bases, app.deploying.as_ref(), &app.deploy_form), + Screen::Doctor => doctor::screen(&app.checks), + }; + + let banner: Element<'_, Message> = match &app.offer { + None => Space::new().height(0).into(), + Some(offer) => { + // A notice is guidance the window cannot carry out itself, not a + // failure, and painting it in the error colours would tell the user + // something went wrong when nothing did. + let (ink, ground) = match offer.kind { + crate::errors::Kind::Notice => (p.text_primary, p.bg_modal_section), + _ => (p.error, p.error_bg), + }; + let mut r = row![text(offer.message.clone()).size(t.sz(13)).color(ink)] + .spacing(t.sz(8)) + .align_y(iced::Alignment::Center); + // Both actions stop the environment; only the words differ, and + // they are the action's own so the view cannot mislabel one. + if let Some(action) = &offer.action { + r = r.push( + button(text(action.label()).size(t.sz(12))) + .style(theme::button_style(p)) + .on_press(action.message()), + ); + } + container(r) + .padding(t.sz(10) as u16) + .width(Length::Fill) + .style(move |_| container::Style { + background: Some(ground.into()), + ..Default::default() + }) + .into() + } + }; + + container(row![ + sidebar, + container(column![banner, body].spacing(t.sz(8))) + .padding(t.sz(16) as u16) + .width(Length::Fill) + ]) + .width(Length::Fill) + .height(Length::Fill) + .style(move |_| container::Style { + background: Some(p.bg_primary.into()), + ..Default::default() + }) + .into() +} diff --git a/crates/raven-gui/tests/helper_verbs.rs b/crates/raven-gui/tests/helper_verbs.rs new file mode 100644 index 0000000..2847d5e --- /dev/null +++ b/crates/raven-gui/tests/helper_verbs.rs @@ -0,0 +1,78 @@ +//! The window must never open a window when it was not asked for one. +//! +//! The library re-runs its own binary for two things - `session-anchor` and +//! the `exec` behind a registry import - so whichever binary asked has to +//! answer, or answer plainly that it cannot. Falling through to iced would +//! put a second, unexplained Raven window on the user's screen and hand the +//! caller a success it did not earn. +//! +//! Every case runs with the environment cleared, so no display exists for a +//! window to open on even if one were attempted. + +use std::process::{Command, Stdio}; + +fn run(args: &[&str]) -> (Option, String, String) { + let home = std::env::temp_dir().join(format!( + "raven-gui-verbs-{}-{}", + std::process::id(), + args.join("_").replace('/', "_") + )); + std::fs::create_dir_all(&home).unwrap(); + let out = Command::new(env!("CARGO_BIN_EXE_raven-gui")) + .args(args) + .env_clear() + .env("HOME", &home) + .env("PATH", "/usr/bin:/bin") + .stdin(Stdio::null()) + .output() + .expect("raven-gui starts"); + let _ = std::fs::remove_dir_all(&home); + ( + out.status.code(), + String::from_utf8_lossy(&out.stdout).into_owned(), + String::from_utf8_lossy(&out.stderr).into_owned(), + ) +} + +#[test] +fn the_exec_verb_is_refused_in_words_rather_than_with_a_window() { + // `registry::import` re-runs `current_exe() exec --lower ... -- `. + // The window cannot mount and exec - it would have to replace itself - + // so it must say so on stdout, which is the only stream the caller reads. + let (code, stdout, stderr) = run(&[ + "exec", + "--lower", + "/nonexistent/lower", + "--upper", + "/nonexistent/upper", + "--work", + "/nonexistent/work", + "--target", + "/nonexistent/target", + "--", + "/bin/true", + ]); + assert_eq!( + code, + Some(1), + "must fail, and visibly.\nstdout: {stdout:?}\nstderr: {stderr:?}" + ); + assert!( + !stderr.contains("Create event loop"), + "a window was attempted for a verb that is not about windows: {stderr:?}" + ); + assert!( + stdout.contains("raven"), + "the caller reads stdout and must be told which binary to run: {stdout:?}" + ); +} + +#[test] +fn an_argument_the_window_does_not_know_never_reaches_iced() { + let (code, stdout, stderr) = run(&["--frobnicate"]); + assert_eq!(code, Some(1), "stdout: {stdout:?}\nstderr: {stderr:?}"); + assert!( + !stderr.contains("Create event loop"), + "an unknown argument must not open a window: {stderr:?}" + ); +} diff --git a/crates/raven-gui/tests/session_anchor.rs b/crates/raven-gui/tests/session_anchor.rs new file mode 100644 index 0000000..363989b --- /dev/null +++ b/crates/raven-gui/tests/session_anchor.rs @@ -0,0 +1,39 @@ +//! The window can be its own session anchor. +//! +//! `Environment::ensure_session` starts the anchor as `current_exe() +//! session-anchor `, so whichever binary asked for a session has to +//! answer to that argument - the window included, since Start asks. +//! +//! The environment is cleared so that `DISPLAY`, `WAYLAND_DISPLAY` and +//! `WAYLAND_SOCKET` are all unset: whatever this binary does with the +//! argument, it cannot open a window on the machine running the tests. + +use std::process::{Command, Stdio}; + +#[test] +fn session_anchor_is_answered_before_any_window_is_attempted() { + let home = std::env::temp_dir().join(format!("raven-gui-anchor-{}", std::process::id())); + std::fs::create_dir_all(&home).unwrap(); + let out = Command::new(env!("CARGO_BIN_EXE_raven-gui")) + .args(["session-anchor", "nope"]) + .env_clear() + .env("HOME", &home) + .env("PATH", "/usr/bin:/bin") + .stdin(Stdio::null()) + .output() + .expect("raven-gui starts"); + let _ = std::fs::remove_dir_all(&home); + let stdout = String::from_utf8_lossy(&out.stdout); + let stderr = String::from_utf8_lossy(&out.stderr); + // The anchor protocol: every failure leaves through stdout with status + // 1, and an environment that does not exist is the first it can hit. + assert_eq!( + out.status.code(), + Some(1), + "stdout: {stdout:?}\nstderr: {stderr:?}" + ); + assert!( + stdout.contains("no environment called \"nope\""), + "the anchor protocol was not answered on stdout.\nstdout: {stdout:?}\nstderr: {stderr:?}" + ); +} diff --git a/crates/raven/Cargo.toml b/crates/raven/Cargo.toml new file mode 100644 index 0000000..b976314 --- /dev/null +++ b/crates/raven/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "raven" +description = "Run Windows programs on Linux against a real Windows installation mounted as C:" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1.0.104" +clap = { version = "4.6.6", features = ["derive"] } +nt-hive = "0.3.0" +rustix = { version = "1.1.4", features = ["mount", "thread", "process", "fs"] } +serde = { version = "1.0.229", features = ["derive"] } +thiserror = "2.0.20" +toml = "1.1.4" + +[dev-dependencies] +regf = "0.1.0" diff --git a/examples/project_real_base.rs b/crates/raven/examples/project_real_base.rs similarity index 100% rename from examples/project_real_base.rs rename to crates/raven/examples/project_real_base.rs diff --git a/src/attach.rs b/crates/raven/src/attach.rs similarity index 93% rename from src/attach.rs rename to crates/raven/src/attach.rs index 3602d61..7702c0a 100644 --- a/src/attach.rs +++ b/crates/raven/src/attach.rs @@ -145,8 +145,13 @@ impl Environment { // Either half alone still counts: a detach that failed midway must // be re-runnable until nothing is left. + // Only entries Raven wrote are Raven's to remove. `attach` refuses a + // letter that already carries any mapping, so a `"d:"="cdrom"` the + // user made in winecfg was never ours - and deleting it here would + // be the destruction `attach` went out of its way to avoid. + // `disk_letters` matches exactly the form `attach` writes. let target = std::fs::read_link(&raw).ok(); - if target.is_none() && !has_drive_entry(&text, letter) { + if target.is_none() && !disk_letters(&text).contains(&letter) { return Err(Error::NotAttached(letter)); } let updated = edit_drives_section(&text, letter, None); @@ -161,11 +166,17 @@ impl Environment { // therefore here. First drop every link that belongs to an // attachment (or to the device just detached), then re-wire. let mut owned: Vec = target.into_iter().collect(); - let survivors: Vec<(char, PathBuf)> = disk_letters(&updated) + // Rank among *every* remaining disk, not among the ones with a device + // behind them: mountmgr counts an entry whether or not anything is + // wired to it, which is what `attach` and `attachments` count too. A + // rank taken from the linked subset would wire a survivor to a number + // naming a different disk. + let survivors: Vec<(usize, PathBuf)> = disk_letters(&updated) .iter() - .filter_map(|&l| { + .enumerate() + .filter_map(|(rank, &l)| { let t = std::fs::read_link(dos.join(format!("{l}::"))).ok()?; - Some((l, t)) + Some((rank + 1, t)) }) .collect(); owned.extend(survivors.iter().map(|(_, t)| t.clone())); @@ -181,8 +192,8 @@ impl Environment { } } } - for (rank, (_, t)) in survivors.iter().enumerate() { - let phys = dos.join(format!("physicaldrive{}", rank + 1)); + for (number, t) in &survivors { + let phys = dos.join(format!("physicaldrive{number}")); std::os::unix::fs::symlink(t, &phys).map_err(|e| Error::Layer(phys, e))?; } diff --git a/src/base.rs b/crates/raven/src/base.rs similarity index 71% rename from src/base.rs rename to crates/raven/src/base.rs index 5f2febe..445365f 100644 --- a/src/base.rs +++ b/crates/raven/src/base.rs @@ -39,6 +39,7 @@ impl Base { .map_err(|e| Error::Layer(dir.clone(), e))? .filter_map(Result::ok) .filter(|e| e.path().is_dir()) + .filter(|e| is_base_dir(&e.file_name().to_string_lossy())) .map(|e| Base { id: e.file_name().to_string_lossy().into_owned(), path: e.path(), @@ -50,7 +51,7 @@ impl Base { pub fn find(id: &str) -> Result { let path = paths::bases_dir()?.join(paths::check_name(id)?); - if !path.is_dir() { + if !is_base_dir(id) || !path.is_dir() { return Err(Error::NoSuchBase(id.to_owned())); } Ok(Base { @@ -129,35 +130,88 @@ fn parse_editions(text: &str) -> Vec { /// Refuses to touch a base that already exists: bases are immutable, and /// "deploy over the top" is how an immutable thing quietly stops being one. pub fn deploy(image: &Path, index: u32, id: &str) -> Result { - let dir = paths::bases_dir()?.join(paths::check_name(id)?); + let bases = paths::bases_dir()?; + let dir = bases.join(paths::check_name(id)?); if dir.exists() { return Err(Error::BaseExists(id.to_owned())); } - std::fs::create_dir_all(&dir).map_err(|e| Error::Layer(dir.clone(), e))?; + // Applied beside the real name and renamed only once it is whole. Ten + // minutes over 143 886 files is long enough to be interrupted, and the + // cleanup below cannot run when the interruption is the process dying: + // what was left then was a directory holding half a Windows under the + // name of a finished one, which `deploy` refused to overwrite and no + // command could remove. + let partial = bases.join(partial_name(id)); + let _ = std::fs::remove_dir_all(&partial); + std::fs::create_dir_all(&partial).map_err(|e| Error::Layer(partial.clone(), e))?; let status = Command::new("wimlib-imagex") .arg("apply") .arg(image) .arg(index.to_string()) - .arg(&dir) + .arg(&partial) .status() .map_err(|e| Error::Tool("wimlib-imagex", e))?; if !status.success() { - // A half-applied base is worse than none: it looks deployable. - let _ = std::fs::remove_dir_all(&dir); + let _ = std::fs::remove_dir_all(&partial); return Err(Error::ToolFailed( "wimlib-imagex apply", format!("exited with {status}"), )); } - let base = Base { + // Before the rename, so the name only ever appears on a base whose + // junctions already resolve inside it. + repoint_absolute_symlinks(&partial)?; + std::fs::rename(&partial, &dir).map_err(|e| Error::Layer(dir.clone(), e))?; + Ok(Base { id: id.to_owned(), path: dir, - }; - repoint_absolute_symlinks(&base.path)?; - Ok(base) + }) +} + +/// The prefix a deploy works under before it has earned the real name. +/// +/// A leading dot so the directory is hidden, and a word after it so the rule +/// that skips it is exact: a name is only ever hidden from `list` when Raven +/// wrote it, never because a user chose one starting with a dot. +const PARTIAL: &str = ".partial-"; + +/// The deploys that were interrupted, and the directories they left. +/// +/// Applying an image into a partial directory means an interrupted deploy +/// leaves one behind - several gigabytes of it - and hiding it from `list` +/// would hide it from the user too. `raven doctor` reports these so the +/// space is accountable; the next deploy of the same id clears one on its +/// own. +pub fn partials() -> Result, Error> { + let dir = paths::bases_dir()?; + if !dir.exists() { + return Ok(Vec::new()); + } + let mut out: Vec<(String, PathBuf)> = std::fs::read_dir(&dir) + .map_err(|e| Error::Layer(dir.clone(), e))? + .filter_map(Result::ok) + .filter(|e| e.path().is_dir()) + .filter_map(|e| { + let name = e.file_name().to_string_lossy().into_owned(); + name.strip_prefix(PARTIAL) + .map(|id| (id.to_owned(), e.path())) + }) + .collect(); + out.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(out) +} + +/// Where a deploy applies before it has earned the real name. +fn partial_name(id: &str) -> String { + format!("{PARTIAL}{id}") +} + +/// Whether a directory in the bases folder is a deployed Windows. +fn is_base_dir(name: &str) -> bool { + !name.starts_with(PARTIAL) } /// Rewrites the reparse points a WIM leaves behind as absolute symlinks. @@ -217,6 +271,24 @@ fn relative_to(target: &Path, from: &Path) -> Option { (!rel.as_os_str().is_empty()).then_some(rel) } +#[cfg(test)] +mod partial_tests { + use super::{is_base_dir, partial_name}; + + #[test] + fn a_half_applied_base_is_never_mistaken_for_a_finished_one() { + // A deploy applies into the partial name and renames only when it + // has finished, so an interrupted one leaves that directory behind. + // It must not be listed, offered to `env create`, or block a retry. + assert_eq!(partial_name("win11-26200-pro"), ".partial-win11-26200-pro"); + assert!(!is_base_dir(&partial_name("win11-26200-pro"))); + assert!(is_base_dir("win11-26200-pro")); + // Only Raven's own working directories are skipped. A base a user + // named with a leading dot was listed before this and still is. + assert!(is_base_dir(".anything")); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/d3d.rs b/crates/raven/src/d3d.rs similarity index 84% rename from src/d3d.rs rename to crates/raven/src/d3d.rs index 88fd428..2fe42da 100644 --- a/src/d3d.rs +++ b/crates/raven/src/d3d.rs @@ -96,7 +96,7 @@ impl Environment { pub fn install_d3d(&self, rt: &Runtime, source: &Path) -> Result, Error> { self.ensure_not_running()?; let (dir, _keep) = unpack(source)?; - let root = build_root(&dir)?; + let root = build_root(&dir, rt.key)?; // Plan every copy before performing any of it. Checking as we went // meant a refusal left the copies already made behind, and those then @@ -132,8 +132,14 @@ impl Environment { } let mut written: Vec = Vec::new(); + let mut created: Vec = Vec::new(); let mut done = Vec::new(); for (from, to, rel, dll, arch) in &plan { + // Whether this call is the reason the file is there. An upgrade + // copies over the previous install's libraries, and undoing that + // by deleting them would turn a failed upgrade into an uninstall + // of the build that was working. + let is_new = !to.exists(); let copy = (|| { if let Some(parent) = to.parent() { std::fs::create_dir_all(parent)?; @@ -143,11 +149,28 @@ impl Environment { if let Err(e) = copy { // Undo the half-install rather than leave the environment in a // state neither `dxvk` nor `--remove` can describe. - for r in &written { + for r in &created { let _ = std::fs::remove_file(self.upper().join(r)); } + // An upgrade has by now overwritten some of the previous + // build's libraries in place, and those are not undone - + // deleting them would uninstall what was working. What is + // left is genuinely neither version, so the record stops + // naming one: `dxvk` says so, and reinstalling either build + // puts it right. + if !ours.is_empty() { + let was = self.d3d_build(rt).unwrap_or_else(|| rt.key.to_string()); + let _ = write_manifest( + &self.d3d_manifest_path(rt), + &format!("{was} - interrupted upgrade, reinstall to settle it"), + &ours, + ); + } return Err(Error::Layer(to.clone(), e)); } + if is_new { + created.push(rel.clone()); + } written.push(rel.clone()); done.push(Shadow { dll: (*dll).to_string(), @@ -159,6 +182,24 @@ impl Environment { written.sort(); written.dedup(); + // The record goes down as soon as the files exist, and describes a + // superset of them from here on. Everything below can fail - the + // superseded sweep, reading and rewriting user.reg - and until this + // was written first, a failure there left libraries in the upper + // layer that `dxvk` could not see, `--remove` could not remove, and + // the next install refused as somebody else's, telling the user to + // move aside files Raven had put there itself. `d3d` already skips + // manifest entries that are not files, so a superset is harmless. + let version = root + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "unknown".into()); + let m = self.d3d_manifest_path(rt); + let mut described: Vec = written.iter().chain(ours.iter()).cloned().collect(); + described.sort(); + described.dedup(); + write_manifest(&m, &version, &described)?; + // Installing over an older build is the normal way to update, and // upstream drops modules between versions - d3d10.dll went that way. // Anything the previous install left that this one does not replace is @@ -192,16 +233,9 @@ impl Environment { } text::write_atomic(®, &text)?; - // The build's own directory name is the only version DXVK ships in a - // release, and "which DXVK do I have" is the first question after "is - // it installed" - so it is recorded rather than left to be guessed. - let version = root - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| "unknown".into()); - let m = self.d3d_manifest_path(rt); - let body = format!("#build {version}\n{}\n", written.join("\n")); - std::fs::write(&m, body).map_err(|e| Error::Layer(m, e))?; + // Narrowed to exactly what is installed now that the superseded + // files are gone and the overrides agree with them. + write_manifest(&m, &version, &written)?; Ok(done) } @@ -311,6 +345,14 @@ fn unique_dlls(done: &[Shadow]) -> Vec { names } +/// Records which libraries Raven has put in the environment, and which build +/// they came from - "which DXVK do I have" being the first question after +/// "is one installed". +fn write_manifest(path: &std::path::Path, version: &str, files: &[String]) -> Result<(), Error> { + let body = format!("#build {version}\n{}\n", files.join("\n")); + std::fs::write(path, body).map_err(|e| Error::Layer(path.to_path_buf(), e)) +} + /// A directory holding the DXVK build, plus a guard that deletes it again if we /// created it by extracting an archive. fn unpack(source: &Path) -> Result<(PathBuf, Option), Error> { @@ -347,7 +389,7 @@ fn unpack(source: &Path) -> Result<(PathBuf, Option), Error> { /// Finds the directory that actually holds `x64/`, so both a release tarball /// (which nests everything under `dxvk-/`) and an already-extracted /// build work without the caller having to know which they have. -fn build_root(dir: &Path) -> Result { +fn build_root(dir: &Path, key: &'static str) -> Result { if dir.join("x64").is_dir() { return Ok(dir.to_path_buf()); } @@ -359,7 +401,7 @@ fn build_root(dir: &Path) -> Result { } } } - Err(Error::NotAD3dBuild("", dir.to_path_buf())) + Err(Error::NotAD3dBuild(key, dir.to_path_buf())) } struct TempDir(PathBuf); @@ -379,10 +421,10 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); // A release tarball extracts to dxvk-2.7/x64, not to x64. std::fs::create_dir_all(dir.join("dxvk-2.7/x64")).unwrap(); - assert_eq!(build_root(&dir).unwrap(), dir.join("dxvk-2.7")); + assert_eq!(build_root(&dir, "dxvk").unwrap(), dir.join("dxvk-2.7")); // An already-extracted build works too. assert_eq!( - build_root(&dir.join("dxvk-2.7")).unwrap(), + build_root(&dir.join("dxvk-2.7"), "dxvk").unwrap(), dir.join("dxvk-2.7") ); let _ = std::fs::remove_dir_all(&dir); @@ -393,7 +435,10 @@ mod tests { let dir = std::env::temp_dir().join(format!("raven-dxvkbad-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(dir.join("lib")).unwrap(); - assert!(matches!(build_root(&dir), Err(Error::NotAD3dBuild(_, _)))); + assert!(matches!( + build_root(&dir, "dxvk"), + Err(Error::NotAD3dBuild("dxvk", _)) + )); let _ = std::fs::remove_dir_all(&dir); } diff --git a/src/env.rs b/crates/raven/src/env.rs similarity index 80% rename from src/env.rs rename to crates/raven/src/env.rs index bd39172..45a3ff8 100644 --- a/src/env.rs +++ b/crates/raven/src/env.rs @@ -49,11 +49,17 @@ impl Environment { self.root.join("registry-rules.toml") } + /// An environment created before the file existed has none, and the + /// defaults are the right answer for it. Any other failure to read is + /// reported: the defaults are the *wider* set, so quietly substituting + /// them for a file someone narrowed by hand projects more of the base's + /// registry than its author allowed - and says nothing. pub fn rules(&self) -> Result { match std::fs::read_to_string(self.rules_file()) { Ok(text) => registry::Rules::parse(&text) .map_err(|e| Error::Manifest(self.rules_file(), e.to_string())), - Err(_) => Ok(registry::Rules::default()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(registry::Rules::default()), + Err(e) => Err(Error::Layer(self.rules_file(), e)), } } @@ -133,7 +139,7 @@ impl Environment { // ensure_not_running, because the environment may not open, and // that is exactly how it came to be the one command still saying // "still running - held by raven". - if holders.iter().all(|h| h.comm == "raven") { + if holders.iter().all(|h| h.anchor) { return Err(Error::SessionHolds(name.to_owned())); } return Err(Error::EnvironmentBusy { @@ -155,6 +161,21 @@ impl Environment { holders_of(&self.upper()) } + /// Whether one particular process is holding this environment's C:. + /// + /// `holders` asks that of every process on the machine; this asks it of + /// one. The difference is the launch path's whole cost: validating the + /// recorded anchor is a question about a single pid, and answering it by + /// scanning `/proc` took all but about 2 ms of a 32 ms warm launch on a + /// 454-process machine - the scan *was* the launch. Reading the one + /// process's `mountinfo` gives the same answer. + pub fn holds(&self, pid: u32) -> bool { + let needle = mountinfo_needle(&self.upper()); + std::fs::read_to_string(format!("/proc/{pid}/mountinfo")) + .map(|mi| mi.contains(&needle)) + .unwrap_or(false) + } + /// Refuses while the environment is held by live processes. /// /// overlayfs will not mount the same upper layer twice, so a second @@ -168,7 +189,7 @@ impl Environment { // A session anchor holding it is the ordinary state after a launch, // not a stuck process, and saying "still running - held by raven" // reads like a bug in Raven rather than the thing the user asked for. - if holders.iter().all(|h| h.comm == "raven") { + if holders.iter().all(|h| h.anchor) { return Err(Error::SessionHolds(self.name.clone())); } Err(Error::EnvironmentBusy { @@ -212,6 +233,12 @@ impl Environment { pub struct Holder { pub pid: u32, pub comm: String, + /// Whether this is Raven's own session anchor, recognised by what it is + /// running - ` session-anchor ` - and not by its name. + /// The anchor is whichever binary asked for the session, so the name is + /// `raven` from the launcher and `raven-gui` from the window, and a + /// test on the name called the window's own session a foreign program. + pub anchor: bool, } fn describe(holders: &[Holder]) -> String { @@ -260,12 +287,21 @@ fn holders_of(upper: &std::path::Path) -> Vec { let comm = std::fs::read_to_string(entry.path().join("comm")) .map(|s| s.trim().to_owned()) .unwrap_or_else(|_| "?".to_owned()); - held.push(Holder { pid, comm }); + let anchor = std::fs::read(entry.path().join("cmdline")) + .map(|c| is_anchor_cmdline(&c)) + .unwrap_or(false); + held.push(Holder { pid, comm, anchor }); } } held } +/// Whether a `/proc//cmdline` - NUL-separated argv - is a session +/// anchor's: its first argument is `session-anchor`, whatever the binary. +fn is_anchor_cmdline(cmdline: &[u8]) -> bool { + cmdline.split(|b| *b == 0).nth(1) == Some(b"session-anchor".as_slice()) +} + /// What this upper directory looks like inside `/proc//mountinfo`. /// /// Two escapings stack, and both matter. The option string handed to @@ -393,11 +429,51 @@ pub fn create(name: &str, base_id: &str) -> Result { })(); if built.is_err() { - let _ = std::fs::remove_dir_all(&root); + // `remove_tree`, not `remove_dir_all`: creating an environment mounts + // it - `project_registry` does - and overlayfs leaves a `work/work` + // with no permissions at all, which stops a plain removal after it + // has already deleted the upper layer. The wreck that leaves is + // refused by a second `create` and by every command that opens it. + let _ = remove_tree(&root); } built } +#[cfg(test)] +mod rules_tests { + use super::{Environment, Manifest}; + + #[test] + fn a_rules_file_that_cannot_be_read_is_not_silently_replaced() { + // The default rules are the *wider* set, so falling back to them + // when a hand-narrowed file cannot be read projects more of the + // base's registry than its author allowed - the opposite of the + // conservative direction, and invisible. + let root = std::env::temp_dir().join(format!("raven-rules-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + let env = Environment { + name: "rules-test".into(), + manifest: Manifest { + base: "none".into(), + }, + root: root.clone(), + }; + + // Absent is the one case that legitimately means "the defaults". + assert!(env.rules().is_ok(), "no file means the default rules"); + + // Not valid UTF-8: an editor saving a comment in Latin-1 is enough. + std::fs::write(env.rules_file(), [0xff, 0xfe, 0x41]).unwrap(); + assert!( + env.rules().is_err(), + "a rules file that exists and cannot be read must be reported" + ); + + let _ = std::fs::remove_dir_all(&root); + } +} + #[cfg(test)] mod tests { use super::*; @@ -507,3 +583,26 @@ mod tests { assert!(env.layer().ends_with("layer")); } } + +#[cfg(test)] +mod anchor_tests { + use super::is_anchor_cmdline; + + #[test] + fn the_anchor_is_recognised_by_what_it_runs_not_by_its_name() { + // Whichever binary asked for the session becomes its anchor, so the + // name is `raven` from the launcher and `raven-gui` from the window. + assert!(is_anchor_cmdline( + b"/usr/bin/raven\0session-anchor\0games\0" + )); + assert!(is_anchor_cmdline( + b"/usr/bin/raven-gui\0session-anchor\0games\0" + )); + // A launcher joining the session, and the programs inside it, are not. + assert!(!is_anchor_cmdline( + b"/usr/bin/raven\0run\0games\0--\0wine\0a.exe\0" + )); + assert!(!is_anchor_cmdline(b"wineserver\0")); + assert!(!is_anchor_cmdline(b"")); + } +} diff --git a/src/launch.rs b/crates/raven/src/launch.rs similarity index 74% rename from src/launch.rs rename to crates/raven/src/launch.rs index 97ca061..6a0ed17 100644 --- a/src/launch.rs +++ b/crates/raven/src/launch.rs @@ -170,6 +170,45 @@ pub fn resolve(exe: &Path) -> Result { } } +/// The program to run and the directory to run it from, both absolute. +/// +/// `run` joins the session's mount namespace before it execs, and +/// `setns(CLONE_NEWNS)` resets the process's working directory to that +/// namespace's root. So every relative path is resolved against `/` by the +/// time wine sees it - `demo/game.exe` is looked for at `/demo/game.exe`, +/// and a bare `game.exe`, whose directory used to be left unset, at +/// `/game.exe`. Settling both here, while the caller's directory is still +/// ours, is what makes a relative path mean what the user meant. +/// +/// Lexical, not `canonicalize`: a program inside a live environment is under +/// a mount point that exists only in the anchor's namespace, so resolving +/// through the filesystem would fail for exactly the paths that work. +pub fn target(exe: &Path) -> Result<(PathBuf, PathBuf), Error> { + // An absolute path needs no directory to be resolved against, and asking + // for one would fail where the caller's own directory has been deleted - + // which a file manager launching by absolute path should not care about. + if exe.is_absolute() { + return Ok(resolve_against(Path::new("/"), exe)); + } + let here = std::env::current_dir().map_err(|e| Error::Tool("raven", e))?; + Ok(resolve_against(&here, exe)) +} + +fn resolve_against(here: &Path, exe: &Path) -> (PathBuf, PathBuf) { + let mut full = PathBuf::new(); + // `Component::CurDir` is dropped by pushing components; `..` is kept as + // written rather than collapsed, since collapsing it would change which + // directory a symlink means. + for c in here.join(exe).components() { + match c { + std::path::Component::CurDir => {} + other => full.push(other), + } + } + let dir = full.parent().unwrap_or(here).to_path_buf(); + (full, dir) +} + /// The environment used for programs that are not inside one. pub fn default_environment() -> Result, Error> { let file = paths::config_dir()?.join("default-environment"); @@ -189,11 +228,74 @@ pub fn set_default_environment(name: &str) -> Result<(), Error> { std::fs::write(&file, name).map_err(|e| Error::Layer(file, e)) } -/// Where the packaged registration file belongs. +/// Where a hand-written registration belongs, for a build that no package +/// installed. +/// +/// Not where the *package* puts its own: that goes to +/// `/usr/lib/binfmt.d/raven.conf`, the directory for files a package owns, +/// and `/etc` deliberately takes precedence over it - the same mechanism +/// `packaging/wine-mask.conf` uses to shadow Wine's registration. So this +/// path is right for `raven binfmt`, which prints what to install when +/// nothing has installed it, and would silently override the package if it +/// were used on a system that has one. pub fn conf_path() -> PathBuf { PathBuf::from("/etc/binfmt.d/raven.conf") } +#[cfg(test)] +mod target_tests { + use super::resolve_against; + use std::path::Path; + + #[test] + fn a_relative_program_is_resolved_before_the_namespace_moves_underneath_it() { + // `run` joins the session's mount namespace before it execs, and + // `setns(CLONE_NEWNS)` resets the working directory to that + // namespace's root. Anything still relative by then is looked for + // under `/`, so every form has to be settled here, against the + // directory the user actually typed it in. + let here = Path::new("/home/u/games"); + assert_eq!( + resolve_against(here, Path::new("demo/game.exe")), + ( + Path::new("/home/u/games/demo/game.exe").to_path_buf(), + Path::new("/home/u/games/demo").to_path_buf() + ) + ); + // A bare name used to leave the directory unset, which after the + // join means `/` - the program's own files would be looked for at + // the root of the mount. + assert_eq!( + resolve_against(here, Path::new("game.exe")), + ( + Path::new("/home/u/games/game.exe").to_path_buf(), + here.to_path_buf() + ) + ); + assert_eq!( + resolve_against(here, Path::new("./game.exe")), + ( + Path::new("/home/u/games/game.exe").to_path_buf(), + here.to_path_buf() + ) + ); + // An absolute path is already what it means, whatever it is + // resolved against - which is what lets `target` skip asking for a + // working directory that may no longer exist. + assert_eq!( + resolve_against(Path::new("/"), Path::new("/opt/g/game.exe")), + resolve_against(here, Path::new("/opt/g/game.exe")) + ); + assert_eq!( + resolve_against(here, Path::new("/opt/g/game.exe")), + ( + Path::new("/opt/g/game.exe").to_path_buf(), + Path::new("/opt/g").to_path_buf() + ) + ); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/layer.rs b/crates/raven/src/layer.rs similarity index 100% rename from src/layer.rs rename to crates/raven/src/layer.rs diff --git a/src/lib.rs b/crates/raven/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/raven/src/lib.rs diff --git a/src/main.rs b/crates/raven/src/main.rs similarity index 87% rename from src/main.rs rename to crates/raven/src/main.rs index 40246cf..b816fa0 100644 --- a/src/main.rs +++ b/crates/raven/src/main.rs @@ -215,6 +215,10 @@ fn main() -> Result<()> { Commands::Env(c) => env_cmd(c), Commands::Run { name, argv } => run(&name, argv, None), Commands::Launch { exe, args } => { + // Both absolute before anything joins a namespace: `setns` resets + // the working directory, so a relative path settled later is + // resolved against the mount's root instead of the user's. + let (exe, cwd) = launch::target(&exe)?; let e = launch::resolve(&exe)?; // The kernel invokes this with no terminal of its own, so when a // double-clicked program misbehaves there is nothing to look at. @@ -231,14 +235,10 @@ fn main() -> Result<()> { let mut argv = vec!["wine".to_string(), exe.display().to_string()]; argv.extend(args); // The program's own directory, as Windows would give it. - let cwd = exe - .parent() - .filter(|p| !p.as_os_str().is_empty()) - .map(PathBuf::from); - run(&e.name, argv, cwd) + run(&e.name, argv, Some(cwd)) } Commands::Binfmt => binfmt(), - Commands::SessionAnchor { name } => session_anchor(&name), + Commands::SessionAnchor { name } => raven::session::anchor(&name), Commands::Exec { lower, upper, @@ -289,9 +289,9 @@ fn doctor() -> Result<()> { } ); match raven::prefix::media_decoders() { - None => out!("media playback : GStreamer decoders present"), + None => out!("media playback : GStreamer decoders present"), Some(missing) => { - out!("media playback : INCOMPLETE"); + out!("media playback : INCOMPLETE"); for m in missing { out!(" missing: {m}"); } @@ -306,6 +306,16 @@ fn doctor() -> Result<()> { "bases : {}", base::Base::list()?.len() ); + // A deploy that was interrupted leaves several gigabytes under a hidden + // name that `base list` passes over on purpose. Nothing else would ever + // mention it, so the diagnostics do. + for (id, path) in base::partials()? { + out!( + " interrupted deploy of {id:?}, still on disk at {}", + path.display() + ); + out!(" deploy {id:?} again to finish it, or remove that directory"); + } out!( "environments : {}", env::Environment::list()?.len() @@ -496,7 +506,8 @@ fn env_cmd(cmd: EnvCmd) -> Result<()> { let (n, s) = plural(holders.len()); out!("{name}: running - {n} process{s} holding its C:"); for h in &holders { - out!(" {:>7} {}", h.pid, h.comm); + let role = if h.anchor { " (session anchor)" } else { "" }; + out!(" {:>7} {}{role}", h.pid, h.comm); } out!("Release it: raven env stop {name}"); } @@ -555,20 +566,15 @@ fn env_cmd(cmd: EnvCmd) -> Result<()> { // services.exe and the rest are standing when the user arrives. out!("Starting Wine's services so the first launch does not wait..."); let started = std::time::Instant::now(); - match std::process::Command::new(std::env::current_exe()?) - .args(["run", &name, "--", "wine", "cmd", "/c", "exit"]) - .env("WINEDEBUG", "-all") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - { - Ok(s) if s.success() => out!( + if e.warm_up() { + out!( "{name} is ready in {:.1}s. Launches will be immediate.", started.elapsed().as_secs_f32() - ), + ); + } else { // The mount is up either way, so this is a warning and not a // failure: the next launch simply pays what this would have. - _ => out!("{name} is mounted, but Wine did not start; the first launch will."), + out!("{name} is mounted, but Wine did not start; the first launch will."); } Ok(()) } @@ -633,7 +639,11 @@ fn run(name: &str, argv: Vec, cwd: Option) -> Result<()> { let mut cmd = Command::new(&argv[0]); cmd.args(&argv[1..]); cmd.env("WINEPREFIX", e.prefix()); - if let Some(d) = cwd { + // Tested inside the namespace, which is the only place the answer is + // true, and skipped rather than fatal: a directory that is not there is + // no reason to refuse to start the program. Setting it anyway makes + // `exec` fail with an ENOENT that reads as "wine is missing". + if let Some(d) = cwd.filter(|d| d.is_dir()) { cmd.current_dir(d); } Err(cmd.exec()).with_context(|| format!("could not run {}", argv[0])) @@ -699,68 +709,6 @@ fn d3d_cmd( Ok(()) } -/// Holds a namespace open so later launches can join it. -/// -/// Mounts, reports itself, then does nothing for as long as it is wanted. It -/// must stay single-threaded until the mount is done - the kernel refuses -/// `CLONE_NEWUSER` to a threaded process - which is why the readiness line is -/// written only afterwards. -fn session_anchor(name: &str) -> Result<()> { - use std::io::Write as _; - // Every failure below has to leave through stdout: the launcher reads that - // pipe and nothing else, and the anchor's stderr goes to /dev/null because - // it outlives the terminal that started it. A `?` here would print to a - // stderr nobody is holding and the launcher would report only silence. - let report = |what: std::fmt::Arguments<'_>| -> ! { - println!("{what}"); - let _ = std::io::stdout().flush(); - std::process::exit(1); - }; - let e = match env::Environment::open(name) { - Ok(e) => e, - Err(err) => report(format_args!("{err}")), - }; - let spec = match e.spec() { - Ok(s) => s, - Err(err) => report(format_args!("{err}")), - }; - if let Err(err) = std::fs::create_dir_all(&spec.target) { - report(format_args!( - "could not create the mount point {}: {err}", - spec.target.display() - )); - } - // Bring the layer's opaque markers in line with the current shadow set - // before mounting. An environment created when Windows/Fonts was masked - // would otherwise stay masked for ever, and a user should not have to - // rebuild to receive a fix. - if let Err(err) = raven::layer::reconcile(&e.layer()) { - report(format_args!("could not reconcile the layer: {err}")); - } - if !UserNsOverlay::is_available() { - report(format_args!( - "this kernel restricts unprivileged user namespaces; run `raven doctor`" - )); - } - if let Err(err) = UserNsOverlay.mount(&spec) { - report(format_args!("could not mount the overlay: {err}")); - } - let pid = std::process::id(); - // Written only after the mount exists, so a reader of this file never sees - // a session that cannot be joined. - std::fs::write(e.session_file(), format!("{pid}\n")).with_context(|| { - format!( - "could not record the session at {}", - e.session_file().display() - ) - })?; - println!("ready {pid}"); - let _ = std::io::stdout().flush(); - loop { - std::thread::sleep(std::time::Duration::from_secs(3600)); - } -} - fn exec( spec: OverlaySpec, argv: Vec, diff --git a/src/mount/mod.rs b/crates/raven/src/mount/mod.rs similarity index 100% rename from src/mount/mod.rs rename to crates/raven/src/mount/mod.rs diff --git a/src/mount/userns.rs b/crates/raven/src/mount/userns.rs similarity index 100% rename from src/mount/userns.rs rename to crates/raven/src/mount/userns.rs diff --git a/src/paths.rs b/crates/raven/src/paths.rs similarity index 100% rename from src/paths.rs rename to crates/raven/src/paths.rs diff --git a/src/prefix.rs b/crates/raven/src/prefix.rs similarity index 100% rename from src/prefix.rs rename to crates/raven/src/prefix.rs diff --git a/src/registry/emit.rs b/crates/raven/src/registry/emit.rs similarity index 100% rename from src/registry/emit.rs rename to crates/raven/src/registry/emit.rs diff --git a/src/registry/hive.rs b/crates/raven/src/registry/hive.rs similarity index 100% rename from src/registry/hive.rs rename to crates/raven/src/registry/hive.rs diff --git a/src/registry/mod.rs b/crates/raven/src/registry/mod.rs similarity index 97% rename from src/registry/mod.rs rename to crates/raven/src/registry/mod.rs index 058eb25..558e376 100644 --- a/src/registry/mod.rs +++ b/crates/raven/src/registry/mod.rs @@ -66,8 +66,7 @@ pub fn import( ) -> Result<(), Error> { std::fs::create_dir_all(&spec.target).map_err(|e| Error::Layer(spec.target.clone(), e))?; - let me = std::env::current_exe().map_err(|e| Error::Tool("raven", e))?; - let mut cmd = Command::new(me); + let mut cmd = Command::new(crate::session::helper()?); cmd.arg("exec"); for l in &spec.lower { cmd.arg("--lower").arg(l); diff --git a/src/registry/rules.rs b/crates/raven/src/registry/rules.rs similarity index 100% rename from src/registry/rules.rs rename to crates/raven/src/registry/rules.rs diff --git a/src/registry/text.rs b/crates/raven/src/registry/text.rs similarity index 100% rename from src/registry/text.rs rename to crates/raven/src/registry/text.rs diff --git a/src/session.rs b/crates/raven/src/session.rs similarity index 59% rename from src/session.rs rename to crates/raven/src/session.rs index 2847a55..1d2df63 100644 --- a/src/session.rs +++ b/crates/raven/src/session.rs @@ -53,7 +53,7 @@ impl Environment { .trim() .parse() .ok()?; - self.holders().iter().any(|h| h.pid == pid).then_some(pid) + self.holds(pid).then_some(pid) } /// The pid of a session to join, starting one if none is running. @@ -90,7 +90,7 @@ impl Environment { // Somebody else is starting one. Wait for their session // rather than racing it, and fall back to trying // ourselves if their attempt died without cleaning up. - if let Some(pid) = self.wait_for_session() { + if let Some(pid) = self.wait_for_session(&lock) { return Ok(pid); } let _ = std::fs::remove_file(&lock); @@ -103,13 +103,23 @@ impl Environment { started } - /// Waits briefly for somebody else's anchor to come up. - fn wait_for_session(&self) -> Option { + /// Waits for somebody else's anchor to come up, or for them to give up. + /// + /// Both endings matter. Watching only for the session meant that when the + /// holder failed - a renamed base, a kernel that refuses the mount - it + /// dropped its lock within milliseconds and the waiter still sat out the + /// full thirty seconds before trying anything itself. The lock going away + /// is the holder saying it is no longer starting one, and it is the same + /// signal that clears a lock left behind by a launcher that was killed. + fn wait_for_session(&self, lock: &std::path::Path) -> Option { let deadline = std::time::Instant::now() + READY_TIMEOUT; while std::time::Instant::now() < deadline { if let Some(pid) = self.session() { return Some(pid); } + if !lock.exists() { + return None; + } std::thread::sleep(std::time::Duration::from_millis(50)); } None @@ -117,8 +127,7 @@ impl Environment { /// Starts an anchor and waits for it to report that the mount is up. fn start_session(&self) -> Result { - let exe = std::env::current_exe().map_err(|e| Error::Tool("raven", e))?; - let mut cmd = Command::new(exe); + let mut cmd = Command::new(helper()?); cmd.arg("session-anchor") .arg(&self.name) .stdin(Stdio::null()) @@ -203,12 +212,66 @@ impl Environment { Ok(()) } + /// Brings Wine's services up, so the next launch does not pay for them. + /// + /// Mounting is the cheap half - hundredths of a second. The seconds a + /// first launch pays are `wineserver`, `services.exe` and the rest + /// starting, and they only start when something runs. So the smallest + /// possible program is run: what matters is that they are standing when + /// the user arrives. + /// + /// Reports whether they came up. A failure is not the caller's problem: + /// the mount is up either way and the next launch simply pays what this + /// would have. + pub fn warm_up(&self) -> bool { + Command::new(match helper() { + Ok(exe) => exe, + Err(_) => return false, + }) + .args(["run", &self.name, "--", "wine", "cmd", "/c", "exit"]) + .env("WINEDEBUG", "-all") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|s| s.success()) + } + /// Forgets a session's record. The processes are `stop`'s business. pub fn clear_session(&self) { let _ = std::fs::remove_file(self.session_file()); } } +/// The binary to re-run for the work the library cannot do in this process. +/// +/// Two things are done by re-running Raven: holding a session open, and +/// mounting for a registry import. Both need a process that will be replaced +/// or will sleep for ever, so they cannot happen here - and both used to run +/// `current_exe()`, which is right for the launcher and wrong for the window. +/// The window can answer `session-anchor`, but `exec` would have it mount and +/// exec over itself, so it refuses; running the CLI beside it instead means +/// neither verb depends on which binary the user happened to start. +pub(crate) fn helper() -> Result { + let exe = std::env::current_exe().map_err(|e| Error::Tool("raven", e))?; + Ok(helper_binary(&exe)) +} + +/// `raven` next to `exe` if there is one, else `exe` itself. +/// +/// The package installs both binaries into `/usr/bin` and a cargo build puts +/// both in `target/`, so the sibling exists in every layout Raven +/// ships or is developed in. Where it does not, re-running ourselves is the +/// old behaviour and still correct for the launcher. +fn helper_binary(exe: &std::path::Path) -> PathBuf { + let sibling = exe.with_file_name("raven"); + if sibling.is_file() { + sibling + } else { + exe.to_path_buf() + } +} + /// Removes the start lock however the attempt ends. struct LockGuard(PathBuf); impl Drop for LockGuard { @@ -255,6 +318,131 @@ fn read_line_before(pipe: &std::process::ChildStdout, limit: std::time::Duration out } +/// Holds a namespace open so later launches can join it. +/// +/// Mounts, reports itself on stdout, then does nothing for as long as it is +/// wanted. It must stay single-threaded until the mount is done - the kernel +/// refuses `CLONE_NEWUSER` to a threaded process - which is why the readiness +/// line is written only afterwards, and why a binary has to reach this before +/// starting any runtime of its own. +/// +/// In the library rather than the launcher because either binary may end up +/// running it: `start_session` prefers the `raven` beside it, and falls back +/// to whichever binary asked. The window asks too, and until it could answer, +/// its Start started a second window instead, said nothing on the pipe, and +/// was killed thirty seconds later with a message blaming the +/// kernel. +pub fn anchor(name: &str) -> ! { + use crate::mount::MountBackend as _; + use std::io::Write as _; + // Every failure has to leave through stdout: the launcher reads that pipe + // and nothing else, and the anchor's stderr goes to /dev/null because it + // outlives the terminal that started it. An error on stderr would reach + // nobody, and the launcher would report only silence. + let report = |what: std::fmt::Arguments<'_>| -> ! { + println!("{what}"); + let _ = std::io::stdout().flush(); + std::process::exit(1); + }; + // `Error::Layer` reads "preparing the layer failed at " and keeps + // the errno as a source, which anyhow prints for the CLI and `Display` + // drops. This line is all the launcher ever shows, and "permission + // denied" is the difference between a bug and a chmod. + let because = |e: &dyn std::error::Error| -> String { + let mut out = e.to_string(); + let mut source = e.source(); + while let Some(s) = source { + out.push_str(&format!(": {s}")); + source = s.source(); + } + out + }; + let e = match crate::env::Environment::open(name) { + Ok(e) => e, + Err(err) => report(format_args!("{err}")), + }; + let spec = match e.spec() { + Ok(s) => s, + Err(err) => report(format_args!("{err}")), + }; + if let Err(err) = std::fs::create_dir_all(&spec.target) { + report(format_args!( + "could not create the mount point {}: {err}", + spec.target.display() + )); + } + // Bring the layer's opaque markers in line with the current shadow set + // before mounting. An environment created when Windows/Fonts was masked + // would otherwise stay masked for ever, and a user should not have to + // rebuild to receive a fix. + if let Err(err) = crate::layer::reconcile(&e.layer()) { + report(format_args!( + "could not reconcile the layer: {}", + because(&err) + )); + } + if !crate::mount::UserNsOverlay::is_available() { + report(format_args!( + "this kernel restricts unprivileged user namespaces; run `raven doctor`" + )); + } + if let Err(err) = crate::mount::UserNsOverlay.mount(&spec) { + report(format_args!( + "could not mount the overlay: {}", + because(&err) + )); + } + let pid = std::process::id(); + // Written only after the mount exists, so a reader of this file never + // sees a session that cannot be joined. + if let Err(err) = std::fs::write(e.session_file(), format!("{pid}\n")) { + report(format_args!( + "could not record the session at {}: {err}", + e.session_file().display() + )); + } + println!("ready {pid}"); + let _ = std::io::stdout().flush(); + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} + +#[cfg(test)] +mod helper_tests { + use super::helper_binary; + + #[test] + fn the_helper_is_the_cli_beside_us_when_there_is_one() { + // The library re-runs its own binary for `session-anchor` and for the + // `exec` behind a registry import. From the window that would be the + // window - which answers neither well - so a `raven` next to it is + // preferred. The package installs both into /usr/bin and a cargo + // build puts both in target/, so the sibling is there in + // every layout Raven ships or is developed in. + let dir = std::env::temp_dir().join(format!("raven-helper-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gui = dir.join("raven-gui"); + std::fs::write(&gui, b"").unwrap(); + + // No sibling yet: the only thing to re-run is ourselves. + assert_eq!(helper_binary(&gui), gui); + + let cli = dir.join("raven"); + std::fs::write(&cli, b"").unwrap(); + assert_eq!( + helper_binary(&gui), + cli, + "the CLI beside us answers both verbs" + ); + // The CLI itself is its own helper, not a self-reference through a + // path that happens to have the same name. + assert_eq!(helper_binary(&cli), cli); + let _ = std::fs::remove_dir_all(&dir); + } +} + #[cfg(test)] mod tests { use crate::env::{Environment, Manifest}; diff --git a/tests/base_is_immutable.rs b/crates/raven/tests/base_is_immutable.rs similarity index 100% rename from tests/base_is_immutable.rs rename to crates/raven/tests/base_is_immutable.rs diff --git a/crates/raven/tests/d3d_install.rs b/crates/raven/tests/d3d_install.rs new file mode 100644 index 0000000..6b2b24f --- /dev/null +++ b/crates/raven/tests/d3d_install.rs @@ -0,0 +1,125 @@ +//! What an interrupted Direct3D install leaves behind. +//! +//! Installing a runtime copies libraries into the upper layer, then edits +//! the prefix's registry, then records what it did. Everything between the +//! first copy and that record can fail, and what the record says afterwards +//! decides whether the environment can be repaired: a library Raven put +//! there but does not list is one `dxvk` cannot see, `--remove` cannot +//! remove, and the next install refuses as somebody else's. + +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +use raven::d3d::DXVK; +use raven::env::{Environment, Manifest}; + +fn fake_env(name: &str) -> Environment { + let root = std::env::temp_dir().join(format!("raven-d3d-{name}-{}", std::process::id())); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(root.join("prefix/dosdevices")).unwrap(); + fs::create_dir_all(root.join("upper/Windows/System32")).unwrap(); + fs::create_dir_all(root.join("upper/Windows/SysWOW64")).unwrap(); + fs::write( + root.join("prefix/user.reg"), + "WINE REGISTRY Version 2\n\n[Software\\\\Fluff] 100\n\"x\"=\"y\"\n", + ) + .unwrap(); + Environment { + name: name.into(), + manifest: Manifest { + base: "none".into(), + }, + root, + } +} + +/// A directory shaped like a DXVK release, with the libraries it names. +fn fake_build(root: &std::path::Path, version: &str) -> PathBuf { + let build = root.join(version); + for arch in ["x64", "x32"] { + let dir = build.join(arch); + fs::create_dir_all(&dir).unwrap(); + for dll in ["d3d11", "dxgi"] { + fs::write(dir.join(format!("{dll}.dll")), version.as_bytes()).unwrap(); + } + } + build +} + +#[test] +fn a_failure_after_the_copies_leaves_libraries_raven_can_still_account_for() { + let env = fake_env("interrupted"); + let build = fake_build(&env.root, "dxvk-2.7"); + + // The registry is what fails: read it, then refuse the rewrite. The + // prefix is made unwritable, which is exactly what a full disk or a + // read-only home does to the same step. + let prefix = env.prefix(); + let before = fs::metadata(&prefix).unwrap().permissions(); + fs::set_permissions(&prefix, fs::Permissions::from_mode(0o500)).unwrap(); + let outcome = env.install_d3d(&DXVK, &build); + fs::set_permissions(&prefix, before).unwrap(); + + assert!(outcome.is_err(), "the registry could not be written"); + + // The libraries are on disk. What matters is that Raven says so, rather + // than reporting an empty install and then refusing to touch its own + // files as though a stranger had put them there. + let listed = env.d3d(&DXVK); + assert!( + !listed.is_empty(), + "the copies happened, so they must be accounted for" + ); + for shadow in &listed { + assert!( + shadow.path.is_file(), + "{} is listed and must be on disk", + shadow.path.display() + ); + } + let removed = env.remove_d3d(&DXVK).expect("removal must be possible"); + assert_eq!(removed, listed.len(), "everything listed comes back out"); + assert!( + env.d3d(&DXVK).is_empty(), + "and nothing of Raven's is left behind" + ); + + let _ = fs::remove_dir_all(&env.root); +} + +#[test] +fn a_failed_upgrade_does_not_take_the_working_build_with_it() { + let env = fake_env("upgrade"); + let old = fake_build(&env.root, "dxvk-2.4"); + env.install_d3d(&DXVK, &old) + .expect("the first install works"); + let installed: Vec = env.d3d(&DXVK).into_iter().map(|s| s.path).collect(); + assert_eq!(installed.len(), 4, "two libraries, two architectures"); + + // An upgrade whose second architecture cannot be read: x32 is there in + // the plan and unreadable by the time it is copied. + let new = fake_build(&env.root, "dxvk-2.7"); + fs::set_permissions(new.join("x32/dxgi.dll"), fs::Permissions::from_mode(0o000)).unwrap(); + let outcome = env.install_d3d(&DXVK, &new); + fs::set_permissions(new.join("x32/dxgi.dll"), fs::Permissions::from_mode(0o644)).unwrap(); + assert!(outcome.is_err(), "the upgrade could not be completed"); + + // Whatever the mixture of versions now on disk, every library the + // environment had is still there and still described. The rollback must + // not delete files that were a working install before this call. + for path in &installed { + assert!( + path.is_file(), + "{} was working before the upgrade and must survive it", + path.display() + ); + } + assert_eq!( + env.d3d(&DXVK).len(), + installed.len(), + "and all of them are still accounted for" + ); + + let _ = fs::remove_dir_all(&env.root); +} diff --git a/tests/device_attach.rs b/crates/raven/tests/device_attach.rs similarity index 74% rename from tests/device_attach.rs rename to crates/raven/tests/device_attach.rs index c055ce1..fce07ab 100644 --- a/tests/device_attach.rs +++ b/crates/raven/tests/device_attach.rs @@ -198,6 +198,89 @@ fn detaching_renumbers_the_disks_that_remain() { let _ = fs::remove_dir_all(&env.root); } +#[test] +fn detach_leaves_alone_a_drive_letter_raven_did_not_create() { + // winecfg's own mappings live in the same Drives section. `attach` + // refuses a letter that carries one; `detach` must not delete it either, + // and there is nothing of Raven's on that letter to report detaching. + let env = fake_env("foreign"); + let reg = env.prefix().join("system.reg"); + let text = fs::read_to_string(®).unwrap(); + fs::write( + ®, + format!("{text}\n[Software\\\\Wine\\\\Drives] 1700000000\n\"d:\"=\"cdrom\"\n"), + ) + .unwrap(); + + assert!( + matches!(env.detach('d'), Err(raven::Error::NotAttached('d'))), + "a letter Raven never attached is not Raven's to detach" + ); + assert!( + fs::read_to_string(®) + .unwrap() + .contains("\"d:\"=\"cdrom\""), + "the user's own mapping must survive" + ); + + let _ = fs::remove_dir_all(&env.root); +} + +#[test] +fn renumbering_counts_the_drives_mountmgr_counts() { + // mountmgr numbers by rank in the Drives section, and a floppy-typed + // entry with no raw link behind it - a half-finished attach, or one made + // by hand - still takes a number. `attach` counts it; `detach` has to + // count it too, or every later drive is wired to a number that names a + // different disk. + let devs = some_block_devices(); + if devs.len() < 2 { + eprintln!("skipped: needs two block devices, found {}", devs.len()); + return; + } + let env = fake_env("rank"); + let dos = env.prefix().join("dosdevices"); + let reg = env.prefix().join("system.reg"); + + let d = env.attach(&devs[0], 'd').unwrap(); + let f = env.attach(&devs[1], 'f').unwrap(); + assert_eq!((d.number, f.number), (1, 2)); + + // A floppy entry on e: with nothing behind it, sitting between them. + let text = fs::read_to_string(®).unwrap(); + fs::write( + ®, + text.replace("\"f:\"=\"floppy\"", "\"e:\"=\"floppy\"\n\"f:\"=\"floppy\""), + ) + .unwrap(); + assert_eq!( + env.attachments() + .iter() + .find(|a| a.letter == 'f') + .map(|a| a.number), + Some(3), + "f: is now the third disk, because e: takes a number" + ); + + env.detach('d').unwrap(); + // d: is gone, so e: is first and f: second - and f: is the only one of + // them with a device behind it. + assert_eq!( + env.attachments() + .iter() + .find(|a| a.letter == 'f') + .map(|a| a.number), + Some(2) + ); + assert_eq!( + fs::read_link(dos.join("physicaldrive2")).unwrap(), + devs[1], + "the link must carry the number attachments() reports" + ); + + let _ = fs::remove_dir_all(&env.root); +} + #[test] fn attach_refuses_what_would_eat_a_disk() { let env = fake_env("refuse"); diff --git a/tests/environment_recovery.rs b/crates/raven/tests/environment_recovery.rs similarity index 79% rename from tests/environment_recovery.rs rename to crates/raven/tests/environment_recovery.rs index 1a7ea22..ae0dcf0 100644 --- a/tests/environment_recovery.rs +++ b/crates/raven/tests/environment_recovery.rs @@ -82,6 +82,34 @@ fn a_process_left_in_the_namespace_is_found_and_stopped() { "the process inside the namespace must be visible from outside; saw {holders:?}" ); + // The launch path does not ask who holds the environment - it asks + // whether the anchor it recorded still does, which is one process, not + // every process on the machine. The cheap question must give the same + // answer as the expensive one, or a warm launch either misses a live + // session or joins a dead one. + assert!( + env.holds(child.id()), + "the single-process check must agree with the scan that found it" + ); + assert!( + !env.holds(1), + "a live process that holds nothing of ours is not a session" + ); + // And the recorded pid is believed only when it is really a holder. + fs::write(env.session_file(), "1\n").unwrap(); + assert_eq!( + env.session(), + None, + "a stale pid must not pass for a session" + ); + fs::write(env.session_file(), format!("{}\n", child.id())).unwrap(); + assert_eq!( + env.session(), + Some(child.id()), + "a real holder is the session" + ); + let _ = fs::remove_file(env.session_file()); + let stopped = env.stop().expect("stop must release the environment"); assert!( stopped.iter().any(|h| h.pid == child.id()), diff --git a/tests/registry_projection.rs b/crates/raven/tests/registry_projection.rs similarity index 100% rename from tests/registry_projection.rs rename to crates/raven/tests/registry_projection.rs diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md index 5853561..1a3b146 100644 --- a/docs/guide/troubleshooting.md +++ b/docs/guide/troubleshooting.md @@ -53,21 +53,26 @@ pick, and prints the masking fix when it is not Raven's. ## A second launch fails, or a program will not start again -Closing a program's window can leave `wineserver` and a handful of Wine -services alive inside the mount namespace, holding the environment busy. +A launch into an environment that already has a session joins it rather than +refusing — that is what a session is for — so this is no longer what an +ordinary second launch does. What still holds an environment busy is the +wreckage of a session: when the anchor dies, `wineserver` and a handful of Wine +services stay alive inside the mount namespace for a few seconds afterwards, +and overlayfs will not mount the same upper layer twice while they are there. ```bash raven env status games ``` -names the processes holding it, and +names the processes holding it, marking the anchor of a live session as such, +and ```bash raven env stop games ``` -terminates them and releases the environment. Launches into a held environment -refuse with exactly these two commands rather than a bare +terminates them and releases the environment. A launch that cannot mount +refuses with exactly these two commands rather than a bare `Device or resource busy`. ## Launching feels slower than Proton diff --git a/docs/guide/usage.md b/docs/guide/usage.md index a892eb1..f526440 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -115,8 +115,8 @@ Microsoft's. Raven refuses to overwrite a library it did not install itself, so a DLL some installer left in the environment is safe. DXVK has been shown to initialise against a real Windows and reach the GPU - -Microsoft's own `dxdiag.exe` drove it and DXVK enumerated the card. No game has -rendered a frame through it yet, and nothing is benchmarked; see +Microsoft's own `dxdiag.exe` drove it and DXVK enumerated the card, and +*ShineHill*, a Direct3D 11 game from Steam, then ran and drew through it; see [../project/status.md](../project/status.md) for exactly how far that goes. ### Direct3D 12 @@ -129,7 +129,9 @@ raven env vkd3d games --from ~/Downloads/vkd3d-proton-3.0.1.tar.zst ``` A game wanting D3D11 and a game wanting D3D12 are different games, and one -environment serves both. Removing either leaves the other alone. +environment serves both. Removing either leaves the other alone. No D3D12 title +has been run through it yet, though - vkd3d-proton is installed and unproven +where DXVK is not. Neither command has an opinion about whose build you use, and that is not politeness: CachyOS's Proton and Valve's both carry these two projects as @@ -141,7 +143,7 @@ There is no distinct "CachyOS DXVK" to prefer. The first program you run in an environment starts a **session**: Raven mounts C: once and keeps it, so every later launch joins what is already there instead of building a world of its own. The first launch of the day costs about two -seconds; the ones after it cost about a sixth of one. +seconds; the ones after it cost about a quarter of one. You can also bring an environment up before you need it, so nothing waits at all: diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index a664a34..3c74c06 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -103,8 +103,10 @@ read-only Wine layer can shadow a file or directory outright where the override is not enough. Today the shadow set is a short, measurement-justified constant in -`src/layer.rs` — two directory masks, `Windows/WinSxS` and `Windows/Fonts`; -the per-library table the corpus will produce is planned as a data file keyed +`crates/raven/src/layer.rs` — one directory mask, `Windows/WinSxS`. +`Windows/Fonts` was masked there too and has been withdrawn: the spawn time it +bought was not worth a Windows that declared 961 fonts and had none. The +per-library table the corpus will produce is planned as a data file keyed by Windows build. It is the thing the project exists to shrink. See [shadow-set.md](shadow-set.md). @@ -139,9 +141,9 @@ works without special handling, and the mount dies with the process tree that owns it. `binfmt_misc` is the exception, and it is a **packaging** concern rather than a -runtime one: a file in `/etc/binfmt.d/`, applied by `systemd-binfmt` at boot, -installed by the package manager that already holds root legitimately. Nothing -needs to hold privilege while Raven runs. +runtime one: a file in `/usr/lib/binfmt.d/`, applied by `systemd-binfmt` at +boot, installed by the package manager that already holds root legitimately. +Nothing needs to hold privilege while Raven runs. ### Where this breaks, and the seam that anticipates it @@ -179,27 +181,45 @@ error. A bare `mount: operation not permitted` is not. ## Crate layout -Phase 1 is **one crate**, not a workspace. +Raven is a workspace of **two crates**: the library with its command line, and +the window. [The org rule](https://github.com/Project-Colony/Project-Colony-Resources/blob/main/design/repository-layout.md) is that a workspace is for a real boundary — a separate process, a different build target, a library something else genuinely consumes — and that splitting by layer buys nothing but a dependency graph. Once the privileged daemon disappeared, Raven became one program in one process, which is the case the rule -answers with a single crate and subsystems as directories under `src/`. +answers with a single crate and subsystems as directories under `src/`, and that +is what it stayed until the window arrived. A second binary, with its own +dependency tree, is a real boundary rather than a layer. The root manifest is +virtual — nothing of its own but the member list and the one version both crates +inherit, because a window and a library of different ages are not shippable +together. ``` -src/ +crates/raven/src/ ├── main.rs the CLI; a thin shell over the library below ├── lib.rs the library root and error type ├── paths.rs the Colony filesystem layout for Raven ├── base.rs deploying and describing a Windows base ├── env.rs the environment model: create, run, recover, destroy +├── session.rs the anchor holding a namespace open, and joining it +├── attach.rs the real block devices an environment may see +├── d3d.rs installing DXVK and vkd3d-proton into a prefix ├── launch.rs binfmt registration and environment resolution ├── layer.rs case normalisation, and the shadow masks ├── prefix.rs the Wine prefix pieces an environment keeps ├── mount/ the mount backends, behind one interface └── registry/ registry hive reading and Wine .reg projection + +crates/raven-gui/src/ +├── main.rs the window, and the actions it asks the library for +├── model.rs the plain data the screens draw, kept testable +├── load.rs reading that model without freezing the window +├── deploy.rs base deployment, and the progress read from wimlib +├── errors.rs library errors turned into something a window can offer +├── theme.rs colony-ui's palette and scale, applied to iced +└── view/ one module per screen ``` A directory earns its existence by holding more than one file, which is why @@ -213,17 +233,18 @@ operations, and if the logic ends up inside argument handlers, adding one means rewriting it. The cost of the rule now is a few function signatures; the cost of skipping it is the GUI. -### When this becomes a workspace +### What else splits out | Split out | When | |---|---| -| `raven-gui` | there is a model worth showing — it consumes `colony-ui`, so it is cheap once the model exists | +| `raven-gui` | done — there was a model worth showing, and it consumes `colony-ui`, which is what made it cheap | | `raven-daemon`, `raven-proto` | a privileged helper is needed for systems without unprivileged namespaces | | `raven-launch` | measurement shows CLI start-up cost matters on the `binfmt` path | | `raven-hive` | the hive corpus and its tests outgrow living alongside the binary | Each has a trigger, so the split is a decision rather than a drift. None of them -is speculative — they are the four things already known to be coming. +is speculative — they are the four things already known to be coming, and the +first of them has arrived. ## Language diff --git a/docs/internals/contributing.md b/docs/internals/contributing.md index 026ed8e..9fdb2e8 100644 --- a/docs/internals/contributing.md +++ b/docs/internals/contributing.md @@ -48,8 +48,9 @@ root. ### The library is the API, the CLI is a shell over it A GUI is a second caller of the same operations. Logic that ends up inside -argument handlers has to be rewritten to add one. The rule costs a few function -signatures now and saves the GUI later. +argument handlers has to be rewritten to add one. The rule cost a few function +signatures and has already paid for itself: `raven-gui` is that second caller, +and adding it left the CLI untouched. ### Measurements come with their configuration @@ -72,11 +73,11 @@ cargo build --release cargo test ``` -82 tests, no root required, no Windows base required. +142 tests, no root required, no Windows base required. Two things that are true, and stay true: -- `cargo test` at the crate root must work on a plain developer machine, +- `cargo test` at the workspace root must work on a plain developer machine, without root and without a Windows base present. Tests that need either are gated behind a feature or a fixture, because a test command people learn to avoid is a test suite that stops running. diff --git a/docs/internals/mount-stack.md b/docs/internals/mount-stack.md index 711e5f8..ec27dcb 100644 --- a/docs/internals/mount-stack.md +++ b/docs/internals/mount-stack.md @@ -113,7 +113,8 @@ assumption to build on. The overlay is mounted **without root**, inside a user namespace: ``` -unshare -Urm → mount -t overlay → exec wine +unshare -Urm → mount -t overlay → hold the anchor +setns → exec wine every launch ``` This was measured rather than assumed, and measured end to end with Wine in the @@ -136,15 +137,16 @@ Nothing has to hold privilege while Raven runs. Two consequences worth stating: -- **Concurrent independent launches into one environment are not supported at - first.** `overlayfs` does not support two live mounts sharing an `upperdir`, - and the simple path — mount, exec, exit — gives one mount per process tree. - If launching two unrelated programs into the same environment turns out to - matter, the fix is a keeper process holding the namespace with others joining - by `nsenter`, which is proven to work. It is not built until it is needed. -- **`binfmt_misc` still needs root, once.** It is a file in `/etc/binfmt.d/` - applied by `systemd-binfmt` at boot — a packaging concern, handled by the - package manager, not a service that runs. +- **Concurrent independent launches into one environment join it.** + `overlayfs` does not support two live mounts sharing an `upperdir`, and the + simple path — mount, exec, exit — gave one mount per process tree, so a + second independent launch failed. The keeper process that fixes it is built: + an *anchor* creates the namespace, mounts the overlay and stays alive, and + every launch `setns`es into it. It was built for speed rather than for + concurrency — see [performance.md](performance.md) — and got both. +- **`binfmt_misc` still needs root, once.** It is a file in + `/usr/lib/binfmt.d/` applied by `systemd-binfmt` at boot — a packaging + concern, handled by the package manager, not a service that runs. Where unprivileged namespaces are unavailable — `linux-hardened`, Ubuntu's AppArmor policy, some enterprise configurations — the mount goes through a @@ -158,8 +160,8 @@ Four conceptual verbs, and how they map onto the real commands: | | | | |---|---|---| | **create** | `raven env create` | allocate `upper/` and `work/`, build the Wine prefix, normalise the layer's casing, set the shadow masks, project the registry | -| **activate** | implicit in `run` / `launch` | the overlay mounts when a program starts, in that program's namespace | -| **deactivate** | the program exiting — or `raven env stop` | the mount dies with the process tree; `env status` names anything still holding it | +| **activate** | implicit in `run` / `launch`, or `raven env start` | the first launch starts an anchor that mounts the overlay and holds it; every later one joins that namespace | +| **deactivate** | `raven env stop` | the mount dies with the anchor's process tree, so it outlives any one program; `env status` names everything holding it and marks the anchor | | **destroy** | `raven env destroy` | refuse if held, then delete the environment directory; the base is untouched | Nothing in that list writes to a base. That is checkable, and it should be diff --git a/docs/internals/packaging.md b/docs/internals/packaging.md index 036336e..445bcfd 100644 --- a/docs/internals/packaging.md +++ b/docs/internals/packaging.md @@ -17,25 +17,67 @@ because it is typed in front of every program launch, and three characters against five adds up over a day. `rvn` is a link rather than a second binary, so there is one thing to build, one -to sign and one to update. Help output and diagnostics say `raven` under both -names — the short name is a convenience, not a second identity. +to sign and one to update. `--version` and the diagnostics say `raven` under +both names, and only the usage line echoes the name it was invoked as — the +short name is a convenience, not a second identity. ## `binfmt_misc` registration Making `./program.exe` run like any other binary means registering the PE magic with the kernel, and writing to `/proc/sys/fs/binfmt_misc/register` needs root. -It is registered **once, at install time**, through `/etc/binfmt.d/raven.conf` -applied by `systemd-binfmt` — the same mechanism the `wine` package uses. The -package manager already holds root legitimately; Raven does not need to, and -adding a privileged service to do at runtime what a config file does at boot -would be trading a file for an attack surface. +It is registered **once, at install time**, through +`/usr/lib/binfmt.d/raven.conf` applied by `systemd-binfmt` — the same mechanism +the `wine` package uses. That is the directory for files a package owns; a file +of the same name in `/etc/binfmt.d/` takes precedence over it, which is why +`raven binfmt` prints an `/etc` path for a hand-registration on a build no +package installed. The package manager already holds root legitimately; Raven +does not need to, and adding a privileged service to do at runtime what a config +file does at boot would be trading a file for an attack surface. Uninstalling removes the file. A registration left behind pointing at a deleted binary would keep working on the kernel's open handle until the next reboot (the `F` flag) and then make every `.exe` on the machine fail in a way nobody would connect to Raven; `raven doctor` detects and reports both states. +## Two binaries, two desktop entries, one version + +The package installs `raven-gui` beside `raven`, and `raven-gui.desktop` +beside `raven.desktop`. They stay separate rather than merging into one +launcher, because the two desktop entries mean different things to a file +manager: + +`raven.desktop` claims the `.exe` MIME types (`application/x-msdownload` and +friends) and carries `NoDisplay=true`. It is the handler a file manager +reaches for when someone double-clicks a Windows program, launching +`raven launch %f` against that one file — it is not meant to appear in an +application menu on its own. + +`raven-gui.desktop` claims no MIME type and has no `NoDisplay`. It appears in +the menu under Raven's `System;Settings;` categories and launches `raven-gui` +with no arguments, opening the administration window for managing bases and +environments. + +If the GUI launcher also claimed the `.exe` association, double-clicking a +Windows program would open the manager instead of running the program — the +opposite of what a double-click means. Keeping the association on +`raven.desktop` alone, and never adding it to `raven-gui.desktop`, is what +keeps that from happening. Both entries reuse the single `Icon=raven` already +installed into the `hicolor` theme; there is no second icon to keep in sync. + +The window itself has to opt into that entry. A compositor matches a window's +application id against a desktop file's basename, so `raven-gui` sets its +`application_id` to `raven-gui` — without it the entry and the running window +are unrelated as far as a taskbar is concerned, and the `Icon=raven` above +never reaches one. `assets/brand/README.md` records that agreement in full. + +`build()` needed no change to produce the second binary: `cargo build +--release` at the workspace root already builds every workspace member, so +`raven-gui` comes out of the same command as `raven`. Only `package()` gained +two more `install` lines, and the release workflow gained a second staged +asset — the signing job already signs every file it finds under `dist/`, so +it needed no change at all. + ## `ntsync` is not Raven's business Arch's `wine` package depends on `ntsync-autoload`, whose entire content is a diff --git a/docs/internals/performance.md b/docs/internals/performance.md index 2342b75..4653207 100644 --- a/docs/internals/performance.md +++ b/docs/internals/performance.md @@ -1,9 +1,9 @@ # Performance What running against a real Windows costs, what it does not cost, what has -been ruled out — and what has been fixed. The spawn overhead is attributed and -mostly removed (fonts, masked); what stays deferred is the ~20 ms residual and -an in-game frame-time measurement. +been ruled out — and what has been fixed. The spawn overhead is attributed +(fonts), though the mask that removed it has since been withdrawn on +correctness grounds; what stays deferred is an in-game frame-time measurement. ## The wall-clock benchmark @@ -24,8 +24,10 @@ the fonts mask; the fix it motivated is further down): About **+115 ms per process** at the time, consistent with the +95 ms measured earlier by a cruder method — twenty seconds of pure overhead for an installer -spawning two hundred processes. With `Windows/Fonts` masked it is **~135 ms -(1.19×), about +22 ms per process** — some four seconds for the same installer. +spawning two hundred processes. With `Windows/Fonts` masked it fell to **~135 +ms (1.19×), about +22 ms per process** — some four seconds for the same +installer — but that mask has since been withdrawn on correctness grounds, so +the table above is what a spawn costs today. **Directory enumeration** (30 × `dir C:\windows\system32` inside one `cmd`, three samples each): @@ -131,11 +133,14 @@ fontconfig), so plain Wine never pays this. One opaque-overlay mask on |---|---|---|---| | spawn, warm server | ~113 ms | ~227 ms | **~135 ms** | -`Windows/Fonts` is now the shadow set's second measured entry (`layer.rs`), -and the remaining ~20 ms is the overlay plus the rest of the real tree. Fonts -still render — through fontconfig, exactly as under plain Wine — and the game -still reaches its title screen with the mask on. The cost: programs that want -Microsoft's font *files*, not just the faces, will not see them; the corpus +`Windows/Fonts` became the shadow set's second measured entry for a while +(`crates/raven/src/layer.rs`), leaving ~20 ms of overlay and the rest of the +real tree. Fonts still rendered — through fontconfig, exactly as under plain +Wine — and the game still reached its title screen with the mask on. The mask +has since been withdrawn on correctness grounds, so the 227 ms column is what +a spawn costs today; the reasoning is in [shadow-set.md](shadow-set.md). The +cost it carried while it stood: programs that want Microsoft's font *files*, +not just the faces, would not see them; the corpus will say whether such a program exists. The casefold side-benefit worth keeping: on a casefolding filesystem the @@ -200,9 +205,22 @@ namespace is what admits it to the mount namespace. **Eight times faster after the first launch of the day**, which is the launch nobody counts. (0.16 s of that measurement was taken while `Windows/Fonts` was masked; the mask has since been removed on correctness grounds and the figure -is 0.26 s - see [shadow-set.md](shadow-set.md).) The remaining 0.04 s over plain Wine's warm figure is -`setns` plus the real Windows being larger than a Wine prefix, and is not worth -chasing. +is 0.26 s - see [shadow-set.md](shadow-set.md).) What remains over plain Wine's +warm figure is `setns` plus the real Windows being larger than a Wine prefix. + +Raven's own share of that was measured directly afterwards, by running a +trivial *native* program through the same path so Wine's start-up is excluded: + +| `raven run -- /bin/true`, session up | | +|---|---| +| validating the anchor by scanning `/proc` | 33 35 32 32 32 33 30 ms | +| validating it by reading one `mountinfo` | 2 2 2 2 2 2 2 ms | + +The question `session` asks is whether the *one* pid it recorded still holds +the environment, and it answered it by reading `/proc//mountinfo` for +every process on the machine - 37 ms of a 32 ms launch, on 454 processes. The +scan was the launch. Reading the one process's `mountinfo` gives the same +answer, and what is left of Raven on a warm launch is about 2 ms. The crash property that motivated the old design is intact: the mount is still owned by the anchor's process tree, and killing the anchor was verified to take @@ -289,9 +307,10 @@ it fails. A guarantee whose test cannot fail is not a guarantee. ## Where to start when this is picked up -1. **The spawn cost is attributed and mostly fixed** — fonts, masked, 227 → 135 - ms. The residual ~20 ms over plain Wine has no owner yet; profile it only if - an installer measurement says it matters. +1. **The spawn cost is attributed, and the fix was given back** — fonts, + masked, 227 → 135 ms, then unmasked because a Windows declaring 961 fonts + and having none is not the real thing. It is the largest known cost and it + has no owner; profile it only if an installer measurement says it matters. 2. **`casefold` is closed.** Tested on a casefolding tmpfs against identical control trees: detected by Wine, no gain, slight regression on directory listing — and one real benefit (the lowercase-shadow hazard becomes @@ -322,12 +341,15 @@ anything left to win lives: - **A launch costs 0.26 s and almost none of it is Raven's code.** Argument parsing, a few dozen `stat` calls, one `setns` and an `exec`. The time is Wine's services and the kernel's mount. -- **The one place Raven does real work is the registry projection** - a - hand-written parser over a 76 MB binary hive, 1 894 keys in 130 ms - and it - runs at environment creation and on `reproject`, not per launch. +- **The one place Raven does real work is the registry projection** - the + `nt-hive` reader over a 76 MB binary hive, 1 894 keys in 82 ms measured + today - and it runs at environment creation and on `reproject`, not per + launch. - **The measurable wins so far were architectural, not compiler flags.** - Sessions took a launch from 2.07 s to 0.26 s; masking fonts was worth 92 ms - and was reverted anyway on correctness grounds. + Sessions took a launch from 2.07 s to 0.26 s, and asking about one process + instead of all of them took Raven's own share of a warm launch from 32 ms to + 2; masking fonts was worth 92 ms and was reverted anyway on correctness + grounds. If the projection ever becomes the thing people wait on - a much larger hive, a base with far more installed software - that is where profiling should start. diff --git a/docs/internals/registry-projection.md b/docs/internals/registry-projection.md index bc09443..9906e08 100644 --- a/docs/internals/registry-projection.md +++ b/docs/internals/registry-projection.md @@ -51,10 +51,10 @@ genuinely carries over: - `HKLM\Software\\…` — where a program installed itself, its options, its licence state -- `HKLM\Software\Classes\CLSID`, `Interface`, `TypeLib` — COM registrations - pointing at libraries that are present in the base +- a named handful of Microsoft subtrees — `DirectX`, `.NETFramework`, + `NET Framework Setup`, `COM3`, `Ole`, `Windows Script Host` — allowed back + over the blanket refusal of `HKLM\Software\Microsoft` - `HKCU\Software\\…` from `NTUSER.DAT` — per-user settings -- `HKCU\Software\Classes` from `UsrClass.dat` — per-user COM and associations **Never projected** — the keys that describe a *machine*: @@ -63,10 +63,18 @@ genuinely carries over: - the subtrees of `HKLM\Software\Microsoft\Windows NT\CurrentVersion` that record which physical installation this was -**Rewritten or dropped** — values that name storage by device rather than by -drive letter. `C:\Program Files\…` is correct, because C: *is* the base. -`\Device\HarddiskVolume2\…` names a volume that does not exist and must not -survive the projection. +**Rewritten** — the drive letter a never-booted Windows records for itself. A +base applied from a WIM has never run `specialize`, so its hive still describes +the *setup* environment and `SystemRoot` reads `X:\Windows`. Under Raven the +installation is C:, and `emit::rewrite_setup_drive` replaces every such letter +on the way through. + +**Known and unhandled** — values that name storage by device rather than by +drive letter. `C:\Program Files\…` is correct, because C: *is* the base, but +`\Device\HarddiskVolume2\…` names a volume that does not exist. Nothing +rewrites or drops those: the allow list reaches only `HKLM\Software` and +`HKCU\Software`, where none has yet been seen, so the case is recorded here +rather than handled. A projection that produced one would carry it through. ## How it is written @@ -144,12 +152,16 @@ makes a cached projection safe to reuse. ## Testing -- **Round-trip.** A known hive corpus projects to a known `.reg`, byte for byte. -- **The deny list holds.** No output line falls under a denied subtree — asserted - against the output, not the intent, so a rules edit that widens the allow list - too far fails a test rather than shipping. +- **What crosses, crosses.** A third-party vendor key and each named Microsoft + subtree reach the output, including through the 32-bit mirror. +- **The deny list holds.** The OS version key and the machine's identity do not + appear — asserted against the output, not the intent, so a rules edit that + widens the allow list too far fails a test rather than shipping. +- **The setup drive letter is rewritten.** `X:\` becomes `C:\` on the way + through. - **Idempotency.** Projecting twice produces identical output. -- **Device paths do not survive.** No `\Device\` reference reaches the prefix. +- **A hive that is not a hive is an error, not a panic.** The reader is fed + input a real ISO could hand it. The corpus was the awkward part, and it is settled: hives are Microsoft's and the repository cannot carry one, so the fixtures are **built at test time from a diff --git a/docs/internals/shadow-set.md b/docs/internals/shadow-set.md index c97c364..f3c114a 100644 --- a/docs/internals/shadow-set.md +++ b/docs/internals/shadow-set.md @@ -103,9 +103,9 @@ and reviewed, rather than a string that has to be believed. merges on the exact byte path, so the trees stayed separate and the mount showed both. Wine's own case-insensitivity acts a layer above the filesystem and could not help. `env create` now renames the Wine layer to the base's -spelling — `normalise_case` in `src/layer.rs`, 338 renames against a real -Windows 11 base — after which the trees merge, and the merged mount is what -carried a real installer and a running game end to end. +spelling — `normalise_case` in `crates/raven/src/layer.rs`, 338 renames +against a real Windows 11 base — after which the trees merge, and the merged +mount is what carried a real installer and a running game end to end. ## The first measured entry, and it is not a library @@ -147,17 +147,18 @@ investigation were Wine genuinely resolving activation contexts against the real store; and the reason a bare game exited while its dialog flashed was the same machinery failing earlier. -## The second measured entry, also not a library +## The second measured entry, also not a library, and since withdrawn -**`Windows\Fonts` is shadowed.** win32u re-enumerates and re-checks every font +**`Windows\Fonts` was shadowed.** win32u re-enumerates and re-checks every font file at every process start; the real base carries ~340 of them, and that was 92 of the 105 milliseconds each process spawn cost over plain Wine (227 → 135 ms with the mask — the full attribution is in [performance.md](performance.md)). Wine's own `Fonts` directory is empty and -text renders through the host's fontconfig, so the mask restores exactly the -plain-Wine situation, and the game still reaches its title screen under it. -What the entry costs: a program that reads Microsoft's font *files* — not just -the faces — will not find them. None has been seen yet; the corpus will say. +text renders through the host's fontconfig, so the mask restored exactly the +plain-Wine situation, and the game still reached its title screen under it. +What the entry cost: a program that reads Microsoft's font *files* — not just +the faces — would not find them. None was ever seen. The cost that withdrew +the entry was a different one, set out below. ## The two mechanisms diff --git a/docs/project/consolidation.md b/docs/project/consolidation.md index b43806e..48d388b 100644 --- a/docs/project/consolidation.md +++ b/docs/project/consolidation.md @@ -79,9 +79,15 @@ launched into a running environment fails. Children of an already-running program are fine — they inherit the namespace — but a second independent launch is not. -**Done when:** a second launch joins the existing namespace instead of failing, -or refuses with an error that says why. The keeper-plus-`nsenter` pattern is -already proven to work; it is not built. +**Done.** The first launch starts an *anchor* — a Raven process that creates +the namespace, mounts the overlay and then does nothing but stay alive — and +every later launch `setns`es into it, so a second independent launch joins the +same namespace and the same `wineserver` rather than failing. Joining needs no +privilege: the anchor maps the user to uid 0 inside, and owning that user +namespace is what permits entering the mount namespace. `raven env status` +marks the anchor among the holders, recognising it by what the process is +running — `session-anchor` as its first argument — rather than by its name, +because the window can hold a session too. ### 1.4 Uninstall @@ -100,11 +106,12 @@ handler pointing at a path that no longer exists. **Partly done** — see [performance.md](../internals/performance.md). A fixed workload, identical in both conditions, wall-clock: process spawn was **2.0×** -(113 → 228 ms) before the fonts mask and is **1.19×** (113 → 135 ms) since — -see 2.2 and 4 — and directory enumeration is 6.6× — of which 6.0× is the real -`System32` holding six times the entries, leaving **~17% per entry** as the -overlay's share. The lesson joined the list above: a ratio between two -differently-sized workloads measures the workload. +(113 → 228 ms), and **1.19×** (113 → 135 ms) while `Windows\Fonts` was masked +— a mask since withdrawn on correctness grounds, so 2.0× is again what a cold +spawn costs, see 2.2 and 4 — and directory enumeration is 6.6× — of which 6.0× +is the real `System32` holding six times the entries, leaving **~17% per +entry** as the overlay's share. The lesson joined the list above: a ratio +between two differently-sized workloads measures the workload. **Still open:** a frame-time or input-to-response number for the same program under Raven and under plain Wine, on the same machine, in the same scene. The @@ -121,8 +128,8 @@ trace-counting artifact (809 was lines-per-file, not caches; the real count is 9). Tested anyway, on identical control trees on plain and casefolding tmpfs: Wine detects the fold and gains nothing — same caches, same spawn time, and its non-wildcard listing path degrades to a full readdir. The real per-process -cost was `C:\windows\fonts` — see 4, where it became the shadow set's second -measured entry, worth 92 of the 105 ms. +cost was `C:\windows\fonts` — see 4, where masking it was worth 92 of the +105 ms, until that mask was given back. One finding survives `casefold`'s funeral: on a casefolding filesystem the lowercase-shadow hazard (a `wineboot` update creating a literal `windows` @@ -142,8 +149,8 @@ the server; sync is `ntsync`; steady state is the message pump. The 7.75× was an instantaneous CPU% glance, and on capture day the same glance pointed the other way while the request streams stayed identical. A whole line of attack is closed: the launch overhead is entirely client-side — and it turned out to be -the per-process font re-check, which became the shadow set's second measured -entry (see 4). Details in [performance.md](../internals/performance.md). +the per-process font re-check, which the shadow set masked until that mask was +given back (see 4). Details in [performance.md](../internals/performance.md). ### 2.4 Not the problem, so nobody re-checks it @@ -167,15 +174,16 @@ tables costing something real. ### 3.2 The package itself -**Done.** The `PKGBUILD` in `packaging/` installs all four pieces and its +**Done.** The `PKGBUILD` in `packaging/` installs every piece and its description states that installing changes what every `.exe` does: | File | Goes to | |---|---| | `raven.conf` | `/usr/lib/binfmt.d/raven.conf` — applied by pacman's own `systemd-binfmt` hook in the same transaction | | `wine-mask.conf` | `/etc/binfmt.d/wine.conf` — masks Wine's, restored on uninstall | -| `raven.desktop` | `/usr/share/applications/` | -| the binary | `/usr/bin/raven`, plus `rvn` beside it | +| `raven.desktop`, `raven-gui.desktop` | `/usr/share/applications/` | +| the binaries | `/usr/bin/raven`, plus `rvn` beside it, and `/usr/bin/raven-gui` | +| the icons | `/usr/share/icons/hicolor/`, 16 to 512, all named `raven` so the desktop entries' `Icon=` resolves | Uninstall reverses everything: the registration dies with the package instead of dangling, and the mask's removal hands `.exe` files back to Wine. A @@ -196,11 +204,16 @@ can install — the machinery has yet to run once. ## 4. The shadow set — the actual research -Two entries are measured. `Windows\WinSxS` must be hidden, or installers -render without text and ignore every click. `Windows\Fonts` must be hidden, or -every process start pays ~92 ms re-checking ~340 font files (227 → 135 ms -measured; text renders through fontconfig either way, and plain Wine's own -Fonts directory is empty). That is the whole list. +One entry is measured. `Windows\WinSxS` must be hidden, or installers render +without text and ignore every click. `Windows\Fonts` was the second and has +been given back: masking it saved ~92 ms of every process start (227 → 135 ms +measured) because `win32u` re-checks ~340 font files at each one, but a Windows +whose registry declares 961 fonts while `C:\Windows\Fonts` holds none is +incoherent, and a real Windows is the whole premise. Sessions made the trade +cheap to reverse: a launch costs about a quarter of a second rather than two, +so the same per-process 92 ms is a much smaller share of it than it was. +`layer::reconcile` un-masks the directory before every mount, so environments +created under the mask healed themselves. That is the whole list. **Next, in order:** @@ -224,8 +237,10 @@ entry, and a corpus that regression-tests it. Stated so nobody mistakes the current evidence for more than it is. -- **No 3D.** The one game run is 2D software-rendered. Nothing here says - anything about Direct3D, DXVK, or a GPU. +- **One 3D game, and no D3D12.** ShineHill — Steam, GameMaker, Direct3D 11, + 64-bit — renders through DXVK on the GPU against the real Windows, which is + one title on one driver. `raven env vkd3d` installs vkd3d-proton beside it, + but no D3D12 game has been tried, so that route is installed and unproven. - **One installer framework.** - **No anti-cheat.** It is a compatibility target, not a feature: a game whose publisher enabled the Proton path should behave the same under Raven, and diff --git a/docs/project/corpus.md b/docs/project/corpus.md index 68d5e2f..67edf49 100644 --- a/docs/project/corpus.md +++ b/docs/project/corpus.md @@ -43,7 +43,7 @@ statement about a class of them. | `media` | Audio or video missing while the game itself runs. Wine decodes through GStreamer, and a system with GStreamer's libraries but not its plugins fails silently | Not Raven's code, but Raven's job to report: `raven doctor` names the missing package | | `enumeration` | Discovers hardware through SetupDi device interfaces, which Wine never registers - Rufus is the type specimen | A Wine patch, not a Raven change | | `drm` | A copy-protection wrapper refuses before the program starts. Steam's is the common one: launched outside a running Steam client it aborts with `Application load error 5:0000065434` | Not Raven's to fix. Test a title without the wrapper, or run the store client itself | -| `d3d` | A Direct3D problem: version unsupported, device creation fails, rendering wrong | Depends. D3D 8-11 is DXVK's; D3D12 needs vkd3d-proton, which Raven does not install | +| `d3d` | A Direct3D problem: version unsupported, device creation fails, rendering wrong | Depends. D3D 8-11 is DXVK's; D3D12 needs vkd3d-proton, which `raven env vkd3d` installs | | `wine` | Fails identically under plain Wine - not Raven's doing | Upstream | | `raven` | Raven's own bug: the mount, the projection, the shadow set, the layer | **Yes - fix it** | @@ -108,8 +108,9 @@ an evening rediscovering it. Named so the gaps are visible rather than merely absent: -- **A Direct3D 12 title.** D3D12 is vkd3d-proton's, not DXVK's, and Raven - installs neither. Cyberpunk 2077 is the type specimen sitting untested. +- **A Direct3D 12 title.** D3D12 is vkd3d-proton's, not DXVK's, and while + `raven env vkd3d` installs it, nothing has yet driven it. Cyberpunk 2077 is + the type specimen sitting untested. - **A 32-bit Direct3D title.** The x32 libraries are installed and unexercised. - **Anything using COM**, the category most likely to be broken by a design decision rather than a bug. diff --git a/docs/project/landscape.md b/docs/project/landscape.md index 26c08a2..07f1478 100644 --- a/docs/project/landscape.md +++ b/docs/project/landscape.md @@ -107,7 +107,7 @@ Doing it deliberately means three things Wine has no reason to build: [../internals/registry-projection.md](../internals/registry-projection.md). 3. **The library shadow must be exact.** Some libraries must be Wine's for reasons of physics, some may be Microsoft's, and the line is only beginning - to be measured — two entries so far, neither of them a DLL. See + to be measured — one entry so far, and it is not a DLL. See [../internals/shadow-set.md](../internals/shadow-set.md). That third point is the part of Raven that is genuinely new. The first two are diff --git a/docs/project/status.md b/docs/project/status.md index 4ba85da..feac112 100644 --- a/docs/project/status.md +++ b/docs/project/status.md @@ -108,7 +108,12 @@ help text. Traced with `WINEDEBUG=+file`: Wine opens **zero** `.mui` files. So Launching a process against the real Windows cost about **+95 ms** at first. The cause was measured — win32u re-checking ~340 real font files at every process start — and masking the base's `Windows\Fonts` brought it to **135 ms -against plain Wine's 113 (1.19×)**. Four plausible theories were falsified on +against plain Wine's 113 (1.19×)**. That mask has since been withdrawn: a +Windows whose registry declares 961 fonts while `C:\Windows\Fonts` holds none +is not the real thing, and sessions made the trade cheap to reverse: a launch +costs about a quarter of a second rather than two, so the same per-process 92 +ms is a much smaller share of it than it was. Four +plausible theories were falsified on the way, the `wineserver` excess turned out not to exist at all, and the whole investigation is in [../internals/performance.md](../internals/performance.md). @@ -153,19 +158,22 @@ programs tested you cannot know what you have. ## Built -Fifteen commands, counting each leaf subcommand once. The path from an -installation image to `./program.exe` is complete, and so is recovery when -something is left holding an environment. +Twenty commands, counting each leaf subcommand once, and a window over the +same library. The path from an installation image to `./program.exe` is +complete, and so is recovery when something is left holding an environment. | | | |---|---| | `raven doctor` | namespaces, Wine, `ntsync`, what is deployed — and which handler the kernel gives `.exe` files to, with the fix when it is not Raven's | | `raven base editions` / `deploy` / `list` | the immutable Windows installations | | `raven env create` / `list` / `destroy` | environments, cheap and disposable | -| `raven env status` / `stop` | who holds a running environment's mount, and releasing it | +| `raven env status` / `stop` / `start` | who holds a running environment's mount, releasing it, and bringing one up before it is needed | +| `raven env attach` / `detach` | a real block device wired in as a raw drive | +| `raven env dxvk` / `vkd3d` | Direct3D on Vulkan, from a build you supply | | `raven env default` / `reproject` | which environment is used by default; re-run the projection | | `raven binfmt` | what to install so the kernel recognises `.exe` | | `raven launch` / `run` / `exec` | running a program, at three levels of explicitness | +| `raven-gui` | a window over the same library: environments, bases, diagnostics | Measured against the real Windows 11 base: @@ -187,11 +195,13 @@ rather than by extension, and eight projection tests against hives built by a ## Not built -Coverage, more than mechanism. One installer framework has been exercised and -one 2D game runs; NSIS, InstallShield, MSI, Squirrel, and anything touching -Direct3D are all unexplored. Concurrent launches into one environment refuse -cleanly instead of joining the running namespace. And a release has yet to -produce its first signed asset — the machinery is wired, the proof is not. +Coverage, more than mechanism. One installer framework has been exercised; +NSIS, InstallShield, MSI and Squirrel are unexplored, as is COM, as is anything +keeping its strings in `.mui` files. One game renders through Direct3D 11 and +DXVK; vkd3d-proton installs and no Direct3D 12 title has driven it. Concurrent +launches into one environment now join the running namespace rather than +refusing — see 1.3 in +[consolidation.md](consolidation.md). ## A real program, end to end @@ -223,17 +233,20 @@ Ordered by how much damage a wrong assumption would do. | | Question | Why it matters | |---|---|---| | 1 | Can Wine be made to resolve `.mui` resources? | Without it every real Windows console utility is mute, and any program that keeps its strings in MUI — which is the modern default — shows blank text. This is now the largest known gap. | -| 2 | Does the residual ~20 ms per process matter to a running game? | The fonts cost is fixed and `casefold` is tested and closed (Wine detects it on any filesystem and gains nothing). What remains needs a frame-time or input-to-response number in a real scene before it deserves an owner. | +| 2 | Does the per-process spawn cost matter to a running game? | It is attributed — win32u re-checking the base's ~340 font files — and the mask that removed it was withdrawn on correctness grounds, so it is back at about +115 ms. `casefold` is tested and closed (Wine detects it on any filesystem and gains nothing). What remains needs a frame-time or input-to-response number in a real scene before it deserves an owner. | | 3 | Which Wine files must be in the upper lower-layer? | The shadow set, now expressed as "which paths does the Wine layer need to contain". See [../internals/shadow-set.md](../internals/shadow-set.md). | -| 3 | Does the projection's `X:` to `C:` rewrite cover everything, or is a never-booted hive missing more? | `SystemRoot` was found by looking. What else describes the setup environment is unknown until something reads the whole hive. | -| 4 | What do hardened systems need? | `linux-hardened`, Ubuntu's AppArmor policy and SELinux-enforcing systems all change the mount story. Rootless Podman solves this with `fuse-overlayfs` and `context=` labelling, so the answers exist; which one Raven needs is unmeasured. | -| 5 | Does `overlayfs` accept an `ntfs3` lower layer? | Gates the "bring your own Windows partition" path only. The ISO path does not touch NTFS at all. | -| 6 | Synthetic or locally-generated registry test corpus? | The repository cannot carry Microsoft's hives. Decide before the first test, not after. | +| 4 | Does the projection's `X:` to `C:` rewrite cover everything, or is a never-booted hive missing more? | `SystemRoot` was found by looking. What else describes the setup environment is unknown until something reads the whole hive. | +| 5 | What do hardened systems need? | `linux-hardened`, Ubuntu's AppArmor policy and SELinux-enforcing systems all change the mount story. Rootless Podman solves this with `fuse-overlayfs` and `context=` labelling, so the answers exist; which one Raven needs is unmeasured. | +| 6 | Does `overlayfs` accept an `ntfs3` lower layer? | Gates the "bring your own Windows partition" path only. The ISO path does not touch NTFS at all. | | 7 | Do the dropped NTFS attributes matter? | 131 323 security descriptors, 83 967 short names and 14 287 xattr sets were discarded on deployment. Nothing is known to need them yet, and `--unix-data` is the lever if something does. | -The question that used to sit at the top of this table — whether Wine would -accept an `overlayfs` mount as its C: drive — is answered and has moved to -**Measured**. It was the one that could have invalidated the design. +Two have been answered since and moved out of the table. Whether Wine would +accept an `overlayfs` mount as its C: drive was the one that could have +invalidated the design. And the registry test corpus is settled: the +repository cannot carry Microsoft's hives, so the fixtures are built at test +time from a small `.reg` description by `regf` — a different implementation +from the `nt-hive` reader Raven uses, which is what makes the test worth +something. ## Carried upstream @@ -243,8 +256,8 @@ One finding that is not Raven's to fix. iced crate. A command-line program with no user interface cannot use it without pulling in a GUI toolkit to compute `~/.local/share/Colony/Raven/`. Eidos hit this and worked around it with `eidos-paths`; Raven will carry its own -`src/paths.rs` for the same reason, which makes it the second workaround rather -than the first. +`crates/raven/src/paths.rs` for the same reason, which makes it the second +workaround rather than the first. The fix is a `colony-paths` crate that `colony-ui` re-exports, and it belongs in Project-Colony-Resources. Raised there, not solved here. diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index 3e75730..3f10932 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -22,7 +22,14 @@ sha256sums=('SKIP') pkgver() { cd Raven - printf "0.1.0.r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" + # Read rather than repeated: the literal was left at 0.1.0 through two + # releases, so every package built from a 0.3.0 tree called itself 0.1.0 and + # pacman saw an upgrade as a downgrade. `[workspace.package]` is where + # release-please writes it, and both crates inherit it from there. + local v + v=$(sed -n '/^\[workspace\.package\]/,/^\[/ s/^version = "\([^"]*\)".*/\1/p' Cargo.toml) + printf "%s.r%s.%s" "${v:?could not read the workspace version}" \ + "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" } build() { @@ -41,6 +48,7 @@ package() { cd Raven install -Dm755 target/release/raven "$pkgdir/usr/bin/raven" ln -s raven "$pkgdir/usr/bin/rvn" + install -Dm755 target/release/raven-gui "$pkgdir/usr/bin/raven-gui" # The registration: the pacman hook applies it in the same transaction. install -Dm644 packaging/raven.conf "$pkgdir/usr/lib/binfmt.d/raven.conf" @@ -51,6 +59,7 @@ package() { install -Dm644 packaging/wine-mask.conf "$pkgdir/etc/binfmt.d/wine.conf" install -Dm644 packaging/raven.desktop "$pkgdir/usr/share/applications/raven.desktop" + install -Dm644 packaging/raven-gui.desktop "$pkgdir/usr/share/applications/raven-gui.desktop" # Icon=raven in the desktop entry resolves through the icon theme, so the # basename here, the desktop file name and Icon= must all agree or a panel diff --git a/packaging/README.md b/packaging/README.md index f251806..e66b8a1 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -1,6 +1,6 @@ # Packaging files -What the package installs beyond the binary, wired together by +What the package installs beyond the binaries, wired together by [`PKGBUILD`](PKGBUILD); the reasoning behind each file is in [../docs/internals/packaging.md](../docs/internals/packaging.md). @@ -8,6 +8,7 @@ What the package installs beyond the binary, wired together by |---|---|---| | `raven.conf` | `/usr/lib/binfmt.d/` | makes the kernel hand `.exe` files to Raven, so `./program.exe` runs from a shell — pacman's `systemd-binfmt` hook applies it in the same transaction | | `raven.desktop` | `/usr/share/applications/` | makes a **file manager** open them, which `binfmt` alone does not do | +| `raven-gui.desktop` | `/usr/share/applications/` | gives the window an application-menu entry — it manages bases and environments rather than opening a file, so it is launched by name and never resolved through a MIME type | | `wine-mask.conf` | `/etc/binfmt.d/wine.conf` | disables Wine's own `.exe` registration, which otherwise wins — an `/etc` file shadows Wine's `/usr/lib` one by name without touching a file the wine package owns | The two are needed for different things and neither replaces the other. diff --git a/packaging/raven-gui.desktop b/packaging/raven-gui.desktop new file mode 100644 index 0000000..691ed5f --- /dev/null +++ b/packaging/raven-gui.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=Raven +GenericName=Windows environment manager +Comment=Manage the Windows installations and environments Raven runs programs against +Exec=raven-gui +Icon=raven +Terminal=false +Categories=System;Settings; diff --git a/packaging/raven.conf b/packaging/raven.conf index 320b427..0f3fcaa 100644 --- a/packaging/raven.conf +++ b/packaging/raven.conf @@ -1,8 +1,11 @@ # Makes the kernel recognise Windows executables and hand them to Raven. # -# Installed to /etc/binfmt.d/ and applied by systemd-binfmt at boot. This is the -# one step that needs root, and it happens once at install time rather than from -# a service that keeps privilege while Raven runs. +# Installed to /usr/lib/binfmt.d/ - the directory for files a package owns - and +# applied by systemd-binfmt at boot. This is the one step that needs root, and it +# happens once at install time rather than from a service that keeps privilege +# while Raven runs. A file of the same name in /etc/binfmt.d/ would take +# precedence over this one, which is what `raven binfmt` prints for a build that +# no package installed. # # Fields: name, type (Magic), offset, magic, mask, interpreter, flags. # diff --git a/release-please-config.json b/release-please-config.json index 698349a..233cf82 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -2,12 +2,15 @@ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "packages": { ".": { - "release-type": "rust", + "release-type": "simple", "changelog-path": "CHANGELOG.md", "bump-minor-pre-major": false, "bump-patch-for-minor-pre-major": false, "draft": false, - "prerelease": false + "prerelease": false, + "extra-files": [ + "Cargo.toml" + ] } } }