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:
Jonathan Kelley authored and GitHub committed 2025-05-06 23:08:59 -07:00
1 parent 71d85e6dd5
commit d976ca184a
142 files changed
+17867 -7041

No files matched your search

+3
View File
@@ -28,3 +28,6 @@ node_modules/
# ignore the output of tmps # ignore the output of tmps
tmp/ tmp/
bundle/ bundle/
# in debugging we frequently dump wasm to wat with `wasm-tools print`
*.wat
+8 -3
View File
@@ -6,14 +6,19 @@
"[handlebars]": { "[handlebars]": {
"editor.formatOnSave": false "editor.formatOnSave": false
}, },
"[javascript]": {
"editor.formatOnSave": false
},
"dioxus.formatOnSave": "disabled",
// "rust-analyzer.check.workspace": true, // "rust-analyzer.check.workspace": true,
// "rust-analyzer.check.workspace": false, // "rust-analyzer.check.workspace": false,
// "rust-analyzer.check.features": "all", // "rust-analyzer.check.features": "all",
// "rust-analyzer.cargo.buildScripts.rebuildOnSave": false, // "rust-analyzer.cargo.buildScripts.rebuildOnSave": false,
// "rust-analyzer.check.workspace": false, // "rust-analyzer.check.workspace": false,
// "rust-analyzer.check.allTargets": true,
"rust-analyzer.cargo.features": "all", "rust-analyzer.cargo.features": "all",
"rust-analyzer.check.features": "all", "rust-analyzer.check.features": "all",
// "rust-analyzer.check.allTargets": true, "rust-analyzer.cargo.extraArgs": [
// we don't want the formatter to kick in while we're working on dioxus itself "--tests"
"dioxus.formatOnSave": "disabled", ],
} }
Generated
+508 -251
View File
File diff suppressed because it is too large. Load diff
+42 -12
View File
@@ -70,6 +70,8 @@ members = [
"packages/config-macros", "packages/config-macros",
"packages/native", "packages/native",
"packages/asset-resolver", "packages/asset-resolver",
"packages/depinfo",
"packages/server",
# Playwright tests # Playwright tests
"packages/playwright-tests/liveview", "packages/playwright-tests/liveview",
@@ -89,6 +91,10 @@ members = [
"packages/wasm-split/wasm-split-cli", "packages/wasm-split/wasm-split-cli",
"packages/wasm-split/wasm-used", "packages/wasm-split/wasm-used",
# subsecond
"packages/subsecond/subsecond",
"packages/subsecond/subsecond-types",
# Full project examples # Full project examples
"example-projects/fullstack-hackernews", "example-projects/fullstack-hackernews",
"example-projects/ecommerce-site", "example-projects/ecommerce-site",
@@ -107,6 +113,7 @@ members = [
# Playwright tests # Playwright tests
"packages/playwright-tests/liveview", "packages/playwright-tests/liveview",
"packages/playwright-tests/web", "packages/playwright-tests/web",
"packages/playwright-tests/barebones-template",
"packages/playwright-tests/fullstack", "packages/playwright-tests/fullstack",
"packages/playwright-tests/fullstack-mounted", "packages/playwright-tests/fullstack-mounted",
"packages/playwright-tests/fullstack-routing", "packages/playwright-tests/fullstack-routing",
@@ -114,7 +121,7 @@ members = [
"packages/playwright-tests/nested-suspense", "packages/playwright-tests/nested-suspense",
"packages/playwright-tests/cli-optimization", "packages/playwright-tests/cli-optimization",
"packages/playwright-tests/wasm-split-harness", "packages/playwright-tests/wasm-split-harness",
"packages/playwright-tests/default-features-disabled" "packages/playwright-tests/default-features-disabled",
] ]
[workspace.package] [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-cli-opt = { path = "packages/cli-opt", version = "0.6.2" }
dioxus-devtools = { path = "packages/devtools", 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-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 = { path = "packages/fullstack", version = "0.6.2" }
dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" } dioxus-fullstack-hooks = { path = "packages/fullstack-hooks", version = "0.6.3" }
dioxus-fullstack-protocol = { path = "packages/fullstack-protocol", 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" } generational-box = { path = "packages/generational-box", version = "0.6.2" }
lazy-js-bundle = { path = "packages/lazy-js-bundle", 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 = { path = "packages/manganis/manganis", version = "0.6.2" }
manganis-core = { path = "packages/manganis/manganis-core", 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" } 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 = { 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-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-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" } 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" } warnings = { version = "0.2.1" }
# a fork of pretty please for tests - let's get off of this if we can! # 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" } tauri-bundler = { version = "2.2.4" }
lru = "0.13.0" lru = "0.13.0"
async-trait = "0.1.87" 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 } 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" tower = "0.5.2"
http = "1.2.0" http = "1.2.0"
notify = { version = "8.0.0" } notify = { version = "8.0.0" }
@@ -268,11 +284,14 @@ sha2 = "0.10.8"
walrus = { version = "0.23.3", features = ["parallel"] } walrus = { version = "0.23.3", features = ["parallel"] }
id-arena = "2.2.1" id-arena = "2.2.1"
async-compression = { version = "0.4.20", features = ["futures-io", "gzip", "brotli"] } 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" } async-once-cell = { version = "0.5.4" }
rayon = "1.10.0" rayon = "1.10.0"
wasmparser = "0.226.0" wasmparser = "0.226.0"
itertools = "0.14.0" itertools = "0.14.0"
object = { version = "0.36.0" }
bincode = "1.3.3"
inventory = { version = "0.3.5" }
macro-string = "0.1.4" macro-string = "0.1.4"
walkdir = "2.5.0" walkdir = "2.5.0"
url = "2.3.1" url = "2.3.1"
@@ -280,10 +299,15 @@ separator = "0.4.1"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
serde_repr = "0.1" serde_repr = "0.1"
hyper-util = "0.1.10" 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 # desktop
wry = { version = "0.50.3", default-features = false } wry = { version = "0.45.0", default-features = false }
tao = { version = "0.32.6", features = ["rwh_05"] } tao = { version = "0.33.0", features = ["rwh_05"] }
webbrowser = "1.0.3" webbrowser = "1.0.3"
infer = "0.19.0" infer = "0.19.0"
dunce = "1.0.5" dunce = "1.0.5"
@@ -301,13 +325,20 @@ open = "5.1.2"
# web # web
gloo-dialogs = "0.2.0" 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 # native
keyboard-types = { version = "0.7", default-features = false } keyboard-types = { version = "0.7", default-features = false }
winit = { version = "0.30.2", features = ["rwh_06"] } 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] [profile.dev]
debug = 0
# our release profile should be fast to compile and fast to run # 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 # 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 incremental = true
# crank up the opt level for wasm-split-cli in dev mode # 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] [profile.wasm-split-release]
inherits = "release" inherits = "release"
opt-level = 'z' opt-level = 'z'
@@ -386,13 +417,12 @@ separator = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
rand = { workspace = true, features = ["small_rng"] } rand = { workspace = true, features = ["small_rng"] }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.0"
async-std = "1.13.0" async-std = "1.12.0"
web-time = "1.1.0" web-time = "1.1.0"
axum = { workspace = true, default-features = true }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies] [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"] } tokio = { version = "1.43", default-features = false, features = ["sync", "macros", "io-util", "rt", "time"] }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
+13 -13
View File
@@ -48,17 +48,17 @@ optional = true
[features] [features]
default = [] default = []
server = [ server = [
"dioxus-cli-config", "dioxus-fullstack/server",
"axum", "dep:dioxus-cli-config",
"tokio", "dep:axum",
"dioxus-fullstack/axum", "dep:tokio",
"tower-http", "dep:tower-http",
"async-trait", "dep:async-trait",
"sqlx", "dep:sqlx",
"axum_session", "dep:axum_session",
"axum_session_auth", "dep:axum_session_auth",
"axum_session_sqlx", "dep:axum_session_sqlx",
"http", "dep:http",
"tower", "dep:tower",
] ]
web = ["dioxus/web", "dioxus-web"] web = ["dioxus/web", "dep:dioxus-web"]
+18
View File
@@ -69,6 +69,7 @@ pub const APP_TITLE_ENV: &str = "DIOXUS_APP_TITLE";
#[doc(hidden)] #[doc(hidden)]
pub const OUT_DIR: &str = "DIOXUS_OUT_DIR"; pub const OUT_DIR: &str = "DIOXUS_OUT_DIR";
pub const SESSION_CACHE_DIR: &str = "DIOXUS_SESSION_CACHE_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 /// 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 /// 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 { pub fn android_session_cache_dir() -> PathBuf {
PathBuf::from("/data/local/tmp/dx/") 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)
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ keywords = ["dom", "ui", "gui", "react"]
anyhow = { workspace = true } anyhow = { workspace = true }
manganis = { workspace = true } manganis = { workspace = true }
manganis-core = { workspace = true } manganis-core = { workspace = true }
object = {version="0.36.7", features=["wasm"]} object = { workspace = true, features = ["wasm"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
const-serialize = { workspace = true, features = ["serde"] } const-serialize = { workspace = true, features = ["serde"] }
+21 -10
View File
@@ -24,6 +24,8 @@ dioxus-cli-opt = { workspace = true }
dioxus-fullstack = { workspace = true } dioxus-fullstack = { workspace = true }
dioxus-dx-wire-format = { workspace = true } dioxus-dx-wire-format = { workspace = true }
wasm-split-cli = { workspace = true } wasm-split-cli = { workspace = true }
depinfo = { workspace = true }
subsecond-types = { workspace = true }
clap = { workspace = true, features = ["derive", "cargo"] } clap = { workspace = true, features = ["derive", "cargo"] }
convert_case = { workspace = true } convert_case = { workspace = true }
@@ -32,7 +34,7 @@ uuid = { workspace = true, features = ["v4"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
cargo_toml = { workspace = true } cargo_toml = { workspace = true, features = ["features"] }
futures-util = { workspace = true, features = ["async-await-macro"] } futures-util = { workspace = true, features = ["async-await-macro"] }
notify = { workspace = true, features = ["serde"] } notify = { workspace = true, features = ["serde"] }
html_parser = { workspace = true } html_parser = { workspace = true }
@@ -47,8 +49,8 @@ hyper-rustls = { workspace = true }
rustls = { workspace = true } rustls = { workspace = true }
rayon = { workspace = true } rayon = { workspace = true }
futures-channel = { workspace = true } futures-channel = { workspace = true }
cargo-config2 = { workspace = true, optional = true } target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
krates = { version = "0.17.5" } krates = { workspace = true }
regex = "1.11.1" regex = "1.11.1"
console = "0.15.11" console = "0.15.11"
ctrlc = "3.4.5" ctrlc = "3.4.5"
@@ -75,7 +77,7 @@ which = { version = "7.0.2" }
# plugin packages # plugin packages
open = { workspace = true } open = { workspace = true }
cargo-generate = "0.22.1" cargo-generate = "0.23.3"
toml_edit = "0.22.24" toml_edit = "0.22.24"
# formatting # formatting
@@ -92,11 +94,12 @@ tracing-subscriber = { version = "0.3.19", features = ["std", "env-filter", "jso
console-subscriber = { version = "0.4.1", optional = true } console-subscriber = { version = "0.4.1", optional = true }
tracing = { workspace = true } tracing = { workspace = true }
wasm-opt = { version = "0.116.1", optional = true } wasm-opt = { version = "0.116.1", optional = true }
crossterm = { version = "0.28.1", features = ["event-stream"] } ansi-to-tui = { workspace = true }
ansi-to-tui = "7.0.0" ansi-to-html = { workspace = true }
ansi-to-html = "0.2.2" path-absolutize = { workspace = true }
path-absolutize = "3.1" crossterm = { workspace = true, features = ["event-stream"] }
ratatui = { version = "0.29.0", features = ["crossterm", "unstable"] } ratatui = { workspace = true, features = ["crossterm", "unstable"] }
shell-words = { workspace = true }
# disable `log` entirely since `walrus` uses it and is *much* slower with it enableda # 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"] } log = { version = "0.4", features = ["max_level_off", "release_max_level_off"] }
@@ -107,13 +110,16 @@ manganis = { workspace = true }
manganis-core = { workspace = true } manganis-core = { workspace = true }
# Extracting data from an executable # Extracting data from an executable
object = { version = "0.36.7", features = ["wasm"] } object = { workspace = true, features = ["all"] }
tokio-util = { workspace = true, features = ["full"] } tokio-util = { workspace = true, features = ["full"] }
itertools = { workspace = true } itertools = { workspace = true }
throbber-widgets-tui = "0.8.0" throbber-widgets-tui = "0.8.0"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
handlebars = "6.3.1" handlebars = "6.3.1"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
memmap = "0.7.0"
walrus = { workspace = true, features = ["parallel"]}
wasmparser = { workspace = true }
tauri-utils = { workspace = true } tauri-utils = { workspace = true }
tauri-bundler = { workspace = true } tauri-bundler = { workspace = true }
@@ -124,6 +130,11 @@ local-ip-address = "0.6.3"
dircpy = "0.3.19" dircpy = "0.3.19"
plist = "1.7.0" plist = "1.7.0"
memoize = "0.5.1" 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] [build-dependencies]
built = { version = "0.7.5", features = ["git2"] } built = { version = "0.7.5", features = ["git2"] }
@@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application android:hasCode="true" android:supportsRtl="true" android:icon="@mipmap/ic_launcher" <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"> android:label="@string/app_name" android:theme="@style/AppTheme">
<activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true" <activity android:configChanges="orientation|screenLayout|screenSize|keyboardHidden" android:exported="true"
android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity"> android:label="@string/app_name" android:name="dev.dioxus.main.MainActivity">
+184
View File
@@ -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>
-12
View File
@@ -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>
View File
Whitespace-only changes.
+11
View File
@@ -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>
-207
View File
@@ -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>
File diff suppressed because it is too large. Load diff
-955
View File
@@ -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(())
}
}
+177
View File
@@ -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,
});
}
}
+14 -13
View File
@@ -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 //! 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
//! Uses a request -> response architecture that allows you to monitor the progress with an optional message //! including status updates and build customization. The [`patch`] module contains the logic for
//! receiver. //! 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 builder;
mod bundle; mod context;
mod prerender; mod patch;
mod progress;
mod request; mod request;
mod templates; mod tools;
mod verify;
mod web;
pub(crate) use builder::*; pub(crate) use builder::*;
pub(crate) use bundle::*; pub(crate) use context::*;
pub(crate) use progress::*; pub(crate) use patch::*;
pub(crate) use request::*; pub(crate) use request::*;
pub(crate) use tools::*;
File diff suppressed because it is too large. Load diff
-125
View File
@@ -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(())
}
-120
View File
@@ -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
}
}
File diff suppressed because it is too large. Load diff
-7
View File
@@ -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,
}
+257
View File
@@ -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)
}
-143
View File
@@ -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(())
}
}
-272
View File
@@ -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);
}
}
+16 -14
View File
@@ -1,5 +1,5 @@
use super::*; use super::{check::collect_rs_files, *};
use crate::{metadata::collect_rs_files, DioxusCrate}; use crate::Workspace;
use anyhow::Context; use anyhow::Context;
use dioxus_autofmt::{IndentOptions, IndentType}; use dioxus_autofmt::{IndentOptions, IndentType};
use rayon::prelude::*; use rayon::prelude::*;
@@ -38,7 +38,7 @@ pub(crate) struct Autoformat {
} }
impl Autoformat { impl Autoformat {
pub(crate) fn autoformat(self) -> Result<StructuredOutput> { pub(crate) async fn autoformat(self) -> Result<StructuredOutput> {
let Autoformat { let Autoformat {
check, check,
raw, raw,
@@ -62,15 +62,16 @@ impl Autoformat {
} else { } else {
// Default to formatting the project. // Default to formatting the project.
let crate_dir = if let Some(package) = self.package { let crate_dir = if let Some(package) = self.package {
// TODO (matt): Do we need to use the entire `DioxusCrate` here? let workspace = Workspace::current().await?;
let target_args = TargetArgs { let dx_crate = workspace
package: Some(package), .find_main_package(Some(package))
..Default::default() .context("Failed to find package")?;
}; workspace.krates[dx_crate]
let dx_crate = .manifest_path
DioxusCrate::new(&target_args).context("failed to parse crate graph")?; .parent()
.unwrap()
Cow::Owned(dx_crate.crate_dir()) .to_path_buf()
.into()
} else { } else {
Cow::Borrowed(Path::new(".")) Cow::Borrowed(Path::new("."))
}; };
@@ -171,7 +172,8 @@ fn autoformat_project(
format_rust_code: bool, format_rust_code: bool,
dir: impl AsRef<Path>, dir: impl AsRef<Path>,
) -> Result<()> { ) -> 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() { if files_to_format.is_empty() {
return Ok(()); return Ok(());
@@ -297,5 +299,5 @@ async fn test_auto_fmt() {
package: None, package: None,
}; };
fmt.autoformat().unwrap(); fmt.autoformat().await.unwrap();
} }
+132 -162
View File
@@ -1,207 +1,177 @@
use super::*; use crate::{cli::*, AppBuilder, BuildRequest, Workspace, PROFILE_SERVER};
use crate::{Builder, DioxusCrate, Platform, 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. /// Build the Rust Dioxus app and all of its assets.
/// ///
/// Produces a final output bundle designed to be run on the target platform. /// Produces a final output build. If a "server" feature is present in the package's Cargo.toml, it will
#[derive(Clone, Debug, Default, Deserialize, Parser)] /// be considered a fullstack app and the server will be built as well.
pub(crate) struct BuildArgs { #[derive(Clone, Debug, Default, Parser)]
/// Build in release mode [default: false] pub struct BuildArgs {
#[clap(long, short)] /// Enable fullstack mode [default: false]
#[serde(default)] ///
pub(crate) release: bool, /// This is automatically detected from `dx serve` if the "fullstack" feature is enabled by default.
/// 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)] #[clap(long)]
#[serde(default)] pub(crate) fullstack: Option<bool>,
pub(crate) force_sequential: bool,
/// Build the app with custom a profile /// The feature to use for the client in a fullstack app [default: "web"]
#[clap(long)] #[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 /// Build with custom profile for the fullstack server
#[clap(long, default_value_t = PROFILE_SERVER.to_string())] #[clap(long, default_value_t = PROFILE_SERVER.to_string())]
pub(crate) server_profile: String, pub(crate) server_profile: String,
/// Build platform: support Web & Desktop [default: "default_platform"] /// The target to build for the server.
#[clap(long, value_enum)]
pub(crate) platform: Option<Platform>,
/// 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. /// This can be different than the host allowing cross-compilation of the server. This is useful for
#[arg(long, default_missing_value="true", num_args=0..=1)] /// platforms like Cloudflare Workers where the server is compiled to wasm and then uploaded to the edge.
pub(crate) fullstack: Option<bool>,
/// Run the ssg config of the app and generate the files
#[clap(long)] #[clap(long)]
pub(crate) ssg: bool, pub(crate) server_target: Option<Triple>,
/// Skip collecting assets from dependencies [default: false] /// Arguments for the build itself
#[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
#[clap(flatten)] #[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 { impl BuildArgs {
pub async fn run_cmd(mut self) -> Result<StructuredOutput> { pub async fn build(self) -> Result<StructuredOutput> {
tracing::info!("Building project..."); tracing::info!("Building project...");
let krate = let targets = self.into_targets().await?;
DioxusCrate::new(&self.target_args).context("Failed to load Dioxus workspace")?;
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 { tracing::info!(path = ?targets.client.root_dir(), "Server build completed successfully! 🚀");
path: bundle.build.root_dir(), }
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 pub async fn into_targets(self) -> Result<BuildTargets> {
/// the user has configured their app. let workspace = Workspace::current().await?;
///
/// IE if they've specified "fullstack" as a feature on `dioxus`, then we want to build the let mut server = None;
/// fullstack variant even if they omitted the `--fullstack` flag.
pub(crate) async fn resolve(&mut self, krate: &DioxusCrate) -> Result<()> { let client = match self.targets {
let default_platforms = krate.default_platforms(); // A simple `dx serve` command with no explicit targets
let default_platform = default_platforms.iter().find(|p| **p != Platform::Server); None => {
let default_server = default_platforms.iter().any(|p| *p == Platform::Server); // Now resolve the builds that we need to.
let auto_platform = krate.autodetect_platform(); // 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 // 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 // Users need to enable "fullstack" in their default feature set.
self.fullstack = Some( // todo(jon): fullstack *could* be a feature of the app, but right now we're assuming it's always enabled
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
// //
// todo(jon): eventually we want to let users pass a `--server <crate>` flag to specify a package to use as the server // Now we need to resolve the client features
// however, it'll take some time to support that and we don't have a great RPC binding layer between the two yet let fullstack = ((default_server || client.fullstack_feature_enabled())
if self.fullstack() && self.target_args.server_features.is_empty() { || 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()); 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 server = Some(_server);
// We do this for android/wasm since they require
if self.profile.is_none() && !self.release {
match self.platform {
Some(Platform::Android) => {
self.profile = Some(crate::dioxus_crate::PROFILE_ANDROID.to_string());
} }
Some(Platform::Web) => {
self.profile = Some(crate::dioxus_crate::PROFILE_WASM.to_string()); 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 let Some(server_args) = server_args_ {
if self.platform == Some(Platform::Android) && self.target_args.arch.is_none() { server = Some(BuildRequest::new(&server_args, workspace.clone()).await?);
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()
} }
None => {
let a = Arch::default(); BuildRequest::new(&client_args_.unwrap(), workspace.clone()).await?
tracing::debug!(
"Could not detect Android arch, defaulting to `{}`",
a.android_target_triplet()
);
a
} }
}; };
self.target_args.arch = Some(arch); Ok(BuildTargets { client, server })
}
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)
} }
} }
+191 -46
View File
@@ -1,13 +1,24 @@
use crate::{AppBundle, BuildArgs, Builder, DioxusCrate, Platform}; use crate::{AppBuilder, BuildArgs, BuildMode, BuildRequest, Platform};
use anyhow::{anyhow, Context}; 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 path_absolutize::Absolutize;
use std::collections::HashMap; use std::collections::HashMap;
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
path::Path,
time::Duration,
};
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
use tokio::process::Command;
use walkdir::WalkDir; use walkdir::WalkDir;
use super::*; 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)] #[derive(Clone, Debug, Parser)]
pub struct Bundle { pub struct Bundle {
/// The package types to bundle /// The package types to bundle
@@ -24,61 +35,76 @@ pub struct Bundle {
#[clap(long)] #[clap(long)]
pub out_dir: Option<PathBuf>, 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 /// The arguments for the dioxus build
#[clap(flatten)] #[clap(flatten)]
pub(crate) build_arguments: BuildArgs, pub(crate) args: BuildArgs,
} }
impl Bundle { 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> { pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
tracing::info!("Bundling project..."); tracing::info!("Bundling project...");
let krate = DioxusCrate::new(&self.build_arguments.target_args) let BuildTargets { client, server } = self.args.into_targets().await?;
.context("Failed to load Dioxus workspace")?;
// We always use `release` mode for bundling AppBuilder::start(&client, BuildMode::Base)?
self.build_arguments.release = true; .finish_build()
self.build_arguments.resolve(&krate).await?;
tracing::info!("Building app...");
let bundle = Builder::start(&krate, self.build_arguments.clone())?
.finish()
.await?; .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 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]); self.package_types = Some(vec![crate::PackageType::IosBundle]);
} }
let mut bundles = vec![]; let mut bundles = vec![];
// Copy the server over if it exists // Copy the server over if it exists
if bundle.build.build.fullstack() { if let Some(server) = server.as_ref() {
bundles.push(bundle.server_exe().unwrap()); bundles.push(server.main_exe());
} }
// Create a list of bundles that we might need to copy // 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 // By default, mac/win/linux work with tauri bundle
Platform::MacOS | Platform::Linux | Platform::Windows => { Platform::MacOS | Platform::Linux | Platform::Windows => {
tracing::info!("Running desktop bundler..."); 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); bundles.extend(bundle.bundle_paths);
} }
} }
// Web/ios can just use their root_dir // 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 => { Platform::Ios => {
tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing."); 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::Server => bundles.push(client.root_dir()),
Platform::Liveview => bundles.push(bundle.build.root_dir()), Platform::Liveview => bundles.push(client.root_dir()),
Platform::Android => { Platform::Android => {
let aab = bundle let aab = client
.android_gradle_bundle() .android_gradle_bundle()
.await .await
.context("Failed to run gradle bundleRelease")?; .context("Failed to run gradle bundleRelease")?;
@@ -87,7 +113,7 @@ impl Bundle {
}; };
// Copy the bundles to the output directory if one was specified // 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) { if let Some(outdir) = self.out_dir.clone().or(crate_outdir) {
let outdir = outdir let outdir = outdir
.absolutize() .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 }) Ok(StructuredOutput::BundleOutput { bundles })
} }
fn bundle_desktop( fn bundle_desktop(
&self, build: &BuildRequest,
krate: &DioxusCrate, package_types: &Option<Vec<crate::PackageType>>,
bundle: &AppBundle,
) -> Result<Vec<tauri_bundler::Bundle>, Error> { ) -> 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 package = krate.package();
let mut name: PathBuf = krate.executable_name().into(); let mut name: PathBuf = krate.executable_name().into();
if cfg!(windows) { if cfg!(windows) {
name.set_extension("exe"); 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")?; .context("Failed to create bundle directory")?;
std::fs::copy( std::fs::copy(&exe, krate.bundle_dir(build.platform).join(&name))
&bundle.app.exe,
krate
.bundle_dir(self.build_arguments.platform())
.join(&name),
)
.with_context(|| "Failed to copy the output executable into the bundle directory")?; .with_context(|| "Failed to copy the output executable into the bundle directory")?;
let binaries = vec![ let binaries = vec![
// We use the name of the exe but it has to be in the same directory // We use the name of the exe but it has to be in the same directory
BundleBinary::new(krate.executable_name().to_string(), true) 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(); let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into();
@@ -186,7 +220,7 @@ impl Bundle {
bundle_settings.resources_map = Some(HashMap::new()); bundle_settings.resources_map = Some(HashMap::new());
} }
let asset_dir = bundle.build.asset_dir(); let asset_dir = build.asset_dir();
if asset_dir.exists() { if asset_dir.exists() {
for entry in WalkDir::new(&asset_dir) { for entry in WalkDir::new(&asset_dir) {
let entry = entry.unwrap(); let entry = entry.unwrap();
@@ -218,7 +252,7 @@ impl Bundle {
} }
let mut settings = SettingsBuilder::new() 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 { .package_settings(PackageSettings {
product_name: krate.bundled_app_name(), product_name: krate.bundled_app_name(),
version: package.version.to_string(), version: package.version.to_string(),
@@ -231,17 +265,11 @@ impl Bundle {
.binaries(binaries) .binaries(binaries)
.bundle_settings(bundle_settings); .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()); 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(build.triple.to_string());
settings = settings.target(target.to_string());
}
if self.build_arguments.platform() == Platform::Ios {
settings = settings.target("aarch64-apple-ios".to_string());
}
let settings = settings let settings = settings
.build() .build()
@@ -260,4 +288,121 @@ impl Bundle {
Ok(bundles) 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(())
}
} }
+34 -7
View File
@@ -4,9 +4,11 @@
//! https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs //! https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
use super::*; use super::*;
use crate::{metadata::collect_rs_files, DioxusCrate}; use crate::BuildRequest;
use anyhow::Context; use anyhow::Context;
use futures_util::{stream::FuturesUnordered, StreamExt}; use futures_util::{stream::FuturesUnordered, StreamExt};
use std::path::Path;
use walkdir::WalkDir;
/// Check the Rust files in the project for issues. /// Check the Rust files in the project for issues.
#[derive(Clone, Debug, Parser)] #[derive(Clone, Debug, Parser)]
@@ -17,19 +19,28 @@ pub(crate) struct Check {
/// Information about the target to check /// Information about the target to check
#[clap(flatten)] #[clap(flatten)]
pub(crate) target_args: TargetArgs, pub(crate) build_args: BuildArgs,
} }
impl Check { impl Check {
// Todo: check the entire crate // Todo: check the entire crate
pub(crate) async fn check(self) -> Result<StructuredOutput> { pub(crate) async fn check(self) -> Result<StructuredOutput> {
let BuildTargets { client, server } = self.build_args.into_targets().await?;
match self.file { match self.file {
// Default to checking the project // Default to checking the project
None => { None => {
let dioxus_crate = DioxusCrate::new(&self.target_args)?; check_project_and_report(&client)
check_project_and_report(dioxus_crate)
.await .await
.context("error checking project")?; .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) => { Some(file) => {
check_file_and_report(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 /// 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. /// 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<()> { async fn check_project_and_report(build: &BuildRequest) -> Result<()> {
let mut files_to_check = vec![dioxus_crate.main_source_file()]; let dioxus_crate = build
files_to_check.extend(collect_rs_files(dioxus_crate.crate_dir())); .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 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()), _ => 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());
}
}
}
-2
View File
@@ -12,8 +12,6 @@ impl Clean {
pub(crate) async fn clean(self) -> Result<StructuredOutput> { pub(crate) async fn clean(self) -> Result<StructuredOutput> {
let output = tokio::process::Command::new("cargo") let output = tokio::process::Command::new("cargo")
.arg("clean") .arg("clean")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output() .output()
.await?; .await?;
+6 -9
View File
@@ -1,6 +1,5 @@
use super::*; use super::*;
use crate::TraceSrc; use crate::{CliSettings, TraceSrc, Workspace};
use crate::{metadata::crate_root, CliSettings};
/// Dioxus config file controls /// Dioxus config file controls
#[derive(Clone, Debug, Deserialize, Subcommand)] #[derive(Clone, Debug, Deserialize, Subcommand)]
@@ -71,8 +70,8 @@ impl From<BoolValue> for bool {
} }
impl Config { impl Config {
pub(crate) fn config(self) -> Result<StructuredOutput> { pub(crate) async fn config(self) -> Result<StructuredOutput> {
let crate_root = crate_root()?; let crate_root = Workspace::crate_root_from_path()?;
match self { match self {
Config::Init { name, force } => { Config::Init { name, force } => {
let conf_path = crate_root.join("Dioxus.toml"); let conf_path = crate_root.join("Dioxus.toml");
@@ -89,15 +88,13 @@ impl Config {
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Init config file completed."); tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Init config file completed.");
} }
Config::FormatPrint {} => { Config::FormatPrint {} => {
tracing::info!( let workspace = Workspace::current().await?;
"{:#?}", tracing::info!("{:#?}", workspace.settings);
crate::dioxus_crate::DioxusCrate::new(&TargetArgs::default())?.config
);
} }
Config::CustomHtml {} => { Config::CustomHtml {} => {
let html_path = crate_root.join("index.html"); let html_path = crate_root.join("index.html");
let mut file = File::create(html_path)?; 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())?; file.write_all(content.as_bytes())?;
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done."); tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done.");
} }
+11
View File
@@ -58,6 +58,9 @@ impl Create {
// If no template is specified, use the default one and set the branch to the latest release. // 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); 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 { let args = GenerateArgs {
define: self.option, define: self.option,
destination: Some(self.path), destination: Some(self.path),
@@ -77,11 +80,19 @@ impl Create {
tag: self.tag, tag: self.tag,
..Default::default() ..Default::default()
}, },
verbose: crate::logging::VERBOSITY
.get()
.map(|f| f.verbose)
.unwrap_or(false),
..Default::default() ..Default::default()
}; };
restore_cursor_on_sigint(); restore_cursor_on_sigint();
tracing::debug!(dx_src = ?TraceSrc::Dev, "Creating new project with args: {args:#?}");
let path = cargo_generate::generate(args)?; let path = cargo_generate::generate(args)?;
_ = post_create(&path); _ = post_create(&path);
Ok(StructuredOutput::Success) Ok(StructuredOutput::Success)
} }
} }
+3
View File
@@ -55,6 +55,9 @@ impl Init {
// If no template is specified, use the default one and set the branch to the latest release. // 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); 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 { let args = GenerateArgs {
define: self.option, define: self.option,
destination: Some(self.path), destination: Some(self.path),
+204 -57
View File
@@ -1,64 +1,227 @@
use dioxus_cli_opt::AssetManifest; use crate::Result;
use anyhow::Context;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use target_lexicon::Triple;
#[derive(Debug, Serialize, Deserialize)] /// `dx` can act as a linker in a few scenarios. Note that we don't *actually* implement the linker logic,
pub enum LinkAction { /// instead just proxying to a specified linker (or not linking at all!).
BuildAssetManifest { ///
destination: PathBuf, /// This comes in two flavors:
}, /// --------------------------
LinkAndroid { /// - `BaseLink`: We are linking dependencies and want to dynamically select the linker from the environment.
linker: PathBuf, /// This is mostly implemented for Android where the linker is selected in part by the
extra_flags: Vec<String>, /// 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 { 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)? /// Should we write the input arguments to a file (aka act as a linker subprocess)?
/// ///
/// Just check if the magic env var is set /// Just check if the magic env var is set
pub(crate) fn from_env() -> Option<Self> { pub(crate) fn from_env() -> Option<Self> {
std::env::var(Self::ENV_VAR_NAME) if std::env::var(Self::DX_LINK_ARG).is_err() {
.ok() return None;
.map(|var| serde_json::from_str(&var).expect("Failed to parse magic env var"))
} }
pub(crate) fn to_json(&self) -> String { Some(Self {
serde_json::to_string(self).unwrap() 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 /// 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 /// 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. /// it both for determining if we should act as a linker and the for the file name itself.
/// async fn run_link_inner(self) -> Result<()> {
/// This will panic if it fails let mut args: Vec<_> = std::env::args().collect();
/// if args.is_empty() {
/// hmmmmmmmm tbh I'd rather just pass the object files back and do the parsing here, but the interface return Ok(());
/// 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");
} }
// Assemble an asset manifest by walking the object files being passed to us handle_linker_command_file(&mut args);
LinkAction::BuildAssetManifest { destination: dest } => {
let mut args: Vec<_> = std::env::args().collect();
let mut manifest = AssetManifest::default();
// 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. // Handle command files, usually a windows thing.
if let Some(command) = args.iter().find(|arg| arg.starts_with('@')).cloned() { if let Some(command) = args.iter().find(|arg| arg.starts_with('@')).cloned() {
let path = command.trim().trim_start_matches('@'); 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 // Gather linker args, and reset the args to be just the linker args
args = content *args = content
.lines() .lines()
.map(|line| { .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_end_matches('"').to_string();
let line_parsed = line_parsed.trim_start_matches('"').to_string(); let line_parsed = line_parsed.trim_start_matches('"').to_string();
line_parsed line_parsed
}) })
.collect(); .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");
}
}
}
} }
+2 -3
View File
@@ -20,7 +20,6 @@ pub(crate) use target::*;
pub(crate) use verbosity::*; pub(crate) use verbosity::*;
use crate::{error::Result, Error, StructuredOutput}; use crate::{error::Result, Error, StructuredOutput};
use anyhow::Context;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use html_parser::Dom; use html_parser::Dom;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@@ -30,7 +29,7 @@ use std::{
fs::File, fs::File,
io::{Read, Write}, io::{Read, Write},
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::Command,
}; };
/// Build, Bundle & Ship Dioxus Apps. /// Build, Bundle & Ship Dioxus Apps.
@@ -93,7 +92,7 @@ pub(crate) enum Commands {
Config(config::Config), Config(config::Config),
/// Build the assets for a specific target. /// Build the assets for a specific target.
#[clap(name = "build_assets")] #[clap(name = "assets")]
BuildAssets(build_assets::BuildAssets), BuildAssets(build_assets::BuildAssets),
} }
+110 -46
View File
@@ -1,67 +1,131 @@
use super::*; 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 /// 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)] #[derive(Clone, Debug, Parser)]
pub(crate) struct RunArgs { pub(crate) struct RunArgs {
/// Information about the target to build /// Information about the target to build
#[clap(flatten)] #[clap(flatten)]
pub(crate) build_args: BuildArgs, pub(crate) args: ServeArgs,
} }
impl RunArgs { impl RunArgs {
pub(crate) async fn run(mut self) -> Result<StructuredOutput> { pub(crate) async fn run(mut self) -> Result<StructuredOutput> {
let krate = DioxusCrate::new(&self.build_args.target_args) // Override the build arguments, leveraging our serve infrastructure.
.context("Failed to load Dioxus workspace")?; //
// 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 { loop {
match runner.wait().await { let msg = tokio::select! {
ServeUpdate::StderrReceived { platform, msg } => { msg = builder.wait() => msg,
tracing::info!("[{platform}]: {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 } => { BuilderUpdate::Progress { stage } => match stage {
runner.cleanup().await; BuildStage::Initializing => {
tracing::info!("[{platform}]: process exited with status: {status:?}"); 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; break;
} }
ServeUpdate::BuildUpdate { .. } => {} }
ServeUpdate::TracingLog { .. } => {} }
ServeUpdate::Exit { .. } => break, _ => {}
ServeUpdate::NewConnection => {}
ServeUpdate::WsMessage(_) => {}
ServeUpdate::FilesChanged { .. } => {}
ServeUpdate::RequestRebuild => {}
ServeUpdate::Redraw => {}
ServeUpdate::OpenApp => {}
ServeUpdate::ToggleShouldRebuild => {}
} }
} }
+123 -54
View File
@@ -1,7 +1,36 @@
use super::*; 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 /// 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)] #[derive(Clone, Debug, Default, Parser)]
#[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))] #[command(group = clap::ArgGroup::new("release-incompatible").multiple(true).conflicts_with("release"))]
pub(crate) struct ServeArgs { pub(crate) struct ServeArgs {
@@ -38,9 +67,22 @@ pub(crate) struct ServeArgs {
#[arg(long, default_missing_value="true", num_args=0..=1, short = 'i')] #[arg(long, default_missing_value="true", num_args=0..=1, short = 'i')]
pub(crate) interactive: Option<bool>, 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)] #[clap(flatten)]
pub(crate) build_arguments: BuildArgs, pub(crate) targets: BuildArgs,
} }
impl ServeArgs { impl ServeArgs {
@@ -48,71 +90,98 @@ impl ServeArgs {
/// ///
/// Make sure not to do any intermediate logging since our tracing infra has now enabled much /// Make sure not to do any intermediate logging since our tracing infra has now enabled much
/// higher log levels /// 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> { pub(crate) async fn serve(self) -> Result<StructuredOutput> {
crate::serve::serve_all(self).await?; if std::env::var("RUST_BACKTRACE").is_err() {
std::env::set_var("RUST_BACKTRACE", "1");
Ok(StructuredOutput::Success)
} }
pub(crate) async fn load_krate(&mut self) -> Result<DioxusCrate> { struct SavedLocation {
let krate = DioxusCrate::new(&self.build_arguments.target_args)?; file: String,
self.resolve(&krate).await?; line: u32,
Ok(krate) 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<()> { match res {
// Enable hot reload. Ok(Ok(_res)) => Ok(StructuredOutput::Success),
if self.hot_reload.is_none() { Ok(Err(e)) => Err(e),
self.hot_reload = Some(krate.settings.always_hot_reload.unwrap_or(true)); 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. format!("dx serve panicked at {location_display}\n{as_str}\n{backtrace_display} ___rust_try")
if self.open.is_none() { })
self.open = Some(krate.settings.always_open_browser.unwrap_or_default()); .unwrap_or_else(|| format!("dx serve panicked: {as_str}"));
}
// 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?;
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 { pub(crate) fn is_interactive_tty(&self) -> bool {
use std::io::IsTerminal; use std::io::IsTerminal;
std::io::stdout().is_terminal() && self.interactive.unwrap_or(true) 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
View File
@@ -1,235 +1,167 @@
use super::*; use crate::cli::*;
use std::path::Path; 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)] #[derive(Clone, Debug, Default, Deserialize, Parser)]
pub(crate) struct TargetArgs { pub(crate) struct TargetArgs {
/// Build for nightly [default: false] /// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)] #[clap(long, value_enum)]
pub(crate) nightly: bool, pub(crate) platform: Option<Platform>,
/// Build a example [default: ""] /// Build in release mode [default: false]
#[clap(long)] #[clap(long, short)]
pub(crate) example: Option<String>, #[serde(default)]
pub(crate) release: bool,
/// Build a binary [default: ""]
#[clap(long)]
pub(crate) bin: Option<String>,
/// The package to build /// The package to build
#[clap(short, long)] #[clap(short, long)]
pub(crate) package: Option<String>, 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 /// Space separated list of features to activate
#[clap(long)] #[clap(long)]
pub(crate) features: Vec<String>, 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 /// Don't include the default features in the build
#[clap(long)] #[clap(long)]
pub(crate) no_default_features: bool, pub(crate) no_default_features: bool,
/// The architecture to build for. /// Include all features in the build
#[clap(long, value_enum)] #[clap(long)]
pub(crate) arch: Option<Arch>, 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. /// Are we building for a device or just the simulator.
/// If device is false, then we'll build for the simulator /// If device is false, then we'll build for the simulator
#[clap(long)] #[clap(long)]
pub(crate) device: Option<bool>, 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 { /// Chain together multiple target commands
pub(crate) fn arch(&self) -> Arch { #[derive(Debug, Subcommand, Clone)]
self.arch.unwrap_or_default() #[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)] impl<T, U> FromArgMatches for ChainedCommand<T, U>
#[non_exhaustive] where
pub(crate) enum Arch { T: Args,
// Android: armv7l, armv7-linux-androideabi U: Subcommand,
Arm, {
// Android: aarch64, aarch64-linux-android fn from_arg_matches(matches: &ArgMatches) -> Result<Self, clap::Error> {
#[default] // Parse the first command before we try to parse the next one.
Arm64, let inner = T::from_arg_matches(matches)?;
// Android: i386, i686-linux-android
X86,
// Android: x86_64, x86_64-linux-android
X64,
}
impl Arch { // Try to parse the remainder of the command as a subcommand.
pub(crate) fn android_target_triplet(&self) -> &'static str { let next = match matches.subcommand() {
match self { // Subcommand skips into the matched .subcommand, hence we need to pass *outer* matches, ignoring the inner matches
Arch::Arm => "armv7-linux-androideabi", // (which in the average case should only match enumerated T)
Arch::Arm64 => "aarch64-linux-android", //
Arch::X86 => "i686-linux-android", // Here, we might want to eventually enable arbitrary names of subcommands if they're prefixed
Arch::X64 => "x86_64-linux-android", // 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 { // no subcommand matched, we are done
match self { None => None,
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 {
""
}; };
self.android_tools_dir(ndk) Ok(Self { inner, next })
.join(format!("{}24-clang{}", triplet, suffix))
} }
pub(crate) fn android_min_sdk_version(&self) -> u32 { fn update_from_arg_matches(&mut self, _matches: &ArgMatches) -> Result<(), clap::Error> {
// todo(jon): this should be configurable unimplemented!()
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)
} }
} }
-1
View File
@@ -3,7 +3,6 @@ use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ApplicationConfig { pub(crate) struct ApplicationConfig {
#[serde(default)]
pub(crate) asset_dir: Option<PathBuf>, pub(crate) asset_dir: Option<PathBuf>,
#[serde(default)] #[serde(default)]
-51
View File
@@ -1,7 +1,4 @@
use super::*; use super::*;
use crate::Result;
use anyhow::Context;
use krates::{Krates, NodeId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)] #[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.
-3
View File
@@ -1,7 +1,4 @@
#![allow(unused)] // lots of configs...
use clap::Parser; use clap::Parser;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
/// The arguments for the address the server will run on /// The arguments for the address the server will run on
#[derive(Clone, Debug, Default, Parser)] #[derive(Clone, Debug, Default, Parser)]
+30
View File
@@ -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()
}
-855
View File
@@ -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)
}
+7 -5
View File
@@ -1,4 +1,4 @@
use crate::metadata::CargoError; use std::fmt::Debug;
use thiserror::Error as ThisError; use thiserror::Error as ThisError;
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>; pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
@@ -22,7 +22,7 @@ pub(crate) enum Error {
Runtime(String), Runtime(String),
#[error("Cargo Error: {0}")] #[error("Cargo Error: {0}")]
Cargo(#[from] CargoError), Cargo(String),
#[error("Invalid proxy URL: {0}")] #[error("Invalid proxy URL: {0}")]
InvalidProxy(#[from] hyper::http::uri::InvalidUri), InvalidProxy(#[from] hyper::http::uri::InvalidUri),
@@ -33,9 +33,11 @@ pub(crate) enum Error {
#[error("Failed to bundle project: {0}")] #[error("Failed to bundle project: {0}")]
BundleFailed(#[from] tauri_bundler::Error), BundleFailed(#[from] tauri_bundler::Error),
#[allow(unused)] #[error("Failed to perform hotpatch: {0}")]
#[error("Unsupported feature: {0}")] PatchingFailed(#[from] crate::build::PatchError),
UnsupportedFeature(String),
#[error("{0}")]
CapturedPanic(String),
#[error("Failed to render template: {0}")] #[error("Failed to render template: {0}")]
TemplateParse(#[from] handlebars::RenderError), TemplateParse(#[from] handlebars::RenderError),
-168
View File
@@ -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)
}
}
+11 -7
View File
@@ -69,11 +69,11 @@ impl TraceController {
EnvFilter::from_env(LOG_ENV) EnvFilter::from_env(LOG_ENV)
} else if matches!(args.action, Commands::Serve(_)) { } else if matches!(args.action, Commands::Serve(_)) {
EnvFilter::new( 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 { } else {
EnvFilter::new(format!( 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 { our_level = if args.verbosity.verbose {
"debug" "debug"
} else { } else {
@@ -154,19 +154,23 @@ impl TraceController {
ServeUpdate::TracingLog { log } ServeUpdate::TracingLog { log }
} }
}
impl Drop for TraceController { pub(crate) fn shutdown_panic(&mut self) {
fn drop(&mut self) {
TUI_ACTIVE.store(false, Ordering::Relaxed); TUI_ACTIVE.store(false, Ordering::Relaxed);
// re-emit any remaining messages // re-emit any remaining messages
while let Ok(Some(msg)) = self.tui_rx.try_next() { 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::Text(text) => text,
TraceContent::Cargo(msg) => msg.message.to_string(), 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
View File
@@ -2,58 +2,61 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(clippy::doc_overindented_list_items)]
mod build; mod build;
mod bundle_utils; mod bundle_utils;
mod cli; mod cli;
mod config; mod config;
mod dioxus_crate; mod devcfg;
mod dx_build_info; mod dx_build_info;
mod error; mod error;
mod fastfs; mod fastfs;
mod filemap;
mod logging; mod logging;
mod metadata;
mod platform; mod platform;
mod rustc; mod rustcwrapper;
mod serve; mod serve;
mod settings; mod settings;
mod wasm_bindgen; mod wasm_bindgen;
mod wasm_opt; mod wasm_opt;
mod workspace;
pub(crate) use build::*; pub(crate) use build::*;
pub(crate) use cli::*; pub(crate) use cli::*;
pub(crate) use config::*; pub(crate) use config::*;
pub(crate) use dioxus_crate::*;
pub(crate) use dioxus_dx_wire_format::*; pub(crate) use dioxus_dx_wire_format::*;
pub(crate) use error::*; pub(crate) use error::*;
pub(crate) use filemap::*; pub(crate) use link::*;
pub(crate) use logging::*; pub(crate) use logging::*;
pub(crate) use platform::*; pub(crate) use platform::*;
pub(crate) use rustc::*; pub(crate) use rustcwrapper::*;
pub(crate) use settings::*; pub(crate) use settings::*;
pub(crate) use wasm_bindgen::*;
pub(crate) use workspace::*;
#[tokio::main] #[tokio::main]
async fn 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 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() { if let Some(link_args) = link::LinkAction::from_env() {
return link_action.run(); return link_args.run_link().await;
} }
let args = TraceController::initialize(); let args = TraceController::initialize();
#[cfg(debug_assertions)]
tracing::warn!("CLI was built with debug profile. Commands will run slower.");
let result = match args.action { let result = match args.action {
Commands::Translate(opts) => opts.translate(), Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create(), Commands::New(opts) => opts.create(),
Commands::Init(opts) => opts.init(), Commands::Init(opts) => opts.init(),
Commands::Config(opts) => opts.config(), Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat(), Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await, Commands::Check(opts) => opts.check().await,
Commands::Clean(opts) => opts.clean().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::Serve(opts) => opts.serve().await,
Commands::Bundle(opts) => opts.bundle().await, Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await, Commands::Run(opts) => opts.run().await,
@@ -66,8 +69,9 @@ async fn main() {
tracing::debug!(json = ?output); tracing::debug!(json = ?output);
} }
Err(err) => { Err(err) => {
eprintln!("{err}");
tracing::error!( tracing::error!(
?err,
json = ?StructuredOutput::Error { json = ?StructuredOutput::Error {
message: format!("{err:?}"), message: format!("{err:?}"),
}, },
-79
View File
@@ -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
}
-40
View File
@@ -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()
}
}
+77
View File
@@ -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())
}
-32
View File
@@ -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
}
-856
View File
@@ -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
View File
@@ -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 ansi_buffer;
mod detect;
mod handle;
mod output; mod output;
mod proxy; mod proxy;
mod runner; mod runner;
mod server; mod server;
mod update; mod update;
mod watcher;
pub(crate) use handle::*;
pub(crate) use output::*; pub(crate) use output::*;
pub(crate) use runner::*; pub(crate) use runner::*;
pub(crate) use server::*; pub(crate) use server::*;
pub(crate) use update::*; 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. /// 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: /// 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 /// want to emulate GithubPages here since most folks are deploying there and expect things like
/// basepath to match. /// basepath to match.
/// - Desktop: We spin up the dev server but without a filesystem server. /// - 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'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 /// - 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. /// to a dynamic one on the fly.
pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> { pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
// Redirect all logging the cli logger // Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
let mut tracer = TraceController::redirect(args.is_interactive_tty()); let mut builder = AppServer::start(args).await?;
let mut devserver = WebServer::start(&builder)?;
// Load the krate and resolve the server args against it - this might log so do it after we turn on the tracer first let mut screen = Output::start(builder.interactive).await?;
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);
// This is our default splash screen. We might want to make this a fancier splash screen in the future // 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 // 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 • Press `/` for more commands and shortcuts
Learn more at https://dioxuslabs.com/learn/0.6/getting_started 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 // 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 // And then wait for any updates before redrawing
let msg = tokio::select! { let msg = tokio::select! {
msg = builder.wait() => ServeUpdate::BuildUpdate(msg), msg = builder.wait() => msg,
msg = watcher.wait() => msg,
msg = devserver.wait() => msg, msg = devserver.wait() => msg,
msg = screen.wait() => msg, msg = screen.wait() => msg,
msg = runner.wait() => msg,
msg = tracer.wait() => msg, msg = tracer.wait() => msg,
}; };
match msg { match msg {
ServeUpdate::FilesChanged { files } => { ServeUpdate::FilesChanged { files } => {
if files.is_empty() || !args.should_hotreload() { if files.is_empty() || !builder.hot_reload {
continue; continue;
} }
let file = files[0].display().to_string(); tracing::debug!("Starting hotpatching: {:?}", files);
let file = file.trim_start_matches(&krate.crate_dir().display().to_string()); builder.handle_file_change(&files, &mut devserver).await;
// 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);
} }
ServeUpdate::RequestRebuild => { ServeUpdate::RequestRebuild => {
// The spacing here is important-ish: we want // The spacing here is important-ish: we want
// `Full rebuild:` to line up with // `Full rebuild:` to line up with
// `Hotreloading:` to keep the alignment during long edit sessions // `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"); tracing::info!("Full rebuild: triggered manually");
builder.full_rebuild().await;
builder.rebuild(args.build_arguments.clone());
runner.file_map.force_rebuild();
devserver.send_reload_start().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 => { 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}") tracing::error!("Failed to open app: {err}")
} }
} }
@@ -232,10 +190,10 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
} }
ServeUpdate::ToggleShouldRebuild => { ServeUpdate::ToggleShouldRebuild => {
runner.should_full_rebuild = !runner.should_full_rebuild; builder.automatic_rebuilds = !builder.automatic_rebuilds;
tracing::info!( tracing::info!(
"Automatic rebuilds are currently: {}", "Automatic rebuilds are currently: {}",
if runner.should_full_rebuild { if builder.automatic_rebuilds {
"enabled" "enabled"
} else { } else {
"disabled" "disabled"
@@ -243,21 +201,16 @@ pub(crate) async fn serve_all(mut args: ServeArgs) -> Result<()> {
) )
} }
ServeUpdate::Exit { error } => match error { ServeUpdate::Exit { error } => {
Some(err) => break Err(anyhow::anyhow!("{}", err).into()), _ = builder.cleanup_all().await;
None => break Ok(()),
},
}
};
_ = runner.cleanup().await;
_ = devserver.shutdown().await; _ = devserver.shutdown().await;
builder.abort_all();
_ = screen.shutdown(); _ = screen.shutdown();
if let Err(err) = err { match error {
eprintln!("Exiting with error: {}", err); Some(err) => return Err(anyhow::anyhow!("{}", err).into()),
None => return Ok(()),
}
}
}
} }
Ok(())
} }
+60 -70
View File
@@ -1,7 +1,6 @@
use crate::{ use crate::{
serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer}, serve::{ansi_buffer::AnsiStringLine, ServeUpdate, WebServer},
BuildStage, BuildUpdate, DioxusCrate, Platform, RustcDetails, ServeArgs, TraceContent, BuildStage, BuilderUpdate, Platform, TraceContent, TraceMsg, TraceSrc,
TraceMsg, TraceSrc,
}; };
use crossterm::{ use crossterm::{
cursor::{Hide, Show}, cursor::{Hide, Show},
@@ -26,6 +25,8 @@ use std::{
}; };
use tracing::Level; use tracing::Level;
use super::AppServer;
const TICK_RATE_MS: u64 = 100; const TICK_RATE_MS: u64 = 100;
const VIEWPORT_MAX_WIDTH: u16 = 100; const VIEWPORT_MAX_WIDTH: u16 = 100;
const VIEWPORT_HEIGHT_SMALL: u16 = 5; const VIEWPORT_HEIGHT_SMALL: u16 = 5;
@@ -46,7 +47,6 @@ pub struct Output {
// A list of all messages from build, dev, app, and more. // A list of all messages from build, dev, app, and more.
more_modal_open: bool, more_modal_open: bool,
interactive: bool, interactive: bool,
platform: Platform,
// Whether to show verbose logs or not // Whether to show verbose logs or not
// We automatically hide "debug" logs if verbose is false (only showing "info" / "warn" / "error") // 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 // ! 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...) // "render" method only borrows &self (for no particular reason at all...)
throbber: RefCell<throbber_widgets_tui::ThrobberState>, throbber: RefCell<throbber_widgets_tui::ThrobberState>,
rustc_details: RustcDetails,
} }
#[allow(unused)]
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct RenderState<'a> { struct RenderState<'a> {
opts: &'a ServeArgs, runner: &'a AppServer,
krate: &'a DioxusCrate,
build_engine: &'a Builder,
server: &'a WebServer, server: &'a WebServer,
watcher: &'a Watcher,
} }
impl Output { 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 { let mut output = Self {
interactive,
term: Rc::new(RefCell::new(None)), term: Rc::new(RefCell::new(None)),
interactive: cfg.is_interactive_tty(),
dx_version: format!( dx_version: format!(
"{}-{}", "{}-{}",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("main") crate::dx_build_info::GIT_COMMIT_HASH_SHORT.unwrap_or("main")
), ),
platform: cfg.build_arguments.platform.expect("To be resolved by now"),
events: None, events: None,
more_modal_open: false, more_modal_open: false,
pending_logs: VecDeque::new(), pending_logs: VecDeque::new(),
@@ -101,7 +94,6 @@ impl Output {
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval interval
}, },
rustc_details: RustcDetails::from_cli().await?,
}; };
output.startup()?; output.startup()?;
@@ -113,15 +105,6 @@ impl Output {
/// This is meant to be paired with "shutdown" to restore the terminal to its original state. /// This is meant to be paired with "shutdown" to restore the terminal to its original state.
fn startup(&mut self) -> io::Result<()> { fn startup(&mut self) -> io::Result<()> {
if self.interactive { 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. // 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` // 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. // 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 /// 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 /// in "startup" for more details about what we need to unset
pub(crate) fn shutdown(&self) -> io::Result<()> { 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() stdout()
.execute(Show)? .execute(Show)?
.execute(DisableFocusChange)? .execute(DisableFocusChange)?
@@ -329,7 +317,7 @@ impl Output {
} }
/// Push a message from the websocket to the logs /// 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; use dioxus_devtools_types::ClientMsg;
// We can only handle text messages from the websocket... // 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()); let res = serde_json::from_str::<ClientMsg>(text.as_str());
// Client logs being errors aren't fatal, but we should still report them them // 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, Ok(msg) => msg,
Err(err) => { 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; return;
} }
}; };
let ClientMsg::Log { level, messages } = msg else {
return;
};
// FIXME(jon): why are we pulling only the first message here? // FIXME(jon): why are we pulling only the first message here?
let content = messages.first().unwrap_or(&String::new()).clone(); 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 /// 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 /// re-render when external state changes. Ratatui will diff the intermediate buffer, so we at least
/// we won't be drawing it. /// 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 { match update {
BuildUpdate::Progress { BuilderUpdate::Progress {
stage: BuildStage::Starting { .. }, stage: BuildStage::Starting { .. },
} => self.tick_animation = true, } => self.tick_animation = true,
BuildUpdate::BuildReady { .. } => self.tick_animation = false, BuilderUpdate::BuildReady { .. } => self.tick_animation = false,
BuildUpdate::BuildFailed { .. } => self.tick_animation = false, BuilderUpdate::BuildFailed { .. } => self.tick_animation = false,
_ => {} _ => {}
} }
} }
/// Render the current state of everything to the console screen /// Render the current state of everything to the console screen
pub fn render( pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
&mut self,
opts: &ServeArgs,
config: &DioxusCrate,
build_engine: &Builder,
server: &WebServer,
watcher: &Watcher,
) {
if !self.interactive { if !self.interactive {
return; 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 // Then, draw the frame, passing along all the state of the TUI so we can render it properly
_ = term.draw(|frame| { _ = term.draw(|frame| {
self.render_frame( self.render_frame(frame, RenderState { runner, server });
frame,
RenderState {
opts,
krate: config,
build_engine,
server,
watcher,
},
);
}); });
} }
@@ -488,39 +464,46 @@ impl Output {
]) ])
.areas(gauge_area); .areas(gauge_area);
let client = &state.runner.client();
self.render_single_gauge( self.render_single_gauge(
frame, frame,
app_progress, app_progress,
state.build_engine.compile_progress(), client.compile_progress(),
"App: ", "App: ",
state, 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( self.render_single_gauge(
frame, frame,
second_progress, second_progress,
state.build_engine.server_compile_progress(), state.runner.server_compile_progress(),
"Server: ", "Server: ",
state, state,
state.build_engine.compile_duration(), client.compile_duration(),
); );
} else { } else {
self.render_single_gauge( self.render_single_gauge(
frame, frame,
second_progress, second_progress,
state.build_engine.bundle_progress(), client.bundle_progress(),
"Bundle: ", "Bundle: ",
state, state,
state.build_engine.bundle_duration(), client.bundle_duration(),
); );
} }
let mut lines = vec!["Status: ".white()]; let mut lines = vec!["Status: ".white()];
match &state.build_engine.stage { match &client.stage {
BuildStage::Initializing => lines.push("Initializing".yellow()), 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::InstallingTooling => lines.push("Installing tooling".yellow()),
BuildStage::Compiling { BuildStage::Compiling {
current, current,
@@ -535,7 +518,6 @@ impl Output {
BuildStage::OptimizingWasm => lines.push("Optimizing wasm".yellow()), BuildStage::OptimizingWasm => lines.push("Optimizing wasm".yellow()),
BuildStage::SplittingBundle => lines.push("Splitting bundle".yellow()), BuildStage::SplittingBundle => lines.push("Splitting bundle".yellow()),
BuildStage::CompressingAssets => lines.push("Compressing assets".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::RunningBindgen => lines.push("Running wasm-bindgen".yellow()),
BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()), BuildStage::RunningGradle => lines.push("Running gradle assemble".yellow()),
BuildStage::Bundling => lines.push("Bundling app".yellow()), BuildStage::Bundling => lines.push("Bundling app".yellow()),
@@ -552,15 +534,18 @@ impl Output {
} }
BuildStage::Success => { BuildStage::Success => {
lines.push("Serving ".yellow()); lines.push("Serving ".yellow());
lines.push(state.krate.executable_name().white()); lines.push(client.build.executable_name().white());
lines.push(" 🚀 ".green()); 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()); lines.push(format!("{:.1}s", comp_time.as_secs_f32()).dark_gray());
} }
} }
BuildStage::Failed => lines.push("Failed".red()), BuildStage::Failed => lines.push("Failed".red()),
BuildStage::Aborted => lines.push("Aborted".red()), BuildStage::Aborted => lines.push("Aborted".red()),
BuildStage::Restarting => lines.push("Restarting".yellow()), 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, state: RenderState,
time_taken: Option<Duration>, 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 value = if failed { 1.0 } else { value.clamp(0.0, 1.0) };
let [gauge_row, _, icon] = Layout::horizontal([ let [gauge_row, _, icon] = Layout::horizontal([
@@ -647,11 +632,12 @@ impl Output {
]) ])
.areas(area); .areas(area);
let client = &state.runner.client();
frame.render_widget( frame.render_widget(
Paragraph::new(Line::from(vec![ Paragraph::new(Line::from(vec![
"Platform: ".gray(), "Platform: ".gray(),
self.platform.expected_name().yellow(), client.build.platform.expected_name().yellow(),
if state.opts.build_arguments.fullstack() { if state.runner.is_fullstack() {
" + fullstack".yellow() " + fullstack".yellow()
} else { } else {
" ".dark_gray() " ".dark_gray()
@@ -671,7 +657,7 @@ impl Output {
frame.render_widget_ref( frame.render_widget_ref(
Paragraph::new(Line::from(vec![ Paragraph::new(Line::from(vec![
if self.platform == Platform::Web { if client.build.platform == Platform::Web {
"Serving at: ".gray() "Serving at: ".gray()
} else { } else {
"ServerFns at: ".gray() "ServerFns at: ".gray()
@@ -688,7 +674,7 @@ impl Output {
Paragraph::new(Line::from({ Paragraph::new(Line::from({
let mut lines = vec!["App features: ".gray(), "[".yellow()]; 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(); let num_features = feature_list.len();
for (idx, feature) in feature_list.into_iter().enumerate() { for (idx, feature) in feature_list.into_iter().enumerate() {
@@ -737,7 +723,7 @@ impl Output {
frame.render_widget( frame.render_widget(
Paragraph::new(Line::from(vec![ Paragraph::new(Line::from(vec![
"rustc: ".gray(), "rustc: ".gray(),
self.rustc_details.version.as_str().yellow(), state.runner.workspace.rustc_version.as_str().yellow(),
])), ])),
meta_list[2], meta_list[2],
); );
@@ -869,8 +855,8 @@ impl Output {
.iter() .iter()
.map(|line| { .map(|line| {
// Very important to strip ansi codes before counting graphemes - the ansi codes count as multiple graphemes! // 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; let grapheme_count = console::strip_ansi_codes(line).graphemes(true).count();
grapheme_count.max(1).div_ceil(term_size.width) grapheme_count.max(1).div_ceil(term_size.width as usize) as u16
}) })
.sum::<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 // 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(); 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)); lines.push(AnsiStringLine::new(line_length as _).render(&line));
} else {
lines.push(line.to_string())
}
} }
} }
File diff suppressed because it is too large. Load diff
+118 -122
View File
@@ -1,14 +1,13 @@
use crate::{ use crate::{
config::WebHttpsConfig, config::WebHttpsConfig, serve::ServeUpdate, BuildId, BuildStage, BuilderUpdate, Platform,
serve::{ServeArgs, ServeUpdate}, Result, TraceSrc,
BuildStage, BuildUpdate, DioxusCrate, Platform, Result, TraceSrc,
}; };
use anyhow::Context; use anyhow::Context;
use axum::{ use axum::{
body::Body, body::Body,
extract::{ extract::{
ws::{Message, WebSocket}, ws::{Message, WebSocket},
Request, State, WebSocketUpgrade, Query, Request, State, WebSocketUpgrade,
}, },
http::{ http::{
header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA}, header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA},
@@ -34,13 +33,18 @@ use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
path::Path, path::Path,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
time::Duration,
}; };
use subsecond_types::JumpTable;
use tokio::process::Command;
use tower_http::{ use tower_http::{
cors::Any, cors::Any,
services::fs::{ServeDir, ServeFileSystemResponseBody}, services::fs::{ServeDir, ServeFileSystemResponseBody},
ServiceBuilderExt, ServiceBuilderExt,
}; };
use super::AppServer;
/// The webserver that serves statics assets (if fullstack isn't already doing that) and the websocket /// 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. /// 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_bind_ip: IpAddr,
devserver_port: u16, devserver_port: u16,
proxied_port: Option<u16>, proxied_port: Option<u16>,
hot_reload_sockets: Vec<WebSocket>, hot_reload_sockets: Vec<ConnectedWsClient>,
build_status_sockets: Vec<WebSocket>, build_status_sockets: Vec<ConnectedWsClient>,
new_hot_reload_sockets: UnboundedReceiver<WebSocket>, new_hot_reload_sockets: UnboundedReceiver<ConnectedWsClient>,
new_build_status_sockets: UnboundedReceiver<WebSocket>, new_build_status_sockets: UnboundedReceiver<ConnectedWsClient>,
build_status: SharedStatus, build_status: SharedStatus,
application_name: String, application_name: String,
platform: Platform, platform: Platform,
} }
pub(crate) struct ConnectedWsClient {
socket: WebSocket,
build_id: Option<BuildId>,
aslr_reference: Option<u64>,
}
impl WebServer { impl WebServer {
pub const SELF_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
/// Start the development server. /// Start the development server.
/// This will set up the default http server if there's no server specified (usually via fullstack). /// 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 /// 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. /// 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 (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(); 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 // 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 // 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 devserver_bind_address = SocketAddr::new(devserver_bind_ip, devserver_port);
let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| { let listener = std::net::TcpListener::bind(devserver_bind_address).with_context(|| {
anyhow::anyhow!( 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)); 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 // 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 build_status = SharedStatus::new_with_starting_build();
let router = build_devserver_router( let router = build_devserver_router(
args, runner,
krate,
hot_reload_sockets_tx, hot_reload_sockets_tx,
build_status_sockets_tx, build_status_sockets_tx,
proxied_address, proxied_address,
@@ -122,7 +111,7 @@ impl WebServer {
// And finally, start the server mainloop // And finally, start the server mainloop
tokio::spawn(devserver_mainloop( tokio::spawn(devserver_mainloop(
krate.config.web.https.clone(), runner.client().build.config.web.https.clone(),
listener, listener,
router, router,
)); ));
@@ -137,8 +126,8 @@ impl WebServer {
build_status_sockets: Default::default(), build_status_sockets: Default::default(),
new_hot_reload_sockets: hot_reload_sockets_rx, new_hot_reload_sockets: hot_reload_sockets_rx,
new_build_status_sockets: build_status_sockets_rx, new_build_status_sockets: build_status_sockets_rx,
application_name: krate.executable_name().to_string(), application_name: runner.app_name().to_string(),
platform: args.build_arguments.platform(), platform: runner.client.build.platform,
}) })
} }
@@ -150,15 +139,19 @@ impl WebServer {
.hot_reload_sockets .hot_reload_sockets
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.map(|(idx, socket)| async move { (idx, socket.next().await) }) .map(|(idx, socket)| async move { (idx, socket.socket.next().await) })
.collect::<FuturesUnordered<_>>(); .collect::<FuturesUnordered<_>>();
tokio::select! { tokio::select! {
new_hot_reload_socket = &mut new_hot_reload_socket => { new_hot_reload_socket = &mut new_hot_reload_socket => {
if let Some(new_socket) = 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); drop(new_message);
self.hot_reload_sockets.push(new_socket); self.hot_reload_sockets.push(new_socket);
return ServeUpdate::NewConnection;
return ServeUpdate::NewConnection { aslr_reference, id };
} else { } else {
panic!("Could not receive a socket - the devtools could not boot - the port is likely already in use"); 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 // 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 }); 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() { if project_info.send_to(&mut new_socket.socket).await.is_ok() {
_ = self.build_status.send_to(&mut new_socket).await; _ = self.build_status.send_to(&mut new_socket.socket).await;
self.build_status_sockets.push(new_socket); self.build_status_sockets.push(new_socket);
} }
return future::pending::<ServeUpdate>().await; return future::pending::<ServeUpdate>().await;
@@ -180,7 +173,7 @@ impl WebServer {
} }
Some((idx, message)) = new_message.next() => { Some((idx, message)) = new_message.next() => {
match message { match message {
Some(Ok(message)) => return ServeUpdate::WsMessage(message), Some(Ok(msg)) => return ServeUpdate::WsMessage { msg, platform: Platform::Web },
_ => { _ => {
drop(new_message); drop(new_message);
_ = self.hot_reload_sockets.remove(idx); _ = self.hot_reload_sockets.remove(idx);
@@ -193,26 +186,9 @@ impl WebServer {
} }
pub(crate) async fn shutdown(&mut self) { 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; self.send_shutdown().await;
for mut socket in self.hot_reload_sockets.drain(..) { 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; let mut i = 0;
while i < self.build_status_sockets.len() { while i < self.build_status_sockets.len() {
let socket = &mut self.build_status_sockets[i]; 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); self.build_status_sockets.remove(i);
} else { } else {
i += 1; i += 1;
@@ -239,13 +215,9 @@ impl WebServer {
} }
/// Sends an updated build status to all clients. /// Sends an updated build status to all clients.
pub(crate) async fn new_build_update( pub(crate) async fn new_build_update(&mut self, update: &BuilderUpdate) {
&mut self,
update: &BuildUpdate,
builder: &super::Builder,
) {
match update { match update {
BuildUpdate::Progress { stage } => { BuilderUpdate::Progress { stage } => {
// Todo(miles): wire up more messages into the splash screen UI // Todo(miles): wire up more messages into the splash screen UI
match stage { match stage {
BuildStage::Success => {} BuildStage::Success => {}
@@ -259,7 +231,10 @@ impl WebServer {
krate, krate,
.. ..
} => { } => {
if !builder.is_finished() { if !matches!(
self.build_status.get(),
Status::Ready | Status::BuildError { .. }
) {
self.build_status.set(Status::Building { self.build_status.set(Status::Building {
progress: (*current as f64 / *total as f64).clamp(0.0, 1.0), progress: (*current as f64 / *total as f64).clamp(0.0, 1.0),
build_message: format!("{krate} compiling"), build_message: format!("{krate} compiling"),
@@ -273,9 +248,9 @@ impl WebServer {
_ => {} _ => {}
} }
} }
BuildUpdate::CompilerMessage { .. } => {} BuilderUpdate::CompilerMessage { .. } => {}
BuildUpdate::BuildReady { .. } => {} BuilderUpdate::BuildReady { .. } => {}
BuildUpdate::BuildFailed { err } => { BuilderUpdate::BuildFailed { err } => {
let error = err.to_string(); let error = err.to_string();
self.build_status.set(Status::BuildError { self.build_status.set(Status::BuildError {
error: ansi_to_html::convert(&error).unwrap_or(error), error: ansi_to_html::convert(&error).unwrap_or(error),
@@ -283,6 +258,9 @@ impl WebServer {
self.send_reload_failed().await; self.send_reload_failed().await;
self.send_build_status().await; self.send_build_status().await;
} }
BuilderUpdate::StdoutReceived { .. } => {}
BuilderUpdate::StderrReceived { .. } => {}
BuilderUpdate::ProcessExited { .. } => {}
} }
} }
@@ -292,7 +270,6 @@ impl WebServer {
return; return;
} }
#[cfg(debug_assertions)]
tracing::trace!("Sending hotreload to clients {:?}", reload); tracing::trace!("Sending hotreload to clients {:?}", reload);
let msg = DevserverMsg::HotReload(reload); let msg = DevserverMsg::HotReload(reload);
@@ -303,6 +280,7 @@ impl WebServer {
while i < self.hot_reload_sockets.len() { while i < self.hot_reload_sockets.len() {
let socket = &mut self.hot_reload_sockets[i]; let socket = &mut self.hot_reload_sockets[i];
if socket if socket
.socket
.send(Message::Text(msg.clone().into())) .send(Message::Text(msg.clone().into()))
.await .await
.is_err() .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. /// Tells all clients that a full rebuild has started.
pub(crate) async fn send_reload_start(&mut self) { pub(crate) async fn send_reload_start(&mut self) {
self.send_devserver_message(DevserverMsg::FullReloadStart) self.send_devserver_message_to_all(DevserverMsg::FullReloadStart)
.await; .await;
} }
/// Tells all clients that a full rebuild has failed. /// Tells all clients that a full rebuild has failed.
pub(crate) async fn send_reload_failed(&mut self) { pub(crate) async fn send_reload_failed(&mut self) {
self.send_devserver_message(DevserverMsg::FullReloadFailed) self.send_devserver_message_to_all(DevserverMsg::FullReloadFailed)
.await; .await;
} }
@@ -334,19 +334,21 @@ impl WebServer {
self.build_status.set(Status::Ready); self.build_status.set(Status::Ready);
self.send_build_status().await; self.send_build_status().await;
self.send_devserver_message(DevserverMsg::FullReloadCommand) self.send_devserver_message_to_all(DevserverMsg::FullReloadCommand)
.await; .await;
} }
/// Send a shutdown message to all connected clients. /// Send a shutdown message to all connected clients.
pub(crate) async fn send_shutdown(&mut self) { 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. /// 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() { for socket in self.hot_reload_sockets.iter_mut() {
_ = socket _ = socket
.socket
.send(Message::Text(serde_json::to_string(&msg).unwrap().into())) .send(Message::Text(serde_json::to_string(&msg).unwrap().into()))
.await; .await;
} }
@@ -424,23 +426,24 @@ async fn devserver_mainloop(
/// - Setting up the file serve service /// - Setting up the file serve service
/// - Setting up the websocket endpoint for devtools /// - Setting up the websocket endpoint for devtools
fn build_devserver_router( fn build_devserver_router(
args: &ServeArgs, runner: &AppServer,
krate: &DioxusCrate, hot_reload_sockets: UnboundedSender<ConnectedWsClient>,
hot_reload_sockets: UnboundedSender<WebSocket>, build_status_sockets: UnboundedSender<ConnectedWsClient>,
build_status_sockets: UnboundedSender<WebSocket>,
fullstack_address: Option<SocketAddr>, fullstack_address: Option<SocketAddr>,
build_status: SharedStatus, build_status: SharedStatus,
) -> Result<Router> { ) -> Result<Router> {
let mut router = Router::new(); let mut router = Router::new();
let build = runner.client();
// Setup proxy for the endpoint specified in the config // 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)?; 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 // 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}"); tracing::debug!("Proxying requests to fullstack server at {address}");
router = router.fallback_service(super::proxy::proxy_to( router = router.fallback_service(super::proxy::proxy_to(
format!("http://{address}").parse().unwrap(), 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 // Route file service to output the .wasm and assets if this is a web build
let base_path = format!( let base_path = format!(
"/{}", "/{}",
krate runner
.client()
.build
.config .config
.web .web
.app .app
@@ -470,9 +475,9 @@ fn build_devserver_router(
.trim_matches('/') .trim_matches('/')
); );
if base_path == "/" { if base_path == "/" {
router = router.fallback_service(build_serve_dir(args, krate)); router = router.fallback_service(build_serve_dir(runner));
} else { } 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, 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 // Setup websocket endpoint - and pass in the extension layer immediately after
router = router.nest( router = router.nest(
"/_dioxus", "/_dioxus",
@@ -489,9 +500,9 @@ fn build_devserver_router(
.route( .route(
"/", "/",
get( get(
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<WebSocket>>| async move { |ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>, query: Query<ConnectionQuery>| async move {
tracing::debug!("New devtool websocket connection"); tracing::debug!("New devtool websocket connection: {:?}", query);
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) }) 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( .route(
"/build_status", "/build_status",
get( get(
|ws: WebSocketUpgrade, ext: Extension<UnboundedSender<WebSocket>>| async move { |ws: WebSocketUpgrade, ext: Extension<UnboundedSender<ConnectedWsClient>>| async move {
ws.on_upgrade(move |socket| async move { _ = ext.0.unbounded_send(socket) }) 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) 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; use tower::ServiceBuilder;
static CORS_UNSAFE: (HeaderValue, HeaderValue) = ( 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"), 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(), true => CORS_REQUIRE.clone(),
false => CORS_UNSAFE.clone(), false => CORS_UNSAFE.clone(),
}; };
let out_dir = cfg let app = &runner.client;
.build_dir(Platform::Web, args.build_arguments.release) let cfg = &runner.client.build.config;
let out_dir = app
.build
.build_dir(Platform::Web, app.build.release)
.join("public"); .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( get_service(
ServiceBuilder::new() ServiceBuilder::new()
@@ -641,7 +656,7 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
_ = fs::create_dir("ssl"); _ = fs::create_dir("ssl");
} }
let cmd = tokio::process::Command::new("mkcert") let cmd = Command::new("mkcert")
.args([ .args([
"-install", "-install",
"-key-file", "-key-file",
@@ -673,25 +688,6 @@ async fn get_rustls(web_config: &WebHttpsConfig) -> Result<(String, String)> {
Ok((cert_path, key_path)) 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 /// Middleware that intercepts html requests if the status is "Building" and returns a loading page instead
async fn build_status_middleware( async fn build_status_middleware(
@@ -708,7 +704,7 @@ async fn build_status_middleware(
if let Some(true) = accepts_html { if let Some(true) = accepts_html {
let status = state.get(); let status = state.get();
if status != Status::Ready { 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() return axum::response::Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
// Load the html loader then keep loading forever // Load the html loader then keep loading forever
+12 -27
View File
@@ -1,46 +1,31 @@
use crate::{BuildUpdate, Platform, TraceMsg}; use crate::{BuildId, BuilderUpdate, Platform, TraceMsg};
use axum::extract::ws::Message as WsMessage; use axum::extract::ws::Message as WsMessage;
use std::{path::PathBuf, process::ExitStatus}; use std::path::PathBuf;
/// One fat enum to rule them all.... /// One fat enum to rule them all....
/// ///
/// Thanks to libraries like winit for the inspiration /// Thanks to libraries like winit for the inspiration
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub(crate) enum ServeUpdate { pub(crate) enum ServeUpdate {
NewConnection, NewConnection {
WsMessage(WsMessage), id: BuildId,
aslr_reference: Option<u64>,
/// A build update from the build engine },
BuildUpdate(BuildUpdate), WsMessage {
/// A running process has received a stdout.
/// May or may not be a complete line - do not treat it as a line. It will include a line if it is a complete line.
///
/// We will poll lines and any content in a 50ms interval
StdoutReceived {
platform: Platform, platform: Platform,
msg: String, msg: WsMessage,
}, },
/// A running process has received a stderr. /// An update regarding the state of the build and running app from an AppBuilder
/// 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. BuilderUpdate {
/// id: BuildId,
/// We will poll lines and any content in a 50ms interval update: BuilderUpdate,
StderrReceived {
platform: Platform,
msg: String,
},
ProcessExited {
platform: Platform,
status: ExitStatus,
}, },
FilesChanged { FilesChanged {
files: Vec<PathBuf>, files: Vec<PathBuf>,
}, },
/// Open an existing app bundle, if it exists
OpenApp, OpenApp,
RequestRebuild, RequestRebuild,
-165
View File
@@ -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),
)
}
+6 -2
View File
@@ -32,10 +32,14 @@ impl CliSettings {
/// Load the settings from the local, global, or default config in that order /// Load the settings from the local, global, or default config in that order
pub(crate) fn load() -> Arc<Self> { pub(crate) fn load() -> Arc<Self> {
static SETTINGS: Lazy<Arc<CliSettings>> = 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() SETTINGS.clone()
} }
pub fn global_or_default() -> Self {
CliSettings::from_global().unwrap_or_default()
}
/// Get the current settings structure from global. /// Get the current settings structure from global.
pub(crate) fn from_global() -> Option<Self> { pub(crate) fn from_global() -> Option<Self> {
let Some(path) = dirs::data_local_dir() else { let Some(path) = dirs::data_local_dir() else {
@@ -124,7 +128,7 @@ impl CliSettings {
return true; return true;
} }
if std::env::var("NO_DOWNLOADS").is_ok() { if crate::devcfg::no_downloads() {
return true; return true;
} }
+22 -17
View File
@@ -1,8 +1,7 @@
use crate::{CliSettings, Result}; use crate::{CliSettings, Result};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::{path::Path, process::Stdio};
use tar::Archive; use tar::Archive;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::{fs, process::Command}; use tokio::{fs, process::Command};
@@ -100,7 +99,7 @@ impl WasmBindgen {
} }
/// Run the bindgen command with the current settings /// 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 binary = self.get_binary_path().await?;
let mut args = Vec::new(); let mut args = Vec::new();
@@ -160,14 +159,24 @@ impl WasmBindgen {
tracing::debug!("wasm-bindgen args: {:#?}", args); tracing::debug!("wasm-bindgen args: {:#?}", args);
// Run bindgen // Run bindgen
Command::new(binary) let output = Command::new(binary).args(args).output().await?;
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.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 /// Verify the installed version of wasm-bindgen-cli
@@ -288,8 +297,6 @@ impl WasmBindgen {
"--install-path", "--install-path",
]) ])
.arg(tempdir.path()) .arg(tempdir.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output() .output()
.await?; .await?;
@@ -322,8 +329,6 @@ impl WasmBindgen {
"--root", "--root",
]) ])
.arg(tempdir.path()) .arg(tempdir.path())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output() .output()
.await .await
.context("failed to install wasm-bindgen-cli from cargo-install")?; .context("failed to install wasm-bindgen-cli from cargo-install")?;
@@ -455,7 +460,7 @@ mod test {
/// Test the github installer. /// Test the github installer.
#[tokio::test] #[tokio::test]
async fn test_github_install() { async fn test_github_install() {
if std::env::var("TEST_INSTALLS").is_err() { if !crate::devcfg::test_installs() {
return; return;
} }
let binary = WasmBindgen::new(VERSION); let binary = WasmBindgen::new(VERSION);
@@ -468,7 +473,7 @@ mod test {
/// Test the cargo installer. /// Test the cargo installer.
#[tokio::test] #[tokio::test]
async fn test_cargo_install() { async fn test_cargo_install() {
if std::env::var("TEST_INSTALLS").is_err() { if !crate::devcfg::test_installs() {
return; return;
} }
let binary = WasmBindgen::new(VERSION); let binary = WasmBindgen::new(VERSION);
@@ -482,7 +487,7 @@ mod test {
// Test the binstall installer // Test the binstall installer
#[tokio::test] #[tokio::test]
async fn test_binstall_install() { async fn test_binstall_install() {
if std::env::var("TEST_INSTALLS").is_err() { if !crate::devcfg::test_installs() {
return; return;
} }
let binary = WasmBindgen::new(VERSION); let binary = WasmBindgen::new(VERSION);
-9
View File
@@ -2,15 +2,6 @@ use crate::config::WasmOptLevel;
use crate::{Result, WasmOptConfig}; use crate::{Result, WasmOptConfig};
use std::path::Path; 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 /// Write these wasm bytes with a particular set of optimizations
pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> { pub async fn write_wasm(bytes: &[u8], output_path: &Path, cfg: &WasmOptConfig) -> Result<()> {
tokio::fs::write(output_path, bytes).await?; tokio::fs::write(output_path, bytes).await?;
+384
View File
@@ -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()
}
}
+1
View File
@@ -24,6 +24,7 @@ tracing = { workspace = true }
warnings = { workspace = true } warnings = { workspace = true }
futures-util = { workspace = true, default-features = false, features = ["alloc", "std"] } futures-util = { workspace = true, default-features = false, features = ["alloc", "std"] }
serde = { workspace = true, optional = true, features = ["derive"] } serde = { workspace = true, optional = true, features = ["derive"] }
subsecond = { workspace = true }
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true } dioxus = { workspace = true }
+1 -1
View File
@@ -65,7 +65,7 @@ use dioxus::prelude::*;
// First, declare a root component // First, declare a root component
fn app() -> Element { fn app() -> Element {
rsx!{ rsx! {
div { "hello world" } div { "hello world" }
} }
} }
+15
View File
@@ -479,3 +479,18 @@ pub fn use_hook_with_cleanup<T: Clone + 'static>(
use_drop(move || cleanup(_value)); use_drop(move || cleanup(_value));
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
View File
@@ -93,17 +93,18 @@ pub use crate::innerlude::{
pub mod prelude { pub mod prelude {
pub use crate::innerlude::{ pub use crate::innerlude::{
consume_context, consume_context_from_scope, current_owner, current_scope_id, consume_context, consume_context_from_scope, current_owner, current_scope_id,
fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope, fc_to_builder, force_all_dirty, generation, has_context, needs_update, needs_update_any,
provide_context, provide_error_boundary, provide_root_context, queue_effect, remove_future, parent_scope, provide_context, provide_error_boundary, provide_root_context, queue_effect,
schedule_update, schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, remove_future, schedule_update, schedule_update_any, spawn, spawn_forever,
suspense_context, throw_error, try_consume_context, use_after_render, use_before_render, spawn_isomorphic, suspend, suspense_context, throw_error, try_consume_context,
use_drop, use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, use_after_render, use_before_render, use_drop, use_hook, use_hook_with_cleanup, with_owner,
Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, AnyValue, Attribute, Callback, Component, ComponentFunction, Context, Element,
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, ErrorBoundary, ErrorContext, Event, EventHandler, Fragment, HasAttributes,
OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, ReactiveContext,
ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, RenderError, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto,
SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template, SuspendedFuture, SuspenseBoundary, SuspenseBoundaryProps, SuspenseContext,
TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom, SuspenseExtension, Task, Template, TemplateAttribute, TemplateNode, VNode, VNodeInner,
VirtualDom,
}; };
} }
+5 -7
View File
@@ -609,10 +609,8 @@ pub struct VComponent {
/// The name of this component /// The name of this component
pub name: &'static str, pub name: &'static str,
/// The function pointer of the component, known at compile time /// The raw pointer to the render function
/// pub(crate) render_fn: usize,
/// 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 props for this component /// The props for this component
pub(crate) props: BoxedAnyProps, pub(crate) props: BoxedAnyProps,
@@ -622,8 +620,8 @@ impl Clone for VComponent {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
name: self.name, name: self.name,
render_fn: self.render_fn,
props: self.props.duplicate(), props: self.props.duplicate(),
render_fn: self.render_fn,
} }
} }
} }
@@ -638,7 +636,7 @@ impl VComponent {
where where
P: Properties + 'static, P: Properties + 'static,
{ {
let render_fn = component.id(); let render_fn = component.fn_ptr();
let props = Box::new(VProps::new( let props = Box::new(VProps::new(
component, component,
<P as Properties>::memoize, <P as Properties>::memoize,
@@ -647,9 +645,9 @@ impl VComponent {
)); ));
VComponent { VComponent {
render_fn,
name: fn_name, name: fn_name,
props, props,
render_fn,
} }
} }
+22 -10
View File
@@ -1,4 +1,4 @@
use std::{any::TypeId, fmt::Arguments}; use std::fmt::Arguments;
use crate::innerlude::*; 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 { pub trait ComponentFunction<Props, Marker = ()>: Clone + 'static {
/// Get the type id of the component. /// Get the raw address of the component render function.
fn id(&self) -> TypeId { fn fn_ptr(&self) -> usize;
TypeId::of::<Self>()
}
/// Convert the component to a function that takes props and returns an element. /// Convert the component to a function that takes props and returns an element.
fn rebuild(&self, props: Props) -> Element; fn rebuild(&self, props: Props) -> Element;
} }
/// Accept any callbacks that take props /// 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 { 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 /// Accept any callbacks that take no props
pub struct EmptyMarker; pub struct EmptyMarker;
impl<F: Fn() -> Element + Clone + 'static> ComponentFunction<(), EmptyMarker> for F { impl<F> ComponentFunction<(), EmptyMarker> for F
fn rebuild(&self, _: ()) -> Element { where
self() 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
} }
} }
+3
View File
@@ -380,6 +380,9 @@ impl TaskType {
/// These messages control how the scheduler will process updates to the UI. /// These messages control how the scheduler will process updates to the UI.
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum SchedulerMsg { 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 updates from Components that mark them as dirty
Immediate(ScopeId), Immediate(ScopeId),
+28 -2
View File
@@ -16,7 +16,7 @@ use crate::{Task, VComponent};
use futures_util::StreamExt; use futures_util::StreamExt;
use slab::Slab; use slab::Slab;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::{any::Any, rc::Rc}; use std::{any::Any, rc::Rc, sync::Arc};
use tracing::instrument; use tracing::instrument;
/// A virtual node system that progresses user events and diffs UI trees. /// A virtual node system that progresses user events and diffs UI trees.
@@ -294,7 +294,7 @@ impl VirtualDom {
root: impl ComponentFunction<P, M>, root: impl ComponentFunction<P, M>,
root_props: P, root_props: P,
) -> Self { ) -> Self {
let render_fn = root.id(); let render_fn = root.fn_ptr();
let props = VProps::new(root, |_, _| true, root_props, "Root"); let props = VProps::new(root, |_, _| true, root_props, "Root");
Self::new_with_component(VComponent { Self::new_with_component(VComponent {
name: "root", name: "root",
@@ -331,6 +331,9 @@ impl VirtualDom {
); );
dom.new_scope(Box::new(root), "app"); dom.new_scope(Box::new(root), "app");
#[cfg(debug_assertions)]
dom.register_subsecond_handler();
dom dom
} }
@@ -375,6 +378,19 @@ impl VirtualDom {
self.base_scope().state().provide_any_context(context); 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 /// Manually mark a scope as requiring a re-render
/// ///
/// Whenever the Runtime "works", it will re-render this scope /// Whenever the Runtime "works", it will re-render this scope
@@ -458,6 +474,7 @@ impl VirtualDom {
self.mark_task_dirty(Task::from_id(id)); self.mark_task_dirty(Task::from_id(id));
} }
SchedulerMsg::EffectQueued => {} SchedulerMsg::EffectQueued => {}
SchedulerMsg::AllDirty => self.mark_all_dirty(),
}; };
} }
@@ -469,6 +486,7 @@ impl VirtualDom {
SchedulerMsg::Immediate(id) => self.mark_dirty(id), SchedulerMsg::Immediate(id) => self.mark_dirty(id),
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)), SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
SchedulerMsg::EffectQueued => {} SchedulerMsg::EffectQueued => {}
SchedulerMsg::AllDirty => self.mark_all_dirty(),
} }
} }
} }
@@ -744,6 +762,14 @@ impl VirtualDom {
let event = crate::Event::new(event, bubbling); let event = crate::Event::new(event, bubbling);
self.runtime().handle_event(name, event, element); 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 { impl Drop for VirtualDom {
+14
View File
@@ -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 }
+14
View File
@@ -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.
+1
View File
@@ -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
+439
View File
@@ -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
);
}
}
+8
View File
@@ -327,7 +327,14 @@ impl App {
match msg { match msg {
DevserverMsg::HotReload(hr_msg) => { DevserverMsg::HotReload(hr_msg) => {
for webview in self.webviews.values_mut() { 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); dioxus_devtools::apply_changes(&webview.dom, &hr_msg);
drop(lock);
}
webview.poll_vdom(); webview.poll_vdom();
} }
@@ -346,6 +353,7 @@ impl App {
DevserverMsg::Shutdown => { DevserverMsg::Shutdown => {
self.control_flow = ControlFlow::Exit; self.control_flow = ControlFlow::Exit;
} }
_ => {}
} }
} }
+5 -5
View File
@@ -70,12 +70,12 @@ impl LaunchConfig for Config {}
pub(crate) type WryProtocol = ( pub(crate) type WryProtocol = (
String, 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 = ( pub(crate) type AsyncWryProtocol = (
String, String,
Box<dyn Fn(&str, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>, Box<dyn Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static>,
); );
impl Config { impl Config {
@@ -188,7 +188,7 @@ impl Config {
/// Set a custom protocol /// Set a custom protocol
pub fn with_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self pub fn with_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self
where 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.protocols.push((name.to_string(), Box::new(handler)));
self self
@@ -204,7 +204,7 @@ impl Config {
/// # /// #
/// # fn main() { /// # fn main() {
/// let cfg = Config::new() /// let cfg = Config::new()
/// .with_asynchronous_custom_protocol("asset", |_webview_id, request, responder| { /// .with_asynchronous_custom_protocol("asset", |request, responder| {
/// tokio::spawn(async move { /// tokio::spawn(async move {
/// responder.respond( /// responder.respond(
/// HTTPResponse::builder() /// HTTPResponse::builder()
@@ -222,7 +222,7 @@ impl Config {
/// See [`wry`](wry::WebViewBuilder::with_asynchronous_custom_protocol) for more details on implementation /// 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 pub fn with_asynchronous_custom_protocol<F>(mut self, name: impl ToString, handler: F) -> Self
where where
F: Fn(&str, HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static, F: Fn(HttpRequest<Vec<u8>>, RequestAsyncResponder) + 'static,
{ {
self.asynchronous_protocols self.asynchronous_protocols
.push((name.to_string(), Box::new(handler))); .push((name.to_string(), Box::new(handler)));
+36 -35
View File
@@ -235,7 +235,7 @@ impl WebviewInstance {
asset_handlers, asset_handlers,
edits edits
]; ];
move |_id: wry::WebViewId, request, responder: RequestAsyncResponder| { move |request, responder: RequestAsyncResponder| {
protocol::desktop_handler( protocol::desktop_handler(
request, request,
asset_handlers.clone(), 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 { .with_bounds(wry::Rect {
position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)), position: wry::dpi::Position::Logical(wry::dpi::LogicalPosition::new(0.0, 0.0)),
size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new( size: wry::dpi::Size::Physical(wry::dpi::PhysicalSize::new(
@@ -332,23 +358,23 @@ impl WebviewInstance {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
use wry::WebViewBuilderExtWindows; 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 { 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 { 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(..) { 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(..) { 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#" const INITIALIZATION_SCRIPT: &str = r#"
@@ -365,38 +391,12 @@ impl WebviewInstance {
if cfg.disable_context_menu { if cfg.disable_context_menu {
// in release mode, we don't want to show the dev tool or reload menus // 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 { } else {
// in debug, we are okay with the reload menu showing and dev tool // 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 = if cfg!(not(any(target_os = "android", target_os = "ios"))) {
let menu_option = cfg.menu.into(); let menu_option = cfg.menu.into();
if let Some(menu) = &menu_option { if let Some(menu) = &menu_option {
@@ -407,6 +407,7 @@ impl WebviewInstance {
None None
}; };
let webview = webview.build().unwrap();
let desktop_context = Rc::from(DesktopService::new( let desktop_context = Rc::from(DesktopService::new(
webview, webview,
window, window,
+1
View File
@@ -12,3 +12,4 @@ keywords = ["dom", "ui", "gui", "react", ]
[dependencies] [dependencies]
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
dioxus-core = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] }
subsecond-types = { workspace = true }
+10 -2
View File
@@ -1,14 +1,19 @@
use dioxus_core::internal::HotReloadTemplateWithLocation; use dioxus_core::internal::HotReloadTemplateWithLocation;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
use subsecond_types::JumpTable;
/// A message the hot reloading server sends to the client /// A message the hot reloading server sends to the client
#[non_exhaustive]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum DevserverMsg { pub enum DevserverMsg {
/// Attempt a hotreload /// Attempt a hotreload
/// This includes all the templates/literals/assets/binary patches that have changed in one shot /// This includes all the templates/literals/assets/binary patches that have changed in one shot
HotReload(HotReloadMsg), HotReload(HotReloadMsg),
/// Starting a hotpatch
HotPatchStart,
/// The devserver is starting a full rebuild. /// The devserver is starting a full rebuild.
FullReloadStart, FullReloadStart,
@@ -25,6 +30,7 @@ pub enum DevserverMsg {
/// A message the client sends from the frontend to the devserver /// A message the client sends from the frontend to the devserver
/// ///
/// This is used to communicate with the devserver /// This is used to communicate with the devserver
#[non_exhaustive]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ClientMsg { pub enum ClientMsg {
Log { Log {
@@ -37,11 +43,13 @@ pub enum ClientMsg {
pub struct HotReloadMsg { pub struct HotReloadMsg {
pub templates: Vec<HotReloadTemplateWithLocation>, pub templates: Vec<HotReloadTemplateWithLocation>,
pub assets: Vec<PathBuf>, 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 { impl HotReloadMsg {
pub fn is_empty(&self) -> bool { 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()
} }
} }
+3
View File
@@ -13,8 +13,11 @@ keywords = ["dom", "ui", "gui", "react", "hot-reloading"]
dioxus-signals = { workspace = true } dioxus-signals = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] } dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-devtools-types = { workspace = true } dioxus-devtools-types = { workspace = true }
dioxus-cli-config = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
subsecond = { workspace = true }
thiserror = { workspace = true }
# hot reloading serve # hot reloading serve
tracing = { workspace = true } tracing = { workspace = true }
+31 -5
View File
@@ -1,14 +1,24 @@
use dioxus_core::{ScopeId, VirtualDom}; use dioxus_core::{ScopeId, VirtualDom};
pub use dioxus_devtools_types::*;
use dioxus_signals::{GlobalKey, Writable}; use dioxus_signals::{GlobalKey, Writable};
pub use dioxus_devtools_types::*;
pub use subsecond;
use subsecond::PatchError;
/// Applies template and literal changes to the VirtualDom /// Applies template and literal changes to the VirtualDom
/// ///
/// Assets need to be handled by the renderer. /// Assets need to be handled by the renderer.
pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) { pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) {
dom.runtime().on_scope(ScopeId::ROOT, || { try_apply_changes(dom, msg).unwrap()
let ctx = dioxus_signals::get_global_context(); }
/// 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 { for template in &msg.templates {
let value = template.template.clone(); let value = template.template.clone();
let key = GlobalKey::File { let key = GlobalKey::File {
@@ -21,7 +31,17 @@ pub fn apply_changes(dom: &VirtualDom, msg: &HotReloadMsg) {
signal.set(Some(value)); 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. /// 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"))] #[cfg(not(target_arch = "wasm32"))]
pub fn connect(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) { pub fn connect(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Send + 'static) {
std::thread::spawn(move || { 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), Ok((websocket, req)) => (websocket, req),
Err(_) => return, Err(_) => return,
}; };
+12 -3
View File
@@ -26,17 +26,18 @@ dioxus-mobile = { workspace = true, optional = true }
dioxus-desktop = { workspace = true, default-features = true, optional = true } dioxus-desktop = { workspace = true, default-features = true, optional = true }
dioxus-fullstack = { workspace = true, default-features = true, optional = true } dioxus-fullstack = { workspace = true, default-features = true, optional = true }
dioxus-liveview = { workspace = true, optional = true } dioxus-liveview = { workspace = true, optional = true }
dioxus-server = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true } dioxus-ssr = { workspace = true, optional = true }
dioxus-native = { workspace = true, optional = true } dioxus-native = { workspace = true, optional = true }
dioxus_server_macro = { workspace = true, optional = true }
manganis = { workspace = true, features = ["dioxus"], optional = true } manganis = { workspace = true, features = ["dioxus"], optional = true }
dioxus-logger = { workspace = true, optional = true } dioxus-logger = { workspace = true, optional = true }
warnings = { workspace = true, optional = true } warnings = { workspace = true, optional = true }
wasm-split = { workspace = true, optional = true } wasm-split = { workspace = true, optional = true }
subsecond = { workspace = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
dioxus-cli-config = { 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 } dioxus-devtools = { workspace = true, optional = true }
[features] [features]
@@ -89,8 +90,16 @@ web = [
] ]
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"] ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"] 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 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 # This feature just disables the no-renderer-enabled warning
third-party-renderer = [] third-party-renderer = []
+8 -9
View File
@@ -96,11 +96,7 @@ impl LaunchBuilder {
) )
)] )]
pub fn new() -> LaunchBuilder { pub fn new() -> LaunchBuilder {
let platform = if cfg!(feature = "liveview") { let platform = if cfg!(feature = "native") {
KnownPlatform::Liveview
} else if cfg!(feature = "server") {
KnownPlatform::Server
} else if cfg!(feature = "native") {
KnownPlatform::Native KnownPlatform::Native
} else if cfg!(feature = "desktop") { } else if cfg!(feature = "desktop") {
KnownPlatform::Desktop KnownPlatform::Desktop
@@ -108,6 +104,10 @@ impl LaunchBuilder {
KnownPlatform::Mobile KnownPlatform::Mobile
} else if cfg!(feature = "web") { } else if cfg!(feature = "web") {
KnownPlatform::Web KnownPlatform::Web
} else if cfg!(feature = "server") {
KnownPlatform::Server
} else if cfg!(feature = "liveview") {
KnownPlatform::Liveview
} else { } else {
panic!("No platform feature enabled. Please enable one of the following features: liveview, desktop, mobile, web, tui, fullstack to use the launch API.") 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. /// Launch your application.
///
#[allow(clippy::diverging_sub_expression)] #[allow(clippy::diverging_sub_expression)]
pub fn launch(self, app: fn() -> Element) { pub fn launch(self, app: fn() -> Element) {
let Self { let Self {
@@ -343,7 +342,7 @@ impl LaunchBuilder {
#[cfg(feature = "server")] #[cfg(feature = "server")]
if matches!(platform, KnownPlatform::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")] #[cfg(feature = "web")]
@@ -364,8 +363,8 @@ impl LaunchBuilder {
#[cfg(feature = "document")] #[cfg(feature = "document")]
{ {
use dioxus_fullstack::document; use dioxus_fullstack::FullstackWebDocument;
let document = std::rc::Rc::new(document::web::FullstackWebDocument) let document = std::rc::Rc::new(FullstackWebDocument)
as std::rc::Rc<dyn crate::prelude::document::Document>; as std::rc::Rc<dyn crate::prelude::document::Document>;
vdom.provide_root_context(document); vdom.provide_root_context(document);
} }
+3 -4
View File
@@ -78,6 +78,8 @@ pub use dioxus_cli_config as cli_config;
#[cfg_attr(docsrs, doc(cfg(feature = "wasm-split")))] #[cfg_attr(docsrs, doc(cfg(feature = "wasm-split")))]
pub use wasm_split; pub use wasm_split;
pub use subsecond;
pub mod prelude { pub mod prelude {
#[cfg(feature = "document")] #[cfg(feature = "document")]
#[cfg_attr(docsrs, doc(cfg(feature = "document")))] #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
@@ -118,10 +120,7 @@ pub mod prelude {
#[cfg_attr(docsrs, doc(cfg(feature = "html")))] #[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use dioxus_elements::{global_attributes, prelude::*, svg_attributes}; pub use dioxus_elements::{global_attributes, prelude::*, svg_attributes};
#[cfg(all( #[cfg(feature = "devtools")]
not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")),
feature = "devtools"
))]
#[cfg_attr(docsrs, doc(cfg(feature = "devtools")))] #[cfg_attr(docsrs, doc(cfg(feature = "devtools")))]
pub use dioxus_devtools; pub use dioxus_devtools;
+1 -1
View File
@@ -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. 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::*; # use dioxus::prelude::*;
static HIGHLIGHT: Asset = asset!("/assets/highlight/es/highlight.min.js"); static HIGHLIGHT: Asset = asset!("/assets/highlight/es/highlight.min.js");
static RUST: Asset = asset!("/assets/highlight/es/languages/rust.min.js"); static RUST: Asset = asset!("/assets/highlight/es/languages/rust.min.js");
+26 -9
View File
@@ -22,13 +22,29 @@ pub use cargo_metadata;
#[non_exhaustive] #[non_exhaustive]
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub enum StructuredOutput { pub enum StructuredOutput {
BuildFinished { path: PathBuf }, BuildsFinished {
BuildUpdate { stage: BuildStage }, client: PathBuf,
CargoOutput { message: CompilerMessage }, server: Option<PathBuf>,
BundleOutput { bundles: Vec<PathBuf> }, },
HtmlTranslate { html: String }, BuildFinished {
path: PathBuf,
},
BuildUpdate {
stage: BuildStage,
},
CargoOutput {
message: CompilerMessage,
},
BundleOutput {
bundles: Vec<PathBuf>,
},
HtmlTranslate {
html: String,
},
Success, Success,
Error { message: String }, Error {
message: String,
},
} }
impl std::fmt::Debug for StructuredOutput { impl std::fmt::Debug for StructuredOutput {
@@ -46,11 +62,10 @@ pub enum BuildStage {
Initializing, Initializing,
Starting { Starting {
crate_count: usize, crate_count: usize,
is_server: bool, patch: bool,
}, },
InstallingTooling, InstallingTooling,
Compiling { Compiling {
is_server: bool,
current: usize, current: usize,
total: usize, total: usize,
krate: String, krate: String,
@@ -58,7 +73,9 @@ pub enum BuildStage {
RunningBindgen, RunningBindgen,
SplittingBundle, SplittingBundle,
OptimizingWasm, OptimizingWasm,
PrerenderingRoutes, Linking,
Hotpatching,
ExtractingAssets,
CopyingAssets { CopyingAssets {
current: usize, current: usize,
total: usize, total: usize,
+2
View File
@@ -20,6 +20,8 @@ serde = { workspace = true }
[dev-dependencies] [dev-dependencies]
dioxus-fullstack = { workspace = true } dioxus-fullstack = { workspace = true }
dioxus-lib = { workspace = true }
dioxus = { workspace = true }
[features] [features]
web = [] web = []
+1 -1
View File
@@ -89,7 +89,7 @@ pub fn commit_initial_chunk() {
/// // up in the initial chunk. /// // up in the initial chunk.
/// use_hook(|| { /// use_hook(|| {
/// if current_status() == StreamingStatus::InitialChunkCommitted { /// 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.");
/// } /// }
/// }); /// });
/// ///
+5
View File
@@ -399,3 +399,8 @@ impl std::fmt::Display for TakeDataError {
} }
impl std::error::Error 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()
}
+14 -41
View File
@@ -12,7 +12,7 @@ resolver = "2"
[dependencies] [dependencies]
# server functions # 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 } dioxus_server_macro = { workspace = true }
# axum # axum
@@ -23,13 +23,11 @@ dioxus-lib = { workspace = true }
generational-box = { workspace = true } generational-box = { workspace = true }
# Dioxus + SSR # Dioxus + SSR
dioxus-ssr = { workspace = true, optional = true } dioxus-server = { workspace = true, optional = true}
dioxus-isrg = { workspace = true, optional = true } dioxus-isrg = { workspace = true, optional = true }
dioxus-router = { workspace = true, features = ["streaming"], optional = true } dioxus-router = { workspace = true, features = ["streaming"], optional = true }
dioxus-fullstack-hooks = { workspace = true } dioxus-fullstack-hooks = { workspace = true }
dioxus-fullstack-protocol = { workspace = true } dioxus-fullstack-protocol = { workspace = true }
hyper = { workspace = true, optional = true }
http = { workspace = true, optional = true }
# Web Integration # Web Integration
dioxus-web = { workspace = true, features = ["hydrate"], default-features = false, optional = true } 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 } aws-lc-rs = { version = "1.12.5", optional = true }
dioxus-history = { workspace = 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] [dev-dependencies]
dioxus = { workspace = true, features = ["fullstack"] } dioxus = { workspace = true, features = ["fullstack"] }
@@ -92,43 +84,24 @@ devtools = ["dioxus-web?/devtools", "dep:dioxus-devtools"]
mounted = ["dioxus-web?/mounted"] mounted = ["dioxus-web?/mounted"]
file_engine = ["dioxus-web?/file_engine"] file_engine = ["dioxus-web?/file_engine"]
document = ["dioxus-web?/document"] document = ["dioxus-web?/document"]
web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web"] web = ["dep:dioxus-web", "dep:web-sys", "dioxus-fullstack-hooks/web", "server_fn/browser"]
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"] desktop = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
mobile = ["dep:dioxus-mobile", "server_fn/reqwest", "dioxus_server_macro/reqwest"] mobile = ["server_fn/reqwest", "dioxus_server_macro/reqwest"]
default-tls = ["server_fn/default-tls"] default-tls = ["server_fn/default-tls"]
rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"] rustls = ["server_fn/rustls", "dep:rustls", "dep:hyper-rustls"]
axum_core = [ server = [
"dep:axum", "server-core" ,
"default-tls",
"server_fn/axum",
]
server-core = [
"server_fn/axum-no-default", "server_fn/axum-no-default",
"dioxus_server_macro/axum", "dioxus_server_macro/axum",
"default-tls", "server_fn/reqwest",
"server",
]
axum = [
"dep:tower-http",
"server_fn/axum",
"axum_core",
]
server = [
"server_fn/ssr", "server_fn/ssr",
"dioxus_server_macro/server", "dioxus_server_macro/reqwest",
"dioxus-fullstack-hooks/server", "dioxus-fullstack-hooks/server",
"dep:tokio", "dep:dioxus-server",
"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",
"dioxus-interpreter-js", "dioxus-interpreter-js",
] ]
aws-lc-rs = ["dep:aws-lc-rs"] aws-lc-rs = ["dep:aws-lc-rs"]
+1 -1
View File
@@ -72,7 +72,7 @@ First, make sure your `axum` dependency is optional and enabled by the server fe
```toml ```toml
[dependencies] [dependencies]
dioxus = { version = "*", features = ["fullstack"] } 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 } tokio = { version = "1.0", features = ["full"], optional = true }
dioxus-cli-config = { version = "*", optional = true } dioxus-cli-config = { version = "*", optional = true }
-14
View File
@@ -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()
}
+8 -49
View File
@@ -6,63 +6,22 @@
pub use once_cell; pub use once_cell;
#[cfg(feature = "axum")] #[cfg(all(feature = "web", feature = "document"))]
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))] mod web;
pub mod server;
#[cfg(feature = "axum_core")] #[cfg(all(feature = "web", feature = "document"))]
#[cfg_attr(docsrs, doc(cfg(feature = "axum_core")))] pub use web::FullstackWebDocument;
pub mod axum_core;
pub mod document;
#[cfg(feature = "server")]
mod render;
#[cfg(feature = "server")]
mod streaming;
#[cfg(feature = "server")] #[cfg(feature = "server")]
mod serve_config; pub use dioxus_server::*;
#[cfg(feature = "server")]
pub use serve_config::*;
#[cfg(feature = "server")]
mod server_context;
/// A prelude of commonly used items in dioxus-fullstack. /// A prelude of commonly used items in dioxus-fullstack.
pub mod prelude { pub mod prelude {
pub use dioxus_fullstack_hooks::*; 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 dioxus_server_macro::*;
pub use server_fn::{self, ServerFn as _, ServerFnError}; pub use server_fn::{self, ServerFn as _, ServerFnError};
#[cfg(feature = "server")]
pub use dioxus_server::prelude::*;
} }
-110
View File
@@ -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")
}
-199
View File
@@ -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. //! 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 dioxus_web::WebDocument;
use super::head_element_hydration_entry;
fn head_element_written_on_server() -> bool { fn head_element_written_on_server() -> bool {
head_element_hydration_entry() dioxus_fullstack_protocol::head_element_hydration_entry()
.get() .get()
.ok() .ok()
.unwrap_or_default() .unwrap_or_default()
@@ -65,9 +65,9 @@ impl LiveviewRouter for Router {
// Add an extra catch all segment to the route // Add an extra catch all segment to the route
let mut route = route.trim_matches('/').to_string(); let mut route = route.trim_matches('/').to_string();
if route.is_empty() { if route.is_empty() {
route = "/*route".to_string(); route = "/{*route}".to_string();
} else { } else {
route = format!("/{route}/*route"); route = format!("/{route}/{{*route}}");
} }
self.route( self.route(
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff. Show more