diff --git a/.gitignore b/.gitignore index d087576ddd..5605d470d0 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ node_modules/ # ignore the output of tmps tmp/ bundle/ + +# in debugging we frequently dump wasm to wat with `wasm-tools print` +*.wat diff --git a/.vscode/settings.json b/.vscode/settings.json index e7f36e0bb4..17e18e64da 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,14 +6,19 @@ "[handlebars]": { "editor.formatOnSave": false }, + "[javascript]": { + "editor.formatOnSave": false + }, + "dioxus.formatOnSave": "disabled", // "rust-analyzer.check.workspace": true, // "rust-analyzer.check.workspace": false, // "rust-analyzer.check.features": "all", // "rust-analyzer.cargo.buildScripts.rebuildOnSave": false, // "rust-analyzer.check.workspace": false, + // "rust-analyzer.check.allTargets": true, "rust-analyzer.cargo.features": "all", "rust-analyzer.check.features": "all", - // "rust-analyzer.check.allTargets": true, - // we don't want the formatter to kick in while we're working on dioxus itself - "dioxus.formatOnSave": "disabled", + "rust-analyzer.cargo.extraArgs": [ + "--tests" + ], } diff --git a/Cargo.lock b/Cargo.lock index fba4d76c91..a73f933d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,7 +450,7 @@ dependencies = [ "reqwest 0.11.27", "ring", "rsa", - "scroll", + "scroll 0.12.0", "security-framework 2.11.1", "security-framework-sys", "semver 1.0.26", @@ -489,7 +489,7 @@ dependencies = [ "apple-xar", "cpio-archive", "flate2", - "scroll", + "scroll 0.12.0", "serde", "serde-xml-rs", "thiserror 1.0.69", @@ -512,7 +512,7 @@ dependencies = [ "md-5", "rand 0.8.5", "reqwest 0.11.27", - "scroll", + "scroll 0.12.0", "serde", "serde-xml-rs", "sha1", @@ -1343,6 +1343,13 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "barebones-template-test" +version = "0.1.0" +dependencies = [ + "dioxus", +] + [[package]] name = "base16" version = "0.2.1" @@ -1811,9 +1818,9 @@ dependencies = [ [[package]] name = "built" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" dependencies = [ "git2", ] @@ -1983,23 +1990,11 @@ dependencies = [ "serde", ] -[[package]] -name = "cargo-config2" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dc3749a36e0423c991f1e7a3e4ab0c36a1f489658313db4b187d401d79cc461" -dependencies = [ - "serde", - "serde_derive", - "toml_edit 0.22.24", - "windows-sys 0.59.0", -] - [[package]] name = "cargo-generate" -version = "0.22.1" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd20c031c5650a045e60c7bc274aa2a20d32fd604b9265e760562ceda4bdbf26" +checksum = "dd3cea99ecf678f9f12dd63f685f9ae79dca897ebb2a8a0aa1ec4dfb8b64d534" dependencies = [ "anstyle", "anyhow", @@ -2024,7 +2019,6 @@ dependencies = [ "log", "names", "paste", - "path-absolutize", "regex", "remove_dir_all", "rhai", @@ -2049,15 +2043,15 @@ dependencies = [ [[package]] name = "cargo-util-schemas" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f905f68f8cb8a8182592d9858a5895360f0a5b08b6901fdb10498fb91829804" +checksum = "e788664537bc508c6f252ca8b0e64275d89ca3ce11aeb71452a3554f390e3a65" dependencies = [ "semver 1.0.26", "serde", "serde-untagged", "serde-value", - "thiserror 1.0.69", + "thiserror 2.0.12", "toml", "unicode-xid", "url", @@ -2179,14 +2173,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", ] [[package]] name = "cfg-expr" -version = "0.17.2" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +checksum = "15b9a064fcc3a1cd4077688b2a1951e0884dcc098a06610f21d0413ccd687e67" dependencies = [ "smallvec", ] @@ -2620,9 +2614,9 @@ dependencies = [ [[package]] name = "const-str" -version = "0.5.7" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" +checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2" [[package]] name = "const-str-proc-macro" @@ -2676,6 +2670,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.8.0" @@ -2966,7 +2969,6 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.9.0", "crossterm_winapi", - "futures-core", "mio", "parking_lot", "rustix 0.38.44", @@ -2975,6 +2977,25 @@ dependencies = [ "winapi", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.0", + "crossterm_winapi", + "derive_more 2.0.1", + "document-features", + "futures-core", + "mio", + "parking_lot", + "rustix 1.0.5", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -3337,6 +3358,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "depinfo" +version = "0.6.3" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "der" version = "0.7.9" @@ -3419,7 +3447,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -3434,6 +3471,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case 0.7.1", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "des" version = "0.8.1" @@ -3503,14 +3552,17 @@ dependencies = [ "dioxus-mobile", "dioxus-native", "dioxus-router", + "dioxus-server", "dioxus-signals", "dioxus-ssr", "dioxus-web", + "dioxus_server_macro", "env_logger 0.11.8", "futures-util", "manganis", "rand 0.8.5", "serde", + "subsecond", "thiserror 2.0.12", "tokio", "tracing", @@ -3565,12 +3617,13 @@ dependencies = [ "ansi-to-html", "ansi-to-tui", "anyhow", + "ar", "axum 0.8.3", "axum-extra", "axum-server", + "backtrace", "brotli", "built", - "cargo-config2", "cargo-generate", "cargo_metadata", "cargo_toml", @@ -3580,8 +3633,9 @@ dependencies = [ "console-subscriber", "const-serialize", "convert_case 0.8.0", - "crossterm", + "crossterm 0.29.0", "ctrlc", + "depinfo", "dioxus-autofmt", "dioxus-check", "dioxus-cli-config", @@ -3617,12 +3671,14 @@ dependencies = [ "log", "manganis", "manganis-core", + "memmap", "memoize", "notify", "object 0.36.7", "once_cell", "open", "path-absolutize", + "pdb", "plist", "prettyplease", "proc-macro2", @@ -3633,9 +3689,12 @@ dependencies = [ "rustls 0.23.26", "serde", "serde_json", + "shell-words", "strum 0.27.1", + "subsecond-types", "syn 2.0.100", "tar", + "target-lexicon 0.13.2", "tauri-bundler", "tauri-utils", "tempfile", @@ -3653,8 +3712,12 @@ dependencies = [ "unicode-segmentation", "uuid", "walkdir", + "walrus", + "wasm-bindgen-externref-xform", + "wasm-encoder 0.228.0", "wasm-opt", "wasm-split-cli", + "wasmparser 0.226.0", "which 7.0.3", ] @@ -3757,6 +3820,7 @@ dependencies = [ "serde", "slab", "slotmap", + "subsecond", "sysinfo", "tokio", "tracing", @@ -3850,11 +3914,14 @@ dependencies = [ name = "dioxus-devtools" version = "0.6.3" dependencies = [ + "dioxus-cli-config", "dioxus-core", "dioxus-devtools-types", "dioxus-signals", "serde", "serde_json", + "subsecond", + "thiserror 2.0.12", "tokio", "tracing", "tungstenite 0.26.2", @@ -3867,6 +3934,7 @@ version = "0.6.3" dependencies = [ "dioxus-core", "serde", + "subsecond-types", ] [[package]] @@ -3901,14 +3969,13 @@ name = "dioxus-examples" version = "0.6.3" dependencies = [ "async-std", - "axum 0.8.3", "base64 0.22.1", "ciborium", "dioxus", "dioxus-ssr", "form_urlencoded", "futures-util", - "getrandom 0.3.2", + "getrandom 0.2.15", "http-range", "openssl", "ouroboros", @@ -3956,14 +4023,12 @@ dependencies = [ "dioxus-lib", "dioxus-mobile", "dioxus-router", - "dioxus-ssr", + "dioxus-server", "dioxus-web", "dioxus_server_macro", "futures-channel", "futures-util", "generational-box", - "http 1.3.1", - "hyper 1.6.0", "hyper-rustls 0.27.5", "once_cell", "parking_lot", @@ -3972,7 +4037,6 @@ dependencies = [ "serde", "server_fn", "thiserror 2.0.12", - "tokio", "tokio-stream", "tokio-util", "tower 0.5.2", @@ -3987,10 +4051,12 @@ dependencies = [ name = "dioxus-fullstack-hooks" version = "0.6.3" dependencies = [ + "dioxus", "dioxus-core", "dioxus-fullstack", "dioxus-fullstack-protocol", "dioxus-hooks", + "dioxus-lib", "dioxus-signals", "futures-channel", "serde", @@ -4342,6 +4408,55 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "dioxus-server" +version = "0.6.3" +dependencies = [ + "async-trait", + "aws-lc-rs", + "axum 0.8.3", + "base64 0.22.1", + "bytes", + "ciborium", + "dashmap 6.1.0", + "dioxus", + "dioxus-cli-config", + "dioxus-devtools", + "dioxus-fullstack-hooks", + "dioxus-fullstack-protocol", + "dioxus-history", + "dioxus-interpreter-js", + "dioxus-isrg", + "dioxus-lib", + "dioxus-router", + "dioxus-ssr", + "futures-channel", + "futures-util", + "generational-box", + "http 1.3.1", + "hyper 1.6.0", + "hyper-rustls 0.27.5", + "hyper-util", + "inventory", + "once_cell", + "parking_lot", + "pin-project", + "rustls 0.23.26", + "serde", + "server_fn", + "subsecond", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tower-http", + "tower-layer", + "tracing", + "tracing-futures", + "web-sys", +] + [[package]] name = "dioxus-signals" version = "0.6.3" @@ -5008,6 +5123,19 @@ name = "faster-hex" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +dependencies = [ + "serde", +] + +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] [[package]] name = "fastrand" @@ -5096,6 +5224,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.1" @@ -5273,9 +5407,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb60e7409f34ef959985bc9d9c5ee8f5db24ee46ed9775850548021710f807f" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" dependencies = [ "autocfg", "tokio", @@ -5768,9 +5902,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.19.0" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ "bitflags 2.9.0", "libc", @@ -5783,27 +5917,27 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.32.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc19e312cd45c4a66cd003f909163dc2f8e1623e30a0c0c6df3776e89b308665" +checksum = "f438c87d4028aca4b82f82ba8d8ab1569823cfb3e5bc5fa8456a71678b2a20e7" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa 1.0.15", - "thiserror 1.0.69", - "winnow 0.6.26", + "thiserror 2.0.12", + "winnow 0.7.6", ] [[package]] name = "gix-config" -version = "0.40.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e797487e6ca3552491de1131b4f72202f282fb33f198b1c34406d765b42bb0" +checksum = "9c6f830bf746604940261b49abf7f655d2c19cadc9f4142ae9379e3a316e8cfa" dependencies = [ "bstr", "gix-config-value", - "gix-features", + "gix-features 0.41.1", "gix-glob", "gix-path", "gix-ref", @@ -5811,9 +5945,9 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror 1.0.69", + "thiserror 2.0.12", "unicode-bom", - "winnow 0.6.26", + "winnow 0.7.6", ] [[package]] @@ -5843,80 +5977,120 @@ dependencies = [ [[package]] name = "gix-features" -version = "0.38.2" +version = "0.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69" +checksum = "016d6050219458d14520fe22bdfdeb9cb71631dec9bc2724767c983f60109634" dependencies = [ - "gix-hash", + "gix-path", "gix-trace", "gix-utils", "libc", "prodash", - "sha1_smol", "walkdir", ] +[[package]] +name = "gix-features" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" +dependencies = [ + "gix-trace", + "libc", + "prodash", +] + [[package]] name = "gix-fs" -version = "0.11.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" +checksum = "951e886120dc5fa8cac053e5e5c89443f12368ca36811b2e43d1539081f9c111" dependencies = [ + "bstr", "fastrand", - "gix-features", + "gix-features 0.41.1", + "gix-path", "gix-utils", + "thiserror 2.0.12", ] [[package]] name = "gix-glob" -version = "0.16.5" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" +checksum = "20972499c03473e773a2099e5fd0c695b9b72465837797a51a43391a1635a030" dependencies = [ "bitflags 2.9.0", "bstr", - "gix-features", + "gix-features 0.41.1", "gix-path", ] [[package]] name = "gix-hash" -version = "0.14.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" +checksum = "834e79722063958b03342edaa1e17595cd2939bb2b3306b3225d0815566dcb49" dependencies = [ - "faster-hex", - "thiserror 1.0.69", + "faster-hex 0.9.0", + "gix-features 0.41.1", + "sha1-checked", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-hash" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4900562c662852a6b42e2ef03442eccebf24f047d8eab4f23bc12ef0d785d8" +dependencies = [ + "faster-hex 0.10.0", + "gix-features 0.42.1", + "sha1-checked", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-hashtable" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b5cb3c308b4144f2612ff64e32130e641279fcf1a84d8d40dad843b4f64904" +dependencies = [ + "gix-hash 0.18.0", + "hashbrown 0.14.5", + "parking_lot", ] [[package]] name = "gix-lock" -version = "14.0.0" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" +checksum = "df47b8f11c34520db5541bc5fc9fbc8e4b0bdfcec3736af89ccb1a5728a0126f" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "gix-object" -version = "0.44.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5b801834f1de7640731820c2df6ba88d95480dc4ab166a5882f8ff12b88efa" +checksum = "4943fcdae6ffc135920c9ea71e0362ed539182924ab7a85dd9dac8d89b0dd69a" dependencies = [ "bstr", "gix-actor", "gix-date", - "gix-features", - "gix-hash", + "gix-features 0.41.1", + "gix-hash 0.17.0", + "gix-hashtable", + "gix-path", "gix-utils", "gix-validate", "itoa 1.0.15", "smallvec", - "thiserror 1.0.69", - "winnow 0.6.26", + "thiserror 2.0.12", + "winnow 0.7.6", ] [[package]] @@ -5934,14 +6108,14 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.47.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0d8406ebf9aaa91f55a57f053c5a1ad1a39f60fdf0303142b7be7ea44311e5" +checksum = "b2e1f7eb6b7ce82d2d19961f74bd637bab3ea79b1bc7bfb23dbefc67b0415d8b" dependencies = [ "gix-actor", - "gix-features", + "gix-features 0.41.1", "gix-fs", - "gix-hash", + "gix-hash 0.17.0", "gix-lock", "gix-object", "gix-path", @@ -5949,8 +6123,8 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 1.0.69", - "winnow 0.6.26", + "thiserror 2.0.12", + "winnow 0.7.6", ] [[package]] @@ -5967,9 +6141,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "14.0.2" +version = "17.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa" +checksum = "3d6de439bbb9a5d3550c9c7fab0e16d2d637d120fcbe0dfbc538772a187f099b" dependencies = [ "gix-fs", "libc", @@ -5986,9 +6160,9 @@ checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" [[package]] name = "gix-utils" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f" +checksum = "189f8724cf903e7fd57cfe0b7bc209db255cacdcb22c781a022f52c3a774f8d0" dependencies = [ "fastrand", "unicode-normalization", @@ -6214,7 +6388,7 @@ checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" dependencies = [ "log", "plain", - "scroll", + "scroll 0.12.0", ] [[package]] @@ -6469,6 +6643,15 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -6571,6 +6754,16 @@ dependencies = [ "http 1.3.1", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -7765,13 +7958,13 @@ dependencies = [ [[package]] name = "krates" -version = "0.17.5" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5bdd9794c39f6eb77da784fdcd065cc730a95fd0ca7d88ec945ed26c3c5109" +checksum = "c0e4748d9ac5a5c1e557d716a86d1a121caa7dee44c8a3848932c0bdebff075e" dependencies = [ "camino", - "cfg-expr 0.17.2", - "petgraph", + "cfg-expr 0.19.0", + "petgraph 0.7.1", "semver 1.0.26", "serde", "serde_json", @@ -7853,6 +8046,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.2" @@ -7901,9 +8100,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.17.0+1.8.1" +version = "0.18.1+1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" +checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" dependencies = [ "cc", "libc", @@ -8401,6 +8600,25 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.44", +] + +[[package]] +name = "memmap" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "memmap2" version = "0.9.5" @@ -9083,7 +9301,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59" dependencies = [ "objc2-encode", - "objc2-exception-helper", ] [[package]] @@ -9211,15 +9428,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - [[package]] name = "objc2-foundation" version = "0.2.2" @@ -9313,18 +9521,6 @@ dependencies = [ "objc2-user-notifications", ] -[[package]] -name = "objc2-ui-kit" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" -dependencies = [ - "bitflags 2.9.0", - "objc2 0.6.0", - "objc2-core-foundation", - "objc2-foundation 0.3.0", -] - [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -9349,20 +9545,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-web-kit" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" -dependencies = [ - "bitflags 2.9.0", - "block2 0.6.0", - "objc2 0.6.0", - "objc2-app-kit 0.3.0", - "objc2-core-foundation", - "objc2-foundation 0.3.0", -] - [[package]] name = "objc_exception" version = "0.1.2" @@ -9401,7 +9583,10 @@ version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ + "crc32fast", "flate2", + "hashbrown 0.15.2", + "indexmap 2.9.0", "memchr", "ruzstd 0.7.3", "wasmparser 0.222.1", @@ -9823,6 +10008,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pdb" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82040a392923abe6279c00ab4aff62d5250d1c8555dc780e4b02783a7aa74863" +dependencies = [ + "fallible-iterator", + "scroll 0.11.0", + "uuid", +] + [[package]] name = "pear" version = "0.2.9" @@ -9933,7 +10129,17 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.9.0", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.9.0", ] @@ -10458,9 +10664,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -10480,9 +10686,13 @@ dependencies = [ [[package]] name = "prodash" -version = "28.0.0" +version = "29.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" +checksum = "f04bb108f648884c23b98a0e940ebc2c93c0c3b89f04dbaf7eb8256ce617d1bc" +dependencies = [ + "log", + "parking_lot", +] [[package]] name = "profiling" @@ -10872,7 +11082,7 @@ dependencies = [ "bitflags 2.9.0", "cassowary", "compact_str", - "crossterm", + "crossterm 0.28.1", "indoc", "instability", "itertools 0.13.0", @@ -11077,9 +11287,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "remove_dir_all" -version = "0.8.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a694f9e0eb3104451127f6cc1e5de55f59d3b1fc8c5ddfaeb6f1e716479ceb4a" +checksum = "808cc0b475acf76adf36f08ca49429b12aad9f678cb56143d5b3cb49b9a1dd08" dependencies = [ "cfg-if", "cvt", @@ -11244,9 +11454,9 @@ dependencies = [ [[package]] name = "rhai" -version = "1.20.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0277a46f29fe3b3eb10821ca2c65a4751b686b6c84422aae31695ba167b0fbc" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" dependencies = [ "ahash 0.8.11", "bitflags 2.9.0", @@ -11671,11 +11881,10 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" dependencies = [ - "lazy_static", "regex", ] @@ -11706,6 +11915,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" + [[package]] name = "scroll" version = "0.12.0" @@ -11973,13 +12188,13 @@ dependencies = [ [[package]] name = "serde_qs" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6" dependencies = [ "percent-encoding", "serde", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -12059,14 +12274,14 @@ dependencies = [ [[package]] name = "server_fn" -version = "0.8.0-rc1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b279618eba9bf2298336a6b5f72c84bee46a5a76ad048a5763b552d69393988" +checksum = "09b0f92b9d3a62c73f238ac21f7a09f15bad335a9d1651514d9da80d2eaf8d4c" dependencies = [ "axum 0.8.3", "base64 0.22.1", "bytes", - "const-str 0.5.7", + "const-str 0.6.2", "const_format", "dashmap 6.1.0", "futures", @@ -12079,6 +12294,7 @@ dependencies = [ "once_cell", "pin-project-lite", "reqwest 0.12.15", + "rustc_version 0.4.1", "rustversion", "send_wrapper", "serde", @@ -12101,12 +12317,12 @@ dependencies = [ [[package]] name = "server_fn_macro" -version = "0.8.0-rc1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c805d4055915c94d1b168bec37afc9fadcfd7c17798c63259e303667076a67" +checksum = "341dd1087afe9f3e546c5979a4f0b6d55ac072e1201313f86e7fe364223835ac" dependencies = [ "const_format", - "convert_case 0.6.0", + "convert_case 0.8.0", "proc-macro2", "quote", "rustc_version 0.4.1", @@ -12116,9 +12332,9 @@ dependencies = [ [[package]] name = "server_fn_macro_default" -version = "0.8.0-rc1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63085deb77b447034fd865a7a054c0a234bd1ce93c9cf13dd02e0138773930f" +checksum = "bc5ab934f581482a66da82f2b57b15390ad67c9ab85bd9a6c54bb65060fb1380" dependencies = [ "server_fn_macro", "syn 2.0.100", @@ -12560,9 +12776,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e22987355fbf8cfb813a0cf8cd97b1b4ec834b94dbd759a9e8679d41fabe83" +checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" dependencies = [ "sqlx-core", "sqlx-macros", @@ -12573,9 +12789,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c4720d7d4cd3d5b00f61d03751c685ad09c33ae8290c8a2c11335e0604300b" +checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" dependencies = [ "base64 0.22.1", "bigdecimal", @@ -12620,9 +12836,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175147fcb75f353ac7675509bc58abb2cb291caf0fd24a3623b8f7e3eb0a754b" +checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" dependencies = [ "proc-macro2", "quote", @@ -12633,9 +12849,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cde983058e53bfa75998e1982086c5efe3c370f3250bf0357e344fa3352e32b" +checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" dependencies = [ "dotenvy", "either", @@ -12659,9 +12875,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847d2e5393a4f39e47e4f36cab419709bc2b83cbe4223c60e86e1471655be333" +checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" dependencies = [ "atoi", "base64 0.22.1", @@ -12706,9 +12922,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc35947a541b9e0a2e3d85da444f1c4137c13040267141b208395a0d0ca4659f" +checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" dependencies = [ "atoi", "base64 0.22.1", @@ -12753,9 +12969,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c48291dac4e5ed32da0927a0b981788be65674aeb62666d19873ab4289febde" +checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" dependencies = [ "atoi", "chrono", @@ -13105,6 +13321,30 @@ dependencies = [ "url", ] +[[package]] +name = "subsecond" +version = "0.6.3" +dependencies = [ + "js-sys", + "libc", + "libloading 0.8.6", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.6.3" +dependencies = [ + "serde", +] + [[package]] name = "subtle" version = "2.6.1" @@ -13203,7 +13443,7 @@ dependencies = [ "is-macro", "once_cell", "parking_lot", - "petgraph", + "petgraph 0.6.5", "radix_fmt", "relative-path", "swc_atoms", @@ -13464,7 +13704,7 @@ dependencies = [ "dashmap 5.5.3", "indexmap 2.9.0", "once_cell", - "petgraph", + "petgraph 0.6.5", "rustc-hash 1.1.0", "serde_json", "swc_atoms", @@ -13549,7 +13789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c22e0a0478b1b06610453a97c8371cafa742e371a79aff860ccfbabe1ab160a7" dependencies = [ "indexmap 2.9.0", - "petgraph", + "petgraph 0.6.5", "rustc-hash 1.1.0", "swc_common", ] @@ -13561,7 +13801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b9841af596d2ddb37e56defca81387b60a14863e251cede839d1e349e6209d" dependencies = [ "auto_impl", - "petgraph", + "petgraph 0.6.5", "swc_common", "swc_fast_graph", "tracing", @@ -13762,9 +14002,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.32.8" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1" +checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ "bitflags 2.9.0", "core-foundation 0.10.0", @@ -13794,8 +14034,8 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.60.0", - "windows-core 0.60.1", + "windows 0.61.1", + "windows-core 0.61.0", "windows-version", "x11-dl", ] @@ -13834,6 +14074,15 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +dependencies = [ + "serde", +] + [[package]] name = "target-triple" version = "0.1.4" @@ -13952,14 +14201,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.2", "once_cell", - "rustix 0.38.44", + "rustix 1.0.5", "windows-sys 0.59.0", ] @@ -15242,7 +15491,7 @@ dependencies = [ "log", "rayon", "walrus-macro", - "wasm-encoder", + "wasm-encoder 0.214.0", "wasmparser 0.214.0", ] @@ -15342,6 +15591,17 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-externref-xform" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940542c5cdbe96c35f98b5da5c65fb9d18df55a0cb1d81fc5ca4acc4fda4d61c" +dependencies = [ + "anyhow", + "walrus", + "wasm-bindgen-wasm-conventions", +] + [[package]] name = "wasm-bindgen-futures" version = "0.4.50" @@ -15411,6 +15671,19 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "wasm-bindgen-wasm-conventions" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c24fcaa34d2d84407122cfb1d3f37c3586756cf462be18e049b49245a16c08" +dependencies = [ + "anyhow", + "leb128", + "log", + "walrus", + "wasmparser 0.214.0", +] + [[package]] name = "wasm-encoder" version = "0.214.0" @@ -15420,6 +15693,16 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d30290541f2d4242a162bbda76b8f2d8b1ac59eab3568ed6f2327d52c9b2c4" +dependencies = [ + "leb128fmt", + "wasmparser 0.228.0", +] + [[package]] name = "wasm-opt" version = "0.116.1" @@ -15493,7 +15776,7 @@ dependencies = [ "dioxus", "dioxus-router", "futures", - "getrandom 0.3.2", + "getrandom 0.2.15", "js-sys", "once_cell", "reqwest 0.12.15", @@ -15572,6 +15855,17 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.228.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3" +dependencies = [ + "bitflags 2.9.0", + "indexmap 2.9.0", + "semver 1.0.26", +] + [[package]] name = "wayland-backend" version = "0.3.8" @@ -15779,16 +16073,16 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.36.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d606f600e5272b514dbb66539dd068211cc20155be8d3958201b4b5bd79ed3" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.60.0", - "windows-core 0.60.1", - "windows-implement 0.59.0", - "windows-interface 0.59.1", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-implement 0.58.0", + "windows-interface 0.58.0", ] [[package]] @@ -15804,13 +16098,13 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.36.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb27fccd3c27f68e9a6af1bcf48c2d82534b8675b83608a4d81446d095a17ac" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" dependencies = [ - "thiserror 2.0.12", - "windows 0.60.0", - "windows-core 0.60.1", + "thiserror 1.0.69", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] @@ -16156,12 +16450,12 @@ dependencies = [ [[package]] name = "windows" -version = "0.60.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-future", "windows-link", "windows-numerics", @@ -16169,11 +16463,11 @@ dependencies = [ [[package]] name = "windows-collections" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", ] [[package]] @@ -16210,19 +16504,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.1", - "windows-link", - "windows-result 0.3.2", - "windows-strings 0.3.1", -] - [[package]] name = "windows-core" version = "0.61.0" @@ -16238,11 +16519,11 @@ dependencies = [ [[package]] name = "windows-future" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-link", ] @@ -16268,17 +16549,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -16331,11 +16601,11 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-numerics" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.60.1", + "windows-core 0.61.0", "windows-link", ] @@ -16729,7 +16999,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit 0.2.2", + "objc2-ui-kit", "orbclient", "percent-encoding", "pin-project", @@ -16764,15 +17034,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.6" @@ -16821,13 +17082,14 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.50.5" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730" +checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" dependencies = [ "base64 0.22.1", - "block2 0.6.0", - "cookie", + "block", + "cocoa 0.26.0", + "core-graphics 0.24.0", "crossbeam-channel", "dpi", "dunce", @@ -16840,25 +17102,20 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.0", - "objc2-app-kit 0.3.0", - "objc2-core-foundation", - "objc2-foundation 0.3.0", - "objc2-ui-kit 0.3.0", - "objc2-web-kit", + "objc", + "objc_id", "once_cell", "percent-encoding", "raw-window-handle 0.6.2", "sha2", "soup3", "tao-macros", - "thiserror 2.0.12", - "url", + "thiserror 1.0.69", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.60.0", - "windows-core 0.60.1", + "windows 0.58.0", + "windows-core 0.58.0", "windows-version", "x11-dl", ] diff --git a/Cargo.toml b/Cargo.toml index 60a92d156c..e5d392ee19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,8 @@ members = [ "packages/config-macros", "packages/native", "packages/asset-resolver", + "packages/depinfo", + "packages/server", # Playwright tests "packages/playwright-tests/liveview", @@ -89,6 +91,10 @@ members = [ "packages/wasm-split/wasm-split-cli", "packages/wasm-split/wasm-used", + # subsecond + "packages/subsecond/subsecond", + "packages/subsecond/subsecond-types", + # Full project examples "example-projects/fullstack-hackernews", "example-projects/ecommerce-site", @@ -107,6 +113,7 @@ members = [ # Playwright tests "packages/playwright-tests/liveview", "packages/playwright-tests/web", + "packages/playwright-tests/barebones-template", "packages/playwright-tests/fullstack", "packages/playwright-tests/fullstack-mounted", "packages/playwright-tests/fullstack-routing", @@ -114,7 +121,7 @@ members = [ "packages/playwright-tests/nested-suspense", "packages/playwright-tests/cli-optimization", "packages/playwright-tests/wasm-split-harness", - "packages/playwright-tests/default-features-disabled" + "packages/playwright-tests/default-features-disabled", ] [workspace.package] @@ -152,6 +159,7 @@ dioxus-cli-config = { path = "packages/cli-config", version = "0.6.2" } dioxus-cli-opt = { path = "packages/cli-opt", version = "0.6.2" } dioxus-devtools = { path = "packages/devtools", version = "0.6.2" } dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.2" } +dioxus-server = { path = "packages/server", version = "0.6.2" } dioxus-fullstack = { path = "packages/fullstack", version = "0.6.2" } dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" } dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", version = "0.6.3" } @@ -166,16 +174,22 @@ const-serialize-macro = { path = "packages/const-serialize-macro", version = "0. generational-box = { path = "packages/generational-box", version = "0.6.2" } lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.2" } +# subsecond +subsecond-types = { path = "packages/subsecond/subsecond-types", version = "0.6.3" } +subsecond = { path = "packages/subsecond/subsecond", version = "0.6.3" } +# manganis manganis = { path = "packages/manganis/manganis", version = "0.6.2" } manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.2" } manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.2" } +# wasm-split wasm-split = { path = "packages/wasm-split/wasm-split", version = "0.1.0" } wasm-split-macro = { path = "packages/wasm-split/wasm-split-macro", version = "0.1.0" } wasm-split-cli = { path = "packages/wasm-split/wasm-split-cli", version = "0.1.0" } wasm-split-harness = { path = "packages/playwright-tests/wasm-split-harness", version = "0.1.0" } +depinfo = { path = "packages/depinfo", version = "0.6.3" } warnings = { version = "0.2.1" } # a fork of pretty please for tests - let's get off of this if we can! @@ -209,8 +223,10 @@ tauri-utils = { version = "2.2.0" } tauri-bundler = { version = "2.2.4" } lru = "0.13.0" async-trait = "0.1.87" -axum = "0.8.1" +axum = { version = "0.8.1", default-features = false } axum-server = { version = "0.7.1", default-features = false } +server_fn = { version = "0.8.2", default-features = false } +server_fn_macro = { version = "0.8.2" } tower = "0.5.2" http = "1.2.0" notify = { version = "8.0.0" } @@ -268,11 +284,14 @@ sha2 = "0.10.8" walrus = { version = "0.23.3", features = ["parallel"] } id-arena = "2.2.1" async-compression = { version = "0.4.20", features = ["futures-io", "gzip", "brotli"] } -getrandom = { version = "0.3.1" } +getrandom = { version = "0.2.0" } async-once-cell = { version = "0.5.4" } rayon = "1.10.0" wasmparser = "0.226.0" itertools = "0.14.0" +object = { version = "0.36.0" } +bincode = "1.3.3" +inventory = { version = "0.3.5" } macro-string = "0.1.4" walkdir = "2.5.0" url = "2.3.1" @@ -280,10 +299,15 @@ separator = "0.4.1" pretty_assertions = "1.4.0" serde_repr = "0.1" hyper-util = "0.1.10" +krates = { version = "0.19.0" } +libloading = "0.8.6" +libc = "0.2.170" +memmap2 = "0.9.5" +memfd = "0.6.4" # desktop -wry = { version = "0.50.3", default-features = false } -tao = { version = "0.32.6", features = ["rwh_05"] } +wry = { version = "0.45.0", default-features = false } +tao = { version = "0.33.0", features = ["rwh_05"] } webbrowser = "1.0.3" infer = "0.19.0" dunce = "1.0.5" @@ -301,13 +325,20 @@ open = "5.1.2" # web gloo-dialogs = "0.2.0" +# tui stuff +ansi-to-tui = "7.0" +ansi-to-html = "0.2.1" +path-absolutize = "3.1" +crossterm = { version = "0.29.0" } +ratatui = { version = "0.29.0" } +shell-words = "1.1.0" +color-eyre = "0.6.3" + # native keyboard-types = { version = "0.7", default-features = false } winit = { version = "0.30.2", features = ["rwh_06"] } -# disable debug symbols in dev builds - shouldn't matter for downstream crates but makes our binaries (examples, cli, etc) build faster [profile.dev] -debug = 0 # our release profile should be fast to compile and fast to run # when we ship our CI builds, we turn on LTO which improves perf leftover by turning on incremental @@ -315,7 +346,7 @@ debug = 0 incremental = true # crank up the opt level for wasm-split-cli in dev mode -# important here that lto is on and the debug symbols are presenta (since they're used by wasm-opt)a +# important here that lto is on and the debug symbols are present (since they're used by wasm-opt)a [profile.wasm-split-release] inherits = "release" opt-level = 'z' @@ -386,13 +417,12 @@ separator = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } rand = { workspace = true, features = ["small_rng"] } -form_urlencoded = "1.2.1" -async-std = "1.13.0" +form_urlencoded = "1.2.0" +async-std = "1.12.0" web-time = "1.1.0" -axum = { workspace = true, default-features = true } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -getrandom = { workspace = true } +getrandom = { workspace = true, features = ["js"] } tokio = { version = "1.43", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] diff --git a/examples/fullstack-auth/Cargo.toml b/examples/fullstack-auth/Cargo.toml index 40c7bfbaf5..f792dce515 100644 --- a/examples/fullstack-auth/Cargo.toml +++ b/examples/fullstack-auth/Cargo.toml @@ -48,17 +48,17 @@ optional = true [features] default = [] server = [ - "dioxus-cli-config", - "axum", - "tokio", - "dioxus-fullstack/axum", - "tower-http", - "async-trait", - "sqlx", - "axum_session", - "axum_session_auth", - "axum_session_sqlx", - "http", - "tower", + "dioxus-fullstack/server", + "dep:dioxus-cli-config", + "dep:axum", + "dep:tokio", + "dep:tower-http", + "dep:async-trait", + "dep:sqlx", + "dep:axum_session", + "dep:axum_session_auth", + "dep:axum_session_sqlx", + "dep:http", + "dep:tower", ] -web = ["dioxus/web", "dioxus-web"] +web = ["dioxus/web", "dep:dioxus-web"] diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index 640f6d7392..7a5d134b08 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -69,6 +69,7 @@ pub const APP_TITLE_ENV: &str = "DIOXUS_APP_TITLE"; #[doc(hidden)] pub const OUT_DIR: &str = "DIOXUS_OUT_DIR"; pub const SESSION_CACHE_DIR: &str = "DIOXUS_SESSION_CACHE_DIR"; +pub const BUILD_ID: &str = "DIOXUS_BUILD_ID"; /// Reads an environment variable at runtime in debug mode or at compile time in /// release mode. When bundling in release mode, we will not be running under the @@ -306,3 +307,20 @@ pub fn session_cache_dir() -> Option { pub fn android_session_cache_dir() -> PathBuf { PathBuf::from("/data/local/tmp/dx/") } + +/// The unique build id for this application, used to disambiguate between different builds of the same +/// application. +pub fn build_id() -> u64 { + #[cfg(target_arch = "wasm32")] + { + 0 + } + + #[cfg(not(target_arch = "wasm32"))] + { + std::env::var(BUILD_ID) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0) + } +} diff --git a/packages/cli-opt/Cargo.toml b/packages/cli-opt/Cargo.toml index 105e26342e..06c3210194 100644 --- a/packages/cli-opt/Cargo.toml +++ b/packages/cli-opt/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"] anyhow = { workspace = true } manganis = { workspace = true } manganis-core = { workspace = true } -object = {version="0.36.7", features=["wasm"]} +object = { workspace = true, features = ["wasm"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } const-serialize = { workspace = true, features = ["serde"] } diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 9a4bf093a7..09b87db1df 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -24,6 +24,8 @@ dioxus-cli-opt = { workspace = true } dioxus-fullstack = { workspace = true } dioxus-dx-wire-format = { workspace = true } wasm-split-cli = { workspace = true } +depinfo = { workspace = true } +subsecond-types = { workspace = true } clap = { workspace = true, features = ["derive", "cargo"] } convert_case = { workspace = true } @@ -32,7 +34,7 @@ uuid = { workspace = true, features = ["v4"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } toml = { workspace = true } -cargo_toml = { workspace = true } +cargo_toml = { workspace = true, features = ["features"] } futures-util = { workspace = true, features = ["async-await-macro"] } notify = { workspace = true, features = ["serde"] } html_parser = { workspace = true } @@ -47,8 +49,8 @@ hyper-rustls = { workspace = true } rustls = { workspace = true } rayon = { workspace = true } futures-channel = { workspace = true } -cargo-config2 = { workspace = true, optional = true } -krates = { version = "0.17.5" } +target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] } +krates = { workspace = true } regex = "1.11.1" console = "0.15.11" ctrlc = "3.4.5" @@ -75,7 +77,7 @@ which = { version = "7.0.2" } # plugin packages open = { workspace = true } -cargo-generate = "0.22.1" +cargo-generate = "0.23.3" toml_edit = "0.22.24" # formatting @@ -92,11 +94,12 @@ tracing-subscriber = { version = "0.3.19", features = ["std", "env-filter", "jso console-subscriber = { version = "0.4.1", optional = true } tracing = { workspace = true } wasm-opt = { version = "0.116.1", optional = true } -crossterm = { version = "0.28.1", features = ["event-stream"] } -ansi-to-tui = "7.0.0" -ansi-to-html = "0.2.2" -path-absolutize = "3.1" -ratatui = { version = "0.29.0", features = ["crossterm", "unstable"] } +ansi-to-tui = { workspace = true } +ansi-to-html = { workspace = true } +path-absolutize = { workspace = true } +crossterm = { workspace = true, features = ["event-stream"] } +ratatui = { workspace = true, features = ["crossterm", "unstable"] } +shell-words = { workspace = true } # disable `log` entirely since `walrus` uses it and is *much* slower with it enableda log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] } @@ -107,13 +110,16 @@ manganis = { workspace = true } manganis-core = { workspace = true } # Extracting data from an executable -object = { version = "0.36.7", features = ["wasm"] } +object = { workspace = true, features = ["all"] } tokio-util = { workspace = true, features = ["full"] } itertools = { workspace = true } throbber-widgets-tui = "0.8.0" unicode-segmentation = "1.12.0" handlebars = "6.3.1" strum = { version = "0.27.1", features = ["derive"] } +memmap = "0.7.0" +walrus = { workspace = true, features = ["parallel"]} +wasmparser = { workspace = true } tauri-utils = { workspace = true } tauri-bundler = { workspace = true } @@ -124,6 +130,11 @@ local-ip-address = "0.6.3" dircpy = "0.3.19" plist = "1.7.0" memoize = "0.5.1" +wasm-encoder = "0.228.0" +backtrace = "0.3.74" +ar = "0.9.0" +wasm-bindgen-externref-xform = "0.2.100" +pdb = "0.8.0" [build-dependencies] built = { version = "0.7.5", features = ["git2"] } diff --git a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs index e2d3978168..811a606239 100644 --- a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs +++ b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs @@ -3,6 +3,8 @@ diff --git a/packages/cli/assets/web/dev.index.html b/packages/cli/assets/web/dev.index.html new file mode 100644 index 0000000000..9bcac1da3b --- /dev/null +++ b/packages/cli/assets/web/dev.index.html @@ -0,0 +1,184 @@ + + + + {app_title} + + + + + + + +
+
+
+
+
+
+
+ + + + + +

Your app is being rebuilt.

+
+

A non-hot-reloadable change occurred and we must rebuild.

+
+
+
+
+ + diff --git a/packages/cli/assets/web/loading.html b/packages/cli/assets/web/dev.loading.html similarity index 100% rename from packages/cli/assets/web/loading.html rename to packages/cli/assets/web/dev.loading.html diff --git a/packages/cli/assets/web/index.html b/packages/cli/assets/web/index.html deleted file mode 100644 index e07ae83045..0000000000 --- a/packages/cli/assets/web/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - {app_title} - - - - - -
- - diff --git a/packages/cli/assets/web/loading.js b/packages/cli/assets/web/loading.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/cli/assets/web/prod.index.html b/packages/cli/assets/web/prod.index.html new file mode 100644 index 0000000000..a7188671bd --- /dev/null +++ b/packages/cli/assets/web/prod.index.html @@ -0,0 +1,11 @@ + + + {app_title} + + + + + +
+ + diff --git a/packages/cli/assets/web/toast.html b/packages/cli/assets/web/toast.html deleted file mode 100644 index 2344074dfc..0000000000 --- a/packages/cli/assets/web/toast.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - \ No newline at end of file diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index 0d24506066..2fc4e55f30 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -1,10 +1,29 @@ use crate::{ - AppBundle, BuildArgs, BuildRequest, BuildStage, BuildUpdate, DioxusCrate, ProgressRx, - ProgressTx, Result, StructuredOutput, + BuildArtifacts, BuildRequest, BuildStage, BuilderUpdate, Platform, ProgressRx, ProgressTx, + Result, StructuredOutput, }; -use std::time::{Duration, Instant}; +use anyhow::Context; +use dioxus_cli_opt::process_file_to; +use futures_util::future::OptionFuture; +use std::{ + env, + time::{Duration, Instant, SystemTime}, +}; +use std::{ + net::SocketAddr, + path::{Path, PathBuf}, + process::Stdio, +}; +use subsecond_types::JumpTable; +use tokio::{ + io::{AsyncBufReadExt, BufReader, Lines}, + process::{Child, ChildStderr, ChildStdout, Command}, + task::JoinHandle, +}; + +use super::{BuildContext, BuildId, BuildMode, HotpatchModuleCache}; -/// The component of the serve engine that watches ongoing builds and manages their state, handle, +/// The component of the serve engine that watches ongoing builds and manages their state, open handle, /// and progress. /// /// Previously, the builder allowed multiple apps to be built simultaneously, but this newer design @@ -12,82 +31,173 @@ use std::time::{Duration, Instant}; /// /// Here, we track the number of crates being compiled, assets copied, the times of these events, and /// other metadata that gives us useful indicators for the UI. -pub(crate) struct Builder { - // Components of the build - pub krate: DioxusCrate, - pub request: BuildRequest, - pub build: tokio::task::JoinHandle>, +/// +/// A handle to a running app. +/// +/// The actual child processes might not be present (web) or running (died/killed). +/// +/// The purpose of this struct is to accumulate state about the running app and its server, like +/// any runtime information needed to hotreload the app or send it messages. +/// +/// We might want to bring in websockets here too, so we know the exact channels the app is using to +/// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super +/// duper useful. +/// +/// todo: restructure this such that "open" is a running task instead of blocking the main thread +pub(crate) struct AppBuilder { pub tx: ProgressTx, pub rx: ProgressRx, + // The original request with access to its build directory + pub build: BuildRequest, + + // Ongoing build task, if any + pub build_task: JoinHandle>, + + // If a build has already finished, we'll have its artifacts (rustc, link args, etc) to work with + pub artifacts: Option, + + /// The aslr offset of this running app + pub aslr_reference: Option, + + /// The list of patches applied to the app, used to know which ones to reapply and/or iterate from. + pub patches: Vec, + pub patch_cache: Option, + + /// The virtual directory that assets will be served from + /// Used mostly for apk/ipa builds since they live in simulator + pub runtime_asset_dir: Option, + + // These might be None if the app died or the user did not specify a server + pub child: Option, + + // stdio for the app so we can read its stdout/stderr + // we don't map stdin today (todo) but most apps don't need it + pub stdout: Option>>, + pub stderr: Option>>, + + /// The executables but with some extra entropy in their name so we can run two instances of the + /// same app without causing collisions on the filesystem. + pub entropy_app_exe: Option, + pub builds_opened: usize, + // Metadata about the build that needs to be managed by watching build updates // used to render the TUI pub stage: BuildStage, pub compiled_crates: usize, - pub compiled_crates_server: usize, pub expected_crates: usize, - pub expected_crates_server: usize, pub bundling_progress: f64, pub compile_start: Option, pub compile_end: Option, - pub compile_end_server: Option, pub bundle_start: Option, pub bundle_end: Option, } -impl Builder { - /// Create a new builder and immediately start a build - pub(crate) fn start(krate: &DioxusCrate, args: BuildArgs) -> Result { +impl AppBuilder { + /// Create a new `AppBuilder` and immediately start a build process. + /// + /// This method initializes the builder with the provided `BuildRequest` and spawns an asynchronous + /// task (`build_task`) to handle the build process. The build process involves several stages: + /// + /// 1. **Tooling Verification**: Ensures that the necessary tools are available for the build. + /// 2. **Build Directory Preparation**: Sets up the directory structure required for the build. + /// 3. **Build Execution**: Executes the build process asynchronously. + /// 4. **Bundling**: Packages the built artifacts into a final bundle. + /// + /// The `build_task` is a Tokio task that runs the build process in the background. It uses a + /// `BuildContext` to manage the build state and communicate progress or errors via a message + /// channel (`tx`). + /// + /// The builder is initialized with default values for various fields, such as the build stage, + /// progress metrics, and optional runtime configurations. + /// + /// # Notes + /// + /// - The `build_task` is immediately spawned and will run independently of the caller. + /// - The caller can use other methods on the `AppBuilder` to monitor the build progress or handle + /// updates (e.g., `wait`, `finish_build`). + /// - The build process is designed to be cancellable and restartable using methods like `abort_all` + /// or `rebuild`. + pub(crate) fn start(request: &BuildRequest, mode: BuildMode) -> Result { let (tx, rx) = futures_channel::mpsc::unbounded(); - let request = BuildRequest::new(krate.clone(), args, tx.clone()); Ok(Self { - krate: krate.clone(), - request: request.clone(), + build: request.clone(), stage: BuildStage::Initializing, - build: tokio::spawn(async move { - // On the first build, we want to verify the tooling - // We wont bother verifying on subsequent builds - request.verify_tooling().await?; - - request.build_all().await + build_task: tokio::spawn({ + let request = request.clone(); + let tx = tx.clone(); + async move { + let ctx = BuildContext { + mode, + tx: tx.clone(), + }; + request.verify_tooling(&ctx).await?; + request.prepare_build_dir()?; + request.build(&ctx).await + } }), tx, rx, + patches: vec![], compiled_crates: 0, expected_crates: 1, - expected_crates_server: 1, - compiled_crates_server: 0, bundling_progress: 0.0, + builds_opened: 0, compile_start: Some(Instant::now()), + aslr_reference: None, compile_end: None, - compile_end_server: None, bundle_start: None, bundle_end: None, + runtime_asset_dir: None, + child: None, + stderr: None, + stdout: None, + entropy_app_exe: None, + artifacts: None, + patch_cache: None, }) } /// Wait for any new updates to the builder - either it completed or gave us a message etc - pub(crate) async fn wait(&mut self) -> BuildUpdate { + pub(crate) async fn wait(&mut self) -> BuilderUpdate { use futures_util::StreamExt; + use BuilderUpdate::*; // Wait for the build to finish or for it to emit a status message let update = tokio::select! { Some(progress) = self.rx.next() => progress, - bundle = (&mut self.build) => { + bundle = (&mut self.build_task) => { // Replace the build with an infinitely pending task so we can select it again without worrying about deadlocks/spins - self.build = tokio::task::spawn(std::future::pending()); + self.build_task = tokio::task::spawn(std::future::pending()); match bundle { - Ok(Ok(bundle)) => BuildUpdate::BuildReady { bundle }, - Ok(Err(err)) => BuildUpdate::BuildFailed { err }, - Err(err) => BuildUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:?}", err)) }, + Ok(Ok(bundle)) => BuilderUpdate::BuildReady { bundle }, + Ok(Err(err)) => BuilderUpdate::BuildFailed { err }, + Err(err) => BuilderUpdate::BuildFailed { err: crate::Error::Runtime(format!("Build panicked! {:#?}", err)) }, } }, + Some(Ok(Some(msg))) = OptionFuture::from(self.stdout.as_mut().map(|f| f.next_line())) => { + StdoutReceived { msg } + }, + Some(Ok(Some(msg))) = OptionFuture::from(self.stderr.as_mut().map(|f| f.next_line())) => { + StderrReceived { msg } + }, + Some(status) = OptionFuture::from(self.child.as_mut().map(|f| f.wait())) => { + // Panicking here is on purpose. If the task crashes due to a JoinError (a panic), + // we want to propagate that panic up to the serve controller. + let status = status.unwrap(); + self.child = None; + + ProcessExited { status } + } }; // Update the internal stage of the build so the UI can render it + // *VERY IMPORTANT* - DO NOT AWAIT HERE + // doing so will cause the changes to be lost since this wait call is called under a cancellable task + // todo - move this handling to a separate function that won't be cancelled match &update { - BuildUpdate::Progress { stage } => { + BuilderUpdate::Progress { stage } => { // Prevent updates from flowing in after the build has already finished if !self.is_finished() { self.stage = stage.clone(); @@ -95,33 +205,15 @@ impl Builder { match stage { BuildStage::Initializing => { self.compiled_crates = 0; - self.compiled_crates_server = 0; self.bundling_progress = 0.0; } - BuildStage::Starting { - crate_count, - is_server, - } => { - if *is_server { - self.expected_crates_server = *crate_count; - } else { - self.expected_crates = *crate_count; - } + BuildStage::Starting { crate_count, .. } => { + self.expected_crates = *crate_count; } BuildStage::InstallingTooling => {} - BuildStage::Compiling { - current, - total, - is_server, - .. - } => { - if *is_server { - self.compiled_crates_server = *current; - self.expected_crates_server = *total; - } else { - self.compiled_crates = *current; - self.expected_crates = *total; - } + BuildStage::Compiling { current, total, .. } => { + self.compiled_crates = *current; + self.expected_crates = *total; if self.compile_start.is_none() { self.compile_start = Some(Instant::now()); @@ -138,18 +230,15 @@ impl Builder { } BuildStage::Success => { self.compiled_crates = self.expected_crates; - self.compiled_crates_server = self.expected_crates_server; self.bundling_progress = 1.0; } BuildStage::Failed => { self.compiled_crates = self.expected_crates; - self.compiled_crates_server = self.expected_crates_server; self.bundling_progress = 1.0; } BuildStage::Aborted => {} BuildStage::Restarting => { self.compiled_crates = 0; - self.compiled_crates_server = 0; self.expected_crates = 1; self.bundling_progress = 0.0; } @@ -158,53 +247,100 @@ impl Builder { } } } - BuildUpdate::CompilerMessage { .. } => {} - BuildUpdate::BuildReady { .. } => { + BuilderUpdate::CompilerMessage { .. } => {} + BuilderUpdate::BuildReady { .. } => { self.compiled_crates = self.expected_crates; - self.compiled_crates_server = self.expected_crates_server; self.bundling_progress = 1.0; self.stage = BuildStage::Success; self.complete_compile(); self.bundle_end = Some(Instant::now()); } - BuildUpdate::BuildFailed { .. } => { + BuilderUpdate::BuildFailed { .. } => { tracing::debug!("Setting builder to failed state"); self.stage = BuildStage::Failed; } + StdoutReceived { .. } => {} + StderrReceived { .. } => {} + ProcessExited { .. } => {} } update } - /// Restart this builder with new build arguments. - pub(crate) fn rebuild(&mut self, args: BuildArgs) { + pub(crate) fn patch_rebuild(&mut self, changed_files: Vec) { + // We need the rustc args from the original build to pass to the new build + let Some(artifacts) = self.artifacts.as_ref().cloned() else { + tracing::warn!("Ignoring patch rebuild since there is no existing build."); + return; + }; + + // On web, our patches are fully relocatable, so we don't need to worry about ASLR, but + // for all other platforms, we need to use the ASLR reference to know where to insert the patch. + let aslr_reference = match self.aslr_reference { + Some(val) => val, + None if self.build.platform == Platform::Web => 0, + None => { + tracing::warn!( + "Ignoring hotpatch since there is no ASLR reference. Is the client connected?" + ); + return; + } + }; + + let cache = artifacts + .patch_cache + .clone() + .context("Failed to get patch cache") + .unwrap(); + // Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit - self.abort_all(); + self.abort_all(BuildStage::Restarting); + self.build_task = tokio::spawn({ + let request = self.build.clone(); + let ctx = BuildContext { + tx: self.tx.clone(), + mode: BuildMode::Thin { + changed_files, + rustc_args: artifacts.direct_rustc, + aslr_reference, + cache, + }, + }; + async move { request.build(&ctx).await } + }); + } + /// Restart this builder with new build arguments. + pub(crate) fn start_rebuild(&mut self, mode: BuildMode) { + // Abort all the ongoing builds, cleaning up any loose artifacts and waiting to cleanly exit // And then start a new build, resetting our progress/stage to the beginning and replacing the old tokio task - let request = BuildRequest::new(self.krate.clone(), args, self.tx.clone()); - self.request = request.clone(); - self.stage = BuildStage::Restarting; - - // This build doesn't have any extra special logging - rebuilds would get pretty noisy - self.build = tokio::spawn(async move { request.build_all().await }); + self.abort_all(BuildStage::Restarting); + self.artifacts.take(); + self.patch_cache.take(); + self.build_task = tokio::spawn({ + let request = self.build.clone(); + let ctx = BuildContext { + tx: self.tx.clone(), + mode, + }; + async move { request.build(&ctx).await } + }); } /// Shutdown the current build process /// /// todo: might want to use a cancellation token here to allow cleaner shutdowns - pub(crate) fn abort_all(&mut self) { - self.build.abort(); - self.stage = BuildStage::Aborted; + pub(crate) fn abort_all(&mut self, stage: BuildStage) { + self.stage = stage; self.compiled_crates = 0; - self.compiled_crates_server = 0; self.expected_crates = 1; self.bundling_progress = 0.0; self.compile_start = None; self.bundle_start = None; self.bundle_end = None; self.compile_end = None; + self.build_task.abort(); } /// Wait for the build to finish, returning the final bundle @@ -212,10 +348,10 @@ impl Builder { /// /// todo(jon): maybe we want to do some logging here? The build/bundle/run screens could be made to /// use the TUI output for prettier outputs. - pub(crate) async fn finish(&mut self) -> Result { + pub(crate) async fn finish_build(&mut self) -> Result { loop { match self.wait().await { - BuildUpdate::Progress { stage } => { + BuilderUpdate::Progress { stage } => { match &stage { BuildStage::Compiling { current, @@ -239,19 +375,19 @@ impl Builder { tracing::info!(json = ?StructuredOutput::BuildUpdate { stage: stage.clone() }); } - BuildUpdate::CompilerMessage { message } => { + BuilderUpdate::CompilerMessage { message } => { tracing::info!(json = ?StructuredOutput::CargoOutput { message: message.clone() }, %message); } - BuildUpdate::BuildReady { bundle } => { + BuilderUpdate::BuildReady { bundle } => { tracing::debug!(json = ?StructuredOutput::BuildFinished { - path: bundle.build.root_dir(), + path: self.build.root_dir(), }); return Ok(bundle); } - BuildUpdate::BuildFailed { err } => { + BuilderUpdate::BuildFailed { err } => { // Flush remaining compiler messages while let Ok(Some(msg)) = self.rx.try_next() { - if let BuildUpdate::CompilerMessage { message } = msg { + if let BuilderUpdate::CompilerMessage { message } = msg { tracing::info!(json = ?StructuredOutput::CargoOutput { message: message.clone() }, %message); } } @@ -259,15 +395,872 @@ impl Builder { tracing::error!(?err, json = ?StructuredOutput::Error { message: err.to_string() }); return Err(err); } + BuilderUpdate::StdoutReceived { .. } => {} + BuilderUpdate::StderrReceived { .. } => {} + BuilderUpdate::ProcessExited { .. } => {} + } + } + } + + pub(crate) async fn open( + &mut self, + devserver_ip: SocketAddr, + open_address: Option, + start_fullstack_on_address: Option, + open_browser: bool, + always_on_top: bool, + build_id: BuildId, + ) -> Result<()> { + let krate = &self.build; + + // Set the env vars that the clients will expect + // These need to be stable within a release version (ie 0.6.0) + let mut envs = vec![ + (dioxus_cli_config::CLI_ENABLED_ENV, "true".to_string()), + ( + dioxus_cli_config::DEVSERVER_IP_ENV, + devserver_ip.ip().to_string(), + ), + ( + dioxus_cli_config::DEVSERVER_PORT_ENV, + devserver_ip.port().to_string(), + ), + ( + dioxus_cli_config::APP_TITLE_ENV, + krate.config.web.app.title.clone(), + ), + ( + dioxus_cli_config::SESSION_CACHE_DIR, + self.build.session_cache_dir().display().to_string(), + ), + (dioxus_cli_config::BUILD_ID, build_id.0.to_string()), + ( + dioxus_cli_config::ALWAYS_ON_TOP_ENV, + always_on_top.to_string(), + ), + // unset the cargo dirs in the event we're running `dx` locally + // since the child process will inherit the env vars, we don't want to confuse the downstream process + ("CARGO_MANIFEST_DIR", "".to_string()), + ("RUST_BACKTRACE", "1".to_string()), + ]; + + if let Some(base_path) = &krate.config.web.app.base_path { + envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.clone())); + } + + if let Some(env_filter) = env::var_os("RUST_LOG").and_then(|e| e.into_string().ok()) { + envs.push(("RUST_LOG", env_filter)); + } + + // Launch the server if we were given an address to start it on, and the build includes a server. After we + // start the server, consume its stdout/stderr. + if let Some(addr) = start_fullstack_on_address { + envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string())); + envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string())); + } + + // We try to use stdin/stdout to communicate with the app + let running_process = match self.build.platform { + // Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead + // use use the websocket to communicate with it. I wish we could merge the concepts here, + // like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else. + Platform::Web => { + // Only the first build we open the web app, after that the user knows it's running + if open_browser { + self.open_web(open_address.unwrap_or(devserver_ip)); + } + + None } + + Platform::Ios => Some(self.open_ios_sim(envs).await?), + + Platform::Android => { + self.open_android_sim(false, devserver_ip, envs).await?; + None + } + + // These are all just basically running the main exe, but with slightly different resource dir paths + Platform::Server + | Platform::MacOS + | Platform::Windows + | Platform::Linux + | Platform::Liveview => Some(self.open_with_main_exe(envs)?), + }; + + // If we have a running process, we need to attach to it and wait for its outputs + if let Some(mut child) = running_process { + let stdout = BufReader::new(child.stdout.take().unwrap()); + let stderr = BufReader::new(child.stderr.take().unwrap()); + self.stdout = Some(stdout.lines()); + self.stderr = Some(stderr.lines()); + self.child = Some(child); + } + + self.builds_opened += 1; + + Ok(()) + } + + /// Gracefully kill the process and all of its children + /// + /// Uses the `SIGTERM` signal on unix and `taskkill` on windows. + /// This complex logic is necessary for things like window state preservation to work properly. + /// + /// Also wipes away the entropy executables if they exist. + pub(crate) async fn soft_kill(&mut self) { + use futures_util::FutureExt; + + // Kill any running executables on Windows + let Some(mut process) = self.child.take() else { + return; + }; + + let Some(pid) = process.id() else { + _ = process.kill().await; + return; + }; + + // on unix, we can send a signal to the process to shut down + #[cfg(unix)] + { + _ = Command::new("kill") + .args(["-s", "TERM", &pid.to_string()]) + .spawn(); + } + + // on windows, use the `taskkill` command + #[cfg(windows)] + { + _ = Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) + .spawn(); + } + + // join the wait with a 100ms timeout + futures_util::select! { + _ = process.wait().fuse() => {} + _ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {} + }; + + // Wipe out the entropy executables if they exist + if let Some(entropy_app_exe) = self.entropy_app_exe.take() { + _ = std::fs::remove_file(entropy_app_exe); } } + pub(crate) async fn hotpatch( + &mut self, + res: &BuildArtifacts, + cache: &HotpatchModuleCache, + ) -> Result { + let original = self.build.main_exe(); + let new = self.build.patch_exe(res.time_start); + let triple = self.build.triple.clone(); + let original_artifacts = self.artifacts.as_ref().unwrap(); + let asset_dir = self.build.asset_dir(); + + for (k, bundled) in res.assets.assets.iter() { + let k = dunce::canonicalize(k)?; + if original_artifacts.assets.assets.contains_key(k.as_path()) { + continue; + } + + let from = k.clone(); + let to = asset_dir.join(bundled.bundled_path()); + + tracing::debug!("Copying asset from patch: {}", k.display()); + if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) { + tracing::error!("Failed to copy asset: {e}"); + continue; + } + + // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext` + if self.build.platform == Platform::Android { + let changed_file = dunce::canonicalize(k).inspect_err(|e| { + tracing::debug!("Failed to canonicalize hotreloaded asset: {e}") + })?; + let bundled_name = PathBuf::from(bundled.bundled_path()); + _ = self + .copy_file_to_android_tmp(&changed_file, &bundled_name) + .await; + } + } + + tracing::debug!("Patching {} -> {}", original.display(), new.display()); + + let mut jump_table = crate::build::create_jump_table(&new, &triple, cache)?; + + // If it's android, we need to copy the assets to the device and then change the location of the patch + if self.build.platform == Platform::Android { + jump_table.lib = self + .copy_file_to_android_tmp(&new, &(PathBuf::from(new.file_name().unwrap()))) + .await?; + } + + // Rebase the wasm binary to be relocatable once the jump table is generated + if triple.architecture == target_lexicon::Architecture::Wasm32 { + // Make sure we use the dir relative to the public dir, so the web can load it as a proper URL + // + // ie we would've shipped `/Users/foo/Projects/dioxus/target/dx/project/debug/web/public/wasm/lib.wasm` + // but we want to ship `/wasm/lib.wasm` + jump_table.lib = + PathBuf::from("/").join(jump_table.lib.strip_prefix(self.build.root_dir()).unwrap()) + } + + let changed_files = match &res.mode { + BuildMode::Thin { changed_files, .. } => changed_files.clone(), + _ => vec![], + }; + + let changed_file = changed_files.first().unwrap(); + tracing::info!( + "Hot-patching: {} took {:?}ms", + changed_file + .strip_prefix(std::env::current_dir().unwrap()) + .unwrap_or(changed_file.as_path()) + .display(), + SystemTime::now() + .duration_since(res.time_start) + .unwrap() + .as_millis() + ); + + self.patches.push(jump_table.clone()); + + Ok(jump_table) + } + + /// Hotreload an asset in the running app. + /// + /// This will modify the build dir in place! Be careful! We generally assume you want all bundles + /// to reflect the latest changes, so we will modify the bundle. + /// + /// However, not all platforms work like this, so we might also need to update a separate asset + /// dir that the system simulator might be providing. We know this is the case for ios simulators + /// and haven't yet checked for android. + /// + /// This will return the bundled name of the asset such that we can send it to the clients letting + /// them know what to reload. It's not super important that this is robust since most clients will + /// kick all stylsheets without necessarily checking the name. + pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option { + let artifacts = self.artifacts.as_ref()?; + + // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps, + // we won't actually be using the build dir. + let asset_dir = match self.runtime_asset_dir.as_ref() { + Some(dir) => dir.to_path_buf().join("assets/"), + None => self.build.asset_dir(), + }; + + tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}"); + + // Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\". + let changed_file = dunce::canonicalize(changed_file) + .inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}")) + .ok()?; + + // The asset might've been renamed thanks to the manifest, let's attempt to reload that too + let resource = artifacts.assets.assets.get(&changed_file)?; + let output_path = asset_dir.join(resource.bundled_path()); + + // Remove the old asset if it exists + _ = std::fs::remove_file(&output_path); + + // And then process the asset with the options into the **old** asset location. If we recompiled, + // the asset would be in a new location because the contents and hash have changed. Since we are + // hotreloading, we need to use the old asset location it was originally written to. + let options = *resource.options(); + let res = process_file_to(&options, &changed_file, &output_path); + let bundled_name = PathBuf::from(resource.bundled_path()); + if let Err(e) = res { + tracing::debug!("Failed to hotreload asset {e}"); + } + + // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext` + if self.build.platform == Platform::Android { + _ = self + .copy_file_to_android_tmp(&changed_file, &bundled_name) + .await; + } + + Some(bundled_name) + } + + /// Copy this file to the tmp folder on the android device, returning the path to the copied file + /// + /// When we push patches (.so), the runtime will dlopen the file from the tmp folder by first copying + /// it to shared memory. This is a workaround since not all android devices will be rooted and we + /// can't drop the file into the `/data/data/com.org.app/lib/` directory. + pub(crate) async fn copy_file_to_android_tmp( + &self, + changed_file: &Path, + bundled_name: &Path, + ) -> Result { + let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name); + tracing::debug!("Pushing asset to device: {target:?}"); + + let res = tokio::process::Command::new(&self.build.workspace.android_tools()?.adb) + .arg("push") + .arg(changed_file) + .arg(&target) + .output() + .await + .context("Failed to push asset to device"); + + if let Err(e) = res { + tracing::debug!("Failed to push asset to device: {e}"); + } + + Ok(target) + } + + /// Open the native app simply by running its main exe + /// + /// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths, + /// but for now, we just run the exe directly. Very few users should be caring about `dylib` search + /// paths right now, but they will when we start to enable things like swift integration. + /// + /// Server/liveview/desktop are all basically the same, though + fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result { + let main_exe = self.app_exe(); + + tracing::debug!("Opening app with main exe: {main_exe:?}"); + + let child = Command::new(main_exe) + .envs(envs) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + Ok(child) + } + + /// Open the web app by opening the browser to the given address. + /// Check if we need to use https or not, and if so, add the protocol. + /// Go to the basepath if that's set too. + fn open_web(&self, address: SocketAddr) { + let base_path = self.build.config.web.app.base_path.clone(); + let https = self.build.config.web.https.enabled.unwrap_or_default(); + let protocol = if https { "https" } else { "http" }; + let base_path = match base_path.as_deref() { + Some(base_path) => format!("/{}", base_path.trim_matches('/')), + None => "".to_owned(), + }; + _ = open::that_detached(format!("{protocol}://{address}{base_path}")); + } + + /// Use `xcrun` to install the app to the simulator + /// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning + /// or entitlements, or anything like that. + /// + /// However, if there's no simulator running, this *might* fail. + /// + /// TODO(jon): we should probably check if there's a simulator running before trying to install, + /// and open the simulator if we have to. + async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result { + tracing::debug!("Installing app to simulator {:?}", self.build.root_dir()); + + let res = Command::new("xcrun") + .arg("simctl") + .arg("install") + .arg("booted") + .arg(self.build.root_dir()) + .output() + .await?; + + tracing::debug!("Installed app to simulator with exit code: {res:?}"); + + // Remap the envs to the correct simctl env vars + // iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX" + let ios_envs = envs + .iter() + .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone())); + + let child = Command::new("xcrun") + .arg("simctl") + .arg("launch") + .arg("--console") + .arg("booted") + .arg(self.build.bundle_identifier()) + .envs(ios_envs) + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + Ok(child) + } + + /// We have this whole thing figured out, but we don't actually use it yet. + /// + /// Launching on devices is more complicated and requires us to codesign the app, which we don't + /// currently do. + /// + /// Converting these commands shouldn't be too hard, but device support would imply we need + /// better support for codesigning and entitlements. + #[allow(unused)] + async fn open_ios_device(&self) -> Result<()> { + use serde_json::Value; + let app_path = self.build.root_dir(); + + install_app(&app_path).await?; + + // 2. Determine which device the app was installed to + let device_uuid = get_device_uuid().await?; + + // 3. Get the installation URL of the app + let installation_url = get_installation_url(&device_uuid, &app_path).await?; + + // 4. Launch the app into the background, paused + launch_app_paused(&device_uuid, &installation_url).await?; + + // 5. Pick up the paused app and resume it + resume_app(&device_uuid).await?; + + async fn install_app(app_path: &PathBuf) -> Result<()> { + let output = Command::new("xcrun") + .args(["simctl", "install", "booted"]) + .arg(app_path) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to install app: {:?}", output).into()); + } + + Ok(()) + } + + async fn get_device_uuid() -> Result { + let output = Command::new("xcrun") + .args([ + "devicectl", + "list", + "devices", + "--json-output", + "target/deviceid.json", + ]) + .output() + .await?; + + let json: Value = + serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?) + .context("Failed to parse xcrun output")?; + let device_uuid = json["result"]["devices"][0]["identifier"] + .as_str() + .ok_or("Failed to extract device UUID")? + .to_string(); + + Ok(device_uuid) + } + + async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result { + // xcrun devicectl device install app --device --path --json-output + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "install", + "app", + "--device", + device_uuid, + &app_path.display().to_string(), + "--json-output", + "target/xcrun.json", + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to install app: {:?}", output).into()); + } + + let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?) + .context("Failed to parse xcrun output")?; + let installation_url = json["result"]["installedApplications"][0]["installationURL"] + .as_str() + .ok_or("Failed to extract installation URL")? + .to_string(); + + Ok(installation_url) + } + + async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> { + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "process", + "launch", + "--no-activate", + "--verbose", + "--device", + device_uuid, + installation_url, + "--json-output", + "target/launch.json", + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to launch app: {:?}", output).into()); + } + + Ok(()) + } + + async fn resume_app(device_uuid: &str) -> Result<()> { + let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?) + .context("Failed to parse xcrun output")?; + + let status_pid = json["result"]["process"]["processIdentifier"] + .as_u64() + .ok_or("Failed to extract process identifier")?; + + let output = Command::new("xcrun") + .args([ + "devicectl", + "device", + "process", + "resume", + "--device", + device_uuid, + "--pid", + &status_pid.to_string(), + ]) + .output() + .await?; + + if !output.status.success() { + return Err(format!("Failed to resume app: {:?}", output).into()); + } + + Ok(()) + } + + unimplemented!("dioxus-cli doesn't support ios devices yet.") + } + + #[allow(unused)] + async fn codesign_ios(&self) -> Result<()> { + const CODESIGN_ERROR: &str = r#"This is likely because you haven't +- Created a provisioning profile before +- Accepted the Apple Developer Program License Agreement + +The agreement changes frequently and might need to be accepted again. +To accept the agreement, go to https://developer.apple.com/account + +To create a provisioning profile, follow the instructions here: +https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#; + + let profiles_folder = dirs::home_dir() + .context("Your machine has no home-dir")? + .join("Library/MobileDevice/Provisioning Profiles"); + + if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() { + tracing::error!( + r#"No provisioning profiles found when trying to codesign the app. +We checked the folder: {} + +{CODESIGN_ERROR} +"#, + profiles_folder.display() + ) + } + + let identities = Command::new("security") + .args(["find-identity", "-v", "-p", "codesigning"]) + .output() + .await + .context("Failed to run `security find-identity -v -p codesigning`") + .map(|e| { + String::from_utf8(e.stdout) + .context("Failed to parse `security find-identity -v -p codesigning`") + })??; + + // Parsing this: + // 1231231231231asdasdads123123 "Apple Development: foo@gmail.com (XYZYZY)" + let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#) + .unwrap() + .captures(&identities) + .and_then(|caps| caps.get(1)) + .map(|m| m.as_str()) + .context( + "Failed to find Apple Development in `security find-identity -v -p codesigning`", + )?; + + // Acquire the provision file + let provision_file = profiles_folder + .read_dir()? + .flatten() + .find(|entry| { + entry + .file_name() + .to_str() + .map(|s| s.contains("mobileprovision")) + .unwrap_or_default() + }) + .context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?; + + // The .mobileprovision file has some random binary thrown into into, but it's still basically a plist + // Let's use the plist markers to find the start and end of the plist + fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option { + bytes + .windows(byte_match.len()) + .enumerate() + .rev() + .find(|(_, slice)| *slice == byte_match) + .map(|(i, _)| i + byte_match.len()) + } + let bytes = std::fs::read(provision_file.path())?; + let cut1 = cut_plist(&bytes, b""#.as_bytes()) + .context("Failed to parse .mobileprovision file")?; + let sub_bytes = &bytes[(cut1 - 6)..cut2]; + let mbfile: ProvisioningProfile = + plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?; + + #[derive(serde::Deserialize, Debug)] + struct ProvisioningProfile { + #[serde(rename = "TeamIdentifier")] + team_identifier: Vec, + #[serde(rename = "ApplicationIdentifierPrefix")] + application_identifier_prefix: Vec, + #[serde(rename = "Entitlements")] + entitlements: Entitlements, + } + + #[derive(serde::Deserialize, Debug)] + struct Entitlements { + #[serde(rename = "application-identifier")] + application_identifier: String, + #[serde(rename = "keychain-access-groups")] + keychain_access_groups: Vec, + } + + let entielements_xml = format!( + r#" + + + + application-identifier + {APPLICATION_IDENTIFIER} + keychain-access-groups + + {APP_ID_ACCESS_GROUP}.* + + get-task-allow + + com.apple.developer.team-identifier + {TEAM_IDENTIFIER} + + "#, + APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier, + APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0], + TEAM_IDENTIFIER = mbfile.team_identifier[0], + ); + + // write to a temp file + let temp_file = tempfile::NamedTempFile::new()?; + std::fs::write(temp_file.path(), entielements_xml)?; + + // codesign the app + let output = Command::new("codesign") + .args([ + "--force", + "--entitlements", + temp_file.path().to_str().unwrap(), + "--sign", + app_dev_name, + ]) + .arg(self.build.root_dir()) + .output() + .await + .context("Failed to codesign the app")?; + + if !output.status.success() { + let stderr = String::from_utf8(output.stderr).unwrap_or_default(); + return Err(format!("Failed to codesign the app: {stderr}").into()); + } + + Ok(()) + } + + /// Launch the Android simulator and deploy the application. + /// + /// This function handles the process of starting the Android simulator, installing the APK, + /// forwarding the development server port, and launching the application on the simulator. + /// + /// The following `adb` commands are executed: + /// + /// 1. **Enable Root Access**: + /// - `adb root`: Enables root access on the Android simulator, allowing for advanced operations like pushing files to restricted directories. + /// + /// 2. **Port Forwarding**: + /// - `adb reverse tcp: tcp:`: Forwards the development server port from the host + /// machine to the Android simulator, enabling communication between the app and the dev server. + /// + /// 3. **APK Installation**: + /// - `adb install -r `: Installs the APK onto the Android simulator. The `-r` flag + /// ensures that any existing installation of the app is replaced. + /// + /// 4. **Environment Variables**: + /// - Writes environment variables to a `.env` file in the session cache directory. + /// - `adb push `: Pushes the `.env` file to the Android device + /// to configure runtime environment variables for the app. + /// + /// 5. **App Launch**: + /// - `adb shell am start -n /`: Launches the app on the Android + /// simulator. The `` and `` are derived from the app's configuration. + /// + /// # Notes + /// + /// - This function is asynchronous and spawns a background task to handle the simulator setup and app launch. + /// - The Android tools (`adb`) must be available in the system's PATH for this function to work. + /// - If the app fails to launch, errors are logged for debugging purposes. + /// + /// # Resources: + /// - https://developer.android.com/studio/run/emulator-commandline + async fn open_android_sim( + &self, + root: bool, + devserver_socket: SocketAddr, + envs: Vec<(&'static str, String)>, + ) -> Result<()> { + let apk_path = self.build.debug_apk_path(); + let session_cache = self.build.session_cache_dir(); + let full_mobile_app_name = self.build.full_mobile_app_name(); + let adb = self.build.workspace.android_tools()?.adb.clone(); + + // Start backgrounded since .open() is called while in the arm of the top-level match + tokio::task::spawn(async move { + // call `adb root` so we can push patches to the device + if root { + if let Err(e) = Command::new(&adb).arg("root").output().await { + tracing::error!("Failed to run `adb root`: {e}"); + } + } + + let port = devserver_socket.port(); + if let Err(e) = Command::new(&adb) + .arg("reverse") + .arg(format!("tcp:{}", port)) + .arg(format!("tcp:{}", port)) + .output() + .await + { + tracing::error!("failed to forward port {port}: {e}"); + } + + // Install + // adb install -r app-debug.apk + if let Err(e) = Command::new(&adb) + .arg("install") + .arg("-r") + .arg(apk_path) + .output() + .await + { + tracing::error!("Failed to install apk with `adb`: {e}"); + }; + + // Write the env vars to a .env file in our session cache + let env_file = session_cache.join(".env"); + let contents: String = envs + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join("\n"); + _ = std::fs::write(&env_file, contents); + + // Push the env file to the device + if let Err(e) = tokio::process::Command::new(&adb) + .arg("push") + .arg(env_file) + .arg(dioxus_cli_config::android_session_cache_dir().join(".env")) + .output() + .await + .context("Failed to push asset to device") + { + tracing::error!("Failed to push .env file to device: {e}"); + } + + // eventually, use the user's MainActivity, not our MainActivity + // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity + let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,); + + if let Err(e) = Command::new(&adb) + .arg("shell") + .arg("am") + .arg("start") + .arg("-n") + .arg(activity_name) + .output() + .await + { + tracing::error!("Failed to start app with `adb`: {e}"); + }; + }); + + Ok(()) + } + + fn make_entropy_path(exe: &PathBuf) -> PathBuf { + let id = uuid::Uuid::new_v4(); + let name = id.to_string(); + let some_entropy = name.split('-').next().unwrap(); + + // Make a copy of the server exe with a new name + let entropy_server_exe = exe.with_file_name(format!( + "{}-{}", + exe.file_name().unwrap().to_str().unwrap(), + some_entropy + )); + + std::fs::copy(exe, &entropy_server_exe).unwrap(); + + entropy_server_exe + } + + fn app_exe(&mut self) -> PathBuf { + let mut main_exe = self.build.main_exe(); + + // The requirement here is based on the platform, not necessarily our current architecture. + let requires_entropy = match self.build.platform { + // When running "bundled", we don't need entropy + Platform::Web => false, + Platform::MacOS => false, + Platform::Ios => false, + Platform::Android => false, + + // But on platforms that aren't running as "bundled", we do. + Platform::Windows => true, + Platform::Linux => true, + Platform::Server => true, + Platform::Liveview => true, + }; + + if requires_entropy || crate::devcfg::should_force_entropy() { + // If we already have an entropy app exe, return it - this is useful for re-opening the same app + if let Some(existing_app_exe) = self.entropy_app_exe.clone() { + return existing_app_exe; + } + + let entropy_app_exe = Self::make_entropy_path(&main_exe); + self.entropy_app_exe = Some(entropy_app_exe.clone()); + main_exe = entropy_app_exe; + } + + main_exe + } + fn complete_compile(&mut self) { if self.compile_end.is_none() { self.compiled_crates = self.expected_crates; self.compile_end = Some(Instant::now()); - self.compile_end_server = Some(Instant::now()); } } @@ -297,11 +1290,6 @@ impl Builder { self.compiled_crates as f64 / self.expected_crates as f64 } - /// Return a number between 0 and 1 representing the progress of the server build - pub(crate) fn server_compile_progress(&self) -> f64 { - self.compiled_crates_server as f64 / self.expected_crates_server as f64 - } - pub(crate) fn bundle_progress(&self) -> f64 { self.bundling_progress } diff --git a/packages/cli/src/build/bundle.rs b/packages/cli/src/build/bundle.rs deleted file mode 100644 index e2b769f9d2..0000000000 --- a/packages/cli/src/build/bundle.rs +++ /dev/null @@ -1,955 +0,0 @@ -use super::prerender::pre_render_static_routes; -use super::templates::InfoPlistData; -use crate::{BuildRequest, Platform, WasmOptConfig}; -use crate::{Result, TraceSrc}; -use anyhow::Context; -use dioxus_cli_opt::{process_file_to, AssetManifest}; -use manganis::{AssetOptions, JsAssetOptions}; -use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; -use std::future::Future; -use std::path::{Path, PathBuf}; -use std::pin::Pin; -use std::sync::atomic::Ordering; -use std::{collections::HashSet, io::Write}; -use std::{sync::atomic::AtomicUsize, time::Duration}; -use tokio::process::Command; - -/// The end result of a build. -/// -/// Contains the final asset manifest, the executables, and the workdir. -/// -/// Every dioxus app can have an optional server executable which will influence the final bundle. -/// This is built in parallel with the app executable during the `build` phase and the progres/status -/// of the build is aggregated. -/// -/// The server will *always* be dropped into the `web` folder since it is considered "web" in nature, -/// and will likely need to be combined with the public dir to be useful. -/// -/// We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform -/// can just use the build dir -/// -/// When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name: -/// ``` -/// dog-app/ -/// build/ -/// web/ -/// server.exe -/// assets/ -/// some-secret-asset.txt (a server-side asset) -/// public/ -/// index.html -/// assets/ -/// logo.png -/// desktop/ -/// App.app -/// App.appimage -/// App.exe -/// server/ -/// server -/// assets/ -/// some-secret-asset.txt (a server-side asset) -/// ios/ -/// App.app -/// App.ipa -/// android/ -/// App.apk -/// bundle/ -/// build.json -/// Desktop.app -/// Mobile_x64.ipa -/// Mobile_arm64.ipa -/// Mobile_rosetta.ipa -/// web.appimage -/// web/ -/// server.exe -/// assets/ -/// some-secret-asset.txt -/// public/ -/// index.html -/// assets/ -/// logo.png -/// style.css -/// ``` -/// -/// When deploying, the build.json file will provide all the metadata that dx-deploy will use to -/// push the app to stores, set up infra, manage versions, etc. -/// -/// The format of each build will follow the name plus some metadata such that when distributing you -/// can easily trim off the metadata. -/// -/// The idea here is that we can run any of the programs in the same way that they're deployed. -/// -/// -/// ## Bundle structure links -/// - apple: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle -/// - appimage: https://docs.appimage.org/packaging-guide/manual.html#ref-manual -/// -/// ## Extra links -/// - xbuild: https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs -#[derive(Debug)] -pub(crate) struct AppBundle { - pub(crate) build: BuildRequest, - pub(crate) app: BuildArtifacts, - pub(crate) server: Option, -} - -#[derive(Debug)] -pub struct BuildArtifacts { - pub(crate) exe: PathBuf, - pub(crate) assets: AssetManifest, - pub(crate) time_taken: Duration, -} - -impl AppBundle { - /// ## Web: - /// Create a folder that is somewhat similar to an app-image (exe + asset) - /// The server is dropped into the `web` folder, even if there's no `public` folder. - /// If there's no server (SPA), we still use the `web` folder, but it only contains the - /// public folder. - /// ``` - /// web/ - /// server - /// assets/ - /// public/ - /// index.html - /// wasm/ - /// app.wasm - /// glue.js - /// snippets/ - /// ... - /// assets/ - /// logo.png - /// ``` - /// - /// ## Linux: - /// https://docs.appimage.org/reference/appdir.html#ref-appdir - /// current_exe.join("Assets") - /// ``` - /// app.appimage/ - /// AppRun - /// app.desktop - /// package.json - /// assets/ - /// logo.png - /// ``` - /// - /// ## Macos - /// We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources` - /// We put assets in an assets dir such that it generally matches every other platform and we can - /// output `/assets/blah` from manganis. - /// ``` - /// App.app/ - /// Contents/ - /// Info.plist - /// MacOS/ - /// Frameworks/ - /// Resources/ - /// assets/ - /// blah.icns - /// blah.png - /// CodeResources - /// _CodeSignature/ - /// ``` - /// - /// ## iOS - /// Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably - /// since most ios apps don't ship frameworks/plugins and such. - /// - /// todo(jon): include the signing and entitlements in this format diagram. - /// ``` - /// App.app/ - /// main - /// assets/ - /// ``` - /// - /// ## Android: - /// - /// Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since - /// we need to compile kotlin and java. This pushes us into using gradle and following a structure - /// similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and - /// drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires - /// us to manage dependencies (like kotlinc) ourselves (yuck!). - /// - /// https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh - /// - /// Unfortunately, it seems that while we can drop the `android` build plugin, we still will need - /// gradle since kotlin is basically gradle-only. - /// - /// Pre-build: - /// ``` - /// app.apk/ - /// .gradle - /// app/ - /// src/ - /// main/ - /// assets/ - /// jniLibs/ - /// java/ - /// kotlin/ - /// res/ - /// AndroidManifest.xml - /// build.gradle.kts - /// proguard-rules.pro - /// buildSrc/ - /// build.gradle.kts - /// src/ - /// main/ - /// kotlin/ - /// BuildTask.kt - /// build.gradle.kts - /// gradle.properties - /// gradlew - /// gradlew.bat - /// settings.gradle - /// ``` - /// - /// Final build: - /// ``` - /// app.apk/ - /// AndroidManifest.xml - /// classes.dex - /// assets/ - /// logo.png - /// lib/ - /// armeabi-v7a/ - /// libmyapp.so - /// arm64-v8a/ - /// libmyapp.so - /// ``` - /// Notice that we *could* feasibly build this ourselves :) - /// - /// ## Windows: - /// https://superuser.com/questions/749447/creating-a-single-file-executable-from-a-directory-in-windows - /// Windows does not provide an AppImage format, so instead we're going build the same folder - /// structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources - /// as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the - /// Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe - /// entrypoint in the associated Program Files folder. - /// - /// This is, in essence, the same as an installer, so we might eventually just support something like msi/msix - /// which functionally do the same thing but with a sleeker UI. - /// - /// This means no installers are required and we can bake an updater into the host exe. - /// - /// ## Handling asset lookups: - /// current_exe.join("assets") - /// ``` - /// app.appimage/ - /// main.exe - /// main.desktop - /// package.json - /// assets/ - /// logo.png - /// ``` - /// - /// Since we support just a few locations, we could just search for the first that exists - /// - usr - /// - ../Resources - /// - assets - /// - Assets - /// - $cwd/assets - /// - /// ``` - /// assets::root() -> - /// mac -> ../Resources/ - /// ios -> ../Resources/ - /// android -> assets/ - /// server -> assets/ - /// liveview -> assets/ - /// web -> /assets/ - /// root().join(bundled) - /// ``` - pub(crate) async fn new( - build: BuildRequest, - app: BuildArtifacts, - server: Option, - ) -> Result { - let mut bundle = Self { app, server, build }; - - tracing::debug!("Assembling app bundle"); - - bundle.build.status_start_bundle(); - /* - assume the build dir is already created by BuildRequest - todo(jon): maybe refactor this a bit to force AppBundle to be created before it can be filled in - */ - bundle - .write_main_executable() - .await - .context("Failed to write main executable")?; - bundle.write_server_executable().await?; - bundle - .write_assets() - .await - .context("Failed to write assets")?; - bundle.write_metadata().await?; - bundle.optimize().await?; - bundle.pre_render_ssg_routes().await?; - bundle - .assemble() - .await - .context("Failed to assemble app bundle")?; - - tracing::debug!("Bundle created at {}", bundle.build.root_dir().display()); - - Ok(bundle) - } - - /// Take the output of rustc and make it into the main exe of the bundle - /// - /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations - /// Other platforms we might do some stripping or other optimizations - /// Move the executable to the workdir - async fn write_main_executable(&mut self) -> Result<()> { - match self.build.build.platform() { - // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder - // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable". - // - // The wasm stuff will be in a folder called "wasm" in the workdir. - // - // Final output format: - // ``` - // dx/ - // app/ - // web/ - // bundle/ - // build/ - // public/ - // index.html - // wasm/ - // app.wasm - // glue.js - // snippets/ - // ... - // assets/ - // logo.png - // ``` - Platform::Web => { - self.bundle_web().await?; - } - - // this will require some extra oomf to get the multi architecture builds... - // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1) - // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about - // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path - // - // todo(jon): maybe just symlink this rather than copy it? - Platform::Android => { - self.copy_android_exe(&self.app.exe, &self.main_exe()) - .await?; - } - - // These are all super simple, just copy the exe into the folder - // eventually, perhaps, maybe strip + encrypt the exe? - Platform::MacOS - | Platform::Windows - | Platform::Linux - | Platform::Ios - | Platform::Liveview - | Platform::Server => { - std::fs::copy(&self.app.exe, self.main_exe())?; - } - } - - Ok(()) - } - - /// Copy the assets out of the manifest and into the target location - /// - /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory - async fn write_assets(&self) -> Result<()> { - // Server doesn't need assets - web will provide them - if self.build.build.platform() == Platform::Server { - return Ok(()); - } - - let asset_dir = self.build.asset_dir(); - - // First, clear the asset dir of any files that don't exist in the new manifest - _ = tokio::fs::create_dir_all(&asset_dir).await; - // Create a set of all the paths that new files will be bundled to - let mut keep_bundled_output_paths: HashSet<_> = self - .app - .assets - .assets - .values() - .map(|a| asset_dir.join(a.bundled_path())) - .collect(); - // The CLI creates a .version file in the asset dir to keep track of what version of the optimizer - // the asset was processed. If that version doesn't match the CLI version, we need to re-optimize - // all assets. - let version_file = self.build.asset_optimizer_version_file(); - let clear_cache = std::fs::read_to_string(&version_file) - .ok() - .filter(|s| s == crate::VERSION.as_str()) - .is_none(); - if clear_cache { - keep_bundled_output_paths.clear(); - } - - // one possible implementation of walking a directory only visiting files - fn remove_old_assets<'a>( - path: &'a Path, - keep_bundled_output_paths: &'a HashSet, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - // If this asset is in the manifest, we don't need to remove it - let canon_path = dunce::canonicalize(path)?; - if keep_bundled_output_paths.contains(canon_path.as_path()) { - return Ok(()); - } - - // Otherwise, if it is a directory, we need to walk it and remove child files - if path.is_dir() { - for entry in std::fs::read_dir(path)?.flatten() { - let path = entry.path(); - remove_old_assets(&path, keep_bundled_output_paths).await?; - } - if path.read_dir()?.next().is_none() { - // If the directory is empty, remove it - tokio::fs::remove_dir(path).await?; - } - } else { - // If it is a file, remove it - tokio::fs::remove_file(path).await?; - } - - Ok(()) - }) - } - - tracing::debug!("Removing old assets"); - tracing::trace!( - "Keeping bundled output paths: {:#?}", - keep_bundled_output_paths - ); - remove_old_assets(&asset_dir, &keep_bundled_output_paths).await?; - - // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't - let mut assets_to_transfer = vec![]; - - // Queue the bundled assets - for (asset, bundled) in &self.app.assets.assets { - let from = asset.clone(); - let to = asset_dir.join(bundled.bundled_path()); - - // prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir - let from_ = from - .strip_prefix(self.build.krate.workspace_dir()) - .unwrap_or(from.as_path()); - let to_ = from - .strip_prefix(self.build.krate.workspace_dir()) - .unwrap_or(to.as_path()); - - tracing::debug!("Copying asset {from_:?} to {to_:?}"); - assets_to_transfer.push((from, to, *bundled.options())); - } - - // And then queue the legacy assets - // ideally, one day, we can just check the rsx!{} calls for references to assets - for from in self.build.krate.legacy_asset_dir_files() { - let to = asset_dir.join(from.file_name().unwrap()); - tracing::debug!("Copying legacy asset {from:?} to {to:?}"); - assets_to_transfer.push((from, to, AssetOptions::Unknown)); - } - - let asset_count = assets_to_transfer.len(); - let started_processing = AtomicUsize::new(0); - let copied = AtomicUsize::new(0); - - // Parallel Copy over the assets and keep track of progress with an atomic counter - let progress = self.build.progress.clone(); - let ws_dir = self.build.krate.workspace_dir(); - // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task - tokio::task::spawn_blocking(move || { - assets_to_transfer - .par_iter() - .try_for_each(|(from, to, options)| { - let processing = started_processing.fetch_add(1, Ordering::SeqCst); - let from_ = from.strip_prefix(&ws_dir).unwrap_or(from); - tracing::trace!( - "Starting asset copy {processing}/{asset_count} from {from_:?}" - ); - - let res = process_file_to(options, from, to); - if let Err(err) = res.as_ref() { - tracing::error!("Failed to copy asset {from:?}: {err}"); - } - - let finished = copied.fetch_add(1, Ordering::SeqCst); - BuildRequest::status_copied_asset( - &progress, - finished, - asset_count, - from.to_path_buf(), - ); - - res.map(|_| ()) - }) - }) - .await - .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??; - - // // Remove the wasm bindgen output directory if it exists - // _ = std::fs::remove_dir_all(self.build.wasm_bindgen_out_dir()); - - // Write the version file so we know what version of the optimizer we used - std::fs::write( - self.build.asset_optimizer_version_file(), - crate::VERSION.as_str(), - )?; - - Ok(()) - } - - /// The item that we'll try to run directly if we need to. - /// - /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod - pub fn main_exe(&self) -> PathBuf { - self.build.exe_dir().join(self.build.platform_exe_name()) - } - - /// We always put the server in the `web` folder! - /// Only the `web` target will generate a `public` folder though - async fn write_server_executable(&self) -> Result<()> { - if let Some(server) = &self.server { - let to = self - .server_exe() - .expect("server should be set if we're building a server"); - - std::fs::create_dir_all(self.server_exe().unwrap().parent().unwrap())?; - - tracing::debug!("Copying server executable to: {to:?} {server:#?}"); - - // Remove the old server executable if it exists, since copying might corrupt it :( - // todo(jon): do this in more places, I think - _ = std::fs::remove_file(&to); - std::fs::copy(&server.exe, to)?; - } - - Ok(()) - } - - /// todo(jon): use handlebars templates instead of these prebaked templates - async fn write_metadata(&self) -> Result<()> { - // write the Info.plist file - match self.build.build.platform() { - Platform::MacOS => { - let dest = self.build.root_dir().join("Contents").join("Info.plist"); - let plist = self.macos_plist_contents()?; - std::fs::write(dest, plist)?; - } - - Platform::Ios => { - let dest = self.build.root_dir().join("Info.plist"); - let plist = self.ios_plist_contents()?; - std::fs::write(dest, plist)?; - } - - // AndroidManifest.xml - // er.... maybe even all the kotlin/java/gradle stuff? - Platform::Android => {} - - // Probably some custom format or a plist file (haha) - // When we do the proper bundle, we'll need to do something with wix templates, I think? - Platform::Windows => {} - - // eventually we'll create the .appimage file, I guess? - Platform::Linux => {} - - // These are served as folders, not appimages, so we don't need to do anything special (I think?) - // Eventually maybe write some secrets/.env files for the server? - // We could also distribute them as a deb/rpm for linux and msi for windows - Platform::Web => {} - Platform::Server => {} - Platform::Liveview => {} - } - - Ok(()) - } - - /// Run the optimizers, obfuscators, minimizers, signers, etc - pub(crate) async fn optimize(&self) -> Result<()> { - match self.build.build.platform() { - Platform::Web => { - // Compress the asset dir - // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output - let pre_compress = self - .build - .krate - .should_pre_compress_web_assets(self.build.build.release); - - self.build.status_compressing_assets(); - let asset_dir = self.build.asset_dir(); - tokio::task::spawn_blocking(move || { - crate::fastfs::pre_compress_folder(&asset_dir, pre_compress) - }) - .await - .unwrap()?; - } - Platform::MacOS => {} - Platform::Windows => {} - Platform::Linux => {} - Platform::Ios => {} - Platform::Android => {} - Platform::Server => {} - Platform::Liveview => {} - } - - Ok(()) - } - - pub(crate) fn server_exe(&self) -> Option { - if let Some(_server) = &self.server { - let mut path = self - .build - .krate - .build_dir(Platform::Server, self.build.build.release); - - if cfg!(windows) { - path.push("server.exe"); - } else { - path.push("server"); - } - - return Some(path); - } - - None - } - - /// Bundle the web app - /// - Run wasm-bindgen - /// - Bundle split - /// - Run wasm-opt - /// - Register the .wasm and .js files with the asset system - async fn bundle_web(&mut self) -> Result<()> { - use crate::{wasm_bindgen::WasmBindgen, wasm_opt}; - use std::fmt::Write; - - // Locate the output of the build files and the bindgen output - // We'll fill these in a second if they don't already exist - let bindgen_outdir = self.build.wasm_bindgen_out_dir(); - let prebindgen = self.app.exe.clone(); - let post_bindgen_wasm = self.build.wasm_bindgen_wasm_output_file(); - let should_bundle_split = self.build.build.experimental_wasm_split; - let rustc_exe = self.app.exe.with_extension("wasm"); - let bindgen_version = self - .build - .krate - .wasm_bindgen_version() - .expect("this should have been checked by tool verification"); - - // Prepare any work dirs - std::fs::create_dir_all(&bindgen_outdir)?; - - // Prepare our configuration - // - // we turn off debug symbols in dev mode but leave them on in release mode (weird!) since - // wasm-opt and wasm-split need them to do better optimizations. - // - // We leave demangling to false since it's faster and these tools seem to prefer the raw symbols. - // todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically. - let will_wasm_opt = (self.build.build.release || self.build.build.experimental_wasm_split) - && crate::wasm_opt::wasm_opt_available(); - let keep_debug = self.build.krate.config.web.wasm_opt.debug - || self.build.build.debug_symbols - || self.build.build.experimental_wasm_split - || !self.build.build.release - || will_wasm_opt; - let demangle = false; - let wasm_opt_options = WasmOptConfig { - memory_packing: self.build.build.experimental_wasm_split, - debug: self.build.build.debug_symbols, - ..self.build.krate.config.web.wasm_opt.clone() - }; - - // Run wasm-bindgen. Some of the options are not "optimal" but will be fixed up by wasm-opt - // - // There's performance implications here. Running with --debug is slower than without - // We're keeping around lld sections and names but wasm-opt will fix them - // todo(jon): investigate a good balance of wiping debug symbols during dev (or doing a double build?) - self.build.status_wasm_bindgen_start(); - tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen"); - let start = std::time::Instant::now(); - WasmBindgen::new(&bindgen_version) - .input_path(&rustc_exe) - .target("web") - .debug(keep_debug) - .demangle(demangle) - .keep_debug(keep_debug) - .keep_lld_sections(true) - .out_name(self.build.krate.executable_name()) - .out_dir(&bindgen_outdir) - .remove_name_section(!will_wasm_opt) - .remove_producers_section(!will_wasm_opt) - .run() - .await - .context("Failed to generate wasm-bindgen bindings")?; - tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed()); - - // Run bundle splitting if the user has requested it - // It's pretty expensive but because of rayon should be running separate threads, hopefully - // not blocking this thread. Dunno if that's true - if should_bundle_split { - self.build.status_splitting_bundle(); - - if !will_wasm_opt { - return Err(anyhow::anyhow!( - "Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again." - ) - .into()); - } - - // Load the contents of these binaries since we need both of them - // We're going to use the default makeLoad glue from wasm-split - let original = std::fs::read(&prebindgen)?; - let bindgened = std::fs::read(&post_bindgen_wasm)?; - let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string(); - - // Run the emitter - let splitter = wasm_split_cli::Splitter::new(&original, &bindgened); - let modules = splitter - .context("Failed to parse wasm for splitter")? - .emit() - .context("Failed to emit wasm split modules")?; - - // Write the chunks that contain shared imports - // These will be in the format of chunk_0_modulename.wasm - this is hardcoded in wasm-split - tracing::debug!("Writing split chunks to disk"); - for (idx, chunk) in modules.chunks.iter().enumerate() { - let path = bindgen_outdir.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name)); - wasm_opt::write_wasm(&chunk.bytes, &path, &wasm_opt_options).await?; - writeln!( - glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);", - url = self - .app - .assets - .register_asset(&path, AssetOptions::Unknown)?.bundled_path(), - )?; - } - - // Write the modules that contain the entrypoints - tracing::debug!("Writing split modules to disk"); - for (idx, module) in modules.modules.iter().enumerate() { - let comp_name = module - .component_name - .as_ref() - .context("generated bindgen module has no name?")?; - - let path = bindgen_outdir.join(format!("module_{}_{}.wasm", idx, comp_name)); - wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?; - - let hash_id = module.hash_id.as_ref().unwrap(); - - writeln!( - glue, - "export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/assets/{url}\", [{deps}], fusedImports);", - module = module.module_name, - - - // Again, register this wasm with the asset system - url = self - .app - .assets - .register_asset(&path, AssetOptions::Unknown)?.bundled_path(), - - // This time, make sure to write the dependencies of this chunk - // The names here are again, hardcoded in wasm-split - fix this eventually. - deps = module - .relies_on_chunks - .iter() - .map(|idx| format!("__wasm_split_load_chunk_{idx}")) - .collect::>() - .join(", ") - )?; - } - - // Write the js binding - // It's not registered as an asset since it will get included in the main.js file - let js_output_path = bindgen_outdir.join("__wasm_split.js"); - std::fs::write(&js_output_path, &glue)?; - - // Make sure to write some entropy to the main.js file so it gets a new hash - // If we don't do this, the main.js file will be cached and never pick up the chunk names - let uuid = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, glue.as_bytes()); - std::fs::OpenOptions::new() - .append(true) - .open(self.build.wasm_bindgen_js_output_file()) - .context("Failed to open main.js file")? - .write_all(format!("/*{uuid}*/").as_bytes())?; - - // Write the main wasm_bindgen file and register it with the asset system - // This will overwrite the file in place - // We will wasm-opt it in just a second... - std::fs::write(&post_bindgen_wasm, modules.main.bytes)?; - } - - // Make sure to optimize the main wasm file if requested or if bundle splitting - if should_bundle_split || self.build.build.release { - self.build.status_optimizing_wasm(); - wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?; - } - - // Make sure to register the main wasm file with the asset system - self.app - .assets - .register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?; - - // Register the main.js with the asset system so it bundles in the snippets and optimizes - self.app.assets.register_asset( - &self.build.wasm_bindgen_js_output_file(), - AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)), - )?; - - // Write the index.html file with the pre-configured contents we got from pre-rendering - std::fs::write( - self.build.root_dir().join("index.html"), - self.prepare_html()?, - )?; - - Ok(()) - } - - async fn pre_render_ssg_routes(&self) -> Result<()> { - // Run SSG and cache static routes - if !self.build.build.ssg { - return Ok(()); - } - self.build.status_prerendering_routes(); - pre_render_static_routes( - &self - .server_exe() - .context("Failed to find server executable")?, - ) - .await?; - Ok(()) - } - - fn macos_plist_contents(&self) -> Result { - handlebars::Handlebars::new() - .render_template( - include_str!("../../assets/macos/mac.plist.hbs"), - &InfoPlistData { - display_name: self.build.krate.bundled_app_name(), - bundle_name: self.build.krate.bundled_app_name(), - executable_name: self.build.platform_exe_name(), - bundle_identifier: self.build.krate.bundle_identifier(), - }, - ) - .map_err(|e| e.into()) - } - - fn ios_plist_contents(&self) -> Result { - handlebars::Handlebars::new() - .render_template( - include_str!("../../assets/ios/ios.plist.hbs"), - &InfoPlistData { - display_name: self.build.krate.bundled_app_name(), - bundle_name: self.build.krate.bundled_app_name(), - executable_name: self.build.platform_exe_name(), - bundle_identifier: self.build.krate.bundle_identifier(), - }, - ) - .map_err(|e| e.into()) - } - - /// Run any final tools to produce apks or other artifacts we might need. - async fn assemble(&self) -> Result<()> { - if let Platform::Android = self.build.build.platform() { - self.build.status_running_gradle(); - - let output = Command::new(self.gradle_exe()?) - .arg("assembleDebug") - .current_dir(self.build.root_dir()) - .stderr(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .output() - .await?; - - if !output.status.success() { - return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into()); - } - } - - Ok(()) - } - - /// Run bundleRelease and return the path to the `.aab` file - /// - /// https://stackoverflow.com/questions/57072558/whats-the-difference-between-gradlewassemblerelease-gradlewinstallrelease-and - pub(crate) async fn android_gradle_bundle(&self) -> Result { - let output = Command::new(self.gradle_exe()?) - .arg("bundleRelease") - .current_dir(self.build.root_dir()) - .output() - .await - .context("Failed to run gradle bundleRelease")?; - - if !output.status.success() { - return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into()); - } - - let app_release = self - .build - .root_dir() - .join("app") - .join("build") - .join("outputs") - .join("bundle") - .join("release"); - - // Rename it to Name-arch.aab - let from = app_release.join("app-release.aab"); - let to = app_release.join(format!( - "{}-{}.aab", - self.build.krate.bundled_app_name(), - self.build.build.target_args.arch() - )); - - std::fs::rename(from, &to).context("Failed to rename aab")?; - - Ok(to) - } - - fn gradle_exe(&self) -> Result { - // make sure we can execute the gradlew script - #[cfg(unix)] - { - use std::os::unix::prelude::PermissionsExt; - std::fs::set_permissions( - self.build.root_dir().join("gradlew"), - std::fs::Permissions::from_mode(0o755), - )?; - } - - let gradle_exec_name = match cfg!(windows) { - true => "gradlew.bat", - false => "gradlew", - }; - - Ok(self.build.root_dir().join(gradle_exec_name)) - } - - pub(crate) fn apk_path(&self) -> PathBuf { - self.build - .root_dir() - .join("app") - .join("build") - .join("outputs") - .join("apk") - .join("debug") - .join("app-debug.apk") - } - - /// Copy the Android executable to the target directory, and rename the hardcoded com_hardcoded_dioxuslabs entries - /// to the user's app name. - async fn copy_android_exe(&self, source: &Path, destination: &Path) -> Result<()> { - // we might want to eventually use the objcopy logic to handle this - // - // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs - // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19 - std::fs::copy(source, destination)?; - Ok(()) - } -} diff --git a/packages/cli/src/build/context.rs b/packages/cli/src/build/context.rs new file mode 100644 index 0000000000..33eec6f9f6 --- /dev/null +++ b/packages/cli/src/build/context.rs @@ -0,0 +1,177 @@ +//! Report progress about the build to the user. We use channels to report progress back to the CLI. + +use super::BuildMode; +use crate::{BuildArtifacts, BuildStage, Error, TraceSrc}; +use cargo_metadata::CompilerMessage; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf, process::ExitStatus}; + +/// The context of the build process. While the BuildRequest is a "plan" for the build, the BuildContext +/// provides some dynamic configuration that is only known at runtime. For example, the Progress channel +/// and the BuildMode can change while serving. +/// +/// The structure of this is roughly taken from cargo itself which uses a similar pattern. +#[derive(Debug, Clone)] +pub struct BuildContext { + pub tx: ProgressTx, + pub mode: BuildMode, +} + +pub type ProgressTx = UnboundedSender; +pub type ProgressRx = UnboundedReceiver; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuildId(pub(crate) usize); +impl BuildId { + pub const CLIENT: Self = Self(0); + pub const SERVER: Self = Self(1); +} + +#[allow(clippy::large_enum_variant)] +pub enum BuilderUpdate { + Progress { + stage: BuildStage, + }, + + CompilerMessage { + message: CompilerMessage, + }, + + BuildReady { + bundle: BuildArtifacts, + }, + + BuildFailed { + err: Error, + }, + + /// A running process has received a stdout. + /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. + /// + /// We will poll lines and any content in a 50ms interval + StdoutReceived { + msg: String, + }, + + /// A running process has received a stderr. + /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. + /// + /// We will poll lines and any content in a 50ms interval + StderrReceived { + msg: String, + }, + + ProcessExited { + status: ExitStatus, + }, +} + +impl BuildContext { + pub(crate) fn status_wasm_bindgen_start(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::RunningBindgen, + }); + } + + pub(crate) fn status_splitting_bundle(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::SplittingBundle, + }); + } + + pub(crate) fn status_start_bundle(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::Bundling, + }); + } + + pub(crate) fn status_running_gradle(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::RunningGradle, + }) + } + + pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) { + _ = self + .tx + .unbounded_send(BuilderUpdate::CompilerMessage { message }); + } + + pub(crate) fn status_build_error(&self, line: String) { + tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}"); + } + + pub(crate) fn status_build_message(&self, line: String) { + tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}"); + } + + pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::Compiling { + current: count, + total, + krate: name, + }, + }); + } + + pub(crate) fn status_starting_build(&self, crate_count: usize) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::Starting { + patch: matches!(self.mode, BuildMode::Thin { .. }), + crate_count, + }, + }); + } + + pub(crate) fn status_starting_link(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::Linking, + }); + } + + pub(crate) fn status_copied_asset( + progress: &UnboundedSender, + current: usize, + total: usize, + path: PathBuf, + ) { + _ = progress.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::CopyingAssets { + current, + total, + path, + }, + }); + } + + pub(crate) fn status_optimizing_wasm(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::OptimizingWasm, + }); + } + + pub(crate) fn status_hotpatching(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::Hotpatching, + }); + } + + pub(crate) fn status_installing_tooling(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::InstallingTooling, + }); + } + + pub(crate) fn status_compressing_assets(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::CompressingAssets, + }); + } + pub(crate) fn status_extracting_assets(&self) { + _ = self.tx.unbounded_send(BuilderUpdate::Progress { + stage: BuildStage::ExtractingAssets, + }); + } +} diff --git a/packages/cli/src/build/mod.rs b/packages/cli/src/build/mod.rs index 56d9eb40b6..9edfdc5168 100644 --- a/packages/cli/src/build/mod.rs +++ b/packages/cli/src/build/mod.rs @@ -1,20 +1,21 @@ -//! The primary entrypoint for our build + optimize + bundle engine +//! The core build module for `dx`, enabling building, bundling, and runtime hot-patching of Rust +//! applications. This module defines the entire end-to-end build process, including bundling for +//! all major platforms including Mac, Windows, Linux, iOS, Android, and WebAssembly. //! -//! Handles multiple ongoing tasks and allows you to queue up builds from interactive and non-interactive contexts -//! -//! Uses a request -> response architecture that allows you to monitor the progress with an optional message -//! receiver. +//! The bulk of the builder code is contained within the [`request`] module which establishes the +//! arguments and flow of the build process. The [`context`] module contains the context for the build +//! including status updates and build customization. The [`patch`] module contains the logic for +//! hot-patching Rust code through binary analysis and a custom linker. The [`builder`] module contains +//! the management of the ongoing build and methods to open the build as a running app. mod builder; -mod bundle; -mod prerender; -mod progress; +mod context; +mod patch; mod request; -mod templates; -mod verify; -mod web; +mod tools; pub(crate) use builder::*; -pub(crate) use bundle::*; -pub(crate) use progress::*; +pub(crate) use context::*; +pub(crate) use patch::*; pub(crate) use request::*; +pub(crate) use tools::*; diff --git a/packages/cli/src/build/patch.rs b/packages/cli/src/build/patch.rs new file mode 100644 index 0000000000..4bffce1872 --- /dev/null +++ b/packages/cli/src/build/patch.rs @@ -0,0 +1,1369 @@ +use anyhow::Context; +use itertools::Itertools; +use object::{ + macho::{self}, + read::File, + write::{MachOBuildVersion, StandardSection, Symbol, SymbolSection}, + Endianness, Object, ObjectSymbol, SymbolKind, SymbolScope, +}; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + ops::{Deref, Range}, + path::Path, + path::PathBuf, + sync::{Arc, RwLock}, +}; +use subsecond_types::{AddressMap, JumpTable}; +use target_lexicon::{Architecture, OperatingSystem, Triple}; +use thiserror::Error; +use walrus::{ + ConstExpr, DataKind, ElementItems, ElementKind, FunctionBuilder, FunctionId, FunctionKind, + ImportKind, Module, ModuleConfig, TableId, +}; +use wasmparser::{ + BinaryReader, BinaryReaderError, Linking, LinkingSectionReader, Payload, SymbolInfo, +}; + +type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum PatchError { + #[error("Failed to read file: {0}")] + ReadFs(#[from] std::io::Error), + + #[error("No debug symbols in the patch output. Check your profile's `opt-level` and debug symbols config.")] + MissingSymbols, + + #[error("Failed to parse wasm section: {0}")] + ParseSection(#[from] wasmparser::BinaryReaderError), + + #[error("Failed to parse object file, {0}")] + ParseObjectFile(#[from] object::read::Error), + + #[error("Failed to write object file: {0}")] + WriteObjectFIle(#[from] object::write::Error), + + #[error("Failed to emit module: {0}")] + RuntimeError(#[from] anyhow::Error), + + #[error("Failed to read module's PDB file: {0}")] + PdbLoadError(#[from] pdb::Error), + + #[error("{0}")] + InvalidModule(String), + + #[error("Unsupported platform: {0}")] + UnsupportedPlatform(String), +} + +/// A cache for the hotpatching engine that stores the original module's parsed symbol table. +/// For large projects, this can shave up to 50% off the total patching time. Since we compile the base +/// module with every symbol in it, it can be quite large (hundreds of MB), so storing this here lets +/// us avoid re-parsing the module every time we want to patch it. +/// +/// On the Dioxus Docsite, it dropped the patch time from 3s to 1.1s (!) +#[derive(Default)] +pub struct HotpatchModuleCache { + pub path: PathBuf, + + // .... wasm stuff + pub symbol_ifunc_map: HashMap, + pub old_wasm: Module, + pub old_bytes: Vec, + pub old_exports: HashSet, + pub old_imports: HashSet, + + // ... native stuff + pub symbol_table: HashMap, +} + +pub struct CachedSymbol { + pub address: u64, + pub kind: SymbolKind, + pub is_undefined: bool, + pub is_weak: bool, +} + +impl PartialEq for HotpatchModuleCache { + fn eq(&self, other: &Self) -> bool { + self.path == other.path + } +} + +impl std::fmt::Debug for HotpatchModuleCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HotpatchModuleCache") + .field("_path", &self.path) + .finish() + } +} + +impl HotpatchModuleCache { + /// This caching step is crucial for performance on large projects. The original module can be + /// quite large (hundreds of MB), so this step drastically speeds it up. + pub fn new(original: &Path, triple: &Triple) -> Result { + let cache = match triple.operating_system { + OperatingSystem::Windows => { + use pdb::FallibleIterator; + + // due to lifetimes, this code is unfortunately duplicated. + // the pdb crate doesn't bind the lifetime of the items in the iterator to the symbol table, + // so we're stuck with local lifetime.s + let old_pdb_file = original.with_extension("pdb"); + let old_pdb_file_handle = std::fs::File::open(old_pdb_file)?; + let mut pdb_file = pdb::PDB::open(old_pdb_file_handle)?; + let global_symbols = pdb_file.global_symbols()?; + let address_map = pdb_file.address_map()?; + let mut symbol_table = HashMap::new(); + let mut symbols = global_symbols.iter(); + while let Ok(Some(symbol)) = symbols.next() { + match symbol.parse() { + Ok(pdb::SymbolData::Public(data)) => { + let rva = data.offset.to_rva(&address_map); + let is_undefined = rva.is_none(); + + // treat undefined symbols as 0 to match macho/elf + let rva = rva.unwrap_or_default(); + + symbol_table.insert( + data.name.to_string().to_string(), + CachedSymbol { + address: rva.0 as u64, + kind: if data.function { + SymbolKind::Text + } else { + SymbolKind::Data + }, + is_undefined, + is_weak: false, + }, + ); + } + + Ok(pdb::SymbolData::Data(data)) => { + let rva = data.offset.to_rva(&address_map); + let is_undefined = rva.is_none(); + + // treat undefined symbols as 0 to match macho/elf + let rva = rva.unwrap_or_default(); + + symbol_table.insert( + data.name.to_string().to_string(), + CachedSymbol { + address: rva.0 as u64, + kind: SymbolKind::Data, + is_undefined, + is_weak: false, + }, + ); + } + + _ => {} + } + } + + HotpatchModuleCache { + symbol_table, + path: original.to_path_buf(), + ..Default::default() + } + } + + // We need to load the ifunc table from the original module since that gives us the map + // of name to address (since ifunc entries are also pointers in wasm - ie 0x30 is the 30th + // entry in the ifunc table) + // + // One detail here is that with high optimization levels, the names of functions in the ifunc + // table will be smaller than the total number of functions in the module. This is because + // in high opt-levels, functions are merged. Fortunately, the symbol table remains intact + // and functions with different names point to the same function index (not to be confused + // with the function index in the module!). + // + // We need to take an extra step to account for merged functions by mapping function index + // to a set of functions that point to the same index. + _ if triple.architecture == Architecture::Wasm32 => { + let bytes = std::fs::read(original)?; + let ParsedModule { + module, symbols, .. + } = parse_module_with_ids(&bytes)?; + + if symbols.symbols.is_empty() { + return Err(PatchError::MissingSymbols); + } + + let name_to_ifunc_old = collect_func_ifuncs(&module); + + // These are the "real" bindings for functions in the module + // Basically a map between a function's index and its real name + let func_to_index = module + .funcs + .par_iter() + .filter_map(|f| { + let name = f.name.as_deref()?; + Some((*symbols.code_symbol_map.get(name)?, name)) + }) + .collect::>(); + + // Find the corresponding function that shares the same index, but in the ifunc table + let name_to_ifunc_old: HashMap<_, _> = symbols + .code_symbol_map + .par_iter() + .filter_map(|(name, idx)| { + let new_modules_unified_function = func_to_index.get(idx)?; + let offset = name_to_ifunc_old.get(new_modules_unified_function)?; + Some((*name, *offset)) + }) + .collect(); + + let symbol_ifunc_map = name_to_ifunc_old + .par_iter() + .map(|(name, idx)| (name.to_string(), *idx)) + .collect::>(); + + let old_exports = module + .exports + .iter() + .map(|e| e.name.to_string()) + .collect::>(); + + let old_imports = module + .imports + .iter() + .map(|i| i.name.to_string()) + .collect::>(); + + HotpatchModuleCache { + path: original.to_path_buf(), + old_bytes: bytes, + symbol_ifunc_map, + old_exports, + old_imports, + old_wasm: module, + ..Default::default() + } + } + _ => { + let old_bytes = std::fs::read(original)?; + let obj = File::parse(&old_bytes as &[u8])?; + let symbol_table = obj + .symbols() + .filter_map(|s| { + Some(( + s.name().ok()?.to_string(), + CachedSymbol { + address: s.address(), + is_undefined: s.is_undefined(), + is_weak: s.is_weak(), + kind: s.kind(), + }, + )) + }) + .collect::>(); + HotpatchModuleCache { + symbol_table, + path: original.to_path_buf(), + old_bytes, + ..Default::default() + } + } + }; + + Ok(cache) + } +} + +/// Create a jump table for the given original and patch files. +pub fn create_jump_table( + patch: &Path, + triple: &Triple, + cache: &HotpatchModuleCache, +) -> Result { + // Symbols are stored differently based on the platform, so we need to handle them differently. + // - Wasm requires the walrus crate and actually modifies the patch file + // - windows requires the pdb crate and pdb files + // - nix requires the object crate + match triple.operating_system { + OperatingSystem::Windows => create_windows_jump_table(patch, cache), + _ if triple.architecture == Architecture::Wasm32 => create_wasm_jump_table(patch, cache), + _ => create_native_jump_table(patch, triple, cache), + } +} + +fn create_windows_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Result { + use pdb::FallibleIterator; + let old_name_to_addr = &cache.symbol_table; + + let mut new_name_to_addr = HashMap::new(); + let new_pdb_file_handle = std::fs::File::open(patch.with_extension("pdb"))?; + let mut pdb_file = pdb::PDB::open(new_pdb_file_handle)?; + let symbol_table = pdb_file.global_symbols()?; + let address_map = pdb_file.address_map()?; + let mut symbol_iter = symbol_table.iter(); + while let Ok(Some(symbol)) = symbol_iter.next() { + if let Ok(pdb::SymbolData::Public(data)) = symbol.parse() { + let rva = data.offset.to_rva(&address_map); + if let Some(rva) = rva { + new_name_to_addr.insert(data.name.to_string(), rva.0 as u64); + } + } + } + + let mut map = AddressMap::default(); + for (new_name, new_addr) in new_name_to_addr.iter() { + if let Some(old_addr) = old_name_to_addr.get(new_name.as_ref()) { + map.insert(old_addr.address, *new_addr); + } + } + + let new_base_address = new_name_to_addr + .get("main") + .cloned() + .context("failed to find 'main' symbol in patch")?; + + let aslr_reference = old_name_to_addr + .get("__aslr_reference") + .map(|s| s.address) + .context("failed to find '_aslr_reference' symbol in original module")?; + + Ok(JumpTable { + lib: patch.to_path_buf(), + map, + new_base_address, + aslr_reference, + ifunc_count: 0, + }) +} + +/// Assemble a jump table for "nix" architectures. This uses the `object` crate to parse both +/// executable's symbol tables and then creates a mapping between the two. Unlike windows, the symbol +/// tables are stored within the binary itself, so we can use the `object` crate to parse them. +/// +/// We use the `_aslr_reference` as a reference point in the base program to calculate the aslr slide +/// both at compile time and at runtime. +/// +/// This does not work for WASM since the `object` crate does not support emitting the WASM format, +/// and because WASM requires more logic to handle the wasm-bindgen transformations. +fn create_native_jump_table( + patch: &Path, + triple: &Triple, + cache: &HotpatchModuleCache, +) -> Result { + let old_name_to_addr = &cache.symbol_table; + let obj2_bytes = std::fs::read(patch)?; + let obj2 = File::parse(&obj2_bytes as &[u8])?; + let mut map = AddressMap::default(); + let new_syms = obj2.symbol_map(); + + let new_name_to_addr = new_syms + .symbols() + .par_iter() + .map(|s| (s.name(), s.address())) + .collect::>(); + + for (new_name, new_addr) in new_name_to_addr.iter() { + if let Some(old_addr) = old_name_to_addr.get(*new_name) { + map.insert(old_addr.address, *new_addr); + } + } + + let new_base_address = match triple.operating_system { + // The symbol in the symtab is called "_main" but in the dysymtab it is called "main" + OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => { + *new_name_to_addr + .get("_main") + .context("failed to find '_main' symbol in patch")? + } + + // No distincation between the two on these platforms + OperatingSystem::Freebsd + | OperatingSystem::Openbsd + | OperatingSystem::Linux + | OperatingSystem::Windows => *new_name_to_addr + .get("main") + .context("failed to find 'main' symbol in patch")?, + + // On wasm, it doesn't matter what the address is since the binary is PIC + _ => 0, + }; + + let aslr_reference = old_name_to_addr + .get("___aslr_reference") + .or_else(|| old_name_to_addr.get("__aslr_reference")) + .map(|s| s.address) + .context("failed to find '___aslr_reference' symbol in original module")?; + + Ok(JumpTable { + lib: patch.to_path_buf(), + map, + new_base_address, + aslr_reference, + ifunc_count: 0, + }) +} + +/// In the web, our patchable functions are actually ifuncs +/// +/// We need to line up the ifuncs from the main module to the ifuncs in the patch. +/// +/// According to the dylink spec, there will be two sets of entries: +/// +/// - got.func: functions in the indirect function table +/// - got.mem: data objects in the data segments +/// +/// It doesn't seem like we can compile the base module to export these, sadly, so we're going +/// to manually satisfy them here, removing their need to be imported. +/// +/// https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md +fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Result { + let name_to_ifunc_old = &cache.symbol_ifunc_map; + let old = &cache.old_wasm; + let old_symbols = + parse_bytes_to_data_segment(&cache.old_bytes).context("Failed to parse data segment")?; + let new_bytes = std::fs::read(patch).context("Could not read patch file")?; + + let mut new = Module::from_buffer(&new_bytes)?; + let mut got_mems = vec![]; + let mut got_funcs = vec![]; + let mut wbg_funcs = vec![]; + let mut env_funcs = vec![]; + + // Collect all the GOT entries from the new module. + // The GOT imports come from the wasm-ld implementation of the dynamic linking spec + // + // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md#imports + // + // Normally, the base module would synthesize these as exports, but we're not compiling the base + // module with `--pie` (nor does wasm-bindgen support it yet), so we need to manually satisfy them. + // + // One thing to watch out for here is that GOT.func entries have no visibility to any de-duplication + // or merging, so we need to take great care in the base module to export *every* symbol even if + // they point to the same function. + // + // The other thing to watch out for here is the __wbindgen_placeholder__ entries. These are meant + // to be satisfied by wasm-bindgen via manual code generation, but we can't run wasm-bindgen on the + // patch, so we need to do it ourselves. This involves preventing their elimination in the base module + // by prefixing them with `__saved_wbg_`. When handling the imports here, we need modify the imported + // name to match the prefixed export name in the base module. + for import in new.imports.iter() { + match import.module.as_str() { + "GOT.func" => { + let Some(entry) = name_to_ifunc_old.get(import.name.as_str()).cloned() else { + return Err(PatchError::InvalidModule(format!( + "Expected to find GOT.func entry in ifunc table: {}", + import.name.as_str() + ))); + }; + got_funcs.push((import.id(), entry)); + } + "GOT.mem" => got_mems.push(import.id()), + "env" => env_funcs.push(import.id()), + "__wbindgen_placeholder__" => wbg_funcs.push(import.id()), + m => tracing::trace!("Unknown import: {m}:{}", import.name), + } + } + + // We need to satisfy the GOT.func imports of this side module. The GOT imports come from the wasm-ld + // implementation of the dynamic linking spec + // + // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md#imports + // + // Most importantly, these functions are functions meant to be called indirectly. In normal wasm + // code generation, only functions that Rust code references via pointers are given a slot in + // the indirection function table. The optimization here traditionally meaning that if a function + // can be called directly, then it doesn't need to be referenced indirectly and potentially inlined + // or dissolved during LTO. + // + // In our "fat build" setup, we aggregated all symbols from dependencies into a `dependencies.ar` file. + // By promoting these functions to the dynamic scope, we also prevent their inlining because the + // linker can still expect some form of interposition to happen, requiring the symbol *actually* + // exists. + // + // Our technique here takes advantage of that and the [`prepare_wasm_base_module`] function promotes + // every possible function to the indirect function table. This means that the GOT imports that + // `relocation-model=pic` synthesizes can reference the functions via the indirect function table + // even if they are not normally synthesized in regular wasm code generation. + // + // Normally, the dynaic linker setup would resolve GOT.func against the same GOT.func export in + // the main module, but we don't have that. Instead, we simply re-parse the main module, aggregate + // its ifunc table, and then resolve directly to the index in that table. + for (import_id, ifunc_index) in got_funcs { + let import = new.imports.get(import_id); + let ImportKind::Global(id) = import.kind else { + return Err(PatchError::InvalidModule(format!( + "Expected GOT.func import to be a global: {}", + import.name + ))); + }; + + // "satisfying" the import means removing it from the import table and replacing its target + // value with a local global. + new.imports.delete(import_id); + new.globals.get_mut(id).kind = + walrus::GlobalKind::Local(ConstExpr::Value(walrus::ir::Value::I32(ifunc_index))); + } + + // We need to satisfy the GOT.mem imports of this side module. The GOT.mem imports come from the wasm-ld + // implementation of the dynamic linking spec + // + // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md#imports + // + // Unlike the ifunc table, the GOT.mem imports do not need any additional post-processing of the + // base module to satisfy. Since our patching approach works but leveraging the experimental dynamic + // PIC support in rustc[wasm] and wasm-ld, we are using the GOT.mem imports as a way of identifying + // data segments that are present in the base module. + // + // Normally, the dynamic linker would synthesize corresponding GOT.mem exports in the main module, + // but since we're patching on-the-fly, this table will always be out-of-date. + // + // Instead, we use the symbol table from the base module to find the corresponding data symbols + // and then resolve the offset of the data segment in the main module. Using the symbol table + // can be somewhat finicky if the user compiled the code with a high-enough opt level that nukes + // the names of the data segments, but otherwise this system works well. + // + // We simply use the name of the import as a key into the symbol table and then its offset into + // its data segment as the value within the global. + for mem in got_mems { + let import = new.imports.get(mem); + let data_symbol_idx = *old_symbols + .data_symbol_map + .get(import.name.as_str()) + .with_context(|| { + format!("Failed to find GOT.mem import by its name: {}", import.name) + })?; + let data_symbol = old_symbols + .data_symbols + .get(&data_symbol_idx) + .context("Failed to find data symbol by its index")?; + let data = old + .data + .iter() + .nth(data_symbol.which_data_segment) + .context("Missing data segment in the main module")?; + + let offset = match data.kind { + DataKind::Active { + offset: ConstExpr::Value(walrus::ir::Value::I32(idx)), + .. + } => idx, + DataKind::Active { + offset: ConstExpr::Value(walrus::ir::Value::I64(idx)), + .. + } => idx as i32, + _ => { + return Err(PatchError::InvalidModule(format!( + "Data segment of invalid table: {:?}", + data.kind + ))); + } + }; + + let ImportKind::Global(global_id) = import.kind else { + return Err(PatchError::InvalidModule( + "Expected GOT.mem import to be a global".to_string(), + )); + }; + + // "satisfying" the import means removing it from the import table and replacing its target + // value with a local global. + new.imports.delete(mem); + new.globals.get_mut(global_id).kind = walrus::GlobalKind::Local(ConstExpr::Value( + walrus::ir::Value::I32(offset + data_symbol.segment_offset as i32), + )); + } + + // wasm-bindgen has a limit on the number of exports a module can have, so we need to call the main + // module's functions indirectly. This is done by dropping the env import and replacing it with a + // local function that calls the indirect function from the table. + // + // https://github.com/emscripten-core/emscripten/issues/22863 + let ifunc_table_initializer = new + .elements + .iter() + .find_map(|e| match e.kind { + ElementKind::Active { table, .. } => Some(table), + _ => None, + }) + .context("Missing ifunc table")?; + for env_func_import in env_funcs { + let import = new.imports.get(env_func_import); + let ImportKind::Function(func_id) = import.kind else { + continue; + }; + + if cache.old_exports.contains(import.name.as_str()) + || cache.old_imports.contains(import.name.as_str()) + { + continue; + } + + if let Some(table_idx) = name_to_ifunc_old.get(import.name.as_str()) { + let name = import.name.as_str().to_string(); + new.imports.delete(env_func_import); + convert_import_to_ifunc_call( + &mut new, + ifunc_table_initializer, + func_id, + *table_idx, + name, + ); + } + } + + // Wire up the preserved intrinsic functions that we saved before running wasm-bindgen to the expected + // imports from the patch. + for import_id in wbg_funcs { + let import = new.imports.get_mut(import_id); + import.module = "env".into(); + import.name = format!("__saved_wbg_{}", import.name); + } + + // Wipe away the unnecessary sections + let customs = new.customs.iter().map(|f| f.0).collect::>(); + for custom_id in customs { + if let Some(custom) = new.customs.get_mut(custom_id) { + if custom.name().contains("manganis") || custom.name().contains("__wasm_bindgen") { + new.customs.delete(custom_id); + } + } + } + + // Clear the start function from the patch - we don't want any code automatically running! + new.start = None; + + // Update the wasm module on the filesystem to use the newly lifted version + let lib = patch.to_path_buf(); + std::fs::write(&lib, new.emit_wasm())?; + + // And now assemble the jump table by mapping the old ifunc table to the new one, by name + // + // The ifunc_count will be passed to the dynamic loader so it can allocate the right amount of space + // in the indirect function table when loading the patch. + let name_to_ifunc_new = collect_func_ifuncs(&new); + let ifunc_count = name_to_ifunc_new.len() as u64; + let mut map = AddressMap::default(); + for (name, idx) in name_to_ifunc_new.iter() { + if let Some(old_idx) = name_to_ifunc_old.get(*name) { + map.insert(*old_idx as u64, *idx as u64); + } + } + + Ok(JumpTable { + map, + lib, + ifunc_count, + aslr_reference: 0, + new_base_address: 0, + }) +} + +fn convert_import_to_ifunc_call( + new: &mut Module, + ifunc_table_initializer: TableId, + func_id: FunctionId, + table_idx: i32, + name: String, +) { + use walrus::ir; + + let func = new.funcs.get_mut(func_id); + let ty_id = func.ty(); + + // Convert the import function to a local function that calls the indirect function from the table + let ty = new.types.get(ty_id); + let params = ty.params().to_vec(); + let results = ty.results().to_vec(); + let locals: Vec<_> = params.iter().map(|ty| new.locals.add(*ty)).collect(); + + // New function that calls the indirect function + let mut builder = FunctionBuilder::new(&mut new.types, ¶ms, &results); + let mut body = builder.name(name).func_body(); + + // Push the params onto the stack + for arg in locals.iter() { + body.local_get(*arg); + } + + // And then the address of the indirect function + body.instr(ir::Instr::Const(ir::Const { + value: ir::Value::I32(table_idx), + })); + + // And call it + body.instr(ir::Instr::CallIndirect(ir::CallIndirect { + ty: ty_id, + table: ifunc_table_initializer, + })); + + new.funcs.get_mut(func_id).kind = FunctionKind::Local(builder.local_func(locals)); +} + +fn collect_func_ifuncs(m: &Module) -> HashMap<&str, i32> { + // Collect all the functions in the module that are ifuncs + let mut func_to_offset = HashMap::new(); + for el in m.elements.iter() { + let ElementKind::Active { offset, .. } = &el.kind else { + continue; + }; + + let offset = match offset { + // Handle explicit offsets + ConstExpr::Value(value) => match value { + walrus::ir::Value::I32(idx) => *idx, + walrus::ir::Value::I64(idx) => *idx as i32, + _ => continue, + }, + + // Globals are usually imports and thus don't add a specific offset + // ie the ifunc table is offset by a global, so we don't need to push the offset out + ConstExpr::Global(_) => 0, + _ => continue, + }; + + match &el.items { + ElementItems::Functions(ids) => { + for (idx, id) in ids.iter().enumerate() { + if let Some(name) = m.funcs.get(*id).name.as_deref() { + func_to_offset.insert(name, offset + idx as i32); + } + } + } + ElementItems::Expressions(_ref_type, _const_exprs) => {} + } + } + + func_to_offset +} + +/// Resolve the undefined symbols in the incrementals against the original binary, returning an object +/// file that can be linked along the incrementals. +/// +/// This makes it possible to dlopen the resulting object file and use the original binary's symbols +/// bypassing the dynamic linker. +/// +/// This is very similar to malware :) but it's not! +/// +/// Note - this function is not defined to run on WASM binaries. The `object` crate does not +/// +/// todo... we need to wire up the cache +pub fn create_undefined_symbol_stub( + cache: &HotpatchModuleCache, + incrementals: &[PathBuf], + triple: &Triple, + aslr_reference: u64, +) -> Result> { + let sorted: Vec<_> = incrementals.iter().sorted().collect(); + + // Find all the undefined symbols in the incrementals + let mut undefined_symbols = HashSet::new(); + let mut defined_symbols = HashSet::new(); + + for path in sorted { + let bytes = std::fs::read(path).with_context(|| format!("failed to read {:?}", path))?; + let file = File::parse(bytes.deref() as &[u8])?; + for symbol in file.symbols() { + if symbol.is_undefined() { + undefined_symbols.insert(symbol.name()?.to_string()); + } else if symbol.is_global() { + defined_symbols.insert(symbol.name()?.to_string()); + } + } + } + let undefined_symbols: Vec<_> = undefined_symbols + .difference(&defined_symbols) + .cloned() + .collect(); + + tracing::trace!("Undefined symbols: {:#?}", undefined_symbols); + + // Create a new object file (architecture doesn't matter much for our purposes) + let mut obj = object::write::Object::new( + match triple.binary_format { + target_lexicon::BinaryFormat::Elf => object::BinaryFormat::Elf, + target_lexicon::BinaryFormat::Macho => object::BinaryFormat::MachO, + target_lexicon::BinaryFormat::Coff => object::BinaryFormat::Coff, + target_lexicon::BinaryFormat::Wasm => object::BinaryFormat::Wasm, + target_lexicon::BinaryFormat::Xcoff => object::BinaryFormat::Xcoff, + _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())), + }, + match triple.architecture { + Architecture::Aarch64(_) => object::Architecture::Aarch64, + Architecture::Wasm32 => object::Architecture::Wasm32, + Architecture::X86_64 => object::Architecture::X86_64, + _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())), + }, + match triple.endianness() { + Ok(target_lexicon::Endianness::Little) => Endianness::Little, + Ok(target_lexicon::Endianness::Big) => Endianness::Big, + _ => Endianness::Little, + }, + ); + + // Write the headers so we load properly in ios/macos + #[allow(clippy::identity_op)] + match triple.operating_system { + OperatingSystem::Darwin(_) => { + obj.set_macho_build_version({ + let mut build_version = MachOBuildVersion::default(); + build_version.platform = macho::PLATFORM_MACOS; + build_version.minos = (11 << 16) | (0 << 8) | 0; // 11.0.0 + build_version.sdk = (11 << 16) | (0 << 8) | 0; // SDK 11.0.0 + build_version + }); + } + OperatingSystem::IOS(_) => { + obj.set_macho_build_version({ + let mut build_version = MachOBuildVersion::default(); + build_version.platform = match triple.environment { + target_lexicon::Environment::Sim => macho::PLATFORM_IOSSIMULATOR, + _ => macho::PLATFORM_IOS, + }; + build_version.minos = (14 << 16) | (0 << 8) | 0; // 14.0.0 + build_version.sdk = (14 << 16) | (0 << 8) | 0; // SDK 14.0.0 + build_version + }); + } + + _ => {} + } + + let symbol_table = &cache.symbol_table; + + // Get the offset from the main module and adjust the addresses by the slide + let aslr_ref_address = symbol_table + .get("___aslr_reference") + .or_else(|| symbol_table.get("__aslr_reference")) + .map(|s| s.address) + .context("Failed to find ___aslr_reference symbol")?; + let aslr_offset = aslr_reference - aslr_ref_address; + + // we need to assemble a PLT/GOT so direct calls to the patch symbols work + // for each symbol we either write the address directly (as a symbol) or create a PLT/GOT entry + let text_section = obj.section_id(StandardSection::Text); + for name in undefined_symbols { + let Some(sym) = symbol_table.get(name.as_str().trim_start_matches("__imp_")) else { + tracing::error!("Symbol not found: {}", name); + continue; + }; + + // Undefined symbols tend to be import symbols (darwin gives them an address of 0 until defined). + // If we fail to skip these, then we end up with stuff like alloc at 0x0 which is quite bad! + if sym.is_undefined { + continue; + } + + // ld64 likes to prefix symbols in intermediate object files with an underscore, but our symbol + // table doesn't, so we need to strip it off. + let name_offset = match triple.operating_system { + OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => 1, + _ => 0, + }; + + let abs_addr = sym.address + aslr_offset; + + match sym.kind { + // Handle synthesized window linker cross-dll statics. + // + // The `__imp_` prefix is a rather poorly documented feature of link.exe that makes it possible + // to reference statics in DLLs via text sections. The linker will synthesize a function + // that returns the address of the static, so calling that function will return the address. + // We want to satisfy it by creating a data symbol with the contents of the *actual* symbol + // in the original binary. + // + // We ca't use the `__imp_` from the original binary because it was not properly compiled + // with this in mind. Instead we have to create the new symbol. + // + // This is currently only implemented for 64bit architectures (haven't tested 32bit yet). + // + // https://stackoverflow.com/questions/5159353/how-can-i-get-rid-of-the-imp-prefix-in-the-linker-in-vc + _ if name.starts_with("__imp_") => { + let data_section = obj.section_id(StandardSection::Data); + + // Add a pointer to the resolved address + let offset = obj.append_section_data( + data_section, + &abs_addr.to_le_bytes(), + 8, // Use proper alignment + ); + + // Add the symbol as a data symbol in our data section + obj.add_symbol(Symbol { + name: name.as_bytes().to_vec(), + value: offset, // Offset within the data section + size: 8, // Size of pointer + scope: SymbolScope::Linkage, + kind: SymbolKind::Data, // Always Data for IAT entries + weak: false, + section: SymbolSection::Section(data_section), + flags: object::SymbolFlags::None, + }); + } + + // Text symbols are normal code symbols. We need to assemble stubs that resolve the undefined + // symbols and jump to the original address in the original binary. + // + // Unfortunately this isn't simply cross-platform, so we need to handle Unix and Windows + // calling conventions separately. It also depends on the architecture, making it even more + // complicated. + SymbolKind::Text => { + let jump_asm = match triple.operating_system { + // The windows ABI and calling convention is different than the SystemV ABI. + OperatingSystem::Windows => match triple.architecture { + Architecture::X86_64 => { + // Windows x64 has specific requirements for alignment and position-independent code + let mut code = vec![ + 0x48, 0xB8, // movabs RAX, imm64 (move 64-bit immediate to RAX) + ]; + // Append the absolute 64-bit address + code.extend_from_slice(&abs_addr.to_le_bytes()); + // jmp RAX (jump to the address in RAX) + code.extend_from_slice(&[0xFF, 0xE0]); + code + } + Architecture::X86_32(_) => { + // On Windows 32-bit, we can use direct jump but need proper alignment + let mut code = vec![ + 0xB8, // mov EAX, imm32 (move immediate value to EAX) + ]; + // Append the absolute 32-bit address + code.extend_from_slice(&(abs_addr as u32).to_le_bytes()); + // jmp EAX (jump to the address in EAX) + code.extend_from_slice(&[0xFF, 0xE0]); + code + } + Architecture::Aarch64(_) => { + // Use MOV/MOVK sequence to load 64-bit address into X16 + // This is more reliable than ADRP+LDR for direct hotpatching + let mut code = Vec::new(); + + // MOVZ X16, #imm16_0 (bits 0-15 of address) + let imm16_0 = (abs_addr & 0xFFFF) as u16; + let movz = 0xD2800010u32 | ((imm16_0 as u32) << 5); + code.extend_from_slice(&movz.to_le_bytes()); + + // MOVK X16, #imm16_1, LSL #16 (bits 16-31 of address) + let imm16_1 = ((abs_addr >> 16) & 0xFFFF) as u16; + let movk1 = 0xF2A00010u32 | ((imm16_1 as u32) << 5); + code.extend_from_slice(&movk1.to_le_bytes()); + + // MOVK X16, #imm16_2, LSL #32 (bits 32-47 of address) + let imm16_2 = ((abs_addr >> 32) & 0xFFFF) as u16; + let movk2 = 0xF2C00010u32 | ((imm16_2 as u32) << 5); + code.extend_from_slice(&movk2.to_le_bytes()); + + // MOVK X16, #imm16_3, LSL #48 (bits 48-63 of address) + let imm16_3 = ((abs_addr >> 48) & 0xFFFF) as u16; + let movk3 = 0xF2E00010u32 | ((imm16_3 as u32) << 5); + code.extend_from_slice(&movk3.to_le_bytes()); + + // BR X16 (Branch to address in X16) + code.extend_from_slice(&[0x00, 0x02, 0x1F, 0xD6]); + + code + } + Architecture::Arm(_) => { + // For Windows 32-bit ARM, we need a different approach + let mut code = Vec::new(); + // LDR r12, [pc, #8] ; Load the address into r12 + code.extend_from_slice(&[0x08, 0xC0, 0x9F, 0xE5]); + // BX r12 ; Branch to the address in r12 + code.extend_from_slice(&[0x1C, 0xFF, 0x2F, 0xE1]); + // 4-byte alignment padding + code.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + // Store the 32-bit address - 4-byte aligned + code.extend_from_slice(&(abs_addr as u32).to_le_bytes()); + code + } + _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())), + }, + _ => match triple.architecture { + Architecture::X86_64 => { + // Use JMP instruction to absolute address: FF 25 followed by 32-bit offset + // Then the 64-bit absolute address + let mut code = vec![0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]; // jmp [rip+0] + // Append the 64-bit address + code.extend_from_slice(&abs_addr.to_le_bytes()); + code + } + Architecture::X86_32(_) => { + // For 32-bit Intel, use JMP instruction with absolute address + let mut code = vec![0xE9]; // jmp rel32 + let rel_addr = abs_addr as i32 - 5; // Relative address (offset from next instruction) + code.extend_from_slice(&rel_addr.to_le_bytes()); + code + } + Architecture::Aarch64(_) => { + // For ARM64, we load the address into a register and branch + let mut code = Vec::new(); + // LDR X16, [PC, #0] ; Load from the next instruction + code.extend_from_slice(&[0x50, 0x00, 0x00, 0x58]); + // BR X16 ; Branch to the address in X16 + code.extend_from_slice(&[0x00, 0x02, 0x1F, 0xD6]); + // Store the 64-bit address + code.extend_from_slice(&abs_addr.to_le_bytes()); + code + } + Architecture::Arm(_) => { + // For 32-bit ARM, use LDR PC, [PC, #-4] to load the address and branch + let mut code = Vec::new(); + // LDR PC, [PC, #-4] ; Load the address into PC (branching to it) + code.extend_from_slice(&[0x04, 0xF0, 0x1F, 0xE5]); + // Store the 32-bit address + code.extend_from_slice(&(abs_addr as u32).to_le_bytes()); + code + } + _ => return Err(PatchError::UnsupportedPlatform(triple.to_string())), + }, + }; + + let offset = obj.append_section_data(text_section, &jump_asm, 8); + obj.add_symbol(Symbol { + name: name.as_bytes()[name_offset..].to_vec(), + value: offset, + size: jump_asm.len() as u64, + scope: SymbolScope::Linkage, + kind: SymbolKind::Text, + weak: false, + section: SymbolSection::Section(text_section), + flags: object::SymbolFlags::None, + }); + } + + // We just assume all non-text symbols are data (globals, statics, etc) + _ => { + // darwin statics show up as "unknown" symbols even though they are data symbols. + let kind = match sym.kind { + SymbolKind::Unknown => SymbolKind::Data, + k => k, + }; + obj.add_symbol(Symbol { + name: name.as_bytes()[name_offset..].to_vec(), + value: abs_addr, + size: 0, + scope: SymbolScope::Linkage, + kind, + weak: sym.is_weak, + section: SymbolSection::Absolute, + flags: object::SymbolFlags::None, + }); + } + } + } + + Ok(obj.write()?) +} + +/// Prepares the base module before running wasm-bindgen. +/// +/// This tries to work around how wasm-bindgen works by intelligently promoting non-wasm-bindgen functions +/// to the export table. +/// +/// It also moves all functions and memories to be callable indirectly. +pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result> { + let ParsedModule { + mut module, + ids, + symbols, + .. + } = parse_module_with_ids(bytes)?; + + // Due to monomorphizations, functions will get merged and multiple names will point to the same function. + // Walrus loses this information, so we need to manually parse the names table to get the indices + // and names of these functions. + // + // Unfortunately, the indices it gives us ARE NOT VALID. + // We need to work around it by using the FunctionId from the module as a link between the merged function names. + let ifunc_map = collect_func_ifuncs(&module); + let ifuncs = module + .funcs + .par_iter() + .filter_map(|f| ifunc_map.get(f.name.as_deref()?).map(|_| f.id())) + .collect::>(); + + let imported_funcs = module + .imports + .iter() + .filter_map(|i| match i.kind { + ImportKind::Function(id) => Some((id, i.id())), + _ => None, + }) + .collect::>(); + + // Wasm-bindgen will synthesize imports to satisfy its external calls. This facilitates things + // like inline-js, snippets, and literally the `#[wasm_bindgen]` macro. All calls to JS are + // just `extern "wbg"` blocks! + // + // However, wasm-bindgen will run a GC pass on the module, removing any unused imports. + let mut make_indirect = vec![]; + for (imported_func, importid) in imported_funcs { + let import = module.imports.get(importid); + let name_is_wbg = + import.name.starts_with("__wbindgen") || import.name.starts_with("__wbg_"); + + if name_is_wbg && !name_is_bindgen_symbol(import.name.as_str()) { + let func = module.funcs.get(imported_func); + + let ty = module.types.get(func.ty()); + let params = ty.params().to_vec(); + let results = ty.results().to_vec(); + + let mut builder = FunctionBuilder::new(&mut module.types, ¶ms, &results); + let mut body = builder + .name(format!("__saved_wbg_{}", import.name)) + .func_body(); + + let locals = params + .iter() + .map(|ty| module.locals.add(*ty)) + .collect::>(); + + for l in locals.iter() { + body.local_get(*l); + } + + body.call(imported_func); + + let new_func_id = module.funcs.add_local(builder.local_func(locals)); + + module + .exports + .add(&format!("__saved_wbg_{}", import.name), new_func_id); + + make_indirect.push(new_func_id); + } + } + + for (name, index) in symbols.code_symbol_map.iter() { + if name_is_bindgen_symbol(name) { + continue; + } + + let func = module.funcs.get(ids[*index]); + + // We want to preserve the intrinsics from getting gc-ed out. + // + // These will create corresponding shim functions in the main module, that the patches will + // then call. Wasm-bindgen doesn't actually check if anyone uses the `__wbindgen` exports and + // forcefully deletes them literally by checking for symbols that start with `__wbindgen`. We + // preserve these symbols by naming them `__saved_wbg_` and then exporting them. + // + // When wasm-bindgen runs, it will wrap these intrinsics with an `externref shim`, but we + // want to preserve the actual underlying function so side modules can call them directly. + // + // https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1505 + if name.starts_with("__wbindgen") { + module + .exports + .add(&format!("__saved_wbg_{name}"), func.id()); + } + + // This is basically `--export-all` but designed to work around wasm-bindgen not properly gc-ing + // imports like __wbindgen_placeholder__ and __wbindgen_externref__ + // + // We only export local functions, and then make sure they can be accessible indirectly. + // If we weren't dealing with PIC code, then we could just create local ifuncs in the patch that + // call the original function directly. Unfortunately, this would require adding a new relocation + // to corresponding GOT.func entry, which we don't want to deal with. + // + // Note that we don't export via the export table, but rather the ifunc table. This is to work + // around issues on large projects where we hit the maximum number of exports. + // + // https://github.com/emscripten-core/emscripten/issues/22863 + if let FunctionKind::Local(_) = &func.kind { + if !ifuncs.contains(&func.id()) { + make_indirect.push(func.id()); + } + } + } + + // Now we need to make sure to add the new ifuncs to the ifunc segment initializer. + // We just assume the last segment is the safest one we can add to which is common practice. + let segment = module + .elements + .iter_mut() + .last() + .context("Missing ifunc table")?; + let make_indirect_count = make_indirect.len() as u64; + let ElementItems::Functions(segment_ids) = &mut segment.items else { + return Err(PatchError::InvalidModule( + "Expected ifunc table to be a function table".into(), + )); + }; + + for func in make_indirect { + segment_ids.push(func); + } + + if let ElementKind::Active { table, .. } = segment.kind { + let table = module.tables.get_mut(table); + table.initial += make_indirect_count; + if let Some(max) = table.maximum { + table.maximum = Some(max + make_indirect_count); + } + } + + Ok(module.emit_wasm()) +} + +/// Check if the name is a wasm-bindgen symbol +/// +/// todo(jon): I believe we can just look at all the functions the wasm_bindgen describe export references. +/// this is kinda hacky on slow. +/// +/// Uses the heuristics from the wasm-bindgen source code itself: +/// +/// https://github.com/rustwasm/wasm-bindgen/blob/c35cc9369d5e0dc418986f7811a0dd702fb33ef9/crates/cli-support/src/wit/mod.rs#L1165 +fn name_is_bindgen_symbol(name: &str) -> bool { + name.contains("__wbindgen_describe") + || name.contains("__wbindgen_externref") + || name.contains("wasm_bindgen8describe6inform") + || name.contains("wasm_bindgen..describe..WasmDescribe") + || name.contains("wasm_bindgen..closure..WasmClosure$GT$8describe") + || name.contains("wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe") +} + +/// Manually parse the data section from a wasm module +/// +/// We need to do this for data symbols because walrus doesn't provide the right range and offset +/// information for data segments. Fortunately, it provides it for code sections, so we only need to +/// do a small amount extra of parsing here. +fn parse_bytes_to_data_segment(bytes: &[u8]) -> Result { + let parser = wasmparser::Parser::new(0); + let mut parser = parser.parse_all(bytes); + let mut segments = vec![]; + let mut data_range = 0..0; + let mut symbols = vec![]; + + // Process the payloads in the raw wasm file so we can extract the specific sections we need + while let Some(Ok(payload)) = parser.next() { + match payload { + Payload::DataSection(section) => { + data_range = section.range(); + segments = section + .into_iter() + .collect::, BinaryReaderError>>()? + } + Payload::CustomSection(section) if section.name() == "linking" => { + let reader = BinaryReader::new(section.data(), 0); + let reader = LinkingSectionReader::new(reader)?; + for subsection in reader.subsections() { + if let Linking::SymbolTable(map) = subsection? { + symbols = map.into_iter().collect::, _>>()?; + } + } + } + Payload::CustomSection(section) => { + tracing::trace!("Skipping Custom section: {:?}", section.name()); + } + _ => {} + } + } + + // Accumulate the data symbols into a btreemap for later use + let mut data_symbols = BTreeMap::new(); + let mut data_symbol_map = HashMap::new(); + let mut code_symbol_map = BTreeMap::new(); + for (index, symbol) in symbols.iter().enumerate() { + if let SymbolInfo::Func { name, index, .. } = symbol { + if let Some(name) = name { + code_symbol_map.insert(*name, *index as usize); + } + continue; + } + + let SymbolInfo::Data { + symbol: Some(symbol), + name, + .. + } = symbol + else { + continue; + }; + + data_symbol_map.insert(*name, index); + + if symbol.size == 0 { + continue; + } + + let data_segment = segments + .get(symbol.index as usize) + .context("Failed to find data segment")?; + let offset: usize = + data_segment.range.end - data_segment.data.len() + (symbol.offset as usize); + let range = offset..(offset + symbol.size as usize); + + data_symbols.insert( + index, + DataSymbol { + _index: index, + _range: range, + segment_offset: symbol.offset as usize, + _symbol_size: symbol.size as usize, + which_data_segment: symbol.index as usize, + }, + ); + } + + Ok(RawDataSection { + _data_range: data_range, + symbols, + data_symbols, + data_symbol_map, + code_symbol_map, + }) +} + +struct RawDataSection<'a> { + _data_range: Range, + symbols: Vec>, + code_symbol_map: BTreeMap<&'a str, usize>, + data_symbols: BTreeMap, + data_symbol_map: HashMap<&'a str, usize>, +} + +#[derive(Debug)] +struct DataSymbol { + _index: usize, + _range: Range, + segment_offset: usize, + _symbol_size: usize, + which_data_segment: usize, +} + +struct ParsedModule<'a> { + module: Module, + ids: Vec, + symbols: RawDataSection<'a>, +} + +/// Parse a module and return the mapping of index to FunctionID. +/// We'll use this mapping to remap ModuleIDs +fn parse_module_with_ids(bindgened: &[u8]) -> Result { + let ids = Arc::new(RwLock::new(Vec::new())); + let ids_ = ids.clone(); + let module = Module::from_buffer_with_config( + bindgened, + ModuleConfig::new().on_parse(move |_m, our_ids| { + let mut ids = ids_.write().expect("No shared writers"); + let mut idx = 0; + while let Ok(entry) = our_ids.get_func(idx) { + ids.push(entry); + idx += 1; + } + + Ok(()) + }), + )?; + let mut ids_ = ids.write().expect("No shared writers"); + let mut ids = vec![]; + std::mem::swap(&mut ids, &mut *ids_); + + let symbols = parse_bytes_to_data_segment(bindgened).context("Failed to parse data segment")?; + + Ok(ParsedModule { + module, + ids, + symbols, + }) +} diff --git a/packages/cli/src/build/prerender.rs b/packages/cli/src/build/prerender.rs deleted file mode 100644 index eb94c08b3f..0000000000 --- a/packages/cli/src/build/prerender.rs +++ /dev/null @@ -1,125 +0,0 @@ -use anyhow::Context; -use dioxus_cli_config::{server_ip, server_port}; -use futures_util::stream::FuturesUnordered; -use futures_util::StreamExt; -use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::Path, - time::Duration, -}; -use tokio::process::Command; - -pub(crate) async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Result<()> { - // Use the address passed in through environment variables or default to localhost:9999. We need - // to default to a value that is different than the CLI default address to avoid conflicts - let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); - let port = server_port().unwrap_or(9999); - let fullstack_address = SocketAddr::new(ip, port); - let address = fullstack_address.ip().to_string(); - let port = fullstack_address.port().to_string(); - // Borrow port and address so we can easily moe them into multiple tasks below - let address = &address; - let port = &port; - - tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}"); - - // Run the server executable - let _child = Command::new(server_exe) - .env(dioxus_cli_config::SERVER_PORT_ENV, port) - .env(dioxus_cli_config::SERVER_IP_ENV, address) - .current_dir(server_exe.parent().unwrap()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .kill_on_drop(true) - .spawn()?; - - // Borrow reqwest_client so we only move the reference into the futures - let reqwest_client = reqwest::Client::new(); - let reqwest_client = &reqwest_client; - - // Get the routes from the `/static_routes` endpoint - let mut routes = None; - - // The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay - const RETRY_ATTEMPTS: usize = 5; - for i in 0..=RETRY_ATTEMPTS { - tracing::debug!( - "Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}" - ); - - let request = reqwest_client - .post(format!("http://{address}:{port}/api/static_routes")) - .body("{}".to_string()) - .send() - .await; - match request { - Ok(request) => { - routes = Some(request - .json::>() - .await - .inspect(|text| tracing::debug!("Got static routes: {text:?}")) - .context("Failed to parse static routes from the server. Make sure your server function returns Vec with the (default) json encoding")?); - break; - } - Err(err) => { - // If the request fails, try up to 5 times with a one second delay - // If it fails 5 times, return the error - if i == RETRY_ATTEMPTS { - return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec of static routes."); - } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - } - } - } - - let routes = routes.expect( - "static routes should exist or an error should have been returned on the last attempt", - ); - - // Create a pool of futures that cache each route - let mut resolved_routes = routes - .into_iter() - .map(|route| async move { - tracing::info!("Rendering {route} for SSG"); - - // For each route, ping the server to force it to cache the response for ssg - let request = reqwest_client - .get(format!("http://{address}:{port}{route}")) - .header("Accept", "text/html") - .send() - .await?; - - // If it takes longer than 30 seconds to resolve the route, log a warning - let warning_task = tokio::spawn({ - let route = route.clone(); - async move { - tokio::time::sleep(Duration::from_secs(30)).await; - tracing::warn!("Route {route} has been rendering for 30 seconds"); - } - }); - - // Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly - // because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write - // the final clean HTML to the disk automatically after the request completes. - let _html = request.text().await?; - - // Cancel the warning task if it hasn't already run - warning_task.abort(); - - Ok::<_, reqwest::Error>(route) - }) - .collect::>(); - - while let Some(route) = resolved_routes.next().await { - match route { - Ok(route) => tracing::debug!("ssg success: {route:?}"), - Err(err) => tracing::error!("ssg error: {err:?}"), - } - } - - tracing::info!("SSG complete"); - - drop(_child); - - Ok(()) -} diff --git a/packages/cli/src/build/progress.rs b/packages/cli/src/build/progress.rs deleted file mode 100644 index e0efa41023..0000000000 --- a/packages/cli/src/build/progress.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Report progress about the build to the user. We use channels to report progress back to the CLI. -use crate::{AppBundle, BuildRequest, BuildStage, Platform, TraceSrc}; -use cargo_metadata::CompilerMessage; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use std::path::PathBuf; - -pub(crate) type ProgressTx = UnboundedSender; -pub(crate) type ProgressRx = UnboundedReceiver; - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub(crate) enum BuildUpdate { - Progress { stage: BuildStage }, - CompilerMessage { message: CompilerMessage }, - BuildReady { bundle: AppBundle }, - BuildFailed { err: crate::Error }, -} - -impl BuildRequest { - pub(crate) fn status_wasm_bindgen_start(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::RunningBindgen {}, - }); - } - - pub(crate) fn status_splitting_bundle(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::SplittingBundle, - }); - } - - pub(crate) fn status_start_bundle(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::Bundling {}, - }); - } - - pub(crate) fn status_running_gradle(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::RunningGradle, - }) - } - - pub(crate) fn status_build_diagnostic(&self, message: CompilerMessage) { - _ = self - .progress - .unbounded_send(BuildUpdate::CompilerMessage { message }); - } - - pub(crate) fn status_build_error(&self, line: String) { - tracing::error!(dx_src = ?TraceSrc::Cargo, "{line}"); - } - - pub(crate) fn status_build_message(&self, line: String) { - tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}"); - } - - pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::Compiling { - current: count, - total, - krate: name, - is_server: self.is_server(), - }, - }); - } - - pub(crate) fn status_starting_build(&self, crate_count: usize) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::Starting { - is_server: self.build.platform() == Platform::Server, - crate_count, - }, - }); - } - - pub(crate) fn status_copied_asset( - progress: &UnboundedSender, - current: usize, - total: usize, - path: PathBuf, - ) { - _ = progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::CopyingAssets { - current, - total, - path, - }, - }); - } - - pub(crate) fn status_optimizing_wasm(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::OptimizingWasm {}, - }); - } - - pub(crate) fn status_prerendering_routes(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::PrerenderingRoutes {}, - }); - } - - pub(crate) fn status_installing_tooling(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::InstallingTooling {}, - }); - } - - pub(crate) fn status_compressing_assets(&self) { - _ = self.progress.unbounded_send(BuildUpdate::Progress { - stage: BuildStage::CompressingAssets, - }); - } - - pub(crate) fn is_server(&self) -> bool { - self.build.platform() == Platform::Server - } -} diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 77d9464dbf..359156ce43 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -1,155 +1,775 @@ -use super::{progress::ProgressTx, BuildArtifacts}; -use crate::dioxus_crate::DioxusCrate; -use crate::{link::LinkAction, BuildArgs}; -use crate::{AppBundle, Platform, Result, TraceSrc}; +//! # [`BuildRequest`] - the core of the build process +//! +//! The [`BuildRequest`] object is the core of the build process. It contains all the resolved arguments +//! flowing in from the CLI, dioxus.toml, env vars, and the workspace. +//! +//! Every BuildRequest is tied to a given workspace and BuildArgs. For simplicity's sake, the BuildArgs +//! struct is used to represent the CLI arguments and all other configuration is basically just +//! extra CLI arguments, but in a configuration format. +//! +//! When [`BuildRequest::build`] is called, it will prepare its work directory in the target folder +//! and then start running the build process. A [`BuildContext`] is required to customize this +//! build process, containing a channel for progress updates and the build mode. +//! +//! The [`BuildMode`] is extremely important since it influences how the build is performed. Most +//! "normal" builds just use [`BuildMode::Base`], but we also support [`BuildMode::Fat`] and +//! [`BuildMode::Thin`]. These builds are used together to power the hot-patching and fast-linking +//! engine. +//! - BuildMode::Base: A normal build generated using `cargo rustc` +//! - BuildMode::Fat: A "fat" build where all dependency rlibs are merged into a static library +//! - BuildMode::Thin: A "thin" build that dynamically links against the artifacts produced by the "fat" build +//! +//! The BuildRequest is also responsible for writing the final build artifacts to disk. This includes +//! +//! - Writing the executable +//! - Processing assets from the artifact +//! - Writing any metadata or configuration files (Info.plist, AndroidManifest.xml) +//! - Bundle splitting (for wasm) and wasm-bindgen +//! +//! In some cases, the BuildRequest also handles the linking of the final executable. Specifically, +//! - For Android, we use `dx` as an opaque linker to dynamically find the true android linker +//! - For hotpatching, the CLI manually links the final executable with a stub file +//! +//! ## Build formats: +//! +//! We support building for the most popular platforms: +//! - Web via wasm-bindgen +//! - macOS via app-bundle +//! - iOS via app-bundle +//! - Android via gradle +//! - Linux via app-image +//! - Windows via exe, msi/msix +//! +//! Note that we are missing some setups that we *should* support: +//! - PWAs, WebWorkers, ServiceWorkers +//! - Web Extensions +//! - Linux via flatpak/snap +//! +//! There are some less popular formats that we might want to support eventually: +//! - TVOS, watchOS +//! - OpenHarmony +//! +//! Also, some deploy platforms have their own bespoke formats: +//! - Cloudflare workers +//! - AWS Lambda +//! +//! Currently, we defer most of our deploy-based bundling to Tauri bundle, though we should migrate +//! to just bundling everything ourselves. This would require us to implement code-signing which +//! is a bit of a pain, but fortunately a solved process (https://github.com/rust-mobile/xbuild). +//! +//! ## Build Structure +//! +//! Builds generally follow the same structure everywhere: +//! - A main executable +//! - Sidecars (alternate entrypoints, framewrok plugins, etc) +//! - Assets (images, fonts, etc) +//! - Metadata (Info.plist, AndroidManifest.xml) +//! - Glue code (java, kotlin, javascript etc) +//! - Entitlements for code-signing and verification +//! +//! We need to be careful to not try and put a "round peg in a square hole," but most platforms follow +//! the same pattern. +//! +//! As such, we try to assemble a build directory that's somewhat sensible: +//! - A main "staging" dir for a given app +//! - Per-profile dirs (debug/release) +//! - A platform dir (ie web/desktop/android/ios) +//! - The "bundle" dir which is basically the `.app` format or `wwww` dir. +//! - The "executable" dir where the main exe is housed +//! - The "assets" dir where the assets are housed +//! - The "meta" dir where stuff like Info.plist, AndroidManifest.xml, etc are housed +//! +//! There's also some "quirky" folders that need to be stable between builds but don't influence the +//! bundle itself: +//! - session_cache_dir which stores stuff like window position +//! +//! ### Web: +//! +//! Create a folder that is somewhat similar to an app-image (exe + asset) +//! The server is dropped into the `web` folder, even if there's no `public` folder. +//! If there's no server (SPA), we still use the `web` folder, but it only contains the +//! public folder. +//! +//! ``` +//! web/ +//! server +//! assets/ +//! public/ +//! index.html +//! wasm/ +//! app.wasm +//! glue.js +//! snippets/ +//! ... +//! assets/ +//! logo.png +//! ``` +//! +//! ### Linux: +//! +//! https://docs.appimage.org/reference/appdir.html#ref-appdir +//! current_exe.join("Assets") +//! ``` +//! app.appimage/ +//! AppRun +//! app.desktop +//! package.json +//! assets/ +//! logo.png +//! ``` +//! +//! ### Macos +//! +//! We simply use the macos format where binaries are in `Contents/MacOS` and assets are in `Contents/Resources` +//! We put assets in an assets dir such that it generally matches every other platform and we can +//! output `/assets/blah` from manganis. +//! ``` +//! App.app/ +//! Contents/ +//! Info.plist +//! MacOS/ +//! Frameworks/ +//! Resources/ +//! assets/ +//! blah.icns +//! blah.png +//! CodeResources +//! _CodeSignature/ +//! ``` +//! +//! ### iOS +//! +//! Not the same as mac! ios apps are a bit "flattened" in comparison. simpler format, presumably +//! since most ios apps don't ship frameworks/plugins and such. +//! +//! todo(jon): include the signing and entitlements in this format diagram. +//! ``` +//! App.app/ +//! main +//! assets/ +//! ``` +//! +//! ### Android: +//! +//! Currently we need to generate a `src` type structure, not a pre-packaged apk structure, since +//! we need to compile kotlin and java. This pushes us into using gradle and following a structure +//! similar to that of cargo mobile2. Eventually I'd like to slim this down (drop buildSrc) and +//! drive the kotlin build ourselves. This would let us drop gradle (yay! no plugins!) but requires +//! us to manage dependencies (like kotlinc) ourselves (yuck!). +//! +//! https://github.com/WanghongLin/miscellaneous/blob/master/tools/build-apk-manually.sh +//! +//! Unfortunately, it seems that while we can drop the `android` build plugin, we still will need +//! gradle since kotlin is basically gradle-only. +//! +//! Pre-build: +//! ``` +//! app.apk/ +//! .gradle +//! app/ +//! src/ +//! main/ +//! assets/ +//! jniLibs/ +//! java/ +//! kotlin/ +//! res/ +//! AndroidManifest.xml +//! build.gradle.kts +//! proguard-rules.pro +//! buildSrc/ +//! build.gradle.kts +//! src/ +//! main/ +//! kotlin/ +//! BuildTask.kt +//! build.gradle.kts +//! gradle.properties +//! gradlew +//! gradlew.bat +//! settings.gradle +//! ``` +//! +//! Final build: +//! ``` +//! app.apk/ +//! AndroidManifest.xml +//! classes.dex +//! assets/ +//! logo.png +//! lib/ +//! armeabi-v7a/ +//! libmyapp.so +//! arm64-v8a/ +//! libmyapp.so +//! ``` +//! Notice that we *could* feasibly build this ourselves :) +//! +//! ### Windows: +//! +//! Windows does not provide an AppImage format, so instead we're going build the same folder +//! structure as an AppImage, but when distributing, we'll create a .exe that embeds the resources +//! as an embedded .zip file. When the app runs, it will implicitly unzip its resources into the +//! Program Files folder. Any subsequent launches of the parent .exe will simply call the AppRun.exe +//! entrypoint in the associated Program Files folder. +//! +//! This is, in essence, the same as an installer, so we might eventually just support something like msi/msix +//! which functionally do the same thing but with a sleeker UI. +//! +//! This means no installers are required and we can bake an updater into the host exe. +//! +//! ## Handling asset lookups: +//! current_exe.join("assets") +//! ``` +//! app.appimage/ +//! main.exe +//! main.desktop +//! package.json +//! assets/ +//! logo.png +//! ``` +//! +//! Since we support just a few locations, we could just search for the first that exists +//! - usr +//! - ../Resources +//! - assets +//! - Assets +//! - $cwd/assets +//! +//! ``` +//! assets::root() -> +//! mac -> ../Resources/ +//! ios -> ../Resources/ +//! android -> assets/ +//! server -> assets/ +//! liveview -> assets/ +//! web -> /assets/ +//! root().join(bundled) +//! ``` +//! +//! Every dioxus app can have an optional server executable which will influence the final bundle. +//! This is built in parallel with the app executable during the `build` phase and the progres/status +//! of the build is aggregated. +//! +//! The server will *always* be dropped into the `web` folder since it is considered "web" in nature, +//! and will likely need to be combined with the public dir to be useful. +//! +//! We do our best to assemble read-to-go bundles here, such that the "bundle" step for each platform +//! can just use the build dir +//! +//! When we write the AppBundle to a folder, it'll contain each bundle for each platform under the app's name: +//! ``` +//! dog-app/ +//! build/ +//! web/ +//! server.exe +//! assets/ +//! some-secret-asset.txt (a server-side asset) +//! public/ +//! index.html +//! assets/ +//! logo.png +//! desktop/ +//! App.app +//! App.appimage +//! App.exe +//! server/ +//! server +//! assets/ +//! some-secret-asset.txt (a server-side asset) +//! ios/ +//! App.app +//! App.ipa +//! android/ +//! App.apk +//! bundle/ +//! build.json +//! Desktop.app +//! Mobile_x64.ipa +//! Mobile_arm64.ipa +//! Mobile_rosetta.ipa +//! web.appimage +//! web/ +//! server.exe +//! assets/ +//! some-secret-asset.txt +//! public/ +//! index.html +//! assets/ +//! logo.png +//! style.css +//! ``` +//! +//! When deploying, the build.json file will provide all the metadata that dx-deploy will use to +//! push the app to stores, set up infra, manage versions, etc. +//! +//! The format of each build will follow the name plus some metadata such that when distributing you +//! can easily trim off the metadata. +//! +//! The idea here is that we can run any of the programs in the same way that they're deployed. +//! +//! ## Bundle structure links +//! - apple: ://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle> +//! - appimage: ://docs.appimage.org/packaging-guide/manual.html#ref-manual> +//! +//! ## Extra links +//! - xbuild: + +use crate::{ + AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, Platform, Result, RustcArgs, + TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR, +}; use anyhow::Context; +use dioxus_cli_config::format_base_path_meta_element; use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV}; -use dioxus_cli_opt::AssetManifest; -use serde::Deserialize; +use dioxus_cli_opt::{process_file_to, AssetManifest}; +use itertools::Itertools; +use krates::{cm::TargetKind, NodeId}; +use manganis::{AssetOptions, JsAssetOptions}; +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; +use serde::{Deserialize, Serialize}; use std::{ + collections::HashSet, + io::Write, path::{Path, PathBuf}, process::Stdio, - time::Instant, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::{SystemTime, UNIX_EPOCH}, }; +use target_lexicon::{OperatingSystem, Triple}; +use tempfile::{NamedTempFile, TempDir}; use tokio::{io::AsyncBufReadExt, process::Command}; - -#[derive(Clone, Debug)] +use toml_edit::Item; +use uuid::Uuid; + +use super::HotpatchModuleCache; + +/// This struct is used to plan the build process. +/// +/// The point here is to be able to take in the user's config from the CLI without modifying the +/// arguments in place. Creating a buildplan "resolves" their config into a build plan that can be +/// introspected. For example, the users might not specify a "Triple" in the CLI but the triple will +/// be guaranteed to be resolved here. +/// +/// Creating a buildplan also lets us introspect build requests and modularize our build process. +/// This will, however, lead to duplicate fields between the CLI and the build engine. This is fine +/// since we have the freedom to evolve the schema internally without breaking the API. +/// +/// All updates from the build will be sent on a global "BuildProgress" channel. +#[derive(Clone)] pub(crate) struct BuildRequest { - /// The configuration for the crate we are building - pub(crate) krate: DioxusCrate, - - /// The arguments for the build - pub(crate) build: BuildArgs, + pub(crate) workspace: Arc, + pub(crate) config: DioxusConfig, + pub(crate) crate_package: NodeId, + pub(crate) crate_target: krates::cm::Target, + pub(crate) profile: String, + pub(crate) release: bool, + pub(crate) platform: Platform, + pub(crate) enabled_platforms: Vec, + pub(crate) triple: Triple, + pub(crate) _device: bool, + pub(crate) package: String, + pub(crate) features: Vec, + pub(crate) extra_cargo_args: Vec, + pub(crate) extra_rustc_args: Vec, + pub(crate) no_default_features: bool, + pub(crate) custom_target_dir: Option, + pub(crate) skip_assets: bool, + pub(crate) wasm_split: bool, + pub(crate) debug_symbols: bool, + pub(crate) inject_loading_scripts: bool, + pub(crate) custom_linker: Option, + pub(crate) session_cache_dir: Arc, + pub(crate) link_args_file: Arc, + pub(crate) link_err_file: Arc, + pub(crate) rustc_wrapper_args_file: Arc, +} - /// Status channel to send our progress updates to - pub(crate) progress: ProgressTx, +/// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin +/// modes are used together to achieve binary patching and linking. +/// +/// Guide: +/// ---------- +/// - Base: A normal build generated using `cargo rustc`, intended for production use cases +/// +/// - Fat: A "fat" build with -Wl,-all_load and no_dead_strip, keeping *every* symbol in the binary. +/// Intended for development for larger up-front builds with faster link times and the ability +/// to binary patch the final binary. On WASM, this also forces wasm-bindgen to generate all +/// JS-WASM bindings, saving us the need to re-wasmbindgen the final binary. +/// +/// - Thin: A "thin" build that dynamically links against the dependencies produced by the "fat" build. +/// This is generated by calling rustc *directly* and might be more fragile to construct, but +/// generates *much* faster than a regular base or fat build. +#[derive(Clone, Debug, PartialEq)] +pub enum BuildMode { + /// A normal build generated using `cargo rustc` + Base, + + /// A "Fat" build generated with cargo rustc and dx as a custom linker without -Wl,-dead-strip + Fat, + + /// A "thin" build generated with `rustc` directly and dx as a custom linker + Thin { + rustc_args: RustcArgs, + changed_files: Vec, + aslr_reference: u64, + cache: Arc, + }, +} - /// The target directory for the build - pub(crate) custom_target_dir: Option, +/// The end result of a build. +/// +/// Contains the final asset manifest, the executable, and metadata about the build. +/// Note that the `exe` might be stale and/or overwritten by the time you read it! +/// +/// The patch cache is only populated on fat builds and then used for thin builds (see `BuildMode::Thin`). +#[derive(Clone, Debug)] +pub struct BuildArtifacts { + pub(crate) platform: Platform, + pub(crate) exe: PathBuf, + pub(crate) direct_rustc: RustcArgs, + pub(crate) time_start: SystemTime, + pub(crate) time_end: SystemTime, + pub(crate) assets: AssetManifest, + pub(crate) mode: BuildMode, + pub(crate) patch_cache: Option>, } -impl BuildRequest { - pub fn new(krate: DioxusCrate, build: BuildArgs, progress: ProgressTx) -> Self { - Self { - build, - krate, - progress, - custom_target_dir: None, - } - } +pub(crate) static PROFILE_WASM: &str = "wasm-dev"; +pub(crate) static PROFILE_ANDROID: &str = "android-dev"; +pub(crate) static PROFILE_SERVER: &str = "server-dev"; - /// Run the build command with a pretty loader, returning the executable output location +impl BuildRequest { + /// Create a new build request. /// - /// This will also run the fullstack build. Note that fullstack is handled separately within this - /// code flow rather than outside of it. - pub(crate) async fn build_all(self) -> Result { - tracing::debug!( - "Running build command... {}", - if self.build.force_sequential { - "(sequentially)" - } else { - "" - } - ); + /// This method consolidates various inputs into a single source of truth. It combines: + /// - Command-line arguments provided by the user. + /// - The crate's `Cargo.toml`. + /// - The `dioxus.toml` configuration file. + /// - User-specific CLI settings. + /// - The workspace metadata. + /// - Host-specific details (e.g., Android tools, installed frameworks). + /// - The intended target platform. + /// + /// Fields may be duplicated from the inputs to allow for autodetection and resolution. + /// + /// Autodetection is performed for unspecified fields where possible. + /// + /// Note: Build requests are typically created only when the CLI is invoked or when significant + /// changes are detected in the `Cargo.toml` (e.g., features added or removed). + pub(crate) async fn new(args: &TargetArgs, workspace: Arc) -> Result { + let crate_package = workspace.find_main_package(args.package.clone())?; + + let config = workspace + .load_dioxus_config(crate_package)? + .unwrap_or_default(); + + let target_kind = match args.example.is_some() { + true => TargetKind::Example, + false => TargetKind::Bin, + }; + + let main_package = &workspace.krates[crate_package]; + + let target_name = args + .example + .clone() + .or(args.bin.clone()) + .or_else(|| { + if let Some(default_run) = &main_package.default_run { + return Some(default_run.to_string()); + } + + let bin_count = main_package + .targets + .iter() + .filter(|x| x.kind.contains(&target_kind)) + .count(); + + if bin_count != 1 { + return None; + } + + main_package.targets.iter().find_map(|x| { + if x.kind.contains(&target_kind) { + Some(x.name.clone()) + } else { + None + } + }) + }) + .unwrap_or(workspace.krates[crate_package].name.clone()); + + let crate_target = main_package + .targets + .iter() + .find(|target| { + target_name == target.name.as_str() && target.kind.contains(&target_kind) + }) + .with_context(|| { + let target_of_kind = |kind|-> String { + let filtered_packages = main_package + .targets + .iter() + .filter_map(|target| { + target.kind.contains(kind).then_some(target.name.as_str()) + }).collect::>(); + filtered_packages.join(", ")}; + if let Some(example) = &args.example { + let examples = target_of_kind(&TargetKind::Example); + format!("Failed to find example {example}. \nAvailable examples are:\n{}", examples) + } else if let Some(bin) = &args.bin { + let binaries = target_of_kind(&TargetKind::Bin); + format!("Failed to find binary {bin}. \nAvailable binaries are:\n{}", binaries) + } else { + format!("Failed to find target {target_name}. \nIt looks like you are trying to build dioxus in a library crate. \ + You either need to run dx from inside a binary crate or build a specific example with the `--example` flag. \ + Available examples are:\n{}", target_of_kind(&TargetKind::Example)) + } + })? + .clone(); + + // The crate might enable multiple platforms or no platforms at + // We collect all the platforms it enables first and then select based on the --platform arg + let enabled_platforms = + Self::enabled_cargo_toml_platforms(main_package, args.no_default_features); + + let mut features = args.features.clone(); + let mut no_default_features = args.no_default_features; + + let platform: Platform = match args.platform { + Some(platform) => match enabled_platforms.len() { + 0 => platform, + + // The user passed --platform XYZ but already has `default = ["ABC"]` in their Cargo.toml or dioxus = { features = ["abc"] } + // We want to strip out the default platform and use the one they passed, setting no-default-features + _ => { + features.extend(Self::platformless_features(main_package)); + no_default_features = true; + platform + } + }, + None => match enabled_platforms.len() { + 0 => return Err(anyhow::anyhow!("No platform specified and no platform marked as default in Cargo.toml. Try specifying a platform with `--platform`").into()), + 1 => enabled_platforms[0], + _ => { + return Err(anyhow::anyhow!( + "Multiple platforms enabled in Cargo.toml. Please specify a platform with `--platform` or set a default platform in Cargo.toml" + ) + .into()) + } + }, + }; - let (app, server) = match self.build.force_sequential { - true => self.build_sequential().await?, - false => self.build_concurrent().await?, + // Add any features required to turn on the client + features.push(Self::feature_for_platform(main_package, platform)); + + // Set the profile of the build if it's not already set + // This is mostly used for isolation of builds (preventing thrashing) but also useful to have multiple performance profiles + // We might want to move some of these profiles into dioxus.toml and make them "virtual". + let profile = match args.profile.clone() { + Some(profile) => profile, + None if args.release => "release".to_string(), + None => match platform { + Platform::Android => PROFILE_ANDROID.to_string(), + Platform::Web => PROFILE_WASM.to_string(), + Platform::Server => PROFILE_SERVER.to_string(), + _ => "dev".to_string(), + }, }; - AppBundle::new(self, app, server).await - } + // Determining release mode is based on the profile, actually, so we need to check that + let release = workspace.is_release_profile(&profile); + + // Determine the --package we'll pass to cargo. + // todo: I think this might be wrong - we don't want to use main_package necessarily... + let package = args + .package + .clone() + .unwrap_or_else(|| main_package.name.clone()); + + // We usually use the simulator unless --device is passed *or* a device is detected by probing. + // For now, though, since we don't have probing, it just defaults to false + // Tools like xcrun/adb can detect devices + let device = args.device.unwrap_or(false); + + // We want a real triple to build with, so we'll autodetect it if it's not provided + // The triple ends up being a source of truth for us later hence all this work to figure it out + let triple = match args.target.clone() { + Some(target) => target, + None => match platform { + // Generally just use the host's triple for native executables unless specified otherwise + Platform::MacOS + | Platform::Windows + | Platform::Linux + | Platform::Server + | Platform::Liveview => target_lexicon::HOST, + + // We currently assume unknown-unknown for web, but we might want to eventually + // support emscripten + Platform::Web => "wasm32-unknown-unknown".parse().unwrap(), + + // For iOS we should prefer the actual architecture for the simulator, but in lieu of actually + // figuring that out, we'll assume aarch64 on m-series and x86_64 otherwise + Platform::Ios => { + // use the host's architecture and sim if --device is passed + use target_lexicon::{Architecture, HOST}; + match HOST.architecture { + Architecture::Aarch64(_) if device => "aarch64-apple-ios".parse().unwrap(), + Architecture::Aarch64(_) => "aarch64-apple-ios-sim".parse().unwrap(), + _ if device => "x86_64-apple-ios".parse().unwrap(), + _ => "x86_64-apple-ios-sim".parse().unwrap(), + } + } - /// Run the build command with a pretty loader, returning the executable output location - async fn build_concurrent(&self) -> Result<(BuildArtifacts, Option)> { - let (app, server) = - futures_util::future::try_join(self.build_app(), self.build_server()).await?; + // Same idea with android but we figure out the connected device using adb + Platform::Android => { + workspace + .android_tools()? + .autodetect_android_device_triple() + .await + } + }, + }; - Ok((app, server)) - } + let custom_linker = if platform == Platform::Android { + Some(workspace.android_tools()?.android_cc(&triple)) + } else { + None + }; - async fn build_sequential(&self) -> Result<(BuildArtifacts, Option)> { - let app = self.build_app().await?; - let server = self.build_server().await?; - Ok((app, server)) - } + // Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!) + let link_args_file = Arc::new( + NamedTempFile::with_suffix(".txt") + .context("Failed to create temporary file for linker args")?, + ); + let link_err_file = Arc::new( + NamedTempFile::with_suffix(".txt") + .context("Failed to create temporary file for linker args")?, + ); + let rustc_wrapper_args_file = Arc::new( + NamedTempFile::with_suffix(".json") + .context("Failed to create temporary file for rustc wrapper args")?, + ); + let session_cache_dir = Arc::new( + TempDir::new().context("Failed to create temporary directory for session cache")?, + ); - pub(crate) async fn build_app(&self) -> Result { - tracing::debug!("Building app..."); + let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default()) + .context("Failed to parse rustc args")?; - let start = Instant::now(); - self.prepare_build_dir()?; - let exe = self.build_cargo().await?; - let assets = self.collect_assets(&exe).await?; + let extra_cargo_args = shell_words::split(&args.cargo_args.clone().unwrap_or_default()) + .context("Failed to parse cargo args")?; - Ok(BuildArtifacts { - exe, - assets, - time_taken: start.elapsed(), + tracing::debug!( + r#"Log Files: +link_args_file: {}, +link_err_file: {}, +rustc_wrapper_args_file: {}, +session_cache_dir: {}"#, + link_args_file.path().display(), + link_err_file.path().display(), + rustc_wrapper_args_file.path().display(), + session_cache_dir.path().display(), + ); + + Ok(Self { + platform, + features, + no_default_features, + crate_package, + crate_target, + profile, + triple, + _device: device, + workspace, + config, + enabled_platforms, + custom_target_dir: None, + custom_linker, + link_args_file, + link_err_file, + session_cache_dir, + rustc_wrapper_args_file, + extra_rustc_args, + extra_cargo_args, + release, + package, + skip_assets: args.skip_assets, + wasm_split: args.wasm_split, + debug_symbols: args.debug_symbols, + inject_loading_scripts: args.inject_loading_scripts, }) } - pub(crate) async fn build_server(&self) -> Result> { - tracing::debug!("Building server..."); + pub(crate) async fn build(&self, ctx: &BuildContext) -> Result { + // If we forget to do this, then we won't get the linker args since rust skips the full build + // We need to make sure to not react to this though, so the filemap must cache it + _ = self.bust_fingerprint(ctx); + + // Run the cargo build to produce our artifacts + let mut artifacts = self.cargo_build(ctx).await?; + + // Write the build artifacts to the bundle on the disk + match &ctx.mode { + BuildMode::Thin { + aslr_reference, + cache, + .. + } => { + self.write_patch(ctx, *aslr_reference, &mut artifacts, cache) + .await?; + } + + BuildMode::Base | BuildMode::Fat => { + ctx.status_start_bundle(); + + self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets) + .await + .context("Failed to write main executable")?; + self.write_assets(ctx, &artifacts.assets) + .await + .context("Failed to write assets")?; + self.write_metadata().await?; + self.optimize(ctx).await?; + self.assemble(ctx) + .await + .context("Failed to assemble app bundle")?; + + tracing::debug!("Bundle created at {}", self.root_dir().display()); + } + } - if !self.build.fullstack() { - return Ok(None); + // Populate the patch cache if we're in fat mode + if matches!(ctx.mode, BuildMode::Fat) { + artifacts.patch_cache = Some(Arc::new(self.create_patch_cache(&artifacts.exe).await?)); } - let mut cloned = self.clone(); - cloned.build.platform = Some(Platform::Server); - Ok(Some(cloned.build_app().await?)) + Ok(artifacts) } - /// Run `cargo`, returning the location of the final executable + /// Run the cargo build by assembling the build command and executing it. /// - /// todo: add some stats here, like timing reports, crate-graph optimizations, etc - pub(crate) async fn build_cargo(&self) -> Result { - tracing::debug!("Executing cargo..."); + /// This method needs to be very careful with processing output since errors being swallowed will + /// be very confusing to the user. + async fn cargo_build(&self, ctx: &BuildContext) -> Result { + let time_start = SystemTime::now(); // Extract the unit count of the crate graph so build_cargo has more accurate data - let crate_count = self.get_unit_count_estimate().await; + // "Thin" builds only build the final exe, so we only need to build one crate + let crate_count = match ctx.mode { + BuildMode::Thin { .. } => 1, + _ => self.get_unit_count_estimate(ctx).await, + }; // Update the status to show that we're starting the build and how many crates we expect to build - self.status_starting_build(crate_count); - - let mut cmd = Command::new("cargo"); - - cmd.arg("rustc") - .current_dir(self.krate.crate_dir()) - .arg("--message-format") - .arg("json-diagnostic-rendered-ansi") - .args(self.build_arguments()) - .envs(self.env_vars()?); - - if let Some(target_dir) = self.custom_target_dir.as_ref() { - cmd.env("CARGO_TARGET_DIR", target_dir); - } - - // Android needs a special linker since the linker is actually tied to the android toolchain. - // For the sake of simplicity, we're going to pass the linker here using ourselves as the linker, - // but in reality we could simply use the android toolchain's linker as the path. - // - // We don't want to overwrite the user's .cargo/config.toml since that gets committed to git - // and we want everyone's install to be the same. - if self.build.platform() == Platform::Android { - let ndk = self - .krate - .android_ndk() - .context("Could not autodetect android linker")?; - let arch = self.build.target_args.arch(); - let linker = arch.android_linker(&ndk); - - let link_action = LinkAction::LinkAndroid { - linker, - extra_flags: vec![], - } - .to_json(); - - cmd.env(LinkAction::ENV_VAR_NAME, link_action); - } + ctx.status_starting_build(crate_count); - tracing::trace!(dx_src = ?TraceSrc::Build, "Rust cargo args: {:#?}", cmd); + let mut cmd = self.build_command(ctx)?; + tracing::debug!(dx_src = ?TraceSrc::Build, "Executing cargo for {} using {}", self.platform, self.triple); let mut child = cmd .stdout(Stdio::piped()) @@ -159,7 +779,7 @@ impl BuildRequest { let stdout = tokio::io::BufReader::new(child.stdout.take().unwrap()); let stderr = tokio::io::BufReader::new(child.stderr.take().unwrap()); - let mut output_location = None; + let mut output_location: Option = None; let mut stdout = stdout.lines(); let mut stderr = stderr.lines(); let mut units_compiled = 0; @@ -180,33 +800,60 @@ impl BuildRequest { match message { Message::BuildScriptExecuted(_) => units_compiled += 1, + Message::CompilerMessage(msg) => ctx.status_build_diagnostic(msg), Message::TextLine(line) => { + // Handle the case where we're getting lines directly from rustc. + // These are in a different format than the normal cargo output, though I imagine + // this parsing code is quite fragile/sensitive to changes in cargo, cargo_metadata, rustc, etc. + #[derive(Deserialize)] + struct RustcArtifact { + artifact: PathBuf, + emit: String, + } + + // These outputs look something like: + // + // { "artifact":"target/debug/deps/libdioxus_core-4f2a0b3c1e5f8b7c.rlib", "emit":"link" } + // + // There are other outputs like depinfo that we might be interested in in the future. + if let Ok(artifact) = serde_json::from_str::(&line) { + if artifact.emit == "link" { + output_location = Some(artifact.artifact); + } + } + // For whatever reason, if there's an error while building, we still receive the TextLine // instead of an "error" message. However, the following messages *also* tend to // be the error message, and don't start with "error:". So we'll check if we've already // emitted an error message and if so, we'll emit all following messages as errors too. + // + // todo: This can lead to some really ugly output though, so we might want to look + // into a more reliable way to detect errors propagating out of the compiler. If + // we always wrapped rustc, then we could store this data somewhere in a much more + // reliable format. if line.trim_start().starts_with("error:") { emitting_error = true; } - if emitting_error { - self.status_build_error(line); - } else { - self.status_build_message(line) + // Note that previous text lines might have set emitting_error to true + match emitting_error { + true => ctx.status_build_error(line), + false => ctx.status_build_message(line), } } - Message::CompilerMessage(msg) => self.status_build_diagnostic(msg), Message::CompilerArtifact(artifact) => { units_compiled += 1; match artifact.executable { Some(executable) => output_location = Some(executable.into()), - None => self.status_build_progress( + None => ctx.status_build_progress( units_compiled, crate_count, artifact.target.name, ), } } + // todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong + // since that is a really bad user experience. Message::BuildFinished(finished) => { if !finished.success { return Err(anyhow::anyhow!( @@ -219,604 +866,1425 @@ impl BuildRequest { } } - if output_location.is_none() { - tracing::error!("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information."); + let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?; + + // Accumulate the rustc args from the wrapper, if they exist and can be parsed. + let mut direct_rustc = RustcArgs::default(); + if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file.path()) { + if let Ok(res) = serde_json::from_str(&res) { + direct_rustc = res; + } + } + + // If there's any warnings from the linker, we should print them out + if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file.path()) { + if !linker_warnings.is_empty() { + tracing::warn!("Linker warnings: {}", linker_warnings); + } } - let out_location = output_location.context("Build did not return an executable")?; + // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds + if matches!(ctx.mode, BuildMode::Fat) { + self.run_fat_link(ctx, &exe).await?; + } - tracing::debug!( - "Build completed successfully - output location: {:?}", - out_location - ); + let assets = self.collect_assets(&exe, ctx)?; + let time_end = SystemTime::now(); + let mode = ctx.mode.clone(); + let platform = self.platform; + tracing::debug!("Build completed successfully - output location: {:?}", exe); - Ok(out_location) + Ok(BuildArtifacts { + time_end, + platform, + exe, + direct_rustc, + time_start, + assets, + mode, + patch_cache: None, + }) } /// Traverse the target directory and collect all assets from the incremental cache /// /// This uses "known paths" that have stayed relatively stable during cargo's lifetime. /// One day this system might break and we might need to go back to using the linker approach. - pub(crate) async fn collect_assets(&self, exe: &Path) -> Result { - tracing::debug!("Collecting assets ..."); - - if self.build.skip_assets { - return Ok(AssetManifest::default()); - } - - // Experimental feature for testing - if the env var is set, we'll use the deeplinker - if std::env::var("DEEPLINK").is_ok() { - tracing::debug!("Using deeplinker instead of incremental cache"); - return self.deep_linker_asset_extract().await; - } + fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result { + tracing::debug!("Collecting assets from exe at {} ...", exe.display()); // walk every file in the incremental cache dir, reading and inserting items into the manifest. let mut manifest = AssetManifest::default(); // And then add from the exe directly, just in case it's LTO compiled and has no incremental cache - _ = manifest.add_from_object_path(exe); + if !self.skip_assets { + ctx.status_extracting_assets(); + _ = manifest.add_from_object_path(exe); + } Ok(manifest) } - /// Create a list of arguments for cargo builds - pub(crate) fn build_arguments(&self) -> Vec { - let mut cargo_args = Vec::new(); - - // Set the target, profile and features that vary between the app and server builds - if self.build.platform() == Platform::Server { - cargo_args.push("--profile".to_string()); - match self.build.release { - true => cargo_args.push("release".to_string()), - false => cargo_args.push(self.build.server_profile.to_string()), - }; - - // If the user provided a server target, use it, otherwise use the default host target. - if let Some(target) = self.build.target_args.server_target.as_deref() { - cargo_args.push("--target".to_string()); - cargo_args.push(target.to_string()); - } - } else { - // Add required profile flags. --release overrides any custom profiles. - let custom_profile = &self.build.profile.as_ref(); - if custom_profile.is_some() || self.build.release { - cargo_args.push("--profile".to_string()); - match self.build.release { - true => cargo_args.push("release".to_string()), - false => { - cargo_args.push( - custom_profile - .expect("custom_profile should have been checked by is_some") - .to_string(), - ); - } - }; - } - - // todo: use the right arch based on the current arch - let custom_target = match self.build.platform() { - Platform::Web => Some("wasm32-unknown-unknown"), - Platform::Ios => match self.build.target_args.device { - Some(true) => Some("aarch64-apple-ios"), - _ => Some("aarch64-apple-ios-sim"), - }, - Platform::Android => Some(self.build.target_args.arch().android_target_triplet()), - Platform::Server => None, - // we're assuming we're building for the native platform for now... if you're cross-compiling - // the targets here might be different - Platform::MacOS => None, - Platform::Windows => None, - Platform::Linux => None, - Platform::Liveview => None, - }; - - if let Some(target) = custom_target.or(self.build.target_args.target.as_deref()) { - cargo_args.push("--target".to_string()); - cargo_args.push(target.to_string()); + /// Take the output of rustc and make it into the main exe of the bundle + /// + /// For wasm, we'll want to run `wasm-bindgen` to make it a wasm binary along with some other optimizations + /// Other platforms we might do some stripping or other optimizations + /// Move the executable to the workdir + async fn write_executable( + &self, + ctx: &BuildContext, + exe: &Path, + assets: &mut AssetManifest, + ) -> Result<()> { + match self.platform { + // Run wasm-bindgen on the wasm binary and set its output to be in the bundle folder + // Also run wasm-opt on the wasm binary, and sets the index.html since that's also the "executable". + // + // The wasm stuff will be in a folder called "wasm" in the workdir. + // + // Final output format: + // ``` + // dx/ + // app/ + // web/ + // bundle/ + // build/ + // server.exe + // public/ + // index.html + // wasm/ + // app.wasm + // glue.js + // snippets/ + // ... + // assets/ + // logo.png + // ``` + Platform::Web => { + self.bundle_web(ctx, exe, assets).await?; } - } - // We always run in verbose since the CLI itself is the one doing the presentation - cargo_args.push("--verbose".to_string()); + // this will require some extra oomf to get the multi architecture builds... + // for now, we just copy the exe into the current arch (which, sorry, is hardcoded for my m1) + // we'll want to do multi-arch builds in the future, so there won't be *one* exe dir to worry about + // eventually `exe_dir` and `main_exe` will need to take in an arch and return the right exe path + // + // todo(jon): maybe just symlink this rather than copy it? + // we might want to eventually use the objcopy logic to handle this + // + // https://github.com/rust-mobile/xbuild/blob/master/xbuild/template/lib.rs + // https://github.com/rust-mobile/xbuild/blob/master/apk/src/lib.rs#L19 + // + // These are all super simple, just copy the exe into the folder + // eventually, perhaps, maybe strip + encrypt the exe? + Platform::Android + | Platform::MacOS + | Platform::Windows + | Platform::Linux + | Platform::Ios + | Platform::Liveview + | Platform::Server => { + // We wipe away the dir completely, which is not great behavior :/ + // Don't wipe server since web will need this folder too. + if self.platform != Platform::Server { + _ = std::fs::remove_dir_all(self.exe_dir()); + } - if self.build.target_args.no_default_features { - cargo_args.push("--no-default-features".to_string()); + std::fs::create_dir_all(self.exe_dir())?; + std::fs::copy(exe, self.main_exe())?; + } } - let features = self.target_features(); + Ok(()) + } - if !features.is_empty() { - cargo_args.push("--features".to_string()); - cargo_args.push(features.join(" ")); + /// Copy the assets out of the manifest and into the target location + /// + /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory + async fn write_assets(&self, ctx: &BuildContext, assets: &AssetManifest) -> Result<()> { + // Server doesn't need assets - web will provide them + if self.platform == Platform::Server { + return Ok(()); } - if let Some(ref package) = self.build.target_args.package { - cargo_args.push(String::from("-p")); - cargo_args.push(package.clone()); + let asset_dir = self.asset_dir(); + + // First, clear the asset dir of any files that don't exist in the new manifest + _ = std::fs::create_dir_all(&asset_dir); + + // Create a set of all the paths that new files will be bundled to + let mut keep_bundled_output_paths: HashSet<_> = assets + .assets + .values() + .map(|a| asset_dir.join(a.bundled_path())) + .collect(); + + // The CLI creates a .version file in the asset dir to keep track of what version of the optimizer + // the asset was processed. If that version doesn't match the CLI version, we need to re-optimize + // all assets. + let version_file = self.asset_optimizer_version_file(); + let clear_cache = std::fs::read_to_string(&version_file) + .ok() + .filter(|s| s == crate::VERSION.as_str()) + .is_none(); + if clear_cache { + keep_bundled_output_paths.clear(); } - cargo_args.append(&mut self.build.cargo_args.clone()); - - match self.krate.executable_type() { - krates::cm::TargetKind::Bin => cargo_args.push("--bin".to_string()), - krates::cm::TargetKind::Lib => cargo_args.push("--lib".to_string()), - krates::cm::TargetKind::Example => cargo_args.push("--example".to_string()), - _ => {} - }; - - cargo_args.push(self.krate.executable_name().to_string()); + tracing::trace!( + "Keeping bundled output paths: {:#?}", + keep_bundled_output_paths + ); - // the bundle splitter needs relocation data - // we'll trim these out if we don't need them during the bundling process - // todo(jon): for wasm binary patching we might want to leave these on all the time. - if self.build.platform() == Platform::Web && self.build.experimental_wasm_split { - cargo_args.push("--".to_string()); - cargo_args.push("-Clink-args=--emit-relocs".to_string()); + // use walkdir::WalkDir; + // for item in WalkDir::new(&asset_dir).into_iter().flatten() { + // // If this asset is in the manifest, we don't need to remove it + // let canonicalized = dunce::canonicalize(item.path())?; + // if !keep_bundled_output_paths.contains(canonicalized.as_path()) { + // // Remove empty dirs, remove files not in the manifest + // if item.file_type().is_dir() && item.path().read_dir()?.next().is_none() { + // std::fs::remove_dir(item.path())?; + // } else { + // std::fs::remove_file(item.path())?; + // } + // } + // } + + // todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't + let mut assets_to_transfer = vec![]; + + // Queue the bundled assets + for (asset, bundled) in &assets.assets { + let from = asset.clone(); + let to = asset_dir.join(bundled.bundled_path()); + + // prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir + let from_ = from + .strip_prefix(self.workspace_dir()) + .unwrap_or(from.as_path()); + let to_ = from + .strip_prefix(self.workspace_dir()) + .unwrap_or(to.as_path()); + + tracing::debug!("Copying asset {from_:?} to {to_:?}"); + assets_to_transfer.push((from, to, *bundled.options())); } - tracing::debug!(dx_src = ?TraceSrc::Build, "cargo args: {:?}", cargo_args); + let asset_count = assets_to_transfer.len(); + let started_processing = AtomicUsize::new(0); + let copied = AtomicUsize::new(0); + + // Parallel Copy over the assets and keep track of progress with an atomic counter + let progress = ctx.tx.clone(); + let ws_dir = self.workspace_dir(); + + // Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task + tokio::task::spawn_blocking(move || { + assets_to_transfer + .par_iter() + .try_for_each(|(from, to, options)| { + let processing = started_processing.fetch_add(1, Ordering::SeqCst); + let from_ = from.strip_prefix(&ws_dir).unwrap_or(from); + tracing::trace!( + "Starting asset copy {processing}/{asset_count} from {from_:?}" + ); + + let res = process_file_to(options, from, to); + if let Err(err) = res.as_ref() { + tracing::error!("Failed to copy asset {from:?}: {err}"); + } + + let finished = copied.fetch_add(1, Ordering::SeqCst); + BuildContext::status_copied_asset( + &progress, + finished, + asset_count, + from.to_path_buf(), + ); - cargo_args - } + res.map(|_| ()) + }) + }) + .await + .map_err(|e| anyhow::anyhow!("A task failed while trying to copy assets: {e}"))??; - #[allow(dead_code)] - pub(crate) fn android_rust_flags(&self) -> String { - let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default(); + // // Remove the wasm bindgen output directory if it exists + // _ = std::fs::remove_dir_all(self.wasm_bindgen_out_dir()); - // todo(jon): maybe we can make the symbol aliasing logic here instead of using llvm-objcopy - if self.build.platform() == Platform::Android { - let cur_exe = std::env::current_exe().unwrap(); - rust_flags.push_str(format!(" -Clinker={}", cur_exe.display()).as_str()); - rust_flags.push_str(" -Clink-arg=-landroid"); - rust_flags.push_str(" -Clink-arg=-llog"); - rust_flags.push_str(" -Clink-arg=-lOpenSLES"); - rust_flags.push_str(" -Clink-arg=-Wl,--export-dynamic"); - } + // Write the version file so we know what version of the optimizer we used + std::fs::write(self.asset_optimizer_version_file(), crate::VERSION.as_str())?; - rust_flags + Ok(()) } - /// Create the list of features we need to pass to cargo to build the app by merging together - /// either the client or server features depending on if we're building a server or not. - pub(crate) fn target_features(&self) -> Vec { - let mut features = self.build.target_args.features.clone(); - - if self.build.platform() == Platform::Server { - features.extend(self.build.target_args.server_features.clone()); - } else { - features.extend(self.build.target_args.client_features.clone()); - } + /// Run our custom linker setup to generate a patch file in the right location + /// + /// This should be the only case where the cargo output is a "dummy" file and requires us to + /// manually do any linking. + /// + /// We also run some post processing steps here, like extracting out any new assets. + async fn write_patch( + &self, + ctx: &BuildContext, + aslr_reference: u64, + artifacts: &mut BuildArtifacts, + cache: &Arc, + ) -> Result<()> { + ctx.status_hotpatching(); - features - } + tracing::debug!( + "Original builds for patch: {}", + self.link_args_file.path().display() + ); + let raw_args = std::fs::read_to_string(self.link_args_file.path()) + .context("Failed to read link args from file")?; + let args = raw_args.lines().collect::>(); - pub(crate) fn all_target_features(&self) -> Vec { - let mut features = self.target_features(); + // Extract out the incremental object files. + // + // This is sadly somewhat of a hack, but it might be a moderately reliable hack. + // + // When rustc links your project, it passes the args as how a linker would expect, but with + // a somewhat reliable ordering. These are all internal details to cargo/rustc, so we can't + // rely on them *too* much, but the *are* fundamental to how rust compiles your projects, and + // linker interfaces probably won't change drastically for another 40 years. + // + // We need to tear apart this command and only pass the args that are relevant to our thin link. + // Mainly, we don't want any rlibs to be linked. Occasionally some libraries like objc_exception + // export a folder with their artifacts - unsure if we actually need to include them. Generally + // you can err on the side that most *libraries* don't need to be linked here since dlopen + // satisfies those symbols anyways when the binary is loaded. + // + // Many args are passed twice, too, which can be confusing, but generally don't have any real + // effect. Note that on macos/ios, there's a special macho header that needs to be set, otherwise + // dyld will complain.a + // + // Also, some flags in darwin land might become deprecated, need to be super conservative: + // - https://developer.apple.com/forums/thread/773907 + // + // The format of this command roughly follows: + // ``` + // clang + // /dioxus/target/debug/subsecond-cli + // /var/folders/zs/gvrfkj8x33d39cvw2p06yc700000gn/T/rustcAqQ4p2/symbols.o + // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.05stnb4bovskp7a00wyyf7l9s.rcgu.o + // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.08rgcutgrtj2mxoogjg3ufs0g.rcgu.o + // /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa.0941bd8fa2bydcv9hfmgzzne9.rcgu.o + // /dioxus/target/subsecond-dev/deps/libbincode-c215feeb7886f81b.rlib + // /dioxus/target/subsecond-dev/deps/libanyhow-e69ac15c094daba6.rlib + // /dioxus/target/subsecond-dev/deps/libratatui-c3364579b86a1dfc.rlib + // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libstd-019f0f6ae6e6562b.rlib + // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libpanic_unwind-7387d38173a2eb37.rlib + // /.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/lib/libobject-2b03cf6ece171d21.rlib + // -framework AppKit + // -lc + // -framework Foundation + // -framework Carbon + // -lSystem + // -framework CoreFoundation + // -lobjc + // -liconv + // -lm + // -arch arm64 + // -mmacosx-version-min=11.0.0 + // -L /dioxus/target/subsecond-dev/build/objc_exception-dc226cad0480ea65/out + // -o /dioxus/target/subsecond-dev/deps/subsecond_harness-acfb69cb29ffb8fa + // -nodefaultlibs + // -Wl,-all_load + // ``` + let mut object_files = args + .iter() + .filter(|arg| arg.ends_with(".rcgu.o")) + .sorted() + .map(PathBuf::from) + .collect::>(); + + // On non-wasm platforms, we generate a special shim object file which converts symbols from + // fat binary into direct addresses from the running process. + // + // Our wasm approach is quite specific to wasm. We don't need to resolve any missing symbols + // there since wasm is relocatable, but there is considerable pre and post processing work to + // satisfy undefined symbols that we do by munging the binary directly. + // + // todo: can we adjust our wasm approach to also use a similar system? + // todo: don't require the aslr reference and just patch the got when loading. + // + // Requiring the ASLR offset here is necessary but unfortunately might be flakey in practice. + // Android apps can take a long time to open, and a hot patch might've been issued in the interim, + // making this hotpatch a failure. + if self.platform != Platform::Web { + let stub_bytes = crate::build::create_undefined_symbol_stub( + cache, + &object_files, + &self.triple, + aslr_reference, + ) + .expect("failed to resolve patch symbols"); - if !self.build.target_args.no_default_features { - features.extend( - self.krate - .package() - .features - .get("default") - .cloned() - .unwrap_or_default(), - ); + // Currently we're dropping stub.o in the exe dir, but should probably just move to a tempfile? + let patch_file = self.main_exe().with_file_name("stub.o"); + std::fs::write(&patch_file, stub_bytes)?; + object_files.push(patch_file); } - features.dedup(); + // And now we can run the linker with our new args + let linker = self.select_linker()?; - features - } + let out_exe = self.patch_exe(artifacts.time_start); + let out_arg = match self.triple.operating_system { + OperatingSystem::Windows => vec![format!("/OUT:{}", out_exe.display())], + _ => vec!["-o".to_string(), out_exe.display().to_string()], + }; - /// Try to get the unit graph for the crate. This is a nightly only feature which may not be available with the current version of rustc the user has installed. - pub(crate) async fn get_unit_count(&self) -> crate::Result { - #[derive(Debug, Deserialize)] - struct UnitGraph { - units: Vec, - } + tracing::trace!("Linking with {:?} using args: {:#?}", linker, object_files); - let output = tokio::process::Command::new("cargo") - .arg("+nightly") - .arg("build") - .arg("--unit-graph") - .arg("-Z") - .arg("unstable-options") - .args(self.build_arguments()) - .envs(self.env_vars()?) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + // Run the linker directly! + // + // We dump its output directly into the patch exe location which is different than how rustc + // does it since it uses llvm-objcopy into the `target/debug/` folder. + let res = Command::new(linker) + .args(object_files.iter()) + .args(self.thin_link_args(&args)?) + .args(out_arg) .output() .await?; - if !output.status.success() { - return Err(anyhow::anyhow!("Failed to get unit count").into()); + if !res.stderr.is_empty() { + let errs = String::from_utf8_lossy(&res.stderr); + if !self.patch_exe(artifacts.time_start).exists() || !res.status.success() { + tracing::error!("Failed to generate patch: {}", errs.trim()); + } else { + tracing::trace!("Linker output during thin linking: {}", errs.trim()); + } } - let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?; - let graph: UnitGraph = - serde_json::from_str(&output_text).context("Failed to get unit count")?; + // For some really weird reason that I think is because of dlopen caching, future loads of the + // jump library will fail if we don't remove the original fat file. I think this could be + // because of library versioning and namespaces, but really unsure. + // + // The errors if you forget to do this are *extremely* cryptic - missing symbols that never existed. + // + // Fortunately, this binary exists in two places - the deps dir and the target out dir. We + // can just remove the one in the deps dir and the problem goes away. + if let Some(idx) = args.iter().position(|arg| *arg == "-o") { + _ = std::fs::remove_file(PathBuf::from(args[idx + 1])); + } - Ok(graph.units.len()) - } + // Now extract the assets from the fat binary + artifacts + .assets + .add_from_object_path(&self.patch_exe(artifacts.time_start))?; - /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this will return an estimate of the number of units in the crate based on cargo metadata. - /// TODO: always use https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#unit-graph once it is stable - pub(crate) async fn get_unit_count_estimate(&self) -> usize { - // Try to get it from nightly - self.get_unit_count().await.unwrap_or_else(|_| { - // Otherwise, use cargo metadata - (self - .krate - .krates - .krates_filtered(krates::DepKind::Dev) - .iter() - .map(|k| k.targets.len()) - .sum::() as f64 - / 3.5) as usize - }) + // Clean up the temps manually + // todo: we might want to keep them around for debugging purposes + for file in object_files { + _ = std::fs::remove_file(file); + } + + Ok(()) } - /// We used to require traversing incremental artifacts for assets that were included but not - /// directly exposed to the final binary. Now, however, we force APIs to carry items created - /// from asset calls into top-level items such that they *do* get included in the final binary. + /// Take the original args passed to the "fat" build and then create the "thin" variant. /// - /// There's a chance that's not actually true, so this function is kept around in case we do - /// need to revert to "deep extraction". - #[allow(unused)] - async fn deep_linker_asset_extract(&self) -> Result { - // Create a temp file to put the output of the args - // We need to do this since rustc won't actually print the link args to stdout, so we need to - // give `dx` a file to dump its env::args into - let tmp_file = tempfile::NamedTempFile::new()?; - - // Run `cargo rustc` again, but this time with a custom linker (dx) and an env var to force - // `dx` to act as a linker - // - // This will force `dx` to look through the incremental cache and find the assets from the previous build - Command::new("cargo") - .arg("rustc") - .args(self.build_arguments()) - .envs(self.env_vars()?) - .arg("--offline") /* don't use the network, should already be resolved */ - .arg("--") - .arg(format!( - "-Clinker={}", - std::env::current_exe() - .unwrap() - .canonicalize() - .unwrap() - .display() - )) - .env( - LinkAction::ENV_VAR_NAME, - LinkAction::BuildAssetManifest { - destination: tmp_file.path().to_path_buf().clone(), - } - .to_json(), - ) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; + /// This is basically just stripping away the rlibs and other libraries that will be satisfied + /// by our stub step. + fn thin_link_args(&self, original_args: &[&str]) -> Result> { + use target_lexicon::OperatingSystem; - // The linker wrote the manifest to the temp file, let's load it! - let manifest = AssetManifest::load_from_file(tmp_file.path())?; + let triple = self.triple.clone(); + let mut out_args = vec![]; - if let Ok(path) = std::env::var("DEEPLINK").map(|s| s.parse::().unwrap()) { - _ = tmp_file.persist(path); - } + match triple.operating_system { + // wasm32-unknown-unknown -> use wasm-ld (gnu-lld) + // + // We need to import a few things - namely the memory and ifunc table. + // + // We can safely export everything, I believe, though that led to issues with the "fat" + // binaries that also might lead to issues here too. wasm-bindgen chokes on some symbols + // and the resulting JS has issues. + // + // We turn on both --pie and --experimental-pic but I think we only need --pie. + // + // We don't use *any* of the original linker args since they do lots of custom exports + // and other things that we don't need. + OperatingSystem::Unknown if self.platform == Platform::Web => { + out_args.extend([ + "--fatal-warnings".to_string(), + "--verbose".to_string(), + "--import-memory".to_string(), + "--import-table".to_string(), + "--growable-table".to_string(), + "--export".to_string(), + "main".to_string(), + "--allow-undefined".to_string(), + "--no-demangle".to_string(), + "--no-entry".to_string(), + "--pie".to_string(), + "--experimental-pic".to_string(), + ]); + } - Ok(manifest) - } + // This uses "cc" and these args need to be ld compatible + // + // Most importantly, we want to pass `-dylib` to both CC and the linker to indicate that + // we want to generate the shared library instead of an executable. + OperatingSystem::IOS(_) | OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) => { + out_args.extend(["-Wl,-dylib".to_string()]); + + // Preserve the original args. We only preserve: + // -framework + // -arch + // -lxyz + // There might be more, but some flags might break our setup. + for (idx, arg) in original_args.iter().enumerate() { + if *arg == "-framework" || *arg == "-arch" || *arg == "-L" { + out_args.push(arg.to_string()); + out_args.push(original_args[idx + 1].to_string()); + } - fn env_vars(&self) -> Result> { - let mut env_vars = vec![]; + if arg.starts_with("-l") || arg.starts_with("-m") { + out_args.push(arg.to_string()); + } + } + } - if self.build.platform() == Platform::Android { - let ndk = self - .krate - .android_ndk() - .context("Could not autodetect android linker")?; - let arch = self.build.target_args.arch(); - let linker = arch.android_linker(&ndk); - let min_sdk_version = arch.android_min_sdk_version(); - let ar_path = arch.android_ar_path(&ndk); - let target_cc = arch.target_cc(&ndk); - let target_cxx = arch.target_cxx(&ndk); - let java_home = arch.java_home(); + // android/linux need to be compatible with lld + // + // android currently drags along its own libraries and other zany flags + OperatingSystem::Linux => { + out_args.extend([ + "-shared".to_string(), + "-Wl,--eh-frame-hdr".to_string(), + "-Wl,-z,noexecstack".to_string(), + "-Wl,-z,relro,-z,now".to_string(), + "-nodefaultlibs".to_string(), + "-Wl,-Bdynamic".to_string(), + ]); + + // Preserve the original args. We only preserve: + // -L + // -arch + // -lxyz + // There might be more, but some flags might break our setup. + for (idx, arg) in original_args.iter().enumerate() { + if *arg == "-L" { + out_args.push(arg.to_string()); + out_args.push(original_args[idx + 1].to_string()); + } - tracing::debug!( - r#"Using android: - min_sdk_version: {min_sdk_version} - linker: {linker:?} - ar_path: {ar_path:?} - target_cc: {target_cc:?} - target_cxx: {target_cxx:?} - java_home: {java_home:?} - "# - ); + if arg.starts_with("-l") + || arg.starts_with("-m") + || arg.starts_with("-Wl,--target=") + { + out_args.push(arg.to_string()); + } + } + } - env_vars.push(("ANDROID_NATIVE_API_LEVEL", min_sdk_version.to_string())); - env_vars.push(("TARGET_AR", ar_path.display().to_string())); - env_vars.push(("TARGET_CC", target_cc.display().to_string())); - env_vars.push(("TARGET_CXX", target_cxx.display().to_string())); - env_vars.push(("ANDROID_NDK_ROOT", ndk.display().to_string())); - - // attempt to set java_home to the android studio java home if it exists. - // https://stackoverflow.com/questions/71381050/java-home-is-set-to-an-invalid-directory-android-studio-flutter - // attempt to set java_home to the android studio java home if it exists and java_home was not already set - if let Some(java_home) = java_home { - tracing::debug!("Setting JAVA_HOME to {java_home:?}"); - env_vars.push(("JAVA_HOME", java_home.display().to_string())); - } - - env_vars.push(("WRY_ANDROID_PACKAGE", "dev.dioxus.main".to_string())); - env_vars.push(("WRY_ANDROID_LIBRARY", "dioxusmain".to_string())); - env_vars.push(( - "WRY_ANDROID_KOTLIN_FILES_OUT_DIR", - self.wry_android_kotlin_files_out_dir() - .display() - .to_string(), - )); + OperatingSystem::Windows => { + out_args.extend([ + "shlwapi.lib".to_string(), + "kernel32.lib".to_string(), + "advapi32.lib".to_string(), + "ntdll.lib".to_string(), + "userenv.lib".to_string(), + "ws2_32.lib".to_string(), + "dbghelp.lib".to_string(), + "/defaultlib:msvcrt".to_string(), + "/DLL".to_string(), + "/DEBUG".to_string(), + "/PDBALTPATH:%_PDB%".to_string(), + "/EXPORT:main".to_string(), + "/HIGHENTROPYVA:NO".to_string(), + // "/SUBSYSTEM:WINDOWS".to_string(), + ]); + } - env_vars.push(("RUSTFLAGS", self.android_rust_flags())) + _ => return Err(anyhow::anyhow!("Unsupported platform for thin linking").into()), + } - // todo(jon): the guide for openssl recommends extending the path to include the tools dir - // in practice I couldn't get this to work, but this might eventually become useful. - // - // https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration - // - // They recommend a configuration like this: - // - // // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570 - // PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH - // ./Configure android-arm64 -D__ANDROID_API__=29 - // make - // - // let tools_dir = arch.android_tools_dir(&ndk); - // let extended_path = format!( - // "{}:{}", - // tools_dir.display(), - // std::env::var("PATH").unwrap_or_default() - // ); - // env_vars.push(("PATH", extended_path)); + let extract_value = |arg: &str| -> Option { + original_args + .iter() + .position(|a| *a == arg) + .map(|i| original_args[i + 1].to_string()) }; - // If this is a release build, bake the base path and title - // into the binary with env vars - if self.build.release { - if let Some(base_path) = &self.krate.config.web.app.base_path { - env_vars.push((ASSET_ROOT_ENV, base_path.clone())); - } - env_vars.push((APP_TITLE_ENV, self.krate.config.web.app.title.clone())); + if let Some(vale) = extract_value("-target") { + out_args.push("-target".to_string()); + out_args.push(vale); } - Ok(env_vars) + if let Some(vale) = extract_value("-isysroot") { + out_args.push("-isysroot".to_string()); + out_args.push(vale); + } + + Ok(out_args) } - /// We only really currently care about: + /// Patches are stored in the same directory as the main executable, but with a name based on the + /// time the patch started compiling. /// - /// - app dir (.app, .exe, .apk, etc) - /// - assets dir - /// - exe dir (.exe, .app, .apk, etc) - /// - extra scaffolding + /// - lib{name}-patch-{time}.(so/dll/dylib) (next to the main exe) /// - /// It's not guaranteed that they're different from any other folder - fn prepare_build_dir(&self) -> Result<()> { - use once_cell::sync::OnceCell; - use std::fs::{create_dir_all, remove_dir_all}; + /// Note that weirdly enough, the name of dylibs can actually matter. In some environments, libs + /// can override each other with symbol interposition. + /// + /// Also, on Android - and some Linux, we *need* to start the lib name with `lib` for the dynamic + /// loader to consider it a shared library. + /// + /// todo: the time format might actually be problematic if two platforms share the same build folder. + pub(crate) fn patch_exe(&self, time_start: SystemTime) -> PathBuf { + let path = self.main_exe().with_file_name(format!( + "lib{}-patch-{}", + self.executable_name(), + time_start + .duration_since(UNIX_EPOCH) + .map(|f| f.as_millis()) + .unwrap_or(0), + )); + + let extension = match self.triple.operating_system { + OperatingSystem::Darwin(_) => "dylib", + OperatingSystem::MacOSX(_) => "dylib", + OperatingSystem::IOS(_) => "dylib", + OperatingSystem::Windows => "dll", + OperatingSystem::Linux => "so", + OperatingSystem::Wasi => "wasm", + OperatingSystem::Unknown if self.platform == Platform::Web => "wasm", + _ => "", + }; - static INITIALIZED: OnceCell> = OnceCell::new(); + path.with_extension(extension) + } - let success = INITIALIZED.get_or_init(|| { - _ = remove_dir_all(self.exe_dir()); + /// When we link together the fat binary, we need to make sure every `.o` file in *every* rlib + /// is taken into account. This is the same work that the rust compiler does when assembling + /// staticlibs. + /// + /// + /// + /// Since we're going to be passing these to the linker, we need to make sure and not provide any + /// weird files (like the rmeta) file that rustc generates. + /// + /// We discovered the need for this after running into issues with wasm-ld not being able to + /// handle the rmeta file. + /// + /// + /// + /// Also, crates might not drag in all their dependent code. The monorphizer won't lift trait-based generics: + /// + /// + /// + /// When Rust normally handles this, it uses the +whole-archive directive which adjusts how the rlib + /// is written to disk. + /// + /// Since creating this object file can be a lot of work, we cache it in the target dir by hashing + /// the names of the rlibs in the command and storing it in the target dir. That way, when we run + /// this command again, we can just used the cached object file. + /// + /// In theory, we only need to do this for every crate accessible by the current crate, but that's + /// hard acquire without knowing the exported symbols from each crate. + /// + /// todo: I think we can traverse our immediate dependencies and inspect their symbols, unless they `pub use` a crate + /// todo: we should try and make this faster with memmapping + pub(crate) async fn run_fat_link(&self, ctx: &BuildContext, exe: &Path) -> Result<()> { + ctx.status_starting_link(); + + let raw_args = std::fs::read_to_string(self.link_args_file.path()) + .context("Failed to read link args from file")?; + let args = raw_args.lines().collect::>(); + + // Filter out the rlib files from the arguments + let rlibs = args + .iter() + .filter(|arg| arg.ends_with(".rlib")) + .map(PathBuf::from) + .collect::>(); + + // Acquire a hash from the rlib names + let hash_id = Uuid::new_v5( + &Uuid::NAMESPACE_OID, + rlibs + .iter() + .map(|p| p.file_name().unwrap().to_string_lossy()) + .collect::() + .as_bytes(), + ); - create_dir_all(self.root_dir())?; - create_dir_all(self.exe_dir())?; - create_dir_all(self.asset_dir())?; + // Check if we already have a cached object file + let out_ar_path = exe.with_file_name(format!("libfatdependencies-{hash_id}.a")); - tracing::debug!("Initialized Root dir: {:?}", self.root_dir()); - tracing::debug!("Initialized Exe dir: {:?}", self.exe_dir()); - tracing::debug!("Initialized Asset dir: {:?}", self.asset_dir()); + let mut compiler_rlibs = vec![]; - // we could download the templates from somewhere (github?) but after having banged my head against - // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates - // by writing them here. - if let Platform::Android = self.build.platform() { - self.build_android_app_dir()?; + // Create it by dumping all the rlibs into it + // This will include the std rlibs too, which can severely bloat the size of the archive + // + // The nature of this process involves making extremely fat archives, so we should try and + // speed up the future linking process by caching the archive. + if !crate::devcfg::should_cache_dep_lib(&out_ar_path) { + let mut bytes = vec![]; + let mut out_ar = ar::Builder::new(&mut bytes); + for rlib in &rlibs { + // Skip compiler rlibs since they're missing bitcode + // + // https://github.com/rust-lang/rust/issues/94232#issuecomment-1048342201 + // + // if the rlib is not in the target directory, we skip it. + if !rlib.starts_with(self.workspace_dir()) { + compiler_rlibs.push(rlib.clone()); + tracing::trace!("Skipping rlib: {:?}", rlib); + continue; + } + + tracing::trace!("Adding rlib to staticlib: {:?}", rlib); + + let rlib_contents = std::fs::read(rlib)?; + let mut reader = ar::Archive::new(std::io::Cursor::new(rlib_contents)); + while let Some(Ok(object_file)) = reader.next_entry() { + let name = std::str::from_utf8(object_file.header().identifier()).unwrap(); + if name.ends_with(".rmeta") { + continue; + } + + if object_file.header().size() == 0 { + continue; + } + + // rlibs might contain dlls/sos/lib files which we don't want to include + if name.ends_with(".dll") || name.ends_with(".so") || name.ends_with(".lib") { + compiler_rlibs.push(rlib.to_owned()); + continue; + } + + if !(name.ends_with(".o") || name.ends_with(".obj")) { + tracing::debug!("Unknown object file in rlib: {:?}", name); + } + + out_ar + .append(&object_file.header().clone(), object_file) + .context("Failed to add object file to archive")?; + } } - Ok(()) - }); + let bytes = out_ar.into_inner().context("Failed to finalize archive")?; + std::fs::write(&out_ar_path, bytes).context("Failed to write archive")?; + tracing::debug!("Wrote fat archive to {:?}", out_ar_path); + } - if let Err(e) = success.as_ref() { - return Err(format!("Failed to initialize build directory: {e}").into()); + compiler_rlibs.dedup(); + + // We're going to replace the first rlib in the args with our fat archive + // And then remove the rest of the rlibs + // + // We also need to insert the -force_load flag to force the linker to load the archive + let mut args = args.iter().map(|s| s.to_string()).collect::>(); + if let Some(first_rlib) = args.iter().position(|arg| arg.ends_with(".rlib")) { + match self.triple.operating_system { + OperatingSystem::Unknown if self.platform == Platform::Web => { + // We need to use the --whole-archive flag for wasm + args[first_rlib] = "--whole-archive".to_string(); + args.insert(first_rlib + 1, out_ar_path.display().to_string()); + args.insert(first_rlib + 2, "--no-whole-archive".to_string()); + args.retain(|arg| !arg.ends_with(".rlib")); + + // add back the compiler rlibs + for rlib in compiler_rlibs.iter().rev() { + args.insert(first_rlib + 3, rlib.display().to_string()); + } + } + + // Subtle difference - on linux and android we go through clang and thus pass `-Wl,` prefix + OperatingSystem::Linux => { + args[first_rlib] = "-Wl,--whole-archive".to_string(); + args.insert(first_rlib + 1, out_ar_path.display().to_string()); + args.insert(first_rlib + 2, "-Wl,--no-whole-archive".to_string()); + args.retain(|arg| !arg.ends_with(".rlib")); + + // add back the compiler rlibs + for rlib in compiler_rlibs.iter().rev() { + args.insert(first_rlib + 3, rlib.display().to_string()); + } + } + + OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => { + args[first_rlib] = "-Wl,-force_load".to_string(); + args.insert(first_rlib + 1, out_ar_path.display().to_string()); + args.retain(|arg| !arg.ends_with(".rlib")); + + // add back the compiler rlibs + for rlib in compiler_rlibs.iter().rev() { + args.insert(first_rlib + 2, rlib.display().to_string()); + } + + args.insert(first_rlib + 3, "-Wl,-all_load".to_string()); + } + + OperatingSystem::Windows => { + args[first_rlib] = format!("/WHOLEARCHIVE:{}", out_ar_path.display()); + args.retain(|arg| !arg.ends_with(".rlib")); + + // add back the compiler rlibs + for rlib in compiler_rlibs.iter().rev() { + args.insert(first_rlib + 1, rlib.display().to_string()); + } + + args.insert(first_rlib, "/HIGHENTROPYVA:NO".to_string()); + } + + _ => {} + }; } - Ok(()) - } + // We also need to remove the `-o` flag since we want the linker output to end up in the + // rust exe location, not in the deps dir as it normally would. + if let Some(idx) = args + .iter() + .position(|arg| *arg == "-o" || *arg == "--output") + { + args.remove(idx + 1); + args.remove(idx); + } - /// The directory in which we'll put the main exe - /// - /// Mac, Android, Web are a little weird - /// - mac wants to be in Contents/MacOS - /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture) - /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work) - /// - /// I think all others are just in the root folder - /// - /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do. - pub fn exe_dir(&self) -> PathBuf { - match self.build.platform() { - Platform::MacOS => self.root_dir().join("Contents").join("MacOS"), - Platform::Web => self.root_dir().join("wasm"), + // same but windows support + if let Some(idx) = args.iter().position(|arg| arg.starts_with("/OUT")) { + args.remove(idx); + } - // Android has a whole build structure to it - Platform::Android => self - .root_dir() - .join("app") - .join("src") - .join("main") - .join("jniLibs") - .join(self.build.target_args.arch().android_jnilib()), + // We want to go through wasm-ld directly, so we need to remove the -flavor flag + if self.platform == Platform::Web { + let flavor_idx = args.iter().position(|arg| *arg == "-flavor").unwrap(); + args.remove(flavor_idx + 1); + args.remove(flavor_idx); + } - // these are all the same, I think? - Platform::Windows - | Platform::Linux - | Platform::Ios - | Platform::Server - | Platform::Liveview => self.root_dir(), + // And now we can run the linker with our new args + let linker = self.select_linker()?; + + tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args); + + // Run the linker directly! + let out_arg = match self.triple.operating_system { + OperatingSystem::Windows => vec![format!("/OUT:{}", exe.display())], + _ => vec!["-o".to_string(), exe.display().to_string()], + }; + + let res = Command::new(linker) + .args(args.iter().skip(1)) + .args(out_arg) + .output() + .await?; + + if !res.stderr.is_empty() { + let errs = String::from_utf8_lossy(&res.stderr); + if !res.status.success() { + tracing::error!("Failed to generate fat binary: {}", errs.trim()); + } else { + tracing::trace!("Warnings during fat linking: {}", errs.trim()); + } } - } - /// Get the path to the wasm bindgen temporary output folder - pub fn wasm_bindgen_out_dir(&self) -> PathBuf { - self.root_dir().join("wasm") - } + if !res.stdout.is_empty() { + let out = String::from_utf8_lossy(&res.stdout); + tracing::trace!("Output from fat linking: {}", out.trim()); + } - /// Get the path to the wasm bindgen javascript output file - pub fn wasm_bindgen_js_output_file(&self) -> PathBuf { - self.wasm_bindgen_out_dir() - .join(self.krate.executable_name()) - .with_extension("js") - } + // Clean up the temps manually + for f in args.iter().filter(|arg| arg.ends_with(".rcgu.o")) { + _ = std::fs::remove_file(f); + } - /// Get the path to the wasm bindgen wasm output file - pub fn wasm_bindgen_wasm_output_file(&self) -> PathBuf { - self.wasm_bindgen_out_dir() - .join(format!("{}_bg", self.krate.executable_name())) - .with_extension("wasm") + Ok(()) } - /// returns the path to root build folder. This will be our working directory for the build. + /// Select the linker to use for this platform. /// - /// we only add an extension to the folders where it sorta matters that it's named with the extension. - /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc. + /// We prefer to use the rust-lld linker when we can since it's usually there. + /// On macos, we use the system linker since macho files can be a bit finicky. /// - /// for our simulator-based platforms, this is less important since they need to be zipped up anyways - /// to run in the simulator. + /// This means we basically ignore the linker flavor that the user configured, which could + /// cause issues with a custom linker setup. In theory, rust translates most flags to the right + /// linker format. + fn select_linker(&self) -> Result { + let cc = match self.triple.operating_system { + OperatingSystem::Unknown if self.platform == Platform::Web => self.workspace.wasm_ld(), + + // The android clang linker is *special* and has some android-specific flags that we need + // + // Note that this is *clang*, not `lld`. + OperatingSystem::Linux if self.platform == Platform::Android => { + self.workspace.android_tools()?.android_cc(&self.triple) + } + + // On macOS, we use the system linker since it's usually there. + // We could also use `lld` here, but it might not be installed by default. + // + // Note that this is *clang*, not `lld`. + OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => self.workspace.cc(), + + // On windows, instead of trying to find the system linker, we just go with the lld.link + // that rustup provides. It's faster and more stable then reyling on link.exe in path. + OperatingSystem::Windows => self.workspace.lld_link(), + + // The rest of the platforms use `cc` as the linker which should be available in your path, + // provided you have build-tools setup. On mac/linux this is the default, but on Windows + // it requires msvc or gnu downloaded, which is a requirement to use rust anyways. + // + // The default linker might actually be slow though, so we could consider using lld or rust-lld + // since those are shipping by default on linux as of 1.86. Window's linker is the really slow one. + // + // https://blog.rust-lang.org/2024/05/17/enabling-rust-lld-on-linux.html + // + // Note that "cc" is *not* a linker. It's a compiler! The arguments we pass need to be in + // the form of `-Wl,` for them to make it to the linker. This matches how rust does it + // which is confusing. + _ => self.workspace.cc(), + }; + + Ok(cc) + } + + /// Assemble the `cargo rustc` / `rustc` command /// - /// For windows/linux, it's also not important since we're just running the exe directly out of the folder + /// When building fat/base binaries, we use `cargo rustc`. + /// When building thin binaries, we use `rustc` directly. /// - /// The idea of this folder is that we can run our top-level build command against it and we'll get - /// a final build output somewhere. Some platforms have basically no build command, and can simply - /// be ran by executing the exe directly. - pub(crate) fn root_dir(&self) -> PathBuf { - let platform_dir = self.platform_dir(); + /// When processing the output of this command, you need to make sure to handle both cases which + /// both have different formats (but with json output for both). + fn build_command(&self, ctx: &BuildContext) -> Result { + match &ctx.mode { + // We're assembling rustc directly, so we need to be *very* careful. Cargo sets rustc's + // env up very particularly, and we want to match it 1:1 but with some changes. + // + // To do this, we reset the env completely, and then pass every env var that the original + // rustc process had 1:1. + // + // We need to unset a few things, like the RUSTC wrappers and then our special env var + // indicating that dx itself is the compiler. If we forget to do this, then the compiler + // ends up doing some recursive nonsense and dx is trying to link instead of compiling. + // + // todo: maybe rustc needs to be found on the FS instead of using the one in the path? + BuildMode::Thin { rustc_args, .. } => { + let mut cmd = Command::new("rustc"); + cmd.current_dir(self.workspace_dir()); + cmd.env_clear(); + cmd.args(rustc_args.args[1..].iter()); + cmd.envs(rustc_args.envs.iter().cloned()); + cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); + cmd.env_remove("RUSTC_WRAPPER"); + cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR); + cmd.envs(self.cargo_build_env_vars(ctx)?); + cmd.arg(format!("-Clinker={}", Workspace::path_to_dx()?.display())); + + if self.platform == Platform::Web { + cmd.arg("-Crelocation-model=pic"); + } - match self.build.platform() { - Platform::Web => platform_dir.join("public"), - Platform::Server => platform_dir.clone(), // ends up *next* to the public folder + tracing::trace!("Direct rustc command: {:#?}", rustc_args.args); - // These might not actually need to be called `.app` but it does let us run these with `open` - Platform::MacOS => platform_dir.join(format!("{}.app", self.krate.bundled_app_name())), - Platform::Ios => platform_dir.join(format!("{}.app", self.krate.bundled_app_name())), + Ok(cmd) + } - // in theory, these all could end up directly in the root dir - Platform::Android => platform_dir.join("app"), // .apk (after bundling) - Platform::Linux => platform_dir.join("app"), // .appimage (after bundling) - Platform::Windows => platform_dir.join("app"), // .exe (after bundling) - Platform::Liveview => platform_dir.join("app"), // .exe (after bundling) + // For Base and Fat builds, we use a regular cargo setup, but we might need to intercept + // rustc itself in case we're hot-patching and need a reliable rustc environment to + // continuously recompile the top-level crate with. + // + // In the future, when we support hot-patching *all* workspace crates, we will need to + // make use of the RUSTC_WORKSPACE_WRAPPER environment variable instead of RUSTC_WRAPPER + // and then keep track of env and args on a per-crate basis. + // + // We've also had a number of issues with incorrect canonicalization when passing paths + // through envs on windows, hence the frequent use of dunce::canonicalize. + _ => { + let mut cmd = Command::new("cargo"); + + cmd.arg("rustc") + .current_dir(self.crate_dir()) + .arg("--message-format") + .arg("json-diagnostic-rendered-ansi") + .args(self.cargo_build_arguments(ctx)) + .envs(self.cargo_build_env_vars(ctx)?); + + tracing::trace!("Cargo command: {:#?}", cmd); + + if ctx.mode == BuildMode::Fat { + cmd.env( + DX_RUSTC_WRAPPER_ENV_VAR, + dunce::canonicalize(self.rustc_wrapper_args_file.path()) + .unwrap() + .display() + .to_string(), + ); + cmd.env( + "RUSTC_WRAPPER", + Workspace::path_to_dx()?.display().to_string(), + ); + } + + Ok(cmd) + } } } - pub(crate) fn platform_dir(&self) -> PathBuf { - self.krate - .build_dir(self.build.platform(), self.build.release) - } + /// Create a list of arguments for cargo builds + /// + /// We always use `cargo rustc` *or* `rustc` directly. This means we can pass extra flags like + /// `-C` arguments directly to the compiler. + #[allow(clippy::vec_init_then_push)] + fn cargo_build_arguments(&self, ctx: &BuildContext) -> Vec { + let mut cargo_args = Vec::with_capacity(4); - pub fn asset_dir(&self) -> PathBuf { - match self.build.platform() { - Platform::MacOS => self - .root_dir() - .join("Contents") - .join("Resources") - .join("assets"), + // Add required profile flags. --release overrides any custom profiles. + cargo_args.push("--profile".to_string()); + cargo_args.push(self.profile.to_string()); - Platform::Android => self - .root_dir() - .join("app") - .join("src") - .join("main") - .join("assets"), + // Pass the appropriate target to cargo. We *always* specify a target which is somewhat helpful for preventing thrashing + cargo_args.push("--target".to_string()); + cargo_args.push(self.triple.to_string()); - // everyone else is soooo normal, just app/assets :) - Platform::Web - | Platform::Ios - | Platform::Windows - | Platform::Linux - | Platform::Server - | Platform::Liveview => self.root_dir().join("assets"), + // We always run in verbose since the CLI itself is the one doing the presentation + cargo_args.push("--verbose".to_string()); + + if self.no_default_features { + cargo_args.push("--no-default-features".to_string()); } - } - /// Get the path to the asset optimizer version file - pub fn asset_optimizer_version_file(&self) -> PathBuf { - self.platform_dir().join(".cli-version") - } + if !self.features.is_empty() { + cargo_args.push("--features".to_string()); + cargo_args.push(self.features.join(" ")); + } - pub fn platform_exe_name(&self) -> String { - match self.build.platform() { - Platform::MacOS => self.krate.executable_name().to_string(), - Platform::Ios => self.krate.executable_name().to_string(), - Platform::Server => self.krate.executable_name().to_string(), - Platform::Liveview => self.krate.executable_name().to_string(), - Platform::Windows => format!("{}.exe", self.krate.executable_name()), + // We *always* set the package since that's discovered from cargo metadata + cargo_args.push(String::from("-p")); + cargo_args.push(self.package.clone()); - // from the apk spec, the root exe is a shared library - // we include the user's rust code as a shared library with a fixed namespacea - Platform::Android => "libdioxusmain.so".to_string(), + // Set the executable + match self.executable_type() { + TargetKind::Bin => cargo_args.push("--bin".to_string()), + TargetKind::Lib => cargo_args.push("--lib".to_string()), + TargetKind::Example => cargo_args.push("--example".to_string()), + _ => {} + }; + cargo_args.push(self.executable_name().to_string()); - Platform::Web => unimplemented!("there's no main exe on web"), // this will be wrong, I think, but not important? + // Merge in extra args. Order shouldn't really matter. + cargo_args.extend(self.extra_cargo_args.clone()); + cargo_args.push("--".to_string()); + cargo_args.extend(self.extra_rustc_args.clone()); - // todo: maybe this should be called AppRun? - Platform::Linux => self.krate.executable_name().to_string(), + // The bundle splitter needs relocation data to create a call-graph. + // This will automatically be erased by wasm-opt during the optimization step. + if self.platform == Platform::Web && self.wasm_split { + cargo_args.push("-Clink-args=--emit-relocs".to_string()); + } + + // dx *always* links android and thin builds + if self.custom_linker.is_some() + || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) + { + cargo_args.push(format!( + "-Clinker={}", + Workspace::path_to_dx().expect("can't find dx").display() + )); + } + + // Our fancy hot-patching engine needs a lot of customization to work properly. + // + // These args are mostly intended to be passed when *fat* linking but are generally fine to + // pass for both fat and thin linking. + // + // We need save-temps and no-dead-strip in both cases though. When we run `cargo rustc` with + // these args, they will be captured and re-ran for the fast compiles in the future, so whatever + // we set here will be set for all future hot patches too. + if matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) { + // rustc gives us some portable flags required: + // - link-dead-code: prevents rust from passing -dead_strip to the linker since that's the default. + // - save-temps=true: keeps the incremental object files around, which we need for manually linking. + cargo_args.extend_from_slice(&[ + "-Csave-temps=true".to_string(), + "-Clink-dead-code".to_string(), + ]); + + // We need to set some extra args that ensure all symbols make it into the final output + // and that the linker doesn't strip them out. + // + // This basically amounts of -all_load or --whole-archive, depending on the linker. + // We just assume an ld-like interface on macos and a gnu-ld interface elsewhere. + // + // macOS/iOS use ld64 but through the `cc` interface. + // cargo_args.push("-Clink-args=-Wl,-all_load".to_string()); + // + // Linux and Android fit under this umbrella, both with the same clang-like entrypoint + // and the gnu-ld interface. + // + // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string()); + // + // If windows -Wl,--whole-archive is required since it follows gnu-ld convention. + // There might be other flags on windows - we haven't tested windows thoroughly. + // + // cargo_args.push("-Clink-args=-Wl,--whole-archive".to_string()); + // https://learn.microsoft.com/en-us/cpp/build/reference/wholearchive-include-all-library-object-files?view=msvc-170 + // + // ------------------------------------------------------------ + // + // if web, -Wl,--whole-archive is required since it follows gnu-ld convention. + // + // We also use --no-gc-sections and --export-table and --export-memory to push + // said symbols into the export table. + // + // We use --emit-relocs to build up a solid call graph. + // + // rust uses its own wasm-ld linker which can be found here (it's just gcc-ld with a `-target wasm` flag): + // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld + // - ~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/gcc-ld/wasm-ld + // + // Note that we can't use --export-all, unfortunately, since some symbols are internal + // to wasm-bindgen and exporting them causes the JS generation to fail. + // + // We are basically replicating what emscripten does here with its dynamic linking + // approach where the MAIN_MODULE is very "fat" and exports the necessary arguments + // for the side modules to be linked in. This guide is really helpful: + // + // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md + // + // The trickiest one here is -Crelocation-model=pic, which forces data symbols + // into a GOT, making it possible to import them from the main module. + // + // I think we can make relocation-model=pic work for non-wasm platforms, enabling + // fully relocatable modules with no host coordination in lieu of sending out + // the aslr slide at runtime. + // + // The other tricky one is -Ctarget-cpu=mvp, which prevents rustc from generating externref + // entries. + // + // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals + // + // It's fine that these exist in the base module but not in the patch. + if self.platform == Platform::Web + || self.triple.operating_system == OperatingSystem::Wasi + { + // cargo_args.push("-Crelocation-model=pic".into()); + cargo_args.push("-Ctarget-cpu=mvp".into()); + cargo_args.push("-Clink-arg=--no-gc-sections".into()); + cargo_args.push("-Clink-arg=--growable-table".into()); + cargo_args.push("-Clink-arg=--export-table".into()); + cargo_args.push("-Clink-arg=--export-memory".into()); + cargo_args.push("-Clink-arg=--emit-relocs".into()); + cargo_args.push("-Clink-arg=--export=__stack_pointer".into()); + cargo_args.push("-Clink-arg=--export=__heap_base".into()); + cargo_args.push("-Clink-arg=--export=__data_end".into()); + } } + + cargo_args } - fn build_android_app_dir(&self) -> Result<()> { - use std::fs::{create_dir_all, write}; - let root = self.root_dir(); + fn cargo_build_env_vars(&self, ctx: &BuildContext) -> Result> { + let mut env_vars = vec![]; - // gradle - let wrapper = root.join("gradle").join("wrapper"); - create_dir_all(&wrapper)?; - tracing::debug!("Initialized Gradle wrapper: {:?}", wrapper); + // Make sure to set all the crazy android flags. Cross-compiling is hard, man. + if self.platform == Platform::Android { + env_vars.extend(self.android_env_vars()?); + }; - // app - let app = root.join("app"); - let app_main = app.join("src").join("main"); - let app_kotlin = app_main.join("kotlin"); - let app_jnilibs = app_main.join("jniLibs"); - let app_assets = app_main.join("assets"); - let app_kotlin_out = self.wry_android_kotlin_files_out_dir(); - create_dir_all(&app)?; - create_dir_all(&app_main)?; - create_dir_all(&app_kotlin)?; - create_dir_all(&app_jnilibs)?; - create_dir_all(&app_assets)?; - create_dir_all(&app_kotlin_out)?; - tracing::debug!("Initialized app: {:?}", app); - tracing::debug!("Initialized app/src: {:?}", app_main); - tracing::debug!("Initialized app/src/kotlin: {:?}", app_kotlin); - tracing::debug!("Initialized app/src/jniLibs: {:?}", app_jnilibs); - tracing::debug!("Initialized app/src/assets: {:?}", app_assets); - tracing::debug!("Initialized app/src/kotlin/main: {:?}", app_kotlin_out); - - // handlerbars - let hbs = handlebars::Handlebars::new(); - #[derive(serde::Serialize)] - struct HbsTypes { - application_id: String, - app_name: String, + // If we're either zero-linking or using a custom linker, make `dx` itself do the linking. + if self.custom_linker.is_some() + || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) + { + LinkAction { + triple: self.triple.clone(), + linker: self.custom_linker.clone(), + link_err_file: dunce::canonicalize(self.link_err_file.path())?, + link_args_file: dunce::canonicalize(self.link_args_file.path())?, + } + .write_env_vars(&mut env_vars)?; } - let hbs_data = HbsTypes { - application_id: self.krate.full_mobile_app_name(), - app_name: self.krate.bundled_app_name(), - }; - // Top-level gradle config + // Disable reference types on wasm when using hotpatching + // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals + if self.platform == Platform::Web + && matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) + { + env_vars.push(("RUSTFLAGS", { + let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default(); + rust_flags.push_str(" -Ctarget-cpu=mvp"); + rust_flags + })); + } + + if let Some(target_dir) = self.custom_target_dir.as_ref() { + env_vars.push(("CARGO_TARGET_DIR", target_dir.display().to_string())); + } + + // If this is a release build, bake the base path and title into the binary with env vars. + // todo: should we even be doing this? might be better being a build.rs or something else. + if self.release { + if let Some(base_path) = &self.config.web.app.base_path { + env_vars.push((ASSET_ROOT_ENV, base_path.clone())); + } + env_vars.push((APP_TITLE_ENV, self.config.web.app.title.clone())); + } + + Ok(env_vars) + } + + fn android_env_vars(&self) -> Result> { + let mut env_vars = vec![]; + + let tools = self.workspace.android_tools()?; + let linker = tools.android_cc(&self.triple); + let min_sdk_version = tools.min_sdk_version(); + let ar_path = tools.ar_path(); + let target_cc = tools.target_cc(); + let target_cxx = tools.target_cxx(); + let java_home = tools.java_home(); + let ndk = tools.ndk.clone(); + tracing::debug!( + r#"Using android: + min_sdk_version: {min_sdk_version} + linker: {linker:?} + ar_path: {ar_path:?} + target_cc: {target_cc:?} + target_cxx: {target_cxx:?} + java_home: {java_home:?} + "# + ); + env_vars.push(("ANDROID_NATIVE_API_LEVEL", min_sdk_version.to_string())); + env_vars.push(("TARGET_AR", ar_path.display().to_string())); + env_vars.push(("TARGET_CC", target_cc.display().to_string())); + env_vars.push(("TARGET_CXX", target_cxx.display().to_string())); + env_vars.push(("ANDROID_NDK_ROOT", ndk.display().to_string())); + + if let Some(java_home) = java_home { + tracing::debug!("Setting JAVA_HOME to {java_home:?}"); + env_vars.push(("JAVA_HOME", java_home.display().to_string())); + } + + // Set the wry env vars - this is where wry will dump its kotlin files. + // Their setup is really annyoing and requires us to hardcode `dx` to specific versions of tao/wry. + env_vars.push(("WRY_ANDROID_PACKAGE", "dev.dioxus.main".to_string())); + env_vars.push(("WRY_ANDROID_LIBRARY", "dioxusmain".to_string())); + env_vars.push(( + "WRY_ANDROID_KOTLIN_FILES_OUT_DIR", + self.wry_android_kotlin_files_out_dir() + .display() + .to_string(), + )); + + // Set the rust flags for android which get passed to *every* crate in the graph. + env_vars.push(("RUSTFLAGS", { + let mut rust_flags = std::env::var("RUSTFLAGS").unwrap_or_default(); + rust_flags.push_str(" -Clink-arg=-landroid"); + rust_flags.push_str(" -Clink-arg=-llog"); + rust_flags.push_str(" -Clink-arg=-lOpenSLES"); + rust_flags.push_str(" -Clink-arg=-Wl,--export-dynamic"); + rust_flags + })); + + // todo(jon): the guide for openssl recommends extending the path to include the tools dir + // in practice I couldn't get this to work, but this might eventually become useful. + // + // https://github.com/openssl/openssl/blob/master/NOTES-ANDROID.md#configuration + // + // They recommend a configuration like this: + // + // // export ANDROID_NDK_ROOT=/home/whoever/Android/android-sdk/ndk/20.0.5594570 + // PATH=$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin:$ANDROID_NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin:$PATH + // ./Configure android-arm64 -D__ANDROID_API__=29 + // make + // + // let tools_dir = arch.android_tools_dir(&ndk); + // let extended_path = format!( + // "{}:{}", + // tools_dir.display(), + // std::env::var("PATH").unwrap_or_default() + // ); + // env_vars.push(("PATH", extended_path)); + + Ok(env_vars) + } + + /// Get an estimate of the number of units in the crate. If nightly rustc is not available, this + /// will return an estimate of the number of units in the crate based on cargo metadata. + /// + /// TODO: always use once it is stable + async fn get_unit_count_estimate(&self, ctx: &BuildContext) -> usize { + // Try to get it from nightly + if let Ok(count) = self.get_unit_count(ctx).await { + return count; + } + + // Otherwise, use cargo metadata + let units = self + .workspace + .krates + .krates_filtered(krates::DepKind::Dev) + .iter() + .map(|k| k.targets.len()) + .sum::(); + + (units as f64 / 3.5) as usize + } + + /// Try to get the unit graph for the crate. This is a nightly only feature which may not be + /// available with the current version of rustc the user has installed. + /// + /// It also might not be super reliable - I think in practice it occasionally returns 2x the units. + async fn get_unit_count(&self, ctx: &BuildContext) -> crate::Result { + #[derive(Debug, Deserialize)] + struct UnitGraph { + units: Vec, + } + + let output = tokio::process::Command::new("cargo") + .arg("+nightly") + .arg("build") + .arg("--unit-graph") + .arg("-Z") + .arg("unstable-options") + .args(self.cargo_build_arguments(ctx)) + .envs(self.cargo_build_env_vars(ctx)?) + .output() + .await?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Failed to get unit count").into()); + } + + let output_text = String::from_utf8(output.stdout).context("Failed to get unit count")?; + let graph: UnitGraph = + serde_json::from_str(&output_text).context("Failed to get unit count")?; + + Ok(graph.units.len()) + } + + pub(crate) fn all_target_features(&self) -> Vec { + let mut features = self.features.clone(); + + if !self.no_default_features { + features.extend( + self.package() + .features + .get("default") + .cloned() + .unwrap_or_default(), + ); + } + + features.dedup(); + + features + } + + /// returns the path to root build folder. This will be our working directory for the build. + /// + /// we only add an extension to the folders where it sorta matters that it's named with the extension. + /// for example, on mac, the `.app` indicates we can `open` it and it pulls in icons, dylibs, etc. + /// + /// for our simulator-based platforms, this is less important since they need to be zipped up anyways + /// to run in the simulator. + /// + /// For windows/linux, it's also not important since we're just running the exe directly out of the folder + /// + /// The idea of this folder is that we can run our top-level build command against it and we'll get + /// a final build output somewhere. Some platforms have basically no build command, and can simply + /// be ran by executing the exe directly. + pub(crate) fn root_dir(&self) -> PathBuf { + let platform_dir = self.platform_dir(); + + match self.platform { + Platform::Web => platform_dir.join("public"), + Platform::Server => platform_dir.clone(), // ends up *next* to the public folder + + // These might not actually need to be called `.app` but it does let us run these with `open` + Platform::MacOS => platform_dir.join(format!("{}.app", self.bundled_app_name())), + Platform::Ios => platform_dir.join(format!("{}.app", self.bundled_app_name())), + + // in theory, these all could end up directly in the root dir + Platform::Android => platform_dir.join("app"), // .apk (after bundling) + Platform::Linux => platform_dir.join("app"), // .appimage (after bundling) + Platform::Windows => platform_dir.join("app"), // .exe (after bundling) + Platform::Liveview => platform_dir.join("app"), // .exe (after bundling) + } + } + + fn platform_dir(&self) -> PathBuf { + self.build_dir(self.platform, self.release) + } + + fn platform_exe_name(&self) -> String { + match self.platform { + Platform::MacOS => self.executable_name().to_string(), + Platform::Ios => self.executable_name().to_string(), + Platform::Server => self.executable_name().to_string(), + Platform::Liveview => self.executable_name().to_string(), + Platform::Windows => format!("{}.exe", self.executable_name()), + + // from the apk spec, the root exe is a shared library + // we include the user's rust code as a shared library with a fixed namespace + Platform::Android => "libdioxusmain.so".to_string(), + + // this will be wrong, I think, but not important? + Platform::Web => format!("{}_bg.wasm", self.executable_name()), + + // todo: maybe this should be called AppRun? + Platform::Linux => self.executable_name().to_string(), + } + } + + /// Assemble the android app dir. + /// + /// This is a bit of a mess since we need to create a lot of directories and files. Other approaches + /// would be to unpack some zip folder or something stored via `include_dir!()`. However, we do + /// need to customize the whole setup a bit, so it's just simpler (though messier) to do it this way. + fn build_android_app_dir(&self) -> Result<()> { + use std::fs::{create_dir_all, write}; + let root = self.root_dir(); + + // gradle + let wrapper = root.join("gradle").join("wrapper"); + create_dir_all(&wrapper)?; + + // app + let app = root.join("app"); + let app_main = app.join("src").join("main"); + let app_kotlin = app_main.join("kotlin"); + let app_jnilibs = app_main.join("jniLibs"); + let app_assets = app_main.join("assets"); + let app_kotlin_out = self.wry_android_kotlin_files_out_dir(); + create_dir_all(&app)?; + create_dir_all(&app_main)?; + create_dir_all(&app_kotlin)?; + create_dir_all(&app_jnilibs)?; + create_dir_all(&app_assets)?; + create_dir_all(&app_kotlin_out)?; + + tracing::debug!( + r#"Initialized android dirs: +- gradle: {wrapper:?} +- app/ {app:?} +- app/src: {app_main:?} +- app/src/kotlin: {app_kotlin:?} +- app/src/jniLibs: {app_jnilibs:?} +- app/src/assets: {app_assets:?} +- app/src/kotlin/main: {app_kotlin_out:?} +"# + ); + + // handlebars + #[derive(Serialize)] + struct AndroidHandlebarsObjects { + application_id: String, + app_name: String, + } + let hbs_data = AndroidHandlebarsObjects { + application_id: self.full_mobile_app_name(), + app_name: self.bundled_app_name(), + }; + let hbs = handlebars::Handlebars::new(); + + // Top-level gradle config write( root.join("build.gradle.kts"), include_bytes!("../../assets/android/gen/build.gradle.kts"), @@ -878,7 +2346,7 @@ impl BuildRequest { )?, )?; - // Write the res folder + // Write the res folder, containing stuff like default icons, colors, and menubars. let res = app_main.join("res"); create_dir_all(&res)?; create_dir_all(res.join("values"))?; @@ -958,7 +2426,7 @@ impl BuildRequest { Ok(()) } - pub(crate) fn wry_android_kotlin_files_out_dir(&self) -> PathBuf { + fn wry_android_kotlin_files_out_dir(&self) -> PathBuf { let mut kotlin_dir = self .root_dir() .join("app") @@ -970,8 +2438,1301 @@ impl BuildRequest { kotlin_dir = kotlin_dir.join(segment); } - tracing::debug!("app_kotlin_out: {:?}", kotlin_dir); - kotlin_dir } + + /// Get the directory where this app can write to for this session that's guaranteed to be stable + /// for the same app. This is useful for emitting state like window position and size. + /// + /// The directory is specific for this app and might be + pub(crate) fn session_cache_dir(&self) -> PathBuf { + self.session_cache_dir.path().to_path_buf() + } + + /// Get the outdir specified by the Dioxus.toml, relative to the crate directory. + /// We don't support workspaces yet since that would cause a collision of bundles per project. + pub(crate) fn crate_out_dir(&self) -> Option { + self.config + .application + .out_dir + .as_ref() + .map(|out_dir| self.crate_dir().join(out_dir)) + } + + /// Compose an out directory. Represents the typical "dist" directory that + /// is "distributed" after building an application (configurable in the + /// `Dioxus.toml`). + fn internal_out_dir(&self) -> PathBuf { + let dir = self.workspace_dir().join("target").join("dx"); + std::fs::create_dir_all(&dir).unwrap(); + dir + } + + /// Create a workdir for the given platform + /// This can be used as a temporary directory for the build, but in an observable way such that + /// you can see the files in the directory via `target` + /// + /// target/dx/build/app/web/ + /// target/dx/build/app/web/public/ + /// target/dx/build/app/web/server.exe + pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf { + self.internal_out_dir() + .join(self.executable_name()) + .join(if release { "release" } else { "debug" }) + .join(platform.build_folder_name()) + } + + /// target/dx/bundle/app/ + /// target/dx/bundle/app/blah.app + /// target/dx/bundle/app/blah.exe + /// target/dx/bundle/app/public/ + pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf { + self.internal_out_dir() + .join(self.executable_name()) + .join("bundle") + .join(platform.build_folder_name()) + } + + /// Get the workspace directory for the crate + pub(crate) fn workspace_dir(&self) -> PathBuf { + self.workspace + .krates + .workspace_root() + .as_std_path() + .to_path_buf() + } + + /// Get the directory of the crate + pub(crate) fn crate_dir(&self) -> PathBuf { + self.package() + .manifest_path + .parent() + .unwrap() + .as_std_path() + .to_path_buf() + } + + /// Get the package we are currently in + pub(crate) fn package(&self) -> &krates::cm::Package { + &self.workspace.krates[self.crate_package] + } + + /// Get the name of the package we are compiling + pub(crate) fn executable_name(&self) -> &str { + &self.crate_target.name + } + + /// Get the type of executable we are compiling + pub(crate) fn executable_type(&self) -> TargetKind { + self.crate_target.kind[0] + } + + /// Get the features required to build for the given platform + fn feature_for_platform(package: &krates::cm::Package, platform: Platform) -> String { + // Try to find the feature that activates the dioxus feature for the given platform + let dioxus_feature = platform.feature_name(); + + let res = package.features.iter().find_map(|(key, features)| { + // if the feature is just the name of the platform, we use that + if key == dioxus_feature { + return Some(key.clone()); + } + + // Otherwise look for the feature that starts with dioxus/ or dioxus?/ and matches the platform + for feature in features { + if let Some((_, after_dioxus)) = feature.split_once("dioxus") { + if let Some(dioxus_feature_enabled) = + after_dioxus.trim_start_matches('?').strip_prefix('/') + { + // If that enables the feature we are looking for, return that feature + if dioxus_feature_enabled == dioxus_feature { + return Some(key.clone()); + } + } + } + } + + None + }); + + res.unwrap_or_else(|| { + let fallback = format!("dioxus/{}", platform.feature_name()) ; + tracing::debug!( + "Could not find explicit feature for platform {platform}, passing `fallback` instead" + ); + fallback + }) + } + + // The `opt-level=1` increases build times, but can noticeably decrease time + // between saving changes and being able to interact with an app (for wasm/web). The "overall" + // time difference (between having and not having the optimization) can be + // almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends + // on setup (hardware, OS, browser, idle load). + // + // Find or create the client and server profiles in the top-level Cargo.toml file + // todo(jon): we should/could make these optional by placing some defaults somewhere + pub(crate) fn initialize_profiles(&self) -> crate::Result<()> { + let config_path = self.workspace_dir().join("Cargo.toml"); + let mut config = match std::fs::read_to_string(&config_path) { + Ok(config) => config.parse::().map_err(|e| { + crate::Error::Other(anyhow::anyhow!("Failed to parse Cargo.toml: {}", e)) + })?, + Err(_) => Default::default(), + }; + + if let Item::Table(table) = config + .as_table_mut() + .entry("profile") + .or_insert(Item::Table(Default::default())) + { + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_WASM) { + let mut client = toml_edit::Table::new(); + client.insert("inherits", Item::Value("dev".into())); + client.insert("opt-level", Item::Value(1.into())); + entry.insert(Item::Table(client)); + } + + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_SERVER) { + let mut server = toml_edit::Table::new(); + server.insert("inherits", Item::Value("dev".into())); + entry.insert(Item::Table(server)); + } + + if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_ANDROID) { + let mut android = toml_edit::Table::new(); + android.insert("inherits", Item::Value("dev".into())); + entry.insert(Item::Table(android)); + } + } + + std::fs::write(config_path, config.to_string()) + .context("Failed to write profiles to Cargo.toml")?; + + Ok(()) + } + + /// Return the version of the wasm-bindgen crate if it exists + fn wasm_bindgen_version(&self) -> Option { + self.workspace + .krates + .krates_by_name("wasm-bindgen") + .next() + .map(|krate| krate.krate.version.to_string()) + } + + /// Return the platforms that are enabled for the package + /// + /// Ideally only one platform is enabled but we need to be able to + pub(crate) fn enabled_cargo_toml_platforms( + package: &krates::cm::Package, + no_default_features: bool, + ) -> Vec { + let mut platforms = vec![]; + + // Attempt to discover the platform directly from the dioxus dependency + // + // [dependencies] + // dioxus = { features = ["web"] } + // + if let Some(dxs) = package.dependencies.iter().find(|dep| dep.name == "dioxus") { + for f in dxs.features.iter() { + if let Some(platform) = Platform::autodetect_from_cargo_feature(f) { + platforms.push(platform); + } + } + } + + // Start searching through the default features + // + // [features] + // default = ["dioxus/web"] + // + // or + // + // [features] + // default = ["web"] + // web = ["dioxus/web"] + if no_default_features { + return platforms; + } + + let Some(default) = package.features.get("default") else { + return platforms; + }; + + tracing::debug!("Default features: {default:?}"); + + // we only trace features 1 level deep.. + for feature in default.iter() { + // If the user directly specified a platform we can just use that. + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + let auto = Platform::autodetect_from_cargo_feature(dx_feature); + if let Some(auto) = auto { + platforms.push(auto); + } + } + + // If the user is specifying an internal feature that points to a platform, we can use that + let internal_feature = package.features.get(feature); + if let Some(internal_feature) = internal_feature { + for feature in internal_feature { + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + let auto = Platform::autodetect_from_cargo_feature(dx_feature); + if let Some(auto) = auto { + platforms.push(auto); + } + } + } + } + } + + platforms.sort(); + platforms.dedup(); + + tracing::debug!("Default platforms: {platforms:?}"); + + platforms + } + + /// Gather the features that are enabled for the package + fn platformless_features(package: &krates::cm::Package) -> Vec { + let Some(default) = package.features.get("default") else { + return Vec::new(); + }; + + let mut kept_features = vec![]; + + // Only keep the top-level features in the default list that don't point to a platform directly + // IE we want to drop `web` if default = ["web"] + 'top: for feature in default { + // Don't keep features that point to a platform via dioxus/blah + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { + continue 'top; + } + } + + // Don't keep features that point to a platform via an internal feature + if let Some(internal_feature) = package.features.get(feature) { + for feature in internal_feature { + if feature.starts_with("dioxus/") { + let dx_feature = feature.trim_start_matches("dioxus/"); + if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { + continue 'top; + } + } + } + } + + // Otherwise we can keep it + kept_features.push(feature.to_string()); + } + + kept_features + } + + pub(crate) fn mobile_org(&self) -> String { + let identifier = self.bundle_identifier(); + let mut split = identifier.splitn(3, '.'); + let sub = split + .next() + .expect("Identifier to have at least 3 periods like `com.example.app`"); + let tld = split + .next() + .expect("Identifier to have at least 3 periods like `com.example.app`"); + format!("{}.{}", sub, tld) + } + + pub(crate) fn bundled_app_name(&self) -> String { + use convert_case::{Case, Casing}; + self.executable_name().to_case(Case::Pascal) + } + + pub(crate) fn full_mobile_app_name(&self) -> String { + format!("{}.{}", self.mobile_org(), self.bundled_app_name()) + } + + pub(crate) fn bundle_identifier(&self) -> String { + if let Some(identifier) = self.config.bundle.identifier.clone() { + return identifier.clone(); + } + + format!("com.example.{}", self.bundled_app_name()) + } + + /// The item that we'll try to run directly if we need to. + /// + /// todo(jon): we should name the app properly instead of making up the exe name. It's kinda okay for dev mode, but def not okay for prod + pub(crate) fn main_exe(&self) -> PathBuf { + self.exe_dir().join(self.platform_exe_name()) + } + + /// Does the app specify: + /// + /// - Dioxus with "fullstack" enabled? (access to serverfns, etc) + /// - An explicit "fullstack" feature that enables said feature? + /// + /// Note that we don't detect if dependencies enable it transitively since we want to be explicit about it. + /// + /// The intention here is to detect if "fullstack" is enabled in the target's features list: + /// ```toml + /// [dependencies] + /// dioxus = { version = "0.4", features = ["fullstack"] } + /// ``` + /// + /// or as an explicit feature in default: + /// ```toml + /// [features] + /// default = ["dioxus/fullstack"] + /// ``` + /// + /// or as a default feature that enables the dioxus feature: + /// ```toml + /// [features] + /// default = ["fullstack"] + /// fullstack = ["dioxus/fullstack"] + /// ``` + /// + /// or as an explicit feature (that enables the dioxus feature): + /// ``` + /// dx serve app --features "fullstack" + /// ``` + pub(crate) fn fullstack_feature_enabled(&self) -> bool { + let dioxus_dep = self + .package() + .dependencies + .iter() + .find(|dep| dep.name == "dioxus"); + + // If we don't have a dioxus dependency, we can't be fullstack. This shouldn't impact non-dioxus projects + let Some(dioxus_dep) = dioxus_dep else { + return false; + }; + + // Check if the dioxus dependency has the "fullstack" feature enabled + if dioxus_dep.features.iter().any(|f| f == "fullstack") { + return true; + } + + // Check if any of the features in our feature list enables a feature that enables "fullstack" + let transitive = self + .package() + .features + .iter() + .filter(|(_name, list)| list.iter().any(|f| f == "dioxus/fullstack")); + + for (name, _list) in transitive { + if self.features.contains(name) { + return true; + } + } + + false + } + + /// todo(jon): use handlebars templates instead of these prebaked templates + async fn write_metadata(&self) -> Result<()> { + // write the Info.plist file + match self.platform { + Platform::MacOS => { + let dest = self.root_dir().join("Contents").join("Info.plist"); + let plist = self.info_plist_contents(self.platform)?; + std::fs::write(dest, plist)?; + } + + Platform::Ios => { + let dest = self.root_dir().join("Info.plist"); + let plist = self.info_plist_contents(self.platform)?; + std::fs::write(dest, plist)?; + } + + // AndroidManifest.xml + // er.... maybe even all the kotlin/java/gradle stuff? + Platform::Android => {} + + // Probably some custom format or a plist file (haha) + // When we do the proper bundle, we'll need to do something with wix templates, I think? + Platform::Windows => {} + + // eventually we'll create the .appimage file, I guess? + Platform::Linux => {} + + // These are served as folders, not appimages, so we don't need to do anything special (I think?) + // Eventually maybe write some secrets/.env files for the server? + // We could also distribute them as a deb/rpm for linux and msi for windows + Platform::Web => {} + Platform::Server => {} + Platform::Liveview => {} + } + + Ok(()) + } + + /// Run the optimizers, obfuscators, minimizers, signers, etc + async fn optimize(&self, ctx: &BuildContext) -> Result<()> { + match self.platform { + Platform::Web => { + // Compress the asset dir + // If pre-compressing is enabled, we can pre_compress the wasm-bindgen output + let pre_compress = self.should_pre_compress_web_assets(self.release); + + ctx.status_compressing_assets(); + let asset_dir = self.asset_dir(); + tokio::task::spawn_blocking(move || { + crate::fastfs::pre_compress_folder(&asset_dir, pre_compress) + }) + .await + .unwrap()?; + } + Platform::MacOS => {} + Platform::Windows => {} + Platform::Linux => {} + Platform::Ios => {} + Platform::Android => {} + Platform::Server => {} + Platform::Liveview => {} + } + + Ok(()) + } + + /// Check if assets should be pre_compressed. This will only be true in release mode if the user + /// has enabled pre_compress in the web config. + fn should_pre_compress_web_assets(&self, release: bool) -> bool { + self.config.web.pre_compress && release + } + + /// Bundle the web app + /// - Run wasm-bindgen + /// - Bundle split + /// - Run wasm-opt + /// - Register the .wasm and .js files with the asset system + async fn bundle_web( + &self, + ctx: &BuildContext, + exe: &Path, + assets: &mut AssetManifest, + ) -> Result<()> { + use crate::{wasm_bindgen::WasmBindgen, wasm_opt}; + use std::fmt::Write; + + // Locate the output of the build files and the bindgen output + // We'll fill these in a second if they don't already exist + let bindgen_outdir = self.wasm_bindgen_out_dir(); + let post_bindgen_wasm = self.wasm_bindgen_wasm_output_file(); + let should_bundle_split: bool = self.wasm_split; + let bindgen_version = self + .wasm_bindgen_version() + .expect("this should have been checked by tool verification"); + + // Prepare any work dirs + std::fs::create_dir_all(&bindgen_outdir)?; + + // Lift the internal functions to exports + if ctx.mode == BuildMode::Fat { + let unprocessed = std::fs::read(exe)?; + let all_exported_bytes = crate::build::prepare_wasm_base_module(&unprocessed)?; + std::fs::write(exe, all_exported_bytes)?; + } + + // Prepare our configuration + // + // we turn off debug symbols in dev mode but leave them on in release mode (weird!) since + // wasm-opt and wasm-split need them to do better optimizations. + // + // We leave demangling to false since it's faster and these tools seem to prefer the raw symbols. + // todo(jon): investigate if the chrome extension needs them demangled or demangles them automatically. + let will_wasm_opt = (self.release || self.wasm_split) + && (self.workspace.wasm_opt.is_some() || cfg!(feature = "optimizations")); + let keep_debug = self.config.web.wasm_opt.debug + || self.debug_symbols + || self.wasm_split + || !self.release + || will_wasm_opt + || ctx.mode == BuildMode::Fat; + let keep_names = will_wasm_opt || ctx.mode == BuildMode::Fat; + let demangle = false; + let wasm_opt_options = WasmOptConfig { + memory_packing: self.wasm_split, + debug: self.debug_symbols, + ..self.config.web.wasm_opt.clone() + }; + + // Run wasm-bindgen. Some of the options are not "optimal" but will be fixed up by wasm-opt + // + // There's performance implications here. Running with --debug is slower than without + // We're keeping around lld sections and names but wasm-opt will fix them + // todo(jon): investigate a good balance of wiping debug symbols during dev (or doing a double build?) + ctx.status_wasm_bindgen_start(); + tracing::debug!(dx_src = ?TraceSrc::Bundle, "Running wasm-bindgen"); + let start = std::time::Instant::now(); + WasmBindgen::new(&bindgen_version) + .input_path(exe) + .target("web") + .debug(keep_debug) + .demangle(demangle) + .keep_debug(keep_debug) + .keep_lld_sections(true) + .out_name(self.executable_name()) + .out_dir(&bindgen_outdir) + .remove_name_section(!keep_names) + .remove_producers_section(!keep_names) + .run() + .await + .context("Failed to generate wasm-bindgen bindings")?; + tracing::debug!(dx_src = ?TraceSrc::Bundle, "wasm-bindgen complete in {:?}", start.elapsed()); + + // Run bundle splitting if the user has requested it + // It's pretty expensive but because of rayon should be running separate threads, hopefully + // not blocking this thread. Dunno if that's true + if should_bundle_split { + ctx.status_splitting_bundle(); + + if !will_wasm_opt { + return Err(anyhow::anyhow!( + "Bundle splitting requires wasm-opt to be installed or the CLI to be built with `--features optimizations`. Please install wasm-opt and try again." + ) + .into()); + } + + // Load the contents of these binaries since we need both of them + // We're going to use the default makeLoad glue from wasm-split + let original = std::fs::read(exe)?; + let bindgened = std::fs::read(&post_bindgen_wasm)?; + let mut glue = wasm_split_cli::MAKE_LOAD_JS.to_string(); + + // Run the emitter + let splitter = wasm_split_cli::Splitter::new(&original, &bindgened); + let modules = splitter + .context("Failed to parse wasm for splitter")? + .emit() + .context("Failed to emit wasm split modules")?; + + // Write the chunks that contain shared imports + // These will be in the format of chunk_0_modulename.wasm - this is hardcoded in wasm-split + tracing::debug!("Writing split chunks to disk"); + for (idx, chunk) in modules.chunks.iter().enumerate() { + let path = bindgen_outdir.join(format!("chunk_{}_{}.wasm", idx, chunk.module_name)); + wasm_opt::write_wasm(&chunk.bytes, &path, &wasm_opt_options).await?; + writeln!( + glue, "export const __wasm_split_load_chunk_{idx} = makeLoad(\"/assets/{url}\", [], fusedImports);", + url = assets + .register_asset(&path, AssetOptions::Unknown)?.bundled_path(), + )?; + } + + // Write the modules that contain the entrypoints + tracing::debug!("Writing split modules to disk"); + for (idx, module) in modules.modules.iter().enumerate() { + let comp_name = module + .component_name + .as_ref() + .context("generated bindgen module has no name?")?; + + let path = bindgen_outdir.join(format!("module_{}_{}.wasm", idx, comp_name)); + wasm_opt::write_wasm(&module.bytes, &path, &wasm_opt_options).await?; + + let hash_id = module + .hash_id + .as_ref() + .context("generated wasm-split bindgen module has no hash id?")?; + + writeln!( + glue, + "export const __wasm_split_load_{module}_{hash_id}_{comp_name} = makeLoad(\"/assets/{url}\", [{deps}], fusedImports);", + module = module.module_name, + + + // Again, register this wasm with the asset system + url = assets + .register_asset(&path, AssetOptions::Unknown)?.bundled_path(), + + // This time, make sure to write the dependencies of this chunk + // The names here are again, hardcoded in wasm-split - fix this eventually. + deps = module + .relies_on_chunks + .iter() + .map(|idx| format!("__wasm_split_load_chunk_{idx}")) + .collect::>() + .join(", ") + )?; + } + + // Write the js binding + // It's not registered as an asset since it will get included in the main.js file + let js_output_path = bindgen_outdir.join("__wasm_split.js"); + std::fs::write(&js_output_path, &glue)?; + + // Make sure to write some entropy to the main.js file so it gets a new hash + // If we don't do this, the main.js file will be cached and never pick up the chunk names + let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, glue.as_bytes()); + std::fs::OpenOptions::new() + .append(true) + .open(self.wasm_bindgen_js_output_file()) + .context("Failed to open main.js file")? + .write_all(format!("/*{uuid}*/").as_bytes())?; + + // Write the main wasm_bindgen file and register it with the asset system + // This will overwrite the file in place + // We will wasm-opt it in just a second... + std::fs::write(&post_bindgen_wasm, modules.main.bytes)?; + } + + if matches!(ctx.mode, BuildMode::Fat) { + // add `export { __wbg_get_imports };` to the end of the wasmbindgen js file + let mut js = std::fs::read(self.wasm_bindgen_js_output_file())?; + writeln!(js, "\nexport {{ __wbg_get_imports }};")?; + std::fs::write(self.wasm_bindgen_js_output_file(), js)?; + } + + // Make sure to optimize the main wasm file if requested or if bundle splitting + if should_bundle_split || self.release { + ctx.status_optimizing_wasm(); + wasm_opt::optimize(&post_bindgen_wasm, &post_bindgen_wasm, &wasm_opt_options).await?; + } + + // In release mode, we make the wasm and bindgen files into assets so they get bundled with max + // optimizations. + let wasm_path = if self.release { + // Register the main.js with the asset system so it bundles in the snippets and optimizes + let name = assets.register_asset( + &self.wasm_bindgen_js_output_file(), + AssetOptions::Js(JsAssetOptions::new().with_minify(true).with_preload(true)), + )?; + format!("assets/{}", name.bundled_path()) + } else { + let asset = self.wasm_bindgen_wasm_output_file(); + format!("wasm/{}", asset.file_name().unwrap().to_str().unwrap()) + }; + + let js_path = if self.release { + // Make sure to register the main wasm file with the asset system + let name = assets.register_asset(&post_bindgen_wasm, AssetOptions::Unknown)?; + format!("assets/{}", name.bundled_path()) + } else { + let asset = self.wasm_bindgen_js_output_file(); + format!("wasm/{}", asset.file_name().unwrap().to_str().unwrap()) + }; + + // Write the index.html file with the pre-configured contents we got from pre-rendering + std::fs::write( + self.root_dir().join("index.html"), + self.prepare_html(assets, &wasm_path, &js_path)?, + )?; + + Ok(()) + } + + fn info_plist_contents(&self, platform: Platform) -> Result { + #[derive(Serialize)] + pub struct InfoPlistData { + pub display_name: String, + pub bundle_name: String, + pub bundle_identifier: String, + pub executable_name: String, + } + + match platform { + Platform::MacOS => handlebars::Handlebars::new() + .render_template( + include_str!("../../assets/macos/mac.plist.hbs"), + &InfoPlistData { + display_name: self.bundled_app_name(), + bundle_name: self.bundled_app_name(), + executable_name: self.platform_exe_name(), + bundle_identifier: self.bundle_identifier(), + }, + ) + .map_err(|e| e.into()), + Platform::Ios => handlebars::Handlebars::new() + .render_template( + include_str!("../../assets/ios/ios.plist.hbs"), + &InfoPlistData { + display_name: self.bundled_app_name(), + bundle_name: self.bundled_app_name(), + executable_name: self.platform_exe_name(), + bundle_identifier: self.bundle_identifier(), + }, + ) + .map_err(|e| e.into()), + _ => Err(anyhow::anyhow!("Unsupported platform for Info.plist").into()), + } + } + + /// Run any final tools to produce apks or other artifacts we might need. + /// + /// This might include codesigning, zipping, creating an appimage, etc + async fn assemble(&self, ctx: &BuildContext) -> Result<()> { + if let Platform::Android = self.platform { + ctx.status_running_gradle(); + + let output = Command::new(self.gradle_exe()?) + .arg("assembleDebug") + .current_dir(self.root_dir()) + .output() + .await?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Failed to assemble apk: {output:?}").into()); + } + } + + Ok(()) + } + + /// Run bundleRelease and return the path to the `.aab` file + /// + /// + pub(crate) async fn android_gradle_bundle(&self) -> Result { + let output = Command::new(self.gradle_exe()?) + .arg("bundleRelease") + .current_dir(self.root_dir()) + .output() + .await + .context("Failed to run gradle bundleRelease")?; + + if !output.status.success() { + return Err(anyhow::anyhow!("Failed to bundleRelease: {output:?}").into()); + } + + let app_release = self + .root_dir() + .join("app") + .join("build") + .join("outputs") + .join("bundle") + .join("release"); + + // Rename it to Name-arch.aab + let from = app_release.join("app-release.aab"); + let to = app_release.join(format!("{}-{}.aab", self.bundled_app_name(), self.triple)); + + std::fs::rename(from, &to).context("Failed to rename aab")?; + + Ok(to) + } + + fn gradle_exe(&self) -> Result { + // make sure we can execute the gradlew script + #[cfg(unix)] + { + use std::os::unix::prelude::PermissionsExt; + std::fs::set_permissions( + self.root_dir().join("gradlew"), + std::fs::Permissions::from_mode(0o755), + )?; + } + + let gradle_exec_name = match cfg!(windows) { + true => "gradlew.bat", + false => "gradlew", + }; + + Ok(self.root_dir().join(gradle_exec_name)) + } + + pub(crate) fn debug_apk_path(&self) -> PathBuf { + self.root_dir() + .join("app") + .join("build") + .join("outputs") + .join("apk") + .join("debug") + .join("app-debug.apk") + } + + /// We only really currently care about: + /// + /// - app dir (.app, .exe, .apk, etc) + /// - assetas dir + /// - exe dir (.exe, .app, .apk, etc) + /// - extra scaffolding + /// + /// It's not guaranteed that they're different from any other folder + pub(crate) fn prepare_build_dir(&self) -> Result<()> { + use once_cell::sync::OnceCell; + use std::fs::{create_dir_all, remove_dir_all}; + + static INITIALIZED: OnceCell> = OnceCell::new(); + + let success = INITIALIZED.get_or_init(|| { + if self.platform != Platform::Server { + _ = remove_dir_all(self.exe_dir()); + } + + self.flush_session_cache(); + + create_dir_all(self.root_dir())?; + create_dir_all(self.exe_dir())?; + create_dir_all(self.asset_dir())?; + + tracing::debug!("Initialized Root dir: {:?}", self.root_dir()); + tracing::debug!("Initialized Exe dir: {:?}", self.exe_dir()); + tracing::debug!("Initialized Asset dir: {:?}", self.asset_dir()); + + // we could download the templates from somewhere (github?) but after having banged my head against + // cargo-mobile2 for ages, I give up with that. We're literally just going to hardcode the templates + // by writing them here. + if let Platform::Android = self.platform { + self.build_android_app_dir()?; + } + + Ok(()) + }); + + if let Err(e) = success.as_ref() { + return Err(format!("Failed to initialize build directory: {e}").into()); + } + + Ok(()) + } + + pub(crate) fn asset_dir(&self) -> PathBuf { + match self.platform { + Platform::MacOS => self + .root_dir() + .join("Contents") + .join("Resources") + .join("assets"), + + Platform::Android => self + .root_dir() + .join("app") + .join("src") + .join("main") + .join("assets"), + + // everyone else is soooo normal, just app/assets :) + Platform::Web + | Platform::Ios + | Platform::Windows + | Platform::Linux + | Platform::Server + | Platform::Liveview => self.root_dir().join("assets"), + } + } + + /// The directory in which we'll put the main exe + /// + /// Mac, Android, Web are a little weird + /// - mac wants to be in Contents/MacOS + /// - android wants to be in jniLibs/arm64-v8a (or others, depending on the platform / architecture) + /// - web wants to be in wasm (which... we don't really need to, we could just drop the wasm into public and it would work) + /// + /// I think all others are just in the root folder + /// + /// todo(jon): investigate if we need to put .wasm in `wasm`. It kinda leaks implementation details, which ideally we don't want to do. + fn exe_dir(&self) -> PathBuf { + match self.platform { + Platform::MacOS => self.root_dir().join("Contents").join("MacOS"), + Platform::Web => self.root_dir().join("wasm"), + + // Android has a whole build structure to it + Platform::Android => self + .root_dir() + .join("app") + .join("src") + .join("main") + .join("jniLibs") + .join(AndroidTools::android_jnilib(&self.triple)), + + // these are all the same, I think? + Platform::Windows + | Platform::Linux + | Platform::Ios + | Platform::Server + | Platform::Liveview => self.root_dir(), + } + } + + /// Get the path to the wasm bindgen temporary output folder + fn wasm_bindgen_out_dir(&self) -> PathBuf { + self.root_dir().join("wasm") + } + + /// Get the path to the wasm bindgen javascript output file + pub(crate) fn wasm_bindgen_js_output_file(&self) -> PathBuf { + self.wasm_bindgen_out_dir() + .join(self.executable_name()) + .with_extension("js") + } + + /// Get the path to the wasm bindgen wasm output file + pub(crate) fn wasm_bindgen_wasm_output_file(&self) -> PathBuf { + self.wasm_bindgen_out_dir() + .join(format!("{}_bg", self.executable_name())) + .with_extension("wasm") + } + + /// Get the path to the asset optimizer version file + pub(crate) fn asset_optimizer_version_file(&self) -> PathBuf { + self.platform_dir().join(".cli-version") + } + + fn flush_session_cache(&self) { + let cache_dir = self.session_cache_dir(); + _ = std::fs::remove_dir_all(&cache_dir); + _ = std::fs::create_dir_all(&cache_dir); + } + + /// Check for tooling that might be required for this build. + /// + /// This should generally be only called on the first build since it takes time to verify the tooling + /// is in place, and we don't want to slow down subsequent builds. + pub(crate) async fn verify_tooling(&self, ctx: &BuildContext) -> Result<()> { + tracing::debug!("Verifying tooling..."); + ctx.status_installing_tooling(); + + self + .initialize_profiles() + .context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?; + + match self.platform { + Platform::Web => self.verify_web_tooling().await?, + Platform::Ios => self.verify_ios_tooling().await?, + Platform::Android => self.verify_android_tooling().await?, + Platform::Linux => self.verify_linux_tooling().await?, + Platform::MacOS | Platform::Windows | Platform::Server | Platform::Liveview => {} + } + + Ok(()) + } + + async fn verify_web_tooling(&self) -> Result<()> { + // Install target using rustup. + #[cfg(not(feature = "no-downloads"))] + if !self.workspace.has_wasm32_unknown_unknown() { + tracing::info!( + "Web platform requires wasm32-unknown-unknown to be installed. Installing..." + ); + + let _ = tokio::process::Command::new("rustup") + .args(["target", "add", "wasm32-unknown-unknown"]) + .output() + .await?; + } + + // Ensure target is installed. + if !self.workspace.has_wasm32_unknown_unknown() { + return Err(Error::Other(anyhow::anyhow!( + "Missing target wasm32-unknown-unknown." + ))); + } + + // Wasm bindgen + let krate_bindgen_version = self.wasm_bindgen_version().ok_or(anyhow::anyhow!( + "failed to detect wasm-bindgen version, unable to proceed" + ))?; + + WasmBindgen::verify_install(&krate_bindgen_version).await?; + + Ok(()) + } + + /// Currently does nothing, but eventually we need to check that the mobile tooling is installed. + /// + /// For ios, this would be just aarch64-apple-ios + aarch64-apple-ios-sim, as well as xcrun and xcode-select + /// + /// We don't auto-install these yet since we're not doing an architecture check. We assume most users + /// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually + /// should be installing the x86 versions. + async fn verify_ios_tooling(&self) -> Result<()> { + // open the simulator + // _ = tokio::process::Command::new("open") + // .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app") + // .output() + // .await; + + // Now xcrun to open the device + // todo: we should try and query the device list and/or parse it rather than hardcode this simulator + // _ = tokio::process::Command::new("xcrun") + // .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"]) + // .output() + // .await; + + // if !rustup + // .installed_toolchains + // .contains(&"aarch64-apple-ios".to_string()) + // { + // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); + // } + + // if !rustup + // .installed_toolchains + // .contains(&"aarch64-apple-ios-sim".to_string()) + // { + // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); + // } + + Ok(()) + } + + /// Check if the android tooling is installed + /// + /// looks for the android sdk + ndk + /// + /// will do its best to fill in the missing bits by exploring the sdk structure + /// IE will attempt to use the Java installed from android studio if possible. + async fn verify_android_tooling(&self) -> Result<()> { + let linker = self.workspace.android_tools()?.android_cc(&self.triple); + + tracing::debug!("Verifying android linker: {linker:?}"); + + if linker.exists() { + return Ok(()); + } + + Err(anyhow::anyhow!( + "Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation." + ).into()) + } + + /// Ensure the right dependencies are installed for linux apps. + /// This varies by distro, so we just do nothing for now. + /// + /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri: + /// + async fn verify_linux_tooling(&self) -> Result<()> { + Ok(()) + } + + /// update the mtime of the "main" file to bust the fingerprint, forcing rustc to recompile it. + /// + /// This prevents rustc from using the cached version of the binary, which can cause issues + /// with our hotpatching setup since it uses linker interception. + /// + /// This is sadly a hack. I think there might be other ways of busting the fingerprint (rustc wrapper?) + /// but that would require relying on cargo internals. + /// + /// This might stop working if/when cargo stabilizes contents-based fingerprinting. + fn bust_fingerprint(&self, ctx: &BuildContext) -> Result<()> { + // if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base) { + if !matches!(ctx.mode, BuildMode::Thin { .. }) { + std::fs::File::open(&self.crate_target.src_path)?.set_modified(SystemTime::now())?; + } + Ok(()) + } + + async fn create_patch_cache(&self, exe: &Path) -> Result { + let exe = match self.platform { + Platform::Web => self.wasm_bindgen_wasm_output_file(), + _ => exe.to_path_buf(), + }; + + Ok(HotpatchModuleCache::new(&exe, &self.triple)?) + } + + /// Users create an index.html for their SPA if they want it + /// + /// We always write our wasm as main.js and main_bg.wasm + /// + /// In prod we run the optimizer which bundles everything together properly + /// + /// So their index.html needs to include main.js in the scripts otherwise nothing happens? + /// + /// Seems like every platform has a weird file that declares a bunch of stuff + /// - web: index.html + /// - ios: info.plist + /// - macos: info.plist + /// - linux: appimage root thing? + /// - android: androidmanifest.xml + /// + /// You also might different variants of these files (staging / prod) and different flavors (eu/us) + /// + /// web's index.html is weird since it's not just a bundle format but also a *content* format + pub(crate) fn prepare_html( + &self, + assets: &AssetManifest, + wasm_path: &str, + js_path: &str, + ) -> Result { + let mut html = { + const DEV_DEFAULT_HTML: &str = include_str!("../../assets/web/dev.index.html"); + const PROD_DEFAULT_HTML: &str = include_str!("../../assets/web/prod.index.html"); + + let crate_root: &Path = &self.crate_dir(); + let custom_html_file = crate_root.join("index.html"); + let default_html = match self.release { + true => PROD_DEFAULT_HTML, + false => DEV_DEFAULT_HTML, + }; + std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(default_html)) + }; + + // Inject any resources from the config into the html + self.inject_resources(assets, &mut html)?; + + // Inject loading scripts if they are not already present + self.inject_loading_scripts(&mut html); + + // Replace any special placeholders in the HTML with resolved values + self.replace_template_placeholders(&mut html, wasm_path, js_path); + + let title = self.config.web.app.title.clone(); + Self::replace_or_insert_before("{app_title}", " bool { + !self.release + } + + // Inject any resources from the config into the html + fn inject_resources(&self, assets: &AssetManifest, html: &mut String) -> Result<()> { + use std::fmt::Write; + + // Collect all resources into a list of styles and scripts + let resources = &self.config.web.resource; + let mut style_list = resources.style.clone().unwrap_or_default(); + let mut script_list = resources.script.clone().unwrap_or_default(); + + if self.is_dev_build() { + style_list.extend(resources.dev.style.iter().cloned()); + script_list.extend(resources.dev.script.iter().cloned()); + } + + let mut head_resources = String::new(); + + // Add all styles to the head + for style in &style_list { + writeln!( + &mut head_resources, + "", + &style.to_str().unwrap(), + )?; + } + + // Add all scripts to the head + for script in &script_list { + writeln!( + &mut head_resources, + "", + &script.to_str().unwrap(), + )?; + } + + // Add the base path to the head if this is a debug build + if self.is_dev_build() { + if let Some(base_path) = &self.config.web.app.base_path { + head_resources.push_str(&format_base_path_meta_element(base_path)); + } + } + + // Inject any resources from manganis into the head + for asset in assets.assets.values() { + let asset_path = asset.bundled_path(); + match asset.options() { + AssetOptions::Css(css_options) => { + if css_options.preloaded() { + head_resources.push_str(&format!( + "" + )) + } + } + AssetOptions::Image(image_options) => { + if image_options.preloaded() { + head_resources.push_str(&format!( + "" + )) + } + } + AssetOptions::Js(js_options) => { + if js_options.preloaded() { + head_resources.push_str(&format!( + "" + )) + } + } + _ => {} + } + } + + // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system + let wasm_source_path = self.wasm_bindgen_wasm_output_file(); + if let Some(wasm_path) = assets.assets.get(&wasm_source_path) { + let wasm_path = wasm_path.bundled_path(); + head_resources.push_str(&format!( + "" + )); + + Self::replace_or_insert_before("{style_include}", " + // We can't use a module script here because we need to start the script immediately when streaming + import("/{base_path}/{js_path}").then( + ({ default: init, initSync, __wbg_get_imports }) => { + // export initSync in case a split module needs to initialize + window.__wasm_split_main_initSync = initSync; + + // Actually perform the load + init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => { + // assign this module to be accessible globally + window.__dx_mainWasm = wasm; + window.__dx_mainInit = init; + window.__dx_mainInitSync = initSync; + window.__dx___wbg_get_imports = __wbg_get_imports; + + if (wasm.__wbindgen_start == undefined) { + wasm.main(); + } + }); + } + ); + + , +} + +pub fn get_android_tools() -> Option> { + // We check for SDK first since users might install Android Studio and then install the SDK + // After that they might install the NDK, so the SDK drives the source of truth. + let sdk = var_or_debug("ANDROID_SDK_ROOT") + .or_else(|| var_or_debug("ANDROID_SDK")) + .or_else(|| var_or_debug("ANDROID_HOME")); + + // Check the ndk. We look for users's overrides first and then look into the SDK. + // Sometimes users set only the NDK (especially if they're somewhat advanced) so we need to look for it manually + // + // Might look like this, typically under "sdk": + // "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang" + let ndk = var_or_debug("NDK_HOME") + .or_else(|| var_or_debug("ANDROID_NDK_HOME")) + .or_else(|| { + // Look for the most recent NDK in the event the user has installed multiple NDK + // Eventually we might need to drive this from Dioxus.toml + let sdk = sdk.as_ref()?; + let ndk_dir = sdk.join("ndk").read_dir().ok()?; + ndk_dir + .flatten() + .map(|dir| (dir.file_name(), dir.path())) + .sorted() + .next_back() + .map(|(_, path)| path.to_path_buf()) + })?; + + // Look for ADB in the SDK. If it's not there we'll use `adb` from the PATH + let adb = sdk + .as_ref() + .and_then(|sdk| { + let tools = sdk.join("platform-tools"); + if tools.join("adb").exists() { + return Some(tools.join("adb")); + } + if tools.join("adb.exe").exists() { + return Some(tools.join("adb.exe")); + } + None + }) + .unwrap_or_else(|| PathBuf::from("adb")); + + // https://stackoverflow.com/questions/71381050/java-home-is-set-to-an-invalid-directory-android-studio-flutter + // always respect the user's JAVA_HOME env var above all other options + // + // we only attempt autodetection if java_home is not set + // + // this is a better fallback than falling onto the users' system java home since many users might + // not even know which java that is - they just know they have android studio installed + let java_home = std::env::var_os("JAVA_HOME") + .map(PathBuf::from) + .or_else(|| { + // Attempt to autodetect java home from the android studio path or jdk path on macos + #[cfg(target_os = "macos")] + { + let jbr_home = + PathBuf::from("/Applications/Android Studio.app/Contents/jbr/Contents/Home/"); + if jbr_home.exists() { + return Some(jbr_home); + } + + let jre_home = + PathBuf::from("/Applications/Android Studio.app/Contents/jre/Contents/Home"); + if jre_home.exists() { + return Some(jre_home); + } + + let jdk_home = + PathBuf::from("/Library/Java/JavaVirtualMachines/openjdk.jdk/Contents/Home/"); + if jdk_home.exists() { + return Some(jdk_home); + } + } + + #[cfg(target_os = "windows")] + { + let jbr_home = PathBuf::from("C:\\Program Files\\Android\\Android Studio\\jbr"); + if jbr_home.exists() { + return Some(jbr_home); + } + } + + // todo(jon): how do we detect java home on linux? + #[cfg(target_os = "linux")] + { + let jbr_home = PathBuf::from("/usr/lib/jvm/java-11-openjdk-amd64"); + if jbr_home.exists() { + return Some(jbr_home); + } + } + + None + }); + + Some(Arc::new(AndroidTools { + ndk, + adb, + java_home, + })) +} + +impl AndroidTools { + pub(crate) fn android_tools_dir(&self) -> PathBuf { + let prebuilt = self.ndk.join("toolchains").join("llvm").join("prebuilt"); + + if cfg!(target_os = "macos") { + // for whatever reason, even on aarch64 macos, the linker is under darwin-x86_64 + return prebuilt.join("darwin-x86_64").join("bin"); + } + + if cfg!(target_os = "linux") { + return prebuilt.join("linux-x86_64").join("bin"); + } + + if cfg!(target_os = "windows") { + return prebuilt.join("windows-x86_64").join("bin"); + } + + // Otherwise return the first entry in the prebuilt directory + prebuilt + .read_dir() + .expect("Failed to read android toolchains directory") + .next() + .expect("Failed to find android toolchains directory") + .expect("Failed to read android toolchain file") + .path() + } + + /// Return the location of the clang toolchain for the given target triple. + /// + /// Note that we use clang: + /// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang" + /// + /// But if we needed the linker, we would use: + /// "~/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/ld" + /// + /// However, for our purposes, we only go through the cc driver and not the linker directly. + pub(crate) fn android_cc(&self, triple: &Triple) -> PathBuf { + let suffix = if cfg!(target_os = "windows") { + ".cmd" + } else { + "" + }; + + self.android_tools_dir().join(format!( + "{}{}-clang{}", + triple, + self.min_sdk_version(), + suffix + )) + } + + // todo(jon): this should be configurable + pub(crate) fn min_sdk_version(&self) -> u32 { + 24 + } + + pub(crate) fn ar_path(&self) -> PathBuf { + self.android_tools_dir().join("llvm-ar") + } + + pub(crate) fn target_cc(&self) -> PathBuf { + self.android_tools_dir().join("clang") + } + + pub(crate) fn target_cxx(&self) -> PathBuf { + self.android_tools_dir().join("clang++") + } + + pub(crate) fn java_home(&self) -> Option { + self.java_home.clone() + } + + pub(crate) fn android_jnilib(triple: &Triple) -> &'static str { + use target_lexicon::Architecture; + match triple.architecture { + Architecture::Arm(_) => "armeabi-v7a", + Architecture::Aarch64(_) => "arm64-v8a", + Architecture::X86_32(_) => "x86", + Architecture::X86_64 => "x86_64", + _ => unimplemented!("Unsupported architecture"), + } + } + + pub(crate) async fn autodetect_android_device_triple(&self) -> Triple { + // Use the host's triple and then convert field by field + // ie, the "best" emulator for an m1 mac would be: "aarch64-linux-android" + // - We assume android is always "linux" + // - We try to match the architecture unless otherwise specified. This is because + // emulators that match the host arch are usually faster. + let mut triple = "aarch64-linux-android".parse::().unwrap(); + triple.operating_system = OperatingSystem::Linux; + triple.environment = Environment::Android; + triple.architecture = target_lexicon::HOST.architecture; + + // TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg) + let output = Command::new(&self.adb) + .arg("shell") + .arg("uname") + .arg("-m") + .output() + .await + .map(|out| String::from_utf8(out.stdout)); + + match output { + Ok(Ok(out)) => match out.trim() { + "armv7l" => triple.architecture = Architecture::Arm(ArmArchitecture::Arm), + "aarch64" => { + triple.architecture = Architecture::Aarch64(Aarch64Architecture::Aarch64) + } + "i386" => triple.architecture = Architecture::X86_32(X86_32Architecture::I386), + "x86_64" => { + triple.architecture = Architecture::X86_64; + } + other => { + tracing::warn!("Unknown architecture from adb: {other}"); + } + }, + Ok(Err(err)) => { + tracing::debug!("Failed to parse adb output: {err}"); + } + Err(err) => { + tracing::debug!("ADB command failed: {:?}", err); + } + }; + + triple + } +} + +fn var_or_debug(name: &str) -> Option { + use std::env::var; + use tracing::debug; + + var(name) + .inspect_err(|_| debug!("{name} not set")) + .ok() + .map(PathBuf::from) +} diff --git a/packages/cli/src/build/verify.rs b/packages/cli/src/build/verify.rs deleted file mode 100644 index c3724ee8be..0000000000 --- a/packages/cli/src/build/verify.rs +++ /dev/null @@ -1,143 +0,0 @@ -use crate::{wasm_bindgen::WasmBindgen, BuildRequest, Error, Platform, Result, RustcDetails}; -use anyhow::{anyhow, Context}; - -impl BuildRequest { - /// Check for tooling that might be required for this build. - /// - /// This should generally be only called on the first build since it takes time to verify the tooling - /// is in place, and we don't want to slow down subsequent builds. - pub(crate) async fn verify_tooling(&self) -> Result<()> { - tracing::debug!("Verifying tooling..."); - self.status_installing_tooling(); - - self.krate - .initialize_profiles() - .context("Failed to initialize profiles - dioxus can't build without them. You might need to initialize them yourself.")?; - - let rustc = match RustcDetails::from_cli().await { - Ok(out) => out, - Err(err) => { - tracing::error!("Failed to verify tooling: {err}\ndx will proceed, but you might run into errors later."); - return Ok(()); - } - }; - - match self.build.platform() { - Platform::Web => self.verify_web_tooling(rustc).await?, - Platform::Ios => self.verify_ios_tooling(rustc).await?, - Platform::Android => self.verify_android_tooling(rustc).await?, - Platform::Linux => self.verify_linux_tooling(rustc).await?, - Platform::MacOS => {} - Platform::Windows => {} - Platform::Server => {} - Platform::Liveview => {} - } - - Ok(()) - } - - pub(crate) async fn verify_web_tooling(&self, rustc: RustcDetails) -> Result<()> { - // Install target using rustup. - #[cfg(not(feature = "no-downloads"))] - if !rustc.has_wasm32_unknown_unknown() { - tracing::info!( - "Web platform requires wasm32-unknown-unknown to be installed. Installing..." - ); - - let _ = tokio::process::Command::new("rustup") - .args(["target", "add", "wasm32-unknown-unknown"]) - .output() - .await?; - } - - // Ensure target is installed. - if !rustc.has_wasm32_unknown_unknown() { - return Err(Error::Other(anyhow!( - "Missing target wasm32-unknown-unknown." - ))); - } - - // Wasm bindgen - let krate_bindgen_version = self.krate.wasm_bindgen_version().ok_or(anyhow!( - "failed to detect wasm-bindgen version, unable to proceed" - ))?; - - WasmBindgen::verify_install(&krate_bindgen_version).await?; - - Ok(()) - } - - /// Currently does nothing, but eventually we need to check that the mobile tooling is installed. - /// - /// For ios, this would be just aarch64-apple-ios + aarch64-apple-ios-sim, as well as xcrun and xcode-select - /// - /// We don't auto-install these yet since we're not doing an architecture check. We assume most users - /// are running on an Apple Silicon Mac, but it would be confusing if we installed these when we actually - /// should be installing the x86 versions. - pub(crate) async fn verify_ios_tooling(&self, _rustc: RustcDetails) -> Result<()> { - // open the simulator - // _ = tokio::process::Command::new("open") - // .arg("/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app") - // .stderr(Stdio::piped()) - // .stdout(Stdio::piped()) - // .status() - // .await; - - // Now xcrun to open the device - // todo: we should try and query the device list and/or parse it rather than hardcode this simulator - // _ = tokio::process::Command::new("xcrun") - // .args(["simctl", "boot", "83AE3067-987F-4F85-AE3D-7079EF48C967"]) - // .stderr(Stdio::piped()) - // .stdout(Stdio::piped()) - // .status() - // .await; - - // if !rustup - // .installed_toolchains - // .contains(&"aarch64-apple-ios".to_string()) - // { - // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); - // } - - // if !rustup - // .installed_toolchains - // .contains(&"aarch64-apple-ios-sim".to_string()) - // { - // tracing::error!("You need to install aarch64-apple-ios to build for ios. Run `rustup target add aarch64-apple-ios` to install it."); - // } - - Ok(()) - } - - /// Check if the android tooling is installed - /// - /// looks for the android sdk + ndk - /// - /// will do its best to fill in the missing bits by exploring the sdk structure - /// IE will attempt to use the Java installed from android studio if possible. - pub(crate) async fn verify_android_tooling(&self, _rustc: RustcDetails) -> Result<()> { - let result = self - .krate - .android_ndk() - .map(|ndk| self.build.target_args.arch().android_linker(&ndk)); - - if let Some(path) = result { - if path.exists() { - return Ok(()); - } - } - - Err(anyhow::anyhow!( - "Android linker not found. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation." - ).into()) - } - - /// Ensure the right dependencies are installed for linux apps. - /// This varies by distro, so we just do nothing for now. - /// - /// Eventually, we want to check for the prereqs for wry/tao as outlined by tauri: - /// https://tauri.app/start/prerequisites/ - pub(crate) async fn verify_linux_tooling(&self, _rustc: RustcDetails) -> Result<()> { - Ok(()) - } -} diff --git a/packages/cli/src/build/web.rs b/packages/cli/src/build/web.rs deleted file mode 100644 index 827a64b72b..0000000000 --- a/packages/cli/src/build/web.rs +++ /dev/null @@ -1,272 +0,0 @@ -use dioxus_cli_config::format_base_path_meta_element; -use manganis::AssetOptions; - -use crate::error::Result; -use std::fmt::Write; -use std::path::{Path, PathBuf}; - -use super::AppBundle; - -const DEFAULT_HTML: &str = include_str!("../../assets/web/index.html"); -const TOAST_HTML: &str = include_str!("../../assets/web/toast.html"); - -impl AppBundle { - pub(crate) fn prepare_html(&self) -> Result { - let mut html = { - let crate_root: &Path = &self.build.krate.crate_dir(); - let custom_html_file = crate_root.join("index.html"); - std::fs::read_to_string(custom_html_file).unwrap_or_else(|_| String::from(DEFAULT_HTML)) - }; - - // Inject any resources from the config into the html - self.inject_resources(&mut html)?; - - // Inject loading scripts if they are not already present - self.inject_loading_scripts(&mut html); - - // Replace any special placeholders in the HTML with resolved values - self.replace_template_placeholders(&mut html); - - let title = self.build.krate.config.web.app.title.clone(); - - replace_or_insert_before("{app_title}", " bool { - !self.build.build.release - } - - // Inject any resources from the config into the html - fn inject_resources(&self, html: &mut String) -> Result<()> { - // Collect all resources into a list of styles and scripts - let resources = &self.build.krate.config.web.resource; - let mut style_list = resources.style.clone().unwrap_or_default(); - let mut script_list = resources.script.clone().unwrap_or_default(); - - if self.is_dev_build() { - style_list.extend(resources.dev.style.iter().cloned()); - script_list.extend(resources.dev.script.iter().cloned()); - } - - let mut head_resources = String::new(); - - // Add all styles to the head - for style in &style_list { - writeln!( - &mut head_resources, - "", - &style.to_str().unwrap(), - )?; - } - - // Add all scripts to the head - for script in &script_list { - writeln!( - &mut head_resources, - "", - &script.to_str().unwrap(), - )?; - } - - // Add the base path to the head if this is a debug build - if self.is_dev_build() { - if let Some(base_path) = &self.build.krate.config.web.app.base_path { - head_resources.push_str(&format_base_path_meta_element(base_path)); - } - } - - if !style_list.is_empty() { - self.send_resource_deprecation_warning(style_list, ResourceType::Style); - } - if !script_list.is_empty() { - self.send_resource_deprecation_warning(script_list, ResourceType::Script); - } - - // Inject any resources from manganis into the head - for asset in self.app.assets.assets.values() { - let asset_path = asset.bundled_path(); - match asset.options() { - AssetOptions::Css(css_options) => { - if css_options.preloaded() { - head_resources.push_str(&format!( - "" - )) - } - } - AssetOptions::Image(image_options) => { - if image_options.preloaded() { - head_resources.push_str(&format!( - "" - )) - } - } - AssetOptions::Js(js_options) => { - if js_options.preloaded() { - head_resources.push_str(&format!( - "" - )) - } - } - _ => {} - } - } - // Manually inject the wasm file for preloading. WASM currently doesn't support preloading in the manganis asset system - let wasm_source_path = self.build.wasm_bindgen_wasm_output_file(); - let wasm_path = self - .app - .assets - .assets - .get(&wasm_source_path) - .expect("WASM asset should exist in web bundles") - .bundled_path(); - head_resources.push_str(&format!( - "" - )); - - replace_or_insert_before("{style_include}", " - // We can't use a module script here because we need to start the script immediately when streaming - import("/{base_path}/{js_path}").then( - ({ default: init, initSync }) => { - // export initSync in case a split module needs to initialize - window.__wasm_split_main_initSync = initSync; - - // Actually perform the load - init({module_or_path: "/{base_path}/{wasm_path}"}).then((wasm) => { - if (wasm.__wbindgen_start == undefined) { - wasm.main(); - } - }); - } - ); - - {DX_TOAST_UTILITIES} - html.replace("{DX_TOAST_UTILITIES}", TOAST_HTML), - false => html.replace("{DX_TOAST_UTILITIES}", ""), - }; - } - - /// Replace any special placeholders in the HTML with resolved values - fn replace_template_placeholders(&self, html: &mut String) { - let base_path = self.build.krate.config.web.app.base_path(); - *html = html.replace("{base_path}", base_path); - - let app_name = &self.build.krate.executable_name(); - let wasm_source_path = self.build.wasm_bindgen_wasm_output_file(); - let wasm_path = self - .app - .assets - .assets - .get(&wasm_source_path) - .expect("WASM asset should exist in web bundles") - .bundled_path(); - let wasm_path = format!("assets/{wasm_path}"); - let js_source_path = self.build.wasm_bindgen_js_output_file(); - let js_path = self - .app - .assets - .assets - .get(&js_source_path) - .expect("JS asset should exist in web bundles") - .bundled_path(); - let js_path = format!("assets/{js_path}"); - - // If the html contains the old `{app_name}` placeholder, replace {app_name}_bg.wasm and {app_name}.js - // with the new paths - *html = html.replace("wasm/{app_name}_bg.wasm", &wasm_path); - *html = html.replace("wasm/{app_name}.js", &js_path); - // Otherwise replace the new placeholders - *html = html.replace("{wasm_path}", &wasm_path); - *html = html.replace("{js_path}", &js_path); - // Replace the app_name if we find it anywhere standalone - *html = html.replace("{app_name}", app_name); - } - - fn send_resource_deprecation_warning(&self, paths: Vec, variant: ResourceType) { - const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release. Instead of including assets in the config, you can include assets with the `asset!` macro and add them to the head with `document::Link` and `Script` components."#; - - let replacement_components = paths - .iter() - .map(|path| { - let path = if path.exists() { - path.to_path_buf() - } else { - // If the path is absolute, make it relative to the current directory before we join it - // The path is actually a web path which is relative to the root of the website - let path = path.strip_prefix("/").unwrap_or(path); - let asset_dir_path = self - .build - .krate - .legacy_asset_dir() - .map(|dir| dir.join(path).canonicalize()); - - if let Some(Ok(absolute_path)) = asset_dir_path { - let absolute_crate_root = - self.build.krate.crate_dir().canonicalize().unwrap(); - PathBuf::from("./") - .join(absolute_path.strip_prefix(absolute_crate_root).unwrap()) - } else { - path.to_path_buf() - } - }; - match variant { - ResourceType::Style => { - format!(" Stylesheet {{ href: asset!(\"{}\") }}", path.display()) - } - ResourceType::Script => { - format!(" Script {{ src: asset!(\"{}\") }}", path.display()) - } - } - }) - .collect::>(); - let replacement_components = format!("rsx! {{\n{}\n}}", replacement_components.join("\n")); - let section_name = match variant { - ResourceType::Style => "web.resource.style", - ResourceType::Script => "web.resource.script", - }; - - tracing::warn!( - "{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```" - ); - } -} - -enum ResourceType { - Style, - Script, -} - -/// Replace a string or insert the new contents before a marker -fn replace_or_insert_before( - replace: &str, - or_insert_before: &str, - with: &str, - content: &mut String, -) { - if content.contains(replace) { - *content = content.replace(replace, with); - } else if let Some(pos) = content.find(or_insert_before) { - content.insert_str(pos, with); - } -} diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index 47d6630261..41d238af93 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -1,5 +1,5 @@ -use super::*; -use crate::{metadata::collect_rs_files, DioxusCrate}; +use super::{check::collect_rs_files, *}; +use crate::Workspace; use anyhow::Context; use dioxus_autofmt::{IndentOptions, IndentType}; use rayon::prelude::*; @@ -38,7 +38,7 @@ pub(crate) struct Autoformat { } impl Autoformat { - pub(crate) fn autoformat(self) -> Result { + pub(crate) async fn autoformat(self) -> Result { let Autoformat { check, raw, @@ -62,15 +62,16 @@ impl Autoformat { } else { // Default to formatting the project. let crate_dir = if let Some(package) = self.package { - // TODO (matt): Do we need to use the entire `DioxusCrate` here? - let target_args = TargetArgs { - package: Some(package), - ..Default::default() - }; - let dx_crate = - DioxusCrate::new(&target_args).context("failed to parse crate graph")?; - - Cow::Owned(dx_crate.crate_dir()) + let workspace = Workspace::current().await?; + let dx_crate = workspace + .find_main_package(Some(package)) + .context("Failed to find package")?; + workspace.krates[dx_crate] + .manifest_path + .parent() + .unwrap() + .to_path_buf() + .into() } else { Cow::Borrowed(Path::new(".")) }; @@ -171,7 +172,8 @@ fn autoformat_project( format_rust_code: bool, dir: impl AsRef, ) -> Result<()> { - let files_to_format = collect_rs_files(dir); + let mut files_to_format = vec![]; + collect_rs_files(dir.as_ref(), &mut files_to_format); if files_to_format.is_empty() { return Ok(()); @@ -297,5 +299,5 @@ async fn test_auto_fmt() { package: None, }; - fmt.autoformat().unwrap(); + fmt.autoformat().await.unwrap(); } diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 31bbe2d56f..df48e35883 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,207 +1,177 @@ -use super::*; -use crate::{Builder, DioxusCrate, Platform, PROFILE_SERVER}; +use crate::{cli::*, AppBuilder, BuildRequest, Workspace, PROFILE_SERVER}; +use crate::{BuildMode, Platform}; +use target_lexicon::Triple; + +use super::target::{TargetArgs, TargetCmd}; /// Build the Rust Dioxus app and all of its assets. /// -/// Produces a final output bundle designed to be run on the target platform. -#[derive(Clone, Debug, Default, Deserialize, Parser)] -pub(crate) struct BuildArgs { - /// Build in release mode [default: false] - #[clap(long, short)] - #[serde(default)] - pub(crate) release: bool, - - /// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false] +/// Produces a final output build. If a "server" feature is present in the package's Cargo.toml, it will +/// be considered a fullstack app and the server will be built as well. +#[derive(Clone, Debug, Default, Parser)] +pub struct BuildArgs { + /// Enable fullstack mode [default: false] + /// + /// This is automatically detected from `dx serve` if the "fullstack" feature is enabled by default. + #[clap(long)] + pub(crate) fullstack: Option, + + /// The feature to use for the client in a fullstack app [default: "web"] #[clap(long)] - #[serde(default)] - pub(crate) force_sequential: bool, + pub(crate) client_features: Vec, - /// Build the app with custom a profile + /// The feature to use for the server in a fullstack app [default: "server"] #[clap(long)] - pub(crate) profile: Option, + pub(crate) server_features: Vec, /// Build with custom profile for the fullstack server #[clap(long, default_value_t = PROFILE_SERVER.to_string())] pub(crate) server_profile: String, - /// Build platform: support Web & Desktop [default: "default_platform"] - #[clap(long, value_enum)] - pub(crate) platform: Option, - - /// Build the fullstack variant of this app, using that as the fileserver and backend + /// The target to build for the server. /// - /// This defaults to `false` but will be overridden to true if the `fullstack` feature is enabled. - #[arg(long, default_missing_value="true", num_args=0..=1)] - pub(crate) fullstack: Option, - - /// Run the ssg config of the app and generate the files - #[clap(long)] - pub(crate) ssg: bool, - - /// Skip collecting assets from dependencies [default: false] + /// This can be different than the host allowing cross-compilation of the server. This is useful for + /// platforms like Cloudflare Workers where the server is compiled to wasm and then uploaded to the edge. #[clap(long)] - #[serde(default)] - pub(crate) skip_assets: bool, + pub(crate) server_target: Option, - /// Extra arguments passed to cargo build - #[clap(last = true)] - pub(crate) cargo_args: Vec, - - /// Inject scripts to load the wasm and js files for your dioxus app if they are not already present [default: true] - #[clap(long, default_value_t = true)] - pub(crate) inject_loading_scripts: bool, - - /// Experimental: Bundle split the wasm binary into multiple chunks based on `#[wasm_split]` annotations [default: false] - #[clap(long, default_value_t = false)] - pub(crate) experimental_wasm_split: bool, + /// Arguments for the build itself + #[clap(flatten)] + pub(crate) build_arguments: TargetArgs, - /// Generate debug symbols for the wasm binary [default: true] + /// A list of additional targets to build. /// - /// This will make the binary larger and take longer to compile, but will allow you to debug the - /// wasm binary - #[clap(long, default_value_t = true)] - pub(crate) debug_symbols: bool, + /// Server and Client are special targets that integrate with `dx serve`, while `crate` is a generic. + /// + /// ``` + /// dx serve \ + /// client --target aarch64-apple-darwin \ + /// server --target wasm32-unknown-unknown \ + /// crate --target aarch64-unknown-linux-gnu --package foo \ + /// crate --target x86_64-unknown-linux-gnu --package bar + /// ``` + #[command(subcommand)] + pub(crate) targets: Option, +} - /// Information about the target to build - #[clap(flatten)] - pub(crate) target_args: TargetArgs, +pub struct BuildTargets { + pub client: BuildRequest, + pub server: Option, } impl BuildArgs { - pub async fn run_cmd(mut self) -> Result { + pub async fn build(self) -> Result { tracing::info!("Building project..."); - let krate = - DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?; - - self.resolve(&krate).await?; + let targets = self.into_targets().await?; - let bundle = Builder::start(&krate, self.clone())?.finish().await?; + AppBuilder::start(&targets.client, BuildMode::Base)? + .finish_build() + .await?; - tracing::info!(path = ?bundle.build.root_dir(), "Build completed successfully! 🚀"); - - Ok(StructuredOutput::BuildFinished { - path: bundle.build.root_dir(), - }) - } + tracing::info!(path = ?targets.client.root_dir(), "Client build completed successfully! 🚀"); - /// Update the arguments of the CLI by inspecting the DioxusCrate itself and learning about how - /// the user has configured their app. - /// - /// IE if they've specified "fullstack" as a feature on `dioxus`, then we want to build the - /// fullstack variant even if they omitted the `--fullstack` flag. - pub(crate) async fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> { - let default_platforms = krate.default_platforms(); - let default_platform = default_platforms.iter().find(|p| **p != Platform::Server); - let default_server = default_platforms.iter().any(|p| *p == Platform::Server); - let auto_platform = krate.autodetect_platform(); - - // Make sure we set the fullstack platform so we actually build the fullstack variant - // Users need to enable "fullstack" in their default feature set or explicitly pass the flag - self.fullstack = Some( - self.fullstack() - || self.fullstack.is_none() - && (default_server || krate.has_dioxus_feature("fullstack")), - ); - - // If the current build is a fullstack build which includes either the client or the server in the default features, - // remove that default feature and just add it back into the client or server args. If they passed in an explicit platform - // but they also have a default feature platform, strip out the default features and add back in the platform they passed in. - if self.fullstack() && (default_server || default_platform.is_some()) - || self.platform.is_some() && default_platform.is_some() - { - self.target_args.no_default_features = true; - self.target_args - .features - .extend(krate.platformless_features()); - } + if let Some(server) = targets.server.as_ref() { + // If the server is present, we need to build it as well + AppBuilder::start(server, BuildMode::Base)? + .finish_build() + .await?; - // Inherit the platform from the args, or auto-detect it - if self.platform.is_none() { - let (platform, _feature) = auto_platform.ok_or_else(|| { - anyhow::anyhow!("No platform was specified and could not be auto-detected. Please specify a platform with `--platform ` or set a default platform using a cargo feature.") - })?; - self.platform = Some(platform); + tracing::info!(path = ?targets.client.root_dir(), "Server build completed successfully! 🚀"); } - let platform = self - .platform - .expect("Platform to be set after autodetection"); - - // Add any features required to turn on the client - self.target_args - .client_features - .push(krate.feature_for_platform(platform)); - - // Add any features required to turn on the server - // This won't take effect in the server is not built, so it's fine to just set it here even if it's not used - self.target_args - .server_features - .push(krate.feature_for_platform(Platform::Server)); - - // Make sure we have a server feature if we're building a fullstack app - // - // todo(jon): eventually we want to let users pass a `--server ` flag to specify a package to use as the server - // however, it'll take some time to support that and we don't have a great RPC binding layer between the two yet - if self.fullstack() && self.target_args.server_features.is_empty() { - return Err(anyhow::anyhow!("Fullstack builds require a server feature on the target crate. Add a `server` feature to the crate and try again.").into()); - } + Ok(StructuredOutput::BuildsFinished { + client: targets.client.root_dir(), + server: targets.server.map(|s| s.root_dir()), + }) + } - // Set the profile of the build if it's not already set - // We do this for android/wasm since they require - if self.profile.is_none() && !self.release { - match self.platform { - Some(Platform::Android) => { - self.profile = Some(crate::dioxus_crate::PROFILE_ANDROID.to_string()); - } - Some(Platform::Web) => { - self.profile = Some(crate::dioxus_crate::PROFILE_WASM.to_string()); - } - Some(Platform::Server) => { - self.profile = Some(crate::dioxus_crate::PROFILE_SERVER.to_string()); + pub async fn into_targets(self) -> Result { + let workspace = Workspace::current().await?; + + let mut server = None; + + let client = match self.targets { + // A simple `dx serve` command with no explicit targets + None => { + // Now resolve the builds that we need to. + // These come from the args, but we'd like them to come from the `TargetCmd` chained object + // + // The process here is as follows: + // + // - Create the BuildRequest for the primary target + // - If that BuildRequest is "fullstack", then add the client features + // - If that BuildRequest is "fullstack", then also create a BuildRequest for the server + // with the server features + // + // This involves modifying the BuildRequest to add the client features and server features + // only if we can properly detect that it's a fullstack build. Careful with this, since + // we didn't build BuildRequest to be generally mutable. + let client = BuildRequest::new(&self.build_arguments, workspace.clone()).await?; + let default_server = client + .enabled_platforms + .iter() + .any(|p| *p == Platform::Server); + + // Make sure we set the fullstack platform so we actually build the fullstack variant + // Users need to enable "fullstack" in their default feature set. + // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled + // + // Now we need to resolve the client features + let fullstack = ((default_server || client.fullstack_feature_enabled()) + || self.fullstack.unwrap_or(false)) + && self.fullstack != Some(false); + + if fullstack { + let mut build_args = self.build_arguments.clone(); + build_args.platform = Some(Platform::Server); + + let _server = BuildRequest::new(&build_args, workspace.clone()).await?; + + // ... todo: add the server features to the server build + // ... todo: add the client features to the client build + // // Make sure we have a server feature if we're building a fullstack app + if self.fullstack.unwrap_or_default() && self.server_features.is_empty() { + return Err(anyhow::anyhow!("Fullstack builds require a server feature on the target crate. Add a `server` feature to the crate and try again.").into()); + } + + server = Some(_server); } - _ => {} - } - } - // Determine arch if android - if self.platform == Some(Platform::Android) && self.target_args.arch.is_none() { - tracing::debug!("No android arch provided, attempting to auto detect."); - - let arch = DioxusCrate::autodetect_android_arch().await; + client + } - // Some extra logs - let arch = match arch { - Some(a) => { - tracing::debug!( - "Autodetected `{}` Android arch.", - a.android_target_triplet() - ); - a.to_owned() - } - None => { - let a = Arch::default(); - tracing::debug!( - "Could not detect Android arch, defaulting to `{}`", - a.android_target_triplet() - ); - a + // A command in the form of: + // ``` + // dx serve \ + // client --package frontend \ + // server --package backend + // ``` + Some(cmd) => { + let mut client_args_ = None; + let mut server_args_ = None; + let mut cmd_outer = Some(Box::new(cmd)); + while let Some(cmd) = cmd_outer.take() { + match *cmd { + TargetCmd::Client(cmd_) => { + client_args_ = Some(cmd_.inner); + cmd_outer = cmd_.next; + } + TargetCmd::Server(cmd) => { + server_args_ = Some(cmd.inner); + cmd_outer = cmd.next; + } + } } - }; - - self.target_args.arch = Some(arch); - } - Ok(()) - } + if let Some(server_args) = server_args_ { + server = Some(BuildRequest::new(&server_args, workspace.clone()).await?); + } - /// Get the platform from the build arguments - pub(crate) fn platform(&self) -> Platform { - self.platform.expect("Platform was not set") - } + BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await? + } + }; - /// Check if this is a fullstack build - pub(crate) fn fullstack(&self) -> bool { - self.fullstack.unwrap_or(false) + Ok(BuildTargets { client, server }) } } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index e3939bc308..dd916441c5 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,13 +1,24 @@ -use crate::{AppBundle, BuildArgs, Builder, DioxusCrate, Platform}; +use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, Platform}; use anyhow::{anyhow, Context}; +use dioxus_cli_config::{server_ip, server_port}; +use futures_util::stream::FuturesUnordered; +use futures_util::StreamExt; use path_absolutize::Absolutize; use std::collections::HashMap; +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::Path, + time::Duration, +}; use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; +use tokio::process::Command; use walkdir::WalkDir; use super::*; -/// Bundle the Rust desktop app and all of its assets +/// Bundle an app and its assets. +/// +/// This will produce a client `public` folder and the associated server executable in the output folder. #[derive(Clone, Debug, Parser)] pub struct Bundle { /// The package types to bundle @@ -24,61 +35,76 @@ pub struct Bundle { #[clap(long)] pub out_dir: Option, + /// Build the fullstack variant of this app, using that as the fileserver and backend + /// + /// This defaults to `false` but will be overridden to true if the `fullstack` feature is enabled. + #[clap(long)] + pub(crate) fullstack: bool, + + /// Run the ssg config of the app and generate the files + #[clap(long)] + pub(crate) ssg: bool, + /// The arguments for the dioxus build #[clap(flatten)] - pub(crate) build_arguments: BuildArgs, + pub(crate) args: BuildArgs, } impl Bundle { + // todo: make sure to run pre-render static routes! we removed this from the other bundling step pub(crate) async fn bundle(mut self) -> Result { tracing::info!("Bundling project..."); - let krate = DioxusCrate::new(&self.build_arguments.target_args) - .context("Failed to load Dioxus workspace")?; + let BuildTargets { client, server } = self.args.into_targets().await?; - // We always use `release` mode for bundling - self.build_arguments.release = true; - self.build_arguments.resolve(&krate).await?; + AppBuilder::start(&client, BuildMode::Base)? + .finish_build() + .await?; - tracing::info!("Building app..."); + tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀"); - let bundle = Builder::start(&krate, self.build_arguments.clone())? - .finish() - .await?; + if let Some(server) = server.as_ref() { + // If the server is present, we need to build it as well + AppBuilder::start(server, BuildMode::Base)? + .finish_build() + .await?; + + tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀"); + } // If we're building for iOS, we need to bundle the iOS bundle - if self.build_arguments.platform() == Platform::Ios && self.package_types.is_none() { + if client.platform == Platform::Ios && self.package_types.is_none() { self.package_types = Some(vec![crate::PackageType::IosBundle]); } let mut bundles = vec![]; // Copy the server over if it exists - if bundle.build.build.fullstack() { - bundles.push(bundle.server_exe().unwrap()); + if let Some(server) = server.as_ref() { + bundles.push(server.main_exe()); } // Create a list of bundles that we might need to copy - match self.build_arguments.platform() { + match client.platform { // By default, mac/win/linux work with tauri bundle Platform::MacOS | Platform::Linux | Platform::Windows => { tracing::info!("Running desktop bundler..."); - for bundle in self.bundle_desktop(&krate, &bundle)? { + for bundle in Self::bundle_desktop(&client, &self.package_types)? { bundles.extend(bundle.bundle_paths); } } // Web/ios can just use their root_dir - Platform::Web => bundles.push(bundle.build.root_dir()), + Platform::Web => bundles.push(client.root_dir()), Platform::Ios => { tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing."); - bundles.push(bundle.build.root_dir()) + bundles.push(client.root_dir()) } - Platform::Server => bundles.push(bundle.build.root_dir()), - Platform::Liveview => bundles.push(bundle.build.root_dir()), + Platform::Server => bundles.push(client.root_dir()), + Platform::Liveview => bundles.push(client.root_dir()), Platform::Android => { - let aab = bundle + let aab = client .android_gradle_bundle() .await .context("Failed to run gradle bundleRelease")?; @@ -87,7 +113,7 @@ impl Bundle { }; // Copy the bundles to the output directory if one was specified - let crate_outdir = bundle.build.krate.crate_out_dir(); + let crate_outdir = client.crate_out_dir(); if let Some(outdir) = self.out_dir.clone().or(crate_outdir) { let outdir = outdir .absolutize() @@ -127,35 +153,43 @@ impl Bundle { ); } + // Run SSG and cache static routes + if self.ssg { + if let Some(server) = server.as_ref() { + tracing::info!("Running SSG for static routes..."); + Self::pre_render_static_routes(&server.main_exe()).await?; + tracing::info!("SSG complete"); + } else { + tracing::error!("SSG is only supported for fullstack apps. Ensure you have the server feature enabled and try again."); + } + } + Ok(StructuredOutput::BundleOutput { bundles }) } fn bundle_desktop( - &self, - krate: &DioxusCrate, - bundle: &AppBundle, + build: &BuildRequest, + package_types: &Option>, ) -> Result, Error> { - _ = std::fs::remove_dir_all(krate.bundle_dir(self.build_arguments.platform())); + let krate = &build; + let exe = build.main_exe(); + + _ = std::fs::remove_dir_all(krate.bundle_dir(build.platform)); let package = krate.package(); let mut name: PathBuf = krate.executable_name().into(); if cfg!(windows) { name.set_extension("exe"); } - std::fs::create_dir_all(krate.bundle_dir(self.build_arguments.platform())) + std::fs::create_dir_all(krate.bundle_dir(build.platform)) .context("Failed to create bundle directory")?; - std::fs::copy( - &bundle.app.exe, - krate - .bundle_dir(self.build_arguments.platform()) - .join(&name), - ) - .with_context(|| "Failed to copy the output executable into the bundle directory")?; + std::fs::copy(&exe, krate.bundle_dir(build.platform).join(&name)) + .with_context(|| "Failed to copy the output executable into the bundle directory")?; let binaries = vec![ // We use the name of the exe but it has to be in the same directory BundleBinary::new(krate.executable_name().to_string(), true) - .set_src_path(Some(bundle.app.exe.display().to_string())), + .set_src_path(Some(exe.display().to_string())), ]; let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into(); @@ -186,7 +220,7 @@ impl Bundle { bundle_settings.resources_map = Some(HashMap::new()); } - let asset_dir = bundle.build.asset_dir(); + let asset_dir = build.asset_dir(); if asset_dir.exists() { for entry in WalkDir::new(&asset_dir) { let entry = entry.unwrap(); @@ -218,7 +252,7 @@ impl Bundle { } let mut settings = SettingsBuilder::new() - .project_out_directory(krate.bundle_dir(self.build_arguments.platform())) + .project_out_directory(krate.bundle_dir(build.platform)) .package_settings(PackageSettings { product_name: krate.bundled_app_name(), version: package.version.to_string(), @@ -231,17 +265,11 @@ impl Bundle { .binaries(binaries) .bundle_settings(bundle_settings); - if let Some(packages) = &self.package_types { + if let Some(packages) = &package_types { settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect()); } - if let Some(target) = self.build_arguments.target_args.target.as_ref() { - settings = settings.target(target.to_string()); - } - - if self.build_arguments.platform() == Platform::Ios { - settings = settings.target("aarch64-apple-ios".to_string()); - } + settings = settings.target(build.triple.to_string()); let settings = settings .build() @@ -260,4 +288,121 @@ impl Bundle { Ok(bundles) } + + /// Pre-render the static routes, performing static-site generation + async fn pre_render_static_routes(server_exe: &Path) -> anyhow::Result<()> { + // Use the address passed in through environment variables or default to localhost:9999. We need + // to default to a value that is different than the CLI default address to avoid conflicts + let ip = server_ip().unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + let port = server_port().unwrap_or(9999); + let fullstack_address = SocketAddr::new(ip, port); + let address = fullstack_address.ip().to_string(); + let port = fullstack_address.port().to_string(); + + // Borrow port and address so we can easily moe them into multiple tasks below + let address = &address; + let port = &port; + + tracing::info!("Running SSG at http://{address}:{port} for {server_exe:?}"); + + // Run the server executable + let _child = Command::new(server_exe) + .env(dioxus_cli_config::SERVER_PORT_ENV, port) + .env(dioxus_cli_config::SERVER_IP_ENV, address) + .current_dir(server_exe.parent().unwrap()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .kill_on_drop(true) + .spawn()?; + + // Borrow reqwest_client so we only move the reference into the futures + let reqwest_client = reqwest::Client::new(); + let reqwest_client = &reqwest_client; + + // Get the routes from the `/static_routes` endpoint + let mut routes = None; + + // The server may take a few seconds to start up. Try fetching the route up to 5 times with a one second delay + const RETRY_ATTEMPTS: usize = 5; + for i in 0..=RETRY_ATTEMPTS { + tracing::debug!( + "Attempting to get static routes from server. Attempt {i} of {RETRY_ATTEMPTS}" + ); + + let request = reqwest_client + .post(format!("http://{address}:{port}/api/static_routes")) + .body("{}".to_string()) + .send() + .await; + match request { + Ok(request) => { + routes = Some(request + .json::>() + .await + .inspect(|text| tracing::debug!("Got static routes: {text:?}")) + .context("Failed to parse static routes from the server. Make sure your server function returns Vec with the (default) json encoding")?); + break; + } + Err(err) => { + // If the request fails, try up to 5 times with a one second delay + // If it fails 5 times, return the error + if i == RETRY_ATTEMPTS { + return Err(err).context("Failed to get static routes from server. Make sure you have a server function at the `/api/static_routes` endpoint that returns Vec of static routes."); + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + } + } + + let routes = routes.expect( + "static routes should exist or an error should have been returned on the last attempt", + ); + + // Create a pool of futures that cache each route + let mut resolved_routes = routes + .into_iter() + .map(|route| async move { + tracing::info!("Rendering {route} for SSG"); + + // For each route, ping the server to force it to cache the response for ssg + let request = reqwest_client + .get(format!("http://{address}:{port}{route}")) + .header("Accept", "text/html") + .send() + .await?; + + // If it takes longer than 30 seconds to resolve the route, log a warning + let warning_task = tokio::spawn({ + let route = route.clone(); + async move { + tokio::time::sleep(Duration::from_secs(30)).await; + tracing::warn!("Route {route} has been rendering for 30 seconds"); + } + }); + + // Wait for the streaming response to completely finish before continuing. We don't use the html it returns directly + // because it may contain artifacts of intermediate streaming steps while the page is loading. The SSG app should write + // the final clean HTML to the disk automatically after the request completes. + let _html = request.text().await?; + + // Cancel the warning task if it hasn't already run + warning_task.abort(); + + Ok::<_, reqwest::Error>(route) + }) + .collect::>(); + + while let Some(route) = resolved_routes.next().await { + match route { + Ok(route) => tracing::debug!("ssg success: {route:?}"), + Err(err) => tracing::error!("ssg error: {err:?}"), + } + } + + tracing::info!("SSG complete"); + + drop(_child); + + Ok(()) + } } diff --git a/packages/cli/src/cli/check.rs b/packages/cli/src/cli/check.rs index ea70c74c47..dd48126984 100644 --- a/packages/cli/src/cli/check.rs +++ b/packages/cli/src/cli/check.rs @@ -4,9 +4,11 @@ //! https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs use super::*; -use crate::{metadata::collect_rs_files, DioxusCrate}; +use crate::BuildRequest; use anyhow::Context; use futures_util::{stream::FuturesUnordered, StreamExt}; +use std::path::Path; +use walkdir::WalkDir; /// Check the Rust files in the project for issues. #[derive(Clone, Debug, Parser)] @@ -17,19 +19,28 @@ pub(crate) struct Check { /// Information about the target to check #[clap(flatten)] - pub(crate) target_args: TargetArgs, + pub(crate) build_args: BuildArgs, } impl Check { // Todo: check the entire crate pub(crate) async fn check(self) -> Result { + let BuildTargets { client, server } = self.build_args.into_targets().await?; + match self.file { // Default to checking the project None => { - let dioxus_crate = DioxusCrate::new(&self.target_args)?; - check_project_and_report(dioxus_crate) + check_project_and_report(&client) .await .context("error checking project")?; + + if let Some(server) = server { + if server.package != client.package { + check_project_and_report(&server) + .await + .context("error checking project")?; + } + } } Some(file) => { check_file_and_report(file) @@ -51,9 +62,16 @@ async fn check_file_and_report(path: PathBuf) -> Result<()> { /// Runs using Tokio for multithreading, so it should be really really fast /// /// Doesn't do mod-descending, so it will still try to check unreachable files. TODO. -async fn check_project_and_report(dioxus_crate: DioxusCrate) -> Result<()> { - let mut files_to_check = vec![dioxus_crate.main_source_file()]; - files_to_check.extend(collect_rs_files(dioxus_crate.crate_dir())); +async fn check_project_and_report(build: &BuildRequest) -> Result<()> { + let dioxus_crate = build + .workspace + .find_main_package(Some(build.package.clone()))?; + let dioxus_crate = &build.workspace.krates[dioxus_crate]; + let mut files_to_check = vec![]; + collect_rs_files( + dioxus_crate.manifest_path.parent().unwrap().as_std_path(), + &mut files_to_check, + ); check_files_and_report(files_to_check).await } @@ -105,3 +123,12 @@ async fn check_files_and_report(files_to_check: Vec) -> Result<()> { _ => Err(format!("{} issues found.", total_issues).into()), } } + +pub(crate) fn collect_rs_files(folder: &Path, files: &mut Vec) { + let dir = WalkDir::new(folder).follow_links(true).into_iter(); + for entry in dir.flatten() { + if entry.path().extension() == Some("rs".as_ref()) { + files.push(entry.path().to_path_buf()); + } + } +} diff --git a/packages/cli/src/cli/clean.rs b/packages/cli/src/cli/clean.rs index a0ba1c49cb..f3c86e58b4 100644 --- a/packages/cli/src/cli/clean.rs +++ b/packages/cli/src/cli/clean.rs @@ -12,8 +12,6 @@ impl Clean { pub(crate) async fn clean(self) -> Result { let output = tokio::process::Command::new("cargo") .arg("clean") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) .output() .await?; diff --git a/packages/cli/src/cli/config.rs b/packages/cli/src/cli/config.rs index 1c03ada561..d07f2f6a34 100644 --- a/packages/cli/src/cli/config.rs +++ b/packages/cli/src/cli/config.rs @@ -1,6 +1,5 @@ use super::*; -use crate::TraceSrc; -use crate::{metadata::crate_root, CliSettings}; +use crate::{CliSettings, TraceSrc, Workspace}; /// Dioxus config file controls #[derive(Clone, Debug, Deserialize, Subcommand)] @@ -71,8 +70,8 @@ impl From for bool { } impl Config { - pub(crate) fn config(self) -> Result { - let crate_root = crate_root()?; + pub(crate) async fn config(self) -> Result { + let crate_root = Workspace::crate_root_from_path()?; match self { Config::Init { name, force } => { let conf_path = crate_root.join("Dioxus.toml"); @@ -89,15 +88,13 @@ impl Config { tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Init config file completed."); } Config::FormatPrint {} => { - tracing::info!( - "{:#?}", - crate::dioxus_crate::DioxusCrate::new(&TargetArgs::default())?.config - ); + let workspace = Workspace::current().await?; + tracing::info!("{:#?}", workspace.settings); } Config::CustomHtml {} => { let html_path = crate_root.join("index.html"); let mut file = File::create(html_path)?; - let content = include_str!("../../assets/web/index.html"); + let content = include_str!("../../assets/web/dev.index.html"); file.write_all(content.as_bytes())?; tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done."); } diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index 913e5aa06f..b4cb8532a0 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -58,6 +58,9 @@ impl Create { // If no template is specified, use the default one and set the branch to the latest release. resolve_template_and_branch(&mut self.template, &mut self.branch); + // cargo-generate requires the path to be created first. + std::fs::create_dir_all(&self.path)?; + let args = GenerateArgs { define: self.option, destination: Some(self.path), @@ -77,11 +80,19 @@ impl Create { tag: self.tag, ..Default::default() }, + verbose: crate::logging::VERBOSITY + .get() + .map(|f| f.verbose) + .unwrap_or(false), ..Default::default() }; + restore_cursor_on_sigint(); + tracing::debug!(dx_src = ?TraceSrc::Dev, "Creating new project with args: {args:#?}"); let path = cargo_generate::generate(args)?; + _ = post_create(&path); + Ok(StructuredOutput::Success) } } diff --git a/packages/cli/src/cli/init.rs b/packages/cli/src/cli/init.rs index 46c55ef967..0712947a5c 100644 --- a/packages/cli/src/cli/init.rs +++ b/packages/cli/src/cli/init.rs @@ -55,6 +55,9 @@ impl Init { // If no template is specified, use the default one and set the branch to the latest release. create::resolve_template_and_branch(&mut self.template, &mut self.branch); + // cargo-generate requires the path to be created first. + std::fs::create_dir_all(&self.path)?; + let args = GenerateArgs { define: self.option, destination: Some(self.path), diff --git a/packages/cli/src/cli/link.rs b/packages/cli/src/cli/link.rs index a4711bd337..a32cf6e42d 100644 --- a/packages/cli/src/cli/link.rs +++ b/packages/cli/src/cli/link.rs @@ -1,105 +1,252 @@ -use dioxus_cli_opt::AssetManifest; +use crate::Result; +use anyhow::Context; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use target_lexicon::Triple; -#[derive(Debug, Serialize, Deserialize)] -pub enum LinkAction { - BuildAssetManifest { - destination: PathBuf, - }, - LinkAndroid { - linker: PathBuf, - extra_flags: Vec, - }, +/// `dx` can act as a linker in a few scenarios. Note that we don't *actually* implement the linker logic, +/// instead just proxying to a specified linker (or not linking at all!). +/// +/// This comes in two flavors: +/// -------------------------- +/// - `BaseLink`: We are linking dependencies and want to dynamically select the linker from the environment. +/// This is mostly implemented for Android where the linker is selected in part by the +/// device connected over ADB which can not be determined by .cargo/Config.toml. +/// We implemented this because previous setups like cargo mobile required a hard-coded +/// linker path in your project which does not work in team-based setups. +/// +/// - `NoLink`: We are not linking at all, and instead deferring our linking to the driving process, +/// usually being `dx` itself. In this case, we are just writing the linker args to a file +/// and then outputting a dummy object file to satisfy the linker. This is generally used +/// by the binary patching engine since we need to actually do "real linker logic" like +/// traversing object files and satisfying missing symbols. That process is *much* easier +/// to do in the driving host process when we have all the information available. Unfortunately, +/// rustc doesn't provide a "real" way of granularly stepping through the compile process +/// so this is basically a hack. +/// +/// We use "BaseLink" when a linker is specified, and "NoLink" when it is not. Both generate a resulting +/// object file. + +#[derive(Debug)] +pub struct LinkAction { + pub linker: Option, + pub triple: Triple, + pub link_args_file: PathBuf, + pub link_err_file: PathBuf, +} + +/// The linker flavor to use. This influences the argument style that gets passed to the linker. +/// We're imitating the rustc linker flavors here. +/// +/// https://doc.rust-lang.org/beta/nightly-rustc/rustc_target/spec/enum.LinkerFlavor.html +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum LinkerFlavor { + Gnu, + Darwin, + WasmLld, + Unix, + Msvc, } impl LinkAction { - pub(crate) const ENV_VAR_NAME: &'static str = "dx_magic_link_file"; + const DX_LINK_ARG: &str = "DX_LINK"; + const DX_ARGS_FILE: &str = "DX_LINK_ARGS_FILE"; + const DX_ERR_FILE: &str = "DX_LINK_ERR_FILE"; + const DX_LINK_TRIPLE: &str = "DX_LINK_TRIPLE"; + const DX_LINK_CUSTOM_LINKER: &str = "DX_LINK_CUSTOM_LINKER"; /// Should we write the input arguments to a file (aka act as a linker subprocess)? /// /// Just check if the magic env var is set pub(crate) fn from_env() -> Option { - std::env::var(Self::ENV_VAR_NAME) - .ok() - .map(|var| serde_json::from_str(&var).expect("Failed to parse magic env var")) + if std::env::var(Self::DX_LINK_ARG).is_err() { + return None; + } + + Some(Self { + linker: std::env::var(Self::DX_LINK_CUSTOM_LINKER) + .ok() + .map(PathBuf::from), + link_args_file: std::env::var(Self::DX_ARGS_FILE) + .expect("Linker args file not set") + .into(), + link_err_file: std::env::var(Self::DX_ERR_FILE) + .expect("Linker error file not set") + .into(), + triple: std::env::var(Self::DX_LINK_TRIPLE) + .expect("Linker triple not set") + .parse() + .expect("Failed to parse linker triple"), + }) } - pub(crate) fn to_json(&self) -> String { - serde_json::to_string(self).unwrap() + pub(crate) fn write_env_vars(&self, env_vars: &mut Vec<(&str, String)>) -> Result<()> { + env_vars.push((Self::DX_LINK_ARG, "1".to_string())); + env_vars.push(( + Self::DX_ARGS_FILE, + dunce::canonicalize(&self.link_args_file)? + .to_string_lossy() + .to_string(), + )); + env_vars.push(( + Self::DX_ERR_FILE, + dunce::canonicalize(&self.link_err_file)? + .to_string_lossy() + .to_string(), + )); + env_vars.push((Self::DX_LINK_TRIPLE, self.triple.to_string())); + if let Some(linker) = &self.linker { + env_vars.push(( + Self::DX_LINK_CUSTOM_LINKER, + dunce::canonicalize(linker)?.to_string_lossy().to_string(), + )); + } + + Ok(()) + } + + pub(crate) async fn run_link(self) { + let link_err_file = self.link_err_file.clone(); + let res = self.run_link_inner().await; + + if let Err(err) = res { + // If we failed to run the linker, we need to write the error to the file + // so that the main process can read it. + _ = std::fs::create_dir_all(link_err_file.parent().unwrap()); + _ = std::fs::write(link_err_file, format!("Linker error: {err}")); + } } /// Write the incoming linker args to a file /// /// The file will be given by the dx-magic-link-arg env var itself, so we use /// it both for determining if we should act as a linker and the for the file name itself. - /// - /// This will panic if it fails - /// - /// hmmmmmmmm tbh I'd rather just pass the object files back and do the parsing here, but the interface - /// is nicer to just bounce back the args and let the host do the parsing/canonicalization - pub(crate) fn run(self) { - match self { - // Literally just run the android linker :) - LinkAction::LinkAndroid { - linker, - extra_flags, - } => { - let mut cmd = std::process::Command::new(linker); - cmd.args(std::env::args().skip(1)); - cmd.args(extra_flags); - cmd.stderr(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .status() - .expect("Failed to run android linker"); - } + async fn run_link_inner(self) -> Result<()> { + let mut args: Vec<_> = std::env::args().collect(); + if args.is_empty() { + return Ok(()); + } - // Assemble an asset manifest by walking the object files being passed to us - LinkAction::BuildAssetManifest { destination: dest } => { - let mut args: Vec<_> = std::env::args().collect(); - let mut manifest = AssetManifest::default(); - - // Handle command files, usually a windows thing. - if let Some(command) = args.iter().find(|arg| arg.starts_with('@')).cloned() { - let path = command.trim().trim_start_matches('@'); - let file_binary = std::fs::read(path).unwrap(); - - // This may be a utf-16le file. Let's try utf-8 first. - let content = String::from_utf8(file_binary.clone()).unwrap_or_else(|_| { - // Convert Vec to Vec to convert into a String - let binary_u16le: Vec = file_binary - .chunks_exact(2) - .map(|a| u16::from_le_bytes([a[0], a[1]])) - .collect(); - - String::from_utf16_lossy(&binary_u16le) - }); - - // Gather linker args, and reset the args to be just the linker args - args = content - .lines() - .map(|line| { - let line_parsed = line.to_string(); - let line_parsed = line_parsed.trim_end_matches('"').to_string(); - let line_parsed = line_parsed.trim_start_matches('"').to_string(); - line_parsed - }) - .collect(); - } + handle_linker_command_file(&mut args); - // Parse through linker args for `.o` or `.rlib` files. - for item in args { - if item.ends_with(".o") || item.ends_with(".rlib") { - let path_to_item = PathBuf::from(item); - if let Ok(path) = path_to_item.canonicalize() { - _ = manifest.add_from_object_path(&path); - } - } + // Write the linker args to a file for the main process to read + // todo: we might need to encode these as escaped shell words in case newlines are passed + std::fs::write(self.link_args_file, args.join("\n"))?; + + // If there's a linker specified, we use that. Otherwise, we write a dummy object file to satisfy + // any post-processing steps that rustc does. + match self.linker { + Some(linker) => { + let res = std::process::Command::new(linker) + .args(args.iter().skip(1)) + .output() + .expect("Failed to run linker"); + + if !res.stderr.is_empty() || !res.stdout.is_empty() { + _ = std::fs::create_dir_all(self.link_err_file.parent().unwrap()); + _ = std::fs::write( + self.link_err_file, + format!( + "Linker error: {}\n{}", + String::from_utf8_lossy(&res.stdout), + String::from_utf8_lossy(&res.stderr) + ), + ); } + } + None => { + // Extract the out path - we're going to write a dummy object file to satisfy the linker + let out_file: PathBuf = match self.triple.operating_system { + target_lexicon::OperatingSystem::Windows => { + let out_arg = args.iter().find(|arg| arg.starts_with("/OUT")).unwrap(); + out_arg.trim_start_matches("/OUT:").to_string().into() + } + _ => { + let out = args.iter().position(|arg| arg == "-o").unwrap(); + args[out + 1].clone().into() + } + }; + + // This creates an object file that satisfies rust's use of llvm-objcopy + // + // I'd rather we *not* do this and instead generate a truly linked file (and then delete it) but + // this at least lets us delay linking until the host compiler is ready. + // + // This is because our host compiler is a stateful server and not a stateless linker. + // + // todo(jon): do we use Triple::host or the target triple? I think I ran into issues + // using the target triple, hence the use of "host" but it might not even matter? + let triple = Triple::host(); + let format = match triple.binary_format { + target_lexicon::BinaryFormat::Elf => object::BinaryFormat::Elf, + target_lexicon::BinaryFormat::Coff => object::BinaryFormat::Coff, + target_lexicon::BinaryFormat::Macho => object::BinaryFormat::MachO, + target_lexicon::BinaryFormat::Wasm => object::BinaryFormat::Wasm, + target_lexicon::BinaryFormat::Xcoff => object::BinaryFormat::Xcoff, + target_lexicon::BinaryFormat::Unknown => todo!(), + _ => todo!("Binary format not supported"), + }; + + let arch = match triple.architecture { + target_lexicon::Architecture::Wasm32 => object::Architecture::Wasm32, + target_lexicon::Architecture::Wasm64 => object::Architecture::Wasm64, + target_lexicon::Architecture::X86_64 => object::Architecture::X86_64, + target_lexicon::Architecture::Arm(_) => object::Architecture::Arm, + target_lexicon::Architecture::Aarch64(_) => object::Architecture::Aarch64, + target_lexicon::Architecture::LoongArch64 => object::Architecture::LoongArch64, + target_lexicon::Architecture::Unknown => object::Architecture::Unknown, + _ => todo!("Architecture not supported"), + }; + + let endian = match triple.endianness() { + Ok(target_lexicon::Endianness::Little) => object::Endianness::Little, + Ok(target_lexicon::Endianness::Big) => object::Endianness::Big, + Err(_) => todo!("Endianness not supported"), + }; - let contents = serde_json::to_string(&manifest).expect("Failed to write manifest"); - std::fs::write(dest, contents).expect("Failed to write output file"); + let bytes = object::write::Object::new(format, arch, endian) + .write() + .context("Failed to emit stub link file")?; + + // Write a dummy object file to satisfy rust/linker since it'll run llvm-objcopy + // ... I wish it *didn't* do that but I can't tell how to disable the linker without + // using --emit=obj which is not exactly what we want since that will still pull in + // the dependencies. + std::fs::create_dir_all(out_file.parent().unwrap())?; + std::fs::write(out_file, bytes)?; } } + + Ok(()) + } +} + +pub fn handle_linker_command_file(args: &mut Vec) { + // Handle command files, usually a windows thing. + if let Some(command) = args.iter().find(|arg| arg.starts_with('@')).cloned() { + let path = command.trim().trim_start_matches('@'); + let file_binary = std::fs::read(path).unwrap(); + + // This may be a utf-16le file. Let's try utf-8 first. + let content = String::from_utf8(file_binary.clone()).unwrap_or_else(|_| { + // Convert Vec to Vec to convert into a String + let binary_u16le: Vec = file_binary + .chunks_exact(2) + .map(|a| u16::from_le_bytes([a[0], a[1]])) + .collect(); + + String::from_utf16_lossy(&binary_u16le) + }); + + // Gather linker args, and reset the args to be just the linker args + *args = content + .lines() + .map(|line| { + let line_parsed = line.trim().to_string(); + let line_parsed = line_parsed.trim_end_matches('"').to_string(); + let line_parsed = line_parsed.trim_start_matches('"').to_string(); + line_parsed + }) + .collect(); } } diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 34b88ce2ca..04f592669a 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -20,7 +20,6 @@ pub(crate) use target::*; pub(crate) use verbosity::*; use crate::{error::Result, Error, StructuredOutput}; -use anyhow::Context; use clap::{Parser, Subcommand}; use html_parser::Dom; use once_cell::sync::Lazy; @@ -30,7 +29,7 @@ use std::{ fs::File, io::{Read, Write}, path::PathBuf, - process::{Command, Stdio}, + process::Command, }; /// Build, Bundle & Ship Dioxus Apps. @@ -93,7 +92,7 @@ pub(crate) enum Commands { Config(config::Config), /// Build the assets for a specific target. - #[clap(name = "build_assets")] + #[clap(name = "assets")] BuildAssets(build_assets::BuildAssets), } diff --git a/packages/cli/src/cli/run.rs b/packages/cli/src/cli/run.rs index 79f303279a..5bf61c0438 100644 --- a/packages/cli/src/cli/run.rs +++ b/packages/cli/src/cli/run.rs @@ -1,67 +1,131 @@ use super::*; -use crate::{serve::ServeUpdate, BuildArgs, Builder, DioxusCrate, Platform, Result}; +use crate::{ + serve::{AppServer, ServeUpdate, WebServer}, + BuilderUpdate, Platform, Result, +}; +use dioxus_dx_wire_format::BuildStage; /// Run the project with the given arguments +/// +/// This is a shorthand for `dx serve` with interactive mode and hot-reload disabled. #[derive(Clone, Debug, Parser)] pub(crate) struct RunArgs { /// Information about the target to build #[clap(flatten)] - pub(crate) build_args: BuildArgs, + pub(crate) args: ServeArgs, } impl RunArgs { pub(crate) async fn run(mut self) -> Result { - let krate = DioxusCrate::new(&self.build_args.target_args) - .context("Failed to load Dioxus workspace")?; + // Override the build arguments, leveraging our serve infrastructure. + // + // We want to turn off the fancy stuff like the TUI, watcher, and hot-reload, but leave logging + // and other things like the devserver on. + self.args.hot_patch = false; + self.args.interactive = Some(false); + self.args.hot_reload = Some(false); + self.args.watch = Some(false); - self.build_args.resolve(&krate).await?; + let mut builder = AppServer::start(self.args).await?; + let mut devserver = WebServer::start(&builder)?; - tracing::trace!("Building crate krate data: {:#?}", krate); - tracing::trace!("Build args: {:#?}", self.build_args); + loop { + let msg = tokio::select! { + msg = builder.wait() => msg, + msg = devserver.wait() => msg, + }; - let bundle = Builder::start(&krate, self.build_args.clone())? - .finish() - .await?; + match msg { + // Wait for logs from the build engine + // These will cause us to update the screen + // We also can check the status of the builds here in case we have multiple ongoing builds + ServeUpdate::BuilderUpdate { id, update } => { + let platform = builder.get_build(id).unwrap().build.platform; - let devserver_ip = "127.0.0.1:8081".parse().unwrap(); - let fullstack_ip = "127.0.0.1:8080".parse().unwrap(); - let mut open_address = None; + // And then update the websocketed clients with the new build status in case they want it + devserver.new_build_update(&update).await; - if self.build_args.platform() == Platform::Web || self.build_args.fullstack() { - open_address = Some(fullstack_ip); - tracing::info!("Serving at: {}", fullstack_ip); - } + // And then open the app if it's ready + match update { + BuilderUpdate::BuildReady { bundle } => { + _ = builder + .open(bundle, &mut devserver) + .await + .inspect_err(|e| tracing::error!("Failed to open app: {}", e)); - let mut runner = crate::serve::AppRunner::start(&krate); - runner - .open(bundle, devserver_ip, open_address, Some(fullstack_ip), true) - .await?; + if platform == Platform::Web { + tracing::info!( + "Serving app at http://{}:{}", + builder.devserver_bind_ip, + builder.devserver_port + ); + } + } + BuilderUpdate::Progress { stage } => match stage { + BuildStage::Initializing => { + tracing::info!("[{platform}] Initializing build") + } + BuildStage::Starting { .. } => {} + BuildStage::InstallingTooling => {} + BuildStage::Compiling { + current, + total, + krate, + } => { + tracing::info!("[{platform}] Compiling {krate} ({current}/{total})",) + } + BuildStage::RunningBindgen => { + tracing::info!("[{platform}] Running WASM bindgen") + } + BuildStage::SplittingBundle => {} + BuildStage::OptimizingWasm => { + tracing::info!("[{platform}] Optimizing WASM with `wasm-opt`") + } + BuildStage::Linking => tracing::info!("Linking app"), + BuildStage::Hotpatching => todo!(), + BuildStage::CopyingAssets { + current, + total, + path, + } => tracing::info!( + "[{platform}] Copying asset {} ({current}/{total})", + path.display(), + ), + BuildStage::Bundling => tracing::info!("[{platform}] Bundling app"), + BuildStage::RunningGradle => { + tracing::info!("[{platform}] Running Gradle") + } + BuildStage::Success => {} + BuildStage::Failed => {} + BuildStage::Aborted => {} + BuildStage::Restarting => {} + BuildStage::CompressingAssets => {} + _ => {} + }, + BuilderUpdate::CompilerMessage { message } => { + print!("{}", message); + } + BuilderUpdate::BuildFailed { err } => { + tracing::error!("Build failed: {:#?}", err); + } + BuilderUpdate::StdoutReceived { msg } => { + tracing::info!("[{platform}] {msg}"); + } + BuilderUpdate::StderrReceived { msg } => { + tracing::error!("[{platform}] {msg}"); + } + BuilderUpdate::ProcessExited { status } => { + if !status.success() { + tracing::error!( + "Application [{platform}] exited with error: {status}" + ); + } - // Run the app, but mostly ignore all the other messages - // They won't generally be emitted - loop { - match runner.wait().await { - ServeUpdate::StderrReceived { platform, msg } => { - tracing::info!("[{platform}]: {msg}") - } - ServeUpdate::StdoutReceived { platform, msg } => { - tracing::info!("[{platform}]: {msg}") - } - ServeUpdate::ProcessExited { platform, status } => { - runner.cleanup().await; - tracing::info!("[{platform}]: process exited with status: {status:?}"); - break; + break; + } + } } - ServeUpdate::BuildUpdate { .. } => {} - ServeUpdate::TracingLog { .. } => {} - ServeUpdate::Exit { .. } => break, - ServeUpdate::NewConnection => {} - ServeUpdate::WsMessage(_) => {} - ServeUpdate::FilesChanged { .. } => {} - ServeUpdate::RequestRebuild => {} - ServeUpdate::Redraw => {} - ServeUpdate::OpenApp => {} - ServeUpdate::ToggleShouldRebuild => {} + _ => {} } } diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index 5cb756de94..ebd1490b64 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -1,7 +1,36 @@ use super::*; -use crate::{AddressArguments, BuildArgs, DioxusCrate, Platform}; +use crate::{AddressArguments, BuildArgs, TraceController}; +use futures_util::FutureExt; +use once_cell::sync::OnceCell; +use std::{backtrace::Backtrace, panic::AssertUnwindSafe}; /// Serve the project +/// +/// `dx serve` takes cargo args by default, except with a required `--platform` arg: +/// +/// ``` +/// dx serve --example blah --target blah --platform android +/// ``` +/// +/// A simple serve: +/// ``` +/// dx serve --platform web +/// ``` +/// +/// A serve with customized arguments: +/// +/// ``` +/// ``` +/// +/// As of dioxus 0.7, `dx serve` allows independent customization of the client and server builds, +/// allowing workspaces and removing any "magic" done to support ergonomic fullstack serving with +/// an plain `dx serve`. These require specifying more arguments like features since they won't be autodetected. +/// +/// ``` +/// dx serve \ +/// client --package frontend \ +/// server --package backend +/// ``` #[derive(Clone, Debug, Default, Parser)] #[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))] pub(crate) struct ServeArgs { @@ -38,9 +67,22 @@ pub(crate) struct ServeArgs { #[arg(long, default_missing_value="true", num_args=0..=1, short = 'i')] pub(crate) interactive: Option, - /// Arguments for the build itself + /// Enable Rust hot-patching instead of full rebuilds [default: false] + /// + /// This is quite experimental and may lead to unexpected segfaults or crashes in development. + #[arg(long, default_value_t = false, alias = "hotpatch")] + pub(crate) hot_patch: bool, + + /// Watch the filesystem for changes and trigger a rebuild [default: true] + #[clap(long, default_missing_value = "true")] + pub(crate) watch: Option, + + /// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false] + #[clap(long)] + pub(crate) force_sequential: bool, + #[clap(flatten)] - pub(crate) build_arguments: BuildArgs, + pub(crate) targets: BuildArgs, } impl ServeArgs { @@ -48,71 +90,98 @@ impl ServeArgs { /// /// Make sure not to do any intermediate logging since our tracing infra has now enabled much /// higher log levels + /// + /// We also set up proper panic handling since the TUI has a tendency to corrupt the terminal. pub(crate) async fn serve(self) -> Result { - crate::serve::serve_all(self).await?; - - Ok(StructuredOutput::Success) - } - - pub(crate) async fn load_krate(&mut self) -> Result { - let krate = DioxusCrate::new(&self.build_arguments.target_args)?; - self.resolve(&krate).await?; - Ok(krate) - } - - pub(crate) async fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> { - // Enable hot reload. - if self.hot_reload.is_none() { - self.hot_reload = Some(krate.settings.always_hot_reload.unwrap_or(true)); + if std::env::var("RUST_BACKTRACE").is_err() { + std::env::set_var("RUST_BACKTRACE", "1"); } - // Open browser. - if self.open.is_none() { - self.open = Some(krate.settings.always_open_browser.unwrap_or_default()); + struct SavedLocation { + file: String, + line: u32, + column: u32, } - - // Set WSL file poll interval. - if self.wsl_file_poll_interval.is_none() { - self.wsl_file_poll_interval = Some(krate.settings.wsl_file_poll_interval.unwrap_or(2)); + static BACKTRACE: OnceCell<(Backtrace, Option)> = OnceCell::new(); + + // We *don't* want printing here, since it'll break the tui and log ordering. + // + // We *will* re-emit the panic after we've drained the tracer, so our panic hook will simply capture the panic + // and save it. + std::panic::set_hook(Box::new(move |panic_info| { + _ = BACKTRACE.set(( + Backtrace::capture(), + panic_info.location().map(|l| SavedLocation { + file: l.file().to_string(), + line: l.line(), + column: l.column(), + }), + )); + })); + + let interactive = self.is_interactive_tty(); + + // Redirect all logging the cli logger - if there's any pending after a panic, we flush it + let mut tracer = TraceController::redirect(interactive); + + let res = AssertUnwindSafe(crate::serve::serve_all(self, &mut tracer)) + .catch_unwind() + .await; + + // Kill the screen so we don't ruin the terminal + _ = crate::serve::Output::remote_shutdown(interactive); + + // And drain the tracer as regular messages. All messages will be logged (including traces) + // and then we can print the panic message + if !matches!(res, Ok(Ok(_))) { + tracer.shutdown_panic(); } - // Set always-on-top for desktop. - if self.always_on_top.is_none() { - self.always_on_top = Some(krate.settings.always_on_top.unwrap_or(true)) + match res { + Ok(Ok(_res)) => Ok(StructuredOutput::Success), + Ok(Err(e)) => Err(e), + Err(panic_err) => { + // And then print the panic itself. + let as_str = if let Some(p) = panic_err.downcast_ref::() { + p.as_ref() + } else if let Some(p) = panic_err.downcast_ref::<&str>() { + p + } else { + "" + }; + + // Attempt to emulate the default panic hook + let message = BACKTRACE + .get() + .map(|(back, location)| { + let location_display = location + .as_ref() + .map(|l| format!("{}:{}:{}", l.file, l.line, l.column)) + .unwrap_or_else(|| "".to_string()); + + let mut backtrace_display = back.to_string(); + + // split at the line that ends with ___rust_try for short backtraces + if std::env::var("RUST_BACKTRACE") == Ok("1".to_string()) { + backtrace_display = backtrace_display + .split(" ___rust_try\n") + .next() + .map(|f| format!("{f} ___rust_try")) + .unwrap_or_default(); + } + + format!("dx serve panicked at {location_display}\n{as_str}\n{backtrace_display} ___rust_try") + }) + .unwrap_or_else(|| format!("dx serve panicked: {as_str}")); + + Err(crate::error::Error::CapturedPanic(message)) + } } - - // Resolve the build arguments - self.build_arguments.resolve(krate).await?; - - Ok(()) - } - - pub(crate) fn should_hotreload(&self) -> bool { - self.hot_reload.unwrap_or(true) - } - - pub(crate) fn build_args(&self) -> BuildArgs { - self.build_arguments.clone() } + /// Check if the server is running in interactive mode. This involves checking the terminal as well pub(crate) fn is_interactive_tty(&self) -> bool { use std::io::IsTerminal; std::io::stdout().is_terminal() && self.interactive.unwrap_or(true) } - - pub(crate) fn should_proxy_build(&self) -> bool { - match self.build_arguments.platform() { - Platform::Server => true, - // During SSG, just serve the static files instead of running the server - _ => self.build_arguments.fullstack() && !self.build_arguments.ssg, - } - } -} - -impl std::ops::Deref for ServeArgs { - type Target = BuildArgs; - - fn deref(&self) -> &Self::Target { - &self.build_arguments - } } diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs index 57914f8b43..abfe382c51 100644 --- a/packages/cli/src/cli/target.rs +++ b/packages/cli/src/cli/target.rs @@ -1,235 +1,167 @@ -use super::*; -use std::path::Path; +use crate::cli::*; +use crate::Platform; +use clap::{ArgMatches, Args, FromArgMatches, Subcommand}; +use target_lexicon::Triple; -/// Information about the target to build +/// A single target to build for #[derive(Clone, Debug, Default, Deserialize, Parser)] pub(crate) struct TargetArgs { - /// Build for nightly [default: false] - #[clap(long)] - pub(crate) nightly: bool, - - /// Build a example [default: ""] - #[clap(long)] - pub(crate) example: Option, + /// Build platform: support Web & Desktop [default: "default_platform"] + #[clap(long, value_enum)] + pub(crate) platform: Option, - /// Build a binary [default: ""] - #[clap(long)] - pub(crate) bin: Option, + /// Build in release mode [default: false] + #[clap(long, short)] + #[serde(default)] + pub(crate) release: bool, /// The package to build #[clap(short, long)] pub(crate) package: Option, - /// Space separated list of features to activate + /// Build a specific binary [default: ""] #[clap(long)] - pub(crate) features: Vec, + pub(crate) bin: Option, - /// The feature to use for the client in a fullstack app [default: "web"] + /// Build a specific example [default: ""] #[clap(long)] - pub(crate) client_features: Vec, + pub(crate) example: Option, - /// The feature to use for the server in a fullstack app [default: "server"] + /// Build the app with custom a profile + #[clap(long)] + pub(crate) profile: Option, + + /// Space separated list of features to activate #[clap(long)] - pub(crate) server_features: Vec, + pub(crate) features: Vec, /// Don't include the default features in the build #[clap(long)] pub(crate) no_default_features: bool, - /// The architecture to build for. - #[clap(long, value_enum)] - pub(crate) arch: Option, - - /// Are we building for a device or just the simulator. - /// If device is false, then we'll build for the simulator + /// Include all features in the build #[clap(long)] - pub(crate) device: Option, + pub(crate) all_features: bool, /// Rustc platform triple #[clap(long)] - pub(crate) target: Option, - - /// Specify the server rustc target triple. + pub(crate) target: Option, + + /// Extra arguments passed to `cargo` + /// + /// To see a list of args, run `cargo rustc --help` + /// + /// This can include stuff like, "--locked", "--frozen", etc. Note that `dx` sets many of these + /// args directly from other args in this command. #[clap(long)] - pub(crate) server_target: Option, -} - -impl TargetArgs { - pub(crate) fn arch(&self) -> Arch { - self.arch.unwrap_or_default() - } -} - -#[derive(Debug, Default, Copy, Clone, PartialEq, Deserialize, clap::ValueEnum)] -#[non_exhaustive] -pub(crate) enum Arch { - // Android: armv7l, armv7-linux-androideabi - Arm, - // Android: aarch64, aarch64-linux-android - #[default] - Arm64, - // Android: i386, i686-linux-android - X86, - // Android: x86_64, x86_64-linux-android - X64, -} - -impl Arch { - pub(crate) fn android_target_triplet(&self) -> &'static str { - match self { - Arch::Arm => "armv7-linux-androideabi", - Arch::Arm64 => "aarch64-linux-android", - Arch::X86 => "i686-linux-android", - Arch::X64 => "x86_64-linux-android", - } - } - - pub(crate) fn android_jnilib(&self) -> &'static str { - match self { - Arch::Arm => "armeabi-v7a", - Arch::Arm64 => "arm64-v8a", - Arch::X86 => "x86", - Arch::X64 => "x86_64", - } - } - - pub(crate) fn android_clang_triplet(&self) -> &'static str { - match self { - Self::Arm => "armv7a-linux-androideabi", - _ => self.android_target_triplet(), - } - } - - pub(crate) fn android_tools_dir(&self, ndk: &Path) -> PathBuf { - let prebuilt = ndk.join("toolchains").join("llvm").join("prebuilt"); - - if cfg!(target_os = "macos") { - // for whatever reason, even on aarch64 macos, the linker is under darwin-x86_64 - return prebuilt.join("darwin-x86_64").join("bin"); - } + pub(crate) cargo_args: Option, + + /// Extra arguments passed to `rustc`. This can be used to customize the linker, or other flags. + /// + /// For example, specifign `dx build --rustc-args "-Clink-arg=-Wl,-blah"` will pass "-Clink-arg=-Wl,-blah" + /// to the underlying the `cargo rustc` command: + /// + /// cargo rustc -- -Clink-arg=-Wl,-blah + /// + #[clap(long)] + pub(crate) rustc_args: Option, - if cfg!(target_os = "linux") { - return prebuilt.join("linux-x86_64").join("bin"); - } + /// Skip collecting assets from dependencies [default: false] + #[clap(long)] + #[serde(default)] + pub(crate) skip_assets: bool, - if cfg!(target_os = "windows") { - return prebuilt.join("windows-x86_64").join("bin"); - } + /// Inject scripts to load the wasm and js files for your dioxus app if they are not already present [default: true] + #[clap(long, default_value_t = true)] + pub(crate) inject_loading_scripts: bool, - unimplemented!("Unsupported target os for android toolchain autodetection") - } + /// Experimental: Bundle split the wasm binary into multiple chunks based on `#[wasm_split]` annotations [default: false] + #[clap(long, default_value_t = false)] + pub(crate) wasm_split: bool, - pub(crate) fn android_linker(&self, ndk: &Path) -> PathBuf { - // "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang" - let triplet = self.android_clang_triplet(); - let suffix = if cfg!(target_os = "windows") { - ".cmd" - } else { - "" - }; + /// Generate debug symbols for the wasm binary [default: true] + /// + /// This will make the binary larger and take longer to compile, but will allow you to debug the + /// wasm binary + #[clap(long, default_value_t = true)] + pub(crate) debug_symbols: bool, - self.android_tools_dir(ndk) - .join(format!("{}24-clang{}", triplet, suffix)) - } + /// Are we building for a device or just the simulator. + /// If device is false, then we'll build for the simulator + #[clap(long)] + pub(crate) device: Option, +} - pub(crate) fn android_min_sdk_version(&self) -> u32 { - // todo(jon): this should be configurable - 24 - } +/// Chain together multiple target commands +#[derive(Debug, Subcommand, Clone)] +#[command(subcommand_precedence_over_arg = true)] +pub(crate) enum TargetCmd { + /// Specify the arguments for the client build + #[clap(name = "client")] + Client(ChainedCommand), + + /// Specify the arguments for the server build + #[clap(name = "server")] + Server(ChainedCommand), +} - pub(crate) fn android_ar_path(&self, ndk: &Path) -> PathBuf { - self.android_tools_dir(ndk).join("llvm-ar") - } +// https://github.com/clap-rs/clap/issues/2222#issuecomment-2524152894 +// +// +/// `[Args]` wrapper to match `T` variants recursively in `U`. +#[derive(Debug, Clone)] +pub struct ChainedCommand { + /// Specific Variant. + pub inner: T, + + /// Enum containing `Self` variants, in other words possible follow-up commands. + pub next: Option>, +} - pub(crate) fn target_cc(&self, ndk: &Path) -> PathBuf { - self.android_tools_dir(ndk).join("clang") +impl Args for ChainedCommand +where + T: Args, + U: Subcommand, +{ + fn augment_args(cmd: clap::Command) -> clap::Command { + // We use the special `defer` method which lets us recursively call `augment_args` on the inner command + // and thus `from_arg_matches` + T::augment_args(cmd).defer(|cmd| U::augment_subcommands(cmd.disable_help_subcommand(true))) } - pub(crate) fn target_cxx(&self, ndk: &Path) -> PathBuf { - self.android_tools_dir(ndk).join("clang++") + fn augment_args_for_update(_cmd: clap::Command) -> clap::Command { + unimplemented!() } +} - pub(crate) fn java_home(&self) -> Option { - // wrap in a lazy so we don't accidentally keep probing for java home and potentially thrash env vars - once_cell::sync::Lazy::new(|| { - // https://stackoverflow.com/questions/71381050/java-home-is-set-to-an-invalid-directory-android-studio-flutter - // always respect the user's JAVA_HOME env var above all other options +impl FromArgMatches for ChainedCommand +where + T: Args, + U: Subcommand, +{ + fn from_arg_matches(matches: &ArgMatches) -> Result { + // Parse the first command before we try to parse the next one. + let inner = T::from_arg_matches(matches)?; + + // Try to parse the remainder of the command as a subcommand. + let next = match matches.subcommand() { + // Subcommand skips into the matched .subcommand, hence we need to pass *outer* matches, ignoring the inner matches + // (which in the average case should only match enumerated T) // - // we only attempt autodetection if java_home is not set + // Here, we might want to eventually enable arbitrary names of subcommands if they're prefixed + // with a prefix like "@" ie `dx serve @dog-app/backend --args @dog-app/frontend --args` // - // this is a better fallback than falling onto the users' system java home since many users might - // not even know which java that is - they just know they have android studio installed - if let Some(java_home) = std::env::var_os("JAVA_HOME") { - return Some(PathBuf::from(java_home)); - } - - // Attempt to autodetect java home from the android studio path or jdk path on macos - #[cfg(target_os = "macos")] - { - let jbr_home = - PathBuf::from("/Applications/Android Studio.app/Contents/jbr/Contents/Home/"); - if jbr_home.exists() { - return Some(jbr_home); - } - - let jre_home = - PathBuf::from("/Applications/Android Studio.app/Contents/jre/Contents/Home"); - if jre_home.exists() { - return Some(jre_home); - } - - let jdk_home = - PathBuf::from("/Library/Java/JavaVirtualMachines/openjdk.jdk/Contents/Home/"); - if jdk_home.exists() { - return Some(jdk_home); - } - } - - #[cfg(target_os = "windows")] - { - let jbr_home = PathBuf::from("C:\\Program Files\\Android\\Android Studio\\jbr"); - if jbr_home.exists() { - return Some(jbr_home); - } - } - - // todo(jon): how do we detect java home on linux? - #[cfg(target_os = "linux")] - { - let jbr_home = PathBuf::from("/usr/lib/jvm/java-11-openjdk-amd64"); - if jbr_home.exists() { - return Some(jbr_home); - } - } - - None - }) - .clone() - } -} + // we are done, since sub-sub commands are matched in U:: + Some(_) => Some(Box::new(U::from_arg_matches(matches)?)), -impl TryFrom for Arch { - type Error = (); - - fn try_from(value: String) -> Result { - match value.as_str() { - "armv7l" => Ok(Self::Arm), - "aarch64" => Ok(Self::Arm64), - "i386" => Ok(Self::X86), - "x86_64" => Ok(Self::X64), - _ => Err(()), - } + // no subcommand matched, we are done + None => None, + }; + + Ok(Self { inner, next }) } -} -impl std::fmt::Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Arch::Arm => "armv7l", - Arch::Arm64 => "aarch64", - Arch::X86 => "i386", - Arch::X64 => "x86_64", - } - .fmt(f) + fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> { + unimplemented!() } } diff --git a/packages/cli/src/config/app.rs b/packages/cli/src/config/app.rs index 02f1874300..77fdcb9146 100644 --- a/packages/cli/src/config/app.rs +++ b/packages/cli/src/config/app.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ApplicationConfig { - #[serde(default)] pub(crate) asset_dir: Option, #[serde(default)] diff --git a/packages/cli/src/config/dioxus_config.rs b/packages/cli/src/config/dioxus_config.rs index 4c03708e87..80467c3b53 100644 --- a/packages/cli/src/config/dioxus_config.rs +++ b/packages/cli/src/config/dioxus_config.rs @@ -1,7 +1,4 @@ use super::*; -use crate::Result; -use anyhow::Context; -use krates::{Krates, NodeId}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -55,51 +52,3 @@ impl Default for DioxusConfig { } } } - -impl DioxusConfig { - pub fn load(krates: &Krates, package: NodeId) -> Result> { - // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml - let mut current_dir = krates[package] - .manifest_path - .parent() - .unwrap() - .as_std_path() - .to_path_buf() - .canonicalize()?; - - let workspace_path = krates - .workspace_root() - .as_std_path() - .to_path_buf() - .canonicalize()?; - - let mut dioxus_conf_file = None; - while current_dir.starts_with(&workspace_path) { - let config = ["Dioxus.toml", "dioxus.toml"] - .into_iter() - .map(|file| current_dir.join(file)) - .find(|path| path.is_file()); - - // Try to find Dioxus.toml in the current directory - if let Some(new_config) = config { - dioxus_conf_file = Some(new_config.as_path().to_path_buf()); - break; - } - // If we can't find it, go up a directory - current_dir = current_dir - .parent() - .context("Failed to find Dioxus.toml")? - .to_path_buf(); - } - - let Some(dioxus_conf_file) = dioxus_conf_file else { - return Ok(None); - }; - - toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) - .map_err(|err| { - anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into() - }) - .map(Some) - } -} diff --git a/packages/cli/src/config.rs b/packages/cli/src/config/mod.rs similarity index 100% rename from packages/cli/src/config.rs rename to packages/cli/src/config/mod.rs diff --git a/packages/cli/src/config/serve.rs b/packages/cli/src/config/serve.rs index 22f40a67b0..0a9917d16a 100644 --- a/packages/cli/src/config/serve.rs +++ b/packages/cli/src/config/serve.rs @@ -1,7 +1,4 @@ -#![allow(unused)] // lots of configs... - use clap::Parser; -use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; /// The arguments for the address the server will run on #[derive(Clone, Debug, Default, Parser)] diff --git a/packages/cli/src/devcfg.rs b/packages/cli/src/devcfg.rs new file mode 100644 index 0000000000..c248ca908f --- /dev/null +++ b/packages/cli/src/devcfg.rs @@ -0,0 +1,30 @@ +//! Configuration of the CLI at runtime to enable certain experimental features. + +use std::path::Path; + +/// Should we cache the dependency library? +/// +/// When the `DIOXUS_CACHE_DEP_LIB` environment variable is set, we will cache the dependency library +/// built from the target's dependencies. +pub(crate) fn should_cache_dep_lib(lib: &Path) -> bool { + std::env::var("DIOXUS_CACHE_DEP_LIB").is_ok() && lib.exists() +} + +/// Should we force the entropy to be used on the main exe? +/// +/// This is used to verify that binaries are copied with different names such that they don't collide +/// and should generally be only enabled on certain platforms that require it. +pub(crate) fn should_force_entropy() -> bool { + std::env::var("DIOXUS_FORCE_ENTRY").is_ok() +} + +/// Should the CLI not download any additional tools? +pub(crate) fn no_downloads() -> bool { + std::env::var("NO_DOWNLOADS").is_ok() +} + +/// Should we test the installs? +#[allow(dead_code)] // -> used in tests only +pub(crate) fn test_installs() -> bool { + std::env::var("TEST_INSTALLS").is_ok() +} diff --git a/packages/cli/src/dioxus_crate.rs b/packages/cli/src/dioxus_crate.rs deleted file mode 100644 index 55222fbc62..0000000000 --- a/packages/cli/src/dioxus_crate.rs +++ /dev/null @@ -1,855 +0,0 @@ -use crate::{config::DioxusConfig, TargetArgs}; -use crate::{Arch, CliSettings}; -use crate::{Platform, Result}; -use anyhow::Context; -use itertools::Itertools; -use krates::{cm::Target, KrateDetails}; -use krates::{cm::TargetKind, Cmd, Krates, NodeId}; -use once_cell::sync::OnceCell; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::process::Command; -use toml_edit::Item; - -// Contains information about the crate we are currently in and the dioxus config for that crate -#[derive(Clone)] -pub(crate) struct DioxusCrate { - pub(crate) krates: Arc, - pub(crate) package: NodeId, - pub(crate) config: DioxusConfig, - pub(crate) target: Target, - pub(crate) settings: Arc, -} - -pub(crate) static PROFILE_WASM: &str = "wasm-dev"; -pub(crate) static PROFILE_ANDROID: &str = "android-dev"; -pub(crate) static PROFILE_SERVER: &str = "server-dev"; - -impl DioxusCrate { - pub(crate) fn new(target: &TargetArgs) -> Result { - tracing::debug!("Loading crate"); - let cmd = Cmd::new(); - let builder = krates::Builder::new(); - let krates = builder - .build(cmd, |_| {}) - .context("Failed to run cargo metadata")?; - - let package = find_main_package(&krates, target.package.clone())?; - tracing::debug!("Found package {package:?}"); - - let dioxus_config = DioxusConfig::load(&krates, package)?.unwrap_or_default(); - - let package_name = krates[package].name.clone(); - let target_kind = if target.example.is_some() { - TargetKind::Example - } else { - TargetKind::Bin - }; - - let main_package = &krates[package]; - - let target_name = target - .example - .clone() - .or(target.bin.clone()) - .or_else(|| { - if let Some(default_run) = &main_package.default_run { - return Some(default_run.to_string()); - } - - let bin_count = main_package - .targets - .iter() - .filter(|x| x.kind.contains(&target_kind)) - .count(); - if bin_count != 1 { - return None; - } - - main_package.targets.iter().find_map(|x| { - if x.kind.contains(&target_kind) { - Some(x.name.clone()) - } else { - None - } - }) - }) - .unwrap_or(package_name); - - let target = main_package - .targets - .iter() - .find(|target| { - target_name == target.name.as_str() && target.kind.contains(&target_kind) - }) - .with_context(|| { - let target_of_kind = |kind|-> String { - let filtered_packages = main_package - .targets - .iter() - .filter_map(|target| { - target.kind.contains(kind).then_some(target.name.as_str()) - }).collect::>(); - filtered_packages.join(", ")}; - if let Some(example) = &target.example { - let examples = target_of_kind(&TargetKind::Example); - format!("Failed to find example {example}. \nAvailable examples are:\n{}", examples) - } else if let Some(bin) = &target.bin { - let binaries = target_of_kind(&TargetKind::Bin); - format!("Failed to find binary {bin}. \nAvailable binaries are:\n{}", binaries) - } else { - format!("Failed to find target {target_name}. \nIt looks like you are trying to build dioxus in a library crate. \ - You either need to run dx from inside a binary crate or build a specific example with the `--example` flag. \ - Available examples are:\n{}", target_of_kind(&TargetKind::Example)) - } - })? - .clone(); - - let settings = CliSettings::load(); - - Ok(Self { - krates: Arc::new(krates), - package, - config: dioxus_config, - target, - settings, - }) - } - - /// The asset dir we used to support before manganis became the default. - /// This generally was just a folder in your Dioxus.toml called "assets" or "public" where users - /// would store their assets. - /// - /// With manganis you now use `asset!()` and we pick it up automatically. - pub(crate) fn legacy_asset_dir(&self) -> Option { - self.config - .application - .asset_dir - .clone() - .map(|dir| self.crate_dir().join(dir)) - } - - /// Get the list of files in the "legacy" asset directory - pub(crate) fn legacy_asset_dir_files(&self) -> Vec { - let mut files = vec![]; - - let Some(legacy_asset_dir) = self.legacy_asset_dir() else { - return files; - }; - - let Ok(read_dir) = legacy_asset_dir.read_dir() else { - return files; - }; - - for entry in read_dir.flatten() { - files.push(entry.path()); - } - - files - } - - /// Get the directory where this app can write to for this session that's guaranteed to be stable - /// for the same app. This is useful for emitting state like window position and size. - /// - /// The directory is specific for this app and might be - pub(crate) fn session_cache_dir(&self) -> PathBuf { - self.internal_out_dir() - .join(self.executable_name()) - .join("session-cache") - } - - /// Get the outdir specified by the Dioxus.toml, relative to the crate directory. - /// We don't support workspaces yet since that would cause a collision of bundles per project. - pub(crate) fn crate_out_dir(&self) -> Option { - self.config - .application - .out_dir - .as_ref() - .map(|out_dir| self.crate_dir().join(out_dir)) - } - - /// Compose an out directory. Represents the typical "dist" directory that - /// is "distributed" after building an application (configurable in the - /// `Dioxus.toml`). - fn internal_out_dir(&self) -> PathBuf { - let dir = self.workspace_dir().join("target").join("dx"); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - /// Create a workdir for the given platform - /// This can be used as a temporary directory for the build, but in an observable way such that - /// you can see the files in the directory via `target` - /// - /// target/dx/build/app/web/ - /// target/dx/build/app/web/public/ - /// target/dx/build/app/web/server.exe - pub(crate) fn build_dir(&self, platform: Platform, release: bool) -> PathBuf { - self.internal_out_dir() - .join(self.executable_name()) - .join(if release { "release" } else { "debug" }) - .join(platform.build_folder_name()) - } - - /// target/dx/bundle/app/ - /// target/dx/bundle/app/blah.app - /// target/dx/bundle/app/blah.exe - /// target/dx/bundle/app/public/ - pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf { - self.internal_out_dir() - .join(self.executable_name()) - .join("bundle") - .join(platform.build_folder_name()) - } - - /// Get the workspace directory for the crate - pub(crate) fn workspace_dir(&self) -> PathBuf { - self.krates.workspace_root().as_std_path().to_path_buf() - } - - /// Get the directory of the crate - pub(crate) fn crate_dir(&self) -> PathBuf { - self.package() - .manifest_path - .parent() - .unwrap() - .as_std_path() - .to_path_buf() - } - - /// Get the main source file of the target - pub(crate) fn main_source_file(&self) -> PathBuf { - self.target.src_path.as_std_path().to_path_buf() - } - - /// Get the package we are currently in - pub(crate) fn package(&self) -> &krates::cm::Package { - &self.krates[self.package] - } - - /// Get the name of the package we are compiling - pub(crate) fn executable_name(&self) -> &str { - &self.target.name - } - - /// Get the type of executable we are compiling - pub(crate) fn executable_type(&self) -> krates::cm::TargetKind { - self.target.kind[0].clone() - } - - /// Try to autodetect the platform from the package by reading its features - /// - /// Read the default-features list and/or the features list on dioxus to see if we can autodetect the platform - pub(crate) fn autodetect_platform(&self) -> Option<(Platform, String)> { - let krate = self.krates.krates_by_name("dioxus").next()?; - - // We're going to accumulate the platforms that are enabled - // This will let us create a better warning if multiple platforms are enabled - let manually_enabled_platforms = self - .krates - .get_enabled_features(krate.kid)? - .iter() - .flat_map(|feature| { - tracing::trace!("Autodetecting platform from feature {feature}"); - Platform::autodetect_from_cargo_feature(feature) - .filter(|platform| *platform != Platform::Server) - .map(|f| (f, feature.to_string())) - }) - .collect::>(); - - if manually_enabled_platforms.len() > 1 { - tracing::error!("Multiple platforms are enabled. Please specify a platform with `--platform ` or set a single default platform using a cargo feature."); - for platform in manually_enabled_platforms { - tracing::error!(" - {platform:?}"); - } - return None; - } - - if manually_enabled_platforms.len() == 1 { - return manually_enabled_platforms.first().cloned(); - } - - // Let's try and find the list of platforms from the feature list - // This lets apps that specify web + server to work without specifying the platform. - // This is because we treat `server` as a binary thing rather than a dedicated platform, so at least we can disambiguate it - let possible_platforms = self - .package() - .features - .iter() - .filter_map(|(feature, _features)| { - match Platform::autodetect_from_cargo_feature(feature) { - Some(platform) => Some((platform, feature.to_string())), - None => { - let auto_implicit = _features - .iter() - .filter_map(|f| { - if !f.starts_with("dioxus?/") && !f.starts_with("dioxus/") { - return None; - } - - let rest = f - .trim_start_matches("dioxus/") - .trim_start_matches("dioxus?/"); - - Platform::autodetect_from_cargo_feature(rest) - }) - .collect::>(); - - if auto_implicit.len() == 1 { - Some((auto_implicit.first().copied().unwrap(), feature.to_string())) - } else { - None - } - } - } - }) - .filter(|platform| platform.0 != Platform::Server) - .collect::>(); - - if possible_platforms.len() == 1 { - return possible_platforms.first().cloned(); - } - - None - } - - /// Check if dioxus is being built with a particular feature - pub(crate) fn has_dioxus_feature(&self, filter: &str) -> bool { - self.krates.krates_by_name("dioxus").any(|dioxus| { - self.krates - .get_enabled_features(dioxus.kid) - .map(|features| features.contains(filter)) - .unwrap_or_default() - }) - } - - /// Get the features required to build for the given platform - pub(crate) fn feature_for_platform(&self, platform: Platform) -> String { - let package = self.package(); - - // Try to find the feature that activates the dioxus feature for the given platform - let dioxus_feature = platform.feature_name(); - - let res = package.features.iter().find_map(|(key, features)| { - // if the feature is just the name of the platform, we use that - if key == dioxus_feature { - return Some(key.clone()); - } - - // Otherwise look for the feature that starts with dioxus/ or dioxus?/ and matches the platform - for feature in features { - if let Some((_, after_dioxus)) = feature.split_once("dioxus") { - if let Some(dioxus_feature_enabled) = - after_dioxus.trim_start_matches('?').strip_prefix('/') - { - // If that enables the feature we are looking for, return that feature - if dioxus_feature_enabled == dioxus_feature { - return Some(key.clone()); - } - } - } - } - - None - }); - - res.unwrap_or_else(|| { - let fallback = format!("dioxus/{}", platform.feature_name()) ; - tracing::debug!( - "Could not find explicit feature for platform {platform}, passing `fallback` instead" - ); - fallback - }) - } - - /// Check if assets should be pre_compressed. This will only be true in release mode if the user - /// has enabled pre_compress in the web config. - pub(crate) fn should_pre_compress_web_assets(&self, release: bool) -> bool { - self.config.web.pre_compress && release - } - - // The `opt-level=1` increases build times, but can noticeably decrease time - // between saving changes and being able to interact with an app (for wasm/web). The "overall" - // time difference (between having and not having the optimization) can be - // almost imperceptible (~1 s) but also can be very noticeable (~6 s) — depends - // on setup (hardware, OS, browser, idle load). - // - // Find or create the client and server profiles in the top-level Cargo.toml file - // todo(jon): we should/could make these optional by placing some defaults somewhere - pub(crate) fn initialize_profiles(&self) -> crate::Result<()> { - let config_path = self.workspace_dir().join("Cargo.toml"); - let mut config = match std::fs::read_to_string(&config_path) { - Ok(config) => config.parse::().map_err(|e| { - crate::Error::Other(anyhow::anyhow!("Failed to parse Cargo.toml: {}", e)) - })?, - Err(_) => Default::default(), - }; - - if let Item::Table(table) = config - .as_table_mut() - .entry("profile") - .or_insert(Item::Table(Default::default())) - { - if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_WASM) { - let mut client = toml_edit::Table::new(); - client.insert("inherits", Item::Value("dev".into())); - client.insert("opt-level", Item::Value(1.into())); - entry.insert(Item::Table(client)); - } - - if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_SERVER) { - let mut server = toml_edit::Table::new(); - server.insert("inherits", Item::Value("dev".into())); - entry.insert(Item::Table(server)); - } - - if let toml_edit::Entry::Vacant(entry) = table.entry(PROFILE_ANDROID) { - let mut android = toml_edit::Table::new(); - android.insert("inherits", Item::Value("dev".into())); - entry.insert(Item::Table(android)); - } - } - - std::fs::write(config_path, config.to_string()) - .context("Failed to write profiles to Cargo.toml")?; - - Ok(()) - } - - fn default_ignore_list(&self) -> Vec<&'static str> { - vec![ - ".git", - ".github", - ".vscode", - "target", - "node_modules", - "dist", - "*~", - ".*", - "*.lock", - "*.log", - ] - } - - /// Create a new gitignore map for this target crate - /// - /// todo(jon): this is a bit expensive to build, so maybe we should cache it? - pub fn workspace_gitignore(&self) -> ignore::gitignore::Gitignore { - let crate_dir = self.crate_dir(); - - let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(&crate_dir); - ignore_builder.add(crate_dir.join(".gitignore")); - - let workspace_dir = self.workspace_dir(); - ignore_builder.add(workspace_dir.join(".gitignore")); - - for path in self.default_ignore_list() { - ignore_builder - .add_line(None, path) - .expect("failed to add path to file excluded"); - } - - ignore_builder.build().unwrap() - } - - /// Return the version of the wasm-bindgen crate if it exists - pub fn wasm_bindgen_version(&self) -> Option { - self.krates - .krates_by_name("wasm-bindgen") - .next() - .map(|krate| krate.krate.version.to_string()) - } - - pub(crate) fn default_platforms(&self) -> Vec { - let Some(default) = self.package().features.get("default") else { - return Vec::new(); - }; - let mut platforms = vec![]; - - // we only trace features 1 level deep.. - for feature in default.iter() { - // If the user directly specified a platform we can just use that. - if feature.starts_with("dioxus/") { - let dx_feature = feature.trim_start_matches("dioxus/"); - let auto = Platform::autodetect_from_cargo_feature(dx_feature); - if let Some(auto) = auto { - platforms.push(auto); - } - } - - // If the user is specifying an internal feature that points to a platform, we can use that - let internal_feature = self.package().features.get(feature); - if let Some(internal_feature) = internal_feature { - for feature in internal_feature { - if feature.starts_with("dioxus/") { - let dx_feature = feature.trim_start_matches("dioxus/"); - let auto = Platform::autodetect_from_cargo_feature(dx_feature); - if let Some(auto) = auto { - platforms.push(auto); - } - } - } - } - } - - platforms.sort(); - platforms.dedup(); - platforms - } - - /// Gather the features that are enabled for the package - pub(crate) fn platformless_features(&self) -> Vec { - let default = self.package().features.get("default").unwrap(); - let mut kept_features = vec![]; - - // Only keep the top-level features in the default list that don't point to a platform directly - // IE we want to drop `web` if default = ["web"] - 'top: for feature in default { - // Don't keep features that point to a platform via dioxus/blah - if feature.starts_with("dioxus/") { - let dx_feature = feature.trim_start_matches("dioxus/"); - if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { - continue 'top; - } - } - - // Don't keep features that point to a platform via an internal feature - if let Some(internal_feature) = self.package().features.get(feature) { - for feature in internal_feature { - if feature.starts_with("dioxus/") { - let dx_feature = feature.trim_start_matches("dioxus/"); - if Platform::autodetect_from_cargo_feature(dx_feature).is_some() { - continue 'top; - } - } - } - } - - // Otherwise we can keep it - kept_features.push(feature.to_string()); - } - - kept_features - } - - /// Return the list of paths that we should watch for changes. - pub(crate) fn watch_paths(&self) -> Vec { - let mut watched_paths = vec![]; - - // Get a list of *all* the crates with Rust code that we need to watch. - // This will end up being dependencies in the workspace and non-workspace dependencies on the user's computer. - let mut watched_crates = self.local_dependencies(); - watched_crates.push(self.crate_dir()); - - // Now, watch all the folders in the crates, but respecting their respective ignore files - for krate_root in watched_crates { - // Build the ignore builder for this crate, but with our default ignore list as well - let ignore = self.ignore_for_krate(&krate_root); - - for entry in krate_root.read_dir().unwrap() { - let Ok(entry) = entry else { - continue; - }; - - if ignore - .matched(entry.path(), entry.path().is_dir()) - .is_ignore() - { - continue; - } - - watched_paths.push(entry.path().to_path_buf()); - } - } - - watched_paths.dedup(); - - watched_paths - } - - fn ignore_for_krate(&self, path: &Path) -> ignore::gitignore::Gitignore { - let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(path); - for path in self.default_ignore_list() { - ignore_builder - .add_line(None, path) - .expect("failed to add path to file excluded"); - } - ignore_builder.build().unwrap() - } - - /// Get all the Manifest paths for dependencies that we should watch. Will not return anything - /// in the `.cargo` folder - only local dependencies will be watched. - /// - /// This returns a list of manifest paths - /// - /// Extend the watch path to include: - /// - /// - the assets directory - this is so we can hotreload CSS and other assets by default - /// - the Cargo.toml file - this is so we can hotreload the project if the user changes dependencies - /// - the Dioxus.toml file - this is so we can hotreload the project if the user changes the Dioxus config - pub(crate) fn local_dependencies(&self) -> Vec { - let mut paths = vec![]; - - for (dependency, _edge) in self.krates.get_deps(self.package) { - let krate = match dependency { - krates::Node::Krate { krate, .. } => krate, - krates::Node::Feature { krate_index, .. } => &self.krates[krate_index.index()], - }; - - if krate - .manifest_path - .components() - .any(|c| c.as_str() == ".cargo") - { - continue; - } - - paths.push( - krate - .manifest_path - .parent() - .unwrap() - .to_path_buf() - .into_std_path_buf(), - ); - } - - paths - } - - pub(crate) fn all_watched_crates(&self) -> Vec { - let mut krates: Vec = self - .local_dependencies() - .into_iter() - .map(|p| { - p.parent() - .expect("Local manifest to exist and have a parent") - .to_path_buf() - }) - .chain(Some(self.crate_dir())) - .collect(); - - krates.dedup(); - - krates - } - - /// Attempt to retrieve the path to ADB - pub(crate) fn android_adb() -> PathBuf { - static PATH: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - let Some(sdk) = DioxusCrate::android_sdk() else { - return PathBuf::from("adb"); - }; - - let tools = sdk.join("platform-tools"); - - if tools.join("adb").exists() { - return tools.join("adb"); - } - - if tools.join("adb.exe").exists() { - return tools.join("adb.exe"); - } - - PathBuf::from("adb") - }); - - PATH.clone() - } - - pub(crate) fn android_sdk() -> Option { - var_or_debug("ANDROID_SDK_ROOT") - .or_else(|| var_or_debug("ANDROID_SDK")) - .or_else(|| var_or_debug("ANDROID_HOME")) - } - - pub(crate) fn android_ndk(&self) -> Option { - // "/Users/jonkelley/Library/Android/sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android24-clang" - static PATH: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { - // attempt to autodetect the ndk path from env vars (usually set by the shell) - let auto_detected_ndk = - var_or_debug("NDK_HOME").or_else(|| var_or_debug("ANDROID_NDK_HOME")); - - if let Some(home) = auto_detected_ndk { - return Some(home); - } - - let sdk = var_or_debug("ANDROID_SDK_ROOT") - .or_else(|| var_or_debug("ANDROID_SDK")) - .or_else(|| var_or_debug("ANDROID_HOME"))?; - - let ndk = sdk.join("ndk"); - - ndk.read_dir() - .ok()? - .flatten() - .map(|dir| (dir.file_name(), dir.path())) - .sorted() - .next_back() - .map(|(_, path)| path.to_path_buf()) - }); - - PATH.clone() - } - - pub(crate) async fn autodetect_android_arch() -> Option { - // Try auto detecting arch through adb. - static AUTO_ARCH: OnceCell> = OnceCell::new(); - - match AUTO_ARCH.get() { - Some(a) => *a, - None => { - // TODO: Wire this up with --device flag. (add `-s serial`` flag before `shell` arg) - let output = Command::new("adb") - .arg("shell") - .arg("uname") - .arg("-m") - .output() - .await; - - let out = match output { - Ok(o) => o, - Err(e) => { - tracing::debug!("ADB command failed: {:?}", e); - return None; - } - }; - - // Parse ADB output - let Ok(out) = String::from_utf8(out.stdout) else { - tracing::debug!("ADB returned unexpected data."); - return None; - }; - let trimmed = out.trim().to_string(); - tracing::trace!("ADB Returned: `{trimmed:?}`"); - - // Set the cell - let arch = Arch::try_from(trimmed).ok(); - AUTO_ARCH - .set(arch) - .expect("the cell should have been checked empty by the match condition"); - - arch - } - } - } - - pub(crate) fn mobile_org(&self) -> String { - let identifier = self.bundle_identifier(); - let mut split = identifier.splitn(3, '.'); - let sub = split - .next() - .expect("Identifier to have at least 3 periods like `com.example.app`"); - let tld = split - .next() - .expect("Identifier to have at least 3 periods like `com.example.app`"); - format!("{}.{}", sub, tld) - } - - pub(crate) fn bundled_app_name(&self) -> String { - use convert_case::{Case, Casing}; - self.executable_name().to_case(Case::Pascal) - } - - pub(crate) fn full_mobile_app_name(&self) -> String { - format!("{}.{}", self.mobile_org(), self.bundled_app_name()) - } - - pub(crate) fn bundle_identifier(&self) -> String { - if let Some(identifier) = self.config.bundle.identifier.clone() { - return identifier.clone(); - } - - format!("com.example.{}", self.bundled_app_name()) - } -} - -impl std::fmt::Debug for DioxusCrate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DioxusCrate") - .field("package", &self.krates[self.package]) - .field("dioxus_config", &self.config) - .field("target", &self.target) - .finish() - } -} - -// Find the main package in the workspace -fn find_main_package(krates: &Krates, package: Option) -> Result { - if let Some(package) = package { - let mut workspace_members = krates.workspace_members(); - let found = workspace_members.find_map(|node| { - if let krates::Node::Krate { id, krate, .. } = node { - if krate.name == package { - return Some(id); - } - } - None - }); - - if found.is_none() { - tracing::error!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?"); - tracing::error!("Packages in the workspace:"); - for package in krates.workspace_members() { - if let krates::Node::Krate { krate, .. } = package { - tracing::error!("{}", krate.name()); - } - } - } - - let kid = found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?; - - return Ok(krates.nid_for_kid(kid).unwrap()); - }; - - // Otherwise find the package that is the closest parent of the current directory - let current_dir = std::env::current_dir()?; - let current_dir = current_dir.as_path(); - - // Go through each member and find the path that is a parent of the current directory - let mut closest_parent = None; - for member in krates.workspace_members() { - if let krates::Node::Krate { id, krate, .. } = member { - let member_path = krate.manifest_path.parent().unwrap(); - if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) { - let len = path.components().count(); - match closest_parent { - Some((_, closest_parent_len)) => { - if len < closest_parent_len { - closest_parent = Some((id, len)); - } - } - None => { - closest_parent = Some((id, len)); - } - } - } - } - } - - let kid = closest_parent - .map(|(id, _)| id) - .with_context(|| { - let bin_targets = krates.workspace_members().filter_map(|krate|match krate { - krates::Node::Krate { krate, .. } if krate.targets.iter().any(|t| t.kind.contains(&krates::cm::TargetKind::Bin))=> { - Some(format!("- {}", krate.name)) - } - _ => None - }).collect::>(); - format!("Failed to find binary package to build.\nYou need to either run dx from inside a binary crate or specify a binary package to build with the `--package` flag. Try building again with one of the binary packages in the workspace:\n{}", bin_targets.join("\n")) - })?; - - let package = krates.nid_for_kid(kid).unwrap(); - Ok(package) -} - -fn var_or_debug(name: &str) -> Option { - use std::env::var; - use tracing::debug; - - var(name) - .inspect_err(|_| debug!("{name} not set")) - .ok() - .map(PathBuf::from) -} diff --git a/packages/cli/src/error.rs b/packages/cli/src/error.rs index 69e0096abc..28f2807a83 100644 --- a/packages/cli/src/error.rs +++ b/packages/cli/src/error.rs @@ -1,4 +1,4 @@ -use crate::metadata::CargoError; +use std::fmt::Debug; use thiserror::Error as ThisError; pub(crate) type Result = std::result::Result; @@ -22,7 +22,7 @@ pub(crate) enum Error { Runtime(String), #[error("Cargo Error: {0}")] - Cargo(#[from] CargoError), + Cargo(String), #[error("Invalid proxy URL: {0}")] InvalidProxy(#[from] hyper::http::uri::InvalidUri), @@ -33,9 +33,11 @@ pub(crate) enum Error { #[error("Failed to bundle project: {0}")] BundleFailed(#[from] tauri_bundler::Error), - #[allow(unused)] - #[error("Unsupported feature: {0}")] - UnsupportedFeature(String), + #[error("Failed to perform hotpatch: {0}")] + PatchingFailed(#[from] crate::build::PatchError), + + #[error("{0}")] + CapturedPanic(String), #[error("Failed to render template: {0}")] TemplateParse(#[from] handlebars::RenderError), diff --git a/packages/cli/src/filemap.rs b/packages/cli/src/filemap.rs deleted file mode 100644 index 33c3f2e428..0000000000 --- a/packages/cli/src/filemap.rs +++ /dev/null @@ -1,168 +0,0 @@ -use dioxus_core::internal::{ - HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey, -}; -use dioxus_core_types::HotReloadingContext; -use dioxus_rsx::CallBody; -use dioxus_rsx_hotreload::{ChangedRsx, HotReloadResult}; -use std::path::PathBuf; -use std::{collections::HashMap, path::Path}; -use syn::spanned::Spanned; - -/// A struct that stores state of rsx! files and their parsed bodies. -/// -/// This keeps track of changes to rsx files and helps determine if a file can be hotreloaded or if -/// the project needs to be rebuilt. -pub(crate) struct HotreloadFilemap { - /// Map of rust files to their contents - /// - /// Once this is created, we won't change the contents, to preserve the ability to hotreload - /// from the original source mapping, unless the file change results in a full rebuild. - map: HashMap, -} - -struct CachedFile { - contents: String, - most_recent: Option, - templates: HashMap, -} - -pub enum HotreloadResult { - Rsx(Vec), - Notreloadable, - NotParseable, -} - -impl HotreloadFilemap { - /// Create a new empty filemap. - /// - /// Make sure to fill the filemap, either automatically with `fill_from_filesystem` or manually with `add_file`; - pub fn new() -> Self { - Self { - map: Default::default(), - } - } - - /// Add a file to the filemap. - pub(crate) fn add_file(&mut self, path: PathBuf, contents: String) { - self.map.insert( - path, - CachedFile { - contents, - most_recent: None, - templates: Default::default(), - }, - ); - } - - /// Commit the changes to the filemap, overwriting the contents of the files - /// - /// Removes any cached templates and replaces the contents of the files with the most recent - /// - /// todo: we should-reparse the contents so we never send a new version, ever - pub fn force_rebuild(&mut self) { - for cached_file in self.map.values_mut() { - if let Some(most_recent) = cached_file.most_recent.take() { - cached_file.contents = most_recent; - } - cached_file.templates.clear(); - } - } - - /// Try to update the rsx in a file, returning the templates that were hotreloaded - /// - /// If the templates could not be hotreloaded, this will return an error. This error isn't fatal, per se, - /// but it does mean that we could not successfully hotreload the file in-place. - /// - /// It's expected that the file path you pass in is relative the crate root. We have no way of - /// knowing if it's *not*, so we'll assume it is. - /// - /// This does not do any caching on what intermediate state, like previous hotreloads, so you need - /// to do that yourself. - pub(crate) fn update_rsx( - &mut self, - path: &Path, - new_contents: String, - ) -> HotreloadResult { - // Get the cached file if it exists - let Some(cached_file) = self.map.get_mut(path) else { - return HotreloadResult::NotParseable; - }; - - // We assume we can parse the old file and the new file - // We should just ignore hotreloading files that we can't parse - // todo(jon): we could probably keep the old `File` around instead of re-parsing on every hotreload - let (Ok(old_file), Ok(new_file)) = ( - syn::parse_file(&cached_file.contents), - syn::parse_file(&new_contents), - ) else { - tracing::debug!("Diff rsx returned not parseable"); - return HotreloadResult::NotParseable; - }; - - // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version - cached_file.most_recent = Some(new_contents); - - // todo(jon): allow server-fn hotreloading - // also whyyyyyyyyy is this (new, old) instead of (old, new)? smh smh smh - let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else { - tracing::debug!("Diff rsx returned notreladable"); - return HotreloadResult::Notreloadable; - }; - - let mut out_templates = vec![]; - for ChangedRsx { old, new } in changed_rsx { - let old_start = old.span().start(); - - let old_parsed = syn::parse2::(old.tokens); - let new_parsed = syn::parse2::(new.tokens); - let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else { - continue; - }; - - // Format the template location, normalizing the path - let file_name: String = path - .components() - .map(|c| c.as_os_str().to_string_lossy()) - .collect::>() - .join("/"); - - // Returns a list of templates that are hotreloadable - let results = HotReloadResult::new::( - &old_call_body.body, - &new_call_body.body, - file_name.clone(), - ); - - // If no result is returned, we can't hotreload this file and need to keep the old file - let Some(results) = results else { - return HotreloadResult::Notreloadable; - }; - - // Only send down templates that have roots, and ideally ones that have changed - // todo(jon): maybe cache these and don't send them down if they're the same - for (index, template) in results.templates { - if template.roots.is_empty() { - continue; - } - - // Create the key we're going to use to identify this template - let key = TemplateGlobalKey { - file: file_name.clone(), - line: old_start.line, - column: old_start.column + 1, - index, - }; - - // if the template is the same, don't send its - if cached_file.templates.get(&key) == Some(&template) { - continue; - }; - - cached_file.templates.insert(key.clone(), template.clone()); - out_templates.push(HotReloadTemplateWithLocation { template, key }); - } - } - - HotreloadResult::Rsx(out_templates) - } -} diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 7d8258c239..3aa5aed8ea 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -69,11 +69,11 @@ impl TraceController { EnvFilter::from_env(LOG_ENV) } else if matches!(args.action, Commands::Serve(_)) { EnvFilter::new( - "error,dx=trace,dioxus_cli=trace,manganis_cli_support=trace,wasm_split_cli=trace", + "error,dx=trace,dioxus_cli=trace,manganis_cli_support=trace,wasm_split_cli=trace,subsecond_cli_support=trace", ) } else { EnvFilter::new(format!( - "error,dx={our_level},dioxus_cli={our_level},manganis_cli_support={our_level},,wasm_split_cli={our_level}", + "error,dx={our_level},dioxus_cli={our_level},manganis_cli_support={our_level},wasm_split_cli={our_level},subsecond_cli_support={our_level}", our_level = if args.verbosity.verbose { "debug" } else { @@ -154,19 +154,23 @@ impl TraceController { ServeUpdate::TracingLog { log } } -} -impl Drop for TraceController { - fn drop(&mut self) { + pub(crate) fn shutdown_panic(&mut self) { TUI_ACTIVE.store(false, Ordering::Relaxed); // re-emit any remaining messages while let Ok(Some(msg)) = self.tui_rx.try_next() { - let contents = match msg.content { + let content = match msg.content { TraceContent::Text(text) => text, TraceContent::Cargo(msg) => msg.message.to_string(), }; - tracing::error!("{}", contents); + match msg.level { + Level::ERROR => tracing::error!("{content}"), + Level::WARN => tracing::warn!("{content}"), + Level::INFO => tracing::info!("{content}"), + Level::DEBUG => tracing::debug!("{content}"), + Level::TRACE => tracing::trace!("{content}"), + } } } } diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index e8104c2fb7..a31af40601 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -2,58 +2,61 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::doc_overindented_list_items)] mod build; mod bundle_utils; mod cli; mod config; -mod dioxus_crate; +mod devcfg; mod dx_build_info; mod error; mod fastfs; -mod filemap; mod logging; -mod metadata; mod platform; -mod rustc; +mod rustcwrapper; mod serve; mod settings; mod wasm_bindgen; mod wasm_opt; +mod workspace; pub(crate) use build::*; pub(crate) use cli::*; pub(crate) use config::*; -pub(crate) use dioxus_crate::*; pub(crate) use dioxus_dx_wire_format::*; pub(crate) use error::*; -pub(crate) use filemap::*; +pub(crate) use link::*; pub(crate) use logging::*; pub(crate) use platform::*; -pub(crate) use rustc::*; +pub(crate) use rustcwrapper::*; pub(crate) use settings::*; +pub(crate) use wasm_bindgen::*; +pub(crate) use workspace::*; #[tokio::main] async fn main() { + // The CLI uses dx as a rustcwrapper in some instances (like binary patching) + if rustcwrapper::is_wrapping_rustc() { + return rustcwrapper::run_rustc().await; + } + // If we're being ran as a linker (likely from ourselves), we want to act as a linker instead. - if let Some(link_action) = link::LinkAction::from_env() { - return link_action.run(); + if let Some(link_args) = link::LinkAction::from_env() { + return link_args.run_link().await; } let args = TraceController::initialize(); - #[cfg(debug_assertions)] - tracing::warn!("CLI was built with debug profile. Commands will run slower."); - let result = match args.action { Commands::Translate(opts) => opts.translate(), Commands::New(opts) => opts.create(), Commands::Init(opts) => opts.init(), - Commands::Config(opts) => opts.config(), - Commands::Autoformat(opts) => opts.autoformat(), + Commands::Config(opts) => opts.config().await, + Commands::Autoformat(opts) => opts.autoformat().await, Commands::Check(opts) => opts.check().await, Commands::Clean(opts) => opts.clean().await, - Commands::Build(opts) => opts.run_cmd().await, + Commands::Build(opts) => opts.build().await, Commands::Serve(opts) => opts.serve().await, Commands::Bundle(opts) => opts.bundle().await, Commands::Run(opts) => opts.run().await, @@ -66,8 +69,9 @@ async fn main() { tracing::debug!(json = ?output); } Err(err) => { + eprintln!("{err}"); + tracing::error!( - ?err, json = ?StructuredOutput::Error { message: format!("{err:?}"), }, diff --git a/packages/cli/src/metadata.rs b/packages/cli/src/metadata.rs deleted file mode 100644 index 7d8a2a321d..0000000000 --- a/packages/cli/src/metadata.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Utilities for working with cargo and rust files -use std::error::Error; -use std::{ - env, - ffi::OsStr, - fmt::{Display, Formatter}, - fs, - path::{Path, PathBuf}, -}; - -#[derive(Debug, Clone)] -pub(crate) struct CargoError { - msg: String, -} - -impl CargoError { - pub(crate) fn new(msg: String) -> Self { - Self { msg } - } -} - -impl Display for CargoError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "CargoError: {}", self.msg) - } -} - -impl Error for CargoError {} - -/// How many parent folders are searched for a `Cargo.toml` -const MAX_ANCESTORS: u32 = 10; - -/// Returns the root of the crate that the command is run from -/// -/// If the command is run from the workspace root, this will return the top-level Cargo.toml -pub(crate) fn crate_root() -> Result { - // From the current directory we work our way up, looking for `Cargo.toml` - env::current_dir() - .ok() - .and_then(|mut wd| { - for _ in 0..MAX_ANCESTORS { - if contains_manifest(&wd) { - return Some(wd); - } - if !wd.pop() { - break; - } - } - None - }) - .ok_or_else(|| { - CargoError::new("Failed to find directory containing Cargo.toml".to_string()) - }) -} - -/// Checks if the directory contains `Cargo.toml` -fn contains_manifest(path: &Path) -> bool { - fs::read_dir(path) - .map(|entries| { - entries - .filter_map(Result::ok) - .any(|ent| &ent.file_name() == "Cargo.toml") - }) - .unwrap_or(false) -} - -/// Collects all `.rs` files in the provided directory, respecting files to ignore (e.g. `.gitignore`) -pub(crate) fn collect_rs_files(dir: impl AsRef) -> Vec { - let mut files = Vec::new(); - for result in ignore::Walk::new(dir) { - let path = result.unwrap().into_path(); - if let Some(ext) = path.extension() { - if ext == OsStr::new("rs") { - files.push(path); - } - } - } - files -} diff --git a/packages/cli/src/rustc.rs b/packages/cli/src/rustc.rs deleted file mode 100644 index 8ec51558c4..0000000000 --- a/packages/cli/src/rustc.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::Result; -use anyhow::Context; -use std::path::PathBuf; -use tokio::process::Command; - -#[derive(Debug, Default)] -pub struct RustcDetails { - pub sysroot: PathBuf, - pub version: String, -} - -impl RustcDetails { - /// Find the current sysroot location using the CLI - pub async fn from_cli() -> Result { - let sysroot = Command::new("rustc") - .args(["--print", "sysroot"]) - .output() - .await - .map(|out| String::from_utf8(out.stdout))? - .context("Failed to extract rustc sysroot output")?; - - let rustc_version = Command::new("rustc") - .args(["--version"]) - .output() - .await - .map(|out| String::from_utf8(out.stdout))? - .context("Failed to extract rustc version output")?; - - Ok(Self { - sysroot: sysroot.trim().into(), - version: rustc_version.trim().into(), - }) - } - - pub fn has_wasm32_unknown_unknown(&self) -> bool { - self.sysroot - .join("lib/rustlib/wasm32-unknown-unknown") - .exists() - } -} diff --git a/packages/cli/src/rustcwrapper.rs b/packages/cli/src/rustcwrapper.rs new file mode 100644 index 0000000000..bd269d6843 --- /dev/null +++ b/packages/cli/src/rustcwrapper.rs @@ -0,0 +1,77 @@ +use serde::{Deserialize, Serialize}; +use std::{ + env::{args, vars}, + path::PathBuf, +}; + +/// The environment variable indicating where the args file is located. +/// +/// When `dx-rustc` runs, it writes its arguments to this file. +pub const DX_RUSTC_WRAPPER_ENV_VAR: &str = "DX_RUSTC"; + +/// Is `dx` being used as a rustc wrapper? +/// +/// This is primarily used to intercept cargo, enabling fast hot-patching by caching the environment +/// cargo setups up for the user's current project. +/// +/// In a differenet world we could simply rely on cargo printing link args and the rustc command, but +/// it doesn't seem to output that in a reliable, parseable, cross-platform format (ie using command +/// files on windows...), so we're forced to do this interception nonsense. +pub fn is_wrapping_rustc() -> bool { + std::env::var(DX_RUSTC_WRAPPER_ENV_VAR).is_ok() +} + +#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RustcArgs { + pub args: Vec, + pub envs: Vec<(String, String)>, +} + +/// Run rustc directly, but output the result to a file. +/// +/// https://doc.rust-lang.org/cargo/reference/config.html#buildrustc +pub async fn run_rustc() { + // if we happen to be both a rustc wrapper and a linker, we want to run the linker if the arguments seem linker-y + // this is a stupid hack + if std::env::args() + .take(5) + .any(|arg| arg.ends_with(".o") || arg == "-flavor" || arg.starts_with("@")) + { + return crate::link::LinkAction::from_env() + .expect("Linker action not found") + .run_link() + .await; + } + + let var_file: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR) + .expect("DX_RUSTC not set") + .into(); + + let rustc_args = RustcArgs { + args: args().skip(1).collect::>(), + envs: vars().collect::<_>(), + }; + + std::fs::create_dir_all(var_file.parent().expect("Failed to get parent dir")) + .expect("Failed to create parent dir"); + std::fs::write( + &var_file, + serde_json::to_string(&rustc_args).expect("Failed to serialize rustc args"), + ) + .expect("Failed to write rustc args to file"); + + // Run the actual rustc command + // We want all stdout/stderr to be inherited, so the running process can see the output + // + // Note that the args format we get from the wrapper includes the `rustc` command itself, so we + // need to skip that - we already skipped the first arg when we created the args struct. + let mut cmd = std::process::Command::new("rustc"); + cmd.args(rustc_args.args.iter().skip(1)); + cmd.envs(rustc_args.envs); + cmd.stdout(std::process::Stdio::inherit()); + cmd.stderr(std::process::Stdio::inherit()); + cmd.current_dir(std::env::current_dir().expect("Failed to get current dir")); + + // Propagate the exit code + std::process::exit(cmd.status().unwrap().code().unwrap()) +} diff --git a/packages/cli/src/serve/detect.rs b/packages/cli/src/serve/detect.rs deleted file mode 100644 index 504731da14..0000000000 --- a/packages/cli/src/serve/detect.rs +++ /dev/null @@ -1,32 +0,0 @@ -/// Detects if `dx` is being ran in a WSL environment. -/// -/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files. -/// This may fail in the future as it isn't guaranteed by Microsoft. -/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364 -pub(crate) fn is_wsl() -> bool { - const WSL_1: &str = "/proc/sys/kernel/osrelease"; - const WSL_2: &str = "/proc/version"; - const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"]; - - // Test 1st File - if let Ok(content) = std::fs::read_to_string(WSL_1) { - let lowercase = content.to_lowercase(); - for keyword in WSL_KEYWORDS { - if lowercase.contains(keyword) { - return true; - } - } - } - - // Test 2nd File - if let Ok(content) = std::fs::read_to_string(WSL_2) { - let lowercase = content.to_lowercase(); - for keyword in WSL_KEYWORDS { - if lowercase.contains(keyword) { - return true; - } - } - } - - false -} diff --git a/packages/cli/src/serve/handle.rs b/packages/cli/src/serve/handle.rs deleted file mode 100644 index 6903e86d56..0000000000 --- a/packages/cli/src/serve/handle.rs +++ /dev/null @@ -1,856 +0,0 @@ -use crate::{AppBundle, DioxusCrate, Platform, Result}; -use anyhow::Context; -use dioxus_cli_opt::process_file_to; -use std::{ - env, - net::SocketAddr, - path::{Path, PathBuf}, - process::Stdio, -}; -use tokio::{ - io::{AsyncBufReadExt, BufReader, Lines}, - process::{Child, ChildStderr, ChildStdout, Command}, -}; - -/// A handle to a running app. -/// -/// Also includes a handle to its server if it exists. -/// The actual child processes might not be present (web) or running (died/killed). -/// -/// The purpose of this struct is to accumulate state about the running app and its server, like -/// any runtime information needed to hotreload the app or send it messages. -/// -/// We might want to bring in websockets here too, so we know the exact channels the app is using to -/// communicate with the devserver. Currently that's a broadcast-type system, so this struct isn't super -/// duper useful. -/// -/// todo: restructure this such that "open" is a running task instead of blocking the main thread -pub(crate) struct AppHandle { - pub(crate) app: AppBundle, - - // These might be None if the app died or the user did not specify a server - pub(crate) app_child: Option, - pub(crate) server_child: Option, - - // stdio for the app so we can read its stdout/stderr - // we don't map stdin today (todo) but most apps don't need it - pub(crate) app_stdout: Option>>, - pub(crate) app_stderr: Option>>, - pub(crate) server_stdout: Option>>, - pub(crate) server_stderr: Option>>, - - /// The executables but with some extra entropy in their name so we can run two instances of the - /// same app without causing collisions on the filesystem. - pub(crate) entropy_app_exe: Option, - pub(crate) entropy_server_exe: Option, - - /// The virtual directory that assets will be served from - /// Used mostly for apk/ipa builds since they live in simulator - pub(crate) runtime_asst_dir: Option, -} - -impl AppHandle { - pub async fn new(app: AppBundle) -> Result { - Ok(AppHandle { - app, - runtime_asst_dir: None, - app_child: None, - app_stderr: None, - app_stdout: None, - server_child: None, - server_stdout: None, - server_stderr: None, - entropy_app_exe: None, - entropy_server_exe: None, - }) - } - - pub(crate) async fn open( - &mut self, - devserver_ip: SocketAddr, - open_address: Option, - start_fullstack_on_address: Option, - open_browser: bool, - ) -> Result<()> { - let krate = &self.app.build.krate; - - // Set the env vars that the clients will expect - // These need to be stable within a release version (ie 0.6.0) - let mut envs = vec![ - (dioxus_cli_config::CLI_ENABLED_ENV, "true".to_string()), - ( - dioxus_cli_config::ALWAYS_ON_TOP_ENV, - krate.settings.always_on_top.unwrap_or(true).to_string(), - ), - ( - dioxus_cli_config::APP_TITLE_ENV, - krate.config.web.app.title.clone(), - ), - ("RUST_BACKTRACE", "1".to_string()), - ( - dioxus_cli_config::DEVSERVER_IP_ENV, - devserver_ip.ip().to_string(), - ), - ( - dioxus_cli_config::DEVSERVER_PORT_ENV, - devserver_ip.port().to_string(), - ), - // unset the cargo dirs in the event we're running `dx` locally - // since the child process will inherit the env vars, we don't want to confuse the downstream process - ("CARGO_MANIFEST_DIR", "".to_string()), - ( - dioxus_cli_config::SESSION_CACHE_DIR, - self.app - .build - .krate - .session_cache_dir() - .display() - .to_string(), - ), - ]; - - if let Some(base_path) = &krate.config.web.app.base_path { - envs.push((dioxus_cli_config::ASSET_ROOT_ENV, base_path.clone())); - } - - if let Some(env_filter) = env::var_os("RUST_LOG").and_then(|e| e.into_string().ok()) { - envs.push(("RUST_LOG", env_filter)); - } - - // Launch the server if we were given an address to start it on, and the build includes a server. After we - // start the server, consume its stdout/stderr. - if let (Some(addr), Some(server)) = (start_fullstack_on_address, self.server_exe()) { - tracing::debug!("Proxying fullstack server from port {:?}", addr); - envs.push((dioxus_cli_config::SERVER_IP_ENV, addr.ip().to_string())); - envs.push((dioxus_cli_config::SERVER_PORT_ENV, addr.port().to_string())); - tracing::debug!("Launching server from path: {server:?}"); - let mut child = Command::new(server) - .envs(envs.clone()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - let stdout = BufReader::new(child.stdout.take().unwrap()); - let stderr = BufReader::new(child.stderr.take().unwrap()); - self.server_stdout = Some(stdout.lines()); - self.server_stderr = Some(stderr.lines()); - self.server_child = Some(child); - } - - // We try to use stdin/stdout to communicate with the app - let running_process = match self.app.build.build.platform() { - // Unfortunately web won't let us get a proc handle to it (to read its stdout/stderr) so instead - // use use the websocket to communicate with it. I wish we could merge the concepts here, - // like say, opening the socket as a subprocess, but alas, it's simpler to do that somewhere else. - Platform::Web => { - // Only the first build we open the web app, after that the user knows it's running - if open_browser { - self.open_web(open_address.unwrap_or(devserver_ip)); - } - - None - } - - Platform::Ios => Some(self.open_ios_sim(envs).await?), - - // https://developer.android.com/studio/run/emulator-commandline - Platform::Android => { - self.open_android_sim(devserver_ip, envs).await; - None - } - - // These are all just basically running the main exe, but with slightly different resource dir paths - Platform::Server - | Platform::MacOS - | Platform::Windows - | Platform::Linux - | Platform::Liveview => Some(self.open_with_main_exe(envs)?), - }; - - // If we have a running process, we need to attach to it and wait for its outputs - if let Some(mut child) = running_process { - let stdout = BufReader::new(child.stdout.take().unwrap()); - let stderr = BufReader::new(child.stderr.take().unwrap()); - self.app_stdout = Some(stdout.lines()); - self.app_stderr = Some(stderr.lines()); - self.app_child = Some(child); - } - - Ok(()) - } - - /// Gracefully kill the process and all of its children - /// - /// Uses the `SIGTERM` signal on unix and `taskkill` on windows. - /// This complex logic is necessary for things like window state preservation to work properly. - /// - /// Also wipes away the entropy executables if they exist. - pub(crate) async fn cleanup(&mut self) { - // Soft-kill the process by sending a sigkill, allowing the process to clean up - self.soft_kill().await; - - // Wipe out the entropy executables if they exist - if let Some(entropy_app_exe) = self.entropy_app_exe.take() { - _ = std::fs::remove_file(entropy_app_exe); - } - - if let Some(entropy_server_exe) = self.entropy_server_exe.take() { - _ = std::fs::remove_file(entropy_server_exe); - } - } - - /// Kill the app and server exes - pub(crate) async fn soft_kill(&mut self) { - use futures_util::FutureExt; - - // Kill any running executables on Windows - let server_process = self.server_child.take(); - let client_process = self.app_child.take(); - let processes = [server_process, client_process] - .into_iter() - .flatten() - .collect::>(); - - for mut process in processes { - let Some(pid) = process.id() else { - _ = process.kill().await; - continue; - }; - - // on unix, we can send a signal to the process to shut down - #[cfg(unix)] - { - _ = Command::new("kill") - .args(["-s", "TERM", &pid.to_string()]) - .spawn(); - } - - // on windows, use the `taskkill` command - #[cfg(windows)] - { - _ = Command::new("taskkill") - .args(["/F", "/PID", &pid.to_string()]) - .spawn(); - } - - // join the wait with a 100ms timeout - futures_util::select! { - _ = process.wait().fuse() => {} - _ = tokio::time::sleep(std::time::Duration::from_millis(1000)).fuse() => {} - }; - } - } - - /// Hotreload an asset in the running app. - /// - /// This will modify the build dir in place! Be careful! We generally assume you want all bundles - /// to reflect the latest changes, so we will modify the bundle. - /// - /// However, not all platforms work like this, so we might also need to update a separate asset - /// dir that the system simulator might be providing. We know this is the case for ios simulators - /// and haven't yet checked for android. - /// - /// This will return the bundled name of the asset such that we can send it to the clients letting - /// them know what to reload. It's not super important that this is robust since most clients will - /// kick all stylsheets without necessarily checking the name. - pub(crate) async fn hotreload_bundled_asset(&self, changed_file: &PathBuf) -> Option { - let mut bundled_name = None; - - // Use the build dir if there's no runtime asset dir as the override. For the case of ios apps, - // we won't actually be using the build dir. - let asset_dir = match self.runtime_asst_dir.as_ref() { - Some(dir) => dir.to_path_buf().join("assets/"), - None => self.app.build.asset_dir(), - }; - - tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}"); - - // If the asset shares the same name in the bundle, reload that - if let Some(legacy_asset_dir) = self.app.build.krate.legacy_asset_dir() { - if changed_file.starts_with(&legacy_asset_dir) { - tracing::debug!("Hotreloading legacy asset {changed_file:?}"); - let trimmed = changed_file.strip_prefix(legacy_asset_dir).unwrap(); - let res = std::fs::copy(changed_file, asset_dir.join(trimmed)); - bundled_name = Some(trimmed.to_path_buf()); - if let Err(e) = res { - tracing::debug!("Failed to hotreload legacy asset {e}"); - } - } - } - - // Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\". - let changed_file = dunce::canonicalize(changed_file) - .inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}")) - .ok()?; - - // The asset might've been renamed thanks to the manifest, let's attempt to reload that too - if let Some(resource) = self.app.app.assets.assets.get(&changed_file).as_ref() { - let output_path = asset_dir.join(resource.bundled_path()); - // Remove the old asset if it exists - _ = std::fs::remove_file(&output_path); - // And then process the asset with the options into the **old** asset location. If we recompiled, - // the asset would be in a new location because the contents and hash have changed. Since we are - // hotreloading, we need to use the old asset location it was originally written to. - let options = *resource.options(); - let res = process_file_to(&options, &changed_file, &output_path); - bundled_name = Some(PathBuf::from(resource.bundled_path())); - if let Err(e) = res { - tracing::debug!("Failed to hotreload asset {e}"); - } - } - - // If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext` - if self.app.build.build.platform() == Platform::Android { - if let Some(bundled_name) = bundled_name.as_ref() { - let target = dioxus_cli_config::android_session_cache_dir().join(bundled_name); - tracing::debug!("Pushing asset to device: {target:?}"); - let res = tokio::process::Command::new(DioxusCrate::android_adb()) - .arg("push") - .arg(&changed_file) - .arg(target) - .output() - .await - .context("Failed to push asset to device"); - - if let Err(e) = res { - tracing::debug!("Failed to push asset to device: {e}"); - } - } - } - - // Now we can return the bundled asset name to send to the hotreload engine - bundled_name - } - - /// Open the native app simply by running its main exe - /// - /// Eventually, for mac, we want to run the `.app` with `open` to fix issues with `dylib` paths, - /// but for now, we just run the exe directly. Very few users should be caring about `dylib` search - /// paths right now, but they will when we start to enable things like swift integration. - /// - /// Server/liveview/desktop are all basically the same, though - fn open_with_main_exe(&mut self, envs: Vec<(&str, String)>) -> Result { - // Create a new entropy app exe if we need to - let main_exe = self.app_exe(); - let child = Command::new(main_exe) - .envs(envs) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - - Ok(child) - } - - /// Open the web app by opening the browser to the given address. - /// Check if we need to use https or not, and if so, add the protocol. - /// Go to the basepath if that's set too. - fn open_web(&self, address: SocketAddr) { - let base_path = self.app.build.krate.config.web.app.base_path.clone(); - let https = self - .app - .build - .krate - .config - .web - .https - .enabled - .unwrap_or_default(); - let protocol = if https { "https" } else { "http" }; - let base_path = match base_path.as_deref() { - Some(base_path) => format!("/{}", base_path.trim_matches('/')), - None => "".to_owned(), - }; - _ = open::that_detached(format!("{protocol}://{address}{base_path}")); - } - - /// Use `xcrun` to install the app to the simulator - /// With simulators, we're free to basically do anything, so we don't need to do any fancy codesigning - /// or entitlements, or anything like that. - /// - /// However, if there's no simulator running, this *might* fail. - /// - /// TODO(jon): we should probably check if there's a simulator running before trying to install, - /// and open the simulator if we have to. - async fn open_ios_sim(&mut self, envs: Vec<(&str, String)>) -> Result { - tracing::debug!( - "Installing app to simulator {:?}", - self.app.build.root_dir() - ); - - let res = Command::new("xcrun") - .arg("simctl") - .arg("install") - .arg("booted") - .arg(self.app.build.root_dir()) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .output() - .await?; - - tracing::debug!("Installed app to simulator with exit code: {res:?}"); - - // Remap the envs to the correct simctl env vars - // iOS sim lets you pass env vars but they need to be in the format "SIMCTL_CHILD_XXX=XXX" - let ios_envs = envs - .iter() - .map(|(k, v)| (format!("SIMCTL_CHILD_{k}"), v.clone())); - - let child = Command::new("xcrun") - .arg("simctl") - .arg("launch") - .arg("--console") - .arg("booted") - .arg(self.app.build.krate.bundle_identifier()) - .envs(ios_envs) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .kill_on_drop(true) - .spawn()?; - - Ok(child) - } - - /// We have this whole thing figured out, but we don't actually use it yet. - /// - /// Launching on devices is more complicated and requires us to codesign the app, which we don't - /// currently do. - /// - /// Converting these commands shouldn't be too hard, but device support would imply we need - /// better support for codesigning and entitlements. - #[allow(unused)] - async fn open_ios_device(&self) -> Result<()> { - use serde_json::Value; - let app_path = self.app.build.root_dir(); - - install_app(&app_path).await?; - - // 2. Determine which device the app was installed to - let device_uuid = get_device_uuid().await?; - - // 3. Get the installation URL of the app - let installation_url = get_installation_url(&device_uuid, &app_path).await?; - - // 4. Launch the app into the background, paused - launch_app_paused(&device_uuid, &installation_url).await?; - - // 5. Pick up the paused app and resume it - resume_app(&device_uuid).await?; - - async fn install_app(app_path: &PathBuf) -> Result<()> { - let output = Command::new("xcrun") - .args(["simctl", "install", "booted"]) - .arg(app_path) - .output() - .await?; - - if !output.status.success() { - return Err(format!("Failed to install app: {:?}", output).into()); - } - - Ok(()) - } - - async fn get_device_uuid() -> Result { - let output = Command::new("xcrun") - .args([ - "devicectl", - "list", - "devices", - "--json-output", - "target/deviceid.json", - ]) - .output() - .await?; - - let json: Value = - serde_json::from_str(&std::fs::read_to_string("target/deviceid.json")?) - .context("Failed to parse xcrun output")?; - let device_uuid = json["result"]["devices"][0]["identifier"] - .as_str() - .ok_or("Failed to extract device UUID")? - .to_string(); - - Ok(device_uuid) - } - - async fn get_installation_url(device_uuid: &str, app_path: &Path) -> Result { - // xcrun devicectl device install app --device --path --json-output - let output = Command::new("xcrun") - .args([ - "devicectl", - "device", - "install", - "app", - "--device", - device_uuid, - &app_path.display().to_string(), - "--json-output", - "target/xcrun.json", - ]) - .output() - .await?; - - if !output.status.success() { - return Err(format!("Failed to install app: {:?}", output).into()); - } - - let json: Value = serde_json::from_str(&std::fs::read_to_string("target/xcrun.json")?) - .context("Failed to parse xcrun output")?; - let installation_url = json["result"]["installedApplications"][0]["installationURL"] - .as_str() - .ok_or("Failed to extract installation URL")? - .to_string(); - - Ok(installation_url) - } - - async fn launch_app_paused(device_uuid: &str, installation_url: &str) -> Result<()> { - let output = Command::new("xcrun") - .args([ - "devicectl", - "device", - "process", - "launch", - "--no-activate", - "--verbose", - "--device", - device_uuid, - installation_url, - "--json-output", - "target/launch.json", - ]) - .output() - .await?; - - if !output.status.success() { - return Err(format!("Failed to launch app: {:?}", output).into()); - } - - Ok(()) - } - - async fn resume_app(device_uuid: &str) -> Result<()> { - let json: Value = serde_json::from_str(&std::fs::read_to_string("target/launch.json")?) - .context("Failed to parse xcrun output")?; - - let status_pid = json["result"]["process"]["processIdentifier"] - .as_u64() - .ok_or("Failed to extract process identifier")?; - - let output = Command::new("xcrun") - .args([ - "devicectl", - "device", - "process", - "resume", - "--device", - device_uuid, - "--pid", - &status_pid.to_string(), - ]) - .output() - .await?; - - if !output.status.success() { - return Err(format!("Failed to resume app: {:?}", output).into()); - } - - Ok(()) - } - - unimplemented!("dioxus-cli doesn't support ios devices yet.") - } - - #[allow(unused)] - async fn codesign_ios(&self) -> Result<()> { - const CODESIGN_ERROR: &str = r#"This is likely because you haven't -- Created a provisioning profile before -- Accepted the Apple Developer Program License Agreement - -The agreement changes frequently and might need to be accepted again. -To accept the agreement, go to https://developer.apple.com/account - -To create a provisioning profile, follow the instructions here: -https://developer.apple.com/documentation/xcode/sharing-your-teams-signing-certificates"#; - - let profiles_folder = dirs::home_dir() - .context("Your machine has no home-dir")? - .join("Library/MobileDevice/Provisioning Profiles"); - - if !profiles_folder.exists() || profiles_folder.read_dir()?.next().is_none() { - tracing::error!( - r#"No provisioning profiles found when trying to codesign the app. -We checked the folder: {} - -{CODESIGN_ERROR} -"#, - profiles_folder.display() - ) - } - - let identities = Command::new("security") - .args(["find-identity", "-v", "-p", "codesigning"]) - .output() - .await - .context("Failed to run `security find-identity -v -p codesigning`") - .map(|e| { - String::from_utf8(e.stdout) - .context("Failed to parse `security find-identity -v -p codesigning`") - })??; - - // Parsing this: - // 51ADE4986E0033A5DB1C794E0D1473D74FD6F871 "Apple Development: jkelleyrtp@gmail.com (XYZYZY)" - let app_dev_name = regex::Regex::new(r#""Apple Development: (.+)""#) - .unwrap() - .captures(&identities) - .and_then(|caps| caps.get(1)) - .map(|m| m.as_str()) - .context( - "Failed to find Apple Development in `security find-identity -v -p codesigning`", - )?; - - // Acquire the provision file - let provision_file = profiles_folder - .read_dir()? - .flatten() - .find(|entry| { - entry - .file_name() - .to_str() - .map(|s| s.contains("mobileprovision")) - .unwrap_or_default() - }) - .context("Failed to find a provisioning profile. \n\n{CODESIGN_ERROR}")?; - - // The .mobileprovision file has some random binary thrown into into, but it's still basically a plist - // Let's use the plist markers to find the start and end of the plist - fn cut_plist(bytes: &[u8], byte_match: &[u8]) -> Option { - bytes - .windows(byte_match.len()) - .enumerate() - .rev() - .find(|(_, slice)| *slice == byte_match) - .map(|(i, _)| i + byte_match.len()) - } - let bytes = std::fs::read(provision_file.path())?; - let cut1 = cut_plist(&bytes, b""#.as_bytes()) - .context("Failed to parse .mobileprovision file")?; - let sub_bytes = &bytes[(cut1 - 6)..cut2]; - let mbfile: ProvisioningProfile = - plist::from_bytes(sub_bytes).context("Failed to parse .mobileprovision file")?; - - #[derive(serde::Deserialize, Debug)] - struct ProvisioningProfile { - #[serde(rename = "TeamIdentifier")] - team_identifier: Vec, - #[serde(rename = "ApplicationIdentifierPrefix")] - application_identifier_prefix: Vec, - #[serde(rename = "Entitlements")] - entitlements: Entitlements, - } - - #[derive(serde::Deserialize, Debug)] - struct Entitlements { - #[serde(rename = "application-identifier")] - application_identifier: String, - #[serde(rename = "keychain-access-groups")] - keychain_access_groups: Vec, - } - - let entielements_xml = format!( - r#" - - - - application-identifier - {APPLICATION_IDENTIFIER} - keychain-access-groups - - {APP_ID_ACCESS_GROUP}.* - - get-task-allow - - com.apple.developer.team-identifier - {TEAM_IDENTIFIER} - - "#, - APPLICATION_IDENTIFIER = mbfile.entitlements.application_identifier, - APP_ID_ACCESS_GROUP = mbfile.entitlements.keychain_access_groups[0], - TEAM_IDENTIFIER = mbfile.team_identifier[0], - ); - - // write to a temp file - let temp_file = tempfile::NamedTempFile::new()?; - std::fs::write(temp_file.path(), entielements_xml)?; - - // codesign the app - let output = Command::new("codesign") - .args([ - "--force", - "--entitlements", - temp_file.path().to_str().unwrap(), - "--sign", - app_dev_name, - ]) - .arg(self.app.build.root_dir()) - .output() - .await - .context("Failed to codesign the app")?; - - if !output.status.success() { - let stderr = String::from_utf8(output.stderr).unwrap_or_default(); - return Err(format!("Failed to codesign the app: {stderr}").into()); - } - - Ok(()) - } - - async fn open_android_sim( - &self, - devserver_socket: SocketAddr, - envs: Vec<(&'static str, String)>, - ) { - let apk_path = self.app.apk_path(); - let session_cache = self.app.build.krate.session_cache_dir(); - let full_mobile_app_name = self.app.build.krate.full_mobile_app_name(); - - // Start backgrounded since .open() is called while in the arm of the top-level match - tokio::task::spawn(async move { - let port = devserver_socket.port(); - if let Err(e) = Command::new("adb") - .arg("reverse") - .arg(format!("tcp:{}", port)) - .arg(format!("tcp:{}", port)) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .output() - .await - { - tracing::error!("failed to forward port {port}: {e}"); - } - - // Install - // adb install -r app-debug.apk - if let Err(e) = Command::new(DioxusCrate::android_adb()) - .arg("install") - .arg("-r") - .arg(apk_path) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .output() - .await - { - tracing::error!("Failed to install apk with `adb`: {e}"); - }; - - // Write the env vars to a .env file in our session cache - let env_file = session_cache.join(".env"); - let contents: String = envs - .iter() - .map(|(key, value)| format!("{key}={value}")) - .collect::>() - .join("\n"); - _ = std::fs::write(&env_file, contents); - - // Push the env file to the device - if let Err(e) = tokio::process::Command::new(DioxusCrate::android_adb()) - .arg("push") - .arg(env_file) - .arg(dioxus_cli_config::android_session_cache_dir().join(".env")) - .output() - .await - .context("Failed to push asset to device") - { - tracing::error!("Failed to push .env file to device: {e}"); - } - - // eventually, use the user's MainActivity, not our MainActivity - // adb shell am start -n dev.dioxus.main/dev.dioxus.main.MainActivity - let activity_name = format!("{}/dev.dioxus.main.MainActivity", full_mobile_app_name,); - - if let Err(e) = Command::new(DioxusCrate::android_adb()) - .arg("shell") - .arg("am") - .arg("start") - .arg("-n") - .arg(activity_name) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .output() - .await - { - tracing::error!("Failed to start app with `adb`: {e}"); - }; - }); - } - - fn make_entropy_path(exe: &PathBuf) -> PathBuf { - let id = uuid::Uuid::new_v4(); - let name = id.to_string(); - let some_entropy = name.split('-').next().unwrap(); - - // Make a copy of the server exe with a new name - let entropy_server_exe = exe.with_file_name(format!( - "{}-{}", - exe.file_name().unwrap().to_str().unwrap(), - some_entropy - )); - - std::fs::copy(exe, &entropy_server_exe).unwrap(); - - entropy_server_exe - } - - fn server_exe(&mut self) -> Option { - let mut server = self.app.server_exe()?; - - // Create a new entropy server exe if we need to - if cfg!(target_os = "windows") || cfg!(target_os = "linux") { - // If we already have an entropy server exe, return it - this is useful for re-opening the same app - if let Some(existing_server) = self.entropy_server_exe.clone() { - return Some(existing_server); - } - - // Otherwise, create a new entropy server exe and save it for re-opning - let entropy_server_exe = Self::make_entropy_path(&server); - self.entropy_server_exe = Some(entropy_server_exe.clone()); - server = entropy_server_exe; - } - - Some(server) - } - - fn app_exe(&mut self) -> PathBuf { - let mut main_exe = self.app.main_exe(); - - // The requirement here is based on the platform, not necessarily our current architecture. - let requires_entropy = match self.app.build.build.platform() { - // When running "bundled", we don't need entropy - Platform::Web => false, - Platform::MacOS => false, - Platform::Ios => false, - Platform::Android => false, - - // But on platforms that aren't running as "bundled", we do. - Platform::Windows => true, - Platform::Linux => true, - Platform::Server => true, - Platform::Liveview => true, - }; - - if requires_entropy || std::env::var("DIOXUS_ENTROPY").is_ok() { - // If we already have an entropy app exe, return it - this is useful for re-opening the same app - if let Some(existing_app_exe) = self.entropy_app_exe.clone() { - return existing_app_exe; - } - - let entropy_app_exe = Self::make_entropy_path(&main_exe); - self.entropy_app_exe = Some(entropy_app_exe.clone()); - main_exe = entropy_app_exe; - } - - main_exe - } -} diff --git a/packages/cli/src/serve/mod.rs b/packages/cli/src/serve/mod.rs index cf33461be8..50cd16df63 100644 --- a/packages/cli/src/serve/mod.rs +++ b/packages/cli/src/serve/mod.rs @@ -1,21 +1,16 @@ -use crate::{BuildUpdate, Builder, Error, Platform, Result, ServeArgs, TraceController, TraceSrc}; +use crate::{AppBuilder, BuildId, BuildMode, BuilderUpdate, Result, ServeArgs, TraceController}; mod ansi_buffer; -mod detect; -mod handle; mod output; mod proxy; mod runner; mod server; mod update; -mod watcher; -pub(crate) use handle::*; pub(crate) use output::*; pub(crate) use runner::*; pub(crate) use server::*; pub(crate) use update::*; -pub(crate) use watcher::*; /// For *all* builds, the CLI spins up a dedicated webserver, file watcher, and build infrastructure to serve the project. /// @@ -23,11 +18,11 @@ pub(crate) use watcher::*; /// /// Platform specifics: /// ------------------- -/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We -/// want to emulate GithubPages here since most folks are deploying there and expect things like -/// basepath to match. -/// - Desktop: We spin up the dev server but without a filesystem server. -/// - Mobile: Basically the same as desktop. +/// - Web: We need to attach a filesystem server to our devtools webserver to serve the project. We +/// want to emulate GithubPages here since most folks are deploying there and expect things like +/// basepath to match. +/// - Desktop: We spin up the dev server but without a filesystem server. +/// - Mobile: Basically the same as desktop. /// /// When fullstack is enabled, we'll also build for the `server` target and then hotreload the server. /// The "server" is special here since "fullstack" is functionally just an addition to the regular client @@ -37,19 +32,11 @@ pub(crate) use watcher::*; /// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly. /// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server /// to a dynamic one on the fly. -pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { - // Redirect all logging the cli logger - let mut tracer = TraceController::redirect(args.is_interactive_tty()); - - // Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first - let krate = args.load_krate().await?; - - // Note that starting the builder will queue up a build immediately - let mut screen = Output::start(&args).await?; - let mut builder = Builder::start(&krate, args.build_args())?; - let mut devserver = WebServer::start(&krate, &args)?; - let mut watcher = Watcher::start(&krate, &args); - let mut runner = AppRunner::start(&krate); +pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> { + // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc + let mut builder = AppServer::start(args).await?; + let mut devserver = WebServer::start(&builder)?; + let mut screen = Output::start(builder.interactive).await?; // This is our default splash screen. We might want to make this a fancier splash screen in the future // Also, these commands might not be the most important, but it's all we've got enabled right now @@ -63,166 +50,137 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { • Press `/` for more commands and shortcuts Learn more at https://dioxuslabs.com/learn/0.6/getting_started ----------------------------------------------------------------"#, - krate.executable_name() + builder.app_name() ); - let err: Result<(), Error> = loop { + loop { // Draw the state of the server to the screen - screen.render(&args, &krate, &builder, &devserver, &watcher); + screen.render(&builder, &devserver); // And then wait for any updates before redrawing let msg = tokio::select! { - msg = builder.wait() => ServeUpdate::BuildUpdate(msg), - msg = watcher.wait() => msg, + msg = builder.wait() => msg, msg = devserver.wait() => msg, msg = screen.wait() => msg, - msg = runner.wait() => msg, msg = tracer.wait() => msg, }; match msg { ServeUpdate::FilesChanged { files } => { - if files.is_empty() || !args.should_hotreload() { + if files.is_empty() || !builder.hot_reload { continue; } - let file = files[0].display().to_string(); - let file = file.trim_start_matches(&krate.crate_dir().display().to_string()); - - // if change is hotreloadable, hotreload it - // and then send that update to all connected clients - if let Some(hr) = runner.attempt_hot_reload(files).await { - // Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild - // - // Also make sure the builder isn't busy since that might cause issues with hotreloads - // https://github.com/DioxusLabs/dioxus/issues/3361 - if hr.is_empty() || !builder.can_receive_hotreloads() { - tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file); - continue; - } - - tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {}", file); - - devserver.send_hotreload(hr).await; - } else if runner.should_full_rebuild { - tracing::info!(dx_src = ?TraceSrc::Dev, "Full rebuild: {}", file); - - // We're going to kick off a new build, interrupting the current build if it's ongoing - builder.rebuild(args.build_arguments.clone()); - - // Clear the hot reload changes so we don't have out-of-sync issues with changed UI - runner.clear_hot_reload_changes(); - runner.file_map.force_rebuild(); + tracing::debug!("Starting hotpatching: {:?}", files); + builder.handle_file_change(&files, &mut devserver).await; + } - // Tell the server to show a loading page for any new requests - devserver.send_reload_start().await; - devserver.start_build().await; - } else { - tracing::warn!( - "Rebuild required but is currently paused - press `r` to rebuild manually" - ) - } + ServeUpdate::RequestRebuild => { + // The spacing here is important-ish: we want + // `Full rebuild:` to line up with + // `Hotreloading:` to keep the alignment during long edit sessions + // `Hot-patching:` to keep the alignment during long edit sessions + tracing::info!("Full rebuild: triggered manually"); + builder.full_rebuild().await; + devserver.send_reload_start().await; + devserver.start_build().await; } // Run the server in the background // Waiting for updates here lets us tap into when clients are added/removed - ServeUpdate::NewConnection => { + ServeUpdate::NewConnection { id, aslr_reference } => { + // Send the client devserver - .send_hotreload(runner.applied_hot_reload_changes()) + .send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT)) .await; - runner.client_connected().await; + if builder.server.is_some() { + devserver + .send_hotreload(builder.applied_hot_reload_changes(BuildId::SERVER)) + .await; + } + + builder.client_connected(id, aslr_reference).await; } // Received a message from the devtools server - currently we only use this for // logging, so we just forward it the tui - ServeUpdate::WsMessage(msg) => { - screen.push_ws_message(Platform::Web, msg); + ServeUpdate::WsMessage { msg, platform } => { + screen.push_ws_message(platform, &msg); } // Wait for logs from the build engine // These will cause us to update the screen // We also can check the status of the builds here in case we have multiple ongoing builds - ServeUpdate::BuildUpdate(update) => { + ServeUpdate::BuilderUpdate { id, update } => { + let platform = builder.get_build(id).unwrap().build.platform; + // Queue any logs to be printed if need be screen.new_build_update(&update); // And then update the websocketed clients with the new build status in case they want it - devserver.new_build_update(&update, &builder).await; + devserver.new_build_update(&update).await; // And then open the app if it's ready - // todo: there might be more things to do here that require coordination with other pieces of the CLI - // todo: maybe we want to shuffle the runner around to send an "open" command instead of doing that match update { - BuildUpdate::Progress { .. } => {} - BuildUpdate::CompilerMessage { message } => { + BuilderUpdate::Progress { .. } => {} + BuilderUpdate::CompilerMessage { message } => { screen.push_cargo_log(message); } - BuildUpdate::BuildFailed { err } => { - tracing::error!("Build failed: {:?}", err); + BuilderUpdate::BuildFailed { err } => { + tracing::error!("Build failed: {:#?}", err); } - BuildUpdate::BuildReady { bundle } => { - let handle = runner - .open( - bundle, - devserver.devserver_address(), - devserver.displayed_address(), - devserver.proxied_server_address(), - args.open.unwrap_or(false), - ) - .await; - - match handle { - // Update the screen + devserver with the new handle info - Ok(_handle) => { - devserver.send_reload_command().await; + BuilderUpdate::BuildReady { bundle } => match bundle.mode { + BuildMode::Thin { ref cache, .. } => { + let elapsed = + bundle.time_end.duration_since(bundle.time_start).unwrap(); + match builder.hotpatch(&bundle, id, cache).await { + Ok(jumptable) => devserver.send_patch(jumptable, elapsed, id).await, + Err(err) => { + tracing::error!("Failed to hot-patch app: {err}"); + + if matches!(err, crate::Error::PatchingFailed(_)) { + tracing::info!("Starting full rebuild: {err}"); + builder.full_rebuild().await; + devserver.send_reload_start().await; + devserver.start_build().await; + } + } } - - Err(e) => tracing::error!("Failed to open app: {}", e), + } + BuildMode::Base | BuildMode::Fat => { + _ = builder + .open(bundle, &mut devserver) + .await + .inspect_err(|e| tracing::error!("Failed to open app: {}", e)); + } + }, + BuilderUpdate::StdoutReceived { msg } => { + screen.push_stdio(platform, msg, tracing::Level::INFO); + } + BuilderUpdate::StderrReceived { msg } => { + screen.push_stdio(platform, msg, tracing::Level::ERROR); + } + BuilderUpdate::ProcessExited { status } => { + if status.success() { + tracing::info!( + r#"Application [{platform}] exited gracefully. + • To restart the app, press `r` to rebuild or `o` to open + • To exit the server, press `ctrl+c`"# + ); + } else { + tracing::error!("Application [{platform}] exited with error: {status}"); } } } } - // If the process exited *cleanly*, we can exit - ServeUpdate::ProcessExited { status, platform } => { - if !status.success() { - tracing::error!("Application [{platform}] exited with error: {status}"); - } else { - tracing::info!( - r#"Application [{platform}] exited gracefully. - - To restart the app, press `r` to rebuild or `o` to open - - To exit the server, press `ctrl+c`"# - ); - } - } - - ServeUpdate::StdoutReceived { platform, msg } => { - screen.push_stdio(platform, msg, tracing::Level::INFO); - } - - ServeUpdate::StderrReceived { platform, msg } => { - screen.push_stdio(platform, msg, tracing::Level::ERROR); - } - ServeUpdate::TracingLog { log } => { screen.push_log(log); } - ServeUpdate::RequestRebuild => { - // The spacing here is important-ish: we want - // `Full rebuild:` to line up with - // `Hotreloading:` to keep the alignment during long edit sessions - tracing::info!("Full rebuild: triggered manually"); - - builder.rebuild(args.build_arguments.clone()); - runner.file_map.force_rebuild(); - devserver.send_reload_start().await; - devserver.start_build().await - } - ServeUpdate::OpenApp => { - if let Err(err) = runner.open_existing(&devserver).await { + if let Err(err) = builder.open_all(&devserver, true).await { tracing::error!("Failed to open app: {err}") } } @@ -232,10 +190,10 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { } ServeUpdate::ToggleShouldRebuild => { - runner.should_full_rebuild = !runner.should_full_rebuild; + builder.automatic_rebuilds = !builder.automatic_rebuilds; tracing::info!( "Automatic rebuilds are currently: {}", - if runner.should_full_rebuild { + if builder.automatic_rebuilds { "enabled" } else { "disabled" @@ -243,21 +201,16 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { ) } - ServeUpdate::Exit { error } => match error { - Some(err) => break Err(anyhow::anyhow!("{}", err).into()), - None => break Ok(()), - }, - } - }; + ServeUpdate::Exit { error } => { + _ = builder.cleanup_all().await; + _ = devserver.shutdown().await; + _ = screen.shutdown(); - _ = runner.cleanup().await; - _ = devserver.shutdown().await; - builder.abort_all(); - _ = screen.shutdown(); - - if let Err(err) = err { - eprintln!("Exiting with error: {}", err); + match error { + Some(err) => return Err(anyhow::anyhow!("{}", err).into()), + None => return Ok(()), + } + } + } } - - Ok(()) } diff --git a/packages/cli/src/serve/output.rs b/packages/cli/src/serve/output.rs index 2a61487ec5..b1ef92e78a 100644 --- a/packages/cli/src/serve/output.rs +++ b/packages/cli/src/serve/output.rs @@ -1,7 +1,6 @@ use crate::{ - serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer}, - BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent, - TraceMsg, TraceSrc, + serve::{ansi_buffer::AnsiStringLine, ServeUpdate, WebServer}, + BuildStage, BuilderUpdate, Platform, TraceContent, TraceMsg, TraceSrc, }; use crossterm::{ cursor::{Hide, Show}, @@ -26,6 +25,8 @@ use std::{ }; use tracing::Level; +use super::AppServer; + const TICK_RATE_MS: u64 = 100; const VIEWPORT_MAX_WIDTH: u16 = 100; const VIEWPORT_HEIGHT_SMALL: u16 = 5; @@ -46,7 +47,6 @@ pub struct Output { // A list of all messages from build, dev, app, and more. more_modal_open: bool, interactive: bool, - platform: Platform, // Whether to show verbose logs or not // We automatically hide "debug" logs if verbose is false (only showing "info" / "warn" / "error") @@ -64,31 +64,24 @@ pub struct Output { // ! needs to be wrapped in an &mut since `render stateful widget` requires &mut... but our // "render" method only borrows &self (for no particular reason at all...) throbber: RefCell, - - rustc_details: RustcDetails, } -#[allow(unused)] #[derive(Clone, Copy)] struct RenderState<'a> { - opts: &'a ServeArgs, - krate: &'a DioxusCrate, - build_engine: &'a Builder, + runner: &'a AppServer, server: &'a WebServer, - watcher: &'a Watcher, } impl Output { - pub(crate) async fn start(cfg: &ServeArgs) -> crate::Result { + pub(crate) async fn start(interactive: bool) -> crate::Result { let mut output = Self { + interactive, term: Rc::new(RefCell::new(None)), - interactive: cfg.is_interactive_tty(), dx_version: format!( "{}-{}", env!("CARGO_PKG_VERSION"), crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("main") ), - platform: cfg.build_arguments.platform.expect("To be resolved by now"), events: None, more_modal_open: false, pending_logs: VecDeque::new(), @@ -101,7 +94,6 @@ impl Output { interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); interval }, - rustc_details: RustcDetails::from_cli().await?, }; output.startup()?; @@ -113,15 +105,6 @@ impl Output { /// This is meant to be paired with "shutdown" to restore the terminal to its original state. fn startup(&mut self) -> io::Result<()> { if self.interactive { - // set the panic hook to fix the terminal in the event of a panic - // The terminal might be left in a wonky state if a panic occurs, and we don't want it to be completely broken - let original_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - _ = disable_raw_mode(); - _ = stdout().execute(Show); - original_hook(info); - })); - // Check if writing the terminal is going to block infinitely. // If it does, we should disable interactive mode. This ensures we work with programs like `bg` // which suspend the process and cause us to block when writing output. @@ -183,7 +166,12 @@ impl Output { /// Call the shutdown functions that might mess with the terminal settings - see the related code /// in "startup" for more details about what we need to unset pub(crate) fn shutdown(&self) -> io::Result<()> { - if self.interactive { + Self::remote_shutdown(self.interactive)?; + Ok(()) + } + + pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> { + if interactive { stdout() .execute(Show)? .execute(DisableFocusChange)? @@ -329,7 +317,7 @@ impl Output { } /// Push a message from the websocket to the logs - pub fn push_ws_message(&mut self, platform: Platform, message: axum::extract::ws::Message) { + pub fn push_ws_message(&mut self, platform: Platform, message: &axum::extract::ws::Message) { use dioxus_devtools_types::ClientMsg; // We can only handle text messages from the websocket... @@ -341,14 +329,18 @@ impl Output { let res = serde_json::from_str::(text.as_str()); // Client logs being errors aren't fatal, but we should still report them them - let ClientMsg::Log { level, messages } = match res { + let msg = match res { Ok(msg) => msg, Err(err) => { - tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {}", platform, err); + tracing::error!(dx_src = ?TraceSrc::Dev, "Error parsing message from {}: {} -> {:?}", platform, err, text.as_str()); return; } }; + let ClientMsg::Log { level, messages } = msg else { + return; + }; + // FIXME(jon): why are we pulling only the first message here? let content = messages.first().unwrap_or(&String::new()).clone(); @@ -373,26 +365,19 @@ impl Output { /// approach, but then we'd need to do that *everywhere* instead of simply performing a react-like /// re-render when external state changes. Ratatui will diff the intermediate buffer, so we at least /// we won't be drawing it. - pub(crate) fn new_build_update(&mut self, update: &BuildUpdate) { + pub(crate) fn new_build_update(&mut self, update: &BuilderUpdate) { match update { - BuildUpdate::Progress { + BuilderUpdate::Progress { stage: BuildStage::Starting { .. }, } => self.tick_animation = true, - BuildUpdate::BuildReady { .. } => self.tick_animation = false, - BuildUpdate::BuildFailed { .. } => self.tick_animation = false, + BuilderUpdate::BuildReady { .. } => self.tick_animation = false, + BuilderUpdate::BuildFailed { .. } => self.tick_animation = false, _ => {} } } /// Render the current state of everything to the console screen - pub fn render( - &mut self, - opts: &ServeArgs, - config: &DioxusCrate, - build_engine: &Builder, - server: &WebServer, - watcher: &Watcher, - ) { + pub fn render(&mut self, runner: &AppServer, server: &WebServer) { if !self.interactive { return; } @@ -409,16 +394,7 @@ impl Output { // Then, draw the frame, passing along all the state of the TUI so we can render it properly _ = term.draw(|frame| { - self.render_frame( - frame, - RenderState { - opts, - krate: config, - build_engine, - server, - watcher, - }, - ); + self.render_frame(frame, RenderState { runner, server }); }); } @@ -488,39 +464,46 @@ impl Output { ]) .areas(gauge_area); + let client = &state.runner.client(); self.render_single_gauge( frame, app_progress, - state.build_engine.compile_progress(), + client.compile_progress(), "App: ", state, - state.build_engine.compile_duration(), + client.compile_duration(), ); - if state.build_engine.request.build.fullstack() { + if state.runner.is_fullstack() { self.render_single_gauge( frame, second_progress, - state.build_engine.server_compile_progress(), + state.runner.server_compile_progress(), "Server: ", state, - state.build_engine.compile_duration(), + client.compile_duration(), ); } else { self.render_single_gauge( frame, second_progress, - state.build_engine.bundle_progress(), + client.bundle_progress(), "Bundle: ", state, - state.build_engine.bundle_duration(), + client.bundle_duration(), ); } let mut lines = vec!["Status: ".white()]; - match &state.build_engine.stage { + match &client.stage { BuildStage::Initializing => lines.push("Initializing".yellow()), - BuildStage::Starting { .. } => lines.push("Starting build".yellow()), + BuildStage::Starting { patch, .. } => { + if *patch { + lines.push("Hot-patching...".yellow()) + } else { + lines.push("Starting build".yellow()) + } + } BuildStage::InstallingTooling => lines.push("Installing tooling".yellow()), BuildStage::Compiling { current, @@ -535,7 +518,6 @@ impl Output { BuildStage::OptimizingWasm => lines.push("Optimizing wasm".yellow()), BuildStage::SplittingBundle => lines.push("Splitting bundle".yellow()), BuildStage::CompressingAssets => lines.push("Compressing assets".yellow()), - BuildStage::PrerenderingRoutes => lines.push("Prerendering static routes".yellow()), BuildStage::RunningBindgen => lines.push("Running wasm-bindgen".yellow()), BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()), BuildStage::Bundling => lines.push("Bundling app".yellow()), @@ -552,15 +534,18 @@ impl Output { } BuildStage::Success => { lines.push("Serving ".yellow()); - lines.push(state.krate.executable_name().white()); + lines.push(client.build.executable_name().white()); lines.push(" 🚀 ".green()); - if let Some(comp_time) = state.build_engine.total_build_time() { + if let Some(comp_time) = client.total_build_time() { lines.push(format!("{:.1}s", comp_time.as_secs_f32()).dark_gray()); } } BuildStage::Failed => lines.push("Failed".red()), BuildStage::Aborted => lines.push("Aborted".red()), BuildStage::Restarting => lines.push("Restarting".yellow()), + BuildStage::Linking => lines.push("Linking".yellow()), + BuildStage::Hotpatching => lines.push("Hot-patching...".yellow()), + BuildStage::ExtractingAssets => lines.push("Extracting assets".yellow()), _ => {} }; @@ -576,7 +561,7 @@ impl Output { state: RenderState, time_taken: Option, ) { - let failed = state.build_engine.stage == BuildStage::Failed; + let failed = state.runner.client.stage == BuildStage::Failed; let value = if failed { 1.0 } else { value.clamp(0.0, 1.0) }; let [gauge_row, _, icon] = Layout::horizontal([ @@ -647,11 +632,12 @@ impl Output { ]) .areas(area); + let client = &state.runner.client(); frame.render_widget( Paragraph::new(Line::from(vec![ "Platform: ".gray(), - self.platform.expected_name().yellow(), - if state.opts.build_arguments.fullstack() { + client.build.platform.expected_name().yellow(), + if state.runner.is_fullstack() { " + fullstack".yellow() } else { " ".dark_gray() @@ -671,7 +657,7 @@ impl Output { frame.render_widget_ref( Paragraph::new(Line::from(vec![ - if self.platform == Platform::Web { + if client.build.platform == Platform::Web { "Serving at: ".gray() } else { "ServerFns at: ".gray() @@ -688,7 +674,7 @@ impl Output { Paragraph::new(Line::from({ let mut lines = vec!["App features: ".gray(), "[".yellow()]; - let feature_list: Vec = state.build_engine.request.all_target_features(); + let feature_list: Vec = state.runner.client().build.all_target_features(); let num_features = feature_list.len(); for (idx, feature) in feature_list.into_iter().enumerate() { @@ -737,7 +723,7 @@ impl Output { frame.render_widget( Paragraph::new(Line::from(vec![ "rustc: ".gray(), - self.rustc_details.version.as_str().yellow(), + state.runner.workspace.rustc_version.as_str().yellow(), ])), meta_list[2], ); @@ -869,8 +855,8 @@ impl Output { .iter() .map(|line| { // Very important to strip ansi codes before counting graphemes - the ansi codes count as multiple graphemes! - let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count() as u16; - grapheme_count.max(1).div_ceil(term_size.width) + let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count(); + grapheme_count.max(1).div_ceil(term_size.width as usize) as u16 }) .sum::(); @@ -1005,7 +991,11 @@ impl Output { // Create the ansi -> raw string line with a width of either the viewport width or the max width let line_length = line.styled_graphemes(Style::default()).count(); - lines.push(AnsiStringLine::new(line_length as _).render(&line)); + if line_length < u16::MAX as usize { + lines.push(AnsiStringLine::new(line_length as _).render(&line)); + } else { + lines.push(line.to_string()) + } } } diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index 8c1ede569e..8a68979f3d 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -1,252 +1,654 @@ -use super::{AppHandle, ServeUpdate, WebServer}; +use super::{AppBuilder, ServeUpdate, WebServer}; use crate::{ - AppBundle, DioxusCrate, HotreloadFilemap, HotreloadResult, Platform, Result, TraceSrc, + BuildArtifacts, BuildId, BuildMode, BuildTargets, Error, HotpatchModuleCache, Platform, Result, + ServeArgs, TraceSrc, Workspace, +}; +use anyhow::Context; +use dioxus_core::internal::{ + HotReloadTemplateWithLocation, HotReloadedTemplate, TemplateGlobalKey, }; -use dioxus_core::internal::TemplateGlobalKey; use dioxus_devtools_types::HotReloadMsg; +use dioxus_dx_wire_format::BuildStage; use dioxus_html::HtmlCtx; +use dioxus_rsx::CallBody; +use dioxus_rsx_hotreload::{ChangedRsx, HotReloadResult}; +use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures_util::future::OptionFuture; -use ignore::gitignore::Gitignore; +use futures_util::StreamExt; +use krates::NodeId; +use notify::{ + event::{MetadataKind, ModifyKind}, + Config, EventKind, RecursiveMode, Watcher as NotifyWatcher, +}; use std::{ collections::{HashMap, HashSet}, - net::SocketAddr, + net::{IpAddr, TcpListener}, path::PathBuf, + sync::Arc, + time::Duration, }; +use subsecond_types::JumpTable; +use syn::spanned::Spanned; +use tokio::process::Command; + +/// This is the primary "state" object that holds the builds and handles for the running apps. +/// +/// It also holds the watcher which is used to watch for changes in the filesystem and trigger rebuilds, +/// hotreloads, asset updates, etc. +/// +/// Since we resolve the build request before initializing the CLI, it also serves as a place to store +/// resolved "serve" arguments, which is why it takes ServeArgs instead of BuildArgs. Simply wrap the +/// BuildArgs in a default ServeArgs and pass it in. +pub(crate) struct AppServer { + /// the platform of the "primary" crate (ie the first) + pub(crate) workspace: Arc, + + pub(crate) client: AppBuilder, + pub(crate) server: Option, + + // Related to to the filesystem watcher + pub(crate) watcher: Box, + pub(crate) _watcher_tx: UnboundedSender, + pub(crate) watcher_rx: UnboundedReceiver, -pub(crate) struct AppRunner { - pub(crate) running: Option, - pub(crate) krate: DioxusCrate, - pub(crate) file_map: HotreloadFilemap, - pub(crate) ignore: Gitignore, + // Tracked state related to open builds and hot reloading pub(crate) applied_hot_reload_message: HotReloadMsg, - pub(crate) builds_opened: usize, - pub(crate) should_full_rebuild: bool, + pub(crate) file_map: HashMap, + + // Resolved args related to how we go about processing the rebuilds and logging + pub(crate) use_hotpatch_engine: bool, + pub(crate) automatic_rebuilds: bool, + pub(crate) interactive: bool, + pub(crate) _force_sequential: bool, + pub(crate) hot_reload: bool, + pub(crate) open_browser: bool, + pub(crate) _wsl_file_poll_interval: u16, + pub(crate) always_on_top: bool, + pub(crate) fullstack: bool, + pub(crate) watch_fs: bool, + + // resolve args related to the webserver + pub(crate) devserver_port: u16, + pub(crate) devserver_bind_ip: IpAddr, + pub(crate) proxied_port: Option, + pub(crate) cross_origin_policy: bool, } -impl AppRunner { +pub(crate) struct CachedFile { + contents: String, + most_recent: Option, + templates: HashMap, +} + +impl AppServer { /// Create the AppRunner and then initialize the filemap with the crate directory. - pub(crate) fn start(krate: &DioxusCrate) -> Self { + pub(crate) async fn start(args: ServeArgs) -> Result { + let workspace = Workspace::current().await?; + + // Resolve the simpler args + let interactive = args.is_interactive_tty(); + let force_sequential = args.force_sequential; + let cross_origin_policy = args.cross_origin_policy; + + // These come from the args but also might come from the workspace settings + // We opt to use the manually specified args over the workspace settings + let hot_reload = args + .hot_reload + .unwrap_or_else(|| workspace.settings.always_hot_reload.unwrap_or(true)); + + let open_browser = args + .open + .unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or_default()); + + let wsl_file_poll_interval = args + .wsl_file_poll_interval + .unwrap_or_else(|| workspace.settings.wsl_file_poll_interval.unwrap_or(2)); + + let always_on_top = args + .always_on_top + .unwrap_or_else(|| workspace.settings.always_on_top.unwrap_or(true)); + + // Use 127.0.0.1 as the default address if none is specified. + // If the user wants to export on the network, they can use `0.0.0.0` instead. + let devserver_bind_ip = args.address.addr.unwrap_or(WebServer::SELF_IP); + + // If the user specified a port, use that, otherwise use any available port, preferring 8080 + let devserver_port = args + .address + .port + .unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080)); + + // Spin up the file watcher + let (watcher_tx, watcher_rx) = futures_channel::mpsc::unbounded(); + let watcher = create_notify_watcher(watcher_tx.clone(), wsl_file_poll_interval as u64); + + let BuildTargets { client, server } = args.targets.into_targets().await?; + + // All servers will end up behind us (the devserver) but on a different port + // This is so we can serve a loading screen as well as devtools without anything particularly fancy + let fullstack = server.is_some(); + let should_proxy_port = match client.platform { + Platform::Server => true, + _ => fullstack, + }; + + let proxied_port = should_proxy_port + .then(|| get_available_port(devserver_bind_ip, None)) + .flatten(); + + let watch_fs = args.watch.unwrap_or(true); + let use_hotpatch_engine = args.hot_patch; + let build_mode = match use_hotpatch_engine { + true => BuildMode::Fat, + false => BuildMode::Base, + }; + + let client = AppBuilder::start(&client, build_mode.clone())?; + let server = server + .map(|server| AppBuilder::start(&server, build_mode)) + .transpose()?; + + tracing::debug!("Proxied port: {:?}", proxied_port); + + // Create the runner let mut runner = Self { - running: Default::default(), - file_map: HotreloadFilemap::new(), + file_map: Default::default(), applied_hot_reload_message: Default::default(), - ignore: krate.workspace_gitignore(), - krate: krate.clone(), - builds_opened: 0, - should_full_rebuild: true, + automatic_rebuilds: true, + watch_fs, + use_hotpatch_engine, + client, + server, + hot_reload, + open_browser, + _wsl_file_poll_interval: wsl_file_poll_interval, + always_on_top, + workspace, + devserver_port, + devserver_bind_ip, + proxied_port, + watcher, + watcher_rx, + _watcher_tx: watcher_tx, + interactive, + _force_sequential: force_sequential, + cross_origin_policy, + fullstack, }; - // todo(jon): this might take a while so we should try and background it, or make it lazy somehow - // we could spawn a thread to search the FS and then when it returns we can fill the filemap - // in testing, if this hits a massive directory, it might take several seconds with no feedback. - for krate in krate.all_watched_crates() { - runner.fill_filemap(krate); - } + // Only register the hot-reload stuff if we're watching the filesystem + if runner.watch_fs { + // Spin up the notify watcher + // When builds load though, we're going to parse their depinfo and add the paths to the watcher + runner.watch_filesystem(); - // Ensure the session cache dir exists and is empty - runner.flush_session_cache(); + // todo(jon): this might take a while so we should try and background it, or make it lazy somehow + // we could spawn a thread to search the FS and then when it returns we can fill the filemap + // in testing, if this hits a massive directory, it might take several seconds with no feedback. + // really, we should be using depinfo to get the files that are actually used, but the depinfo file might not be around yet + // todo(jon): see if we can just guess the depinfo file before it generates. might be stale but at least it catches most of the files + runner.load_rsx_filemap(); + } - runner + Ok(runner) } pub(crate) async fn wait(&mut self) -> ServeUpdate { - // If there are no running apps, we can just return pending to avoid deadlocking - let Some(handle) = self.running.as_mut() else { - return futures_util::future::pending().await; - }; + let client = &mut self.client; + let server = self.server.as_mut(); + + let client_wait = client.wait(); + let server_wait = OptionFuture::from(server.map(|s| s.wait())); + let watcher_wait = self.watcher_rx.next(); - use ServeUpdate::*; - let platform = handle.app.build.build.platform(); tokio::select! { - Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => { - StdoutReceived { platform, msg } - }, - Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => { - StderrReceived { platform, msg } - }, - Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => { - match status { - Ok(status) => { - handle.app_child = None; - ProcessExited { status, platform } - }, - Err(_err) => todo!("handle error in process joining?"), + // Wait for the client to finish + client_update = client_wait => { + ServeUpdate::BuilderUpdate { + id: BuildId::CLIENT, + update: client_update, } } - Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => { - StdoutReceived { platform: Platform::Server, msg } - }, - Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => { - StderrReceived { platform: Platform::Server, msg } - }, - Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => { - match status { - Ok(status) => { - handle.server_child = None; - ProcessExited { status, platform } - }, - Err(_err) => todo!("handle error in process joining?"), + + Some(server_update) = server_wait => { + ServeUpdate::BuilderUpdate { + id: BuildId::SERVER, + update: server_update, } } - else => futures_util::future::pending().await - } - } - /// Finally "bundle" this app and return a handle to it - pub(crate) async fn open( - &mut self, - app: AppBundle, - devserver_ip: SocketAddr, - open_address: Option, - fullstack_address: Option, - should_open_web: bool, - ) -> Result<&AppHandle> { - // Drop the old handle - // This is a more forceful kill than soft_kill since the app entropy will be wiped - self.cleanup().await; - - // Add some cute logging - if self.builds_opened == 0 { - tracing::info!( - "Build completed successfully in {:?}ms, launching app! 💫", - app.app.time_taken.as_millis() - ); - } else { - tracing::info!("Build completed in {:?}ms", app.app.time_taken.as_millis()); - } + // Wait for the watcher to send us an event + event = watcher_wait => { + let mut changes: Vec<_> = event.into_iter().collect(); - // Start the new app before we kill the old one to give it a little bit of time - let mut handle = AppHandle::new(app).await?; - handle - .open( - devserver_ip, - open_address, - fullstack_address, - self.builds_opened == 0 && should_open_web, - ) - .await?; + // Dequeue in bulk if we can, we might've received a lot of events in one go + while let Some(event) = self.watcher_rx.try_next().ok().flatten() { + changes.push(event); + } - self.builds_opened += 1; - self.running = Some(handle); + // Filter the changes + let mut files: Vec = vec![]; - Ok(self.running.as_ref().unwrap()) - } + // Decompose the events into a list of all the files that have changed + for event in changes.drain(..) { + // Make sure we add new folders to the watch list, provided they're not matched by the ignore list + // We'll only watch new folders that are found under the crate, and then update our watcher to watch them + // This unfortunately won't pick up new krates added "at a distance" - IE krates not within the workspace. + if let EventKind::Create(_create_kind) = event.kind { + // If it's a new folder, watch it + // If it's a new cargo.toml (ie dep on the fly), + // todo(jon) support new folders on the fly + } - /// Open an existing app bundle, if it exists - pub(crate) async fn open_existing(&mut self, devserver: &WebServer) -> Result<()> { - let fullstack_address = devserver.proxied_server_address(); + for path in event.paths { + // Workaround for notify and vscode-like editor: + // - when edit & save a file in vscode, there will be two notifications, + // - the first one is a file with empty content. + // - filter the empty file notification to avoid false rebuild during hot-reload + if let Ok(metadata) = std::fs::metadata(&path) { + if metadata.len() == 0 { + continue; + } + } - if let Some(runner) = self.running.as_mut() { - runner.soft_kill().await; - runner - .open( - devserver.devserver_address(), - devserver.displayed_address(), - fullstack_address, - true, - ) - .await?; - } + files.push(path); + } + } - Ok(()) - } + ServeUpdate::FilesChanged { files } + } - /// Shutdown all the running processes - pub(crate) async fn cleanup(&mut self) { - if let Some(mut process) = self.running.take() { - process.cleanup().await; } } - pub(crate) async fn attempt_hot_reload( - &mut self, - modified_files: Vec, - ) -> Option { + /// Handle the list of changed files from the file watcher, attempting to aggressively prevent + /// full rebuilds by hot-reloading RSX and hot-patching Rust code. + /// + /// This will also handle any assets that are linked in the files, and copy them to the bundle + /// and send them to the client. + pub(crate) async fn handle_file_change(&mut self, files: &[PathBuf], server: &mut WebServer) { + // We can attempt to hotpatch if the build is in a bad state, since this patch might be a recovery. + if !matches!( + self.client.stage, + BuildStage::Failed | BuildStage::Aborted | BuildStage::Success + ) { + tracing::debug!( + "Ignoring file change: client is not ready to receive hotreloads. Files: {:#?}", + files + ); + return; + } + // If we have any changes to the rust files, we need to update the file map let mut templates = vec![]; // Prepare the hotreload message we need to send - let mut edited_rust_files = Vec::new(); let mut assets = Vec::new(); + let mut needs_full_rebuild = false; - for path in modified_files { + // We attempt to hotreload rsx blocks without a full rebuild + for path in files { // for various assets that might be linked in, we just try to hotreloading them forcefully // That is, unless they appear in an include! macro, in which case we need to a full rebuild.... let Some(ext) = path.extension().and_then(|v| v.to_str()) else { continue; }; + // If it's an asset, we want to hotreload it + // todo(jon): don't hardcode this here + if let Some(bundled_name) = self.client.hotreload_bundled_asset(path).await { + assets.push(PathBuf::from("/assets/").join(bundled_name)); + } + // If it's a rust file, we want to hotreload it using the filemap if ext == "rs" { - edited_rust_files.push(path); - continue; + // And grabout the contents + let Ok(new_contents) = std::fs::read_to_string(path) else { + tracing::debug!("Failed to read rust file while hotreloading: {:?}", path); + continue; + }; + + // Get the cached file if it exists - ignoring if it doesn't exist + let Some(cached_file) = self.file_map.get_mut(path) else { + tracing::debug!("No entry for file in filemap: {:?}", path); + tracing::debug!("Filemap: {:#?}", self.file_map.keys()); + continue; + }; + + let Ok(local_path) = path.strip_prefix(self.workspace.workspace_root()) else { + tracing::debug!("Skipping file outside workspace dir: {:?}", path); + continue; + }; + + // We assume we can parse the old file and the new file, ignoring untracked rust files + let old_syn = syn::parse_file(&cached_file.contents); + let new_syn = syn::parse_file(&new_contents); + let (Ok(old_file), Ok(new_file)) = (old_syn, new_syn) else { + tracing::debug!("Diff rsx returned not parseable"); + continue; + }; + + // This assumes the two files are structured similarly. If they're not, we can't diff them + let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else { + needs_full_rebuild = true; + break; + }; + + // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version + cached_file.most_recent = Some(new_contents); + + for ChangedRsx { old, new } in changed_rsx { + let old_start = old.span().start(); + + let old_parsed = syn::parse2::(old.tokens); + let new_parsed = syn::parse2::(new.tokens); + let (Ok(old_call_body), Ok(new_call_body)) = (old_parsed, new_parsed) else { + continue; + }; + + // Format the template location, normalizing the path + let file_name: String = local_path + .components() + .map(|c| c.as_os_str().to_string_lossy()) + .collect::>() + .join("/"); + + // Returns a list of templates that are hotreloadable + let results = HotReloadResult::new::( + &old_call_body.body, + &new_call_body.body, + file_name.clone(), + ); + + // If no result is returned, we can't hotreload this file and need to keep the old file + let Some(results) = results else { + needs_full_rebuild = true; + break; + }; + + // Only send down templates that have roots, and ideally ones that have changed + // todo(jon): maybe cache these and don't send them down if they're the same + for (index, template) in results.templates { + if template.roots.is_empty() { + continue; + } + + // Create the key we're going to use to identify this template + let key = TemplateGlobalKey { + file: file_name.clone(), + line: old_start.line, + column: old_start.column + 1, + index, + }; + + // if the template is the same, don't send its + if cached_file.templates.get(&key) == Some(&template) { + continue; + }; + + cached_file.templates.insert(key.clone(), template.clone()); + templates.push(HotReloadTemplateWithLocation { template, key }); + } + } + } + } + + // We decided to make the hotpatch engine drive *all* hotreloads, even if they are just RSX + // hot-reloads. This is a temporary measure to thoroughly test the hotpatch engine until + // we're comfortable with both co-existing. Keeping both would lead to two potential sources + // of errors, and we want to avoid that for now. + if needs_full_rebuild || self.use_hotpatch_engine { + if self.use_hotpatch_engine { + self.client.patch_rebuild(files.to_vec()); + if let Some(server) = self.server.as_mut() { + server.patch_rebuild(files.to_vec()); + } + self.clear_hot_reload_changes(); + self.clear_cached_rsx(); + server.send_patch_start().await; + } else { + self.client.start_rebuild(BuildMode::Base); + if let Some(server) = self.server.as_mut() { + server.start_rebuild(BuildMode::Base); + } + self.clear_hot_reload_changes(); + self.clear_cached_rsx(); + server.send_reload_start().await; } + } else { + let msg = HotReloadMsg { + templates, + assets, + ms_elapsed: 0, + jump_table: Default::default(), + for_build_id: None, + }; + + self.add_hot_reload_message(&msg); + + let file = files[0].display().to_string(); + let file = + file.trim_start_matches(&self.client.build.crate_dir().display().to_string()); - // Special-case the Cargo.toml file - we want updates here to cause a full rebuild - if path.file_name().and_then(|v| v.to_str()) == Some("Cargo.toml") { - return None; + // Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild + // + // todo: move the android file uploading out of hotreload_bundled_asset and + // + // Also make sure the builder isn't busy since that might cause issues with hotreloads + // https://github.com/DioxusLabs/dioxus/issues/3361 + if !msg.is_empty() && self.client.can_receive_hotreloads() { + tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloading: {}", file); + server.send_hotreload(msg).await; + } else { + tracing::debug!(dx_src = ?TraceSrc::Dev, "Ignoring file change: {}", file); } + } + } - // Otherwise, it might be an asset and we should look for it in all the running apps - if let Some(runner) = self.running.as_mut() { - if let Some(bundled_name) = runner.hotreload_bundled_asset(&path).await { - // todo(jon): don't hardcode this here - let asset_relative = PathBuf::from("/assets/").join(bundled_name); - assets.push(asset_relative); + /// Finally "bundle" this app and return a handle to it + pub(crate) async fn open( + &mut self, + artifacts: BuildArtifacts, + devserver: &mut WebServer, + ) -> Result<()> { + // Make sure to save artifacts regardless of if we're opening the app or not + match artifacts.platform { + Platform::Server => { + if let Some(server) = self.server.as_mut() { + server.artifacts = Some(artifacts.clone()); } } + _ => self.client.artifacts = Some(artifacts.clone()), } - // Multiple runners might have queued the same asset, so dedup them - assets.dedup(); + let should_open = self.client.stage == BuildStage::Success + && (self.server.as_ref().map(|s| s.stage == BuildStage::Success)).unwrap_or(true); - // Process the rust files - for rust_file in edited_rust_files { - // Strip the prefix before sending it to the filemap - let Ok(path) = rust_file.strip_prefix(self.krate.workspace_dir()) else { - tracing::error!( - "Hotreloading file outside of the crate directory: {:?}", - rust_file - ); - continue; - }; + if should_open { + let time_taken = artifacts + .time_end + .duration_since(artifacts.time_start) + .unwrap(); - // And grabout the contents - let Ok(contents) = std::fs::read_to_string(&rust_file) else { - tracing::debug!( - "Failed to read rust file while hotreloading: {:?}", - rust_file + if self.client.builds_opened == 0 { + tracing::info!( + "Build completed successfully in {:?}ms, launching app! 💫", + time_taken.as_millis() ); - continue; - }; + } else { + tracing::info!("Build completed in {:?}ms", time_taken.as_millis()); + } - match self.file_map.update_rsx::(path, contents) { - HotreloadResult::Rsx(new) => templates.extend(new), + let open_browser = self.client.builds_opened == 0 && self.open_browser; + self.open_all(devserver, open_browser).await?; - // The rust file may have failed to parse, but that is most likely - // because the user is in the middle of adding new code - // We just ignore the error and let Rust analyzer warn about the problem - HotreloadResult::Notreloadable => return None, - HotreloadResult::NotParseable => { - tracing::debug!(dx_src = ?TraceSrc::Dev, "Error hotreloading file - not parseable {rust_file:?}") - } + // Give a second for the server to boot + tokio::time::sleep(Duration::from_millis(300)).await; + + // Update the screen + devserver with the new handle info + devserver.send_reload_command().await + } + + Ok(()) + } + + /// Open an existing app bundle, if it exists + /// + /// Will attempt to open the server and client together, in a coordinated way such that the server + /// opens first, initializes, and then the client opens. + /// + /// There's a number of issues we need to be careful to work around: + /// - The server failing to boot or crashing on startup (and entering a boot loop) + /// - + pub(crate) async fn open_all( + &mut self, + devserver: &WebServer, + open_browser: bool, + ) -> Result<()> { + let devserver_ip = devserver.devserver_address(); + let fullstack_address = devserver.proxied_server_address(); + let displayed_address = devserver.displayed_address(); + + // Always open the server first after the client has been built + if let Some(server) = self.server.as_mut() { + tracing::debug!("Opening server build"); + server.soft_kill().await; + server + .open( + devserver_ip, + displayed_address, + fullstack_address, + false, + false, + BuildId::SERVER, + ) + .await?; + } + + // Start the new app before we kill the old one to give it a little bit of time + self.client.soft_kill().await; + self.client + .open( + devserver_ip, + displayed_address, + fullstack_address, + open_browser, + self.always_on_top, + BuildId::CLIENT, + ) + .await?; + + Ok(()) + } + + /// Shutdown all the running processes + pub(crate) async fn cleanup_all(&mut self) -> Result<()> { + self.client.soft_kill().await; + + if let Some(server) = self.server.as_mut() { + server.soft_kill().await; + } + + // If the client is running on Android, we need to remove the port forwarding + // todo: use the android tools "adb" + if matches!(self.client.build.platform, Platform::Android) { + if let Err(err) = Command::new(&self.workspace.android_tools()?.adb) + .arg("reverse") + .arg("--remove") + .arg(format!("tcp:{}", self.devserver_port)) + .output() + .await + { + tracing::error!( + "failed to remove forwarded port {}: {err}", + self.devserver_port + ); } } - let msg = HotReloadMsg { - templates, - assets, - unknown_files: vec![], + Ok(()) + } + + /// Perform a full rebuild of the app, equivalent to `cargo rustc` from scratch with no incremental + /// hot-patch engine integration. + pub(crate) async fn full_rebuild(&mut self) { + let build_mode = match self.use_hotpatch_engine { + true => BuildMode::Fat, + false => BuildMode::Base, }; - self.add_hot_reload_message(&msg); + self.client.start_rebuild(build_mode.clone()); + if let Some(s) = self.server.as_mut() { + s.start_rebuild(build_mode) + } + + self.clear_hot_reload_changes(); + self.clear_cached_rsx(); + self.clear_patches(); + } + + pub(crate) async fn hotpatch( + &mut self, + res: &BuildArtifacts, + id: BuildId, + cache: &HotpatchModuleCache, + ) -> Result { + let jump_table = match id { + BuildId::CLIENT => self.client.hotpatch(res, cache).await, + BuildId::SERVER => { + self.server + .as_mut() + .context("Server not found")? + .hotpatch(res, cache) + .await + } + _ => return Err(Error::Runtime("Invalid build id".into())), + }?; + + if id == BuildId::CLIENT { + self.applied_hot_reload_message.jump_table = self.client.patches.last().cloned(); + } + + Ok(jump_table) + } + + pub(crate) fn get_build(&self, id: BuildId) -> Option<&AppBuilder> { + match id { + BuildId::CLIENT => Some(&self.client), + BuildId::SERVER => self.server.as_ref(), + _ => None, + } + } - Some(msg) + pub(crate) fn client(&self) -> &AppBuilder { + &self.client + } + + /// The name of the app being served, to display + pub(crate) fn app_name(&self) -> &str { + self.client.build.executable_name() } /// Get any hot reload changes that have been applied since the last full rebuild - pub(crate) fn applied_hot_reload_changes(&mut self) -> HotReloadMsg { - self.applied_hot_reload_message.clone() + pub(crate) fn applied_hot_reload_changes(&mut self, build: BuildId) -> HotReloadMsg { + let mut msg = self.applied_hot_reload_message.clone(); + + if build == BuildId::CLIENT { + msg.jump_table = self.client.patches.last().cloned(); + msg.for_build_id = Some(BuildId::CLIENT.0 as _); + if let Some(lib) = msg.jump_table.as_mut() { + lib.lib = PathBuf::from("/").join(lib.lib.clone()); + } + } + + if build == BuildId::SERVER { + if let Some(server) = self.server.as_mut() { + msg.jump_table = server.patches.last().cloned(); + msg.for_build_id = Some(BuildId::SERVER.0 as _); + } + } + + msg } /// Clear the hot reload changes. This should be called any time a new build is starting @@ -254,44 +656,44 @@ impl AppRunner { self.applied_hot_reload_message = Default::default(); } - /// Store the hot reload changes for any future clients that connect - fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) { - let applied = &mut self.applied_hot_reload_message; - - // Merge the assets, unknown files, and templates - // We keep the newer change if there is both a old and new change - let mut templates: HashMap = std::mem::take(&mut applied.templates) - .into_iter() - .map(|template| (template.key.clone(), template)) - .collect(); - let mut assets: HashSet = - std::mem::take(&mut applied.assets).into_iter().collect(); - let mut unknown_files: HashSet = std::mem::take(&mut applied.unknown_files) - .into_iter() - .collect(); - for template in &msg.templates { - templates.insert(template.key.clone(), template.clone()); + pub(crate) fn clear_patches(&mut self) { + self.client.patches.clear(); + if let Some(server) = self.server.as_mut() { + server.patches.clear(); } - assets.extend(msg.assets.iter().cloned()); - unknown_files.extend(msg.unknown_files.iter().cloned()); - applied.templates = templates.into_values().collect(); - applied.assets = assets.into_iter().collect(); - applied.unknown_files = unknown_files.into_iter().collect(); } - pub(crate) async fn client_connected(&mut self) { - let Some(handle) = self.running.as_mut() else { - return; - }; + pub(crate) async fn client_connected( + &mut self, + build_id: BuildId, + aslr_reference: Option, + ) { + match build_id { + BuildId::CLIENT => { + // multiple tabs on web can cause this to be called incorrectly, and it doesn't + // make any sense anyways + if self.client.build.platform != Platform::Web { + if let Some(aslr_reference) = aslr_reference { + self.client.aslr_reference = Some(aslr_reference); + } + } + } + BuildId::SERVER => { + if let Some(server) = self.server.as_mut() { + server.aslr_reference = aslr_reference; + } + } + _ => {} + } // Assign the runtime asset dir to the runner - if handle.app.build.build.platform() == Platform::Ios { + if self.client.build.platform == Platform::Ios { // xcrun simctl get_app_container booted com.dioxuslabs - let res = tokio::process::Command::new("xcrun") + let res = Command::new("xcrun") .arg("simctl") .arg("get_app_container") .arg("booted") - .arg(handle.app.build.krate.bundle_identifier()) + .arg(self.client.build.bundle_identifier()) .output() .await; @@ -302,12 +704,54 @@ impl AppRunner { let out = out.trim(); tracing::trace!("Setting Runtime asset dir: {out:?}"); - handle.runtime_asst_dir = Some(PathBuf::from(out)); + self.client.runtime_asset_dir = Some(PathBuf::from(out)); } } } } + /// Store the hot reload changes for any future clients that connect + fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) { + let applied = &mut self.applied_hot_reload_message; + + // Merge the assets, unknown files, and templates + // We keep the newer change if there is both a old and new change + let mut templates: HashMap = std::mem::take(&mut applied.templates) + .into_iter() + .map(|template| (template.key.clone(), template)) + .collect(); + let mut assets: HashSet = + std::mem::take(&mut applied.assets).into_iter().collect(); + for template in &msg.templates { + templates.insert(template.key.clone(), template.clone()); + } + assets.extend(msg.assets.iter().cloned()); + applied.templates = templates.into_values().collect(); + applied.assets = assets.into_iter().collect(); + applied.jump_table = self.client.patches.last().cloned(); + } + + /// Register the files from the workspace into our file watcher. + /// + /// This very simply looks for all Rust files in the workspace and adds them to the filemap. + /// + /// Once the builds complete we'll use the depinfo files to get the actual files that are used, + /// making our watcher more accurate. Filling the filemap here is intended to catch any file changes + /// in between the first build and the depinfo file being generated. + /// + /// We don't want watch any registry files since that generally causes a huge performance hit - + /// we mostly just care about workspace files and local dependencies. + /// + /// Dep-info file background: + /// https://doc.rust-lang.org/stable/nightly-rustc/cargo/core/compiler/fingerprint/index.html#dep-info-files + fn load_rsx_filemap(&mut self) { + self.fill_filemap_from_krate(self.client.build.crate_dir()); + + for krate in self.all_watched_crates() { + self.fill_filemap_from_krate(krate); + } + } + /// Fill the filemap with files from the filesystem, using the given filter to determine which files to include. /// /// You can use the filter with something like a gitignore to only include files that are relevant to your project. @@ -317,34 +761,301 @@ impl AppRunner { /// Generally this will only be .rs files /// /// If a file couldn't be parsed, we don't fail. Instead, we save the error. - pub fn fill_filemap(&mut self, path: PathBuf) { - if self.ignore.matched(&path, path.is_dir()).is_ignore() { - return; + /// + /// todo: There are known bugs here when handling gitignores. + fn fill_filemap_from_krate(&mut self, crate_dir: PathBuf) { + for entry in walkdir::WalkDir::new(crate_dir).into_iter().flatten() { + if self + .workspace + .ignore + .matched(entry.path(), entry.file_type().is_dir()) + .is_ignore() + { + continue; + } + + let path = entry.path(); + if path.extension().and_then(|s| s.to_str()) == Some("rs") { + if let Ok(contents) = std::fs::read_to_string(path) { + self.file_map.insert( + path.to_path_buf(), + CachedFile { + contents, + most_recent: None, + templates: Default::default(), + }, + ); + } + } + } + } + + /// Commit the changes to the filemap, overwriting the contents of the files + /// + /// Removes any cached templates and replaces the contents of the files with the most recent + /// + /// todo: we should-reparse the contents so we never send a new version, ever + fn clear_cached_rsx(&mut self) { + for cached_file in self.file_map.values_mut() { + if let Some(most_recent) = cached_file.most_recent.take() { + cached_file.contents = most_recent; + } + cached_file.templates.clear(); + } + } + + fn watch_filesystem(&mut self) { + // Watch the folders of the crates that we're interested in + for path in self.watch_paths( + self.client.build.crate_dir(), + self.client.build.crate_package, + ) { + tracing::trace!("Watching path {path:?}"); + + if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) { + handle_notify_error(err); + } + } + + // Also watch the crates themselves, but not recursively, such that we can pick up new folders + for krate in self.all_watched_crates() { + tracing::trace!("Watching path {krate:?}"); + if let Err(err) = self.watcher.watch(&krate, RecursiveMode::NonRecursive) { + handle_notify_error(err); + } } - // If the file is a .rs file, add it to the filemap - if path.extension().and_then(|s| s.to_str()) == Some("rs") { - if let Ok(contents) = std::fs::read_to_string(&path) { - if let Ok(path) = path.strip_prefix(self.krate.workspace_dir()) { - self.file_map.add_file(path.to_path_buf(), contents); + // Also watch the workspace dir, non recursively, such that we can pick up new folders there too + if let Err(err) = self.watcher.watch( + self.workspace.krates.workspace_root().as_std_path(), + RecursiveMode::NonRecursive, + ) { + handle_notify_error(err); + } + } + + /// Return the list of paths that we should watch for changes. + fn watch_paths(&self, crate_dir: PathBuf, crate_package: NodeId) -> Vec { + let mut watched_paths = vec![]; + + // Get a list of *all* the crates with Rust code that we need to watch. + // This will end up being dependencies in the workspace and non-workspace dependencies on the user's computer. + let mut watched_crates = self.local_dependencies(crate_package); + watched_crates.push(crate_dir); + + // Now, watch all the folders in the crates, but respecting their respective ignore files + for krate_root in watched_crates { + // Build the ignore builder for this crate, but with our default ignore list as well + let ignore = self.workspace.ignore_for_krate(&krate_root); + + for entry in krate_root.read_dir().into_iter().flatten() { + let Ok(entry) = entry else { + continue; + }; + + if ignore + .matched(entry.path(), entry.path().is_dir()) + .is_ignore() + { + continue; } + + watched_paths.push(entry.path().to_path_buf()); } - return; } - // If it's not, we'll try to read the directory - if path.is_dir() { - if let Ok(read_dir) = std::fs::read_dir(&path) { - for entry in read_dir.flatten() { - self.fill_filemap(entry.path()); + watched_paths.dedup(); + + watched_paths + } + + /// Get all the Manifest paths for dependencies that we should watch. Will not return anything + /// in the `.cargo` folder - only local dependencies will be watched. + /// + /// This returns a list of manifest paths + /// + /// Extend the watch path to include: + /// + /// - the assets directory - this is so we can hotreload CSS and other assets by default + /// - the Cargo.toml file - this is so we can hotreload the project if the user changes dependencies + /// - the Dioxus.toml file - this is so we can hotreload the project if the user changes the Dioxus config + fn local_dependencies(&self, crate_package: NodeId) -> Vec { + let mut paths = vec![]; + + for (dependency, _edge) in self.workspace.krates.get_deps(crate_package) { + let krate = match dependency { + krates::Node::Krate { krate, .. } => krate, + krates::Node::Feature { krate_index, .. } => { + &self.workspace.krates[krate_index.index()] } + }; + + if krate + .manifest_path + .components() + .any(|c| c.as_str() == ".cargo") + { + continue; } + + paths.push( + krate + .manifest_path + .parent() + .unwrap() + .to_path_buf() + .into_std_path_buf(), + ); } + + paths + } + + // todo: we need to make sure we merge this for all the running packages + fn all_watched_crates(&self) -> Vec { + let crate_package = self.client().build.crate_package; + let crate_dir = self.client().build.crate_dir(); + + let mut krates: Vec = self + .local_dependencies(crate_package) + .into_iter() + .map(|p| { + p.parent() + .expect("Local manifest to exist and have a parent") + .to_path_buf() + }) + .chain(Some(crate_dir)) + .collect(); + + krates.dedup(); + + krates } - fn flush_session_cache(&self) { - let cache_dir = self.krate.session_cache_dir(); - _ = std::fs::remove_dir_all(&cache_dir); - _ = std::fs::create_dir_all(&cache_dir); + /// Check if this is a fullstack build. This means that there is an additional build with the `server` platform. + pub(crate) fn is_fullstack(&self) -> bool { + self.fullstack } + + /// Return a number between 0 and 1 representing the progress of the server build + pub(crate) fn server_compile_progress(&self) -> f64 { + let Some(server) = self.server.as_ref() else { + return 0.0; + }; + + server.compiled_crates as f64 / server.expected_crates as f64 + } +} + +/// Bind a listener to any point and return it +/// When the listener is dropped, the socket will be closed, but we'll still have a port that we +/// can bind our proxy to. +/// +/// Todo: we might want to do this on every new build in case the OS tries to bind things to this port +/// and we don't already have something bound to it. There's no great way of "reserving" a port. +fn get_available_port(address: IpAddr, prefer: Option) -> Option { + // First, try to bind to the preferred port + if let Some(port) = prefer { + if let Ok(_listener) = TcpListener::bind((address, port)) { + return Some(port); + } + } + + // Otherwise, try to bind to any port and return the first one we can + TcpListener::bind((address, 0)) + .and_then(|listener| listener.local_addr().map(|f| f.port())) + .ok() +} + +fn create_notify_watcher( + tx: UnboundedSender, + wsl_poll_interval: u64, +) -> Box { + // Build the event handler for notify. + // This has been known to be a source of many problems, unfortunately, since notify handling seems to be flakey across platforms + let handler = move |info: notify::Result| { + let Ok(event) = info else { + return; + }; + + let is_allowed_notify_event = match event.kind { + EventKind::Modify(ModifyKind::Data(_)) => true, + EventKind::Modify(ModifyKind::Name(_)) => true, + // The primary modification event on WSL's poll watcher. + EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true, + // Catch-all for unknown event types (windows) + EventKind::Modify(ModifyKind::Any) => true, + EventKind::Modify(ModifyKind::Metadata(_)) => false, + // Don't care about anything else. + EventKind::Create(_) => true, + EventKind::Remove(_) => true, + _ => false, + }; + + if is_allowed_notify_event { + _ = tx.unbounded_send(event); + } + }; + + const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories."; + + // On wsl, we need to poll the filesystem for changes + if is_wsl() { + return Box::new( + notify::PollWatcher::new( + handler, + Config::default().with_poll_interval(Duration::from_secs(wsl_poll_interval)), + ) + .expect(NOTIFY_ERROR_MSG), + ); + } + + // Otherwise we can use the recommended watcher + Box::new(notify::recommended_watcher(handler).expect(NOTIFY_ERROR_MSG)) +} + +fn handle_notify_error(err: notify::Error) { + tracing::debug!("Failed to watch path: {}", err); + match err.kind { + notify::ErrorKind::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { + tracing::error!("Failed to watch path: permission denied. {:?}", err.paths) + } + notify::ErrorKind::MaxFilesWatch => { + tracing::error!("Failed to set up file watcher: too many files to watch") + } + _ => {} + } +} + +/// Detects if `dx` is being ran in a WSL environment. +/// +/// We determine this based on whether the keyword `microsoft` or `wsl` is contained within the [`WSL_1`] or [`WSL_2`] files. +/// This may fail in the future as it isn't guaranteed by Microsoft. +/// See https://github.com/microsoft/WSL/issues/423#issuecomment-221627364 +fn is_wsl() -> bool { + const WSL_1: &str = "/proc/sys/kernel/osrelease"; + const WSL_2: &str = "/proc/version"; + const WSL_KEYWORDS: [&str; 2] = ["microsoft", "wsl"]; + + // Test 1st File + if let Ok(content) = std::fs::read_to_string(WSL_1) { + let lowercase = content.to_lowercase(); + for keyword in WSL_KEYWORDS { + if lowercase.contains(keyword) { + return true; + } + } + } + + // Test 2nd File + if let Ok(content) = std::fs::read_to_string(WSL_2) { + let lowercase = content.to_lowercase(); + for keyword in WSL_KEYWORDS { + if lowercase.contains(keyword) { + return true; + } + } + } + + false } diff --git a/packages/cli/src/serve/server.rs b/packages/cli/src/serve/server.rs index df9ec84892..b173727d56 100644 --- a/packages/cli/src/serve/server.rs +++ b/packages/cli/src/serve/server.rs @@ -1,14 +1,13 @@ use crate::{ - config::WebHttpsConfig, - serve::{ServeArgs, ServeUpdate}, - BuildStage, BuildUpdate, DioxusCrate, Platform, Result, TraceSrc, + config::WebHttpsConfig, serve::ServeUpdate, BuildId, BuildStage, BuilderUpdate, Platform, + Result, TraceSrc, }; use anyhow::Context; use axum::{ body::Body, extract::{ ws::{Message, WebSocket}, - Request, State, WebSocketUpgrade, + Query, Request, State, WebSocketUpgrade, }, http::{ header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA}, @@ -34,13 +33,18 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, path::Path, sync::{Arc, RwLock}, + time::Duration, }; +use subsecond_types::JumpTable; +use tokio::process::Command; use tower_http::{ cors::Any, services::fs::{ServeDir, ServeFileSystemResponseBody}, ServiceBuilderExt, }; +use super::AppServer; + /// The webserver that serves statics assets (if fullstack isn't already doing that) and the websocket /// communication layer that we use to send status updates and hotreloads to the client. /// @@ -52,46 +56,40 @@ pub(crate) struct WebServer { devserver_bind_ip: IpAddr, devserver_port: u16, proxied_port: Option, - hot_reload_sockets: Vec, - build_status_sockets: Vec, - new_hot_reload_sockets: UnboundedReceiver, - new_build_status_sockets: UnboundedReceiver, + hot_reload_sockets: Vec, + build_status_sockets: Vec, + new_hot_reload_sockets: UnboundedReceiver, + new_build_status_sockets: UnboundedReceiver, build_status: SharedStatus, application_name: String, platform: Platform, } +pub(crate) struct ConnectedWsClient { + socket: WebSocket, + build_id: Option, + aslr_reference: Option, +} + impl WebServer { + pub const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + /// Start the development server. /// This will set up the default http server if there's no server specified (usually via fullstack). /// /// This will also start the websocket server that powers the devtools. If you want to communicate /// with connected devtools clients, this is the place to do it. - pub(crate) fn start(krate: &DioxusCrate, args: &ServeArgs) -> Result { + pub(crate) fn start(runner: &AppServer) -> Result { let (hot_reload_sockets_tx, hot_reload_sockets_rx) = futures_channel::mpsc::unbounded(); let (build_status_sockets_tx, build_status_sockets_rx) = futures_channel::mpsc::unbounded(); - const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); - - // Use 0.0.0.0 as the default address if none is specified - this will let us expose the - // devserver to the network (for other devices like phones/embedded) - let devserver_bind_ip = args.address.addr.unwrap_or(SELF_IP); - - // If the user specified a port, use that, otherwise use any available port, preferring 8080 - let devserver_port = args - .address - .port - .unwrap_or_else(|| get_available_port(devserver_bind_ip, Some(8080)).unwrap_or(8080)); - - // All servers will end up behind us (the devserver) but on a different port - // This is so we can serve a loading screen as well as devtools without anything particularly fancy - let proxied_port = args - .should_proxy_build() - .then(|| get_available_port(devserver_bind_ip, None)) - .flatten(); - // Create the listener that we'll pass into the devserver, but save its IP here so // we can display it to the user in the tui + let devserver_bind_ip = runner.devserver_bind_ip; + let devserver_port = runner.devserver_port; + let proxied_port = runner.proxied_port; + let devserver_exposed_ip = devserver_bind_ip; + let devserver_bind_address = SocketAddr::new(devserver_bind_ip, devserver_port); let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| { anyhow::anyhow!( @@ -99,21 +97,12 @@ impl WebServer { ) })?; - // If the IP is 0.0.0.0, we need to get the actual IP of the machine - // This will let ios/android/network clients connect to the devserver - let devserver_exposed_ip = if devserver_bind_ip == SELF_IP { - local_ip_address::local_ip().unwrap_or(devserver_bind_ip) - } else { - devserver_bind_ip - }; - let proxied_address = proxied_port.map(|port| SocketAddr::new(devserver_exposed_ip, port)); // Set up the router with some shared state that we'll update later to reflect the current state of the build let build_status = SharedStatus::new_with_starting_build(); let router = build_devserver_router( - args, - krate, + runner, hot_reload_sockets_tx, build_status_sockets_tx, proxied_address, @@ -122,7 +111,7 @@ impl WebServer { // And finally, start the server mainloop tokio::spawn(devserver_mainloop( - krate.config.web.https.clone(), + runner.client().build.config.web.https.clone(), listener, router, )); @@ -137,8 +126,8 @@ impl WebServer { build_status_sockets: Default::default(), new_hot_reload_sockets: hot_reload_sockets_rx, new_build_status_sockets: build_status_sockets_rx, - application_name: krate.executable_name().to_string(), - platform: args.build_arguments.platform(), + application_name: runner.app_name().to_string(), + platform: runner.client.build.platform, }) } @@ -150,15 +139,19 @@ impl WebServer { .hot_reload_sockets .iter_mut() .enumerate() - .map(|(idx, socket)| async move { (idx, socket.next().await) }) + .map(|(idx, socket)| async move { (idx, socket.socket.next().await) }) .collect::>(); tokio::select! { new_hot_reload_socket = &mut new_hot_reload_socket => { if let Some(new_socket) = new_hot_reload_socket { + let aslr_reference = new_socket.aslr_reference; + let id = new_socket.build_id.unwrap_or(BuildId::CLIENT); + drop(new_message); self.hot_reload_sockets.push(new_socket); - return ServeUpdate::NewConnection; + + return ServeUpdate::NewConnection { aslr_reference, id }; } else { panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); } @@ -169,8 +162,8 @@ impl WebServer { // Update the socket with project info and current build status let project_info = SharedStatus::new(Status::ClientInit { application_name: self.application_name.clone(), platform: self.platform }); - if project_info.send_to(&mut new_socket).await.is_ok() { - _ = self.build_status.send_to(&mut new_socket).await; + if project_info.send_to(&mut new_socket.socket).await.is_ok() { + _ = self.build_status.send_to(&mut new_socket.socket).await; self.build_status_sockets.push(new_socket); } return future::pending::().await; @@ -180,7 +173,7 @@ impl WebServer { } Some((idx, message)) = new_message.next() => { match message { - Some(Ok(message)) => return ServeUpdate::WsMessage(message), + Some(Ok(msg)) => return ServeUpdate::WsMessage { msg, platform: Platform::Web }, _ => { drop(new_message); _ = self.hot_reload_sockets.remove(idx); @@ -193,26 +186,9 @@ impl WebServer { } pub(crate) async fn shutdown(&mut self) { - if matches!(self.platform, Platform::Android) { - use std::process::{Command, Stdio}; - if let Err(err) = Command::new("adb") - .arg("reverse") - .arg("--remove") - .arg(format!("tcp:{}", self.devserver_port)) - .stderr(Stdio::piped()) - .stdout(Stdio::piped()) - .output() - { - tracing::error!( - "failed to remove forwarded port {}: {err}", - self.devserver_port - ); - } - } - self.send_shutdown().await; for mut socket in self.hot_reload_sockets.drain(..) { - _ = socket.send(Message::Close(None)).await; + _ = socket.socket.send(Message::Close(None)).await; } } @@ -221,7 +197,7 @@ impl WebServer { let mut i = 0; while i < self.build_status_sockets.len() { let socket = &mut self.build_status_sockets[i]; - if self.build_status.send_to(socket).await.is_err() { + if self.build_status.send_to(&mut socket.socket).await.is_err() { self.build_status_sockets.remove(i); } else { i += 1; @@ -239,13 +215,9 @@ impl WebServer { } /// Sends an updated build status to all clients. - pub(crate) async fn new_build_update( - &mut self, - update: &BuildUpdate, - builder: &super::Builder, - ) { + pub(crate) async fn new_build_update(&mut self, update: &BuilderUpdate) { match update { - BuildUpdate::Progress { stage } => { + BuilderUpdate::Progress { stage } => { // Todo(miles): wire up more messages into the splash screen UI match stage { BuildStage::Success => {} @@ -259,7 +231,10 @@ impl WebServer { krate, .. } => { - if !builder.is_finished() { + if !matches!( + self.build_status.get(), + Status::Ready | Status::BuildError { .. } + ) { self.build_status.set(Status::Building { progress: (*current as f64 / *total as f64).clamp(0.0, 1.0), build_message: format!("{krate} compiling"), @@ -273,9 +248,9 @@ impl WebServer { _ => {} } } - BuildUpdate::CompilerMessage { .. } => {} - BuildUpdate::BuildReady { .. } => {} - BuildUpdate::BuildFailed { err } => { + BuilderUpdate::CompilerMessage { .. } => {} + BuilderUpdate::BuildReady { .. } => {} + BuilderUpdate::BuildFailed { err } => { let error = err.to_string(); self.build_status.set(Status::BuildError { error: ansi_to_html::convert(&error).unwrap_or(error), @@ -283,6 +258,9 @@ impl WebServer { self.send_reload_failed().await; self.send_build_status().await; } + BuilderUpdate::StdoutReceived { .. } => {} + BuilderUpdate::StderrReceived { .. } => {} + BuilderUpdate::ProcessExited { .. } => {} } } @@ -292,7 +270,6 @@ impl WebServer { return; } - #[cfg(debug_assertions)] tracing::trace!("Sending hotreload to clients {:?}", reload); let msg = DevserverMsg::HotReload(reload); @@ -303,6 +280,7 @@ impl WebServer { while i < self.hot_reload_sockets.len() { let socket = &mut self.hot_reload_sockets[i]; if socket + .socket .send(Message::Text(msg.clone().into())) .await .is_err() @@ -314,15 +292,37 @@ impl WebServer { } } + pub(crate) async fn send_patch( + &mut self, + jump_table: JumpTable, + time_taken: Duration, + build: BuildId, + ) { + let msg = DevserverMsg::HotReload(HotReloadMsg { + jump_table: Some(jump_table), + ms_elapsed: time_taken.as_millis() as u64, + templates: vec![], + assets: vec![], + for_build_id: Some(build.0 as _), + }); + self.send_devserver_message_to_all(msg).await; + } + + /// Tells all clients that a hot patch has started. + pub(crate) async fn send_patch_start(&mut self) { + self.send_devserver_message_to_all(DevserverMsg::HotPatchStart) + .await; + } + /// Tells all clients that a full rebuild has started. pub(crate) async fn send_reload_start(&mut self) { - self.send_devserver_message(DevserverMsg::FullReloadStart) + self.send_devserver_message_to_all(DevserverMsg::FullReloadStart) .await; } /// Tells all clients that a full rebuild has failed. pub(crate) async fn send_reload_failed(&mut self) { - self.send_devserver_message(DevserverMsg::FullReloadFailed) + self.send_devserver_message_to_all(DevserverMsg::FullReloadFailed) .await; } @@ -334,19 +334,21 @@ impl WebServer { self.build_status.set(Status::Ready); self.send_build_status().await; - self.send_devserver_message(DevserverMsg::FullReloadCommand) + self.send_devserver_message_to_all(DevserverMsg::FullReloadCommand) .await; } /// Send a shutdown message to all connected clients. pub(crate) async fn send_shutdown(&mut self) { - self.send_devserver_message(DevserverMsg::Shutdown).await; + self.send_devserver_message_to_all(DevserverMsg::Shutdown) + .await; } /// Sends a devserver message to all connected clients. - async fn send_devserver_message(&mut self, msg: DevserverMsg) { + async fn send_devserver_message_to_all(&mut self, msg: DevserverMsg) { for socket in self.hot_reload_sockets.iter_mut() { _ = socket + .socket .send(Message::Text(serde_json::to_string(&msg).unwrap().into())) .await; } @@ -424,23 +426,24 @@ async fn devserver_mainloop( /// - Setting up the file serve service /// - Setting up the websocket endpoint for devtools fn build_devserver_router( - args: &ServeArgs, - krate: &DioxusCrate, - hot_reload_sockets: UnboundedSender, - build_status_sockets: UnboundedSender, + runner: &AppServer, + hot_reload_sockets: UnboundedSender, + build_status_sockets: UnboundedSender, fullstack_address: Option, build_status: SharedStatus, ) -> Result { let mut router = Router::new(); + let build = runner.client(); // Setup proxy for the endpoint specified in the config - for proxy_config in krate.config.web.proxy.iter() { + for proxy_config in build.build.config.web.proxy.iter() { router = super::proxy::add_proxy(router, proxy_config)?; } - if args.should_proxy_build() { - // For fullstack, liveview, and server, forward all requests to the inner server - let address = fullstack_address.unwrap(); + // For fullstack, liveview, and server, forward all requests to the inner server + if runner.proxied_port.is_some() { + tracing::debug!("Proxying requests to fullstack server at {fullstack_address:?}"); + let address = fullstack_address.context("No fullstack address specified")?; tracing::debug!("Proxying requests to fullstack server at {address}"); router = router.fallback_service(super::proxy::proxy_to( format!("http://{address}").parse().unwrap(), @@ -460,7 +463,9 @@ fn build_devserver_router( // Route file service to output the .wasm and assets if this is a web build let base_path = format!( "/{}", - krate + runner + .client() + .build .config .web .app @@ -470,9 +475,9 @@ fn build_devserver_router( .trim_matches('/') ); if base_path == "/" { - router = router.fallback_service(build_serve_dir(args, krate)); + router = router.fallback_service(build_serve_dir(runner)); } else { - router = router.nest_service(&base_path, build_serve_dir(args, krate)); + router = router.nest_service(&base_path, build_serve_dir(runner)); } } @@ -482,6 +487,12 @@ fn build_devserver_router( build_status_middleware, )); + #[derive(Deserialize, Debug)] + struct ConnectionQuery { + aslr_reference: Option, + build_id: Option, + } + // Setup websocket endpoint - and pass in the extension layer immediately after router = router.nest( "/_dioxus", @@ -489,9 +500,9 @@ fn build_devserver_router( .route( "/", get( - |ws: WebSocketUpgrade, ext: Extension>| async move { - tracing::debug!("New devtool websocket connection"); - ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) }) + |ws: WebSocketUpgrade, ext: Extension>, query: Query| async move { + tracing::debug!("New devtool websocket connection: {:?}", query); + ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(ConnectedWsClient { socket, aslr_reference: query.aslr_reference, build_id: query.build_id }) }) }, ), ) @@ -499,8 +510,8 @@ fn build_devserver_router( .route( "/build_status", get( - |ws: WebSocketUpgrade, ext: Extension>| async move { - ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) }) + |ws: WebSocketUpgrade, ext: Extension>| async move { + ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(ConnectedWsClient { socket, aslr_reference: None, build_id: None }) }) }, ), ) @@ -520,7 +531,7 @@ fn build_devserver_router( Ok(router) } -fn build_serve_dir(args: &ServeArgs, cfg: &DioxusCrate) -> axum::routing::MethodRouter { +fn build_serve_dir(runner: &AppServer) -> axum::routing::MethodRouter { use tower::ServiceBuilder; static CORS_UNSAFE: (HeaderValue, HeaderValue) = ( @@ -533,15 +544,19 @@ fn build_serve_dir(args: &ServeArgs, cfg: &DioxusCrate) -> axum::routing::Method HeaderValue::from_static("same-origin"), ); - let (coep, coop) = match args.cross_origin_policy { + let (coep, coop) = match runner.cross_origin_policy { true => CORS_REQUIRE.clone(), false => CORS_UNSAFE.clone(), }; - let out_dir = cfg - .build_dir(Platform::Web, args.build_arguments.release) + let app = &runner.client; + let cfg = &runner.client.build.config; + + let out_dir = app + .build + .build_dir(Platform::Web, app.build.release) .join("public"); - let index_on_404 = cfg.config.web.watcher.index_on_404; + let index_on_404: bool = cfg.web.watcher.index_on_404; get_service( ServiceBuilder::new() @@ -641,7 +656,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> { _ = fs::create_dir("ssl"); } - let cmd = tokio::process::Command::new("mkcert") + let cmd = Command::new("mkcert") .args([ "-install", "-key-file", @@ -673,25 +688,6 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> { Ok((cert_path, key_path)) } -/// Bind a listener to any point and return it -/// When the listener is dropped, the socket will be closed, but we'll still have a port that we -/// can bind our proxy to. -/// -/// Todo: we might want to do this on every new build in case the OS tries to bind things to this port -/// and we don't already have something bound to it. There's no great way of "reserving" a port. -fn get_available_port(address: IpAddr, prefer: Option) -> Option { - // First, try to bind to the preferred port - if let Some(port) = prefer { - if let Ok(_listener) = TcpListener::bind((address, port)) { - return Some(port); - } - } - - // Otherwise, try to bind to any port and return the first one we can - TcpListener::bind((address, 0)) - .map(|listener| listener.local_addr().unwrap().port()) - .ok() -} /// Middleware that intercepts html requests if the status is "Building" and returns a loading page instead async fn build_status_middleware( @@ -708,7 +704,7 @@ async fn build_status_middleware( if let Some(true) = accepts_html { let status = state.get(); if status != Status::Ready { - let html = include_str!("../../assets/web/loading.html"); + let html = include_str!("../../assets/web/dev.loading.html"); return axum::response::Response::builder() .status(StatusCode::OK) // Load the html loader then keep loading forever diff --git a/packages/cli/src/serve/update.rs b/packages/cli/src/serve/update.rs index 49e094adc7..4193c40cb8 100644 --- a/packages/cli/src/serve/update.rs +++ b/packages/cli/src/serve/update.rs @@ -1,46 +1,31 @@ -use crate::{BuildUpdate, Platform, TraceMsg}; +use crate::{BuildId, BuilderUpdate, Platform, TraceMsg}; use axum::extract::ws::Message as WsMessage; -use std::{path::PathBuf, process::ExitStatus}; +use std::path::PathBuf; /// One fat enum to rule them all.... /// /// Thanks to libraries like winit for the inspiration #[allow(clippy::large_enum_variant)] pub(crate) enum ServeUpdate { - NewConnection, - WsMessage(WsMessage), - - /// A build update from the build engine - BuildUpdate(BuildUpdate), - - /// A running process has received a stdout. - /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. - /// - /// We will poll lines and any content in a 50ms interval - StdoutReceived { - platform: Platform, - msg: String, + NewConnection { + id: BuildId, + aslr_reference: Option, }, - - /// A running process has received a stderr. - /// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line. - /// - /// We will poll lines and any content in a 50ms interval - StderrReceived { + WsMessage { platform: Platform, - msg: String, + msg: WsMessage, }, - ProcessExited { - platform: Platform, - status: ExitStatus, + /// An update regarding the state of the build and running app from an AppBuilder + BuilderUpdate { + id: BuildId, + update: BuilderUpdate, }, FilesChanged { files: Vec, }, - /// Open an existing app bundle, if it exists OpenApp, RequestRebuild, diff --git a/packages/cli/src/serve/watcher.rs b/packages/cli/src/serve/watcher.rs deleted file mode 100644 index 555f9c2291..0000000000 --- a/packages/cli/src/serve/watcher.rs +++ /dev/null @@ -1,165 +0,0 @@ -use super::detect::is_wsl; -use super::update::ServeUpdate; -use crate::{cli::serve::ServeArgs, dioxus_crate::DioxusCrate}; -use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures_util::StreamExt; -use notify::{ - event::{MetadataKind, ModifyKind}, - Config, EventKind, RecursiveMode, Watcher as NotifyWatcher, -}; -use std::{path::PathBuf, time::Duration}; - -/// This struct stores the file watcher and the filemap for the project. -/// -/// This is where we do workspace discovery and recursively listen for changes in Rust files and asset -/// directories. -pub(crate) struct Watcher { - rx: UnboundedReceiver, - krate: DioxusCrate, - _tx: UnboundedSender, - watcher: Box, -} - -impl Watcher { - pub(crate) fn start(krate: &DioxusCrate, serve: &ServeArgs) -> Self { - let (tx, rx) = futures_channel::mpsc::unbounded(); - - let mut watcher = Self { - watcher: create_notify_watcher(serve, tx.clone()), - _tx: tx, - krate: krate.clone(), - rx, - }; - - watcher.watch_filesystem(); - - watcher - } - - /// Wait for changed files to be detected - pub(crate) async fn wait(&mut self) -> ServeUpdate { - // Wait for the next file to change - let mut changes: Vec<_> = self.rx.next().await.into_iter().collect(); - - // Dequeue in bulk if we can, we might've received a lot of events in one go - while let Some(event) = self.rx.try_next().ok().flatten() { - changes.push(event); - } - - // Filter the changes - let mut files: Vec = vec![]; - - // Decompose the events into a list of all the files that have changed - for event in changes.drain(..) { - // Make sure we add new folders to the watch list, provided they're not matched by the ignore list - // We'll only watch new folders that are found under the crate, and then update our watcher to watch them - // This unfortunately won't pick up new krates added "at a distance" - IE krates not within the workspace. - if let EventKind::Create(_create_kind) = event.kind { - // If it's a new folder, watch it - // If it's a new cargo.toml (ie dep on the fly), - // todo(jon) support new folders on the fly - } - - for path in event.paths { - // Workaround for notify and vscode-like editor: - // when edit & save a file in vscode, there will be two notifications, - // the first one is a file with empty content. - // filter the empty file notification to avoid false rebuild during hot-reload - if let Ok(metadata) = std::fs::metadata(&path) { - if metadata.len() == 0 { - continue; - } - } - - files.push(path); - } - } - - tracing::debug!("Files changed: {files:?}"); - - ServeUpdate::FilesChanged { files } - } - - fn watch_filesystem(&mut self) { - // Watch the folders of the crates that we're interested in - for path in self.krate.watch_paths() { - tracing::debug!("Watching path {path:?}"); - - if let Err(err) = self.watcher.watch(&path, RecursiveMode::Recursive) { - handle_notify_error(err); - } - } - - // Also watch the crates themselves, but not recursively, such that we can pick up new folders - for krate in self.krate.all_watched_crates() { - tracing::debug!("Watching path {krate:?}"); - if let Err(err) = self.watcher.watch(&krate, RecursiveMode::NonRecursive) { - handle_notify_error(err); - } - } - - // Also watch the workspace dir, non recursively, such that we can pick up new folders there too - if let Err(err) = self - .watcher - .watch(&self.krate.workspace_dir(), RecursiveMode::NonRecursive) - { - handle_notify_error(err); - } - } -} - -fn handle_notify_error(err: notify::Error) { - tracing::debug!("Failed to watch path: {}", err); - match err.kind { - notify::ErrorKind::Io(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { - tracing::error!("Failed to watch path: permission denied. {:?}", err.paths) - } - notify::ErrorKind::MaxFilesWatch => { - tracing::error!("Failed to set up file watcher: too many files to watch") - } - _ => {} - } -} - -fn create_notify_watcher( - serve: &ServeArgs, - tx: UnboundedSender, -) -> Box { - // Build the event handler for notify. - let handler = move |info: notify::Result| { - let Ok(event) = info else { - return; - }; - - let is_allowed_notify_event = match event.kind { - EventKind::Modify(ModifyKind::Data(_)) => true, - EventKind::Modify(ModifyKind::Name(_)) => true, - // The primary modification event on WSL's poll watcher. - EventKind::Modify(ModifyKind::Metadata(MetadataKind::WriteTime)) => true, - // Catch-all for unknown event types (windows) - EventKind::Modify(ModifyKind::Any) => true, - EventKind::Modify(ModifyKind::Metadata(_)) => false, - // Don't care about anything else. - EventKind::Create(_) => true, - EventKind::Remove(_) => true, - _ => false, - }; - - if is_allowed_notify_event { - _ = tx.unbounded_send(event); - } - }; - - const NOTIFY_ERROR_MSG: &str = "Failed to create file watcher.\nEnsure you have the required permissions to watch the specified directories."; - - if !is_wsl() { - return Box::new(notify::recommended_watcher(handler).expect(NOTIFY_ERROR_MSG)); - } - - let poll_interval = Duration::from_secs(serve.wsl_file_poll_interval.unwrap_or(2) as u64); - - Box::new( - notify::PollWatcher::new(handler, Config::default().with_poll_interval(poll_interval)) - .expect(NOTIFY_ERROR_MSG), - ) -} diff --git a/packages/cli/src/settings.rs b/packages/cli/src/settings.rs index 1946619202..a16961947f 100644 --- a/packages/cli/src/settings.rs +++ b/packages/cli/src/settings.rs @@ -32,10 +32,14 @@ impl CliSettings { /// Load the settings from the local, global, or default config in that order pub(crate) fn load() -> Arc { static SETTINGS: Lazy> = - Lazy::new(|| Arc::new(CliSettings::from_global().unwrap_or_default())); + Lazy::new(|| Arc::new(CliSettings::global_or_default())); SETTINGS.clone() } + pub fn global_or_default() -> Self { + CliSettings::from_global().unwrap_or_default() + } + /// Get the current settings structure from global. pub(crate) fn from_global() -> Option { let Some(path) = dirs::data_local_dir() else { @@ -124,7 +128,7 @@ impl CliSettings { return true; } - if std::env::var("NO_DOWNLOADS").is_ok() { + if crate::devcfg::no_downloads() { return true; } diff --git a/packages/cli/src/wasm_bindgen.rs b/packages/cli/src/wasm_bindgen.rs index ceea2e7983..b2b6626b8f 100644 --- a/packages/cli/src/wasm_bindgen.rs +++ b/packages/cli/src/wasm_bindgen.rs @@ -1,8 +1,7 @@ use crate::{CliSettings, Result}; use anyhow::{anyhow, Context}; use flate2::read::GzDecoder; -use std::path::PathBuf; -use std::{path::Path, process::Stdio}; +use std::path::{Path, PathBuf}; use tar::Archive; use tempfile::TempDir; use tokio::{fs, process::Command}; @@ -100,7 +99,7 @@ impl WasmBindgen { } /// Run the bindgen command with the current settings - pub(crate) async fn run(&self) -> Result<()> { + pub(crate) async fn run(&self) -> Result { let binary = self.get_binary_path().await?; let mut args = Vec::new(); @@ -160,14 +159,24 @@ impl WasmBindgen { tracing::debug!("wasm-bindgen args: {:#?}", args); // Run bindgen - Command::new(binary) - .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; + let output = Command::new(binary).args(args).output().await?; + + // Check for errors + if !output.stderr.is_empty() { + if output.status.success() { + tracing::debug!( + "wasm-bindgen warnings: {}", + String::from_utf8_lossy(&output.stderr) + ); + } else { + tracing::error!( + "wasm-bindgen error: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } - Ok(()) + Ok(output) } /// Verify the installed version of wasm-bindgen-cli @@ -288,8 +297,6 @@ impl WasmBindgen { "--install-path", ]) .arg(tempdir.path()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) .output() .await?; @@ -322,8 +329,6 @@ impl WasmBindgen { "--root", ]) .arg(tempdir.path()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) .output() .await .context("failed to install wasm-bindgen-cli from cargo-install")?; @@ -455,7 +460,7 @@ mod test { /// Test the github installer. #[tokio::test] async fn test_github_install() { - if std::env::var("TEST_INSTALLS").is_err() { + if !crate::devcfg::test_installs() { return; } let binary = WasmBindgen::new(VERSION); @@ -468,7 +473,7 @@ mod test { /// Test the cargo installer. #[tokio::test] async fn test_cargo_install() { - if std::env::var("TEST_INSTALLS").is_err() { + if !crate::devcfg::test_installs() { return; } let binary = WasmBindgen::new(VERSION); @@ -482,7 +487,7 @@ mod test { // Test the binstall installer #[tokio::test] async fn test_binstall_install() { - if std::env::var("TEST_INSTALLS").is_err() { + if !crate::devcfg::test_installs() { return; } let binary = WasmBindgen::new(VERSION); diff --git a/packages/cli/src/wasm_opt.rs b/packages/cli/src/wasm_opt.rs index 855ab61d16..fa3607c0d8 100644 --- a/packages/cli/src/wasm_opt.rs +++ b/packages/cli/src/wasm_opt.rs @@ -2,15 +2,6 @@ use crate::config::WasmOptLevel; use crate::{Result, WasmOptConfig}; use std::path::Path; -#[memoize::memoize(SharedCache)] -pub fn wasm_opt_available() -> bool { - if cfg!(feature = "optimizations") { - return true; - } - - which::which("wasm-opt").is_ok() -} - /// Write these wasm bytes with a particular set of optimizations pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { tokio::fs::write(output_path, bytes).await?; diff --git a/packages/cli/src/workspace.rs b/packages/cli/src/workspace.rs new file mode 100644 index 0000000000..16e858d81f --- /dev/null +++ b/packages/cli/src/workspace.rs @@ -0,0 +1,384 @@ +use crate::CliSettings; +use crate::Result; +use crate::{config::DioxusConfig, AndroidTools}; +use anyhow::Context; +use ignore::gitignore::Gitignore; +use krates::KrateDetails; +use krates::{Cmd, Krates, NodeId}; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use target_lexicon::Triple; +use tokio::process::Command; + +pub struct Workspace { + pub(crate) krates: Krates, + pub(crate) settings: CliSettings, + pub(crate) wasm_opt: Option, + pub(crate) sysroot: PathBuf, + pub(crate) rustc_version: String, + pub(crate) ignore: Gitignore, + pub(crate) cargo_toml: cargo_toml::Manifest, + pub(crate) android_tools: Option>, +} + +impl Workspace { + /// Load the workspace from the current directory. This is cached and will only be loaded once. + pub async fn current() -> Result> { + static WS: tokio::sync::Mutex>> = tokio::sync::Mutex::const_new(None); + + // Lock the workspace to prevent multiple threads from loading it at the same time + // If loading the workspace failed the first time, it won't be set and therefore permeate an error. + let mut lock = WS.lock().await; + if let Some(ws) = lock.as_ref() { + return Ok(ws.clone()); + } + + tracing::debug!("Loading workspace!"); + + let cmd = Cmd::new(); + let mut builder = krates::Builder::new(); + builder.workspace(true); + let krates = builder + .build(cmd, |_| {}) + .context("Failed to run cargo metadata")?; + + let settings = CliSettings::global_or_default(); + let sysroot = Command::new("rustc") + .args(["--print", "sysroot"]) + .output() + .await + .map(|out| String::from_utf8(out.stdout))? + .context("Failed to extract rustc sysroot output")?; + + let rustc_version = Command::new("rustc") + .args(["--version"]) + .output() + .await + .map(|out| String::from_utf8(out.stdout))? + .context("Failed to extract rustc version output")?; + + let wasm_opt = which::which("wasm-opt").ok(); + + let ignore = Self::workspace_gitignore(krates.workspace_root().as_std_path()); + + let cargo_toml = + cargo_toml::Manifest::from_path(krates.workspace_root().join("Cargo.toml")) + .context("Failed to load Cargo.toml")?; + + let android_tools = crate::build::get_android_tools(); + + let workspace = Arc::new(Self { + krates, + settings, + wasm_opt, + sysroot: sysroot.trim().into(), + rustc_version: rustc_version.trim().into(), + ignore, + cargo_toml, + android_tools, + }); + + lock.replace(workspace.clone()); + + Ok(workspace) + } + + pub fn android_tools(&self) -> Result> { + Ok(self + .android_tools + .clone() + .context("Android not installed properly. Please set the `ANDROID_NDK_HOME` environment variable to the root of your NDK installation.")?) + } + + pub fn is_release_profile(&self, profile: &str) -> bool { + // Check if the profile inherits from release by traversing the `inherits` chain + let mut current_profile_name = profile; + + // Try to find the current profile in the custom profiles section + while let Some(profile_settings) = self.cargo_toml.profile.custom.get(current_profile_name) + { + if profile == "release" { + return true; + } + + // Check what this profile inherits from + match &profile_settings.inherits { + // Otherwise, continue checking the profile it inherits from + Some(inherits_name) => current_profile_name = inherits_name, + + // This profile doesn't explicitly inherit anything, so the chain ends here. + // Since it didn't lead to "release", return false. + None => break, + } + } + + false + } + + #[allow(unused)] + pub fn rust_lld(&self) -> PathBuf { + self.sysroot + .join("lib") + .join("rustlib") + .join(Triple::host().to_string()) + .join("bin") + .join("rust-lld") + } + + /// Return the path to the `cc` compiler + /// + /// This is used for the patching system to run the linker. + /// We could also just use lld given to us by rust itself. + pub fn cc(&self) -> PathBuf { + PathBuf::from("cc") + } + + /// The windows linker + pub fn lld_link(&self) -> PathBuf { + self.gcc_ld_dir().join("lld-link") + } + + pub fn wasm_ld(&self) -> PathBuf { + self.gcc_ld_dir().join("wasm-ld") + } + + // wasm-ld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/wasm-ld + // rust-lld: ./rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rust-lld + fn gcc_ld_dir(&self) -> PathBuf { + self.sysroot + .join("lib") + .join("rustlib") + .join(Triple::host().to_string()) + .join("bin") + .join("gcc-ld") + } + + pub fn has_wasm32_unknown_unknown(&self) -> bool { + self.sysroot + .join("lib/rustlib/wasm32-unknown-unknown") + .exists() + } + + /// Find the "main" package in the workspace. There might not be one! + pub fn find_main_package(&self, package: Option) -> Result { + if let Some(package) = package { + let mut workspace_members = self.krates.workspace_members(); + let found = workspace_members.find_map(|node| { + if let krates::Node::Krate { id, krate, .. } = node { + if krate.name == package { + return Some(id); + } + } + None + }); + + if found.is_none() { + tracing::error!("Could not find package {package} in the workspace. Did you forget to add it to the workspace?"); + tracing::error!("Packages in the workspace:"); + for package in self.krates.workspace_members() { + if let krates::Node::Krate { krate, .. } = package { + tracing::error!("{}", krate.name()); + } + } + } + + let kid = found.ok_or_else(|| anyhow::anyhow!("Failed to find package {package}"))?; + + return Ok(self.krates.nid_for_kid(kid).unwrap()); + }; + + // Otherwise find the package that is the closest parent of the current directory + let current_dir = std::env::current_dir()?; + let current_dir = current_dir.as_path(); + + // Go through each member and find the path that is a parent of the current directory + let mut closest_parent = None; + for member in self.krates.workspace_members() { + if let krates::Node::Krate { id, krate, .. } = member { + let member_path = krate.manifest_path.parent().unwrap(); + if let Ok(path) = current_dir.strip_prefix(member_path.as_std_path()) { + let len = path.components().count(); + match closest_parent { + Some((_, closest_parent_len)) => { + if len < closest_parent_len { + closest_parent = Some((id, len)); + } + } + None => { + closest_parent = Some((id, len)); + } + } + } + } + } + + let kid = closest_parent + .map(|(id, _)| id) + .with_context(|| { + let bin_targets = self.krates.workspace_members().filter_map(|krate|match krate { + krates::Node::Krate { krate, .. } if krate.targets.iter().any(|t| t.kind.contains(&krates::cm::TargetKind::Bin))=> { + Some(format!("- {}", krate.name)) + } + _ => None + }).collect::>(); + format!("Failed to find binary package to build.\nYou need to either run dx from inside a binary crate or specify a binary package to build with the `--package` flag. Try building again with one of the binary packages in the workspace:\n{}", bin_targets.join("\n")) + })?; + + let package = self.krates.nid_for_kid(kid).unwrap(); + Ok(package) + } + + pub fn load_dioxus_config(&self, package: NodeId) -> Result> { + // Walk up from the cargo.toml to the root of the workspace looking for Dioxus.toml + let mut current_dir = self.krates[package] + .manifest_path + .parent() + .unwrap() + .as_std_path() + .to_path_buf() + .canonicalize()?; + + let workspace_path = self + .krates + .workspace_root() + .as_std_path() + .to_path_buf() + .canonicalize()?; + + let mut dioxus_conf_file = None; + while current_dir.starts_with(&workspace_path) { + let config = ["Dioxus.toml", "dioxus.toml"] + .into_iter() + .map(|file| current_dir.join(file)) + .find(|path| path.is_file()); + + // Try to find Dioxus.toml in the current directory + if let Some(new_config) = config { + dioxus_conf_file = Some(new_config.as_path().to_path_buf()); + break; + } + // If we can't find it, go up a directory + current_dir = current_dir + .parent() + .context("Failed to find Dioxus.toml")? + .to_path_buf(); + } + + let Some(dioxus_conf_file) = dioxus_conf_file else { + return Ok(None); + }; + + toml::from_str::(&std::fs::read_to_string(&dioxus_conf_file)?) + .map_err(|err| { + anyhow::anyhow!("Failed to parse Dioxus.toml at {dioxus_conf_file:?}: {err}").into() + }) + .map(Some) + } + + /// Create a new gitignore map for this target crate + /// + /// todo(jon): this is a bit expensive to build, so maybe we should cache it? + pub fn workspace_gitignore(workspace_dir: &Path) -> Gitignore { + let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(workspace_dir); + ignore_builder.add(workspace_dir.join(".gitignore")); + + // todo!() + // let workspace_dir = self.workspace_dir(); + // ignore_builder.add(workspace_dir.join(".gitignore")); + + for path in Self::default_ignore_list() { + ignore_builder + .add_line(None, path) + .expect("failed to add path to file excluded"); + } + + ignore_builder.build().unwrap() + } + + pub fn ignore_for_krate(&self, path: &Path) -> ignore::gitignore::Gitignore { + let mut ignore_builder = ignore::gitignore::GitignoreBuilder::new(path); + for path in Self::default_ignore_list() { + ignore_builder + .add_line(None, path) + .expect("failed to add path to file excluded"); + } + ignore_builder.build().unwrap() + } + + pub fn default_ignore_list() -> Vec<&'static str> { + vec![ + ".git", + ".github", + ".vscode", + "target", + "node_modules", + "dist", + "*~", + ".*", + "*.lock", + "*.log", + ] + } + + pub(crate) fn workspace_root(&self) -> PathBuf { + self.krates.workspace_root().as_std_path().to_path_buf() + } + + /// Returns the root of the crate that the command is run from, without calling `cargo metadata` + /// + /// If the command is run from the workspace root, this will return the top-level Cargo.toml + pub(crate) fn crate_root_from_path() -> Result { + /// How many parent folders are searched for a `Cargo.toml` + const MAX_ANCESTORS: u32 = 10; + + /// Checks if the directory contains `Cargo.toml` + fn contains_manifest(path: &Path) -> bool { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(Result::ok) + .any(|ent| &ent.file_name() == "Cargo.toml") + }) + .unwrap_or(false) + } + + // From the current directory we work our way up, looking for `Cargo.toml` + std::env::current_dir() + .ok() + .and_then(|mut wd| { + for _ in 0..MAX_ANCESTORS { + if contains_manifest(&wd) { + return Some(wd); + } + if !wd.pop() { + break; + } + } + None + }) + .ok_or_else(|| { + crate::Error::Cargo("Failed to find directory containing Cargo.toml".to_string()) + }) + } + + /// Returns the properly canonicalized path to the dx executable, used for linking and wrapping rustc + pub(crate) fn path_to_dx() -> Result { + Ok( + dunce::canonicalize(std::env::current_exe().context("Failed to find dx")?) + .context("Failed to find dx")?, + ) + } +} + +impl std::fmt::Debug for Workspace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Workspace") + .field("krates", &"..") + .field("settings", &self.settings) + .field("rustc_version", &self.rustc_version) + .field("sysroot", &self.sysroot) + .field("wasm_opt", &self.wasm_opt) + .finish() + } +} diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index f082a5bb40..f39a1d14e8 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -24,6 +24,7 @@ tracing = { workspace = true } warnings = { workspace = true } futures-util = { workspace = true, default-features = false, features = ["alloc", "std"] } serde = { workspace = true, optional = true, features = ["derive"] } +subsecond = { workspace = true } [dev-dependencies] dioxus = { workspace = true } diff --git a/packages/core/README.md b/packages/core/README.md index a4b80f5000..c7a7e0526a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -65,7 +65,7 @@ use dioxus::prelude::*; // First, declare a root component fn app() -> Element { - rsx!{ + rsx! { div { "hello world" } } } diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index a8289151b4..da05b2d9f2 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -479,3 +479,18 @@ pub fn use_hook_with_cleanup( use_drop(move || cleanup(_value)); value } + +/// Force every component to be dirty and require a re-render. Used by hot-reloading. +/// +/// This might need to change to a different flag in the event hooks order changes within components. +/// What we really need is a way to mark components as needing a complete rebuild if they were hit by changes. +pub fn force_all_dirty() { + Runtime::with(|rt| { + rt.scope_states.borrow_mut().iter().for_each(|state| { + if let Some(scope) = state.as_ref() { + scope.needs_update(); + } + }); + }) + .expect("Runtime to exist"); +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 3407814ab7..672c846e96 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -93,17 +93,18 @@ pub use crate::innerlude::{ pub mod prelude { pub use crate::innerlude::{ consume_context, consume_context_from_scope, current_owner, current_scope_id, - fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope, - provide_context, provide_error_boundary, provide_root_context, queue_effect, remove_future, - schedule_update, schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, - suspense_context, throw_error, try_consume_context, use_after_render, use_before_render, - use_drop, use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, - Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, - EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, - OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard, - ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, - SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template, - TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom, + fc_to_builder, force_all_dirty, generation, has_context, needs_update, needs_update_any, + parent_scope, provide_context, provide_error_boundary, provide_root_context, queue_effect, + remove_future, schedule_update, schedule_update_any, spawn, spawn_forever, + spawn_isomorphic, suspend, suspense_context, throw_error, try_consume_context, + use_after_render, use_before_render, use_drop, use_hook, use_hook_with_cleanup, with_owner, + AnyValue, Attribute, Callback, Component, ComponentFunction, Context, Element, + ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes, + IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, ReactiveContext, + RenderError, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, + SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext, + SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode, VNodeInner, + VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 8131899c6d..66e7c9bcf7 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -609,10 +609,8 @@ pub struct VComponent { /// The name of this component pub name: &'static str, - /// The function pointer of the component, known at compile time - /// - /// It is possible that components get folded at compile time, so these shouldn't be really used as a key - pub(crate) render_fn: TypeId, + /// The raw pointer to the render function + pub(crate) render_fn: usize, /// The props for this component pub(crate) props: BoxedAnyProps, @@ -622,8 +620,8 @@ impl Clone for VComponent { fn clone(&self) -> Self { Self { name: self.name, - render_fn: self.render_fn, props: self.props.duplicate(), + render_fn: self.render_fn, } } } @@ -638,7 +636,7 @@ impl VComponent { where P: Properties + 'static, { - let render_fn = component.id(); + let render_fn = component.fn_ptr(); let props = Box::new(VProps::new( component,

::memoize, @@ -647,9 +645,9 @@ impl VComponent { )); VComponent { + render_fn, name: fn_name, props, - render_fn, } } diff --git a/packages/core/src/properties.rs b/packages/core/src/properties.rs index 08d048c6b1..f0c65b2ad3 100644 --- a/packages/core/src/properties.rs +++ b/packages/core/src/properties.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, fmt::Arguments}; +use std::fmt::Arguments; use crate::innerlude::*; @@ -167,27 +167,39 @@ pub fn verify_component_called_as_component, P, M>(co ) )] pub trait ComponentFunction: Clone + 'static { - /// Get the type id of the component. - fn id(&self) -> TypeId { - TypeId::of::() - } + /// Get the raw address of the component render function. + fn fn_ptr(&self) -> usize; /// Convert the component to a function that takes props and returns an element. fn rebuild(&self, props: Props) -> Element; } /// Accept any callbacks that take props -impl Element + Clone + 'static, P> ComponentFunction

for F { +impl ComponentFunction

for F +where + F: Fn(P) -> Element + Clone + 'static, +{ fn rebuild(&self, props: P) -> Element { - self(props) + subsecond::HotFn::current(self.clone()).call((props,)) + } + + fn fn_ptr(&self) -> usize { + subsecond::HotFn::current(self.clone()).ptr_address() as usize } } /// Accept any callbacks that take no props pub struct EmptyMarker; -impl Element + Clone + 'static> ComponentFunction<(), EmptyMarker> for F { - fn rebuild(&self, _: ()) -> Element { - self() +impl ComponentFunction<(), EmptyMarker> for F +where + F: Fn() -> Element + Clone + 'static, +{ + fn rebuild(&self, props: ()) -> Element { + subsecond::HotFn::current(self.clone()).call(props) + } + + fn fn_ptr(&self) -> usize { + subsecond::HotFn::current(self.clone()).ptr_address() as usize } } diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index d9d7790704..4311476872 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -380,6 +380,9 @@ impl TaskType { /// These messages control how the scheduler will process updates to the UI. #[derive(Debug)] pub(crate) enum SchedulerMsg { + /// All components have been marked as dirty, requiring a full render + AllDirty, + /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index b5b55e4456..761f57d901 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -16,7 +16,7 @@ use crate::{Task, VComponent}; use futures_util::StreamExt; use slab::Slab; use std::collections::BTreeSet; -use std::{any::Any, rc::Rc}; +use std::{any::Any, rc::Rc, sync::Arc}; use tracing::instrument; /// A virtual node system that progresses user events and diffs UI trees. @@ -294,7 +294,7 @@ impl VirtualDom { root: impl ComponentFunction, root_props: P, ) -> Self { - let render_fn = root.id(); + let render_fn = root.fn_ptr(); let props = VProps::new(root, |_, _| true, root_props, "Root"); Self::new_with_component(VComponent { name: "root", @@ -331,6 +331,9 @@ impl VirtualDom { ); dom.new_scope(Box::new(root), "app"); + #[cfg(debug_assertions)] + dom.register_subsecond_handler(); + dom } @@ -375,6 +378,19 @@ impl VirtualDom { self.base_scope().state().provide_any_context(context); } + /// Mark all scopes as dirty. Each scope will be re-rendered. + pub fn mark_all_dirty(&mut self) { + let mut orders = vec![]; + + for (_idx, scope) in self.scopes.iter() { + orders.push(ScopeOrder::new(scope.state().height(), scope.id())); + } + + for order in orders { + self.queue_scope(order); + } + } + /// Manually mark a scope as requiring a re-render /// /// Whenever the Runtime "works", it will re-render this scope @@ -458,6 +474,7 @@ impl VirtualDom { self.mark_task_dirty(Task::from_id(id)); } SchedulerMsg::EffectQueued => {} + SchedulerMsg::AllDirty => self.mark_all_dirty(), }; } @@ -469,6 +486,7 @@ impl VirtualDom { SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)), SchedulerMsg::EffectQueued => {} + SchedulerMsg::AllDirty => self.mark_all_dirty(), } } } @@ -744,6 +762,14 @@ impl VirtualDom { let event = crate::Event::new(event, bubbling); self.runtime().handle_event(name, event, element); } + + #[cfg(debug_assertions)] + fn register_subsecond_handler(&self) { + let sender = self.runtime().sender.clone(); + subsecond::register_handler(Arc::new(move || { + _ = sender.unbounded_send(SchedulerMsg::AllDirty); + })); + } } impl Drop for VirtualDom { diff --git a/packages/depinfo/Cargo.toml b/packages/depinfo/Cargo.toml new file mode 100644 index 0000000000..2071e44e1d --- /dev/null +++ b/packages/depinfo/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "depinfo" +version.workspace = true +edition = "2021" +authors = ["Jonathan Kelley"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +description = "Depinfo parser for Rust" +keywords = ["rustc", "cargo", "dep-info" ] + +[dependencies] +thiserror = { workspace = true } + diff --git a/packages/depinfo/README.md b/packages/depinfo/README.md new file mode 100644 index 0000000000..cc9bebc996 --- /dev/null +++ b/packages/depinfo/README.md @@ -0,0 +1,14 @@ +# Rustc Dep-Info Parser + +This crate parses the output of rustc's `.d` dep-info file. It is used by the hot-reloading engine and other libraries to provide higher quality dependency analysis for the user's project. + +## License + +This project is licensed under either the [MIT license] or the [Apache-2 License]. + +[apache-2 license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-APACHE +[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in Dioxus by you, shall be licensed as MIT or Apache-2, without any additional +terms or conditions. diff --git a/packages/depinfo/src/dx.d b/packages/depinfo/src/dx.d new file mode 100644 index 0000000000..be32ed87c0 --- /dev/null +++ b/packages/depinfo/src/dx.d @@ -0,0 +1 @@ +/dioxus/target/debug/dx: /dioxus/packages/autofmt/README.md /dioxus/packages/autofmt/src/buffer.rs /dioxus/packages/autofmt/src/collect_macros.rs /dioxus/packages/autofmt/src/indent.rs /dioxus/packages/autofmt/src/lib.rs /dioxus/packages/autofmt/src/prettier_please.rs /dioxus/packages/autofmt/src/writer.rs /dioxus/packages/check/README.md /dioxus/packages/check/src/check.rs /dioxus/packages/check/src/issues.rs /dioxus/packages/check/src/lib.rs /dioxus/packages/check/src/metadata.rs /dioxus/packages/cli/README.md /dioxus/packages/cli/assets/android/MainActivity.kt.hbs /dioxus/packages/cli/assets/android/gen/app/build.gradle.kts.hbs /dioxus/packages/cli/assets/android/gen/app/proguard-rules.pro /dioxus/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs /dioxus/packages/cli/assets/android/gen/app/src/main/res/drawable/ic_launcher_background.xml /dioxus/packages/cli/assets/android/gen/app/src/main/res/drawable-v24/ic_launcher_foreground.xml /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-hdpi/ic_launcher.webp /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-mdpi/ic_launcher.webp /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xhdpi/ic_launcher.webp /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp /dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp /dioxus/packages/cli/assets/android/gen/app/src/main/res/values/colors.xml /dioxus/packages/cli/assets/android/gen/app/src/main/res/values/strings.xml.hbs /dioxus/packages/cli/assets/android/gen/app/src/main/res/values/styles.xml /dioxus/packages/cli/assets/android/gen/build.gradle.kts /dioxus/packages/cli/assets/android/gen/gradle/wrapper/gradle-wrapper.jar /dioxus/packages/cli/assets/android/gen/gradle/wrapper/gradle-wrapper.properties /dioxus/packages/cli/assets/android/gen/gradle.properties /dioxus/packages/cli/assets/android/gen/gradlew /dioxus/packages/cli/assets/android/gen/gradlew.bat /dioxus/packages/cli/assets/android/gen/settings.gradle /dioxus/packages/cli/assets/dioxus.toml /dioxus/packages/cli/assets/ios/ios.plist.hbs /dioxus/packages/cli/assets/macos/mac.plist.hbs /dioxus/packages/cli/assets/web/index.html /dioxus/packages/cli/assets/web/loading.html /dioxus/packages/cli/assets/web/toast.html /dioxus/packages/cli/build.rs /dioxus/packages/cli/src/build/builder.rs /dioxus/packages/cli/src/build/bundle.rs /dioxus/packages/cli/src/build/mod.rs /dioxus/packages/cli/src/build/prerender.rs /dioxus/packages/cli/src/build/progress.rs /dioxus/packages/cli/src/build/request.rs /dioxus/packages/cli/src/build/templates.rs /dioxus/packages/cli/src/build/verify.rs /dioxus/packages/cli/src/build/web.rs /dioxus/packages/cli/src/bundle_utils.rs /dioxus/packages/cli/src/cli/autoformat.rs /dioxus/packages/cli/src/cli/build.rs /dioxus/packages/cli/src/cli/bundle.rs /dioxus/packages/cli/src/cli/check.rs /dioxus/packages/cli/src/cli/clean.rs /dioxus/packages/cli/src/cli/config.rs /dioxus/packages/cli/src/cli/create.rs /dioxus/packages/cli/src/cli/init.rs /dioxus/packages/cli/src/cli/link.rs /dioxus/packages/cli/src/cli/mod.rs /dioxus/packages/cli/src/cli/run.rs /dioxus/packages/cli/src/cli/serve.rs /dioxus/packages/cli/src/cli/target.rs /dioxus/packages/cli/src/cli/translate.rs /dioxus/packages/cli/src/cli/verbosity.rs /dioxus/packages/cli/src/config/app.rs /dioxus/packages/cli/src/config/bundle.rs /dioxus/packages/cli/src/config/desktop.rs /dioxus/packages/cli/src/config/dioxus_config.rs /dioxus/packages/cli/src/config/serve.rs /dioxus/packages/cli/src/config/web.rs /dioxus/packages/cli/src/config.rs /dioxus/packages/cli/src/dioxus_crate.rs /dioxus/packages/cli/src/dx_build_info.rs /dioxus/packages/cli/src/error.rs /dioxus/packages/cli/src/fastfs.rs /dioxus/packages/cli/src/filemap.rs /dioxus/packages/cli/src/logging.rs /dioxus/packages/cli/src/main.rs /dioxus/packages/cli/src/metadata.rs /dioxus/packages/cli/src/platform.rs /dioxus/packages/cli/src/rustc.rs /dioxus/packages/cli/src/serve/ansi_buffer.rs /dioxus/packages/cli/src/serve/detect.rs /dioxus/packages/cli/src/serve/handle.rs /dioxus/packages/cli/src/serve/mod.rs /dioxus/packages/cli/src/serve/output.rs /dioxus/packages/cli/src/serve/proxy.rs /dioxus/packages/cli/src/serve/runner.rs /dioxus/packages/cli/src/serve/server.rs /dioxus/packages/cli/src/serve/update.rs /dioxus/packages/cli/src/serve/watcher.rs /dioxus/packages/cli/src/settings.rs /dioxus/packages/cli/src/wasm_bindgen.rs /dioxus/packages/cli-config/src/lib.rs /dioxus/packages/cli-opt/src/css.rs /dioxus/packages/cli-opt/src/file.rs /dioxus/packages/cli-opt/src/folder.rs /dioxus/packages/cli-opt/src/image/jpg.rs /dioxus/packages/cli-opt/src/image/mod.rs /dioxus/packages/cli-opt/src/image/png.rs /dioxus/packages/cli-opt/src/js.rs /dioxus/packages/cli-opt/src/json.rs /dioxus/packages/cli-opt/src/lib.rs /dioxus/packages/config-macro/README.md /dioxus/packages/config-macro/src/lib.rs /dioxus/packages/const-serialize/README.md /dioxus/packages/const-serialize/src/const_buffers.rs /dioxus/packages/const-serialize/src/const_vec.rs /dioxus/packages/const-serialize/src/lib.rs /dioxus/packages/const-serialize-macro/src/lib.rs /dioxus/packages/core/README.md /dioxus/packages/core/docs/common_spawn_errors.md /dioxus/packages/core/docs/reactivity.md /dioxus/packages/core/src/any_props.rs /dioxus/packages/core/src/arena.rs /dioxus/packages/core/src/diff/component.rs /dioxus/packages/core/src/diff/iterator.rs /dioxus/packages/core/src/diff/mod.rs /dioxus/packages/core/src/diff/node.rs /dioxus/packages/core/src/effect.rs /dioxus/packages/core/src/error_boundary.rs /dioxus/packages/core/src/events.rs /dioxus/packages/core/src/fragment.rs /dioxus/packages/core/src/generational_box.rs /dioxus/packages/core/src/global_context.rs /dioxus/packages/core/src/hotreload_utils.rs /dioxus/packages/core/src/launch.rs /dioxus/packages/core/src/lib.rs /dioxus/packages/core/src/mutations.rs /dioxus/packages/core/src/nodes.rs /dioxus/packages/core/src/properties.rs /dioxus/packages/core/src/reactive_context.rs /dioxus/packages/core/src/render_error.rs /dioxus/packages/core/src/root_wrapper.rs /dioxus/packages/core/src/runtime.rs /dioxus/packages/core/src/scheduler.rs /dioxus/packages/core/src/scope_arena.rs /dioxus/packages/core/src/scope_context.rs /dioxus/packages/core/src/scopes.rs /dioxus/packages/core/src/suspense/component.rs /dioxus/packages/core/src/suspense/mod.rs /dioxus/packages/core/src/tasks.rs /dioxus/packages/core/src/virtual_dom.rs /dioxus/packages/core-macro/README.md /dioxus/packages/core-macro/docs/component.md /dioxus/packages/core-macro/docs/props.md /dioxus/packages/core-macro/docs/rsx.md /dioxus/packages/core-macro/src/component.rs /dioxus/packages/core-macro/src/lib.rs /dioxus/packages/core-macro/src/props/mod.rs /dioxus/packages/core-macro/src/utils.rs /dioxus/packages/core-types/src/bubbles.rs /dioxus/packages/core-types/src/bundled.rs /dioxus/packages/core-types/src/formatter.rs /dioxus/packages/core-types/src/hr_context.rs /dioxus/packages/core-types/src/lib.rs /dioxus/packages/devtools/src/lib.rs /dioxus/packages/devtools-types/src/lib.rs /dioxus/packages/dioxus-lib/README.md /dioxus/packages/dioxus-lib/src/lib.rs /dioxus/packages/document/build.rs /dioxus/packages/document/docs/eval.md /dioxus/packages/document/docs/head.md /dioxus/packages/document/src/document.rs /dioxus/packages/document/src/elements/link.rs /dioxus/packages/document/src/elements/meta.rs /dioxus/packages/document/src/elements/mod.rs /dioxus/packages/document/src/elements/script.rs /dioxus/packages/document/src/elements/style.rs /dioxus/packages/document/src/elements/stylesheet.rs /dioxus/packages/document/src/elements/title.rs /dioxus/packages/document/src/error.rs /dioxus/packages/document/src/eval.rs /dioxus/packages/document/src/js/head.js /dioxus/packages/document/src/lib.rs /dioxus/packages/document/./src/ts/eval.ts /dioxus/packages/document/./src/ts/head.ts /dioxus/packages/dx-wire-format/src/lib.rs /dioxus/packages/fullstack/README.md /dioxus/packages/fullstack/src/document/mod.rs /dioxus/packages/fullstack/src/hooks/mod.rs /dioxus/packages/fullstack/src/hooks/server_cached.rs /dioxus/packages/fullstack/src/hooks/server_future.rs /dioxus/packages/fullstack/src/html_storage/mod.rs /dioxus/packages/fullstack/src/lib.rs /dioxus/packages/generational-box/README.md /dioxus/packages/generational-box/src/entry.rs /dioxus/packages/generational-box/src/error.rs /dioxus/packages/generational-box/src/lib.rs /dioxus/packages/generational-box/src/references.rs /dioxus/packages/generational-box/src/sync.rs /dioxus/packages/generational-box/src/unsync.rs /dioxus/packages/history/src/lib.rs /dioxus/packages/history/src/memory.rs /dioxus/packages/hooks/README.md /dioxus/packages/hooks/docs/derived_state.md /dioxus/packages/hooks/docs/moving_state_around.md /dioxus/packages/hooks/docs/rules_of_hooks.md /dioxus/packages/hooks/docs/side_effects.md /dioxus/packages/hooks/docs/use_resource.md /dioxus/packages/hooks/src/lib.rs /dioxus/packages/hooks/src/use_callback.rs /dioxus/packages/hooks/src/use_context.rs /dioxus/packages/hooks/src/use_coroutine.rs /dioxus/packages/hooks/src/use_effect.rs /dioxus/packages/hooks/src/use_future.rs /dioxus/packages/hooks/src/use_hook_did_run.rs /dioxus/packages/hooks/src/use_memo.rs /dioxus/packages/hooks/src/use_on_destroy.rs /dioxus/packages/hooks/src/use_reactive.rs /dioxus/packages/hooks/src/use_resource.rs /dioxus/packages/hooks/src/use_root_context.rs /dioxus/packages/hooks/src/use_set_compare.rs /dioxus/packages/hooks/src/use_signal.rs /dioxus/packages/html/README.md /dioxus/packages/html/docs/common_event_handler_errors.md /dioxus/packages/html/docs/event_handlers.md /dioxus/packages/html/src/attribute_groups.rs /dioxus/packages/html/src/elements.rs /dioxus/packages/html/src/events/animation.rs /dioxus/packages/html/src/events/clipboard.rs /dioxus/packages/html/src/events/composition.rs /dioxus/packages/html/src/events/drag.rs /dioxus/packages/html/src/events/focus.rs /dioxus/packages/html/src/events/form.rs /dioxus/packages/html/src/events/image.rs /dioxus/packages/html/src/events/keyboard.rs /dioxus/packages/html/src/events/media.rs /dioxus/packages/html/src/events/mod.rs /dioxus/packages/html/src/events/mounted.rs /dioxus/packages/html/src/events/mouse.rs /dioxus/packages/html/src/events/pointer.rs /dioxus/packages/html/src/events/resize.rs /dioxus/packages/html/src/events/scroll.rs /dioxus/packages/html/src/events/selection.rs /dioxus/packages/html/src/events/toggle.rs /dioxus/packages/html/src/events/touch.rs /dioxus/packages/html/src/events/transition.rs /dioxus/packages/html/src/events/visible.rs /dioxus/packages/html/src/events/wheel.rs /dioxus/packages/html/src/file_data.rs /dioxus/packages/html/src/geometry.rs /dioxus/packages/html/src/input_data.rs /dioxus/packages/html/src/lib.rs /dioxus/packages/html/src/point_interaction.rs /dioxus/packages/html/src/render_template.rs /dioxus/packages/html-internal-macro/src/lib.rs /dioxus/packages/lazy-js-bundle/src/lib.rs /dioxus/packages/manganis/manganis/README.md /dioxus/packages/manganis/manganis/src/hash.rs /dioxus/packages/manganis/manganis/src/lib.rs /dioxus/packages/manganis/manganis/src/macro_helpers.rs /dioxus/packages/manganis/manganis-core/src/asset.rs /dioxus/packages/manganis/manganis-core/src/css.rs /dioxus/packages/manganis/manganis-core/src/folder.rs /dioxus/packages/manganis/manganis-core/src/hash.rs /dioxus/packages/manganis/manganis-core/src/images.rs /dioxus/packages/manganis/manganis-core/src/js.rs /dioxus/packages/manganis/manganis-core/src/lib.rs /dioxus/packages/manganis/manganis-core/src/linker.rs /dioxus/packages/manganis/manganis-core/src/options.rs /dioxus/packages/manganis/manganis-macro/README.md /dioxus/packages/manganis/manganis-macro/src/asset.rs /dioxus/packages/manganis/manganis-macro/src/lib.rs /dioxus/packages/manganis/manganis-macro/src/linker.rs /dioxus/packages/rsx/src/assign_dyn_ids.rs /dioxus/packages/rsx/src/attribute.rs /dioxus/packages/rsx/src/component.rs /dioxus/packages/rsx/src/diagnostics.rs /dioxus/packages/rsx/src/element.rs /dioxus/packages/rsx/src/expr_node.rs /dioxus/packages/rsx/src/forloop.rs /dioxus/packages/rsx/src/ifchain.rs /dioxus/packages/rsx/src/ifmt.rs /dioxus/packages/rsx/src/lib.rs /dioxus/packages/rsx/src/literal.rs /dioxus/packages/rsx/src/location.rs /dioxus/packages/rsx/src/node.rs /dioxus/packages/rsx/src/partial_closure.rs /dioxus/packages/rsx/src/raw_expr.rs /dioxus/packages/rsx/src/rsx_block.rs /dioxus/packages/rsx/src/rsx_call.rs /dioxus/packages/rsx/src/template_body.rs /dioxus/packages/rsx/src/text_node.rs /dioxus/packages/rsx/src/util.rs /dioxus/packages/rsx-hotreload/src/collect.rs /dioxus/packages/rsx-hotreload/src/diff.rs /dioxus/packages/rsx-hotreload/src/extensions.rs /dioxus/packages/rsx-hotreload/src/last_build_state.rs /dioxus/packages/rsx-hotreload/src/lib.rs /dioxus/packages/rsx-rosetta/README.md /dioxus/packages/rsx-rosetta/src/lib.rs /dioxus/packages/server-macro/src/lib.rs /dioxus/packages/signals/README.md /dioxus/packages/signals/docs/hoist/error.rs /dioxus/packages/signals/docs/hoist/fixed_list.rs /dioxus/packages/signals/docs/memo.md /dioxus/packages/signals/docs/signals.md /dioxus/packages/signals/src/copy_value.rs /dioxus/packages/signals/src/global/memo.rs /dioxus/packages/signals/src/global/mod.rs /dioxus/packages/signals/src/global/signal.rs /dioxus/packages/signals/src/impls.rs /dioxus/packages/signals/src/lib.rs /dioxus/packages/signals/src/map.rs /dioxus/packages/signals/src/memo.rs /dioxus/packages/signals/src/props.rs /dioxus/packages/signals/src/read.rs /dioxus/packages/signals/src/read_only_signal.rs /dioxus/packages/signals/src/set_compare.rs /dioxus/packages/signals/src/signal.rs /dioxus/packages/signals/src/warnings.rs /dioxus/packages/signals/src/write.rs /dioxus/target/debug/build/dioxus-cli-90993e55e02b7cee/out/built.rs diff --git a/packages/depinfo/src/lib.rs b/packages/depinfo/src/lib.rs new file mode 100644 index 0000000000..a14e6d28aa --- /dev/null +++ b/packages/depinfo/src/lib.rs @@ -0,0 +1,439 @@ +//! Parse the output of rustc's `.d` dep-info file. +//! +//! Used by the hot-reloading engine and other libraries to provide higher quality dependency analysis +//! for the user's project. + +use std::path::PathBuf; + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum DepInfoParseError { + /// The input was malformed - maybe this `.d` format is no longer supported? + #[error("Malformed input")] + MalformedInput, + + /// An env var could not be escaped or parsed - this might be a bug in rustc. + #[error("Failed to parse env var name")] + InvalidEnvVarName, +} + +#[non_exhaustive] +#[derive(Default, Debug)] +pub struct RustcDepInfo { + /// The list of files that the main target in the dep-info file depends on. + pub files: Vec, + + /// The list of environment variables we found that the rustc compilation + /// depends on. + /// + /// The first element of the pair is the name of the env var and the second + /// item is the value. `Some` means that the env var was set, and `None` + /// means that the env var wasn't actually set and the compilation depends + /// on it not being set. + pub env: Vec<(String, Option)>, +} + +impl std::str::FromStr for RustcDepInfo { + type Err = DepInfoParseError; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl RustcDepInfo { + /// Parse the `.d` dep-info file generated by rustc. + pub fn new(contents: &str) -> Result { + let mut ret = RustcDepInfo::default(); + let mut found_deps = false; + + for line in contents.lines() { + if let Some(rest) = line.strip_prefix("# env-dep:") { + let mut parts = rest.splitn(2, '='); + let env_var = match parts.next() { + Some(s) => s, + None => continue, + }; + let env_val = match parts.next() { + Some(s) => Some(unescape_env(s)?), + None => None, + }; + ret.env.push((unescape_env(env_var)?, env_val)); + } else if let Some(pos) = line.find(": ") { + if found_deps { + continue; + } + found_deps = true; + let mut deps = line[pos + 2..].split_whitespace(); + + while let Some(s) = deps.next() { + let mut file = s.to_string(); + while file.ends_with('\\') { + file.pop(); + file.push(' '); + file.push_str(deps.next().ok_or(DepInfoParseError::MalformedInput)?); + } + ret.files.push(file.into()); + } + } + } + return Ok(ret); + + // rustc tries to fit env var names and values all on a single line, which + // means it needs to escape `\r` and `\n`. The escape syntax used is "\n" + // which means that `\` also needs to be escaped. + fn unescape_env(s: &str) -> Result { + let mut ret = String::with_capacity(s.len()); + let mut chars = s.chars(); + while let Some(c) = chars.next() { + if c != '\\' { + ret.push(c); + continue; + } + match chars.next() { + Some('\\') => ret.push('\\'), + Some('n') => ret.push('\n'), + Some('r') => ret.push('\r'), + Some(_) => return Err(DepInfoParseError::InvalidEnvVarName), + None => return Err(DepInfoParseError::InvalidEnvVarName), + } + } + Ok(ret) + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn parses_from_path() { + let contents = include_str!("./dx.d"); + let info: RustcDepInfo = contents.parse().unwrap(); + let answer = vec![ + "/dioxus/packages/autofmt/README.md", + "/dioxus/packages/autofmt/src/buffer.rs", + "/dioxus/packages/autofmt/src/collect_macros.rs", + "/dioxus/packages/autofmt/src/indent.rs", + "/dioxus/packages/autofmt/src/lib.rs", + "/dioxus/packages/autofmt/src/prettier_please.rs", + "/dioxus/packages/autofmt/src/writer.rs", + "/dioxus/packages/check/README.md", + "/dioxus/packages/check/src/check.rs", + "/dioxus/packages/check/src/issues.rs", + "/dioxus/packages/check/src/lib.rs", + "/dioxus/packages/check/src/metadata.rs", + "/dioxus/packages/cli/README.md", + "/dioxus/packages/cli/assets/android/MainActivity.kt.hbs", + "/dioxus/packages/cli/assets/android/gen/app/build.gradle.kts.hbs", + "/dioxus/packages/cli/assets/android/gen/app/proguard-rules.pro", + "/dioxus/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/drawable/ic_launcher_background.xml", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/drawable-v24/ic_launcher_foreground.xml", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-hdpi/ic_launcher.webp", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-mdpi/ic_launcher.webp", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xhdpi/ic_launcher.webp", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/values/colors.xml", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/values/strings.xml.hbs", + "/dioxus/packages/cli/assets/android/gen/app/src/main/res/values/styles.xml", + "/dioxus/packages/cli/assets/android/gen/build.gradle.kts", + "/dioxus/packages/cli/assets/android/gen/gradle/wrapper/gradle-wrapper.jar", + "/dioxus/packages/cli/assets/android/gen/gradle/wrapper/gradle-wrapper.properties", + "/dioxus/packages/cli/assets/android/gen/gradle.properties", + "/dioxus/packages/cli/assets/android/gen/gradlew", + "/dioxus/packages/cli/assets/android/gen/gradlew.bat", + "/dioxus/packages/cli/assets/android/gen/settings.gradle", + "/dioxus/packages/cli/assets/dioxus.toml", + "/dioxus/packages/cli/assets/ios/ios.plist.hbs", + "/dioxus/packages/cli/assets/macos/mac.plist.hbs", + "/dioxus/packages/cli/assets/web/index.html", + "/dioxus/packages/cli/assets/web/loading.html", + "/dioxus/packages/cli/assets/web/toast.html", + "/dioxus/packages/cli/build.rs", + "/dioxus/packages/cli/src/build/builder.rs", + "/dioxus/packages/cli/src/build/bundle.rs", + "/dioxus/packages/cli/src/build/mod.rs", + "/dioxus/packages/cli/src/build/prerender.rs", + "/dioxus/packages/cli/src/build/progress.rs", + "/dioxus/packages/cli/src/build/request.rs", + "/dioxus/packages/cli/src/build/templates.rs", + "/dioxus/packages/cli/src/build/verify.rs", + "/dioxus/packages/cli/src/build/web.rs", + "/dioxus/packages/cli/src/bundle_utils.rs", + "/dioxus/packages/cli/src/cli/autoformat.rs", + "/dioxus/packages/cli/src/cli/build.rs", + "/dioxus/packages/cli/src/cli/bundle.rs", + "/dioxus/packages/cli/src/cli/check.rs", + "/dioxus/packages/cli/src/cli/clean.rs", + "/dioxus/packages/cli/src/cli/config.rs", + "/dioxus/packages/cli/src/cli/create.rs", + "/dioxus/packages/cli/src/cli/init.rs", + "/dioxus/packages/cli/src/cli/link.rs", + "/dioxus/packages/cli/src/cli/mod.rs", + "/dioxus/packages/cli/src/cli/run.rs", + "/dioxus/packages/cli/src/cli/serve.rs", + "/dioxus/packages/cli/src/cli/target.rs", + "/dioxus/packages/cli/src/cli/translate.rs", + "/dioxus/packages/cli/src/cli/verbosity.rs", + "/dioxus/packages/cli/src/config/app.rs", + "/dioxus/packages/cli/src/config/bundle.rs", + "/dioxus/packages/cli/src/config/desktop.rs", + "/dioxus/packages/cli/src/config/dioxus_config.rs", + "/dioxus/packages/cli/src/config/serve.rs", + "/dioxus/packages/cli/src/config/web.rs", + "/dioxus/packages/cli/src/config.rs", + "/dioxus/packages/cli/src/dioxus_crate.rs", + "/dioxus/packages/cli/src/dx_build_info.rs", + "/dioxus/packages/cli/src/error.rs", + "/dioxus/packages/cli/src/fastfs.rs", + "/dioxus/packages/cli/src/filemap.rs", + "/dioxus/packages/cli/src/logging.rs", + "/dioxus/packages/cli/src/main.rs", + "/dioxus/packages/cli/src/metadata.rs", + "/dioxus/packages/cli/src/platform.rs", + "/dioxus/packages/cli/src/rustc.rs", + "/dioxus/packages/cli/src/serve/ansi_buffer.rs", + "/dioxus/packages/cli/src/serve/detect.rs", + "/dioxus/packages/cli/src/serve/handle.rs", + "/dioxus/packages/cli/src/serve/mod.rs", + "/dioxus/packages/cli/src/serve/output.rs", + "/dioxus/packages/cli/src/serve/proxy.rs", + "/dioxus/packages/cli/src/serve/runner.rs", + "/dioxus/packages/cli/src/serve/server.rs", + "/dioxus/packages/cli/src/serve/update.rs", + "/dioxus/packages/cli/src/serve/watcher.rs", + "/dioxus/packages/cli/src/settings.rs", + "/dioxus/packages/cli/src/wasm_bindgen.rs", + "/dioxus/packages/cli-config/src/lib.rs", + "/dioxus/packages/cli-opt/src/css.rs", + "/dioxus/packages/cli-opt/src/file.rs", + "/dioxus/packages/cli-opt/src/folder.rs", + "/dioxus/packages/cli-opt/src/image/jpg.rs", + "/dioxus/packages/cli-opt/src/image/mod.rs", + "/dioxus/packages/cli-opt/src/image/png.rs", + "/dioxus/packages/cli-opt/src/js.rs", + "/dioxus/packages/cli-opt/src/json.rs", + "/dioxus/packages/cli-opt/src/lib.rs", + "/dioxus/packages/config-macro/README.md", + "/dioxus/packages/config-macro/src/lib.rs", + "/dioxus/packages/const-serialize/README.md", + "/dioxus/packages/const-serialize/src/const_buffers.rs", + "/dioxus/packages/const-serialize/src/const_vec.rs", + "/dioxus/packages/const-serialize/src/lib.rs", + "/dioxus/packages/const-serialize-macro/src/lib.rs", + "/dioxus/packages/core/README.md", + "/dioxus/packages/core/docs/common_spawn_errors.md", + "/dioxus/packages/core/docs/reactivity.md", + "/dioxus/packages/core/src/any_props.rs", + "/dioxus/packages/core/src/arena.rs", + "/dioxus/packages/core/src/diff/component.rs", + "/dioxus/packages/core/src/diff/iterator.rs", + "/dioxus/packages/core/src/diff/mod.rs", + "/dioxus/packages/core/src/diff/node.rs", + "/dioxus/packages/core/src/effect.rs", + "/dioxus/packages/core/src/error_boundary.rs", + "/dioxus/packages/core/src/events.rs", + "/dioxus/packages/core/src/fragment.rs", + "/dioxus/packages/core/src/generational_box.rs", + "/dioxus/packages/core/src/global_context.rs", + "/dioxus/packages/core/src/hotreload_utils.rs", + "/dioxus/packages/core/src/launch.rs", + "/dioxus/packages/core/src/lib.rs", + "/dioxus/packages/core/src/mutations.rs", + "/dioxus/packages/core/src/nodes.rs", + "/dioxus/packages/core/src/properties.rs", + "/dioxus/packages/core/src/reactive_context.rs", + "/dioxus/packages/core/src/render_error.rs", + "/dioxus/packages/core/src/root_wrapper.rs", + "/dioxus/packages/core/src/runtime.rs", + "/dioxus/packages/core/src/scheduler.rs", + "/dioxus/packages/core/src/scope_arena.rs", + "/dioxus/packages/core/src/scope_context.rs", + "/dioxus/packages/core/src/scopes.rs", + "/dioxus/packages/core/src/suspense/component.rs", + "/dioxus/packages/core/src/suspense/mod.rs", + "/dioxus/packages/core/src/tasks.rs", + "/dioxus/packages/core/src/virtual_dom.rs", + "/dioxus/packages/core-macro/README.md", + "/dioxus/packages/core-macro/docs/component.md", + "/dioxus/packages/core-macro/docs/props.md", + "/dioxus/packages/core-macro/docs/rsx.md", + "/dioxus/packages/core-macro/src/component.rs", + "/dioxus/packages/core-macro/src/lib.rs", + "/dioxus/packages/core-macro/src/props/mod.rs", + "/dioxus/packages/core-macro/src/utils.rs", + "/dioxus/packages/core-types/src/bubbles.rs", + "/dioxus/packages/core-types/src/bundled.rs", + "/dioxus/packages/core-types/src/formatter.rs", + "/dioxus/packages/core-types/src/hr_context.rs", + "/dioxus/packages/core-types/src/lib.rs", + "/dioxus/packages/devtools/src/lib.rs", + "/dioxus/packages/devtools-types/src/lib.rs", + "/dioxus/packages/dioxus-lib/README.md", + "/dioxus/packages/dioxus-lib/src/lib.rs", + "/dioxus/packages/document/build.rs", + "/dioxus/packages/document/docs/eval.md", + "/dioxus/packages/document/docs/head.md", + "/dioxus/packages/document/src/document.rs", + "/dioxus/packages/document/src/elements/link.rs", + "/dioxus/packages/document/src/elements/meta.rs", + "/dioxus/packages/document/src/elements/mod.rs", + "/dioxus/packages/document/src/elements/script.rs", + "/dioxus/packages/document/src/elements/style.rs", + "/dioxus/packages/document/src/elements/stylesheet.rs", + "/dioxus/packages/document/src/elements/title.rs", + "/dioxus/packages/document/src/error.rs", + "/dioxus/packages/document/src/eval.rs", + "/dioxus/packages/document/src/js/head.js", + "/dioxus/packages/document/src/lib.rs", + "/dioxus/packages/document/./src/ts/eval.ts", + "/dioxus/packages/document/./src/ts/head.ts", + "/dioxus/packages/dx-wire-format/src/lib.rs", + "/dioxus/packages/fullstack/README.md", + "/dioxus/packages/fullstack/src/document/mod.rs", + "/dioxus/packages/fullstack/src/hooks/mod.rs", + "/dioxus/packages/fullstack/src/hooks/server_cached.rs", + "/dioxus/packages/fullstack/src/hooks/server_future.rs", + "/dioxus/packages/fullstack/src/html_storage/mod.rs", + "/dioxus/packages/fullstack/src/lib.rs", + "/dioxus/packages/generational-box/README.md", + "/dioxus/packages/generational-box/src/entry.rs", + "/dioxus/packages/generational-box/src/error.rs", + "/dioxus/packages/generational-box/src/lib.rs", + "/dioxus/packages/generational-box/src/references.rs", + "/dioxus/packages/generational-box/src/sync.rs", + "/dioxus/packages/generational-box/src/unsync.rs", + "/dioxus/packages/history/src/lib.rs", + "/dioxus/packages/history/src/memory.rs", + "/dioxus/packages/hooks/README.md", + "/dioxus/packages/hooks/docs/derived_state.md", + "/dioxus/packages/hooks/docs/moving_state_around.md", + "/dioxus/packages/hooks/docs/rules_of_hooks.md", + "/dioxus/packages/hooks/docs/side_effects.md", + "/dioxus/packages/hooks/docs/use_resource.md", + "/dioxus/packages/hooks/src/lib.rs", + "/dioxus/packages/hooks/src/use_callback.rs", + "/dioxus/packages/hooks/src/use_context.rs", + "/dioxus/packages/hooks/src/use_coroutine.rs", + "/dioxus/packages/hooks/src/use_effect.rs", + "/dioxus/packages/hooks/src/use_future.rs", + "/dioxus/packages/hooks/src/use_hook_did_run.rs", + "/dioxus/packages/hooks/src/use_memo.rs", + "/dioxus/packages/hooks/src/use_on_destroy.rs", + "/dioxus/packages/hooks/src/use_reactive.rs", + "/dioxus/packages/hooks/src/use_resource.rs", + "/dioxus/packages/hooks/src/use_root_context.rs", + "/dioxus/packages/hooks/src/use_set_compare.rs", + "/dioxus/packages/hooks/src/use_signal.rs", + "/dioxus/packages/html/README.md", + "/dioxus/packages/html/docs/common_event_handler_errors.md", + "/dioxus/packages/html/docs/event_handlers.md", + "/dioxus/packages/html/src/attribute_groups.rs", + "/dioxus/packages/html/src/elements.rs", + "/dioxus/packages/html/src/events/animation.rs", + "/dioxus/packages/html/src/events/clipboard.rs", + "/dioxus/packages/html/src/events/composition.rs", + "/dioxus/packages/html/src/events/drag.rs", + "/dioxus/packages/html/src/events/focus.rs", + "/dioxus/packages/html/src/events/form.rs", + "/dioxus/packages/html/src/events/image.rs", + "/dioxus/packages/html/src/events/keyboard.rs", + "/dioxus/packages/html/src/events/media.rs", + "/dioxus/packages/html/src/events/mod.rs", + "/dioxus/packages/html/src/events/mounted.rs", + "/dioxus/packages/html/src/events/mouse.rs", + "/dioxus/packages/html/src/events/pointer.rs", + "/dioxus/packages/html/src/events/resize.rs", + "/dioxus/packages/html/src/events/scroll.rs", + "/dioxus/packages/html/src/events/selection.rs", + "/dioxus/packages/html/src/events/toggle.rs", + "/dioxus/packages/html/src/events/touch.rs", + "/dioxus/packages/html/src/events/transition.rs", + "/dioxus/packages/html/src/events/visible.rs", + "/dioxus/packages/html/src/events/wheel.rs", + "/dioxus/packages/html/src/file_data.rs", + "/dioxus/packages/html/src/geometry.rs", + "/dioxus/packages/html/src/input_data.rs", + "/dioxus/packages/html/src/lib.rs", + "/dioxus/packages/html/src/point_interaction.rs", + "/dioxus/packages/html/src/render_template.rs", + "/dioxus/packages/html-internal-macro/src/lib.rs", + "/dioxus/packages/lazy-js-bundle/src/lib.rs", + "/dioxus/packages/manganis/manganis/README.md", + "/dioxus/packages/manganis/manganis/src/hash.rs", + "/dioxus/packages/manganis/manganis/src/lib.rs", + "/dioxus/packages/manganis/manganis/src/macro_helpers.rs", + "/dioxus/packages/manganis/manganis-core/src/asset.rs", + "/dioxus/packages/manganis/manganis-core/src/css.rs", + "/dioxus/packages/manganis/manganis-core/src/folder.rs", + "/dioxus/packages/manganis/manganis-core/src/hash.rs", + "/dioxus/packages/manganis/manganis-core/src/images.rs", + "/dioxus/packages/manganis/manganis-core/src/js.rs", + "/dioxus/packages/manganis/manganis-core/src/lib.rs", + "/dioxus/packages/manganis/manganis-core/src/linker.rs", + "/dioxus/packages/manganis/manganis-core/src/options.rs", + "/dioxus/packages/manganis/manganis-macro/README.md", + "/dioxus/packages/manganis/manganis-macro/src/asset.rs", + "/dioxus/packages/manganis/manganis-macro/src/lib.rs", + "/dioxus/packages/manganis/manganis-macro/src/linker.rs", + "/dioxus/packages/rsx/src/assign_dyn_ids.rs", + "/dioxus/packages/rsx/src/attribute.rs", + "/dioxus/packages/rsx/src/component.rs", + "/dioxus/packages/rsx/src/diagnostics.rs", + "/dioxus/packages/rsx/src/element.rs", + "/dioxus/packages/rsx/src/expr_node.rs", + "/dioxus/packages/rsx/src/forloop.rs", + "/dioxus/packages/rsx/src/ifchain.rs", + "/dioxus/packages/rsx/src/ifmt.rs", + "/dioxus/packages/rsx/src/lib.rs", + "/dioxus/packages/rsx/src/literal.rs", + "/dioxus/packages/rsx/src/location.rs", + "/dioxus/packages/rsx/src/node.rs", + "/dioxus/packages/rsx/src/partial_closure.rs", + "/dioxus/packages/rsx/src/raw_expr.rs", + "/dioxus/packages/rsx/src/rsx_block.rs", + "/dioxus/packages/rsx/src/rsx_call.rs", + "/dioxus/packages/rsx/src/template_body.rs", + "/dioxus/packages/rsx/src/text_node.rs", + "/dioxus/packages/rsx/src/util.rs", + "/dioxus/packages/rsx-hotreload/src/collect.rs", + "/dioxus/packages/rsx-hotreload/src/diff.rs", + "/dioxus/packages/rsx-hotreload/src/extensions.rs", + "/dioxus/packages/rsx-hotreload/src/last_build_state.rs", + "/dioxus/packages/rsx-hotreload/src/lib.rs", + "/dioxus/packages/rsx-rosetta/README.md", + "/dioxus/packages/rsx-rosetta/src/lib.rs", + "/dioxus/packages/server-macro/src/lib.rs", + "/dioxus/packages/signals/README.md", + "/dioxus/packages/signals/docs/hoist/error.rs", + "/dioxus/packages/signals/docs/hoist/fixed_list.rs", + "/dioxus/packages/signals/docs/memo.md", + "/dioxus/packages/signals/docs/signals.md", + "/dioxus/packages/signals/src/copy_value.rs", + "/dioxus/packages/signals/src/global/memo.rs", + "/dioxus/packages/signals/src/global/mod.rs", + "/dioxus/packages/signals/src/global/signal.rs", + "/dioxus/packages/signals/src/impls.rs", + "/dioxus/packages/signals/src/lib.rs", + "/dioxus/packages/signals/src/map.rs", + "/dioxus/packages/signals/src/memo.rs", + "/dioxus/packages/signals/src/props.rs", + "/dioxus/packages/signals/src/read.rs", + "/dioxus/packages/signals/src/read_only_signal.rs", + "/dioxus/packages/signals/src/set_compare.rs", + "/dioxus/packages/signals/src/signal.rs", + "/dioxus/packages/signals/src/warnings.rs", + "/dioxus/packages/signals/src/write.rs", + "/dioxus/target/debug/build/dioxus-cli-90993e55e02b7cee/out/built.rs", + ]; + assert_eq!( + answer.iter().map(PathBuf::from).collect::>(), + info.files + ); + } +} diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index 48b1ac67da..2a87c199de 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -327,7 +327,14 @@ impl App { match msg { DevserverMsg::HotReload(hr_msg) => { for webview in self.webviews.values_mut() { - dioxus_devtools::apply_changes(&webview.dom, &hr_msg); + { + // This is a place where wry says it's threadsafe but it's actually not. + // If we're patching the app, we want to make sure it's not going to progress in the interim. + let lock = crate::android_sync_lock::android_runtime_lock(); + dioxus_devtools::apply_changes(&webview.dom, &hr_msg); + drop(lock); + } + webview.poll_vdom(); } @@ -346,6 +353,7 @@ impl App { DevserverMsg::Shutdown => { self.control_flow = ControlFlow::Exit; } + _ => {} } } diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 883c2b94c3..9a89b4f055 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -70,12 +70,12 @@ impl LaunchConfig for Config {} pub(crate) type WryProtocol = ( String, - Box>) -> HttpResponse> + 'static>, + Box>) -> HttpResponse> + 'static>, ); pub(crate) type AsyncWryProtocol = ( String, - Box>, RequestAsyncResponder) + 'static>, + Box>, RequestAsyncResponder) + 'static>, ); impl Config { @@ -188,7 +188,7 @@ impl Config { /// Set a custom protocol pub fn with_custom_protocol(mut self, name: impl ToString, handler: F) -> Self where - F: Fn(&str, HttpRequest>) -> HttpResponse> + 'static, + F: Fn(HttpRequest>) -> HttpResponse> + 'static, { self.protocols.push((name.to_string(), Box::new(handler))); self @@ -204,7 +204,7 @@ impl Config { /// # /// # fn main() { /// let cfg = Config::new() - /// .with_asynchronous_custom_protocol("asset", |_webview_id, request, responder| { + /// .with_asynchronous_custom_protocol("asset", |request, responder| { /// tokio::spawn(async move { /// responder.respond( /// HTTPResponse::builder() @@ -222,7 +222,7 @@ impl Config { /// See [`wry`](wry::WebViewBuilder::with_asynchronous_custom_protocol) for more details on implementation pub fn with_asynchronous_custom_protocol(mut self, name: impl ToString, handler: F) -> Self where - F: Fn(&str, HttpRequest>, RequestAsyncResponder) + 'static, + F: Fn(HttpRequest>, RequestAsyncResponder) + 'static, { self.asynchronous_protocols .push((name.to_string(), Box::new(handler))); diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index ca025db7aa..b9d8ecad1d 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -235,7 +235,7 @@ impl WebviewInstance { asset_handlers, edits ]; - move |_id: wry::WebViewId, request, responder: RequestAsyncResponder| { + move |request, responder: RequestAsyncResponder| { protocol::desktop_handler( request, asset_handlers.clone(), @@ -303,7 +303,33 @@ impl WebviewInstance { } }; - let mut wv_builder = WebViewBuilder::with_web_context(&mut web_context) + #[cfg(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + ))] + let mut webview = if cfg.as_child_window { + WebViewBuilder::new_as_child(&window) + } else { + WebViewBuilder::new(&window) + }; + + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android" + )))] + let mut webview = { + use tao::platform::unix::WindowExtUnix; + use wry::WebViewBuilderExtUnix; + let vbox = window.default_vbox().unwrap(); + WebViewBuilder::new_gtk(vbox) + }; + + webview = webview + .with_web_context(&mut web_context) .with_bounds(wry::Rect { position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)), size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new( @@ -332,23 +358,23 @@ impl WebviewInstance { #[cfg(target_os = "windows")] { use wry::WebViewBuilderExtWindows; - wv_builder = wv_builder.with_browser_accelerator_keys(false); + webview = webview.with_browser_accelerator_keys(false); } if !cfg.disable_file_drop_handler { - wv_builder = wv_builder.with_drag_drop_handler(file_drop_handler); + webview = webview.with_drag_drop_handler(file_drop_handler); } if let Some(color) = cfg.background_color { - wv_builder = wv_builder.with_background_color(color); + webview = webview.with_background_color(color); } for (name, handler) in cfg.protocols.drain(..) { - wv_builder = wv_builder.with_custom_protocol(name, handler); + webview = webview.with_custom_protocol(name, handler); } for (name, handler) in cfg.asynchronous_protocols.drain(..) { - wv_builder = wv_builder.with_asynchronous_custom_protocol(name, handler); + webview = webview.with_asynchronous_custom_protocol(name, handler); } const INITIALIZATION_SCRIPT: &str = r#" @@ -365,38 +391,12 @@ impl WebviewInstance { if cfg.disable_context_menu { // in release mode, we don't want to show the dev tool or reload menus - wv_builder = wv_builder.with_initialization_script(INITIALIZATION_SCRIPT) + webview = webview.with_initialization_script(INITIALIZATION_SCRIPT) } else { // in debug, we are okay with the reload menu showing and dev tool - wv_builder = wv_builder.with_devtools(true); + webview = webview.with_devtools(true); } - #[cfg(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - ))] - let webview = if cfg.as_child_window { - wv_builder.build_as_child(&window) - } else { - wv_builder.build(&window) - } - .unwrap(); - - #[cfg(not(any( - target_os = "windows", - target_os = "macos", - target_os = "ios", - target_os = "android" - )))] - let webview = { - use tao::platform::unix::WindowExtUnix; - use wry::WebViewBuilderExtUnix; - let vbox = window.default_vbox().unwrap(); - wv_builder.build_gtk(vbox).unwrap() - }; - let menu = if cfg!(not(any(target_os = "android", target_os = "ios"))) { let menu_option = cfg.menu.into(); if let Some(menu) = &menu_option { @@ -407,6 +407,7 @@ impl WebviewInstance { None }; + let webview = webview.build().unwrap(); let desktop_context = Rc::from(DesktopService::new( webview, window, diff --git a/packages/devtools-types/Cargo.toml b/packages/devtools-types/Cargo.toml index 4542962650..78cb019261 100644 --- a/packages/devtools-types/Cargo.toml +++ b/packages/devtools-types/Cargo.toml @@ -12,3 +12,4 @@ keywords = ["dom", "ui", "gui", "react", ] [dependencies] serde = { workspace = true, features = ["derive"] } dioxus-core = { workspace = true, features = ["serialize"] } +subsecond-types = { workspace = true } diff --git a/packages/devtools-types/src/lib.rs b/packages/devtools-types/src/lib.rs index 0450e02361..bc50f6d464 100644 --- a/packages/devtools-types/src/lib.rs +++ b/packages/devtools-types/src/lib.rs @@ -1,14 +1,19 @@ use dioxus_core::internal::HotReloadTemplateWithLocation; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use subsecond_types::JumpTable; /// A message the hot reloading server sends to the client +#[non_exhaustive] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum DevserverMsg { /// Attempt a hotreload /// This includes all the templates/literals/assets/binary patches that have changed in one shot HotReload(HotReloadMsg), + /// Starting a hotpatch + HotPatchStart, + /// The devserver is starting a full rebuild. FullReloadStart, @@ -25,6 +30,7 @@ pub enum DevserverMsg { /// A message the client sends from the frontend to the devserver /// /// This is used to communicate with the devserver +#[non_exhaustive] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum ClientMsg { Log { @@ -37,11 +43,13 @@ pub enum ClientMsg { pub struct HotReloadMsg { pub templates: Vec, pub assets: Vec, - pub unknown_files: Vec, + pub ms_elapsed: u64, + pub jump_table: Option, + pub for_build_id: Option, } impl HotReloadMsg { pub fn is_empty(&self) -> bool { - self.templates.is_empty() && self.assets.is_empty() && self.unknown_files.is_empty() + self.templates.is_empty() && self.assets.is_empty() && self.jump_table.is_none() } } diff --git a/packages/devtools/Cargo.toml b/packages/devtools/Cargo.toml index cc68aca44a..79f7011799 100644 --- a/packages/devtools/Cargo.toml +++ b/packages/devtools/Cargo.toml @@ -13,8 +13,11 @@ keywords = ["dom", "ui", "gui", "react", "hot-reloading"] dioxus-signals = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-devtools-types = { workspace = true } +dioxus-cli-config = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +subsecond = { workspace = true } +thiserror = { workspace = true } # hot reloading serve tracing = { workspace = true } diff --git a/packages/devtools/src/lib.rs b/packages/devtools/src/lib.rs index 58fc5f4f96..d9f71bfb31 100644 --- a/packages/devtools/src/lib.rs +++ b/packages/devtools/src/lib.rs @@ -1,14 +1,24 @@ use dioxus_core::{ScopeId, VirtualDom}; -pub use dioxus_devtools_types::*; use dioxus_signals::{GlobalKey, Writable}; +pub use dioxus_devtools_types::*; +pub use subsecond; +use subsecond::PatchError; + /// Applies template and literal changes to the VirtualDom /// /// Assets need to be handled by the renderer. pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) { + try_apply_changes(dom, msg).unwrap() +} + +/// Applies template and literal changes to the VirtualDom, but doesn't panic if patching fails. +/// +/// Assets need to be handled by the renderer. +pub fn try_apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) -> Result<(), PatchError> { dom.runtime().on_scope(ScopeId::ROOT, || { + // 1. Update signals... let ctx = dioxus_signals::get_global_context(); - for template in &msg.templates { let value = template.template.clone(); let key = GlobalKey::File { @@ -21,7 +31,17 @@ pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) { signal.set(Some(value)); } } - }); + + // 2. Attempt to hotpatch + if let Some(jump_table) = msg.jump_table.as_ref().cloned() { + if msg.for_build_id == Some(dioxus_cli_config::build_id()) { + unsafe { subsecond::apply_patch(jump_table) }?; + dioxus_core::prelude::force_all_dirty(); + } + } + + Ok(()) + }) } /// Connect to the devserver and handle its messages with a callback. @@ -30,7 +50,13 @@ pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) { #[cfg(not(target_arch = "wasm32"))] pub fn connect(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) { std::thread::spawn(move || { - let (mut websocket, _req) = match tungstenite::connect(endpoint.clone()) { + let uri = format!( + "{endpoint}?aslr_reference={}&build_id={}", + subsecond::__aslr_reference(), + dioxus_cli_config::build_id() + ); + + let (mut websocket, _req) = match tungstenite::connect(uri) { Ok((websocket, req)) => (websocket, req), Err(_) => return, }; diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 5f44fedc3d..daa2601509 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -26,17 +26,18 @@ dioxus-mobile = { workspace = true, optional = true } dioxus-desktop = { workspace = true, default-features = true, optional = true } dioxus-fullstack = { workspace = true, default-features = true, optional = true } dioxus-liveview = { workspace = true, optional = true } +dioxus-server = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true } dioxus-native = { workspace = true, optional = true } +dioxus_server_macro = { workspace = true, optional = true } manganis = { workspace = true, features = ["dioxus"], optional = true } dioxus-logger = { workspace = true, optional = true } warnings = { workspace = true, optional = true } wasm-split = { workspace = true, optional = true } +subsecond = { workspace = true } serde = { workspace = true, optional = true } dioxus-cli-config = { workspace = true, optional = true } - -[target.'cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))'.dependencies] dioxus-devtools = { workspace = true, optional = true } [features] @@ -89,8 +90,16 @@ web = [ ] ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"] liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"] -server = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "ssr", "dioxus-liveview?/axum"] native = ["dep:dioxus-native"] # todo(jon): decompose the desktop crate such that "webview" is the default and native is opt-in +server = [ + "dep:dioxus-server", + "dep:dioxus_server_macro", + "dioxus_server_macro/server", + "dioxus_server_macro/axum", + "ssr", + "dioxus-liveview?/axum", + "dioxus-fullstack?/server", +] # This feature just disables the no-renderer-enabled warning third-party-renderer = [] diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index e1466b4b61..087be1ca6b 100644 --- a/packages/dioxus/src/launch.rs +++ b/packages/dioxus/src/launch.rs @@ -96,11 +96,7 @@ impl LaunchBuilder { ) )] pub fn new() -> LaunchBuilder { - let platform = if cfg!(feature = "liveview") { - KnownPlatform::Liveview - } else if cfg!(feature = "server") { - KnownPlatform::Server - } else if cfg!(feature = "native") { + let platform = if cfg!(feature = "native") { KnownPlatform::Native } else if cfg!(feature = "desktop") { KnownPlatform::Desktop @@ -108,6 +104,10 @@ impl LaunchBuilder { KnownPlatform::Mobile } else if cfg!(feature = "web") { KnownPlatform::Web + } else if cfg!(feature = "server") { + KnownPlatform::Server + } else if cfg!(feature = "liveview") { + KnownPlatform::Liveview } else { panic!("No platform feature enabled. Please enable one of the following features: liveview, desktop, mobile, web, tui, fullstack to use the launch API.") }; @@ -284,7 +284,6 @@ impl LaunchBuilder { } /// Launch your application. - /// #[allow(clippy::diverging_sub_expression)] pub fn launch(self, app: fn() -> Element) { let Self { @@ -343,7 +342,7 @@ impl LaunchBuilder { #[cfg(feature = "server")] if matches!(platform, KnownPlatform::Server) { - return dioxus_fullstack::server::launch::launch(app, contexts, configs); + return dioxus_server::launch(app, contexts, configs); } #[cfg(feature = "web")] @@ -364,8 +363,8 @@ impl LaunchBuilder { #[cfg(feature = "document")] { - use dioxus_fullstack::document; - let document = std::rc::Rc::new(document::web::FullstackWebDocument) + use dioxus_fullstack::FullstackWebDocument; + let document = std::rc::Rc::new(FullstackWebDocument) as std::rc::Rc; vdom.provide_root_context(document); } diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 5cd36c4079..ff47951695 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -78,6 +78,8 @@ pub use dioxus_cli_config as cli_config; #[cfg_attr(docsrs, doc(cfg(feature = "wasm-split")))] pub use wasm_split; +pub use subsecond; + pub mod prelude { #[cfg(feature = "document")] #[cfg_attr(docsrs, doc(cfg(feature = "document")))] @@ -118,10 +120,7 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(feature = "html")))] pub use dioxus_elements::{global_attributes, prelude::*, svg_attributes}; - #[cfg(all( - not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")), - feature = "devtools" - ))] + #[cfg(feature = "devtools")] #[cfg_attr(docsrs, doc(cfg(feature = "devtools")))] pub use dioxus_devtools; diff --git a/packages/document/docs/head.md b/packages/document/docs/head.md index 0488d3f6bb..4be75e3844 100644 --- a/packages/document/docs/head.md +++ b/packages/document/docs/head.md @@ -38,7 +38,7 @@ fn RedirectToDioxusHomepageWithoutJS() -> Element { Since Dioxus does not guarantee head element ordering, one can use es6 imports as a more predictable way to handle dependencies. -```rust +```rust, ignore # use dioxus::prelude::*; static HIGHLIGHT: Asset = asset!("/assets/highlight/es/highlight.min.js"); static RUST: Asset = asset!("/assets/highlight/es/languages/rust.min.js"); diff --git a/packages/dx-wire-format/src/lib.rs b/packages/dx-wire-format/src/lib.rs index 99d507b430..43da1a1a6f 100644 --- a/packages/dx-wire-format/src/lib.rs +++ b/packages/dx-wire-format/src/lib.rs @@ -22,13 +22,29 @@ pub use cargo_metadata; #[non_exhaustive] #[derive(Serialize, Deserialize, Clone)] pub enum StructuredOutput { - BuildFinished { path: PathBuf }, - BuildUpdate { stage: BuildStage }, - CargoOutput { message: CompilerMessage }, - BundleOutput { bundles: Vec }, - HtmlTranslate { html: String }, + BuildsFinished { + client: PathBuf, + server: Option, + }, + BuildFinished { + path: PathBuf, + }, + BuildUpdate { + stage: BuildStage, + }, + CargoOutput { + message: CompilerMessage, + }, + BundleOutput { + bundles: Vec, + }, + HtmlTranslate { + html: String, + }, Success, - Error { message: String }, + Error { + message: String, + }, } impl std::fmt::Debug for StructuredOutput { @@ -46,11 +62,10 @@ pub enum BuildStage { Initializing, Starting { crate_count: usize, - is_server: bool, + patch: bool, }, InstallingTooling, Compiling { - is_server: bool, current: usize, total: usize, krate: String, @@ -58,7 +73,9 @@ pub enum BuildStage { RunningBindgen, SplittingBundle, OptimizingWasm, - PrerenderingRoutes, + Linking, + Hotpatching, + ExtractingAssets, CopyingAssets { current: usize, total: usize, diff --git a/packages/fullstack-hooks/Cargo.toml b/packages/fullstack-hooks/Cargo.toml index bd0219fa56..21b3dadc9b 100644 --- a/packages/fullstack-hooks/Cargo.toml +++ b/packages/fullstack-hooks/Cargo.toml @@ -20,6 +20,8 @@ serde = { workspace = true } [dev-dependencies] dioxus-fullstack = { workspace = true } +dioxus-lib = { workspace = true } +dioxus = { workspace = true } [features] web = [] diff --git a/packages/fullstack-hooks/src/streaming.rs b/packages/fullstack-hooks/src/streaming.rs index 5f3e3c011b..5958d9cd97 100644 --- a/packages/fullstack-hooks/src/streaming.rs +++ b/packages/fullstack-hooks/src/streaming.rs @@ -89,7 +89,7 @@ pub fn commit_initial_chunk() { /// // up in the initial chunk. /// use_hook(|| { /// if current_status() == StreamingStatus::InitialChunkCommitted { -/// log::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled."); +/// dioxus::logger::tracing::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled."); /// } /// }); /// diff --git a/packages/fullstack-protocol/src/lib.rs b/packages/fullstack-protocol/src/lib.rs index eb9e0f332f..204aceb410 100644 --- a/packages/fullstack-protocol/src/lib.rs +++ b/packages/fullstack-protocol/src/lib.rs @@ -399,3 +399,8 @@ impl std::fmt::Display for TakeDataError { } impl std::error::Error for TakeDataError {} + +/// Create a new entry in the serialize context for the head element hydration +pub fn head_element_hydration_entry() -> SerializeContextEntry { + serialize_context().create_entry() +} diff --git a/packages/fullstack/Cargo.toml b/packages/fullstack/Cargo.toml index 4a429569e1..e98c9c4009 100644 --- a/packages/fullstack/Cargo.toml +++ b/packages/fullstack/Cargo.toml @@ -12,7 +12,7 @@ resolver = "2" [dependencies] # server functions -server_fn = { version = "0.8.0-rc1", features = ["browser"], default-features = false } +server_fn = { workspace = true, default-features = false } dioxus_server_macro = { workspace = true } # axum @@ -23,13 +23,11 @@ dioxus-lib = { workspace = true } generational-box = { workspace = true } # Dioxus + SSR -dioxus-ssr = { workspace = true, optional = true } +dioxus-server = { workspace = true, optional = true} dioxus-isrg = { workspace = true, optional = true } dioxus-router = { workspace = true, features = ["streaming"], optional = true } dioxus-fullstack-hooks = { workspace = true } dioxus-fullstack-protocol = { workspace = true } -hyper = { workspace = true, optional = true } -http = { workspace = true, optional = true } # Web Integration dioxus-web = { workspace = true, features = ["hydrate"], default-features = false, optional = true } @@ -77,12 +75,6 @@ dioxus-devtools = { workspace = true, optional = true } aws-lc-rs = { version = "1.12.5", optional = true } dioxus-history = { workspace = true } -[target.'cfg(target_arch = "wasm32")'.dependencies] -tokio = { workspace = true, features = ["rt", "sync"], optional = true } - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], optional = true } - [dev-dependencies] dioxus = { workspace = true, features = ["fullstack"] } @@ -92,43 +84,24 @@ devtools = ["dioxus-web?/devtools", "dep:dioxus-devtools"] mounted = ["dioxus-web?/mounted"] file_engine = ["dioxus-web?/file_engine"] document = ["dioxus-web?/document"] -web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web"] -desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"] -mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"] +web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web", "server_fn/browser"] +desktop = ["server_fn/reqwest", "dioxus_server_macro/reqwest"] +mobile = ["server_fn/reqwest", "dioxus_server_macro/reqwest"] default-tls = ["server_fn/default-tls"] rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"] -axum_core = [ - "dep:axum", - "server_fn/axum-no-default", - "dioxus_server_macro/axum", +server = [ + "server-core" , "default-tls", - "server", -] -axum = [ - "dep:tower-http", "server_fn/axum", - "axum_core", ] -server = [ +server-core = [ + "server_fn/axum-no-default", + "dioxus_server_macro/axum", + "server_fn/reqwest", "server_fn/ssr", - "dioxus_server_macro/server", + "dioxus_server_macro/reqwest", "dioxus-fullstack-hooks/server", - "dep:tokio", - "dep:tokio-util", - "dep:tokio-stream", - "dep:dioxus-ssr", - "dep:dioxus-isrg", - "dep:dioxus-router", - "dep:tower", - "dep:hyper", - "dep:http", - "dep:tower-layer", - "dep:tracing-futures", - "dep:pin-project", - "dep:thiserror", - "dep:dioxus-cli-config", - "dep:async-trait", - "dep:parking_lot", + "dep:dioxus-server", "dioxus-interpreter-js", ] aws-lc-rs = ["dep:aws-lc-rs"] diff --git a/packages/fullstack/README.md b/packages/fullstack/README.md index 21dfb0e1f2..aaadd1a4b2 100644 --- a/packages/fullstack/README.md +++ b/packages/fullstack/README.md @@ -72,7 +72,7 @@ First, make sure your `axum` dependency is optional and enabled by the server fe ```toml [dependencies] dioxus = { version = "*", features = ["fullstack"] } -axum = { version = "0.7.0", optional = true } +axum = { version = "0.8.0", optional = true } tokio = { version = "1.0", features = ["full"], optional = true } dioxus-cli-config = { version = "*", optional = true } diff --git a/packages/fullstack/src/document/mod.rs b/packages/fullstack/src/document/mod.rs deleted file mode 100644 index 153cb0d85d..0000000000 --- a/packages/fullstack/src/document/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! This module contains the document providers for the fullstack platform. - -#[cfg(feature = "server")] -pub mod server; -use dioxus_fullstack_protocol::SerializeContextEntry; -#[cfg(feature = "server")] -pub use server::ServerDocument; -#[cfg(all(feature = "web", feature = "document"))] -pub mod web; - -#[allow(unused)] -pub(crate) fn head_element_hydration_entry() -> SerializeContextEntry { - dioxus_fullstack_protocol::serialize_context().create_entry() -} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 9284d8a2e6..2cd07f8eb1 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -6,63 +6,22 @@ pub use once_cell; -#[cfg(feature = "axum")] -#[cfg_attr(docsrs, doc(cfg(feature = "axum")))] -pub mod server; +#[cfg(all(feature = "web", feature = "document"))] +mod web; -#[cfg(feature = "axum_core")] -#[cfg_attr(docsrs, doc(cfg(feature = "axum_core")))] -pub mod axum_core; +#[cfg(all(feature = "web", feature = "document"))] +pub use web::FullstackWebDocument; -pub mod document; #[cfg(feature = "server")] -mod render; -#[cfg(feature = "server")] -mod streaming; - -#[cfg(feature = "server")] -mod serve_config; - -#[cfg(feature = "server")] -pub use serve_config::*; - -#[cfg(feature = "server")] -mod server_context; +pub use dioxus_server::*; /// A prelude of commonly used items in dioxus-fullstack. pub mod prelude { pub use dioxus_fullstack_hooks::*; - #[cfg(feature = "axum")] - #[cfg_attr(docsrs, doc(cfg(feature = "axum")))] - pub use crate::server::*; - - #[cfg(feature = "axum_core")] - pub use crate::axum_core::*; - - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use crate::render::{FullstackHTMLTemplate, SSRState}; - - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use crate::serve_config::{ServeConfig, ServeConfigBuilder}; - - #[cfg(feature = "axum")] - #[cfg_attr(docsrs, doc(cfg(feature = "axum")))] - pub use crate::server_context::Axum; - - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use crate::server_context::{ - extract, server_context, with_server_context, DioxusServerContext, FromContext, - FromServerContext, ProvideServerContext, - }; - - #[cfg(feature = "server")] - #[cfg_attr(docsrs, doc(cfg(feature = "server")))] - pub use dioxus_isrg::{IncrementalRenderer, IncrementalRendererConfig}; - pub use dioxus_server_macro::*; pub use server_fn::{self, ServerFn as _, ServerFnError}; + + #[cfg(feature = "server")] + pub use dioxus_server::prelude::*; } diff --git a/packages/fullstack/src/server/launch.rs b/packages/fullstack/src/server/launch.rs deleted file mode 100644 index e73561d037..0000000000 --- a/packages/fullstack/src/server/launch.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! A launch function that creates an axum router for the LaunchBuilder - -use std::any::Any; - -use axum::{ - body::Body, - extract::{Request, State}, - response::IntoResponse, -}; -use dioxus_cli_config::base_path; -use dioxus_lib::prelude::*; - -use crate::server::{render_handler, RenderHandleState, SSRState}; - -/// Launch a fullstack app with the given root component, contexts, and config. -#[allow(unused)] -pub fn launch( - root: fn() -> Element, - contexts: Vec Box + Send + Sync>>, - platform_config: Vec>, -) -> ! { - use crate::{ServeConfig, ServeConfigBuilder}; - - #[cfg(not(target_arch = "wasm32"))] - tokio::runtime::Runtime::new() - .unwrap() - .block_on(async move { - let platform_config = platform_config - .into_iter() - .find_map(|cfg| { - cfg.downcast::() - .map(|cfg| Result::Ok(*cfg)) - .or_else(|cfg| { - cfg.downcast::() - .map(|builder| builder.build()) - }) - .ok() - }) - .unwrap_or_else(ServeConfig::new); - - // Extend the config's context providers with the context providers from the launch builder - let platform_config = platform_config.map(|mut cfg| { - let mut contexts = contexts; - let cfg_context_providers = cfg.context_providers.clone(); - for i in 0..cfg_context_providers.len() { - contexts.push(Box::new({ - let cfg_context_providers = cfg_context_providers.clone(); - move || (cfg_context_providers[i])() - })); - } - cfg.context_providers = std::sync::Arc::new(contexts); - cfg - }); - - // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address - // and we use the generated address the CLI gives us - let address = dioxus_cli_config::fullstack_address_or_localhost(); - - use crate::server::DioxusRouterExt; - - struct TryIntoResult(Result); - - impl TryInto for TryIntoResult { - type Error = crate::UnableToLoadIndex; - - fn try_into(self) -> Result { - self.0 - } - } - - let mut base_path = base_path(); - let config = platform_config.as_ref().ok().cloned(); - let dioxus_router = - axum::Router::new().serve_dioxus_application(TryIntoResult(platform_config), root); - let mut router; - match base_path.as_deref() { - Some(base_path) => { - let base_path = base_path.trim_matches('/'); - // If there is a base path, nest the router under it and serve the root route manually - // Nesting a route in axum only serves /base_path or /base_path/ not both - router = axum::Router::new().nest(&format!("/{base_path}/"), dioxus_router); - async fn root_render_handler( - state: State, - mut request: Request, - ) -> impl IntoResponse { - // The root of the base path always looks like the root from dioxus fullstack - *request.uri_mut() = "/".parse().unwrap(); - render_handler(state, request).await - } - if let Some(cfg) = config { - let ssr_state = SSRState::new(&cfg); - router = router.route( - &format!("/{base_path}"), - axum::routing::method_routing::get(root_render_handler).with_state( - RenderHandleState::new(cfg, root).with_ssr_state(ssr_state), - ), - ) - } - } - None => router = dioxus_router, - } - - let router = router.into_make_service(); - let listener = tokio::net::TcpListener::bind(address).await.unwrap(); - - axum::serve(listener, router).await.unwrap(); - }); - - unreachable!("Launching a fullstack app should never return") -} diff --git a/packages/fullstack/src/server/mod.rs b/packages/fullstack/src/server/mod.rs deleted file mode 100644 index cfe8bb00de..0000000000 --- a/packages/fullstack/src/server/mod.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework. -//! -//! # Example -//! ```rust, no_run -//! #![allow(non_snake_case)] -//! use dioxus::prelude::*; -//! -//! fn main() { -//! #[cfg(feature = "web")] -//! // Hydrate the application on the client -//! dioxus::launch(app); -//! #[cfg(feature = "server")] -//! { -//! tokio::runtime::Runtime::new() -//! .unwrap() -//! .block_on(async move { -//! // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address -//! // and we use the generated address the CLI gives us -//! let address = dioxus::cli_config::fullstack_address_or_localhost(); -//! let listener = tokio::net::TcpListener::bind(address) -//! .await -//! .unwrap(); -//! axum::serve( -//! listener, -//! axum::Router::new() -//! // Server side render the application, serve static assets, and register server functions -//! .serve_dioxus_application(ServeConfigBuilder::default(), app) -//! .into_make_service(), -//! ) -//! .await -//! .unwrap(); -//! }); -//! } -//! } -//! -//! fn app() -> Element { -//! let mut text = use_signal(|| "...".to_string()); -//! -//! rsx! { -//! button { -//! onclick: move |_| async move { -//! if let Ok(data) = get_server_data().await { -//! text.set(data); -//! } -//! }, -//! "Run a server function" -//! } -//! "Server said: {text}" -//! } -//! } -//! -//! #[server(GetServerData)] -//! async fn get_server_data() -> Result { -//! Ok("Hello from the server!".to_string()) -//! } -//! ``` - -pub mod launch; - -use axum::routing::*; -use dioxus_lib::prelude::Element; - -use crate::prelude::*; - -/// A extension trait with utilities for integrating Dioxus with your Axum router. -pub trait DioxusRouterExt: DioxusRouterFnExt { - /// Serves the static WASM for your Dioxus application (except the generated index.html). - /// - /// # Example - /// ```rust, no_run - /// # #![allow(non_snake_case)] - /// # use dioxus_lib::prelude::*; - /// # use dioxus_fullstack::prelude::*; - /// #[tokio::main] - /// async fn main() { - /// let addr = dioxus::cli_config::fullstack_address_or_localhost(); - /// let router = axum::Router::new() - /// // Server side render the application, serve static assets, and register server functions - /// .serve_static_assets() - /// // Server render the application - /// // ... - /// .into_make_service(); - /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - /// axum::serve(listener, router).await.unwrap(); - /// } - /// ``` - fn serve_static_assets(self) -> Self - where - Self: Sized; - - /// Serves the Dioxus application. This will serve a complete server side rendered application. - /// This will serve static assets, server render the application, register server functions, and integrate with hot reloading. - /// - /// # Example - /// ```rust, no_run - /// # #![allow(non_snake_case)] - /// # use dioxus_lib::prelude::*; - /// # use dioxus_fullstack::prelude::*; - /// #[tokio::main] - /// async fn main() { - /// let addr = dioxus::cli_config::fullstack_address_or_localhost(); - /// let router = axum::Router::new() - /// // Server side render the application, serve static assets, and register server functions - /// .serve_dioxus_application(ServeConfig::new().unwrap(), app) - /// .into_make_service(); - /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - /// axum::serve(listener, router).await.unwrap(); - /// } - /// - /// fn app() -> Element { - /// rsx! { "Hello World" } - /// } - /// ``` - fn serve_dioxus_application(self, cfg: Cfg, app: fn() -> Element) -> Self - where - Cfg: TryInto, - Error: std::error::Error, - Self: Sized; -} - -impl DioxusRouterExt for Router -where - S: Send + Sync + Clone + 'static, -{ - fn serve_static_assets(mut self) -> Self { - use tower_http::services::{ServeDir, ServeFile}; - - let public_path = crate::public_path(); - - if !public_path.exists() { - return self; - } - - // Serve all files in public folder except index.html - let dir = std::fs::read_dir(&public_path).unwrap_or_else(|e| { - panic!( - "Couldn't read public directory at {:?}: {}", - &public_path, e - ) - }); - - for entry in dir.flatten() { - let path = entry.path(); - if path.ends_with("index.html") { - continue; - } - let route = path - .strip_prefix(&public_path) - .unwrap() - .iter() - .map(|segment| { - segment.to_str().unwrap_or_else(|| { - panic!("Failed to convert path segment {:?} to string", segment) - }) - }) - .collect::>() - .join("/"); - let route = format!("/{}", route); - if path.is_dir() { - self = self.nest_service(&route, ServeDir::new(path).precompressed_br()); - } else { - self = self.nest_service(&route, ServeFile::new(path).precompressed_br()); - } - } - - self - } - - fn serve_dioxus_application(self, cfg: Cfg, app: fn() -> Element) -> Self - where - Cfg: TryInto, - Error: std::error::Error, - { - let cfg = cfg.try_into(); - let context_providers = cfg - .as_ref() - .map(|cfg| cfg.context_providers.clone()) - .unwrap_or_default(); - - // Add server functions and render index.html - let server = self - .serve_static_assets() - .register_server_functions_with_context(context_providers); - - match cfg { - Ok(cfg) => { - let ssr_state = SSRState::new(&cfg); - server.fallback( - get(render_handler) - .with_state(RenderHandleState::new(cfg, app).with_ssr_state(ssr_state)), - ) - } - Err(err) => { - tracing::trace!("Failed to create render handler. This is expected if you are only using fullstack for desktop/mobile server functions: {}", err); - server - } - } - } -} diff --git a/packages/fullstack/src/document/web.rs b/packages/fullstack/src/web.rs similarity index 89% rename from packages/fullstack/src/document/web.rs rename to packages/fullstack/src/web.rs index c57cb17eec..21b6547d54 100644 --- a/packages/fullstack/src/document/web.rs +++ b/packages/fullstack/src/web.rs @@ -1,13 +1,10 @@ -#![allow(unused)] //! On the client, we use the [`WebDocument`] implementation to render the head for any elements that were not rendered on the server. -use dioxus_lib::{document::*, prelude::queue_effect}; +use dioxus_lib::document::*; use dioxus_web::WebDocument; -use super::head_element_hydration_entry; - fn head_element_written_on_server() -> bool { - head_element_hydration_entry() + dioxus_fullstack_protocol::head_element_hydration_entry() .get() .ok() .unwrap_or_default() diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index a57a972474..a0e5b9209f 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -65,9 +65,9 @@ impl LiveviewRouter for Router { // Add an extra catch all segment to the route let mut route = route.trim_matches('/').to_string(); if route.is_empty() { - route = "/*route".to_string(); + route = "/{*route}".to_string(); } else { - route = format!("/{route}/*route"); + route = format!("/{route}/{{*route}}"); } self.route( diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index caa9f0a35a..add0910b1a 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -215,7 +215,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li Some(msg) = hot_reload_wait => { #[cfg(all(feature = "devtools", debug_assertions))] - match msg{ + match msg { dioxus_devtools::DevserverMsg::HotReload(msg)=> { dioxus_devtools::apply_changes(&vdom, &msg); } @@ -228,6 +228,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // usually only web gets this message - what are we supposed to do? // Maybe we could just binary patch ourselves in place without losing window state? }, + _ => {} } #[cfg(not(all(feature = "devtools", debug_assertions)))] let () = msg; diff --git a/packages/native/src/dioxus_application.rs b/packages/native/src/dioxus_application.rs index 58bf47b4be..3c65cc6836 100644 --- a/packages/native/src/dioxus_application.rs +++ b/packages/native/src/dioxus_application.rs @@ -56,6 +56,7 @@ impl DioxusNativeApplication { dioxus_devtools::DevserverMsg::FullReloadStart => {} dioxus_devtools::DevserverMsg::FullReloadFailed => {} dioxus_devtools::DevserverMsg::FullReloadCommand => {} + _ => {} }, DioxusNativeEvent::CreateHeadElement { diff --git a/packages/playwright-tests/barebones-template/.gitignore b/packages/playwright-tests/barebones-template/.gitignore new file mode 100644 index 0000000000..80aab8ea95 --- /dev/null +++ b/packages/playwright-tests/barebones-template/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target +.DS_Store + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/packages/playwright-tests/barebones-template/Cargo.lock b/packages/playwright-tests/barebones-template/Cargo.lock new file mode 100644 index 0000000000..c4820952eb --- /dev/null +++ b/packages/playwright-tests/barebones-template/Cargo.lock @@ -0,0 +1,5295 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + +[[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-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +dependencies = [ + "serde", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.9.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.9.0", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.9.0", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-serialize" +version = "0.6.3" +dependencies = [ + "const-serialize-macro", + "serde", +] + +[[package]] +name = "const-serialize-macro" +version = "0.6.3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "const-str" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2" + +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.101", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dioxus" +version = "0.6.3" +dependencies = [ + "dioxus-cli-config", + "dioxus-config-macro", + "dioxus-config-macros", + "dioxus-core", + "dioxus-core-macro", + "dioxus-desktop", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-logger", + "dioxus-mobile", + "dioxus-signals", + "dioxus-web", + "manganis", + "subsecond", + "warnings", +] + +[[package]] +name = "dioxus-asset-resolver" +version = "0.6.3" +dependencies = [ + "dioxus-cli-config", + "http", + "infer", + "jni", + "ndk", + "ndk-context", + "ndk-sys", + "thiserror 2.0.12", + "urlencoding", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.6.3" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.6.3" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-config-macros" +version = "0.6.3" + +[[package]] +name = "dioxus-core" +version = "0.6.3" +dependencies = [ + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 2.1.1", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.6.3" +dependencies = [ + "convert_case 0.8.0", + "dioxus-rsx", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dioxus-core-types" +version = "0.6.3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "dioxus-desktop" +version = "0.6.3" +dependencies = [ + "async-trait", + "base64", + "cocoa", + "core-foundation", + "dioxus-asset-resolver", + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "dunce", + "futures-channel", + "futures-util", + "generational-box", + "global-hotkey", + "infer", + "jni", + "lazy-js-bundle", + "muda", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "objc_id", + "once_cell", + "rfd", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "signal-hook", + "slab", + "tao", + "thiserror 2.0.12", + "tokio", + "tracing", + "tray-icon", + "urlencoding", + "webbrowser", + "wry", +] + +[[package]] +name = "dioxus-devtools" +version = "0.6.3" +dependencies = [ + "dioxus-cli-config", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.12", + "tracing", + "tungstenite", + "warnings", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-document" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-html", + "futures-channel", + "futures-util", + "generational-box", + "lazy-js-bundle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-fullstack" +version = "0.6.3" +dependencies = [ + "base64", + "bytes", + "ciborium", + "dioxus-devtools", + "dioxus-fullstack-hooks", + "dioxus-fullstack-protocol", + "dioxus-history", + "dioxus-lib", + "dioxus-web", + "dioxus_server_macro", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "serde", + "server_fn", + "tracing", + "web-sys", +] + +[[package]] +name = "dioxus-fullstack-hooks" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "dioxus-fullstack-protocol", + "dioxus-hooks", + "dioxus-signals", + "futures-channel", + "serde", +] + +[[package]] +name = "dioxus-fullstack-protocol" +version = "0.6.3" +dependencies = [ + "base64", + "ciborium", + "dioxus-core", + "serde", + "tracing", +] + +[[package]] +name = "dioxus-history" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "rustversion", + "slab", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-html" +version = "0.6.3" +dependencies = [ + "async-trait", + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-hooks", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "generational-box", + "keyboard-types", + "lazy-js-bundle", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "tracing", +] + +[[package]] +name = "dioxus-html-internal-macro" +version = "0.6.3" +dependencies = [ + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "dioxus-core-types", + "dioxus-html", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.1", + "serde", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-lib" +version = "0.6.3" +dependencies = [ + "dioxus-config-macro", + "dioxus-core", + "dioxus-core-macro", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-rsx", + "dioxus-signals", + "warnings", +] + +[[package]] +name = "dioxus-logger" +version = "0.6.3" +dependencies = [ + "console_error_panic_hook", + "dioxus-cli-config", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-mobile" +version = "0.6.3" +dependencies = [ + "dioxus-cli-config", + "dioxus-desktop", + "dioxus-lib", + "jni", + "libc", + "once_cell", +] + +[[package]] +name = "dioxus-rsx" +version = "0.6.3" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dioxus-signals" +version = "0.6.3" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "parking_lot", + "rustc-hash 2.1.1", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-web" +version = "0.6.3" +dependencies = [ + "async-trait", + "dioxus-cli-config", + "dioxus-core", + "dioxus-core-types", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack-protocol", + "dioxus-history", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "gloo-timers", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.1", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus_server_macro" +version = "0.6.3" +dependencies = [ + "proc-macro2", + "quote", + "server_fn_macro", + "syn 2.0.101", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.0", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "enumset" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a6b7c3d347de0a9f7bfd2f853be43fe32fa6fac30c70f6d6d67a1e936b87ee" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da3ea9e1d1a3b1593e15781f930120e72aa7501610b2f82e5b6739c72e8eac5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generational-box" +version = "0.6.3" +dependencies = [ + "parking_lot", + "tracing", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.9.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "global-hotkey" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "thiserror 2.0.12", + "windows-sys 0.59.0", + "x11-dl", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.15", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa 1.0.15", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.3", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy-js-bundle" +version = "0.6.3" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.0", + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + +[[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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "manganis" +version = "0.6.3" +dependencies = [ + "const-serialize", + "manganis-core", + "manganis-macro", +] + +[[package]] +name = "manganis-core" +version = "0.6.3" +dependencies = [ + "const-serialize", + "dioxus-cli-config", + "dioxus-core-types", + "serde", +] + +[[package]] +name = "manganis-macro" +version = "0.6.3" +dependencies = [ + "dunce", + "macro-string", + "manganis-core", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.44", +] + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +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 = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "myapp" +version = "0.1.0" +dependencies = [ + "dioxus", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.6.2", + "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", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "bitflags 2.9.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.0", + "dispatch2 0.3.0", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +dependencies = [ + "bitflags 2.9.0", + "objc2-core-foundation", +] + +[[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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "bitflags 2.9.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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 = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.26", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd", + "block2", + "dispatch2 0.2.0", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[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.9.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa 1.0.15", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b417bedc008acbdf6d6b4bc482d29859924114bbe2650b7921fb68a261d0aa6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.15", + "ryu", + "serde", +] + +[[package]] +name = "server_fn" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b0f92b9d3a62c73f238ac21f7a09f15bad335a9d1651514d9da80d2eaf8d4c" +dependencies = [ + "base64", + "bytes", + "const-str", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "pin-project-lite", + "reqwest", + "rustc_version", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.12", + "throw_error", + "tokio", + "tokio-tungstenite", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "341dd1087afe9f3e546c5979a4f0b6d55ac072e1201313f86e7fe364223835ac" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.101", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ab934f581482a66da82f2b57b15390ad67c9ab85bd9a6c54bb65060fb1380" +dependencies = [ + "server_fn_macro", + "syn 2.0.101", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62f06db0370222f7f498ef478fce9f8df5828848d1d3517e3331936d7074f55" +dependencies = [ + "quote", + "syn 2.0.101", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash 1.1.0", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "subsecond" +version = "0.6.3" +dependencies = [ + "js-sys", + "libc", + "libloading 0.8.6", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.6.3" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +dependencies = [ + "bitflags 2.9.0", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.1", + "windows-core 0.61.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[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.101", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "throw_error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e42a6afdde94f3e656fae18f837cb9bbe500a5ac5de325b09f3ec05b9c28e3" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.9.0", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.9.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap 2.9.0", + "toml_datetime", + "winnow 0.7.10", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tray-icon" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.12", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5df295f8451142f1856b1bd86a606dfe9587d439bc036e319c827700dbd555e" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-implement 0.58.0", + "windows-interface 0.58.0", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "webview2-com-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +dependencies = [ + "thiserror 1.0.69", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[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.101", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[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.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +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 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-version" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04a5c6627e310a23ad2358483286c7df260c964eb2d003d8efd6d0f4e79265c" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" +dependencies = [ + "base64", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc", + "objc_id", + "once_cell", + "percent-encoding", + "raw-window-handle 0.6.2", + "sha2", + "soup3", + "tao-macros", + "thiserror 1.0.69", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.58.0", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[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 = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2522b82023923eecb0b366da727ec883ace092e7887b61d3da5139f26b44da58" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.10", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d2e12843c75108c00c618c2e8ef9675b50b6ec095b36dc965f2e5aed463c15" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.10", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "zvariant" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557e89d54880377a507c94cd5452f20e35d14325faf9d2958ebeadce0966c1b2" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.10", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757779842a0d242061d24c28be589ce392e45350dfb9186dfd7a042a2e19870c" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.101", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.101", + "winnow 0.7.10", +] diff --git a/packages/playwright-tests/barebones-template/Cargo.toml b/packages/playwright-tests/barebones-template/Cargo.toml new file mode 100644 index 0000000000..f37c8eb6ed --- /dev/null +++ b/packages/playwright-tests/barebones-template/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "barebones-template-test" +version = "0.1.0" +authors = ["Jonathan Kelley "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = { workspace = true, features = [] } + +[features] +default = ["web"] +web = ["dioxus/web"] +desktop = ["dioxus/desktop"] +mobile = ["dioxus/mobile"] diff --git a/packages/playwright-tests/barebones-template/Dioxus.toml b/packages/playwright-tests/barebones-template/Dioxus.toml new file mode 100644 index 0000000000..a66bb00f84 --- /dev/null +++ b/packages/playwright-tests/barebones-template/Dioxus.toml @@ -0,0 +1,21 @@ +[application] + +[web.app] + +# HTML title tag content +title = "myapp" + +# include `assets` in web platform +[web.resource] + +# Additional CSS style files +style = [] + +# Additional JavaScript files +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/packages/playwright-tests/barebones-template/README.md b/packages/playwright-tests/barebones-template/README.md new file mode 100644 index 0000000000..1e629b6306 --- /dev/null +++ b/packages/playwright-tests/barebones-template/README.md @@ -0,0 +1,25 @@ +# Development + +Your new bare-bones project includes minimal organization with a single `main.rs` file and a few assets. + +``` +project/ +├─ assets/ # Any assets that are used by the app should be placed here +├─ src/ +│ ├─ main.rs # main.rs is the entry point to your application and currently contains all components for the app +├─ Cargo.toml # The Cargo.toml file defines the dependencies and feature flags for your project +``` + +### Serving Your App + +Run the following command in the root of your project to start developing with the default platform: + +```bash +dx serve +``` + +To run for a different platform, use the `--platform platform` flag. E.g. +```bash +dx serve --platform desktop +``` + diff --git a/packages/playwright-tests/barebones-template/assets/favicon.ico b/packages/playwright-tests/barebones-template/assets/favicon.ico new file mode 100644 index 0000000000..eed0c09735 Binary files /dev/null and b/packages/playwright-tests/barebones-template/assets/favicon.ico differ diff --git a/packages/playwright-tests/barebones-template/assets/header.svg b/packages/playwright-tests/barebones-template/assets/header.svg new file mode 100644 index 0000000000..59c96f2f2e --- /dev/null +++ b/packages/playwright-tests/barebones-template/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/packages/playwright-tests/barebones-template/assets/main.css b/packages/playwright-tests/barebones-template/assets/main.css new file mode 100644 index 0000000000..90c0fc1c09 --- /dev/null +++ b/packages/playwright-tests/barebones-template/assets/main.css @@ -0,0 +1,46 @@ +/* App-wide styling */ +body { + background-color: #0f1116; + color: #ffffff; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + margin: 20px; +} + +#hero { + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +#links { + width: 400px; + text-align: left; + font-size: x-large; + color: white; + display: flex; + flex-direction: column; +} + +#links a { + color: white; + text-decoration: none; + margin-top: 20px; + margin: 10px 0px; + border: white 1px solid; + border-radius: 5px; + padding: 10px; +} + +#links a:hover { + background-color: #1f1f1f; + cursor: pointer; +} + +#header { + max-width: 1200px; +} + + + diff --git a/packages/playwright-tests/barebones-template/src/main.rs b/packages/playwright-tests/barebones-template/src/main.rs new file mode 100644 index 0000000000..4593314917 --- /dev/null +++ b/packages/playwright-tests/barebones-template/src/main.rs @@ -0,0 +1,37 @@ +use dioxus::prelude::*; + +const FAVICON: Asset = asset!("/assets/favicon.ico"); +const MAIN_CSS: Asset = asset!("/assets/main.css"); +const HEADER_SVG: Asset = asset!("/assets/header.svg"); + +fn main() { + dioxus::launch(App); +} + +#[component] +fn App() -> Element { + rsx! { + document::Link { rel: "icon", href: FAVICON } + document::Link { rel: "stylesheet", href: MAIN_CSS } + Hero {} + + } +} + +#[component] +pub fn Hero() -> Element { + rsx! { + div { + id: "hero", + img { src: HEADER_SVG, id: "header" } + div { id: "links", + a { href: "https://dioxuslabs.com/learn/0.6/", "📚 Learn Dioxus" } + a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" } + a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" } + a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" } + a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" } + a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" } + } + } + } +} diff --git a/packages/playwright-tests/cli-optimization/Cargo.toml b/packages/playwright-tests/cli-optimization/Cargo.toml index 1729f9efca..38df64d150 100644 --- a/packages/playwright-tests/cli-optimization/Cargo.toml +++ b/packages/playwright-tests/cli-optimization/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -dioxus = { workspace = true, features = ["web"]} +dioxus = { workspace = true, features = ["web"] } [build-dependencies] reqwest = { workspace = true, features = ["blocking"] } diff --git a/packages/playwright-tests/default-features-disabled/src/main.rs b/packages/playwright-tests/default-features-disabled/src/main.rs index 2f0f15a005..990bee2c08 100644 --- a/packages/playwright-tests/default-features-disabled/src/main.rs +++ b/packages/playwright-tests/default-features-disabled/src/main.rs @@ -11,10 +11,13 @@ fn main() { fn app() -> Element { let server_features = use_server_future(get_server_features)?.unwrap().unwrap(); let mut client_features = use_signal(Vec::new); + use_effect(move || { client_features.set(current_platform_features()); }); + let mut count = use_signal(|| 0); + rsx! { div { "server features: {server_features:?}" @@ -22,6 +25,10 @@ fn app() -> Element { div { "client features: {client_features:?}" } + button { + onclick: move |_| count += 1, + "{count}" + } } } diff --git a/packages/playwright-tests/playwright.config.js b/packages/playwright-tests/playwright.config.js index 0432af9738..321935dd0e 100644 --- a/packages/playwright-tests/playwright.config.js +++ b/packages/playwright-tests/playwright.config.js @@ -138,15 +138,16 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, - { - cwd: path.join(process.cwd(), "nested-suspense"), - command: - 'cargo run --package dioxus-cli --release -- serve --bin nested-suspense-ssg --force-sequential --platform web --ssg --addr "127.0.0.1" --port 6060', - port: 6060, - timeout: 50 * 60 * 1000, - reuseExistingServer: !process.env.CI, - stdout: "pipe", - }, + // currently disabled - we want ssg to be a "bundle" feature, not a serve feature. + // { + // cwd: path.join(process.cwd(), "nested-suspense"), + // command: + // 'cargo run --package dioxus-cli --release -- serve --bin nested-suspense-ssg --force-sequential --platform web --ssg --addr "127.0.0.1" --port 6060', + // port: 6060, + // timeout: 50 * 60 * 1000, + // reuseExistingServer: !process.env.CI, + // stdout: "pipe", + // }, { cwd: path.join(process.cwd(), "cli-optimization"), // Remove the cache folder for the cli-optimization build to force a full cache reset @@ -160,7 +161,7 @@ module.exports = defineConfig({ { cwd: path.join(process.cwd(), "wasm-split-harness"), command: - 'cargo run --package dioxus-cli --release --features optimizations -- serve --bin wasm-split-harness --platform web --addr "127.0.0.1" --port 8001 --experimental-wasm-split --profile wasm-split-release', + 'cargo run --package dioxus-cli --release --features optimizations -- serve --bin wasm-split-harness --platform web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release', port: 8001, timeout: 50 * 60 * 1000, reuseExistingServer: !process.env.CI, @@ -175,5 +176,14 @@ module.exports = defineConfig({ reuseExistingServer: !process.env.CI, stdout: "pipe", }, + { + cwd: path.join(process.cwd(), "barebones-template"), + command: + 'cargo run --package dioxus-cli --release -- serve --force-sequential --addr "127.0.0.1" --port 8123', + port: 8123, + timeout: 50 * 60 * 1000, + reuseExistingServer: !process.env.CI, + stdout: "pipe", + }, ], }); diff --git a/packages/playwright-tests/suspense-carousel/Cargo.toml b/packages/playwright-tests/suspense-carousel/Cargo.toml index 784c6cedb7..6de003c710 100644 --- a/packages/playwright-tests/suspense-carousel/Cargo.toml +++ b/packages/playwright-tests/suspense-carousel/Cargo.toml @@ -5,8 +5,8 @@ version.workspace = true publish = false [dependencies] -async-std = "1.13.0" dioxus = { workspace = true, features = ["fullstack"] } +async-std = "1.13.0" serde = "1.0.218" [features] diff --git a/packages/playwright-tests/test-results/.last-run.json b/packages/playwright-tests/test-results/.last-run.json deleted file mode 100644 index 02f9f17861..0000000000 --- a/packages/playwright-tests/test-results/.last-run.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "status": "interrupted", - "failedTests": [ - "d630e8b735ec4c8447b5-3853da72eac8a6a9a3b7", - "ff750809a07139231c1d-e791c1c230e601a8bc52", - "7d44990a3cf257f22406-5c4d478bb363aff6da09" - ] -} \ No newline at end of file diff --git a/packages/playwright-tests/wasm-split-harness/Cargo.toml b/packages/playwright-tests/wasm-split-harness/Cargo.toml index 37fb94edf8..9b74c0a843 100644 --- a/packages/playwright-tests/wasm-split-harness/Cargo.toml +++ b/packages/playwright-tests/wasm-split-harness/Cargo.toml @@ -16,5 +16,5 @@ wasm-bindgen = { workspace = true } wasm-bindgen-futures = { workspace = true } web-sys = { workspace = true, features = ["Document", "Window", "HtmlElement", "Text", "DomRectReadOnly", "console"] } once_cell = { workspace = true } -getrandom = { workspace = true } +getrandom = { workspace = true, features = ["js"] } reqwest = { workspace = true, features = ["json"] } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index ffdafeff3d..66dc587169 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -1,4 +1,4 @@ -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::parse::Parse; use syn::parse::ParseStream; use syn::parse_quote; @@ -271,6 +271,11 @@ impl Route { let dynamic_segments = self.dynamic_segments(); let dynamic_segments_from_route = self.dynamic_segments(); + + let component = quote_spanned! { name.span() => + #component + }; + /* The implementation of this is pretty gnarly/gross. diff --git a/packages/router/src/contexts/outlet.rs b/packages/router/src/contexts/outlet.rs index b672bef301..78c336fcf1 100644 --- a/packages/router/src/contexts/outlet.rs +++ b/packages/router/src/contexts/outlet.rs @@ -74,7 +74,12 @@ impl OutletContext { /// /// # Examples /// -/// ```rust +/// ```rust, no_run +/// # use dioxus::prelude::*; +/// +/// #[derive(Clone)] +/// struct MyRouter; +/// /// let outlet_ctx = use_outlet_context::(); /// println!("Current nesting level: {}", outlet_ctx.level()); /// ``` diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index 50a890bbff..bd328ed306 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -220,7 +220,10 @@ impl Component { let mut tokens = if let Some(props) = manual_props.as_ref() { quote_spanned! { props.span() => let mut __manual_props = #props; } } else { - quote_spanned! { self.name.span() => fc_to_builder(#name #generics) } + // we only want to span the name and generics, not the `fc_to_builder` call so jump-to-def + // only finds the single entry (#name) + let spanned = quote_spanned! { self.name.span() => #name #generics }; + quote! { fc_to_builder(#spanned) } }; tokens.append_all(self.add_fields_to_builder( diff --git a/packages/rsx/src/ifmt.rs b/packages/rsx/src/ifmt.rs index 5e47f4f1f6..c4f2112f7d 100644 --- a/packages/rsx/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -230,10 +230,7 @@ impl ToTokens for IfmtInput { // If the segments are not complex exprs, we can just use format! directly to take advantage of RA rename/expansion if self.is_simple_expr() { let raw = &self.source; - tokens.extend(quote! { - ::std::format!(#raw) - }); - return; + return quote_spanned! { raw.span() => ::std::format!(#raw) }.to_tokens(tokens); } // build format_literal diff --git a/packages/server-macro/Cargo.toml b/packages/server-macro/Cargo.toml index cc4ca8057f..98300630b5 100644 --- a/packages/server-macro/Cargo.toml +++ b/packages/server-macro/Cargo.toml @@ -13,7 +13,8 @@ description = "Server function macros for Dioxus" proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true, features = ["full"] } -server_fn_macro = "0.8.0-rc1" +server_fn_macro = { workspace = true } + [dev-dependencies] dioxus = { workspace = true, features = ["fullstack"] } @@ -28,6 +29,7 @@ proc-macro = true axum = ["server_fn_macro/axum"] server = ["server_fn_macro/ssr"] reqwest = ["server_fn_macro/reqwest"] +generic = ["server_fn_macro/generic"] [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/server/Cargo.toml b/packages/server/Cargo.toml new file mode 100644 index 0000000000..f320df25b1 --- /dev/null +++ b/packages/server/Cargo.toml @@ -0,0 +1,110 @@ +[package] +name = "dioxus-server" +authors = ["Jonathan Kelley", "Evan Almloff"] +version = { workspace = true } +edition = "2021" +description = "Fullstack utilities for Dioxus: Build fullstack web, desktop, and mobile apps with a single codebase." +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +homepage = "https://dioxuslabs.com" +keywords = ["web", "desktop", "mobile", "gui", "server"] +resolver = "2" + +[dependencies] +# dioxus +dioxus-lib = { workspace = true } +generational-box = { workspace = true } + +# server functions +server_fn = { workspace = true, default-features = false } + +# axum + native deps +axum = { workspace = true, default-features = false } +tower-http = { workspace = true, features = ["fs"], optional = true } +tokio-util = { workspace = true, features = ["rt"], optional = true } +tokio-stream = { workspace = true, features = ["sync"], optional = true } +tower = { workspace = true, features = ["util"], optional = true} +tower-layer = { version = "0.3.2", optional = true} +hyper-util = { workspace = true, features = ["full"], optional = true } +hyper = { workspace = true, optional = true } +http = { workspace = true } + +# Dioxus + SSR +dioxus-ssr = { workspace = true } +dioxus-isrg = { workspace = true } +dioxus-router = { workspace = true, features = ["streaming"] } +dioxus-fullstack-hooks = { workspace = true } +dioxus-fullstack-protocol = { workspace = true } +dioxus-interpreter-js = { workspace = true, optional = true } + +tracing = { workspace = true } +tracing-futures = { workspace = true } +once_cell = { workspace = true } +async-trait = { workspace = true } +serde = { workspace = true } + +futures-util = { workspace = true } +futures-channel = { workspace = true } +ciborium = { workspace = true } +base64 = { workspace = true } +rustls = { workspace = true, optional = true } +hyper-rustls = { workspace = true, optional = true } + +pin-project = { version = "1.1.2" } +thiserror = { workspace = true } +bytes = "1.4.0" +parking_lot = { workspace = true, features = ["send_guard"] } +web-sys = { version = "0.3.61", features = [ + "Window", + "Document", + "Element", + "HtmlDocument", + "Storage", + "console", +] } + +dioxus-cli-config = { workspace = true } +dioxus-devtools = { workspace = true, optional = true } +aws-lc-rs = { version = "1.8.1", optional = true } +dioxus-history = { workspace = true } +subsecond.workspace = true +inventory = { workspace = true } +dashmap = "6.1.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["rt", "sync", "macros"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread", "macros", "net"] } + +[dev-dependencies] +dioxus = { workspace = true, features = ["fullstack"] } + +[features] +default = ["devtools", "full"] +full = [ + "core", + "server_fn/ssr", + "dep:tower-http", + "default-tls", + "dep:tower", + "dep:hyper", + "dep:tower-layer", + "dep:tokio-util", + "dep:hyper-util", +] +core = [ + "server_fn/axum-no-default", + "server_fn/ssr", + "document", +] +devtools = ["dep:dioxus-devtools"] +document = ["dep:dioxus-interpreter-js"] +default-tls = ["server_fn/default-tls"] +rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"] +aws-lc-rs = ["dep:aws-lc-rs"] + +[package.metadata.docs.rs] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +features = ["axum", "web", "aws-lc-rs"] + diff --git a/packages/server/docs/request_origin.md b/packages/server/docs/request_origin.md new file mode 100644 index 0000000000..0e946f4b0f --- /dev/null +++ b/packages/server/docs/request_origin.md @@ -0,0 +1,48 @@ +This method interacts with information from the current request. The request may come from: + +1. The initial SSR render if this method called from a [`Component`](dioxus_lib::prelude::component) or a [`server`](crate::prelude::server) function that is called during the initial render + +```rust +# use dioxus::prelude::*; +#[component] +fn PrintHtmlRequestInfo() -> Element { + // The server context only exists on the server, so we need to put it behind a server_only! config + server_only! { + // Since we are calling this from a component, the server context that is returned will be from + // the html request for ssr rendering + let context = server_context(); + let request_parts = context.request_parts(); + println!("headers are {:?}", request_parts.headers); + } + rsx! {} +} +``` + +2. A request to a [`server`](crate::prelude::server) function called directly from the client (either on desktop/mobile or on the web frontend after the initial render) + +```rust +# use dioxus::prelude::*; +#[server] +async fn read_headers() -> Result<(), ServerFnError> { + // Since we are calling this from a server function, the server context that is may be from the + // initial request or a request from the client + let context = server_context(); + let request_parts = context.request_parts(); + println!("headers are {:?}", request_parts.headers); + Ok(()) +} + +#[component] +fn CallServerFunction() -> Element { + rsx! { + button { + // If you click the button, the server function will be called and the server context will be + // from the client request + onclick: move |_| async { + _ = read_headers().await + }, + "Call server function" + } + } +} +``` diff --git a/packages/fullstack/src/serve_config.rs b/packages/server/src/config.rs similarity index 97% rename from packages/fullstack/src/serve_config.rs rename to packages/server/src/config.rs index 34be96f09b..f1698bf64a 100644 --- a/packages/fullstack/src/serve_config.rs +++ b/packages/server/src/config.rs @@ -355,6 +355,18 @@ impl ServeConfigBuilder { .index_path .unwrap_or_else(|| public_path.join("index.html")); load_index_path(index_path)? + // load_index_path(index_path).unwrap_or_else(|_| { + // r#" + // + // + // + // + //

+ // + // + // "# + // .to_string() + // }) } }; @@ -400,7 +412,7 @@ pub(crate) fn public_path() -> PathBuf { } /// An error that can occur when loading the index.html file -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UnableToLoadIndex(PathBuf); impl std::fmt::Display for UnableToLoadIndex { @@ -465,7 +477,7 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct IndexHtml { pub(crate) head_before_title: String, pub(crate) head_after_title: String, diff --git a/packages/fullstack/src/server_context.rs b/packages/server/src/context.rs similarity index 99% rename from packages/fullstack/src/server_context.rs rename to packages/server/src/context.rs index a160828e90..be5902d7d5 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/server/src/context.rs @@ -275,7 +275,6 @@ mod server_fn_impl { } /// Copy the response parts to a response and mark this server context as sent - #[cfg(feature = "axum")] pub(crate) fn send_response(&self, response: &mut http::response::Response) { self.response_sent .store(true, std::sync::atomic::Ordering::Relaxed); @@ -426,12 +425,9 @@ impl FromServerContext for FromContext { } } -#[cfg(feature = "axum")] -#[cfg_attr(docsrs, doc(cfg(feature = "axum")))] /// An adapter for axum extractors for the server context pub struct Axum; -#[cfg(feature = "axum")] #[async_trait::async_trait] impl> FromServerContext for I { type Rejection = I::Rejection; diff --git a/packages/fullstack/src/document/server.rs b/packages/server/src/document.rs similarity index 96% rename from packages/fullstack/src/document/server.rs rename to packages/server/src/document.rs index ab7afa2bb1..be688e2f98 100644 --- a/packages/fullstack/src/document/server.rs +++ b/packages/server/src/document.rs @@ -11,6 +11,11 @@ use parking_lot::RwLock; static RENDERER: Lazy> = Lazy::new(|| RwLock::new(Renderer::new())); +/// Reset the static renderer to a fresh state, clearing its cache. +pub(crate) fn reset_renderer() { + RENDERER.write().clear(); +} + #[derive(Default)] struct ServerDocumentInner { streaming: bool, @@ -64,7 +69,7 @@ impl ServerDocument { // We only serialize the head elements if the web document feature is enabled #[cfg(feature = "document")] { - super::head_element_hydration_entry() + dioxus_fullstack_protocol::head_element_hydration_entry() .insert(&!self.0.borrow().streaming, std::panic::Location::caller()); } } diff --git a/packages/server/src/launch.rs b/packages/server/src/launch.rs new file mode 100644 index 0000000000..1c2d21a30a --- /dev/null +++ b/packages/server/src/launch.rs @@ -0,0 +1,283 @@ +//! A launch function that creates an axum router for the LaunchBuilder + +use crate::{ + collect_raw_server_fns, render_handler, server::DioxusRouterExt, RenderHandleState, SSRState, + ServeConfig, ServeConfigBuilder, +}; +use axum::{ + body::Body, + extract::{Request, State}, + response::IntoResponse, + routing::IntoMakeService, +}; +use dioxus_cli_config::base_path; +use dioxus_devtools::DevserverMsg; +use dioxus_lib::prelude::*; +use futures_util::{stream::FusedStream, StreamExt}; +use hyper::body::Incoming; +use hyper_util::server::conn::auto::Builder as HyperBuilder; +use hyper_util::{ + rt::{TokioExecutor, TokioIo}, + service::TowerToHyperService, +}; +use std::{any::Any, collections::HashMap, net::SocketAddr}; +use tokio::net::TcpStream; +use tokio_util::task::LocalPoolHandle; +use tower::Service; +use tower::ServiceExt as _; + +type ContextList = Vec Box + Send + Sync>>; + +type BaseComp = fn() -> Element; + +/// Launch a fullstack app with the given root component, contexts, and config. +#[allow(unused)] +pub fn launch(root: BaseComp, contexts: ContextList, platform_config: Vec>) -> ! { + #[cfg(not(target_arch = "wasm32"))] + tokio::runtime::Runtime::new() + .unwrap() + .block_on(async move { + serve_server(root, contexts, platform_config).await; + }); + + unreachable!("Launching a fullstack app should never return") +} + +#[cfg(not(target_arch = "wasm32"))] +async fn serve_server( + original_root: fn() -> Result, + contexts: Vec Box + Send + Sync>>, + platform_config: Vec>, +) { + let (devtools_tx, mut devtools_rx) = futures_channel::mpsc::unbounded(); + + if let Some(endpoint) = dioxus_cli_config::devserver_ws_endpoint() { + dioxus_devtools::connect(endpoint, move |msg| { + _ = devtools_tx.unbounded_send(msg); + }) + } + + let platform_config = platform_config + .into_iter() + .find_map(|cfg| { + cfg.downcast::() + .map(|cfg| Result::Ok(*cfg)) + .or_else(|cfg| { + cfg.downcast::() + .map(|builder| builder.build()) + }) + .ok() + }) + .unwrap_or_else(ServeConfig::new); + + // Extend the config's context providers with the context providers from the launch builder + let cfg = platform_config + .map(|mut cfg| { + let mut contexts = contexts; + let cfg_context_providers = cfg.context_providers.clone(); + for i in 0..cfg_context_providers.len() { + contexts.push(Box::new({ + let cfg_context_providers = cfg_context_providers.clone(); + move || (cfg_context_providers[i])() + })); + } + cfg.context_providers = std::sync::Arc::new(contexts); + cfg + }) + .unwrap(); + + // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address + // and we use the generated address the CLI gives us + let address = dioxus_cli_config::fullstack_address_or_localhost(); + + // Create the router and register the server functions under the basepath. + let router = apply_base_path( + axum::Router::new().serve_dioxus_application(cfg.clone(), original_root), + original_root, + cfg.clone(), + base_path().map(|s| s.to_string()), + ); + + let task_pool = LocalPoolHandle::new(5); + let mut make_service = router.into_make_service(); + + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + + tracing::trace!("Listening on {address}"); + + enum Msg { + TcpStream(std::io::Result<(TcpStream, SocketAddr)>), + Devtools(DevserverMsg), + } + + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(0); + let mut hr_idx = 0; + + // Manually loop on accepting connections so we can also respond to devtools messages + loop { + let res = tokio::select! { + res = listener.accept() => Msg::TcpStream(res), + msg = devtools_rx.next(), if !devtools_rx.is_terminated() => { + if let Some(msg) = msg { + Msg::Devtools(msg) + } else { + continue; + } + } + }; + + match res { + // We need to delete our old router and build a new one + // + // one challenge is that the server functions are sitting in the dlopened lib and no longer + // accessible by us (the original process) + // + // We need to somehow get them out... ? + // + // for now we just support editing existing server functions + Msg::Devtools(devserver_msg) => { + match devserver_msg { + DevserverMsg::HotReload(hot_reload_msg) => { + if hot_reload_msg.for_build_id == Some(dioxus_cli_config::build_id()) { + if let Some(table) = hot_reload_msg.jump_table { + unsafe { dioxus_devtools::subsecond::apply_patch(table).unwrap() }; + + let mut new_router = axum::Router::new().serve_static_assets(); + let new_cfg = ServeConfig::new().unwrap(); + + let server_fn_iter = collect_raw_server_fns(); + + // de-duplicate iteratively by preferring the most recent (first, since it's linked) + let mut server_fn_map: HashMap<_, _> = HashMap::new(); + for f in server_fn_iter.into_iter().rev() { + server_fn_map.insert(f.path(), f); + } + + for (_, fn_) in server_fn_map { + tracing::trace!( + "Registering server function: {:?} {:?}", + fn_.path(), + fn_.method() + ); + new_router = crate::register_server_fn_on_router( + fn_, + new_router, + new_cfg.context_providers.clone(), + ); + } + + let hot_root = subsecond::HotFn::current(original_root); + let new_root_addr = hot_root.ptr_address() as usize as *const (); + let new_root = unsafe { + std::mem::transmute::<*const (), fn() -> Element>(new_root_addr) + }; + + crate::document::reset_renderer(); + + let state = RenderHandleState::new(new_cfg.clone(), new_root) + .with_ssr_state(SSRState::new(&new_cfg)); + + let fallback_handler = + axum::routing::get(render_handler).with_state(state); + + make_service = apply_base_path( + new_router.fallback(fallback_handler), + new_root, + new_cfg.clone(), + base_path().map(|s| s.to_string()), + ) + .into_make_service(); + + shutdown_tx.send_modify(|i| { + *i += 1; + hr_idx += 1; + }); + } + } + } + DevserverMsg::FullReloadStart => {} + DevserverMsg::FullReloadFailed => {} + DevserverMsg::FullReloadCommand => {} + DevserverMsg::Shutdown => {} + _ => {} + } + } + Msg::TcpStream(Ok((tcp_stream, _remote_addr))) => { + let this_hr_index = hr_idx; + let mut make_service = make_service.clone(); + let mut shutdown_rx = shutdown_rx.clone(); + task_pool.spawn_pinned(move || async move { + let tcp_stream = TokioIo::new(tcp_stream); + + std::future::poll_fn(|cx| { + as tower::Service>::poll_ready( + &mut make_service, + cx, + ) + }) + .await + .unwrap_or_else(|err| match err {}); + + let tower_service = make_service + .call(()) + .await + .unwrap_or_else(|err| match err {}) + .map_request(|req: Request| req.map(Body::new)); + + // upgrades needed for websockets + let builder = HyperBuilder::new(TokioExecutor::new()); + let connection = builder.serve_connection_with_upgrades( + tcp_stream, + TowerToHyperService::new(tower_service), + ); + + tokio::select! { + res = connection => { + if let Err(_err) = res { + // This error only appears when the client doesn't send a request and + // terminate the connection. + // + // If client sends one request then terminate connection whenever, it doesn't + // appear. + } + } + _res = shutdown_rx.wait_for(|i| *i == this_hr_index + 1) => {} + } + }); + } + Msg::TcpStream(Err(_)) => {} + } + } +} + +fn apply_base_path( + mut router: axum::Router, + root: fn() -> Result, + cfg: ServeConfig, + base_path: Option, +) -> axum::Router { + if let Some(base_path) = base_path { + let base_path = base_path.trim_matches('/'); + // If there is a base path, nest the router under it and serve the root route manually + // Nesting a route in axum only serves /base_path or /base_path/ not both + router = axum::Router::new().nest(&format!("/{base_path}/"), router); + + async fn root_render_handler( + state: State, + mut request: Request, + ) -> impl IntoResponse { + // The root of the base path always looks like the root from dioxus fullstack + *request.uri_mut() = "/".parse().unwrap(); + render_handler(state, request).await + } + + let ssr_state = SSRState::new(&cfg); + router = router.route( + &format!("/{base_path}"), + axum::routing::method_routing::get(root_render_handler) + .with_state(RenderHandleState::new(cfg, root).with_ssr_state(ssr_state)), + ) + } + + router +} diff --git a/packages/server/src/lib.rs b/packages/server/src/lib.rs new file mode 100644 index 0000000000..e2e2649443 --- /dev/null +++ b/packages/server/src/lib.rs @@ -0,0 +1,98 @@ +//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework. +//! +//! # Example +//! ```rust, no_run +//! #![allow(non_snake_case)] +//! use dioxus::prelude::*; +//! +//! fn main() { +//! #[cfg(feature = "web")] +//! // Hydrate the application on the client +//! dioxus::launch(app); +//! #[cfg(feature = "server")] +//! { +//! tokio::runtime::Runtime::new() +//! .unwrap() +//! .block_on(async move { +//! // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address +//! // and we use the generated address the CLI gives us +//! let address = dioxus::cli_config::fullstack_address_or_localhost(); +//! let listener = tokio::net::TcpListener::bind(address) +//! .await +//! .unwrap(); +//! axum::serve( +//! listener, +//! axum::Router::new() +//! // Server side render the application, serve static assets, and register server functions +//! .serve_dioxus_application(ServeConfigBuilder::default(), app) +//! .into_make_service(), +//! ) +//! .await +//! .unwrap(); +//! }); +//! } +//! } +//! +//! fn app() -> Element { +//! +//! +//! rsx! { +//! button { +//! onclick: move |_| async move { +//! if let Ok(data) = get_server_data().await { +//! text.set(data); +//! } +//! }, +//! "Run a server function" +//! } +//! "Server said: {text}" +//! } +//! } +//! +//! #[server(GetServerData)] +//! async fn get_server_data() -> Result { +//! Ok("Hello from the server!".to_string()) +//! } +//! ``` + +pub mod config; +pub mod context; + +mod document; +mod render; +mod server; +mod streaming; + +pub(crate) use config::*; + +pub use crate::config::{ServeConfig, ServeConfigBuilder}; +pub use crate::context::Axum; +pub use crate::render::{FullstackHTMLTemplate, SSRState}; +pub use crate::server::*; +pub use config::*; +pub use context::{ + extract, server_context, with_server_context, DioxusServerContext, FromContext, + FromServerContext, ProvideServerContext, +}; +pub use document::ServerDocument; + +#[cfg(not(target_arch = "wasm32"))] +mod launch; + +#[cfg(not(target_arch = "wasm32"))] +pub use launch::launch; + +/// Re-export commonly used items +pub mod prelude { + pub use crate::config::{ServeConfig, ServeConfigBuilder}; + pub use crate::context::Axum; + pub use crate::context::{ + extract, server_context, with_server_context, DioxusServerContext, FromContext, + FromServerContext, ProvideServerContext, + }; + pub use crate::render::{FullstackHTMLTemplate, SSRState}; + pub use crate::server::*; + pub use dioxus_isrg::{IncrementalRenderer, IncrementalRendererConfig}; +} + +pub use dioxus_isrg::{IncrementalRenderer, IncrementalRendererConfig}; diff --git a/packages/fullstack/src/render.rs b/packages/server/src/render.rs similarity index 96% rename from packages/fullstack/src/render.rs rename to packages/server/src/render.rs index 564eb2ea33..8db0ac495c 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/server/src/render.rs @@ -1,10 +1,12 @@ //! A shared pool of renderers for efficient server side rendering. -use crate::document::ServerDocument; -use crate::streaming::{Mount, StreamingRenderer}; +use crate::{document::ServerDocument, ProvideServerContext, ServeConfig}; +use crate::{ + streaming::{Mount, StreamingRenderer}, + DioxusServerContext, +}; use dioxus_cli_config::base_path; use dioxus_fullstack_hooks::{StreamingContext, StreamingStatus}; use dioxus_fullstack_protocol::{HydrationContext, SerializedHydrationData}; -use dioxus_interpreter_js::INITIALIZE_STREAMING_JS; use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness}; use dioxus_lib::document::Document; use dioxus_lib::prelude::dioxus_core::DynamicNode; @@ -12,14 +14,10 @@ use dioxus_router::prelude::ParseRouteError; use dioxus_ssr::Renderer; use futures_channel::mpsc::Sender; use futures_util::{Stream, StreamExt}; -use std::fmt::Write; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::RwLock; -use std::{collections::HashMap, future::Future}; +use std::{collections::HashMap, fmt::Write, future::Future, rc::Rc, sync::Arc, sync::RwLock}; use tokio::task::JoinHandle; -use crate::{prelude::*, StreamingMode}; +use crate::StreamingMode; use dioxus_lib::prelude::*; /// A suspense boundary that is pending with a placeholder in the client @@ -175,6 +173,8 @@ impl SsrRendererPool { let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() }; + tracing::debug!("Renering wrapper with index: {:#?}", cfg.index); + let server_context = server_context.clone(); let mut renderer = self .renderers @@ -188,7 +188,7 @@ impl SsrRendererPool { let create_render_future = move || async move { let mut virtual_dom = virtual_dom_factory(); - let document = std::rc::Rc::new(crate::document::server::ServerDocument::default()); + let document = Rc::new(ServerDocument::default()); virtual_dom.provide_root_context(document.clone()); // If there is a base path, trim the base path from the route and add the base path formatting to the // history provider @@ -202,9 +202,10 @@ impl SsrRendererPool { } else { history = dioxus_history::MemoryHistory::with_initial_path(&route); } + let streaming_context = in_root_scope(&virtual_dom, StreamingContext::new); virtual_dom.provide_root_context(Rc::new(history) as Rc); - virtual_dom.provide_root_context(document.clone() as std::rc::Rc); + virtual_dom.provide_root_context(document.clone() as Rc); virtual_dom.provide_root_context(streaming_context); // rebuild the virtual dom @@ -621,7 +622,7 @@ impl FullstackHTMLTemplate { let ServeConfig { index, .. } = &self.cfg; let title = { - let document: Option> = + let document: Option> = virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context()); // Collect any head content from the document provider and inject that into the head document.and_then(|document| document.title()) @@ -635,7 +636,7 @@ impl FullstackHTMLTemplate { } to.write_str(&index.head_after_title)?; - let document: Option> = + let document: Option> = virtual_dom.in_runtime(|| ScopeId::ROOT.consume_context()); if let Some(document) = document { // Collect any head content from the document provider and inject that into the head @@ -659,7 +660,11 @@ impl FullstackHTMLTemplate { to.write_str(&index.close_head)?; + // // #[cfg(feature = "document")] + // { + use dioxus_interpreter_js::INITIALIZE_STREAMING_JS; write!(to, "")?; + // } Ok(()) } @@ -712,21 +717,6 @@ impl FullstackHTMLTemplate { Ok(()) } - - /// Wrap a body in the template - pub fn wrap_body( - &self, - to: &mut R, - virtual_dom: &VirtualDom, - body: impl std::fmt::Display, - ) -> Result<(), dioxus_isrg::IncrementalRendererError> { - self.render_head(to, virtual_dom)?; - write!(to, "{body}")?; - self.render_after_main(to, virtual_dom)?; - self.render_after_body(to)?; - - Ok(()) - } } fn pre_renderer() -> Renderer { diff --git a/packages/fullstack/src/axum_core.rs b/packages/server/src/server.rs similarity index 53% rename from packages/fullstack/src/axum_core.rs rename to packages/server/src/server.rs index 8b4c966dbb..d400878a2d 100644 --- a/packages/fullstack/src/axum_core.rs +++ b/packages/server/src/server.rs @@ -1,74 +1,5 @@ -//! Dioxus core utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework. -//! -//! # Example -//! ```rust, no_run -//! #![allow(non_snake_case)] -//! use dioxus::prelude::*; -//! -//! fn main() { -//! #[cfg(feature = "web")] -//! // Hydrate the application on the client -//! dioxus::launch(app); -//! #[cfg(feature = "server")] -//! { -//! tokio::runtime::Runtime::new() -//! .unwrap() -//! .block_on(async move { -//! // Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address -//! // and we use the generated address the CLI gives us -//! let address = dioxus::cli_config::fullstack_address_or_localhost(); -//! let listener = tokio::net::TcpListener::bind(address) -//! .await -//! .unwrap(); -//! axum::serve( -//! listener, -//! axum::Router::new() -//! // Server side render the application, serve static assets, and register server functions -//! .register_server_functions() -//! .fallback(get(render_handler) -//! // Note: ServeConfig::new won't work on WASM -//! .with_state(RenderHandler::new(ServeConfig::new().unwrap(), app)) -//! ) -//! .into_make_service(), -//! ) -//! .await -//! .unwrap(); -//! }); -//! } -//! } -//! -//! fn app() -> Element { -//! let mut text = use_signal(|| "...".to_string()); -//! -//! rsx! { -//! button { -//! onclick: move |_| async move { -//! if let Ok(data) = get_server_data().await { -//! text.set(data); -//! } -//! }, -//! "Run a server function" -//! } -//! "Server said: {text}" -//! } -//! } -//! -//! #[server(GetServerData)] -//! async fn get_server_data() -> Result { -//! Ok("Hello from the server!".to_string()) -//! } -//! -//! # WASM support -//! -//! These utilities compile to the WASM family of targets, while the more complete ones found in [server] don't -//! ``` - -use std::sync::Arc; - -use crate::prelude::*; -use crate::render::SSRError; -use crate::ContextProviders; - +use crate::{render::SSRError, with_server_context, DioxusServerContext, SSRState, ServeConfig}; +use crate::{ContextProviders, ProvideServerContext}; use axum::body; use axum::extract::State; use axum::routing::*; @@ -79,6 +10,126 @@ use axum::{ }; use dioxus_lib::prelude::{Element, VirtualDom}; use http::header::*; +use server_fn::ServerFnTraitObj; +use std::sync::Arc; + +/// A extension trait with utilities for integrating Dioxus with your Axum router. +pub trait DioxusRouterExt: DioxusRouterFnExt { + /// Serves the static WASM for your Dioxus application (except the generated index.html). + /// + /// # Example + /// ```rust, no_run + /// # #![allow(non_snake_case)] + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; + /// #[tokio::main] + /// async fn main() { + /// let addr = dioxus::cli_config::fullstack_address_or_localhost(); + /// let router = axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .serve_static_assets() + /// // Server render the application + /// // ... + /// .into_make_service(); + /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + /// axum::serve(listener, router).await.unwrap(); + /// } + /// ``` + fn serve_static_assets(self) -> Self + where + Self: Sized; + + /// Serves the Dioxus application. This will serve a complete server side rendered application. + /// This will serve static assets, server render the application, register server functions, and integrate with hot reloading. + /// + /// # Example + /// ```rust, no_run + /// # #![allow(non_snake_case)] + /// # use dioxus_lib::prelude::*; + /// # use dioxus_fullstack::prelude::*; + /// #[tokio::main] + /// async fn main() { + /// let addr = dioxus::cli_config::fullstack_address_or_localhost(); + /// let router = axum::Router::new() + /// // Server side render the application, serve static assets, and register server functions + /// .serve_dioxus_application(ServeConfig::new().unwrap(), app) + /// .into_make_service(); + /// let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + /// axum::serve(listener, router).await.unwrap(); + /// } + /// + /// fn app() -> Element { + /// rsx! { "Hello World" } + /// } + /// ``` + fn serve_dioxus_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self + where + Self: Sized; +} + +#[cfg(not(target_arch = "wasm32"))] +impl DioxusRouterExt for Router +where + S: Send + Sync + Clone + 'static, +{ + fn serve_static_assets(mut self) -> Self { + use tower_http::services::{ServeDir, ServeFile}; + + let public_path = crate::public_path(); + + if !public_path.exists() { + return self; + } + + // Serve all files in public folder except index.html + let dir = std::fs::read_dir(&public_path).unwrap_or_else(|e| { + panic!( + "Couldn't read public directory at {:?}: {}", + &public_path, e + ) + }); + + for entry in dir.flatten() { + let path = entry.path(); + if path.ends_with("index.html") { + continue; + } + let route = path + .strip_prefix(&public_path) + .unwrap() + .iter() + .map(|segment| { + segment.to_str().unwrap_or_else(|| { + panic!("Failed to convert path segment {:?} to string", segment) + }) + }) + .collect::>() + .join("/"); + let route = format!("/{}", route); + if path.is_dir() { + self = self.nest_service(&route, ServeDir::new(path).precompressed_br()); + } else { + self = self.nest_service(&route, ServeFile::new(path).precompressed_br()); + } + } + + self + } + + fn serve_dioxus_application(self, cfg: ServeConfig, app: fn() -> Element) -> Self { + // Add server functions and render index.html + let server = self + .serve_static_assets() + .register_server_functions_with_context(cfg.context_providers.clone()); + + let ssr_state = SSRState::new(&cfg); + + server.fallback( + get(render_handler) + .with_state(RenderHandleState::new(cfg, app).with_ssr_state(ssr_state)), + ) + } +} /// A extension trait with server function utilities for integrating Dioxus with your Axum router. pub trait DioxusRouterFnExt { @@ -136,92 +187,98 @@ where mut self, context_providers: ContextProviders, ) -> Self { - use http::method::Method; - - for (path, method) in server_fn::axum::server_fn_paths() { - tracing::trace!("Registering server function: {} {}", method, path); - let context_providers = context_providers.clone(); - let handler = move |req| handle_server_fns_inner(path, context_providers, req); - self = match method { - Method::GET => self.route(path, get(handler)), - Method::POST => self.route(path, post(handler)), - Method::PUT => self.route(path, put(handler)), - _ => unimplemented!("Unsupported server function method: {}", method), - }; + for f in collect_raw_server_fns() { + self = register_server_fn_on_router(f, self, context_providers.clone()); } - self } } +pub fn register_server_fn_on_router( + f: &'static AxumServerFn, + router: Router, + context_providers: ContextProviders, +) -> Router +where + S: Send + Sync + Clone + 'static, +{ + use http::method::Method; + let path = f.path(); + let method = f.method(); + + tracing::trace!("Registering server function: {} {}", method, path); + let handler = move |req| handle_server_fns_inner(f, context_providers, req); + match method { + Method::GET => router.route(path, get(handler)), + Method::POST => router.route(path, post(handler)), + Method::PUT => router.route(path, put(handler)), + _ => unimplemented!("Unsupported server function method: {}", method), + } +} + +pub type AxumServerFn = ServerFnTraitObj, http::Response>; + +pub(crate) fn collect_raw_server_fns() -> Vec<&'static AxumServerFn> { + inventory::iter::().collect() +} + /// A handler for Dioxus server functions. This will run the server function and return the result. async fn handle_server_fns_inner( - path: &str, + f: &AxumServerFn, additional_context: ContextProviders, req: Request, -) -> impl IntoResponse { - let path_string = path.to_string(); - +) -> Response { let (parts, body) = req.into_parts(); let req = Request::from_parts(parts.clone(), body); - let method = req.method().clone(); - - if let Some(mut service) = - server_fn::axum::get_server_fn_service(&path_string, method) - { - // Create the server context with info from the request - let server_context = DioxusServerContext::new(parts); - // Provide additional context from the render state - add_server_context(&server_context, &additional_context); - - // store Accepts and Referrer in case we need them for redirect (below) - let accepts_html = req - .headers() - .get(ACCEPT) - .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("text/html")) - .unwrap_or(false); - let referrer = req.headers().get(REFERER).cloned(); - - // actually run the server fn (which may use the server context) - let fut = with_server_context(server_context.clone(), || service.run(req)); - let mut res = ProvideServerContext::new(fut, server_context.clone()).await; - - // it it accepts text/html (i.e., is a plain form post) and doesn't already have a - // Location set, then redirect to Referer - if accepts_html { - if let Some(referrer) = referrer { - let has_location = res.headers().get(LOCATION).is_some(); - if !has_location { - *res.status_mut() = StatusCode::FOUND; - res.headers_mut().insert(LOCATION, referrer); - } - } - } - // apply the response parts from the server context to the response - server_context.send_response(&mut res); - - Ok(res) - } else { - Response::builder().status(StatusCode::BAD_REQUEST).body( - { - #[cfg(target_family = "wasm")] - { - Body::from(format!( - "No server function found for path: {path_string}\nYou may need to explicitly register the server function with `register_explicit`, rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.", - )) - } - #[cfg(not(target_family = "wasm"))] - { - Body::from(format!( - "No server function found for path: {path_string}\nYou may need to rebuild your wasm binary to update a server function link or make sure the prefix your server and client use for server functions match.", - )) - } - } - ) + // Create the server context with info from the request + let server_context = DioxusServerContext::new(parts); + + // Provide additional context from the render state + add_server_context(&server_context, &additional_context); + + // store Accepts and Referrer in case we need them for redirect (below) + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + let referrer = req.headers().get(REFERER).cloned(); + + // this is taken from server_fn source... + // + // [`server_fn::axum::get_server_fn_service`] + let mut service = { + let middleware = f.middleware(); + let mut service = f.clone().boxed(); + for middleware in middleware { + service = middleware.layer(service); } - .expect("could not build Response") + service + }; + + // actually run the server fn (which may use the server context) + let fut = with_server_context(server_context.clone(), || service.run(req)); + + let mut res = ProvideServerContext::new(fut, server_context.clone()).await; + + // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // Location set, then redirect to Referer + if accepts_html { + if let Some(referrer) = referrer { + let has_location = res.headers().get(LOCATION).is_some(); + if !has_location { + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); + } + } + } + + // apply the response parts from the server context to the response + server_context.send_response(&mut res); + + res } pub(crate) fn add_server_context( diff --git a/packages/fullstack/src/streaming.rs b/packages/server/src/streaming.rs similarity index 100% rename from packages/fullstack/src/streaming.rs rename to packages/server/src/streaming.rs diff --git a/packages/ssr/README.md b/packages/ssr/README.md index a7d1a76c66..5290fa6880 100644 --- a/packages/ssr/README.md +++ b/packages/ssr/README.md @@ -20,7 +20,7 @@ Dioxus SSR provides utilities to render Dioxus components to valid HTML. Once re ```rust # use dioxus::prelude::*; fn app() -> Element { - rsx!{ + rsx! { div {"hello world!"} } } @@ -38,7 +38,7 @@ The simplest example is to simply render some `rsx!` nodes to HTML. This can be ```rust, no_run # use dioxus::prelude::*; -let content = dioxus_ssr::render_element(rsx!{ +let content = dioxus_ssr::render_element(rsx! { div { for i in 0..5 { "Number: {i}" diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 532743cae4..d7e871df72 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -42,6 +42,13 @@ impl Renderer { self.render_components = Some(Arc::new(callback)); } + /// Completely clear the renderer cache and reset the dynamic node id + pub fn clear(&mut self) { + self.template_cache.clear(); + self.dynamic_node_id = 0; + self.render_components = None; + } + /// Reset the callback that the renderer uses to render components pub fn reset_render_components(&mut self) { self.render_components = None; diff --git a/packages/subsecond/README.md b/packages/subsecond/README.md new file mode 100644 index 0000000000..c992652bb6 --- /dev/null +++ b/packages/subsecond/README.md @@ -0,0 +1,76 @@ +# Subsecond + +Subsecond is a hot-reloading library for Rust. It makes it easy to add Rust hot-reloading to your +existing Rust project with minimal integration overhead. + +## Usage: + +For library authors you can use "hot" functions with the `subsecond::current` function: + +```rust +/// A user-facing tick / launch / start function +/// +/// Typically this will be a request/response handler, a game loop, a main function, callback, etc +/// +/// `current` accepts function pointers and Fn types +pub fn tick(handler: Fn(Request) -> Response) { + // Create a "hot" function that we can inspect + let hot_fn = subsecond::current(handler); + + // Check if this function has been patched + if hot_fn.changed() { + // do thing + } + + // Register a handler to be called when the function is patched + hot_fn.on_changed(|| /* do thing */); + + // Call the hot function + hot_fn.call((request)) +} +``` + +For application authors, you can use `subsecond::call()` to make a function hot-reloadable: + +```rust +fn handle_request(request: Request) -> Response { + subsecond::call(|| { + // do_thing... + }) +} +``` + +If a hot function is actively being called, then subsecond will rewind the stack to the "cleanest" entrypoint. For example, a hot-reloadable server will have two "hot" points: at the start of the server, and at the start of the request handler. When the server is reloaded, subsecond will rewind the stack to the first hot point, and then call the function again. + +```rust +// Changes to `serve` will reload the server +fn serve() { + let router = Router::new(); + router.get("/", handle_request); + router.serve("0.0.0.0:8080"); +} + +// Changes below "handle_request" won't be reload the router +fn handle_request(request: Request) -> Response { + // do thing +} +``` + +Framework authors can interleave their own hot-reload entrypoints alongside user code. This lets you add new anchors into long-running stateful code: + +```rust +fn main() { + // serve is "hot" and will rebuild the router if it changes + webserver::serve("0.0.0.0:8080", || { + Router::new() + // get is "hot" and changes to handle_request won't rebuild the router + .get("/", |req| Response::websocket(handle_socket)) + }) +} + +fn handle_socket(ws: &WebSocket) { + subsecond::call(|| { + // do things with the websocket + }) +} +``` diff --git a/packages/subsecond/subsecond-types/Cargo.toml b/packages/subsecond/subsecond-types/Cargo.toml new file mode 100644 index 0000000000..9d4bc33d8d --- /dev/null +++ b/packages/subsecond/subsecond-types/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "subsecond-types" +version.workspace = true +edition = "2021" +authors = ["Jonathan Kelley"] +description = "Types crate for the Subsecond hotpatch engine." +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +keywords = ["hotpatch", "engine", "subsecond", "dioxus"] + + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/packages/subsecond/subsecond-types/src/lib.rs b/packages/subsecond/subsecond-types/src/lib.rs new file mode 100644 index 0000000000..5c6e7f8925 --- /dev/null +++ b/packages/subsecond/subsecond-types/src/lib.rs @@ -0,0 +1,92 @@ +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + hash::{BuildHasherDefault, Hasher}, + path::PathBuf, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct JumpTable { + /// The dylib containing the patch. This should be a valid path so you can just pass it to LibLoading + /// + /// On wasm you will need to fetch() this file and then pass it to the WebAssembly.instantiate() function + pub lib: PathBuf, + + /// old -> new + /// does not take into account the base address of the patch when loaded into memory - need dlopen for that + /// + /// These are intended to be `*const ()` pointers but need to be `u64` for the hashmap. On 32-bit platforms + /// you will need to cast to `usize` before using them. + pub map: AddressMap, + + /// the address of the base address of the old original binary + /// + /// machos: this is the address of the `_mh_execute_header` symbol usually at 0x100000000 and loaded near 0x100000000 + /// linux: this is the address of the `__executable_start` symbol usually at 0x0 but loaded around 0x555555550000 + /// windows: this is the address of the `ImageBase` field of the PE header + /// wasm: not useful since there's no ASLR + /// + /// While we can generally guess that these values are, it's possible they are different and thus reading + /// them dynamically is worthwhile. + pub aslr_reference: u64, + + /// the address of the base address of the new binary + /// + /// machos: this is the address of the `_mh_execute_header` symbol usually at 0x100000000 and loaded near 0x100000000 + /// linux: this is the address of the `__executable_start` symbol usually at 0x0 but loaded around 0x555555550000 + /// windows: this is the address of the `ImageBase` field of the PE header + /// wasm: not useful since there's no ASLR + /// + /// While we can generally guess that these values are, it's possible they are different and thus reading + /// them dynamically is worthwhile. + pub new_base_address: u64, + + /// The amount of ifuncs this will register. This is used by WASM to know how much space to allocate + /// for the ifuncs in the ifunc table + pub ifunc_count: u64, +} + +/// An address to address hashmap that does not hash addresses since addresses are by definition unique. +pub type AddressMap = HashMap; +pub type BuildAddressHasher = BuildHasherDefault; + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct AddressHasher(u64); +impl Hasher for AddressHasher { + fn write(&mut self, _: &[u8]) { + panic!("Invalid use of NoHashHasher") + } + fn write_u8(&mut self, n: u8) { + self.0 = u64::from(n) + } + fn write_u16(&mut self, n: u16) { + self.0 = u64::from(n) + } + fn write_u32(&mut self, n: u32) { + self.0 = u64::from(n) + } + fn write_u64(&mut self, n: u64) { + self.0 = n + } + fn write_usize(&mut self, n: usize) { + self.0 = n as u64 + } + fn write_i8(&mut self, n: i8) { + self.0 = n as u64 + } + fn write_i16(&mut self, n: i16) { + self.0 = n as u64 + } + fn write_i32(&mut self, n: i32) { + self.0 = n as u64 + } + fn write_i64(&mut self, n: i64) { + self.0 = n as u64 + } + fn write_isize(&mut self, n: isize) { + self.0 = n as u64 + } + fn finish(&self) -> u64 { + self.0 + } +} diff --git a/packages/subsecond/subsecond/Cargo.toml b/packages/subsecond/subsecond/Cargo.toml new file mode 100644 index 0000000000..d2e9b9da55 --- /dev/null +++ b/packages/subsecond/subsecond/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "subsecond" +edition = "2021" +version.workspace = true +authors = ["Jonathan Kelley"] +description = "A runtime hotpatching engine for Rust hot-reloading." +license = "MIT OR Apache-2.0" +repository = "https://github.com/DioxusLabs/dioxus/" +keywords = ["hotpatch", "engine", "subsecond", "dioxus", "hot-reload"] + +[dependencies] +serde = { workspace = true, features = ["derive"] } +subsecond-types = { workspace = true } +thiserror = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["FetchEvent", "Request", "Window", "Response", "ResponseType", "console"] } +wasm-bindgen = { workspace = true } +wasm-bindgen-futures = { workspace = true } +js-sys = { workspace = true} + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +libloading = { workspace = true} +libc = { workspace = true} +memmap2 = { workspace = true} + +[target.'cfg(target_os = "android")'.dependencies] +memfd = { workspace = true} diff --git a/packages/subsecond/subsecond/src/lib.rs b/packages/subsecond/subsecond/src/lib.rs new file mode 100644 index 0000000000..63d657d119 --- /dev/null +++ b/packages/subsecond/subsecond/src/lib.rs @@ -0,0 +1,815 @@ +#![allow(clippy::needless_doctest_main)] +//! # Subsecond: Hot-patching for Rust +//! +//! Subsecond is a library that enables hot-patching for Rust applications. This allows you to change +//! the code of a running application without restarting it. This is useful for game engines, servers, +//! and other long-running applications where the typical edit-compile-run cycle is too slow. +//! +//! Subsecond also implements a technique we call "ThinLinking" which makes compiling Rust code +//! significantly faster in development mode, which can be used outside of hot-patching. +//! +//! # Usage +//! +//! Subsecond is designed to be as simple for both application developers and library authors. +//! +//! Simply call your existing functions with [`call`] and Subsecond will automatically detour +//! that call to the latest version of the function. +//! +//! ```rust +//! for x in 0..5 { +//! subsecond::call(|| { +//! println!("Hello, world! {}", x); +//! }); +//! } +//! ``` +//! +//! To actually load patches into your application, a third-party tool that implements the Subsecond +//! compiler and protocol is required. Subsecond is built and maintained by the Dioxus team, so we +//! suggest using the dioxus CLI tool to use subsecond. +//! +//! To install the Dioxus CLI, we recommend using [`cargo binstall`](https://crates.io/crates/cargo-binstall): +//! +//! ```sh +//! cargo binstall dioxus-cli +//! ``` +//! +//! The Dioxus CLI provides several tools for development. To run your application with Subsecond enabled, +//! use `dx serve` - this takes the same arguments as `cargo run` but will automatically hot-reload your +//! application when changes are detected. +//! +//! As of Dioxus 0.7, "--hotpatch" is required to use hotpatching while Subsecond is still experimental. +//! +//! ```sh +//! dx serve --hotpatch +//! ``` +//! +//! ## How it works +//! +//! Subsecond works by detouring function calls through a jump table. This jump table contains the latest +//! version of the program's function pointers, and when a function is called, Subsecond will look up +//! the function in the jump table and call that instead. +//! +//! Unlike libraries like [detour](https://crates.io/crates/detour), Subsecond *does not* modify your +//! process memory. Patching pointers is wildly unsafe and can lead to crashes and undefined behavior. +//! +//! Instead, an external tool compiles only the parts of your project that changed, links them together +//! using the addresses of the functions in your running program, and then sends the new jump table to +//! your application. Subsecond then applies the patch and continues running. Since Subsecond doesn't +//! modify memory, the program must have a runtime integration to handle the patching. +//! +//! If the framework you're using doesn't integrate with subsecond, you can rely on the fact that calls +//! to stale [`call`] instances will emit a safe panic that is automatically caught and retried +//! by the next [`call`] instance up the callstack. +//! +//! Subsecond is only enabled when debug_assertions are enabled so you can safely ship your application +//! with Subsecond enabled without worrying about the performance overhead. +//! +//! ## Globals and statics +//! +//! Subsecond *does* support hot-reloading of globals, statics, and thread locals. However, there are several limitations: +//! +//! - You may add new globals at runtime, but their destructors will never be called. +//! - Globals are tracked across patches, but will renames are considered to be *new* globals. +//! - Changes to static initializers will not be observed. +//! +//! Subsecond purposefully handles statics this way since many libraries like Dioxus and Tokio rely +//! on persistent global runtimes. +//! +//! ## Struct layout and alignment +//! +//! Subsecond currently does not support hot-reloading of structs. This is because the generated code +//! assumes a particular layout and alignment of the struct. If layout or alignment change and new +//! functions are called referencing an old version of the struct, the program will crash. +//! +//! To mitigate this, framework authors can integrate with Subsecond to either dispose of the old struct +//! or to re-allocate the struct in a way that is compatible with the new layout. This is called "re-instancing." +//! +//! Because Subsecond performs a safe panic if a stale function is called, you should never witness +//! a crash due to a struct layout change. However, changing a struct's layout will likely cause a +//! re-instantiation of the struct and potentially a loss of state. +//! +//! We'd like to lift this limitation in the future by providing utilities to re-instantiate structs, +//! but for now it's up to the framework authors to handle this. For example, Dioxus apps simply throw +//! out the old state and rebuild it from scratch. +//! +//! ## Nesting Calls +//! +//! Subsecond calls are designed to be nested. This provides clean integration points to know exactly +//! where a hooked function is called. +//! +//! The highest level call is `fn main()` though by default this is not hooked since initialization code +//! tends to be side-effectual and modify global state. Instead, we recommend wrapping the hot-patch +//! points manually with [`call`]. +//! +//! ```rust +//! fn main() { +//! // Changes to the the `for` loop will cause an unwind to this call. +//! subsecond::call(|| { +//! for x in 0..5 { +//! // Changes to the `println!` will be isolated to this call. +//! subsecond::call(|| { +//! println!("Hello, world! {}", x); +//! }); +//! } +//! }); +//! } +//! ``` +//! +//! The goal here is to provide granular control over where patches are applied to limit loss of state +//! when new code is loaded. +//! +//! ## Applying patches +//! +//! When running under the Dioxus CLI, the `dx serve` command will automatically apply patches when +//! changes are detected. Patches are delivered over the [Dioxus Devtools](https://crates.io/crates/dioxus-devtools) +//! websocket protocol and received by corresponding websocket. +//! +//! If you're using Subsecond in your own application that doesn't have a runtime integration, you can +//! build an integration using the [`apply_patch`] function. This function takes a `JumpTable` which +//! the dioxus-cli crate can generate. +//! +//! To add support for the Dioxus Devtools protocol to your app, you can use the [dioxus-devtools](https://crates.io/crates/dioxus-devtools) +//! crate which provides a `connect` method that will automatically apply patches to your application. +//! +//! Unfortunately, one design quirk of Subsecond is that running apps need to communicate the address +//! of `main` to the patcher. This is due to a security technique called [ASLR](https://en.wikipedia.org/wiki/Address_space_layout_randomization) +//! which randomizes the address of functions in memory. See the subsecond-harness and subsecond-cli +//! for more details on how to implement the protocol. +//! +//! ## ThinLink +//! +//! ThinLink is a program linker for Rust that is designed to be used with Subsecond. It implements +//! the powerful patching system that Subsecond uses to hot-reload Rust applications. +//! +//! ThinLink is simply a wrapper around your existing linker but with extra features: +//! +//! - Automatic dynamic linking to dependencies +//! - Generation of Subsecond jump tables +//! - Diffing of object files for function invalidation +//! +//! Because ThinLink performs very to little actual linking, it drastically speeds up traditional Rust +//! development. With a development-optimized profile, ThinLink can shrink an incremental build to less than 500ms. +//! +//! ThinLink is automatically integrated into the Dioxus CLI though it's currently not available as +//! a standalone tool. +//! +//! ## Limitations +//! +//! Subsecond is a powerful tool but it has several limitations. We talk about them above, but here's +//! a quick summary: +//! +//! - Struct hot reloading requires instancing or unwinding +//! - Statics are tracked but not destructed +//! +//! ## Platform support +//! +//! Subsecond works across all major platforms: +//! +//! - Android (arm64-v8a, armeabi-v7a) +//! - iOS (arm64) +//! - Linux (x86_64, aarch64) +//! - macOS (x86_64, aarch64) +//! - Windows (x86_64, arm64) +//! - WebAssembly (wasm32) +//! +//! If you have a new platform you'd like to see supported, please open an issue on the Subsecond repository. +//! We are keen to add support for new platforms like wasm64, riscv64, and more. +//! +//! Note that iOS device is currently not supported due to code-signing requirements. We hope to fix +//! this in the future, but for now you can use the simulator to test your app. +//! +//! ## Adding the Subsecond badge to your project +//! +//! If you're a framework author and want your users to know that your library supports Subsecond, you +//! can add the Subsecond badge to your README! Users will know that your library is hot-reloadable and +//! can be used with Subsecond. +//! +//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond) +//! +//! ```markdown +//! [![Subsecond](https://img.shields.io/badge/Subsecond-Enabled-orange)](https://crates.io/crates/subsecond) +//! ``` +//! +//! ## License +//! +//! Subsecond and ThinLink are licensed under the MIT license. See the LICENSE file for more information. +//! +//! ## Supporting this work +//! +//! Subsecond is a project by the Dioxus team. If you'd like to support our work, please consider +//! [sponsoring us on GitHub](https://github.com/sponsors/DioxusLabs) or eventually deploying your +//! apps with Dioxus Deploy (currently under construction). + +pub use subsecond_types::JumpTable; + +use std::{ + backtrace, + mem::transmute, + panic::AssertUnwindSafe, + sync::{atomic::AtomicPtr, Arc, Mutex}, +}; + +/// Call a given function with hot-reloading enabled. If the function's code changes, `call` will use +/// the new version of the function. If code *above* the function changes, this will emit a panic +/// that forces an unwind to the next [`call`] instance. +/// +/// WASM/rust does not support unwinding, so [`call`] will not track dependency graph changes. +/// If you are building a framework for use on WASM, you will need to use `Subsecond::HotFn` directly. +/// +/// However, if you wrap your calling code in a future, you *can* simply drop the future which will +/// cause `drop` to execute and get something similar to unwinding. Not great if refcells are open. +pub fn call(mut f: impl FnMut() -> O) -> O { + // Only run in debug mode - the rest of this function will dissolve away + if !cfg!(debug_assertions) { + return f(); + } + + let mut hotfn = HotFn::current(f); + loop { + let res = std::panic::catch_unwind(AssertUnwindSafe(|| hotfn.call(()))); + + // If the call succeeds just return the result, otherwise we try to handle the panic if its our own. + let err = match res { + Ok(res) => return res, + Err(err) => err, + }; + + // If this is our panic then let's handle it, otherwise we just resume unwinding + let Some(_hot_payload) = err.downcast_ref::() else { + std::panic::resume_unwind(err); + }; + } +} + +// We use an AtomicPtr with a leaked JumpTable and Relaxed ordering to give us a global jump table +// with very very little overhead. Reading this amounts of a Relaxed atomic load which basically +// is no overhead. We might want to look into using a thread_local with a stop-the-world approach +// just in case multiple threads try to call the jump table before synchronization with the runtime. +// For Dioxus purposes, this is not a big deal, but for libraries like bevy which heavily rely on +// multithreading, it might become an issue. +static APP_JUMP_TABLE: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); +static HOTRELOAD_HANDLERS: Mutex>> = Mutex::new(Vec::new()); +pub fn register_handler(handler: Arc) { + HOTRELOAD_HANDLERS.lock().unwrap().push(handler); +} +fn get_jump_table() -> Option<&'static JumpTable> { + let ptr = APP_JUMP_TABLE.load(std::sync::atomic::Ordering::Relaxed); + if ptr.is_null() { + return None; + } + + Some(unsafe { &*ptr }) +} +unsafe fn commit_patch(table: JumpTable) { + APP_JUMP_TABLE.store( + Box::into_raw(Box::new(table)), + std::sync::atomic::Ordering::Relaxed, + ); + HOTRELOAD_HANDLERS + .lock() + .unwrap() + .clone() + .iter() + .for_each(|handler| { + handler(); + }); +} + +/// A panic issued by the [`call`] function if the caller would be stale if called. This causes +/// an unwind to the next [`call`] instance that can properly handle the panic and retry the call. +/// +/// This technique allows Subsecond to provide hot-reloading of codebases that don't have a runtime integration. +#[derive(Debug)] +pub struct HotFnPanic { + _backtrace: backtrace::Backtrace, +} + +/// A hot-reloadable function. +/// +/// To call this function, use the [`HotFn::call`] method. This will automatically use the latest +/// version of the function from the JumpTable. +pub struct HotFn +where + F: HotFunction, +{ + inner: F, + _marker: std::marker::PhantomData<(A, M)>, +} + +impl> HotFn { + /// Create a new [`HotFn`] instance with the current function. + /// + /// Whenever you call [`HotFn::call`], it will use the current function from the [`JumpTable`]. + pub const fn current(f: F) -> HotFn { + HotFn { + inner: f, + _marker: std::marker::PhantomData, + } + } + + /// Call the function with the given arguments. + /// + /// This will unwrap the [`HotFnPanic`] panic, propagating up to the next [`HotFn::call`]. + /// + /// If you want to handle the panic yourself, use [`HotFn::try_call`]. + pub fn call(&mut self, args: A) -> F::Return { + self.try_call(args).unwrap() + } + + /// Get the address of the function in memory which might be different than the original. + /// + /// This is useful for implementing a memoization strategy to safely preserve state across + /// hot-patches. If the ptr_address of a function did not change between patches, then the + /// state that exists "above" the function is still valid. + /// + /// Note that Subsecond does not track this state over time, so it's up to the runtime integration + /// to track this state and diff it. + pub fn ptr_address(&self) -> u64 { + if size_of::() == size_of:: ()>() { + let ptr: usize = unsafe { std::mem::transmute_copy(&self.inner) }; + return ptr as u64; + } + + let known_fn_ptr = >::call_it as *const () as usize; + if let Some(jump_table) = get_jump_table() { + if let Some(ptr) = jump_table.map.get(&(known_fn_ptr as u64)).cloned() { + return ptr; + } + } + + known_fn_ptr as u64 + } + + /// Attempt to call the function with the given arguments. + /// + /// If this function is stale and can't be updated in place (ie, changes occurred above this call), + /// then this function will emit an [`HotFnPanic`] which can be unwrapped and handled by next [`call`] + /// instance. + pub fn try_call(&mut self, args: A) -> Result { + if !cfg!(debug_assertions) { + return Ok(self.inner.call_it(args)); + } + + unsafe { + // Try to handle known function pointers. This is *really really* unsafe, but due to how + // rust trait objects work, it's impossible to make an arbitrary usize-sized type implement Fn() + // since that would require a vtable pointer, pushing out the bounds of the pointer size. + if size_of::() == size_of:: ()>() { + return Ok(self.inner.call_as_ptr(args)); + } + + // Handle trait objects. This will occur for sizes other than usize. Normal rust functions + // become ZST's and thus their ::call becomes a function pointer to the function. + // + // For non-zst (trait object) types, then there might be an issue. The real call function + // will likely end up in the vtable and will never be hot-reloaded since signature takes self. + if let Some(jump_table) = get_jump_table() { + let known_fn_ptr = >::call_it as *const () as u64; + if let Some(ptr) = jump_table.map.get(&known_fn_ptr).cloned() { + // The type sig of the cast should match the call_it function + // Technically function pointers need to be aligned, but that alignment is 1 so we're good + let call_it = transmute::<*const (), fn(&F, A) -> F::Return>(ptr as _); + return Ok(call_it(&self.inner, args)); + } + } + + Ok(self.inner.call_it(args)) + } + } +} + +/// Apply the patch using a given jump table. +/// +/// # Safety +/// +/// This function is unsafe because it detours existing functions in memory. This is *wildly* unsafe, +/// especially if the JumpTable is malformed. Only run this if you know what you're doing. +/// +/// If the pointers are incorrect, function type signatures will be incorrect and the program will crash, +/// sometimes in a way that requires a restart of your entire computer. Be careful. +/// +/// # Warning +/// +/// This function will load the library and thus allocates. In cannot be used when the program is +/// stopped (ie in a signal handler). +pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { + // On non-wasm platforms we can just use libloading and the known aslr offsets to load the library + #[cfg(any(unix, windows))] + { + // on android we try to cirumvent permissions issues by copying the library to a memmap and then libloading that + #[cfg(target_os = "android")] + let lib = Box::leak(Box::new(android_memmap_dlopen(&table.lib)?)); + + #[cfg(not(target_os = "android"))] + let lib = Box::leak(Box::new({ + match libloading::Library::new(&table.lib) { + Ok(lib) => lib, + Err(err) => return Err(PatchError::Dlopen(err.to_string())), + } + })); + + // Use the `__aslr_reference` symbol as a sentinel for the current executable. This is basically a + // cross-platform version of `__mh_execute_header` on macOS that sets a reference point for the + // jump table. + let old_offset = __aslr_reference() - table.aslr_reference as usize; + + // Use the `main` symbol as a sentinel for the loaded library. Might want to move away + // from this at some point, or make it configurable + let new_offset = unsafe { + // Leak the library. dlopen is basically a no-op on many platforms and if we even try to drop it, + // some code might be called (ie drop) that results in really bad crashes (restart your computer...) + // + // This code currently assumes "main" always makes it to the export list (which it should) + // and requires coordination from the CLI to export it. + lib.get::<*const ()>(b"main") + .ok() + .unwrap() + .try_as_raw_ptr() + .unwrap() + .wrapping_byte_sub(table.new_base_address as usize) as usize + }; + + // Modify the jump table to be relative to the base address of the loaded library + table.map = table + .map + .iter() + .map(|(k, v)| { + ( + (*k as usize + old_offset) as u64, + (*v as usize + new_offset) as u64, + ) + }) + .collect(); + + commit_patch(table); + }; + + // On wasm, we need to download the module, compile it, and then run it. + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move { + use js_sys::{ + ArrayBuffer, Object, Reflect, + WebAssembly::{self, Memory, Table}, + }; + use wasm_bindgen::prelude::*; + use wasm_bindgen::JsValue; + use wasm_bindgen::UnwrapThrowExt; + use wasm_bindgen_futures::JsFuture; + + let funcs: Table = wasm_bindgen::function_table().unchecked_into(); + let memory: Memory = wasm_bindgen::memory().unchecked_into(); + let exports: Object = wasm_bindgen::exports().unchecked_into(); + let buffer: ArrayBuffer = memory.buffer().unchecked_into(); + + let path = table.lib.to_str().unwrap(); + if !path.ends_with(".wasm") { + return; + } + + // Start the fetch of the module + let response = web_sys::window().unwrap_throw().fetch_with_str(&path); + + // Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory + let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into(); + let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap()) + .await + .unwrap() + .unchecked_into(); + + // Expand the memory and table size to accommodate the new data and functions + // + // Normally we wouldn't be able to trust that we are allocating *enough* memory + // for BSS segments, but ld emits them in the binary when using import-memory. + // + // Make sure we align the memory base to the page size + const PAGE_SIZE: u32 = 64 * 1024; + let page_count = (buffer.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32; + let memory_base = (page_count + 1) * PAGE_SIZE; + + // We need to grow the memory to accommodate the new module + memory.grow((dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1); + + // We grow the ifunc table to accommodate the new functions + // In theory we could just put all the ifuncs in the jump map and use that for our count, + // but there's no guarantee from the jump table that it references "itself" + // We might need a sentinel value for each ifunc in the jump map to indicate that it is + let table_base = funcs.grow(table.ifunc_count as u32).unwrap(); + + // Adjust the jump table to be relative to the new base address + for v in table.map.values_mut() { + *v += table_base as u64; + } + + // Build up the import object. We copy everything over from the current exports, but then + // need to add in the memory and table base offsets for the relocations to work. + // + // let imports = { + // env: { + // memory: base.memory, + // __tls_base: base.__tls_base, + // __stack_pointer: base.__stack_pointer, + // __indirect_function_table: base.__indirect_function_table, + // __memory_base: memory_base, + // __table_base: table_base, + // ..base_exports + // }, + // }; + let env = Object::new(); + + // Move memory, __tls_base, __stack_pointer, __indirect_function_table, and all exports over + for key in Object::keys(&exports) { + Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap(); + } + + // Set the memory and table in the imports + // Following this pattern: Global.new({ value: "i32", mutable: false }, value) + for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] { + let descriptor = Object::new(); + Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap(); + Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap(); + let value = WebAssembly::Global::new(&descriptor, &value.into()).unwrap(); + Reflect::set(&env, &name.into(), &value.into()).unwrap(); + } + + // Set the memory and table in the imports + let imports = Object::new(); + Reflect::set(&imports, &"env".into(), &env).unwrap(); + + // Download the module, returning { module, instance } + // we unwrap here instead of using `?` since this whole thing is async + let result_object = JsFuture::from(WebAssembly::instantiate_module( + dl_bytes.unchecked_ref(), + &imports, + )) + .await + .unwrap(); + + // We need to run the data relocations and then fire off the constructors + let res: Object = result_object.unchecked_into(); + let instance: Object = Reflect::get(&res, &"instance".into()) + .unwrap() + .unchecked_into(); + let exports: Object = Reflect::get(&instance, &"exports".into()) + .unwrap() + .unchecked_into(); + _ = Reflect::get(&exports, &"__wasm_apply_data_relocs".into()) + .unwrap() + .unchecked_into::() + .call0(&JsValue::undefined()); + _ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into()) + .unwrap() + .unchecked_into::() + .call0(&JsValue::undefined()); + _ = Reflect::get(&exports, &"__wasm_call_ctors".into()) + .unwrap() + .unchecked_into::() + .call0(&JsValue::undefined()); + + unsafe { commit_patch(table) }; + }); + + Ok(()) +} + +#[derive(Debug, PartialEq, thiserror::Error)] +pub enum PatchError { + /// The patch failed to apply. + /// + /// This returns a string instead of the Dlopen error type so we don't need to bring the libloading + /// dependency into the public API. + #[error("Failed to load library: {0}")] + Dlopen(String), + + /// The patch failed to apply on Android, most likely due to a permissions issue. + #[error("Failed to load library on Android: {0}")] + AndroidMemfd(String), +} + +/// This function returns its own address, providing a stable reference point for hot-patch engine +/// to hook onto. If you were to write an object file for this function, it would amount to: +/// +/// ```asm +/// __aslr_reference: +/// mov rax, qword ptr [rip + __aslr_reference@GOTPCREL] // notice the @GOTPCREL relocation +/// ret +/// ``` +/// +/// The point here being that we have a stable address both at runtime and compile time, making it +/// possible to calculate the ASLR offset from within the process to correct the jump table. +#[doc(hidden)] +#[inline(never)] +#[no_mangle] +pub extern "C" fn __aslr_reference() -> usize { + __aslr_reference as *const () as usize +} + +/// On Android, we can't dlopen libraries that aren't placed inside /data/data//lib/ +/// +/// If the device isn't rooted, then we can't push the library there. +/// This is a workaround to copy the library to a memfd and then dlopen it. +/// +/// I haven't tested it on device yet, so if if it doesn't work, then we can simply revert to using +/// "adb root" and then pushing the library to the /data/data folder instead of the tmp folder. +/// +/// Android provides us a flag when calling dlopen to use a file descriptor instead of a path, presumably +/// because they want to support this. +/// - https://developer.android.com/ndk/reference/group/libdl +/// - https://developer.android.com/ndk/reference/structandroid/dlextinfo +#[cfg(target_os = "android")] +unsafe fn android_memmap_dlopen(file: &std::path::Path) -> Result { + use std::ffi::{c_void, CStr, CString}; + use std::os::fd::{AsRawFd, BorrowedFd}; + use std::ptr; + + #[repr(C)] + struct ExtInfo { + flags: u64, + reserved_addr: *const c_void, + reserved_size: libc::size_t, + relro_fd: libc::c_int, + library_fd: libc::c_int, + library_fd_offset: libc::off64_t, + library_namespace: *const c_void, + } + + extern "C" { + fn android_dlopen_ext( + filename: *const libc::c_char, + flags: libc::c_int, + ext_info: *const ExtInfo, + ) -> *const c_void; + } + + use memmap2::MmapAsRawDesc; + use std::os::unix::prelude::{FromRawFd, IntoRawFd}; + + let contents = std::fs::read(file) + .map_err(|e| PatchError::AndroidMemfd(format!("Failed to read file: {}", e)))?; + let mut mfd = memfd::MemfdOptions::default() + .create("subsecond-patch") + .map_err(|e| PatchError::AndroidMemfd(format!("Failed to create memfd: {}", e)))?; + mfd.as_file() + .set_len(contents.len() as u64) + .map_err(|e| PatchError::AndroidMemfd(format!("Failed to set memfd length: {}", e)))?; + + let raw_fd = mfd.into_raw_fd(); + + let mut map = memmap2::MmapMut::map_mut(raw_fd) + .map_err(|e| PatchError::AndroidMemfd(format!("Failed to map memfd: {}", e)))?; + map.copy_from_slice(&contents); + let map = map + .make_exec() + .map_err(|e| PatchError::AndroidMemfd(format!("Failed to make memfd executable: {}", e)))?; + + let filename = c"/subsecond-patch"; + + let info = ExtInfo { + flags: 0x10, // ANDROID_DLEXT_USE_LIBRARY_FD + reserved_addr: ptr::null(), + reserved_size: 0, + relro_fd: 0, + library_fd: raw_fd, + library_fd_offset: 0, + library_namespace: ptr::null(), + }; + + let flags = libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL; + + let handle = libloading::os::unix::with_dlerror( + || { + let ptr = android_dlopen_ext(filename.as_ptr() as _, flags, &info); + if ptr.is_null() { + return None; + } else { + return Some(ptr); + } + }, + |err| err.to_str().unwrap_or_default().to_string(), + ) + .map_err(|e| { + PatchError::AndroidMemfd(format!( + "android_dlopen_ext failed: {}", + e.unwrap_or_default() + )) + })?; + + let lib = unsafe { libloading::os::unix::Library::from_raw(handle as *mut c_void) }; + let lib: libloading::Library = lib.into(); + Ok(lib) +} + +/// A trait that enables types to be hot-patched. +/// +/// This trait is only implemented for FnMut types which naturally includes function pointers and +/// closures that can be re-ran. FnOnce closures are currently not supported since the hot-patching +/// system we use implies that the function can be called multiple times. +pub trait HotFunction { + /// The return type of the function. + type Return; + + /// The real function type. This is meant to be a function pointer. + /// When we call `call_as_ptr`, we will transmute the function to this type and call it. + type Real; + + /// Call the HotFunction with the given arguments. + /// + /// # Why + /// + /// "rust-call" isn't stable, so we wrap the underlying call with our own, giving it a stable vtable entry. + /// This is more important than it seems since this function becomes "real" and can be hot-patched. + fn call_it(&mut self, args: Args) -> Self::Return; + + /// Call the HotFunction as if it were a function pointer. + /// + /// # Safety + /// + /// This is only safe if the underlying type is a function (function pointer or virtual/fat pointer). + /// Using this will use the JumpTable to find the patched function and call it. + unsafe fn call_as_ptr(&mut self, _args: Args) -> Self::Return; +} + +macro_rules! impl_hot_function { + ( + $( + ($marker:ident, $($arg:ident),*) + ),* + ) => { + $( + /// A marker type for the function. + /// This is hidden with the intention to seal this trait. + #[doc(hidden)] + pub struct $marker; + + impl HotFunction<($($arg,)*), $marker> for T + where + T: FnMut($($arg),*) -> R, + { + type Return = R; + type Real = fn($($arg),*) -> R; + + fn call_it(&mut self, args: ($($arg,)*)) -> Self::Return { + #[allow(non_snake_case)] + let ( $($arg,)* ) = args; + self($($arg),*) + } + + unsafe fn call_as_ptr(&mut self, args: ($($arg,)*)) -> Self::Return { + unsafe { + if let Some(jump_table) = get_jump_table() { + + + let real = std::mem::transmute_copy::(&self) as *const (); + + // Android implements MTE / pointer tagging and we need to preserve the tag. + // If we leave the tag, then indexing our jump table will fail and patching won't work (or crash!) + // This is only implemented on 64-bit platforms since pointer tagging is not available on 32-bit platforms + // In dev, Dioxus disables MTE to work around this issue, but we still handle it anyways. + #[cfg(all(target_pointer_width = "64", target_os = "android"))] let nibble = real as u64 & 0xFF00_0000_0000_0000; + #[cfg(target_pointer_width = "64")] let real = real as u64 & 0x00FFF_FFF_FFFF_FFFF; + + #[cfg(target_pointer_width = "64")] let real = real as u64; + + // No nibble on 32-bit platforms, but we still need to assume u64 since the host always writes 64-bit addresses + #[cfg(target_pointer_width = "32")] let real = real as u64; + + if let Some(ptr) = jump_table.map.get(&real).cloned() { + // Re-apply the nibble - though this might not be required (we aren't calling malloc for a new pointer) + #[cfg(all(target_pointer_width = "64", target_os = "android"))] let ptr: u64 = ptr | nibble; + + #[cfg(target_pointer_width = "64")] let ptr: u64 = ptr; + #[cfg(target_pointer_width = "32")] let ptr: u32 = ptr as u32; + + // Macro-rules requires unpacking the tuple before we call it + #[allow(non_snake_case)] + let ( $($arg,)* ) = args; + + + #[cfg(target_pointer_width = "64")] + type PtrWidth = u64; + #[cfg(target_pointer_width = "32")] + type PtrWidth = u32; + + return std::mem::transmute::(ptr)($($arg),*); + } + } + + self.call_it(args) + } + } + } + )* + }; +} + +impl_hot_function!( + (Fn0Marker,), + (Fn1Marker, A), + (Fn2Marker, A, B), + (Fn3Marker, A, B, C), + (Fn4Marker, A, B, C, D), + (Fn5Marker, A, B, C, D, E), + (Fn6Marker, A, B, C, D, E, F), + (Fn7Marker, A, B, C, D, E, F, G), + (Fn8Marker, A, B, C, D, E, F, G, H), + (Fn9Marker, A, B, C, D, E, F, G, H, I) +); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7963d38c5f..f3cc44a8a5 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -38,6 +38,7 @@ serde-wasm-bindgen = { version = "0.6.5", optional = true } ciborium = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } +gloo-timers = { workspace = true, optional = true, features = ["futures"] } [dependencies.web-sys] version = "0.3.77" @@ -108,6 +109,7 @@ devtools = [ "dep:serde_json", "dep:serde", "dioxus-core/serialize", + "dep:gloo-timers" ] document = ["dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"] @@ -115,7 +117,6 @@ document = ["dep:serde-wasm-bindgen", "dep:serde_json", "dep:serde"] dioxus = { workspace = true, default-features = true } wasm-bindgen-test = "0.3.50" dioxus-ssr = { workspace = true, default-features = false } -gloo-timers = { workspace = true } gloo-dialogs = { workspace = true } dioxus-web = { path = ".", features = ["hydrate"] } tracing-wasm = { workspace = true } diff --git a/packages/web/src/devtools.rs b/packages/web/src/devtools.rs index 606866c804..caf20e1388 100644 --- a/packages/web/src/devtools.rs +++ b/packages/web/src/devtools.rs @@ -4,13 +4,9 @@ //! We also set up a little recursive timer that will attempt to reconnect if the connection is lost. use std::fmt::Display; -use std::rc::Rc; use std::time::Duration; -use dioxus_core::prelude::RuntimeGuard; -use dioxus_core::{Runtime, ScopeId}; use dioxus_devtools::{DevserverMsg, HotReloadMsg}; -use dioxus_document::eval; use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; use js_sys::JsString; use wasm_bindgen::JsCast; @@ -25,40 +21,36 @@ const POLL_INTERVAL_SCALE_FACTOR: i32 = 2; const TOAST_TIMEOUT: Duration = Duration::from_secs(5); const TOAST_TIMEOUT_LONG: Duration = Duration::from_secs(3600); // Duration::MAX is too long for JS. -pub(crate) fn init(runtime: Rc) -> UnboundedReceiver { +pub(crate) fn init() -> UnboundedReceiver { // Create the tx/rx pair that we'll use for the top-level future in the dioxus loop let (tx, rx) = unbounded(); // Wire up the websocket to the devserver - make_ws(runtime, tx.clone(), POLL_INTERVAL_MIN, false); + make_ws(tx.clone(), POLL_INTERVAL_MIN, false); + playground(tx); rx } -fn make_ws( - runtime: Rc, - tx: UnboundedSender, - poll_interval: i32, - reload: bool, -) { +fn make_ws(tx: UnboundedSender, poll_interval: i32, reload: bool) { // Get the location of the devserver, using the current location plus the /_dioxus path // The idea here being that the devserver is always located on the /_dioxus behind a proxy let location = web_sys::window().unwrap().location(); let url = format!( - "{protocol}//{host}/_dioxus", + "{protocol}//{host}/_dioxus?build_id={build_id}", protocol = match location.protocol().unwrap() { prot if prot == "https:" => "wss:", _ => "ws:", }, host = location.host().unwrap(), + build_id = dioxus_cli_config::build_id(), ); let ws = WebSocket::new(&url).unwrap(); // Set the onmessage handler to bounce messages off to the main dioxus loop let tx_ = tx.clone(); - let runtime_ = runtime.clone(); ws.set_onmessage(Some( Closure::::new(move |e: MessageEvent| { let Ok(text) = e.data().dyn_into::() else { @@ -67,9 +59,9 @@ fn make_ws( // The devserver messages have some &'static strs in them, so we need to leak the source string let string: String = text.into(); - // let leaked: &'static str = Box::leak(Box::new(string)); + let string = Box::leak(string.into_boxed_str()); - match serde_json::from_str::(&string) { + match serde_json::from_str::(string) { Ok(DevserverMsg::HotReload(hr)) => _ = tx_.unbounded_send(hr), // todo: we want to throw a screen here that shows the user that the devserver has disconnected @@ -82,16 +74,24 @@ fn make_ws( // The devserver is telling us that it started a full rebuild. This does not mean that it is ready. Ok(DevserverMsg::FullReloadStart) => show_toast( - runtime_.clone(), "Your app is being rebuilt.", "A non-hot-reloadable change occurred and we must rebuild.", ToastLevel::Info, TOAST_TIMEOUT_LONG, false, ), + + // The devserver is telling us that it started a full rebuild. This does not mean that it is ready. + Ok(DevserverMsg::HotPatchStart) => show_toast( + "Hot-patching app...", + "Hot-patching modified Rust code.", + ToastLevel::Info, + TOAST_TIMEOUT_LONG, + false, + ), + // The devserver is telling us that the full rebuild failed. Ok(DevserverMsg::FullReloadFailed) => show_toast( - runtime_.clone(), "Oops! The build failed.", "We tried to rebuild your app, but something went wrong.", ToastLevel::Error, @@ -102,7 +102,6 @@ fn make_ws( // The devserver is telling us to reload the whole page Ok(DevserverMsg::FullReloadCommand) => { show_toast( - runtime_.clone(), "Successfully rebuilt.", "Your app was rebuilt successfully and without error.", ToastLevel::Success, @@ -115,6 +114,12 @@ fn make_ws( Err(e) => web_sys::console::error_1( &format!("Error parsing devserver message: {}", e).into(), ), + + e => { + web_sys::console::error_1( + &format!("Error parsing devserver message: {:?}", e).into(), + ); + } } }) .into_js_value() @@ -134,13 +139,11 @@ fn make_ws( // set timeout to reload the page in timeout_ms let tx = tx.clone(); - let runtime = runtime.clone(); web_sys::window() .unwrap() .set_timeout_with_callback_and_timeout_and_arguments_0( Closure::::new(move || { make_ws( - runtime.clone(), tx.clone(), POLL_INTERVAL_MAX.min(poll_interval * POLL_INTERVAL_SCALE_FACTOR), true, @@ -162,7 +165,7 @@ fn make_ws( ws.set_onopen(Some( Closure::::new(move |_evt| { if reload { - window().unwrap().location().reload().unwrap() + window().unwrap().location().reload().unwrap(); } }) .into_js_value() @@ -183,7 +186,7 @@ fn make_ws( } /// Represents what color the toast should have. -enum ToastLevel { +pub(crate) enum ToastLevel { /// Green Success, /// Blue @@ -203,8 +206,7 @@ impl Display for ToastLevel { } /// Displays a toast to the developer. -fn show_toast( - runtime: Rc, +pub(crate) fn show_toast( header_text: &str, message: &str, level: ToastLevel, @@ -218,17 +220,13 @@ fn show_toast( false => "showDXToast", }; - // Create the guard before running eval which uses the global runtime context - let _guard = RuntimeGuard::new(runtime); - ScopeId::ROOT.in_runtime(|| { - eval(&format!( - r#" + _ = js_sys::eval(&format!( + r#" if (typeof {js_fn_name} !== "undefined") {{ - {js_fn_name}("{header_text}", "{message}", "{level}", {as_ms}); + window.{js_fn_name}("{header_text}", "{message}", "{level}", {as_ms}); }} "#, - )); - }); + )); } /// Force a hotreload of the assets on this page by walking them and changing their URLs to include diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 81f5b3e192..6d7a9de9ec 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -2,23 +2,9 @@ #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![deny(missing_docs)] -//! Dioxus WebSys -//! -//! ## Overview -//! ------------ -//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser using WebSys. This web render for -//! Dioxus is one of the more advanced renderers, supporting: -//! - idle work -//! - animations -//! - jank-free rendering -//! - controlled components -//! - hydration -//! - and more. -//! -//! The actual implementation is farily thin, with the heavy lifting happening inside the Dioxus Core crate. -//! -//! To purview the examples, check of the root Dioxus crate - the examples in this crate are mostly meant to provide -//! validation of websys-specific features and not the general use of Dioxus. +//! # Dioxus Web + +use std::time::Duration; pub use crate::cfg::Config; use crate::hydration::SuspenseMessage; @@ -69,7 +55,7 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { let runtime = virtual_dom.runtime(); #[cfg(all(feature = "devtools", debug_assertions))] - let mut hotreload_rx = devtools::init(runtime.clone()); + let mut hotreload_rx = devtools::init(); let should_hydrate = web_config.hydrate; @@ -79,6 +65,26 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { None; if should_hydrate { + // If we are hydrating, then the hotreload message might actually have a patch for us to apply. + // Let's wait for a moment to see if we get a hotreload message before we start hydrating. + // That way, the hydration will use the same functions that the server used to serialize the data. + #[cfg(all(feature = "devtools", debug_assertions))] + loop { + let mut timeout = gloo_timers::future::TimeoutFuture::new(100).fuse(); + futures_util::select! { + msg = hotreload_rx.next() => { + if let Some(msg) = msg { + if msg.for_build_id == Some(dioxus_cli_config::build_id()) { + dioxus_devtools::apply_changes(&virtual_dom, &msg); + } + } + } + _ = &mut timeout => { + break; + } + } + } + #[cfg(feature = "hydrate")] { use dioxus_fullstack_protocol::HydrationContext; @@ -205,6 +211,16 @@ pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! { if !hr_msg.assets.is_empty() { crate::devtools::invalidate_browser_asset_cache(); } + + if hr_msg.for_build_id == Some(dioxus_cli_config::build_id()) { + devtools::show_toast( + "Hot-patch success!", + &format!("App successfully patched in {} ms", hr_msg.ms_elapsed), + devtools::ToastLevel::Success, + Duration::from_millis(2000), + false, + ); + } } #[cfg(feature = "hydrate")]