diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index 3147990d5..2f2070b1a 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -72,10 +72,14 @@ mlua = { version = "0.8.1", features = [ ctrlc = "3.2.3" gitignore = "1.0.7" open = "4.1.0" -cargo-generate = "0.18.3" +cargo-generate = { git = "https://github.com/Demonthos/cargo-generate", branch = "update-tempfile" } toml_edit = "0.19.11" # dioxus-rsx = "0.0.1" +# bundling +tauri-bundler = { version = "2.0.0-alpha.6", features = ["native-tls-vendored"] } +tauri-utils = "2.0.0-alpha.6" + dioxus-autofmt = { workspace = true } dioxus-check = { workspace = true } rsx-rosetta = { workspace = true } diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml index dfdeb9f92..6386fb76e 100644 --- a/packages/cli/src/assets/dioxus.toml +++ b/packages/cli/src/assets/dioxus.toml @@ -45,3 +45,30 @@ script = [] available = true required = [] + +[bundler] +# Bundle identifier +identifier = "io.github.{{project-name}}" + +# Bundle publisher +publisher = "{{project-name}}" + +# Bundle icon +icon = ["icons/icon.png"] + +# Bundle resources +resources = ["public/*"] + +# Bundle copyright +copyright = "" + +# Bundle category +category = "Utility" + +# Bundle short description +short_description = "An amazing dioxus application." + +# Bundle long description +long_description = """ +An amazing dioxus application. +""" \ No newline at end of file diff --git a/packages/cli/src/assets/icon.ico b/packages/cli/src/assets/icon.ico new file mode 100644 index 000000000..eed0c0973 Binary files /dev/null and b/packages/cli/src/assets/icon.ico differ diff --git a/packages/cli/src/cli/bundle.rs b/packages/cli/src/cli/bundle.rs new file mode 100644 index 000000000..76c494056 --- /dev/null +++ b/packages/cli/src/cli/bundle.rs @@ -0,0 +1,166 @@ +use core::panic; +use std::{fs::create_dir_all, str::FromStr}; + +use tauri_bundler::{BundleSettings, PackageSettings, SettingsBuilder}; + +use super::*; +use crate::{build_desktop, cfg::ConfigOptsBundle}; + +/// Build the Rust WASM app and all of its assets. +#[derive(Clone, Debug, Parser)] +#[clap(name = "bundle")] +pub struct Bundle { + #[clap(long)] + pub package: Option>, + #[clap(flatten)] + pub build: ConfigOptsBundle, +} + +#[derive(Clone, Debug)] +pub enum PackageType { + MacOsBundle, + IosBundle, + WindowsMsi, + Deb, + Rpm, + AppImage, + Dmg, + Updater, +} + +impl FromStr for PackageType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "macos" => Ok(PackageType::MacOsBundle), + "ios" => Ok(PackageType::IosBundle), + "msi" => Ok(PackageType::WindowsMsi), + "deb" => Ok(PackageType::Deb), + "rpm" => Ok(PackageType::Rpm), + "appimage" => Ok(PackageType::AppImage), + "dmg" => Ok(PackageType::Dmg), + _ => Err(format!("{} is not a valid package type", s)), + } + } +} + +impl From for tauri_bundler::PackageType { + fn from(val: PackageType) -> Self { + match val { + PackageType::MacOsBundle => tauri_bundler::PackageType::MacOsBundle, + PackageType::IosBundle => tauri_bundler::PackageType::IosBundle, + PackageType::WindowsMsi => tauri_bundler::PackageType::WindowsMsi, + PackageType::Deb => tauri_bundler::PackageType::Deb, + PackageType::Rpm => tauri_bundler::PackageType::Rpm, + PackageType::AppImage => tauri_bundler::PackageType::AppImage, + PackageType::Dmg => tauri_bundler::PackageType::Dmg, + PackageType::Updater => tauri_bundler::PackageType::Updater, + } + } +} + +impl Bundle { + pub fn bundle(self, bin: Option) -> Result<()> { + let mut crate_config = crate::CrateConfig::new(bin)?; + + // change the release state. + crate_config.with_release(self.build.release); + crate_config.with_verbose(self.build.verbose); + + if self.build.example.is_some() { + crate_config.as_example(self.build.example.unwrap()); + } + + if self.build.profile.is_some() { + crate_config.set_profile(self.build.profile.unwrap()); + } + + // build the desktop app + build_desktop(&crate_config, false)?; + + // copy the binary to the out dir + let package = crate_config.manifest.package.unwrap(); + + let mut name: PathBuf = match &crate_config.executable { + crate::ExecutableType::Binary(name) + | crate::ExecutableType::Lib(name) + | crate::ExecutableType::Example(name) => name, + } + .into(); + if cfg!(windows) { + name.set_extension("exe"); + } + + // bundle the app + let binaries = vec![ + tauri_bundler::BundleBinary::new(name.display().to_string(), true) + .set_src_path(Some(crate_config.crate_dir.display().to_string())), + ]; + + let mut bundle_settings: BundleSettings = crate_config.dioxus_config.bundle.clone().into(); + if cfg!(windows) { + let windows_icon_override = crate_config + .dioxus_config + .bundle + .windows + .as_ref() + .map(|w| &w.icon_path); + if windows_icon_override.is_none() { + let icon_path = bundle_settings + .icon + .as_ref() + .and_then(|icons| icons.first()); + let icon_path = if let Some(icon_path) = icon_path { + icon_path.into() + } else { + let path = PathBuf::from("./icons/icon.ico"); + // create the icon if it doesn't exist + if !path.exists() { + create_dir_all(path.parent().unwrap()).unwrap(); + let mut file = File::create(&path).unwrap(); + file.write_all(include_bytes!("../assets/icon.ico")) + .unwrap(); + } + path + }; + bundle_settings.windows.icon_path = icon_path; + } + } + + let mut settings = SettingsBuilder::new() + .project_out_directory(crate_config.out_dir) + .package_settings(PackageSettings { + product_name: crate_config.dioxus_config.application.name.clone(), + version: package.version, + description: package.description.unwrap_or_default(), + homepage: package.homepage, + authors: Some(package.authors), + default_run: Some(crate_config.dioxus_config.application.name.clone()), + }) + .binaries(binaries) + .bundle_settings(bundle_settings); + if let Some(packages) = self.package { + settings = settings.package_types( + packages + .into_iter() + .map(|p| p.parse::().unwrap().into()) + .collect(), + ); + } + let settings = settings.build(); + + // on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567) + #[cfg(target_os = "macos")] + std::env::set_var("CI", "true"); + + tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{ + #[cfg(target_os = "macos")] + panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err); + #[cfg(not(target_os = "macos"))] + panic!("Failed to bundle project: {}", err); + }); + + Ok(()) + } +} diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index edc32f346..687780228 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -107,3 +107,33 @@ pub fn parse_public_url(val: &str) -> String { let suffix = if !val.ends_with('/') { "/" } else { "" }; format!("{}{}{}", prefix, val, suffix) } + +/// Config options for the bundling system. +#[derive(Clone, Debug, Default, Deserialize, Parser)] +pub struct ConfigOptsBundle { + /// Build in release mode [default: false] + #[clap(long)] + #[serde(default)] + pub release: bool, + + // Use verbose output [default: false] + #[clap(long)] + #[serde(default)] + pub verbose: bool, + + /// Build a example [default: ""] + #[clap(long)] + pub example: Option, + + /// Build with custom profile + #[clap(long)] + pub profile: Option, + + /// Build platform: support Web & Desktop [default: "default_platform"] + #[clap(long)] + pub platform: Option, + + /// Space separated list of features to activate + #[clap(long)] + pub features: Option>, +} diff --git a/packages/cli/src/cli/mod.rs b/packages/cli/src/cli/mod.rs index fcbb55dcf..37d64d2f3 100644 --- a/packages/cli/src/cli/mod.rs +++ b/packages/cli/src/cli/mod.rs @@ -1,5 +1,6 @@ pub mod autoformat; pub mod build; +pub mod bundle; pub mod cfg; pub mod check; pub mod clean; @@ -60,6 +61,9 @@ pub enum Commands { /// Clean output artifacts. Clean(clean::Clean), + /// Bundle the Rust desktop app and all of its assets. + Bundle(bundle::Bundle), + /// Print the version of this extension #[clap(name = "version")] Version(version::Version), @@ -93,6 +97,7 @@ impl Display for Commands { Commands::Version(_) => write!(f, "version"), Commands::Autoformat(_) => write!(f, "fmt"), Commands::Check(_) => write!(f, "check"), + Commands::Bundle(_) => write!(f, "bundle"), #[cfg(feature = "plugin")] Commands::Plugin(_) => write!(f, "plugin"), diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index e46b4f0fe..e405721d8 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -11,6 +11,9 @@ pub struct DioxusConfig { pub web: WebConfig, + #[serde(default)] + pub bundle: BundleConfig, + #[serde(default = "default_plugin")] pub plugin: toml::Value, } @@ -40,7 +43,7 @@ impl DioxusConfig { }; let dioxus_conf_file = dioxus_conf_file.as_path(); - toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) + let cfg = toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) .map_err(|err| { let error_location = dioxus_conf_file .strip_prefix(crate_dir) @@ -48,7 +51,20 @@ impl DioxusConfig { .display(); crate::Error::Unique(format!("{error_location} {err}")) }) - .map(Some) + .map(Some); + match cfg { + Ok(Some(mut cfg)) => { + let name = cfg.application.name.clone(); + if cfg.bundle.identifier.is_none() { + cfg.bundle.identifier = Some(format!("io.github.{name}")); + } + if cfg.bundle.publisher.is_none() { + cfg.bundle.publisher = Some(name); + } + Ok(Some(cfg)) + } + cfg => cfg, + } } } @@ -70,9 +86,10 @@ fn acquire_dioxus_toml(dir: &Path) -> Option { impl Default for DioxusConfig { fn default() -> Self { + let name = "name"; Self { application: ApplicationConfig { - name: "dioxus".into(), + name: name.into(), default_platform: Platform::Web, out_dir: Some(PathBuf::from("dist")), asset_dir: Some(PathBuf::from("public")), @@ -107,6 +124,11 @@ impl Default for DioxusConfig { cert_path: None, }, }, + bundle: BundleConfig { + identifier: Some(format!("io.github.{name}")), + publisher: Some(name.into()), + ..Default::default() + }, plugin: toml::Value::Table(toml::map::Map::new()), } } @@ -310,3 +332,251 @@ impl CrateConfig { self } } + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct BundleConfig { + pub identifier: Option, + pub publisher: Option, + pub icon: Option>, + pub resources: Option>, + pub copyright: Option, + pub category: Option, + pub short_description: Option, + pub long_description: Option, + pub external_bin: Option>, + pub deb: Option, + pub macos: Option, + pub windows: Option, +} + +impl From for tauri_bundler::BundleSettings { + fn from(val: BundleConfig) -> Self { + tauri_bundler::BundleSettings { + identifier: val.identifier, + publisher: val.publisher, + icon: val.icon, + resources: val.resources, + copyright: val.copyright, + category: val.category.and_then(|c| c.parse().ok()), + short_description: val.short_description, + long_description: val.long_description, + external_bin: val.external_bin, + deb: val.deb.map(Into::into).unwrap_or_default(), + macos: val.macos.map(Into::into).unwrap_or_default(), + windows: val.windows.map(Into::into).unwrap_or_default(), + ..Default::default() + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DebianSettings { + pub depends: Option>, + pub files: HashMap, + pub nsis: Option, +} + +impl From for tauri_bundler::DebianSettings { + fn from(val: DebianSettings) -> Self { + tauri_bundler::DebianSettings { + depends: val.depends, + files: val.files, + desktop_template: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WixSettings { + pub language: Vec<(String, Option)>, + pub template: Option, + pub fragment_paths: Vec, + pub component_group_refs: Vec, + pub component_refs: Vec, + pub feature_group_refs: Vec, + pub feature_refs: Vec, + pub merge_refs: Vec, + pub skip_webview_install: bool, + pub license: Option, + pub enable_elevated_update_task: bool, + pub banner_path: Option, + pub dialog_image_path: Option, + pub fips_compliant: bool, +} + +impl From for tauri_bundler::WixSettings { + fn from(val: WixSettings) -> Self { + tauri_bundler::WixSettings { + language: tauri_bundler::bundle::WixLanguage({ + let mut languages: Vec<_> = val + .language + .iter() + .map(|l| { + ( + l.0.clone(), + tauri_bundler::bundle::WixLanguageConfig { + locale_path: l.1.clone(), + }, + ) + }) + .collect(); + if languages.is_empty() { + languages.push(("en-US".into(), Default::default())); + } + languages + }), + template: val.template, + fragment_paths: val.fragment_paths, + component_group_refs: val.component_group_refs, + component_refs: val.component_refs, + feature_group_refs: val.feature_group_refs, + feature_refs: val.feature_refs, + merge_refs: val.merge_refs, + skip_webview_install: val.skip_webview_install, + license: val.license, + enable_elevated_update_task: val.enable_elevated_update_task, + banner_path: val.banner_path, + dialog_image_path: val.dialog_image_path, + fips_compliant: val.fips_compliant, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct MacOsSettings { + pub frameworks: Option>, + pub minimum_system_version: Option, + pub license: Option, + pub exception_domain: Option, + pub signing_identity: Option, + pub provider_short_name: Option, + pub entitlements: Option, + pub info_plist_path: Option, +} + +impl From for tauri_bundler::MacOsSettings { + fn from(val: MacOsSettings) -> Self { + tauri_bundler::MacOsSettings { + frameworks: val.frameworks, + minimum_system_version: val.minimum_system_version, + license: val.license, + exception_domain: val.exception_domain, + signing_identity: val.signing_identity, + provider_short_name: val.provider_short_name, + entitlements: val.entitlements, + info_plist_path: val.info_plist_path, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WindowsSettings { + pub digest_algorithm: Option, + pub certificate_thumbprint: Option, + pub timestamp_url: Option, + pub tsp: bool, + pub wix: Option, + pub icon_path: Option, + pub webview_install_mode: WebviewInstallMode, + pub webview_fixed_runtime_path: Option, + pub allow_downgrades: bool, + pub nsis: Option, +} + +impl From for tauri_bundler::WindowsSettings { + fn from(val: WindowsSettings) -> Self { + tauri_bundler::WindowsSettings { + digest_algorithm: val.digest_algorithm, + certificate_thumbprint: val.certificate_thumbprint, + timestamp_url: val.timestamp_url, + tsp: val.tsp, + wix: val.wix.map(Into::into), + icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()), + webview_install_mode: val.webview_install_mode.into(), + webview_fixed_runtime_path: val.webview_fixed_runtime_path, + allow_downgrades: val.allow_downgrades, + nsis: val.nsis.map(Into::into), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NsisSettings { + pub template: Option, + pub license: Option, + pub header_image: Option, + pub sidebar_image: Option, + pub installer_icon: Option, + pub install_mode: NSISInstallerMode, + pub languages: Option>, + pub custom_language_files: Option>, + pub display_language_selector: bool, +} + +impl From for tauri_bundler::NsisSettings { + fn from(val: NsisSettings) -> Self { + tauri_bundler::NsisSettings { + template: val.template, + license: val.license, + header_image: val.header_image, + sidebar_image: val.sidebar_image, + installer_icon: val.installer_icon, + install_mode: val.install_mode.into(), + languages: val.languages, + custom_language_files: val.custom_language_files, + display_language_selector: val.display_language_selector, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NSISInstallerMode { + CurrentUser, + PerMachine, + Both, +} + +impl From for tauri_utils::config::NSISInstallerMode { + fn from(val: NSISInstallerMode) -> Self { + match val { + NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser, + NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine, + NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum WebviewInstallMode { + Skip, + DownloadBootstrapper { silent: bool }, + EmbedBootstrapper { silent: bool }, + OfflineInstaller { silent: bool }, + FixedRuntime { path: PathBuf }, +} + +impl WebviewInstallMode { + fn into(self) -> tauri_utils::config::WebviewInstallMode { + match self { + Self::Skip => tauri_utils::config::WebviewInstallMode::Skip, + Self::DownloadBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent } + } + Self::EmbedBootstrapper { silent } => { + tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent } + } + Self::OfflineInstaller { silent } => { + tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent } + } + Self::FixedRuntime { path } => { + tauri_utils::config::WebviewInstallMode::FixedRuntime { path } + } + } + } +} + +impl Default for WebviewInstallMode { + fn default() -> Self { + Self::OfflineInstaller { silent: false } + } +} diff --git a/packages/cli/src/main.rs b/packages/cli/src/main.rs index d17aa652e..fe860a0e6 100644 --- a/packages/cli/src/main.rs +++ b/packages/cli/src/main.rs @@ -92,6 +92,10 @@ async fn main() -> anyhow::Result<()> { .config() .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), + Bundle(opts) => opts + .bundle(bin.clone()) + .map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)), + #[cfg(feature = "plugin")] Plugin(opts) => opts .plugin()