diff --git a/Cargo.lock b/Cargo.lock index fe8e75818..a7a4e36f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5442,8 +5442,10 @@ name = "dioxus-dx-wire-format" version = "0.7.0-rc.0" dependencies = [ "cargo_metadata", + "manganis-core", "serde", "serde_json", + "subsecond-types", ] [[package]] diff --git a/packages/cli/src/build/builder.rs b/packages/cli/src/build/builder.rs index 10ca3c09a..33c4fa83c 100644 --- a/packages/cli/src/build/builder.rs +++ b/packages/cli/src/build/builder.rs @@ -170,7 +170,7 @@ impl AppBuilder { tx: tx.clone(), }; request.verify_tooling(&ctx).await?; - request.prepare_build_dir()?; + request.prebuild(&ctx).await?; request.build(&ctx).await } }); @@ -406,14 +406,14 @@ impl AppBuilder { _ => {} } - tracing::info!(json = ?StructuredOutput::BuildUpdate { stage: stage.clone() }); + tracing::info!(json = %StructuredOutput::BuildUpdate { stage: stage.clone() }); } BuilderUpdate::CompilerMessage { message } => { - tracing::info!(json = ?StructuredOutput::RustcOutput { message: message.clone() }, %message); + tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message); } BuilderUpdate::BuildReady { bundle } => { - tracing::debug!(json = ?StructuredOutput::BuildFinished { - path: self.build.root_dir(), + tracing::debug!(json = %StructuredOutput::BuildFinished { + artifacts: bundle.clone().into_structured_output(), }); return Ok(bundle); } @@ -421,7 +421,7 @@ impl AppBuilder { // Flush remaining compiler messages while let Ok(Some(msg)) = self.rx.try_next() { if let BuilderUpdate::CompilerMessage { message } = msg { - tracing::info!(json = ?StructuredOutput::RustcOutput { message: message.clone() }, %message); + tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message); } } @@ -517,7 +517,7 @@ impl AppBuilder { } // If there's any CARGO vars in the rustc_wrapper files, push those too - if let Ok(res) = std::fs::read_to_string(self.build.rustc_wrapper_args_file.path()) { + if let Ok(res) = std::fs::read_to_string(self.build.rustc_wrapper_args_file()) { if let Ok(res) = serde_json::from_str::(&res) { for (key, value) in res.envs { if key.starts_with("CARGO_") { diff --git a/packages/cli/src/build/request.rs b/packages/cli/src/build/request.rs index 4f2962dcf..37e0419b2 100644 --- a/packages/cli/src/build/request.rs +++ b/packages/cli/src/build/request.rs @@ -349,7 +349,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; use target_lexicon::{Architecture, Environment, OperatingSystem, Triple}; -use tempfile::{NamedTempFile, TempDir}; +use tempfile::TempDir; use tokio::{io::AsyncBufReadExt, process::Command}; use uuid::Uuid; @@ -392,15 +392,11 @@ pub(crate) struct BuildRequest { pub(crate) debug_symbols: bool, pub(crate) inject_loading_scripts: bool, pub(crate) custom_linker: Option, - pub(crate) session_cache_dir: Arc, - pub(crate) link_args_file: Arc, - pub(crate) link_err_file: Arc, - pub(crate) rustc_wrapper_args_file: Arc, - pub(crate) command_file: Arc, pub(crate) base_path: Option, pub(crate) using_dioxus_explicitly: bool, pub(crate) apple_entitlements: Option, pub(crate) apple_team_id: Option, + pub(crate) session_cache_dir: PathBuf, } /// dx can produce different "modes" of a build. A "regular" build is a "base" build. The Fat and Thin @@ -447,6 +443,7 @@ pub enum BuildMode { /// The patch cache is only populated on fat builds and then used for thin builds (see `BuildMode::Thin`). #[derive(Clone, Debug)] pub struct BuildArtifacts { + pub(crate) root_dir: PathBuf, pub(crate) bundle: BundleFormat, pub(crate) exe: PathBuf, pub(crate) direct_rustc: RustcArgs, @@ -476,11 +473,7 @@ impl BuildRequest { /// /// Note: Build requests are typically created only when the CLI is invoked or when significant /// changes are detected in the `Cargo.toml` (e.g., features added or removed). - pub(crate) async fn new( - args: &TargetArgs, - main_target: Option, - workspace: Arc, - ) -> Result { + pub(crate) async fn new(args: &TargetArgs, workspace: Arc) -> Result { let crate_package = workspace.find_main_package(args.package.clone())?; let config = workspace @@ -524,8 +517,8 @@ impl BuildRequest { .unwrap_or(workspace.krates[crate_package].name.clone()); // Use the main_target for the client + server build if it is set, otherwise use the target name for this - // specific build - let main_target = main_target.unwrap_or(target_name.clone()); + // specific build. This is important for @client @server syntax so we use the client's output directory for the bundle. + let main_target = args.client_target.clone().unwrap_or(target_name.clone()); let crate_target = main_package .targets @@ -771,25 +764,10 @@ impl BuildRequest { } } - // Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!) - let link_args_file = Arc::new( - NamedTempFile::with_suffix(".txt") - .context("Failed to create temporary file for linker args")?, - ); - let link_err_file = Arc::new( - NamedTempFile::with_suffix(".txt") - .context("Failed to create temporary file for linker args")?, - ); - let rustc_wrapper_args_file = Arc::new( - NamedTempFile::with_suffix(".json") - .context("Failed to create temporary file for rustc wrapper args")?, - ); - let session_cache_dir = Arc::new( - TempDir::new().context("Failed to create temporary directory for session cache")?, - ); - let command_file = Arc::new( - NamedTempFile::new().context("Failed to create temporary file for linker args")?, - ); + let session_cache_dir = args + .session_cache_dir + .clone() + .unwrap_or_else(|| TempDir::new().unwrap().into_path()); let extra_rustc_args = shell_words::split(&args.rustc_args.clone().unwrap_or_default()) .context("Failed to parse rustc args")?; @@ -801,22 +779,10 @@ impl BuildRequest { r#"Target Info: • features: {features:?} • triple: {triple} - • bundle format: {bundle:?}"# - ); - tracing::debug!( - r#"Log Files: - • link_args_file: {}, - • link_err_file: {}, - • rustc_wrapper_args_file: {}, - • session_cache_dir: {} - • linker: {:?} - • target_dir: {:?}"#, - link_args_file.path().display(), - link_err_file.path().display(), - rustc_wrapper_args_file.path().display(), - session_cache_dir.path().display(), - custom_linker, - target_dir, + • bundle format: {bundle:?} + • session cache dir: {session_cache_dir:?} + • linker: {custom_linker:?} + • target_dir: {target_dir:?}"#, ); Ok(Self { @@ -832,11 +798,6 @@ impl BuildRequest { config, target_dir, custom_linker, - link_args_file, - link_err_file, - command_file, - session_cache_dir, - rustc_wrapper_args_file, extra_rustc_args, extra_cargo_args, release, @@ -845,6 +806,7 @@ impl BuildRequest { rustflags, using_dioxus_explicitly, should_codesign, + session_cache_dir, skip_assets: args.skip_assets, base_path: args.base_path.clone(), wasm_split: args.wasm_split, @@ -855,6 +817,39 @@ impl BuildRequest { }) } + pub(crate) async fn prebuild(&self, ctx: &BuildContext) -> Result<()> { + // Create the session cache directory + let cache_dir = self.session_cache_dir(); + _ = std::fs::create_dir_all(&cache_dir); + _ = std::fs::File::create_new(self.rustc_wrapper_args_file()); + _ = std::fs::File::create_new(self.link_err_file()); + _ = std::fs::File::create_new(self.link_args_file()); + _ = std::fs::File::create_new(self.windows_command_file()); + + if !matches!(ctx.mode, BuildMode::Thin { .. }) { + self.prepare_build_dir()?; + } + + if self.bundle == BundleFormat::Server { + return Ok(()); + } + + // Run the tailwind build before bundling anything else + _ = crate::TailwindCli::run_once( + self.package_manifest_dir(), + self.config.application.tailwind_input.clone(), + self.config.application.tailwind_output.clone(), + ) + .await; + + // We want to copy over the prebuilt OpenSSL binaries to ~/.dx/prebuilt/openssl- + if self.bundle == BundleFormat::Android { + AndroidTools::unpack_prebuilt_openssl()?; + } + + Ok(()) + } + pub(crate) async fn build(&self, ctx: &BuildContext) -> Result { let time_start = SystemTime::now(); @@ -862,9 +857,6 @@ impl BuildRequest { // We need to make sure to not react to this though, so the filemap must cache it _ = self.bust_fingerprint(ctx); - // Run any pre-build steps like tailwind, etc - self.prebuild().await?; - // Run the cargo build to produce our artifacts let mut artifacts = self.cargo_build(ctx).await?; @@ -908,7 +900,7 @@ impl BuildRequest { // Populate the patch cache if we're in fat mode if matches!(ctx.mode, BuildMode::Fat) { - artifacts.patch_cache = Some(Arc::new(self.create_patch_cache(&artifacts.exe).await?)); + artifacts.patch_cache = Some(Arc::new(self.create_patch_cache(&artifacts.exe)?)); } // Calculate some final metadata for logging @@ -947,13 +939,13 @@ impl BuildRequest { // "Thin" builds only build the final exe, so we only need to build one crate let crate_count = match ctx.mode { BuildMode::Thin { .. } => 1, - _ => self.get_unit_count_estimate(ctx).await, + _ => self.get_unit_count_estimate(&ctx.mode).await, }; // Update the status to show that we're starting the build and how many crates we expect to build ctx.status_starting_build(crate_count); - let mut cmd = self.build_command(ctx)?; + let mut cmd = self.build_command(&ctx.mode)?; tracing::debug!(dx_src = ?TraceSrc::Build, "Executing cargo for {} using {}", self.bundle, self.triple); let mut child = cmd @@ -1049,14 +1041,14 @@ impl BuildRequest { // Accumulate the rustc args from the wrapper, if they exist and can be parsed. let mut direct_rustc = RustcArgs::default(); - if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file.path()) { + if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file()) { if let Ok(res) = serde_json::from_str(&res) { direct_rustc = res; } } // If there's any warnings from the linker, we should print them out - if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file.path()) { + if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file()) { if !linker_warnings.is_empty() { if output_location.is_none() { tracing::error!("Linker warnings: {}", linker_warnings); @@ -1067,7 +1059,7 @@ impl BuildRequest { } // Collect the linker args from the and update the rustc args - direct_rustc.link_args = std::fs::read_to_string(self.link_args_file.path()) + direct_rustc.link_args = std::fs::read_to_string(self.link_args_file()) .context("Failed to read link args from file")? .lines() .map(|s| s.to_string()) @@ -1077,8 +1069,9 @@ impl BuildRequest { // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds if matches!(ctx.mode, BuildMode::Fat) { + ctx.status_starting_link(); let link_start = SystemTime::now(); - self.run_fat_link(ctx, &exe, &direct_rustc).await?; + self.run_fat_link(&exe, &direct_rustc).await?; tracing::debug!( "Fat linking completed in {}us", SystemTime::now() @@ -1109,6 +1102,7 @@ impl BuildRequest { assets, mode, depinfo, + root_dir: self.root_dir(), patch_cache: None, }) } @@ -1425,13 +1419,7 @@ impl BuildRequest { ) -> Result<()> { ctx.status_hotpatching(); - tracing::debug!( - "Original builds for patch: {}", - self.link_args_file.path().display() - ); - let raw_args = std::fs::read_to_string(self.link_args_file.path()) - .context("Failed to read link args from file")?; - let args = raw_args.lines().collect::>(); + let args = artifacts.direct_rustc.link_args.clone(); // Extract out the incremental object files. // @@ -1551,9 +1539,9 @@ impl BuildRequest { .iter() .map(|s| format!("\"{}\"", s.to_string_lossy())) .join(" "); - std::fs::write(self.command_file.path(), cmd_contents) + std::fs::write(self.windows_command_file(), cmd_contents) .context("Failed to write linker command file")?; - out_args = vec![format!("@{}", self.command_file.path().display()).into()]; + out_args = vec![format!("@{}", self.windows_command_file().display()).into()]; } // Run the linker directly! @@ -1589,7 +1577,7 @@ impl BuildRequest { // Fortunately, this binary exists in two places - the deps dir and the target out dir. We // can just remove the one in the deps dir and the problem goes away. if let Some(idx) = args.iter().position(|arg| *arg == "-o") { - _ = std::fs::remove_file(PathBuf::from(args[idx + 1])); + _ = std::fs::remove_file(PathBuf::from(args[idx + 1].as_str())); } // Now extract the assets from the fat binary @@ -1614,7 +1602,7 @@ impl BuildRequest { /// /// This is basically just stripping away the rlibs and other libraries that will be satisfied /// by our stub step. - fn thin_link_args(&self, original_args: &[&str]) -> Result> { + fn thin_link_args(&self, original_args: &[String]) -> Result> { let mut out_args = vec![]; match self.linker_flavor() { @@ -1828,14 +1816,7 @@ impl BuildRequest { /// /// todo: I think we can traverse our immediate dependencies and inspect their symbols, unless they `pub use` a crate /// todo: we should try and make this faster with memmapping - pub(crate) async fn run_fat_link( - &self, - ctx: &BuildContext, - exe: &Path, - rustc_args: &RustcArgs, - ) -> Result<()> { - ctx.status_starting_link(); - + pub(crate) async fn run_fat_link(&self, exe: &Path, rustc_args: &RustcArgs) -> Result<()> { // Filter out the rlib files from the arguments let rlibs = rustc_args .link_args @@ -2078,9 +2059,9 @@ impl BuildRequest { let mut out_args = args.clone(); if cfg!(windows) { let cmd_contents: String = out_args.iter().map(|f| format!("\"{f}\"")).join(" "); - std::fs::write(self.command_file.path(), cmd_contents) + std::fs::write(self.windows_command_file(), cmd_contents) .context("Failed to write linker command file")?; - out_args = vec![format!("@{}", self.command_file.path().display())]; + out_args = vec![format!("@{}", self.windows_command_file().display())]; } // Run the linker directly! @@ -2226,8 +2207,8 @@ impl BuildRequest { /// /// When processing the output of this command, you need to make sure to handle both cases which /// both have different formats (but with json output for both). - fn build_command(&self, ctx: &BuildContext) -> Result { - match &ctx.mode { + fn build_command(&self, build_mode: &BuildMode) -> Result { + match build_mode { // We're assembling rustc directly, so we need to be *very* careful. Cargo sets rustc's // env up very particularly, and we want to match it 1:1 but with some changes. // @@ -2248,7 +2229,7 @@ impl BuildRequest { cmd.env_remove("RUSTC_WRAPPER"); cmd.env_remove(DX_RUSTC_WRAPPER_ENV_VAR); cmd.envs( - self.cargo_build_env_vars(ctx)? + self.cargo_build_env_vars(build_mode)? .iter() .map(|(k, v)| (k.as_ref(), v)), ); @@ -2276,12 +2257,12 @@ impl BuildRequest { _ => { let mut cmd = Command::new("cargo"); - let env = self.cargo_build_env_vars(ctx)?; - let args = self.cargo_build_arguments(ctx); + let env = self.cargo_build_env_vars(build_mode)?; + let args = self.cargo_build_arguments(build_mode); tracing::trace!("Building with cargo rustc"); for e in env.iter() { - tracing::trace!(": {}={}", e.0, e.1); + tracing::trace!(": {}={}", e.0, e.1.to_string_lossy()); } for a in args.iter() { @@ -2295,11 +2276,11 @@ impl BuildRequest { .args(args) .envs(env.iter().map(|(k, v)| (k.as_ref(), v))); - if matches!(ctx.mode, BuildMode::Fat | BuildMode::Base { run: true }) { + if matches!(build_mode, BuildMode::Fat | BuildMode::Base { run: true }) { cmd.env( DX_RUSTC_WRAPPER_ENV_VAR, - dunce::canonicalize(self.rustc_wrapper_args_file.path()) - .unwrap() + dunce::canonicalize(self.rustc_wrapper_args_file()) + .context("Failed to canonicalize rustc wrapper args file")? .display() .to_string(), ); @@ -2319,7 +2300,7 @@ impl BuildRequest { /// We always use `cargo rustc` *or* `rustc` directly. This means we can pass extra flags like /// `-C` arguments directly to the compiler. #[allow(clippy::vec_init_then_push)] - fn cargo_build_arguments(&self, ctx: &BuildContext) -> Vec { + pub(crate) fn cargo_build_arguments(&self, build_mode: &BuildMode) -> Vec { let mut cargo_args = Vec::with_capacity(4); // Set the `--config profile.{profile}.{key}={value}` flags for the profile, filling in adhoc profile @@ -2383,7 +2364,7 @@ impl BuildRequest { // dx *always* links android and thin builds if self.custom_linker.is_some() - || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) + || matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) { cargo_args.push(format!( "-Clinker={}", @@ -2420,7 +2401,7 @@ impl BuildRequest { // We need save-temps and no-dead-strip in both cases though. When we run `cargo rustc` with // these args, they will be captured and re-ran for the fast compiles in the future, so whatever // we set here will be set for all future hot patches too. - if matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) { + if matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) { // rustc gives us some portable flags required: // - link-dead-code: prevents rust from passing -dead_strip to the linker since that's the default. // - save-temps=true: keeps the incremental object files around, which we need for manually linking. @@ -2497,7 +2478,10 @@ impl BuildRequest { cargo_args } - fn cargo_build_env_vars(&self, ctx: &BuildContext) -> Result, String)>> { + pub(crate) fn cargo_build_env_vars( + &self, + build_mode: &BuildMode, + ) -> Result, OsString)>> { let mut env_vars = vec![]; // Make sure to set all the crazy android flags. Cross-compiling is hard, man. @@ -2509,10 +2493,13 @@ impl BuildRequest { // todo: should we even be doing this? might be better being a build.rs or something else. if self.release { if let Some(base_path) = self.base_path() { - env_vars.push((ASSET_ROOT_ENV.into(), base_path.to_string())); + env_vars.push((ASSET_ROOT_ENV.into(), base_path.to_string().into())); } - env_vars.push((APP_TITLE_ENV.into(), self.config.web.app.title.clone())); - env_vars.push((PRODUCT_NAME_ENV.into(), self.bundled_app_name())); + env_vars.push(( + APP_TITLE_ENV.into(), + self.config.web.app.title.clone().into(), + )); + env_vars.push((PRODUCT_NAME_ENV.into(), self.bundled_app_name().into())); } // Assemble the rustflags by peering into the `.cargo/config.toml` file @@ -2520,7 +2507,7 @@ impl BuildRequest { // Disable reference types on wasm when using hotpatching // https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features/#disabling-on-by-default-webassembly-proposals - if self.is_wasm_or_wasi() && matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) { + if self.is_wasm_or_wasi() && matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) { rust_flags.flags.push("-Ctarget-cpu=mvp".to_string()); } @@ -2530,19 +2517,20 @@ impl BuildRequest { "RUSTFLAGS".into(), rust_flags .encode_space_separated() - .context("Failed to encode RUSTFLAGS")?, + .context("Failed to encode RUSTFLAGS")? + .into(), )); } // If we're either zero-linking or using a custom linker, make `dx` itself do the linking. if self.custom_linker.is_some() - || matches!(ctx.mode, BuildMode::Thin { .. } | BuildMode::Fat) + || matches!(build_mode, BuildMode::Thin { .. } | BuildMode::Fat) { LinkAction { triple: self.triple.clone(), linker: self.custom_linker.clone(), - link_err_file: dunce::canonicalize(self.link_err_file.path())?, - link_args_file: dunce::canonicalize(self.link_args_file.path())?, + link_err_file: dunce::canonicalize(self.link_err_file())?, + link_args_file: dunce::canonicalize(self.link_args_file())?, } .write_env_vars(&mut env_vars)?; } @@ -2563,7 +2551,7 @@ impl BuildRequest { /// cargo-ndk is MIT licensed. /// /// - fn android_env_vars(&self) -> Result, String)>> { + fn android_env_vars(&self) -> Result, OsString)>> { // Derived from getenv_with_target_prefixes in `cc` crate. fn cc_env(var_base: &str, triple: &str) -> (String, Option) { #[inline] @@ -2611,7 +2599,7 @@ impl BuildRequest { }) as _ } - let mut env_vars: Vec<(Cow<'static, str>, String)> = vec![]; + let mut env_vars: Vec<(Cow<'static, str>, OsString)> = vec![]; let min_sdk_version = self.min_sdk_version_or_default(); @@ -2635,7 +2623,7 @@ impl BuildRequest { if let Some(java_home) = java_home { tracing::debug!("Setting JAVA_HOME to {java_home:?}"); - env_vars.push(("JAVA_HOME".into(), java_home.display().to_string())); + env_vars.push(("JAVA_HOME".into(), java_home.into_os_string())); } let triple = self.triple.to_string(); @@ -2786,13 +2774,7 @@ impl BuildRequest { // https://github.com/KyleMayes/clang-sys?tab=readme-ov-file#environment-variables ("CLANG_PATH".into(), target_cc.with_extension("exe").into()), ] { - env_vars.push(( - env.0.into(), - env.1 - .to_str() - .expect("Failed to convert env var value to string") - .to_string(), - )); + env_vars.push((env.0.into(), env.1)); } if std::env::var("MSYSTEM").is_ok() || std::env::var("CYGWIN").is_ok() { @@ -2800,7 +2782,7 @@ impl BuildRequest { // Convert windows paths to unix-style paths // This is a workaround for the fact that the `cc` crate expects unix-style paths // and will fail if it encounters windows-style paths. - var.1 = var.1.replace('\\', "/"); + var.1 = var.1.to_string_lossy().replace('\\', "/").into(); } } @@ -2811,9 +2793,9 @@ impl BuildRequest { /// will return an estimate of the number of units in the crate based on cargo metadata. /// /// TODO: always use once it is stable - async fn get_unit_count_estimate(&self, ctx: &BuildContext) -> usize { + async fn get_unit_count_estimate(&self, build_mode: &BuildMode) -> usize { // Try to get it from nightly - if let Ok(count) = self.get_unit_count(ctx).await { + if let Ok(count) = self.get_unit_count(build_mode).await { return count; } @@ -2833,7 +2815,7 @@ impl BuildRequest { /// available with the current version of rustc the user has installed. /// /// It also might not be super reliable - I think in practice it occasionally returns 2x the units. - async fn get_unit_count(&self, ctx: &BuildContext) -> crate::Result { + async fn get_unit_count(&self, build_mode: &BuildMode) -> crate::Result { #[derive(Debug, Deserialize)] struct UnitGraph { units: Vec, @@ -2845,9 +2827,9 @@ impl BuildRequest { .arg("--unit-graph") .arg("-Z") .arg("unstable-options") - .args(self.cargo_build_arguments(ctx)) + .args(self.cargo_build_arguments(build_mode)) .envs( - self.cargo_build_env_vars(ctx)? + self.cargo_build_env_vars(build_mode)? .iter() .map(|(k, v)| (k.as_ref(), v)), ) @@ -3180,7 +3162,23 @@ impl BuildRequest { /// /// The directory is specific for this app and might be pub(crate) fn session_cache_dir(&self) -> PathBuf { - self.session_cache_dir.path().to_path_buf() + self.session_cache_dir.join(self.bundle.to_string()) + } + + pub(crate) fn rustc_wrapper_args_file(&self) -> PathBuf { + self.session_cache_dir().join("rustc_wrapper_args.txt") + } + + fn link_err_file(&self) -> PathBuf { + self.session_cache_dir().join("link_err.txt") + } + + fn link_args_file(&self) -> PathBuf { + self.session_cache_dir().join("link_args.json") + } + + fn windows_command_file(&self) -> PathBuf { + self.session_cache_dir().join("windows_command.txt") } /// Get the outdir specified by the Dioxus.toml, relative to the crate directory. @@ -4120,8 +4118,6 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ _ = remove_dir_all(self.exe_dir()); } - self.flush_session_cache(); - create_dir_all(self.root_dir())?; create_dir_all(self.exe_dir())?; create_dir_all(self.asset_dir())?; @@ -4233,12 +4229,6 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ self.platform_dir().join(".cli-version") } - fn flush_session_cache(&self) { - let cache_dir = self.session_cache_dir(); - _ = std::fs::remove_dir_all(&cache_dir); - _ = std::fs::create_dir_all(&cache_dir); - } - /// Check for tooling that might be required for this build. /// /// This should generally be only called on the first build since it takes time to verify the tooling @@ -4433,13 +4423,18 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ Ok(()) } - async fn create_patch_cache(&self, exe: &Path) -> Result { - let exe = match self.bundle { + pub(crate) fn patch_cache_exe(&self, exe: &Path) -> PathBuf { + match self.bundle { BundleFormat::Web => self.wasm_bindgen_wasm_output_file(), _ => exe.to_path_buf(), - }; + } + } - Ok(HotpatchModuleCache::new(&exe, &self.triple)?) + pub(crate) fn create_patch_cache(&self, exe: &Path) -> Result { + Ok(HotpatchModuleCache::new( + &self.patch_cache_exe(exe), + &self.triple, + )?) } /// Users create an index.html for their SPA if they want it @@ -4850,27 +4845,6 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{ .collect() } - async fn prebuild(&self) -> Result<()> { - if self.bundle == BundleFormat::Server { - return Ok(()); - } - - // Run the tailwind build before bundling anything else - crate::TailwindCli::run_once( - self.package_manifest_dir(), - self.config.application.tailwind_input.clone(), - self.config.application.tailwind_output.clone(), - ) - .await?; - - // We want to copy over the prebuilt OpenSSL binaries to ~/.dx/prebuilt/openssl- - if self.bundle == BundleFormat::Android { - AndroidTools::unpack_prebuilt_openssl()?; - } - - Ok(()) - } - pub async fn codesign_ios(&self) -> Result<()> { // We don't want to drop the entitlements file, until the end of the block, so we hoist it to this temporary. let mut _saved_entitlements = None; diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index e86b3cbba..67efc708e 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -1,4 +1,8 @@ -use crate::{cli::*, Anonymized, AppBuilder, BuildMode, BuildRequest, TargetArgs, Workspace}; +use dioxus_dx_wire_format::StructuredBuildArtifacts; + +use crate::{ + cli::*, Anonymized, AppBuilder, BuildArtifacts, BuildMode, BuildRequest, TargetArgs, Workspace, +}; /// Build the Rust Dioxus app and all of its assets. /// @@ -20,11 +24,33 @@ pub struct BuildArgs { #[clap(long)] pub(crate) ssg: bool, + /// Force a "fat" binary, required to use `dx build-tools hotpatch` + #[clap(long)] + pub(crate) fat_binary: 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] + /// + /// If CI is enabled, this will be set to true by default. + /// + #[clap( + long, default_missing_value = "true", + num_args = 0..=1, + )] + pub(crate) force_sequential: Option, + /// Arguments for the build itself #[clap(flatten)] pub(crate) build_arguments: TargetArgs, } +impl BuildArgs { + pub(crate) fn force_sequential_build(&self) -> bool { + self.force_sequential + .unwrap_or_else(|| std::env::var("CI").is_ok()) + } +} + impl Anonymized for BuildArgs { fn anonymized(&self) -> Value { json! {{ @@ -68,7 +94,7 @@ impl CommandWithPlatformOverrides { /// ``` /// /// Currently it is not possible to serve the server without the client, but this could be added in the future. - pub async fn into_targets(self) -> Result { + pub async fn into_targets(mut self) -> Result { let workspace = Workspace::current().await?; // do some logging to ensure dx matches the dioxus version since we're not always API compatible @@ -78,24 +104,20 @@ impl CommandWithPlatformOverrides { let client_args = &self.client.as_ref().unwrap_or(&self.shared).build_arguments; // Create the client build request - let client = BuildRequest::new(client_args, None, workspace.clone()).await?; + let client = BuildRequest::new(client_args, workspace.clone()).await?; // Create the server build request if needed - // This happens when 1) fullstack is enabled, 2) let mut server = None; if matches!(self.shared.fullstack, Some(true)) || client.fullstack_feature_enabled() || self.server.is_some() { - match self.server.as_ref() { + match self.server.as_mut() { Some(server_args) => { + // Make sure we set the client target here so @server knows to place its output into the @client target directory. + server_args.build_arguments.client_target = Some(client.main_target.clone()); server = Some( - BuildRequest::new( - &server_args.build_arguments, - Some(client.main_target.clone()), - workspace.clone(), - ) - .await?, + BuildRequest::new(&server_args.build_arguments, workspace.clone()).await?, ); } None => { @@ -103,7 +125,7 @@ impl CommandWithPlatformOverrides { args.platform = Some(crate::Platform::Server); args.renderer.renderer = Some(crate::Renderer::Server); args.target = Some(target_lexicon::Triple::host()); - server = Some(BuildRequest::new(&args, None, workspace.clone()).await?); + server = Some(BuildRequest::new(&args, workspace.clone()).await?); } } } @@ -114,31 +136,73 @@ impl CommandWithPlatformOverrides { pub async fn build(self) -> Result { tracing::info!("Building project..."); + let force_sequential = self.shared.force_sequential_build(); let ssg = self.shared.ssg; + let mode = match self.shared.fat_binary { + true => BuildMode::Fat, + false => BuildMode::Base { run: false }, + }; let targets = self.into_targets().await?; - AppBuilder::started(&targets.client, BuildMode::Base { run: false })? - .finish_build() - .await?; + let build_client = Self::build_client_inner(&targets.client, mode.clone()); + let build_server = Self::build_server_inner(&targets.server, mode.clone(), ssg); - tracing::info!(path = ?targets.client.root_dir(), "Client build completed successfully! 🚀"); - - if let Some(server) = targets.server.as_ref() { - // If the server is present, we need to build it as well - let mut server_build = AppBuilder::started(server, BuildMode::Base { run: false })?; - server_build.finish_build().await?; - - // Run SSG and cache static routes - if ssg { - crate::pre_render_static_routes(None, &mut server_build, None).await?; - } - - tracing::info!(path = ?targets.client.root_dir(), "Server build completed successfully! 🚀"); - } + let (client, server) = match force_sequential { + true => (build_client.await, build_server.await), + false => tokio::join!(build_client, build_server), + }; Ok(StructuredOutput::BuildsFinished { - client: targets.client.root_dir(), - server: targets.server.map(|s| s.root_dir()), + client: client?.into_structured_output(), + server: server?.map(|s| s.into_structured_output()), }) } + + pub(crate) async fn build_client_inner( + request: &BuildRequest, + mode: BuildMode, + ) -> Result { + AppBuilder::started(request, mode)? + .finish_build() + .await + .inspect(|_| { + tracing::info!(path = ?request.root_dir(), "Client build completed successfully! 🚀"); + }) + } + + pub(crate) async fn build_server_inner( + request: &Option, + mode: BuildMode, + ssg: bool, + ) -> Result> { + let Some(server) = request.as_ref() else { + return Ok(None); + }; + + // If the server is present, we need to build it as well + let mut server_build = AppBuilder::started(server, mode)?; + let server_artifacts = server_build.finish_build().await?; + + // Run SSG and cache static routes + if ssg { + crate::pre_render_static_routes(None, &mut server_build, None).await?; + } + + tracing::info!(path = ?server.root_dir(), "Server build completed successfully! 🚀"); + + Ok(Some(server_artifacts)) + } +} + +impl BuildArtifacts { + pub(crate) fn into_structured_output(self) -> StructuredBuildArtifacts { + StructuredBuildArtifacts { + path: self.root_dir, + exe: self.exe, + rustc_args: self.direct_rustc.args, + rustc_envs: self.direct_rustc.envs, + link_args: self.direct_rustc.link_args, + assets: self.assets.unique_assets().cloned().collect(), + } + } } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index 81064cb72..313c0199c 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -39,7 +39,8 @@ impl Bundle { let BuildTargets { client, server } = self.args.into_targets().await?; - AppBuilder::started(&client, BuildMode::Base { run: false })? + let mut server_artifacts = None; + let client_artifacts = AppBuilder::started(&client, BuildMode::Base { run: false })? .finish_build() .await?; @@ -47,9 +48,11 @@ impl Bundle { if let Some(server) = server.as_ref() { // If the server is present, we need to build it as well - AppBuilder::started(server, BuildMode::Base { run: false })? - .finish_build() - .await?; + server_artifacts = Some( + AppBuilder::started(server, BuildMode::Base { run: false })? + .finish_build() + .await?, + ); tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀"); } @@ -134,7 +137,14 @@ impl Bundle { ); } - Ok(StructuredOutput::BundleOutput { bundles }) + let client = client_artifacts.into_structured_output(); + let server = server_artifacts.map(|s| s.into_structured_output()); + + Ok(StructuredOutput::BundleOutput { + bundles, + client, + server, + }) } fn bundle_desktop( diff --git a/packages/cli/src/cli/create.rs b/packages/cli/src/cli/create.rs index 4f8ae74ed..6c1e2cfae 100644 --- a/packages/cli/src/cli/create.rs +++ b/packages/cli/src/cli/create.rs @@ -260,9 +260,9 @@ pub(crate) async fn connectivity_check() -> Result<()> { _ = tokio::time::sleep(std::time::Duration::from_millis(if x == 1 { 500 } else { 2000 })) => {} } if x == 0 { - println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for {LINK_STYLE}https://github.com/dioxuslabs{LINK_STYLE:#}...") + eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for {LINK_STYLE}https://github.com/dioxuslabs{LINK_STYLE:#}...") } else { - println!( + eprintln!( "{GLOW_STYLE}warning{GLOW_STYLE:#}: ({x}/5) Taking a while, maybe your internet is down?" ); } diff --git a/packages/cli/src/cli/hotpatch.rs b/packages/cli/src/cli/hotpatch.rs new file mode 100644 index 000000000..a2d619afd --- /dev/null +++ b/packages/cli/src/cli/hotpatch.rs @@ -0,0 +1,79 @@ +use crate::{ + platform_override::CommandWithPlatformOverrides, AppBuilder, BuildArgs, BuildMode, + HotpatchModuleCache, Result, StructuredOutput, +}; +use anyhow::Context; +use clap::Parser; +use dioxus_dx_wire_format::StructuredBuildArtifacts; +use std::sync::Arc; + +const HELP_HEADING: &str = "Hotpatching a binary"; + +/// Patches a single binary, but takes the same arguments as a `cargo build`. +/// +/// By default, patches the client, but you can set patch_server to true to patch the server instead. +#[derive(Clone, Debug, Parser)] +pub struct HotpatchTip { + /// Should we patch the server or the client? False = client, True = server + #[clap(long, num_args = 0..=1, default_missing_value="true", help_heading = HELP_HEADING)] + pub patch_server: Option, + + /// The serialized output from the `dx build --fat-binary` command + #[clap(long, help_heading = HELP_HEADING)] + pub serialized_artifacts: String, + + /// The ASLR reference of the running app being patched. Used to generate sensible offsets for patched code. + #[clap(long, help_heading = HELP_HEADING)] + pub aslr_reference: u64, + + #[clap(flatten)] + pub build_args: CommandWithPlatformOverrides, +} + +impl HotpatchTip { + pub async fn run(self) -> Result { + let targets = self.build_args.into_targets().await?; + + // Select which target to patch + let request = if self.patch_server.unwrap_or_default() { + targets.server.as_ref().context("No server to patch!")? + } else { + &targets.client + }; + + let structured_build_artifacts = + serde_json::from_str::(&self.serialized_artifacts) + .context("Failed to parse structured build artifacts")?; + + let StructuredBuildArtifacts { + exe, + rustc_args, + rustc_envs, + link_args, + .. + } = structured_build_artifacts; + + // todo: loading this cache over and over defeats the purpose of a cache + // consider a shared-mem approach or a binary serializer? something like arrow / parquet / bincode? + let cache = Arc::new(HotpatchModuleCache::new(&exe, &request.triple)?); + + let mode = BuildMode::Thin { + rustc_args: crate::RustcArgs { + args: rustc_args, + envs: rustc_envs, + link_args, + }, + changed_files: vec![], + aslr_reference: self.aslr_reference, + cache: cache.clone(), + }; + + let artifacts = AppBuilder::started(request, mode)?.finish_build().await?; + let patch_exe = request.patch_exe(artifacts.time_start); + + Ok(StructuredOutput::Hotpatch { + jump_table: crate::build::create_jump_table(&patch_exe, &request.triple, &cache)?, + artifacts: artifacts.into_structured_output(), + }) + } +} diff --git a/packages/cli/src/cli/link.rs b/packages/cli/src/cli/link.rs index 9d17f7ebd..0e121435f 100644 --- a/packages/cli/src/cli/link.rs +++ b/packages/cli/src/cli/link.rs @@ -1,7 +1,7 @@ use crate::Result; use anyhow::Context; use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, ffi::OsString, path::PathBuf}; use target_lexicon::Triple; /// `dx` can act as a linker in a few scenarios. Note that we don't *actually* implement the linker logic, @@ -82,29 +82,24 @@ impl LinkAction { pub(crate) fn write_env_vars( &self, - env_vars: &mut Vec<(Cow<'static, str>, String)>, + env_vars: &mut Vec<(Cow<'static, str>, OsString)>, ) -> Result<()> { - env_vars.push((Self::DX_LINK_ARG.into(), "1".to_string())); + env_vars.push((Self::DX_LINK_ARG.into(), "1".into())); env_vars.push(( Self::DX_ARGS_FILE.into(), - dunce::canonicalize(&self.link_args_file)? - .to_string_lossy() - .to_string(), + dunce::canonicalize(&self.link_args_file)?.into_os_string(), )); env_vars.push(( Self::DX_ERR_FILE.into(), - dunce::canonicalize(&self.link_err_file)? - .to_string_lossy() - .to_string(), + dunce::canonicalize(&self.link_err_file)?.into_os_string(), )); - env_vars.push((Self::DX_LINK_TRIPLE.into(), self.triple.to_string())); + env_vars.push((Self::DX_LINK_TRIPLE.into(), self.triple.to_string().into())); if let Some(linker) = &self.linker { env_vars.push(( Self::DX_LINK_CUSTOM_LINKER.into(), dunce::canonicalize(linker) .unwrap_or(linker.clone()) - .to_string_lossy() - .to_string(), + .into_os_string(), )); } diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index 4b60055bb..0c4692098 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -6,9 +6,11 @@ pub(crate) mod check; pub(crate) mod config; pub(crate) mod create; pub(crate) mod doctor; +pub(crate) mod hotpatch; pub(crate) mod init; pub(crate) mod link; pub(crate) mod platform_override; +pub(crate) mod print; pub(crate) mod run; pub(crate) mod serve; pub(crate) mod target; @@ -81,6 +83,12 @@ pub(crate) enum Commands { #[clap(name = "doctor")] Doctor(doctor::Doctor), + /// Print project information in a structured format, like cargo args, linker args, and other + /// flags DX sets that might be useful in third-party tools. + #[clap(name = "print")] + #[clap(subcommand)] + Print(print::Print), + /// Translate a source file into Dioxus code. #[clap(name = "translate")] Translate(translate::Translate), @@ -102,17 +110,22 @@ pub(crate) enum Commands { #[clap(name = "self-update")] SelfUpdate(update::SelfUpdate), - /// Run a dioxus build tool. IE `build-assets`, etc + /// Run a dioxus build tool. IE `build-assets`, `hotpatch`, etc #[clap(name = "tools")] #[clap(subcommand)] Tools(BuildTools), } +#[allow(clippy::large_enum_variant)] #[derive(Subcommand)] pub enum BuildTools { /// Build the assets for a specific target. #[clap(name = "assets")] BuildAssets(build_assets::BuildAssets), + + /// Hotpatch the "tip" of a given "fat" binary. The output here must be from the `dx build` command with "fat" enabled + #[clap(name = "hotpatch")] + HotpatchTip(hotpatch::HotpatchTip), } pub(crate) static VERSION: LazyLock = LazyLock::new(|| { diff --git a/packages/cli/src/cli/print.rs b/packages/cli/src/cli/print.rs new file mode 100644 index 000000000..d6313f79b --- /dev/null +++ b/packages/cli/src/cli/print.rs @@ -0,0 +1,221 @@ +use std::{borrow::Cow, ffi::OsString}; + +use super::*; +use crate::{BuildMode, Result}; +use anyhow::Context; + +/// Perform a system analysis to verify the system install is working correctly. +#[derive(Clone, Debug, Subcommand)] +pub(crate) enum Print { + /// Print the cargo args dioxus uses to build the server app. + /// Environment variables will be set with the `env` command. + #[clap(name = "client-args")] + ClientArgs(PrintCargoArgs), + + /// Print the cargo args dioxus uses to build the client app. + /// Environment variables will be set with the `env` command. + #[clap(name = "server-args")] + ServerArgs(PrintCargoArgs), +} + +#[derive(Clone, Debug, Parser)] +pub(crate) struct PrintCargoArgs { + #[clap(flatten)] + pub(crate) args: CommandWithPlatformOverrides, + + /// The print output style to use. By default, this uses the current-platform's best fit, + /// though you can customize it in the case you might be driving an external build system. + /// - Unix style uses the `env` command + /// - Windows style uses the `set` command + /// - JSON style prints the arguments as JSON + /// - Pretty JSON style prints the arguments as pretty JSON + /// - Args style prints only the arguments, one per line, without any environment variables. + /// - Env style prints only the environment variables, one key-pair per line, without any arguments. + #[clap(long)] + pub(crate) style: Option, +} + +#[derive(Clone, Debug, clap::ValueEnum)] +pub(crate) enum PrintStyle { + /// Print the arguments as a list of arguments, one per line. + /// Does not include the `cargo rustc` command itself + Args, + + /// Print the environment variables as a list of key=value pairs, one per line. + Env, + + /// Print the arguments using the Unix `env` command. + Unix, + + /// Print the arguments using the Windows `set` command. + Cmd, + + /// Print the arguments as JSON. Does not include the `cargo rustc` command itself + Json, + + /// Print the arguments as pretty JSON. Does not include the `cargo rustc` command itself + PrettyJson, +} + +impl Print { + pub(crate) async fn print(self) -> Result { + match self { + Self::ClientArgs(opts) => { + let targets = opts.args.into_targets().await?; + let mode = BuildMode::Base { run: false }; + let args = targets.client.cargo_build_arguments(&mode); + let env = targets.client.cargo_build_env_vars(&mode)?; + Self::print_as_unified_command(&env, &args, &opts.style); + Ok(StructuredOutput::PrintCargoArgs { + args, + env: env + .into_iter() + .map(|(k, v)| (k, v.to_string_lossy().to_string())) + .collect::>(), + }) + } + Self::ServerArgs(print_cargo_args) => { + let targets = print_cargo_args.args.into_targets().await?; + let mode = BuildMode::Base { run: false }; + let server = targets + .server + .context("No server target found, cannot print server args")?; + let args = server.cargo_build_arguments(&mode); + let env = server.cargo_build_env_vars(&mode)?; + Self::print_as_unified_command(&env, &args, &print_cargo_args.style); + Ok(StructuredOutput::PrintCargoArgs { + args, + env: env + .into_iter() + .map(|(k, v)| (k, v.to_string_lossy().to_string())) + .collect::>(), + }) + } + } + } + + /// Prints the given env and args as a unified command. + /// - Uses `env` on unix systems + /// - Uses `set VAR=value &&` on windows systems + /// - Prints structured JSON on json style + fn print_as_unified_command( + env: &[(Cow<'static, str>, OsString)], + args: &[String], + style: &Option, + ) { + let style = style.clone().unwrap_or({ + if cfg!(unix) || std::env::var("MSYSTEM").is_ok() || std::env::var("CYGWIN").is_ok() { + PrintStyle::Unix + } else { + PrintStyle::Cmd + } + }); + + match style { + PrintStyle::Args => { + for arg in args { + println!("{}", arg); + } + } + + PrintStyle::Env => { + for (key, value) in env { + println!("{}={}", key, value.to_string_lossy()); + } + } + + PrintStyle::Unix => { + let mut cmd = String::from("env"); + for (key, value) in env { + cmd.push_str(&format!( + " {}={}", + key, + shell_words::quote(&value.to_string_lossy()) + )); + } + cmd.push_str(" cargo rustc"); + for arg in args { + cmd.push_str(&format!(" {}", shell_words::quote(arg))); + } + println!("{}", cmd); + } + PrintStyle::Cmd => { + let mut cmd = String::new(); + for (key, value) in env { + cmd.push_str(&format!( + "set {}={} && ", + key, + Self::escape_windows(value.to_string_lossy()) + )); + } + cmd.push_str("cargo rustc"); + for arg in args { + cmd.push_str(&format!( + " {}", + Self::escape_windows(Cow::Borrowed(arg.as_str())) + )); + } + println!("{}", cmd); + } + PrintStyle::Json | PrintStyle::PrettyJson => { + let output = serde_json::json!({ + "env": env.iter().map(|(k, v)| (k.as_ref(), v)).collect::>(), + "args": args + }); + if matches!(style, PrintStyle::PrettyJson) { + println!("{}", serde_json::to_string_pretty(&output).unwrap()); + } else { + println!("{}", serde_json::to_string(&output).unwrap()); + } + } + } + } + /// Escape for the windows cmd.exe shell. + /// + /// See [here][msdn] for more information. + /// + /// [msdn]: http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// This function comes from shell-escape + fn escape_windows(s: Cow) -> Cow { + use std::iter::repeat; + + let mut needs_escape = s.is_empty(); + for ch in s.chars() { + match ch { + '"' | '\t' | '\n' | ' ' => needs_escape = true, + _ => {} + } + } + if !needs_escape { + return s; + } + let mut es = String::with_capacity(s.len()); + es.push('"'); + let mut chars = s.chars().peekable(); + loop { + let mut nslashes = 0; + while let Some(&'\\') = chars.peek() { + chars.next(); + nslashes += 1; + } + + match chars.next() { + Some('"') => { + es.extend(repeat('\\').take(nslashes * 2 + 1)); + es.push('"'); + } + Some(c) => { + es.extend(repeat('\\').take(nslashes)); + es.push(c); + } + None => { + es.extend(repeat('\\').take(nslashes * 2)); + break; + } + } + } + es.push('"'); + es.into() + } +} diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index d08298051..d0e235fbb 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -64,10 +64,6 @@ pub(crate) struct ServeArgs { #[clap(long, default_missing_value = "true")] pub(crate) watch: Option, - /// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false] - #[clap(long)] - pub(crate) force_sequential: bool, - /// Exit the CLI after running into an error. This is mainly used to test hot patching internally #[clap(long)] #[clap(hide = true)] @@ -130,7 +126,6 @@ impl Anonymized for ServeArgs { "interactive": self.interactive, "hot_patch": self.hot_patch, "watch": self.watch, - "force_sequential": self.force_sequential, "exit_on_error": self.exit_on_error, "platform_args": self.platform_args.anonymized(), }} diff --git a/packages/cli/src/cli/target.rs b/packages/cli/src/cli/target.rs index c131a345c..7829b4449 100644 --- a/packages/cli/src/cli/target.rs +++ b/packages/cli/src/cli/target.rs @@ -112,7 +112,7 @@ pub(crate) struct TargetArgs { /// simulator. If the device name is passed, we will upload to that device instead. /// /// This performs a search among devices, and fuzzy matches might be found. - #[arg(long, default_missing_value=None, num_args=0..=1)] + #[arg(long, default_missing_value=None, num_args=0..=1, help_heading = HELP_HEADING)] pub(crate) device: Option, /// The base path the build will fetch assets relative to. This will override the @@ -142,6 +142,17 @@ pub(crate) struct TargetArgs { /// ``` #[clap(long, help_heading = HELP_HEADING)] pub(crate) apple_team_id: Option, + + /// The folder where DX stores its temporary artifacts for things like hotpatching, build caches, + /// window position, etc. This is meant to be stable within an invocation of the CLI, but you can + /// persist it by setting this flag. + #[clap(long, help_heading = HELP_HEADING)] + pub(crate) session_cache_dir: Option, + + /// The target for the client build, used for specifying which target the server should end up in + /// when merging `@client and @server` targets together. + #[clap(long, help_heading = HELP_HEADING)] + pub(crate) client_target: Option, } impl Anonymized for TargetArgs { diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 3db8a78c2..194d2127b 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -184,6 +184,10 @@ impl TraceController { return Ok(()); } + if field.name() == "telemetry" { + return Ok(()); + } + write!(writer, "{}", format_field(field.name(), value)) }) .delimited(" "), @@ -794,7 +798,7 @@ impl TraceController { /// The second return is a JSON object with the anonymized arguments as a structured value. pub(crate) fn command_anonymized(arg: &crate::Commands) -> (String, serde_json::Value) { use crate::cli::config::{Config, Setting}; - use crate::BuildTools; + use crate::{print::Print, BuildTools}; use cargo_generate::Vcs; match arg { @@ -892,6 +896,11 @@ impl TraceController { ), Commands::Tools(tool) => match tool { BuildTools::BuildAssets(_build_assets) => ("tools assets".to_string(), json!({})), + BuildTools::HotpatchTip(_hotpatch_tip) => ("tools hotpatch".to_string(), json!({})), + }, + Commands::Print(print) => match print { + Print::ClientArgs(_args) => ("print client-args".to_string(), json!({})), + Print::ServerArgs(_args) => ("print server-args".to_string(), json!({})), }, } } diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index 428f47564..d6c063363 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -63,7 +63,9 @@ async fn main() { Commands::Run(opts) => opts.run().await, Commands::SelfUpdate(opts) => opts.self_update().await, Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await, + Commands::Tools(BuildTools::HotpatchTip(opts)) => opts.run().await, Commands::Doctor(opts) => opts.doctor().await, + Commands::Print(opts) => opts.print().await, } }); @@ -71,10 +73,10 @@ async fn main() { // Make sure we do this as the last step so you can always `tail -1` it match result.await { StructuredOutput::Error { message } => { - tracing::error!(json = ?StructuredOutput::Error { message }); + tracing::error!(json = %StructuredOutput::Error { message }); std::process::exit(1); } - output => tracing::debug!(json = ?output), + output => tracing::info!(json = %output), } } diff --git a/packages/cli/src/serve/runner.rs b/packages/cli/src/serve/runner.rs index 3b434a8db..5c539ca31 100644 --- a/packages/cli/src/serve/runner.rs +++ b/packages/cli/src/serve/runner.rs @@ -97,7 +97,7 @@ impl AppServer { // Resolve the simpler args let interactive = args.is_interactive_tty(); - let force_sequential = args.force_sequential; + let force_sequential = args.platform_args.shared.targets.force_sequential_build(); let cross_origin_policy = args.cross_origin_policy; // Find the launch args for the client and server diff --git a/packages/cli/src/workspace.rs b/packages/cli/src/workspace.rs index 6dba1ec4e..a72c0b13b 100644 --- a/packages/cli/src/workspace.rs +++ b/packages/cli/src/workspace.rs @@ -61,14 +61,14 @@ impl Workspace { let spin_future = async move { tokio::time::sleep(Duration::from_millis(1000)).await; - println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for cargo-metadata..."); + eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: Waiting for cargo-metadata..."); tokio::time::sleep(Duration::from_millis(2000)).await; for x in 1..=100 { tokio::time::sleep(Duration::from_millis(2000)).await; - println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: (Try {x}) Taking a while..."); + eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: (Try {x}) Taking a while..."); if x % 10 == 0 { - println!("{GLOW_STYLE}warning{GLOW_STYLE:#}: maybe check your network connection or build lock?"); + eprintln!("{GLOW_STYLE}warning{GLOW_STYLE:#}: maybe check your network connection or build lock?"); } } }; diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index e3ba8087a..e3dcc90c8 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -75,7 +75,7 @@ impl + Clone, P: Clone + 'static, M: 'static> AnyProp } fn render(&self) -> Element { - fn render_inner(name: &str, res: Result>) -> Element { + fn render_inner(_name: &str, res: Result>) -> Element { match res { Ok(node) => node, Err(err) => { @@ -83,7 +83,7 @@ impl + Clone, P: Clone + 'static, M: 'static> AnyProp // so do nothing #[cfg(not(target_arch = "wasm32"))] { - tracing::error!("Panic while rendering component `{name}`: {err:?}"); + tracing::error!("Panic while rendering component `{_name}`: {err:?}"); } Element::Err(CapturedPanic { error: err }.into()) } diff --git a/packages/dx-wire-format/Cargo.toml b/packages/dx-wire-format/Cargo.toml index 6292bac7c..3342c2cb5 100644 --- a/packages/dx-wire-format/Cargo.toml +++ b/packages/dx-wire-format/Cargo.toml @@ -15,3 +15,5 @@ rust-version = "1.79.0" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } cargo_metadata = { workspace = true } +manganis-core = { workspace = true } +subsecond-types = { workspace = true } diff --git a/packages/dx-wire-format/src/lib.rs b/packages/dx-wire-format/src/lib.rs index dcad395b5..f3453f163 100644 --- a/packages/dx-wire-format/src/lib.rs +++ b/packages/dx-wire-format/src/lib.rs @@ -1,6 +1,8 @@ use cargo_metadata::{diagnostic::Diagnostic, CompilerMessage}; +use manganis_core::BundledAsset; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{borrow::Cow, collections::HashSet, path::PathBuf}; +use subsecond_types::JumpTable; pub use cargo_metadata; @@ -20,18 +22,26 @@ pub use cargo_metadata; /// but they are not guaranteed to be, such that we can provide better error messages for the user. #[allow(clippy::large_enum_variant)] #[non_exhaustive] -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub enum StructuredOutput { BuildsFinished { - client: PathBuf, - server: Option, + client: StructuredBuildArtifacts, + server: Option, + }, + PrintCargoArgs { + args: Vec, + env: Vec<(Cow<'static, str>, String)>, }, BuildFinished { - path: PathBuf, + artifacts: StructuredBuildArtifacts, }, BuildUpdate { stage: BuildStage, }, + Hotpatch { + jump_table: JumpTable, + artifacts: StructuredBuildArtifacts, + }, CargoOutput { message: CompilerMessage, }, @@ -40,6 +50,8 @@ pub enum StructuredOutput { }, BundleOutput { bundles: Vec, + client: StructuredBuildArtifacts, + server: Option, }, HtmlTranslate { html: String, @@ -50,12 +62,22 @@ pub enum StructuredOutput { }, } -impl std::fmt::Debug for StructuredOutput { +impl std::fmt::Display for StructuredOutput { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&serde_json::to_string(self).map_err(|_e| std::fmt::Error)?) } } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct StructuredBuildArtifacts { + pub path: PathBuf, + pub exe: PathBuf, + pub rustc_args: Vec, + pub rustc_envs: Vec<(String, String)>, + pub link_args: Vec, + pub assets: HashSet, // the serialized asset manifest +} + /// The current stage of the ongoing build /// /// This is a perma-unstable interface that is subject to change at any time.