Binary patching rust hot-reloading, sub-second rebuilds, independent server/client hot-reload (#3797)
Enable runtime rust hot patching. * roll back wry to 0.45 * cleanup rustc * wip * wip: fix client/server * get the subsecond cli thing working again * back to list of builds but with accessors * migrate some methods to workspace, clean up impl a bit * pass mode through build context * use a build context, similar to cargo/rustc * merge BuildUpdate and HandleUpdate into BuilderUpdate * Move more resolution of args into runner * migrate some methods to runner * hoist out fullstack * fix build request resolver * yay request is cleaned up * fixup resolution a bit * spawn off the build! * re-wire android autodetection * re-wire android autodetection * wire back up a few more things * re-wire tooling verification * I think it's mostly in the right condition for regular app * add depinfo parser * yay okay works with regular apps again * full rebuilds are back * rewire file wtcher * wire patch! * yayyyyyy patching works and is really fast * yay android works * clean up stdout piping * yayyy memap on android so we don't need root * create a static wasm jump table * wip: global offset table for wasm * wip... thinking about making the patches relocatable * wip: customized relocation * YESSSSSS RELOCATABLE WASM * clean up impl a bit * lil bit more cleanup * lil bit more * lil bit more cleanup * lil bit more cleanup, support 32 bit platforms * sick, wasm is completely pic * hmmmm not quite working yet * woooo, patches loading from rust+wasm * integrated into the cli 😎 * condense a bit * cleaned up request a bit * bust fingerprints * Make the file diffing logic a bit more sensible * still working through a few issues, server launches but is acting weird * fix merge conflict * more merge issues * remove stuff we don't want anymore * revert name change * wip: server not working anymore :( * split apart "dioxus fullstack" into "dioxus server" and "dioxus web" * fixup a few more compile errors * grrr opening server * simultaneous frontend and backend patching * use rustc wrapper * wip: passing assets to jumptable * migrate project-like examples * patchy patchy server fns! * rollback some random changes * unwind the js files * rollback hash * no need for patch * more cleanups * lil bit more cleanup, remove some old cruft * allow patching when wasm webpage is closed * tiny bit more robust * lil bit of clean up * clean up ws code * bit more clean ups * condense * undo file moves * move back other project * move other project * migrate out harness and janky CLI * fix compile * anonymize some files, heavily document others * lots more documentation, for posterity! * ton more docs * clean up the cli a bit more * more ws cleanup * more cleanup * migrate build id to cli-config * add command file handling * random small cleanups * wip.... * wip.... * fix: use workspace for krate discovery * fix panic logging on serve by placing it *after* the logs * reorder logging to be sensible on success * bring back swc * ws cruft * small patches to relocs in loader * bump krates * hoist main funcs into the ifunc table, fixing wasm * fix tui output for long lines * add more linker logging * wow, incredible, we build fat binaries, holy heckkkkk * fix args for frontend/backend * small cleanups * properly send along build IDs to clients * fix workspace compiles * clean up jump table work a bit * clean up logging a bit * open existing * fixup some commands * open existing browser integration * wire up the new dx serve client/server syntax * fixup `dx run` command * bring back some old functionality * fix serverfn feature sets * remove toast animation and simplify its code a bit * less intrusive toast * dont change build status when patching on web * add proper cache busting to vcomponent * clean up the patch file a bit * more lints/checks cleaned up * go back to TargetArgs but a BuildTargets resolved struct * clean up more nits * use an atomicptr for jumptable * fix interaction with initial web load + suspense * don't run asset system on wasm/bindgen js in dev * reduce blast radius of PR * use profile to determine if we're in release * cleanup prod html template * cleanup profiles * fix feature resolution of fullstack * if fullstack is explicitly false, don't use it * light cleanups * drop carg config2 * pass along linker args properly * make workspace examples compile with dx again * fewer unwraps and better error loggign * bit more error handlign * small cleanup * drive-by cleanups * use queries instead of initialize for aslr stuff * fix hotpatch with higher opt level * fix aslr bug from multiple clients * fix merge conflict * fix typos * clippy * fix miscompile * fix doctest * properly rollback wry to 0.45 * fix markdown path issues, other test issues * fix test in router * fix release mode test * fix some more tests, clean up a few more items * use fnptr instead of typeid * clean up memozation via subsecond in core * use "main" as sentinel * fix imports and re-exports * get off __rust_alloc for aslr offset detection * wip * wip... fixing unoptimized web * hmmmm mmmm * close,ish, still missing the wasmbindgen dynaimc imports * aha! full wasm works now - disable externref when compilng patch modules - manually link wasm instrinsics - displace the __wbindgen_placeholder__ table with the synthesized wbg imports what an awful pain. There might be a few items missing, so will test against the docsite shortly * remove failing test * hmmmm things aren't perfect * from what I can tell, relocatable wasm just doesn't work with wasm-bindgen sad https://github.com/rustwasm/wasm-bindgen/issues/1420 * AI IS LITERALLY THE WORST * preserve wasm snippets across compiles * IT HOTPATCHES THE DOCSITE, WE ARE LIVING IN THE FUTURE * remove the macos cfg-out * fix: need to allocate 1 more page * fix url issue * properly span props * properly fill in extra symbols * light cleanups and good docs * delete cruft, better docs * More cleanups and simplifications * clippy, typos * remove harnesses * wasm-bindgen error logging * clean up deps * remove cruft * proper priv/pub * use ifuncs for all env imports * cruft * use par_iter, better output * implement a cache for the base module * delete custom sections * cache the main native module too * add par iter in a few more places * clippy, map ident * better logging and status bars * wip: some small windows support * wip more windowss * fix weird regression * implement windows jump table * small fixes for windows * better windows support * windows support pt2 * rust lld is not msvc * whole archive the rlibs * ignore windows and sys crates * logging * ohhhhhh, pop out dlls * dedupe * thANK GOD FAT LINKING WORKS ON WINDOWS * hoist caching * implement patch based on cache * fix location for pdb * pass data symbols, correct object * add all symbol for windows * add log * treat none rva as undefined * whoops * handle windows calling convention * dont use relocated arm64 stub windows * not all are text? * WINDOWS FUNCTION PATCHING WORKS but issue with statics? * use our own entry * handle windows imp symbols * add log * jump to the real symbol not the layout * whoops * hotpatching windows works completely holy heck yay * drop linker args crate * disable ssg playwright for now * light cleanups * some cleanups / preps: - bump to serverfn 0.8.2 - fix playwright test - fix clippy * use fix from #4042 * bring in other fix for liveview * hoist some configuration * use __aslr_reference instead of aslr_reference * add better errors to patcherror * fix android * panic if patching fails * clean up tomls * wip: fixing wasm compile for server * fullstack wasm again * fix cfg * fixup feature detection * fix issue with features * fix playwright! * add barebones template test harness * fix compatibility between the two hotreload engines * fix preldue of server * fix check for wasm-opt * small fixes
This commit is contained in:
1 parent
71d85e6dd5
commit
d976ca184a
139 files changed
+17857
-7031
No files matched your search
@@ -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
|
||||
Vendored
+8
-3
@@ -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"
|
||||
],
|
||||
}
|
||||
Generated
+508
-251
File diff suppressed because it is too large.
Load diff
+42
-12
@@ -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]
|
||||
|
||||
@@ -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"]
|
||||
@@ -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<PathBuf> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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"] }
|
||||
|
||||
+21
-10
@@ -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"] }
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher"
|
||||
android:extractNativeLibs="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:label="@string/app_name" android:theme="@style/AppTheme">
|
||||
<activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true"
|
||||
android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity">
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
/* Inter Font */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||
|
||||
#dx-toast-template {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dx-toast {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
padding-right: 10px;
|
||||
user-select: none;
|
||||
/* transition: transform 0.2s ease; */
|
||||
z-index: 2147483647;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-inner {
|
||||
/* transition: right 0.2s ease-out; */
|
||||
position: fixed;
|
||||
|
||||
background-color: #181B20;
|
||||
color: #ffffff;
|
||||
font-family: "Inter", sans-serif;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
max-width: 400px;
|
||||
min-height: 56px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-inner {
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-level-bar-container {
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-content {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
align-items: end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header>svg {
|
||||
height: 18px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header .dx-toast-header-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-msg {
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.info {
|
||||
background-color: #428EFF;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.success {
|
||||
background-color: #42FF65;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.error {
|
||||
background-color: #FF4242;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const STORAGE_KEY = "SCHEDULED-DX-TOAST";
|
||||
let currentTimeout = null;
|
||||
let currentToastId = 0;
|
||||
|
||||
// Show a toast, removing the previous one.
|
||||
function showDXToast(headerText, message, progressLevel, durationMs) {
|
||||
document.getElementById("__dx-toast-decor").className = `dx-toast-level-bar ${progressLevel}`;
|
||||
document.getElementById("__dx-toast-text").innerText = headerText;
|
||||
document.getElementById("__dx-toast-msg").innerText = message;
|
||||
document.getElementById("__dx-toast-inner").style.right = "0";
|
||||
document.getElementById("__dx-toast").addEventListener("click", closeDXToast);
|
||||
|
||||
|
||||
// Wait a bit of time so animation plays correctly.
|
||||
setTimeout(
|
||||
() => {
|
||||
let ourToastId = currentToastId;
|
||||
currentTimeout = setTimeout(() => {
|
||||
if (ourToastId == currentToastId) {
|
||||
closeDXToast();
|
||||
}
|
||||
}, durationMs);
|
||||
},
|
||||
100
|
||||
);
|
||||
|
||||
currentToastId += 1;
|
||||
}
|
||||
|
||||
// Schedule a toast to be displayed after reload.
|
||||
function scheduleDXToast(headerText, message, level, durationMs) {
|
||||
let data = {
|
||||
headerText,
|
||||
message,
|
||||
level,
|
||||
durationMs,
|
||||
};
|
||||
|
||||
let jsonData = JSON.stringify(data);
|
||||
sessionStorage.setItem(STORAGE_KEY, jsonData);
|
||||
}
|
||||
|
||||
// Close the current toast.
|
||||
function closeDXToast() {
|
||||
document.getElementById("__dx-toast-inner").style.right = "-1000px";
|
||||
clearTimeout(currentTimeout);
|
||||
}
|
||||
|
||||
// Handle any scheduled toasts after reload.
|
||||
let potentialData = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (potentialData) {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
let data = JSON.parse(potentialData);
|
||||
showDXToast(data.headerText, data.message, data.level, data.durationMs);
|
||||
}
|
||||
|
||||
window.scheduleDXToast = scheduleDXToast;
|
||||
window.showDXToast = showDXToast;
|
||||
window.closeDXToast = closeDXToast;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="__dx-toast" class="dx-toast">
|
||||
<div id="__dx-toast-inner" class="dx-toast-inner" style="right:-1000px;">
|
||||
<div class="dx-toast-level-bar-container">
|
||||
<div id="__dx-toast-decor" class="dx-toast-level-bar __info"></div>
|
||||
</div>
|
||||
<div class="dx-toast-content">
|
||||
<div class="dx-toast-header">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
|
||||
<path d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z" fill="#e96020"/>
|
||||
<path d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z" fill="#2d323b"/>
|
||||
<path d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z" fill="#00a8d6"/>
|
||||
</svg>
|
||||
<h3 id="__dx-toast-text" class="dx-toast-header-text">Your app is being rebuilt.</h3>
|
||||
</div>
|
||||
<p id="__dx-toast-msg" class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
File renamed without changes.
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
Whitespace-only changes.
@@ -0,0 +1,11 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,207 +0,0 @@
|
||||
<style>
|
||||
/* Inter Font */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
||||
|
||||
#dx-toast-template {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dx-toast {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
padding-right: 10px;
|
||||
user-select: none;
|
||||
transition: transform 0.2s ease;
|
||||
z-index: 2147483647;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-inner {
|
||||
transition: right 0.2s ease-out;
|
||||
position: fixed;
|
||||
|
||||
background-color: #181B20;
|
||||
color: #ffffff;
|
||||
font-family: "Inter", sans-serif;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
min-width: 280px;
|
||||
min-height: 92px;
|
||||
width: min-content;
|
||||
border-radius: 5px;
|
||||
|
||||
}
|
||||
|
||||
.dx-toast:hover {
|
||||
cursor: pointer;
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-level-bar-container {
|
||||
height: 100%;
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-level-bar-container .dx-toast-level-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-content {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: start;
|
||||
align-items: end;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header>svg {
|
||||
height: 22px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-header .dx-toast-header-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dx-toast .dx-toast-msg {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.info {
|
||||
background-color: #428EFF;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.success {
|
||||
background-color: #42FF65;
|
||||
}
|
||||
|
||||
.dx-toast-level-bar.error {
|
||||
background-color: #FF4242;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="dx-toast-template" class="dx-toast" hidden>
|
||||
<div class="dx-toast-inner" style="right:-300px;">
|
||||
<!-- Level/Color decor -->
|
||||
<div class="dx-toast-level-bar-container">
|
||||
<div class="dx-toast-level-bar info"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="dx-toast-content">
|
||||
<!-- Header -->
|
||||
<div class="dx-toast-header">
|
||||
<!-- Dioxus Logo -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" preserveAspectRatio="none">
|
||||
<path
|
||||
d="M22.158 1.783c0 3.077-.851 5.482-2.215 7.377s-3.32 3.557-5.447 5.33-4.425 3.657-6.252 6.195-3.102 5.515-3.102 9.532h4.699c0-3.077.853-5.377 2.217-7.272s3.32-3.557 5.447-5.33 4.425-3.657 6.252-6.195 3.102-5.62 3.102-9.637z"
|
||||
fill="#e96020" />
|
||||
<path
|
||||
d="M9.531 25.927c-.635 0-1.021.515-1.02 1.15s.385 1.151 1.02 1.15H22.47a1.151 1.151 0 1 0 0-2.301zm1.361-4.076c-.608 0-.954.558-.953 1.166s.346 1.035.953 1.035h10.217a1.101 1.101 0 1 0 0-2.201zm0-13.594a1.101 1.101 0 1 0 0 2.201h10.217c.607 0 .953-.598.953-1.205s-.345-.996-.953-.996zM9.531 4.021A1.15 1.15 0 0 0 8.38 5.17a1.15 1.15 0 0 0 1.15 1.15h12.94c.635 0 1.021-.498 1.02-1.133s-.386-1.166-1.02-1.166z"
|
||||
fill="#2d323b" />
|
||||
<path
|
||||
d="M5.142 1.783c0 4.016 1.275 7.099 3.102 9.637s4.125 4.422 6.252 6.195 4.083 3.656 5.447 5.551 2.215 3.974 2.215 7.051h4.701c0-4.016-1.275-7.038-3.102-9.576s-4.125-4.422-6.252-6.195-4.083-3.435-5.447-5.33S9.841 4.86 9.841 1.783z"
|
||||
fill="#00a8d6" />
|
||||
</svg>
|
||||
<!-- Toast Title Text -->
|
||||
<h3 class="dx-toast-header-text">Your app is being rebuilt.</h3>
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<p class="dx-toast-msg">A non-hot-reloadable change occurred and we must rebuild.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STORAGE_KEY = "SCHEDULED-DX-TOAST";
|
||||
let currentToast = null;
|
||||
let currentTimeout = null;
|
||||
|
||||
// Show a toast, removing the previous one.
|
||||
function showDXToast(headerText, message, progressLevel, durationMs) {
|
||||
// Close current toast if exists.
|
||||
closeDXToast();
|
||||
|
||||
// Clone template and add unique id.
|
||||
let toastTemplate = document.getElementById("dx-toast-template");
|
||||
let cloned = toastTemplate.cloneNode(true);
|
||||
let toastId = `dx-toast`;
|
||||
cloned.id = toastId;
|
||||
currentToast = cloned;
|
||||
|
||||
let innerElem = currentToast.querySelector(`#${toastId} .dx-toast-inner`);
|
||||
|
||||
// Set the progress level
|
||||
let progressBarElem = innerElem.querySelector(".dx-toast-inner .dx-toast-level-bar-container .dx-toast-level-bar");
|
||||
progressBarElem.className = `dx-toast-level-bar ${progressLevel}`;
|
||||
|
||||
// Set header text
|
||||
let headerTextElem = innerElem.querySelector(".dx-toast-inner .dx-toast-header .dx-toast-header-text");
|
||||
headerTextElem.innerText = headerText;
|
||||
|
||||
// Set message
|
||||
let messageElem = innerElem.querySelector(".dx-toast-inner .dx-toast-msg");
|
||||
messageElem.innerText = message;
|
||||
|
||||
currentToast.removeAttribute("hidden");
|
||||
document.body.appendChild(currentToast);
|
||||
|
||||
// Add listener to close toasts when clicked.
|
||||
// Safety: Calling `closeToast` removes the element and all event listeners with it.
|
||||
currentToast.addEventListener("click", closeDXToast);
|
||||
|
||||
// Wait a bit of time so animation plays correctly.
|
||||
setTimeout(() => {
|
||||
innerElem.style.right = "0";
|
||||
|
||||
currentTimeout = setTimeout(() => {
|
||||
closeDXToast();
|
||||
}, durationMs);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Schedule a toast to be displayed after reload.
|
||||
function scheduleDXToast(headerText, message, level, durationMs) {
|
||||
let data = {
|
||||
headerText,
|
||||
message,
|
||||
level,
|
||||
durationMs,
|
||||
};
|
||||
|
||||
let jsonData = JSON.stringify(data);
|
||||
sessionStorage.setItem(STORAGE_KEY, jsonData);
|
||||
}
|
||||
|
||||
// Close the current toast.
|
||||
function closeDXToast() {
|
||||
if (currentToast) {
|
||||
currentToast.remove();
|
||||
}
|
||||
clearTimeout(currentTimeout);
|
||||
}
|
||||
|
||||
// Handle any scheduled toasts after reload.
|
||||
let potentialData = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (potentialData) {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
let data = JSON.parse(potentialData);
|
||||
showDXToast(data.headerText, data.message, data.level, data.durationMs);
|
||||
}
|
||||
|
||||
</script>
|
||||
+1075
-87
File diff suppressed because it is too large.
Load diff
@@ -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<BuildArtifacts>,
|
||||
}
|
||||
|
||||
#[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<BuildArtifacts>,
|
||||
) -> Result<Self> {
|
||||
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<PathBuf>,
|
||||
) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + 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<PathBuf> {
|
||||
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::<Vec<_>>()
|
||||
.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<String> {
|
||||
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<String> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
@@ -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<BuilderUpdate>;
|
||||
pub type ProgressRx = UnboundedReceiver<BuilderUpdate>;
|
||||
|
||||
#[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<BuilderUpdate>,
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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::<Vec<String>>()
|
||||
.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<String> 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<String> 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::<FuturesUnordered<_>>();
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -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<BuildUpdate>;
|
||||
pub(crate) type ProgressRx = UnboundedReceiver<BuildUpdate>;
|
||||
|
||||
#[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<BuildUpdate>,
|
||||
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
|
||||
}
|
||||
}
|
||||
+3289
-528
File diff suppressed because it is too large.
Load diff
@@ -1,7 +0,0 @@
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct InfoPlistData {
|
||||
pub display_name: String,
|
||||
pub bundle_name: String,
|
||||
pub bundle_identifier: String,
|
||||
pub executable_name: String,
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
use itertools::Itertools;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use target_lexicon::{
|
||||
Aarch64Architecture, Architecture, ArmArchitecture, Environment, OperatingSystem, Triple,
|
||||
X86_32Architecture,
|
||||
};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// The tools for Android (ndk, sdk, etc)
|
||||
///
|
||||
/// https://gist.github.com/Pulimet/5013acf2cd5b28e55036c82c91bd56d8?permalink_comment_id=3678614
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AndroidTools {
|
||||
pub(crate) ndk: PathBuf,
|
||||
pub(crate) adb: PathBuf,
|
||||
pub(crate) java_home: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn get_android_tools() -> Option<Arc<AndroidTools>> {
|
||||
// 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<PathBuf> {
|
||||
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::<Triple>().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<PathBuf> {
|
||||
use std::env::var;
|
||||
use tracing::debug;
|
||||
|
||||
var(name)
|
||||
.inspect_err(|_| debug!("{name} not set"))
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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<String> {
|
||||
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}", "</title", &title, &mut html);
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
fn is_dev_build(&self) -> 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,
|
||||
"<link rel=\"stylesheet\" href=\"{}\">",
|
||||
&style.to_str().unwrap(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add all scripts to the head
|
||||
for script in &script_list {
|
||||
writeln!(
|
||||
&mut head_resources,
|
||||
"<script src=\"{}\"></script>",
|
||||
&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!(
|
||||
"<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Image(image_options) => {
|
||||
if image_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
AssetOptions::Js(js_options) => {
|
||||
if js_options.preloaded() {
|
||||
head_resources.push_str(&format!(
|
||||
"<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// 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!(
|
||||
"<link rel=\"preload\" as=\"fetch\" type=\"application/wasm\" href=\"/{{base_path}}/assets/{wasm_path}\" crossorigin>"
|
||||
));
|
||||
|
||||
replace_or_insert_before("{style_include}", "</head", &head_resources, html);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inject loading scripts if they are not already present
|
||||
fn inject_loading_scripts(&self, html: &mut String) {
|
||||
// If it looks like we are already loading wasm or the current build opted out of injecting loading scripts, don't inject anything
|
||||
if !self.build.build.inject_loading_scripts || html.contains("__wbindgen_start") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, insert the script
|
||||
*html = html.replace(
|
||||
"</body",
|
||||
r#" <script>
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{DX_TOAST_UTILITIES}
|
||||
</body"#,
|
||||
);
|
||||
|
||||
// Trim out the toasts if we're in release, or add them if we're serving
|
||||
*html = match self.is_dev_build() {
|
||||
true => 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<PathBuf>, 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::<Vec<_>>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<StructuredOutput> {
|
||||
pub(crate) async fn autoformat(self) -> Result<StructuredOutput> {
|
||||
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<Path>,
|
||||
) -> 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();
|
||||
}
|
||||
+132
-162
@@ -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)]
|
||||
#[serde(default)]
|
||||
pub(crate) force_sequential: bool,
|
||||
pub(crate) fullstack: Option<bool>,
|
||||
|
||||
/// Build the app with custom a profile
|
||||
/// The feature to use for the client in a fullstack app [default: "web"]
|
||||
#[clap(long)]
|
||||
pub(crate) profile: Option<String>,
|
||||
pub(crate) client_features: Vec<String>,
|
||||
|
||||
/// The feature to use for the server in a fullstack app [default: "server"]
|
||||
#[clap(long)]
|
||||
pub(crate) server_features: Vec<String>,
|
||||
|
||||
/// 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<Platform>,
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
/// Run the ssg config of the app and generate the files
|
||||
/// 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)]
|
||||
pub(crate) ssg: bool,
|
||||
pub(crate) server_target: Option<Triple>,
|
||||
|
||||
/// Skip collecting assets from dependencies [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub(crate) skip_assets: bool,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub(crate) cargo_args: Vec<String>,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Information about the target to build
|
||||
/// Arguments for the build itself
|
||||
#[clap(flatten)]
|
||||
pub(crate) target_args: TargetArgs,
|
||||
pub(crate) build_arguments: TargetArgs,
|
||||
|
||||
/// A list of additional targets to build.
|
||||
///
|
||||
/// 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<TargetCmd>,
|
||||
}
|
||||
|
||||
pub struct BuildTargets {
|
||||
pub client: BuildRequest,
|
||||
pub server: Option<BuildRequest>,
|
||||
}
|
||||
|
||||
impl BuildArgs {
|
||||
pub async fn run_cmd(mut self) -> Result<StructuredOutput> {
|
||||
pub async fn build(self) -> Result<StructuredOutput> {
|
||||
tracing::info!("Building project...");
|
||||
|
||||
let krate =
|
||||
DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?;
|
||||
let targets = self.into_targets().await?;
|
||||
|
||||
self.resolve(&krate).await?;
|
||||
AppBuilder::start(&targets.client, BuildMode::Base)?
|
||||
.finish_build()
|
||||
.await?;
|
||||
|
||||
let bundle = Builder::start(&krate, self.clone())?.finish().await?;
|
||||
tracing::info!(path = ?targets.client.root_dir(), "Client build completed successfully! 🚀");
|
||||
|
||||
tracing::info!(path = ?bundle.build.root_dir(), "Build completed successfully! 🚀");
|
||||
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?;
|
||||
|
||||
Ok(StructuredOutput::BuildFinished {
|
||||
path: bundle.build.root_dir(),
|
||||
tracing::info!(path = ?targets.client.root_dir(), "Server build completed successfully! 🚀");
|
||||
}
|
||||
|
||||
Ok(StructuredOutput::BuildsFinished {
|
||||
client: targets.client.root_dir(),
|
||||
server: targets.server.map(|s| s.root_dir()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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();
|
||||
pub async fn into_targets(self) -> Result<BuildTargets> {
|
||||
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 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());
|
||||
}
|
||||
|
||||
// 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 <platform>` or set a default platform using a cargo feature.")
|
||||
})?;
|
||||
self.platform = Some(platform);
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
//
|
||||
// todo(jon): eventually we want to let users pass a `--server <crate>` 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() {
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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());
|
||||
server = Some(_server);
|
||||
}
|
||||
Some(Platform::Web) => {
|
||||
self.profile = Some(crate::dioxus_crate::PROFILE_WASM.to_string());
|
||||
|
||||
client
|
||||
}
|
||||
Some(Platform::Server) => {
|
||||
self.profile = Some(crate::dioxus_crate::PROFILE_SERVER.to_string());
|
||||
|
||||
// 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// Some extra logs
|
||||
let arch = match arch {
|
||||
Some(a) => {
|
||||
tracing::debug!(
|
||||
"Autodetected `{}` Android arch.",
|
||||
a.android_target_triplet()
|
||||
);
|
||||
a.to_owned()
|
||||
if let Some(server_args) = server_args_ {
|
||||
server = Some(BuildRequest::new(&server_args, workspace.clone()).await?);
|
||||
}
|
||||
None => {
|
||||
let a = Arch::default();
|
||||
tracing::debug!(
|
||||
"Could not detect Android arch, defaulting to `{}`",
|
||||
a.android_target_triplet()
|
||||
);
|
||||
a
|
||||
|
||||
BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await?
|
||||
}
|
||||
};
|
||||
|
||||
self.target_args.arch = Some(arch);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the platform from the build arguments
|
||||
pub(crate) fn platform(&self) -> Platform {
|
||||
self.platform.expect("Platform was not set")
|
||||
}
|
||||
|
||||
/// Check if this is a fullstack build
|
||||
pub(crate) fn fullstack(&self) -> bool {
|
||||
self.fullstack.unwrap_or(false)
|
||||
Ok(BuildTargets { client, server })
|
||||
}
|
||||
}
|
||||
+191
-46
@@ -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<PathBuf>,
|
||||
|
||||
/// 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<StructuredOutput> {
|
||||
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?;
|
||||
|
||||
tracing::info!("Building app...");
|
||||
|
||||
let bundle = Builder::start(&krate, self.build_arguments.clone())?
|
||||
.finish()
|
||||
AppBuilder::start(&client, BuildMode::Base)?
|
||||
.finish_build()
|
||||
.await?;
|
||||
|
||||
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
|
||||
|
||||
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<Vec<crate::PackageType>>,
|
||||
) -> Result<Vec<tauri_bundler::Bundle>, 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),
|
||||
)
|
||||
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::<Vec<String>>()
|
||||
.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<String> 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<String> 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::<FuturesUnordered<_>>();
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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<StructuredOutput> {
|
||||
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<PathBuf>) -> Result<()> {
|
||||
_ => Err(format!("{} issues found.", total_issues).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ impl Clean {
|
||||
pub(crate) async fn clean(self) -> Result<StructuredOutput> {
|
||||
let output = tokio::process::Command::new("cargo")
|
||||
.arg("clean")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -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<BoolValue> for bool {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn config(self) -> Result<StructuredOutput> {
|
||||
let crate_root = crate_root()?;
|
||||
pub(crate) async fn config(self) -> Result<StructuredOutput> {
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
+204
-57
@@ -1,64 +1,227 @@
|
||||
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<String>,
|
||||
},
|
||||
/// `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<PathBuf>,
|
||||
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<Self> {
|
||||
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;
|
||||
}
|
||||
|
||||
pub(crate) fn to_json(&self) -> String {
|
||||
serde_json::to_string(self).unwrap()
|
||||
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 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_linker_command_file(&mut args);
|
||||
|
||||
// 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 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<String>) {
|
||||
// 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('@');
|
||||
@@ -76,30 +239,14 @@ impl LinkAction {
|
||||
});
|
||||
|
||||
// Gather linker args, and reset the args to be just the linker args
|
||||
args = content
|
||||
*args = content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let line_parsed = line.to_string();
|
||||
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();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let contents = serde_json::to_string(&manifest).expect("Failed to write manifest");
|
||||
std::fs::write(dest, contents).expect("Failed to write output file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
+110
-46
@@ -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<StructuredOutput> {
|
||||
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);
|
||||
|
||||
let bundle = Builder::start(&krate, self.build_args.clone())?
|
||||
.finish()
|
||||
.await?;
|
||||
|
||||
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;
|
||||
|
||||
if self.build_args.platform() == Platform::Web || self.build_args.fullstack() {
|
||||
open_address = Some(fullstack_ip);
|
||||
tracing::info!("Serving at: {}", fullstack_ip);
|
||||
}
|
||||
|
||||
let mut runner = crate::serve::AppRunner::start(&krate);
|
||||
runner
|
||||
.open(bundle, devserver_ip, open_address, Some(fullstack_ip), true)
|
||||
.await?;
|
||||
|
||||
// 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}")
|
||||
let msg = tokio::select! {
|
||||
msg = builder.wait() => msg,
|
||||
msg = devserver.wait() => msg,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// And then update the websocketed clients with the new build status in case they want it
|
||||
devserver.new_build_update(&update).await;
|
||||
|
||||
// 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));
|
||||
|
||||
if platform == Platform::Web {
|
||||
tracing::info!(
|
||||
"Serving app at http://{}:{}",
|
||||
builder.devserver_bind_ip,
|
||||
builder.devserver_port
|
||||
);
|
||||
}
|
||||
ServeUpdate::StdoutReceived { platform, msg } => {
|
||||
tracing::info!("[{platform}]: {msg}")
|
||||
}
|
||||
ServeUpdate::ProcessExited { platform, status } => {
|
||||
runner.cleanup().await;
|
||||
tracing::info!("[{platform}]: process exited with status: {status:?}");
|
||||
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}"
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
ServeUpdate::BuildUpdate { .. } => {}
|
||||
ServeUpdate::TracingLog { .. } => {}
|
||||
ServeUpdate::Exit { .. } => break,
|
||||
ServeUpdate::NewConnection => {}
|
||||
ServeUpdate::WsMessage(_) => {}
|
||||
ServeUpdate::FilesChanged { .. } => {}
|
||||
ServeUpdate::RequestRebuild => {}
|
||||
ServeUpdate::Redraw => {}
|
||||
ServeUpdate::OpenApp => {}
|
||||
ServeUpdate::ToggleShouldRebuild => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+123
-54
@@ -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<bool>,
|
||||
|
||||
/// 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<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]
|
||||
#[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<StructuredOutput> {
|
||||
crate::serve::serve_all(self).await?;
|
||||
|
||||
Ok(StructuredOutput::Success)
|
||||
if std::env::var("RUST_BACKTRACE").is_err() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
pub(crate) async fn load_krate(&mut self) -> Result<DioxusCrate> {
|
||||
let krate = DioxusCrate::new(&self.build_arguments.target_args)?;
|
||||
self.resolve(&krate).await?;
|
||||
Ok(krate)
|
||||
struct SavedLocation {
|
||||
file: String,
|
||||
line: u32,
|
||||
column: u32,
|
||||
}
|
||||
static BACKTRACE: OnceCell<(Backtrace, Option<SavedLocation>)> = 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();
|
||||
}
|
||||
|
||||
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));
|
||||
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::<String>() {
|
||||
p.as_ref()
|
||||
} else if let Some(p) = panic_err.downcast_ref::<&str>() {
|
||||
p
|
||||
} else {
|
||||
"<unknown panic>"
|
||||
};
|
||||
|
||||
// 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(|| "<unknown>".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();
|
||||
}
|
||||
|
||||
// Open browser.
|
||||
if self.open.is_none() {
|
||||
self.open = Some(krate.settings.always_open_browser.unwrap_or_default());
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Resolve the build arguments
|
||||
self.build_arguments.resolve(krate).await?;
|
||||
format!("dx serve panicked at {location_display}\n{as_str}\n{backtrace_display} ___rust_try")
|
||||
})
|
||||
.unwrap_or_else(|| format!("dx serve panicked: {as_str}"));
|
||||
|
||||
Ok(())
|
||||
Err(crate::error::Error::CapturedPanic(message))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
+132
-200
@@ -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 platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long, value_enum)]
|
||||
pub(crate) platform: Option<Platform>,
|
||||
|
||||
/// Build a example [default: ""]
|
||||
#[clap(long)]
|
||||
pub(crate) example: Option<String>,
|
||||
|
||||
/// Build a binary [default: ""]
|
||||
#[clap(long)]
|
||||
pub(crate) bin: Option<String>,
|
||||
/// 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<String>,
|
||||
|
||||
/// Build a specific binary [default: ""]
|
||||
#[clap(long)]
|
||||
pub(crate) bin: Option<String>,
|
||||
|
||||
/// Build a specific example [default: ""]
|
||||
#[clap(long)]
|
||||
pub(crate) example: Option<String>,
|
||||
|
||||
/// Build the app with custom a profile
|
||||
#[clap(long)]
|
||||
pub(crate) profile: Option<String>,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub(crate) features: Vec<String>,
|
||||
|
||||
/// The feature to use for the client in a fullstack app [default: "web"]
|
||||
#[clap(long)]
|
||||
pub(crate) client_features: Vec<String>,
|
||||
|
||||
/// The feature to use for the server in a fullstack app [default: "server"]
|
||||
#[clap(long)]
|
||||
pub(crate) server_features: Vec<String>,
|
||||
|
||||
/// 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<Arch>,
|
||||
/// Include all features in the build
|
||||
#[clap(long)]
|
||||
pub(crate) all_features: bool,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub(crate) target: Option<Triple>,
|
||||
|
||||
/// 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) cargo_args: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Skip collecting assets from dependencies [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub(crate) skip_assets: bool,
|
||||
|
||||
/// 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) wasm_split: bool,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub(crate) target: Option<String>,
|
||||
|
||||
/// Specify the server rustc target triple.
|
||||
#[clap(long)]
|
||||
pub(crate) server_target: Option<String>,
|
||||
}
|
||||
|
||||
impl TargetArgs {
|
||||
pub(crate) fn arch(&self) -> Arch {
|
||||
self.arch.unwrap_or_default()
|
||||
/// 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<TargetArgs, TargetCmd>),
|
||||
|
||||
/// Specify the arguments for the server build
|
||||
#[clap(name = "server")]
|
||||
Server(ChainedCommand<TargetArgs, TargetCmd>),
|
||||
}
|
||||
|
||||
// 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<T, U> {
|
||||
/// Specific Variant.
|
||||
pub inner: T,
|
||||
|
||||
/// Enum containing `Self<T>` variants, in other words possible follow-up commands.
|
||||
pub next: Option<Box<U>>,
|
||||
}
|
||||
|
||||
impl<T, U> Args for ChainedCommand<T, U>
|
||||
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)))
|
||||
}
|
||||
|
||||
fn augment_args_for_update(_cmd: clap::Command) -> clap::Command {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[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<T, U> FromArgMatches for ChainedCommand<T, U>
|
||||
where
|
||||
T: Args,
|
||||
U: Subcommand,
|
||||
{
|
||||
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
|
||||
// Parse the first command before we try to parse the next one.
|
||||
let inner = T::from_arg_matches(matches)?;
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
//
|
||||
// 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`
|
||||
//
|
||||
// we are done, since sub-sub commands are matched in U::
|
||||
Some(_) => Some(Box::new(U::from_arg_matches(matches)?)),
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
unimplemented!("Unsupported target os for android toolchain autodetection")
|
||||
}
|
||||
|
||||
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 {
|
||||
""
|
||||
// no subcommand matched, we are done
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.android_tools_dir(ndk)
|
||||
.join(format!("{}24-clang{}", triplet, suffix))
|
||||
Ok(Self { inner, next })
|
||||
}
|
||||
|
||||
pub(crate) fn android_min_sdk_version(&self) -> u32 {
|
||||
// todo(jon): this should be configurable
|
||||
24
|
||||
}
|
||||
|
||||
pub(crate) fn android_ar_path(&self, ndk: &Path) -> PathBuf {
|
||||
self.android_tools_dir(ndk).join("llvm-ar")
|
||||
}
|
||||
|
||||
pub(crate) fn target_cc(&self, ndk: &Path) -> PathBuf {
|
||||
self.android_tools_dir(ndk).join("clang")
|
||||
}
|
||||
|
||||
pub(crate) fn target_cxx(&self, ndk: &Path) -> PathBuf {
|
||||
self.android_tools_dir(ndk).join("clang++")
|
||||
}
|
||||
|
||||
pub(crate) fn java_home(&self) -> Option<PathBuf> {
|
||||
// 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
|
||||
//
|
||||
// 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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Arch {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
match value.as_str() {
|
||||
"armv7l" => Ok(Self::Arm),
|
||||
"aarch64" => Ok(Self::Arm64),
|
||||
"i386" => Ok(Self::X86),
|
||||
"x86_64" => Ok(Self::X64),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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!()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ApplicationConfig {
|
||||
#[serde(default)]
|
||||
pub(crate) asset_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
|
||||
@@ -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<Option<Self>> {
|
||||
// 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::<DioxusConfig>(&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)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -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)]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<Krates>,
|
||||
pub(crate) package: NodeId,
|
||||
pub(crate) config: DioxusConfig,
|
||||
pub(crate) target: Target,
|
||||
pub(crate) settings: Arc<CliSettings>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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::<Vec<_>>();
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
if manually_enabled_platforms.len() > 1 {
|
||||
tracing::error!("Multiple platforms are enabled. Please specify a platform with `--platform <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::<Vec<_>>();
|
||||
|
||||
if auto_implicit.len() == 1 {
|
||||
Some((auto_implicit.first().copied().unwrap(), feature.to_string()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(|platform| platform.0 != Platform::Server)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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::<toml_edit::DocumentMut>().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<String> {
|
||||
self.krates
|
||||
.krates_by_name("wasm-bindgen")
|
||||
.next()
|
||||
.map(|krate| krate.krate.version.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn default_platforms(&self) -> Vec<Platform> {
|
||||
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<String> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
let mut krates: Vec<PathBuf> = 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<PathBuf> = 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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
// "/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<Option<PathBuf>> = 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<Arch> {
|
||||
// Try auto detecting arch through adb.
|
||||
static AUTO_ARCH: OnceCell<Option<Arch>> = 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<String>) -> Result<NodeId> {
|
||||
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::<Vec<_>>();
|
||||
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<PathBuf> {
|
||||
use std::env::var;
|
||||
use tracing::debug;
|
||||
|
||||
var(name)
|
||||
.inspect_err(|_| debug!("{name} not set"))
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::metadata::CargoError;
|
||||
use std::fmt::Debug;
|
||||
use thiserror::Error as ThisError;
|
||||
|
||||
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
@@ -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),
|
||||
|
||||
@@ -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<PathBuf, CachedFile>,
|
||||
}
|
||||
|
||||
struct CachedFile {
|
||||
contents: String,
|
||||
most_recent: Option<String>,
|
||||
templates: HashMap<TemplateGlobalKey, HotReloadedTemplate>,
|
||||
}
|
||||
|
||||
pub enum HotreloadResult {
|
||||
Rsx(Vec<HotReloadTemplateWithLocation>),
|
||||
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<Ctx: HotReloadingContext>(
|
||||
&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::<CallBody>(old.tokens);
|
||||
let new_parsed = syn::parse2::<CallBody>(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::<Vec<_>>()
|
||||
.join("/");
|
||||
|
||||
// Returns a list of templates that are hotreloadable
|
||||
let results = HotReloadResult::new::<Ctx>(
|
||||
&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)
|
||||
}
|
||||
}
|
||||
@@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-16
@@ -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:?}"),
|
||||
},
|
||||
|
||||
@@ -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<PathBuf, CargoError> {
|
||||
// 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<Path>) -> Vec<PathBuf> {
|
||||
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
|
||||
}
|
||||
@@ -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<RustcDetails> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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::<Vec<_>>(),
|
||||
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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<Child>,
|
||||
pub(crate) server_child: Option<Child>,
|
||||
|
||||
// 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<Lines<BufReader<ChildStdout>>>,
|
||||
pub(crate) app_stderr: Option<Lines<BufReader<ChildStderr>>>,
|
||||
pub(crate) server_stdout: Option<Lines<BufReader<ChildStdout>>>,
|
||||
pub(crate) server_stderr: Option<Lines<BufReader<ChildStderr>>>,
|
||||
|
||||
/// 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<PathBuf>,
|
||||
pub(crate) entropy_server_exe: Option<PathBuf>,
|
||||
|
||||
/// 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<PathBuf>,
|
||||
}
|
||||
|
||||
impl AppHandle {
|
||||
pub async fn new(app: AppBundle) -> Result<Self> {
|
||||
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<SocketAddr>,
|
||||
start_fullstack_on_address: Option<SocketAddr>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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<PathBuf> {
|
||||
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<Child> {
|
||||
// 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<Child> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
// xcrun devicectl device install app --device <uuid> --path <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<usize> {
|
||||
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"<plist").context("Failed to parse .mobileprovision file")?;
|
||||
let cut2 = cut_plist(&bytes, r#"</dict>"#.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<String>,
|
||||
#[serde(rename = "ApplicationIdentifierPrefix")]
|
||||
application_identifier_prefix: Vec<String>,
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
let entielements_xml = format!(
|
||||
r#"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>application-identifier</key>
|
||||
<string>{APPLICATION_IDENTIFIER}</string>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>{APP_ID_ACCESS_GROUP}.*</string>
|
||||
</array>
|
||||
<key>get-task-allow</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>{TEAM_IDENTIFIER}</string>
|
||||
</dict></plist>
|
||||
"#,
|
||||
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::<Vec<_>>()
|
||||
.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<PathBuf> {
|
||||
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
|
||||
}
|
||||
}
|
||||
+121
-168
@@ -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,7 +18,7 @@ pub(crate) use watcher::*;
|
||||
///
|
||||
/// Platform specifics:
|
||||
/// -------------------
|
||||
/// - Web: we need to attach a filesystem server to our devtools webserver to serve the project. We
|
||||
/// - 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.
|
||||
@@ -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();
|
||||
|
||||
// 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the server in the background
|
||||
// Waiting for updates here lets us tap into when clients are added/removed
|
||||
ServeUpdate::NewConnection => {
|
||||
devserver
|
||||
.send_hotreload(runner.applied_hot_reload_changes())
|
||||
.await;
|
||||
|
||||
runner.client_connected().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);
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
// 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;
|
||||
|
||||
// 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 } => {
|
||||
screen.push_cargo_log(message);
|
||||
}
|
||||
BuildUpdate::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;
|
||||
}
|
||||
|
||||
Err(e) => tracing::error!("Failed to open app: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
tracing::debug!("Starting hotpatching: {:?}", files);
|
||||
builder.handle_file_change(&files, &mut devserver).await;
|
||||
}
|
||||
|
||||
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.rebuild(args.build_arguments.clone());
|
||||
runner.file_map.force_rebuild();
|
||||
builder.full_rebuild().await;
|
||||
devserver.send_reload_start().await;
|
||||
devserver.start_build().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 { id, aslr_reference } => {
|
||||
// Send the client
|
||||
devserver
|
||||
.send_hotreload(builder.applied_hot_reload_changes(BuildId::CLIENT))
|
||||
.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, 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::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).await;
|
||||
|
||||
// And then open the app if it's ready
|
||||
match update {
|
||||
BuilderUpdate::Progress { .. } => {}
|
||||
BuilderUpdate::CompilerMessage { message } => {
|
||||
screen.push_cargo_log(message);
|
||||
}
|
||||
BuilderUpdate::BuildFailed { err } => {
|
||||
tracing::error!("Build failed: {:#?}", err);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServeUpdate::TracingLog { log } => {
|
||||
screen.push_log(log);
|
||||
}
|
||||
|
||||
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(()),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
_ = runner.cleanup().await;
|
||||
ServeUpdate::Exit { error } => {
|
||||
_ = builder.cleanup_all().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(())
|
||||
}
|
||||
@@ -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<throbber_widgets_tui::ThrobberState>,
|
||||
|
||||
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<Self> {
|
||||
pub(crate) async fn start(interactive: bool) -> crate::Result<Self> {
|
||||
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::<ClientMsg>(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<Duration>,
|
||||
) {
|
||||
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<String> = state.build_engine.request.all_target_features();
|
||||
let feature_list: Vec<String> = 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::<u16>();
|
||||
|
||||
@@ -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();
|
||||
if line_length < u16::MAX as usize {
|
||||
lines.push(AnsiStringLine::new(line_length as _).render(&line));
|
||||
} else {
|
||||
lines.push(line.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+940
-229
File diff suppressed because it is too large.
Load diff
+118
-122
@@ -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<u16>,
|
||||
hot_reload_sockets: Vec<WebSocket>,
|
||||
build_status_sockets: Vec<WebSocket>,
|
||||
new_hot_reload_sockets: UnboundedReceiver<WebSocket>,
|
||||
new_build_status_sockets: UnboundedReceiver<WebSocket>,
|
||||
hot_reload_sockets: Vec<ConnectedWsClient>,
|
||||
build_status_sockets: Vec<ConnectedWsClient>,
|
||||
new_hot_reload_sockets: UnboundedReceiver<ConnectedWsClient>,
|
||||
new_build_status_sockets: UnboundedReceiver<ConnectedWsClient>,
|
||||
build_status: SharedStatus,
|
||||
application_name: String,
|
||||
platform: Platform,
|
||||
}
|
||||
|
||||
pub(crate) struct ConnectedWsClient {
|
||||
socket: WebSocket,
|
||||
build_id: Option<BuildId>,
|
||||
aslr_reference: Option<u64>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
pub(crate) fn start(runner: &AppServer) -> Result<Self> {
|
||||
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::<FuturesUnordered<_>>();
|
||||
|
||||
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::<ServeUpdate>().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<WebSocket>,
|
||||
build_status_sockets: UnboundedSender<WebSocket>,
|
||||
runner: &AppServer,
|
||||
hot_reload_sockets: UnboundedSender<ConnectedWsClient>,
|
||||
build_status_sockets: UnboundedSender<ConnectedWsClient>,
|
||||
fullstack_address: Option<SocketAddr>,
|
||||
build_status: SharedStatus,
|
||||
) -> Result<Router> {
|
||||
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();
|
||||
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<u64>,
|
||||
build_id: Option<BuildId>,
|
||||
}
|
||||
|
||||
// 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<UnboundedSender<WebSocket>>| async move {
|
||||
tracing::debug!("New devtool websocket connection");
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) })
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>, query: Query<ConnectionQuery>| 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<UnboundedSender<WebSocket>>| async move {
|
||||
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) })
|
||||
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>| 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<u16>) -> Option<u16> {
|
||||
// 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
|
||||
|
||||
@@ -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 {
|
||||
NewConnection {
|
||||
id: BuildId,
|
||||
aslr_reference: Option<u64>,
|
||||
},
|
||||
WsMessage {
|
||||
platform: Platform,
|
||||
msg: String,
|
||||
msg: WsMessage,
|
||||
},
|
||||
|
||||
/// 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 {
|
||||
platform: Platform,
|
||||
msg: String,
|
||||
},
|
||||
|
||||
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<PathBuf>,
|
||||
},
|
||||
|
||||
/// Open an existing app bundle, if it exists
|
||||
OpenApp,
|
||||
|
||||
RequestRebuild,
|
||||
|
||||
@@ -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<notify::Event>,
|
||||
krate: DioxusCrate,
|
||||
_tx: UnboundedSender<notify::Event>,
|
||||
watcher: Box<dyn notify::Watcher>,
|
||||
}
|
||||
|
||||
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<PathBuf> = 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<notify::Event>,
|
||||
) -> Box<dyn NotifyWatcher> {
|
||||
// Build the event handler for notify.
|
||||
let handler = move |info: notify::Result<notify::Event>| {
|
||||
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),
|
||||
)
|
||||
}
|
||||
@@ -32,10 +32,14 @@ impl CliSettings {
|
||||
/// Load the settings from the local, global, or default config in that order
|
||||
pub(crate) fn load() -> Arc<Self> {
|
||||
static SETTINGS: Lazy<Arc<CliSettings>> =
|
||||
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<Self> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::process::Output> {
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
// 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(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);
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
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<Arc<AndroidTools>>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// Load the workspace from the current directory. This is cached and will only be loaded once.
|
||||
pub async fn current() -> Result<Arc<Workspace>> {
|
||||
static WS: tokio::sync::Mutex<Option<Arc<Workspace>>> = 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<Arc<AndroidTools>> {
|
||||
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<String>) -> Result<NodeId> {
|
||||
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::<Vec<_>>();
|
||||
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<Option<DioxusConfig>> {
|
||||
// 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::<DioxusConfig>(&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<PathBuf> {
|
||||
/// 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<PathBuf> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -479,3 +479,18 @@ pub fn use_hook_with_cleanup<T: Clone + 'static>(
|
||||
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");
|
||||
}
|
||||
+12
-11
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
<P as Properties>::memoize,
|
||||
@@ -647,9 +645,9 @@ impl VComponent {
|
||||
));
|
||||
|
||||
VComponent {
|
||||
render_fn,
|
||||
name: fn_name,
|
||||
props,
|
||||
render_fn,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<C: ComponentFunction<P, M>, P, M>(co
|
||||
)
|
||||
)]
|
||||
pub trait ComponentFunction<Props, Marker = ()>: Clone + 'static {
|
||||
/// Get the type id of the component.
|
||||
fn id(&self) -> TypeId {
|
||||
TypeId::of::<Self>()
|
||||
}
|
||||
/// 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<F: Fn(P) -> Element + Clone + 'static, P> ComponentFunction<P> for F {
|
||||
impl<F, P> ComponentFunction<P> 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<F: Fn() -> Element + Clone + 'static> ComponentFunction<(), EmptyMarker> for F {
|
||||
fn rebuild(&self, _: ()) -> Element {
|
||||
self()
|
||||
impl<F> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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<P, M>,
|
||||
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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
@@ -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-macroLine truncated
|
||||
@@ -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<PathBuf>,
|
||||
|
||||
/// 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<String>)>,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for RustcDepInfo {
|
||||
type Err = DepInfoParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl RustcDepInfo {
|
||||
/// Parse the `.d` dep-info file generated by rustc.
|
||||
pub fn new(contents: &str) -> Result<RustcDepInfo, DepInfoParseError> {
|
||||
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<String, DepInfoParseError> {
|
||||
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::<Vec<_>>(),
|
||||
info.files
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -327,7 +327,14 @@ impl App {
|
||||
match msg {
|
||||
DevserverMsg::HotReload(hr_msg) => {
|
||||
for webview in self.webviews.values_mut() {
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,12 +70,12 @@ impl LaunchConfig for Config {}
|
||||
|
||||
pub(crate) type WryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&str, HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||
Box<dyn Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static>,
|
||||
);
|
||||
|
||||
pub(crate) type AsyncWryProtocol = (
|
||||
String,
|
||||
Box<dyn Fn(&str, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>,
|
||||
Box<dyn Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>,
|
||||
);
|
||||
|
||||
impl Config {
|
||||
@@ -188,7 +188,7 @@ impl Config {
|
||||
/// Set a custom protocol
|
||||
pub fn with_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&str, HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + 'static,
|
||||
F: Fn(HttpRequest<Vec<u8>>) -> HttpResponse<Cow<'static, [u8]>> + '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<F>(mut self, name: impl ToString, handler: F) -> Self
|
||||
where
|
||||
F: Fn(&str, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static,
|
||||
F: Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static,
|
||||
{
|
||||
self.asynchronous_protocols
|
||||
.push((name.to_string(), Box::new(handler)));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
@@ -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<HotReloadTemplateWithLocation>,
|
||||
pub assets: Vec<PathBuf>,
|
||||
pub unknown_files: Vec<PathBuf>,
|
||||
pub ms_elapsed: u64,
|
||||
pub jump_table: Option<JumpTable>,
|
||||
pub for_build_id: Option<u64>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) {
|
||||
dom.runtime().on_scope(ScopeId::ROOT, || {
|
||||
let ctx = dioxus_signals::get_global_context();
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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<dyn crate::prelude::document::Document>;
|
||||
vdom.provide_root_context(document);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<PathBuf> },
|
||||
HtmlTranslate { html: String },
|
||||
BuildsFinished {
|
||||
client: PathBuf,
|
||||
server: Option<PathBuf>,
|
||||
},
|
||||
BuildFinished {
|
||||
path: PathBuf,
|
||||
},
|
||||
BuildUpdate {
|
||||
stage: BuildStage,
|
||||
},
|
||||
CargoOutput {
|
||||
message: CompilerMessage,
|
||||
},
|
||||
BundleOutput {
|
||||
bundles: Vec<PathBuf>,
|
||||
},
|
||||
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,
|
||||
|
||||
@@ -20,6 +20,8 @@ serde = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-lib = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
|
||||
[features]
|
||||
web = []
|
||||
|
||||
@@ -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.");
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
|
||||
@@ -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<bool> {
|
||||
serialize_context().create_entry()
|
||||
}
|
||||
@@ -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 = [
|
||||
"server-core" ,
|
||||
"default-tls",
|
||||
"server_fn/axum",
|
||||
]
|
||||
server-core = [
|
||||
"server_fn/axum-no-default",
|
||||
"dioxus_server_macro/axum",
|
||||
"default-tls",
|
||||
"server",
|
||||
]
|
||||
axum = [
|
||||
"dep:tower-http",
|
||||
"server_fn/axum",
|
||||
"axum_core",
|
||||
]
|
||||
server = [
|
||||
"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"]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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<bool> {
|
||||
dioxus_fullstack_protocol::serialize_context().create_entry()
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
pub mod document;
|
||||
#[cfg(feature = "server")]
|
||||
mod render;
|
||||
#[cfg(feature = "server")]
|
||||
mod streaming;
|
||||
#[cfg(all(feature = "web", feature = "document"))]
|
||||
pub use web::FullstackWebDocument;
|
||||
|
||||
#[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::*;
|
||||
}
|
||||
@@ -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<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
platform_config: Vec<Box<dyn Any>>,
|
||||
) -> ! {
|
||||
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::<ServeConfig>()
|
||||
.map(|cfg| Result::Ok(*cfg))
|
||||
.or_else(|cfg| {
|
||||
cfg.downcast::<ServeConfigBuilder>()
|
||||
.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<ServeConfig, crate::UnableToLoadIndex>);
|
||||
|
||||
impl TryInto<ServeConfig> for TryIntoResult {
|
||||
type Error = crate::UnableToLoadIndex;
|
||||
|
||||
fn try_into(self) -> Result<ServeConfig, Self::Error> {
|
||||
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<RenderHandleState>,
|
||||
mut request: Request<Body>,
|
||||
) -> 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")
|
||||
}
|
||||
@@ -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<String, ServerFnError> {
|
||||
//! 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<S>: DioxusRouterFnExt<S> {
|
||||
/// 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<Cfg, Error>(self, cfg: Cfg, app: fn() -> Element) -> Self
|
||||
where
|
||||
Cfg: TryInto<ServeConfig, Error = Error>,
|
||||
Error: std::error::Error,
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<S> DioxusRouterExt<S> for Router<S>
|
||||
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::<Vec<_>>()
|
||||
.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<Cfg, Error>(self, cfg: Cfg, app: fn() -> Element) -> Self
|
||||
where
|
||||
Cfg: TryInto<ServeConfig, Error = Error>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user