diff --git a/Cargo.lock b/Cargo.lock index dade4720d..1721b4a37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,6 +2855,19 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -3478,6 +3491,7 @@ dependencies = [ "dioxus-rsx", "dioxus-rsx-hotreload", "dioxus-rsx-rosetta", + "dircpy", "dirs", "dunce", "env_logger 0.11.5", @@ -4242,6 +4256,17 @@ dependencies = [ "tower-http", ] +[[package]] +name = "dircpy" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88521b0517f5f9d51d11925d8ab4523497dcf947073fa3231a311b63941131c" +dependencies = [ + "jwalk", + "log", + "walkdir", +] + [[package]] name = "dirs" version = "5.0.1" @@ -7206,6 +7231,16 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + [[package]] name = "jzon" version = "0.12.5" diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index be167cd7c..89235aa48 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -119,6 +119,7 @@ tauri-bundler = { workspace = true } include_dir = "0.7.4" flate2 = "1.0.35" tar = "0.4.43" +dircpy = "0.3.19" [build-dependencies] built = { version = "=0.7.4", features = ["git2"] } diff --git a/packages/cli/src/build/web.rs b/packages/cli/src/build/web.rs index f89d43e7d..b113dbfb9 100644 --- a/packages/cli/src/build/web.rs +++ b/packages/cli/src/build/web.rs @@ -154,8 +154,12 @@ impl BuildRequest { // 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.krate.legacy_asset_dir().join(path); - if let Ok(absolute_path) = asset_dir_path.canonicalize() { + let asset_dir_path = self + .krate + .legacy_asset_dir() + .map(|dir| dir.join(path).canonicalize()); + + if let Some(Ok(absolute_path)) = asset_dir_path { let absolute_crate_root = self.krate.crate_dir().canonicalize().unwrap(); PathBuf::from("./") .join(absolute_path.strip_prefix(absolute_crate_root).unwrap()) @@ -179,7 +183,7 @@ impl BuildRequest { ResourceType::Script => "web.resource.script", }; - tracing::debug!( + 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```" ); } diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs index bdeadc2f6..cb07393c8 100644 --- a/packages/cli/src/cli/bundle.rs +++ b/packages/cli/src/cli/bundle.rs @@ -1,5 +1,6 @@ use crate::{AppBundle, BuildArgs, Builder, DioxusCrate, Platform}; use anyhow::Context; +use path_absolutize::Absolutize; use std::collections::HashMap; use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder}; @@ -25,12 +26,13 @@ pub struct Bundle { /// The directory in which the final bundle will be placed. /// - /// Relative paths will be placed relative to the current working directory. + /// Relative paths will be placed relative to the current working directory if specified. + /// Otherwise, the out_dir path specified in Dioxus.toml will be used (relative to the crate root). /// /// We will flatten the artifacts into this directory - there will be no differentiation between /// artifacts produced by different platforms. #[clap(long)] - pub outdir: Option, + pub out_dir: Option, /// The arguments for the dioxus build #[clap(flatten)] @@ -54,72 +56,38 @@ impl Bundle { .finish() .await?; - tracing::info!("Copying app to output directory..."); - // 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() { self.package_types = Some(vec![crate::PackageType::IosBundle]); } - let mut cmd_result = StructuredOutput::Success; + let mut bundles = vec![]; + // Copy the server over if it exists + if bundle.build.build.fullstack { + bundles.push(bundle.server_exe().unwrap()); + } + + // Create a list of bundles that we might need to copy match self.build_arguments.platform() { // By default, mac/win/linux work with tauri bundle Platform::MacOS | Platform::Linux | Platform::Windows => { - let bundles = self.bundle_desktop(krate, bundle)?; - - tracing::info!("Bundled app successfully!"); - tracing::info!("App produced {} outputs:", bundles.len()); - tracing::debug!("Bundling produced bundles: {:#?}", bundles); - - // Copy the bundles to the output directory and log their locations - let mut bundle_paths = vec![]; - for bundle in bundles { - for src in bundle.bundle_paths { - let src = if let Some(outdir) = &self.outdir { - let dest = outdir.join(src.file_name().expect("Filename to exist")); - crate::fastfs::copy_asset(&src, &dest) - .context("Failed to copy or compress optimized asset")?; - dest - } else { - src.clone() - }; - - tracing::info!( - "{} - [{}]", - bundle.package_type.short_name(), - src.display() - ); - - bundle_paths.push(src); - } + tracing::info!("Running desktop bundler..."); + for bundle in self.bundle_desktop(&krate, &bundle)? { + bundles.extend(bundle.bundle_paths); } - - cmd_result = StructuredOutput::BundleOutput { - bundles: bundle_paths, - }; - } - - Platform::Web => { - tracing::info!("App available at: {}", bundle.build.root_dir().display()); } + // Web/ios can just use their root_dir + Platform::Web => bundles.push(bundle.build.root_dir()), Platform::Ios => { - tracing::warn!("Signed iOS bundles are not yet supported"); - tracing::info!( - "The bundle is available at: {}", - bundle.build.root_dir().display() - ); + tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing."); + bundles.push(bundle.build.root_dir()) } + Platform::Server => bundles.push(bundle.build.root_dir()), + Platform::Liveview => bundles.push(bundle.build.root_dir()), - Platform::Server => { - tracing::info!("Server available at: {}", bundle.build.root_dir().display()) - } - Platform::Liveview => tracing::info!( - "Liveview server available at: {}", - bundle.build.root_dir().display() - ), - + // todo(jon): we can technically create apks (already do...) just need to expose it Platform::Android => { return Err(Error::UnsupportedFeature( "Android bundles are not yet supported".into(), @@ -127,13 +95,54 @@ impl Bundle { } }; - Ok(cmd_result) + // Copy the bundles to the output directory if one was specified + let crate_outdir = bundle.build.krate.crate_out_dir(); + if let Some(outdir) = self.out_dir.clone().or(crate_outdir) { + let outdir = outdir + .absolutize() + .context("Failed to absolutize output directory")?; + + tracing::info!("Copying bundles to output directory: {}", outdir.display()); + + std::fs::create_dir_all(&outdir)?; + + for bundle_path in bundles.iter_mut() { + let destination = outdir.join(bundle_path.file_name().unwrap()); + + tracing::debug!( + "Copying from {} to {}", + bundle_path.display(), + destination.display() + ); + + if bundle_path.is_dir() { + dircpy::CopyBuilder::new(&bundle_path, &destination) + .overwrite(true) + .run_par() + .context("Failed to copy the app to output directory")?; + } else { + std::fs::copy(&bundle_path, &destination) + .context("Failed to copy the app to output directory")?; + } + + *bundle_path = destination; + } + } + + for bundle_path in bundles.iter() { + tracing::info!( + "Bundled app at: {}", + bundle_path.absolutize().unwrap().display() + ); + } + + Ok(StructuredOutput::BundleOutput { bundles }) } fn bundle_desktop( &self, - krate: DioxusCrate, - bundle: AppBundle, + krate: &DioxusCrate, + bundle: &AppBundle, ) -> Result, Error> { _ = std::fs::remove_dir_all(krate.bundle_dir(self.build_arguments.platform())); diff --git a/packages/cli/src/config/app.rs b/packages/cli/src/config/app.rs index c1e4bd72d..02f187430 100644 --- a/packages/cli/src/config/app.rs +++ b/packages/cli/src/config/app.rs @@ -3,13 +3,12 @@ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct ApplicationConfig { - #[serde(default = "asset_dir_default")] - pub(crate) asset_dir: PathBuf, + #[serde(default)] + pub(crate) asset_dir: Option, #[serde(default)] pub(crate) sub_package: Option, -} -pub(crate) fn asset_dir_default() -> PathBuf { - PathBuf::from("assets") + #[serde(default)] + pub(crate) out_dir: Option, } diff --git a/packages/cli/src/config/dioxus_config.rs b/packages/cli/src/config/dioxus_config.rs index ec8e7607f..4c03708e8 100644 --- a/packages/cli/src/config/dioxus_config.rs +++ b/packages/cli/src/config/dioxus_config.rs @@ -22,8 +22,9 @@ impl Default for DioxusConfig { fn default() -> Self { Self { application: ApplicationConfig { - asset_dir: asset_dir_default(), + asset_dir: None, sub_package: None, + out_dir: None, }, web: WebConfig { app: WebAppConfig { diff --git a/packages/cli/src/dioxus_crate.rs b/packages/cli/src/dioxus_crate.rs index 4b8ae80e9..eeaabf177 100644 --- a/packages/cli/src/dioxus_crate.rs +++ b/packages/cli/src/dioxus_crate.rs @@ -115,17 +115,28 @@ impl DioxusCrate { }) } - /// Compose an asset directory. Represents the typical "public" directory - /// with publicly available resources (configurable in the `Dioxus.toml`). - pub(crate) fn legacy_asset_dir(&self) -> PathBuf { - self.crate_dir().join(&self.config.application.asset_dir) + /// The asset dir we used to support before manganis became the default. + /// This generally was just a folder in your Dioxus.toml called "assets" or "public" where users + /// would store their assets. + /// + /// With manganis you now use `asset!()` and we pick it up automatically. + pub(crate) fn legacy_asset_dir(&self) -> Option { + self.config + .application + .asset_dir + .clone() + .map(|dir| self.crate_dir().join(dir)) } /// Get the list of files in the "legacy" asset directory pub(crate) fn legacy_asset_dir_files(&self) -> Vec { let mut files = vec![]; - let Ok(read_dir) = self.legacy_asset_dir().read_dir() else { + let Some(legacy_asset_dir) = self.legacy_asset_dir() else { + return files; + }; + + let Ok(read_dir) = legacy_asset_dir.read_dir() else { return files; }; @@ -136,10 +147,20 @@ impl DioxusCrate { files } + /// Get the outdir specified by the Dioxus.toml, relative to the crate directory. + /// We don't support workspaces yet since that would cause a collision of bundles per project. + pub(crate) fn crate_out_dir(&self) -> Option { + self.config + .application + .out_dir + .as_ref() + .map(|out_dir| self.crate_dir().join(out_dir)) + } + /// Compose an out directory. Represents the typical "dist" directory that /// is "distributed" after building an application (configurable in the /// `Dioxus.toml`). - fn out_dir(&self) -> PathBuf { + fn internal_out_dir(&self) -> PathBuf { let dir = self.workspace_dir().join("target").join("dx"); std::fs::create_dir_all(&dir).unwrap(); dir @@ -153,7 +174,7 @@ impl DioxusCrate { /// 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.out_dir() + self.internal_out_dir() .join(self.executable_name()) .join(if release { "release" } else { "debug" }) .join(platform.build_folder_name()) @@ -164,7 +185,7 @@ impl DioxusCrate { /// target/dx/bundle/app/blah.exe /// target/dx/bundle/app/public/ pub(crate) fn bundle_dir(&self, platform: Platform) -> PathBuf { - self.out_dir() + self.internal_out_dir() .join(self.executable_name()) .join("bundle") .join(platform.build_folder_name()) diff --git a/packages/cli/src/fastfs.rs b/packages/cli/src/fastfs.rs index 106a513dd..cce66bd71 100644 --- a/packages/cli/src/fastfs.rs +++ b/packages/cli/src/fastfs.rs @@ -11,68 +11,6 @@ use std::{ use brotli::enc::BrotliEncoderParams; use walkdir::WalkDir; -pub fn copy_asset(src: &Path, dest: &Path) -> std::io::Result<()> { - if src.is_dir() { - copy_dir_to(src, dest, false)?; - } else { - std::fs::copy(src, dest)?; - } - - Ok(()) -} - -pub(crate) fn copy_dir_to( - src_dir: &Path, - dest_dir: &Path, - pre_compress: bool, -) -> std::io::Result<()> { - let entries = std::fs::read_dir(src_dir)?; - let mut children: Vec>> = Vec::new(); - - for entry in entries.flatten() { - let entry_path = entry.path(); - let path_relative_to_src = entry_path.strip_prefix(src_dir).unwrap(); - let output_file_location = dest_dir.join(path_relative_to_src); - children.push(std::thread::spawn(move || { - if entry.file_type()?.is_dir() { - // If the file is a directory, recursively copy it into the output directory - if let Err(err) = copy_dir_to(&entry_path, &output_file_location, pre_compress) { - tracing::error!( - "Failed to pre-compress directory {}: {}", - entry_path.display(), - err - ); - } - } else { - // Make sure the directory exists - std::fs::create_dir_all(output_file_location.parent().unwrap())?; - // Copy the file to the output directory - std::fs::copy(&entry_path, &output_file_location)?; - - // Then pre-compress the file if needed - if pre_compress { - if let Err(err) = pre_compress_file(&output_file_location) { - tracing::error!( - "Failed to pre-compress static assets {}: {}", - output_file_location.display(), - err - ); - } - // If pre-compression isn't enabled, we should remove the old compressed file if it exists - } else if let Some(compressed_path) = compressed_path(&output_file_location) { - _ = std::fs::remove_file(compressed_path); - } - } - - Ok(()) - })); - } - for child in children { - child.join().unwrap()?; - } - Ok(()) -} - /// Get the path to the compressed version of a file fn compressed_path(path: &Path) -> Option { let new_extension = match path.extension() { diff --git a/packages/cli/src/serve/handle.rs b/packages/cli/src/serve/handle.rs index ff6e16245..da48141db 100644 --- a/packages/cli/src/serve/handle.rs +++ b/packages/cli/src/serve/handle.rs @@ -172,14 +172,15 @@ impl AppHandle { tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}"); // If the asset shares the same name in the bundle, reload that - let 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}"); + 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}"); + } } }