Sfoglia il codice sorgente

Merge pull request #1254 from Demonthos/add-bundle-command

Create cli bundle command
Jonathan Kelley 1 anno fa
parent
commit
4b96b00591

+ 5 - 1
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 = "0.18"
 toml_edit = "0.19.11"
 # dioxus-rsx = "0.0.1"
 
+# bundling
+tauri-bundler = { version = "1.2", features = ["native-tls-vendored"] }
+tauri-utils = "1.3"
+
 dioxus-autofmt = { workspace = true }
 dioxus-check = { workspace = true }
 rsx-rosetta = { workspace = true }

+ 27 - 0
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.
+"""

BIN
packages/cli/src/assets/icon.ico


+ 166 - 0
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<Vec<String>>,
+    #[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<Self, Self::Err> {
+        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<PackageType> 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<PathBuf>) -> 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::<PackageType>().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(())
+    }
+}

+ 30 - 0
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<String>,
+
+    /// Build with custom profile
+    #[clap(long)]
+    pub profile: Option<String>,
+
+    /// Build platform: support Web & Desktop [default: "default_platform"]
+    #[clap(long)]
+    pub platform: Option<String>,
+
+    /// Space separated list of features to activate
+    #[clap(long)]
+    pub features: Option<Vec<String>>,
+}

+ 5 - 0
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"),

+ 270 - 3
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::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
+        let cfg = toml::from_str::<DioxusConfig>(&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<PathBuf> {
 
 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,248 @@ impl CrateConfig {
         self
     }
 }
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct BundleConfig {
+    pub identifier: Option<String>,
+    pub publisher: Option<String>,
+    pub icon: Option<Vec<String>>,
+    pub resources: Option<Vec<String>>,
+    pub copyright: Option<String>,
+    pub category: Option<String>,
+    pub short_description: Option<String>,
+    pub long_description: Option<String>,
+    pub external_bin: Option<Vec<String>>,
+    pub deb: Option<DebianSettings>,
+    pub macos: Option<MacOsSettings>,
+    pub windows: Option<WindowsSettings>,
+}
+
+impl From<BundleConfig> 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<Vec<String>>,
+    pub files: HashMap<PathBuf, PathBuf>,
+    pub nsis: Option<NsisSettings>,
+}
+
+impl From<DebianSettings> for tauri_bundler::DebianSettings {
+    fn from(val: DebianSettings) -> Self {
+        tauri_bundler::DebianSettings {
+            depends: val.depends,
+            files: val.files,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct WixSettings {
+    pub language: Vec<(String, Option<PathBuf>)>,
+    pub template: Option<PathBuf>,
+    pub fragment_paths: Vec<PathBuf>,
+    pub component_group_refs: Vec<String>,
+    pub component_refs: Vec<String>,
+    pub feature_group_refs: Vec<String>,
+    pub feature_refs: Vec<String>,
+    pub merge_refs: Vec<String>,
+    pub skip_webview_install: bool,
+    pub license: Option<PathBuf>,
+    pub enable_elevated_update_task: bool,
+    pub banner_path: Option<PathBuf>,
+    pub dialog_image_path: Option<PathBuf>,
+    pub fips_compliant: bool,
+}
+
+impl From<WixSettings> 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<Vec<String>>,
+    pub minimum_system_version: Option<String>,
+    pub license: Option<String>,
+    pub exception_domain: Option<String>,
+    pub signing_identity: Option<String>,
+    pub provider_short_name: Option<String>,
+    pub entitlements: Option<String>,
+    pub info_plist_path: Option<PathBuf>,
+}
+
+impl From<MacOsSettings> 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<String>,
+    pub certificate_thumbprint: Option<String>,
+    pub timestamp_url: Option<String>,
+    pub tsp: bool,
+    pub wix: Option<WixSettings>,
+    pub icon_path: Option<PathBuf>,
+    pub webview_install_mode: WebviewInstallMode,
+    pub webview_fixed_runtime_path: Option<PathBuf>,
+    pub allow_downgrades: bool,
+    pub nsis: Option<NsisSettings>,
+}
+
+impl From<WindowsSettings> 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<PathBuf>,
+    pub license: Option<PathBuf>,
+    pub header_image: Option<PathBuf>,
+    pub sidebar_image: Option<PathBuf>,
+    pub installer_icon: Option<PathBuf>,
+    pub install_mode: NSISInstallerMode,
+    pub languages: Option<Vec<String>>,
+    pub custom_language_files: Option<HashMap<String, PathBuf>>,
+    pub display_language_selector: bool,
+}
+
+impl From<NsisSettings> for tauri_bundler::NsisSettings {
+    fn from(val: NsisSettings) -> Self {
+        tauri_bundler::NsisSettings {
+            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,
+            display_language_selector: val.display_language_selector,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum NSISInstallerMode {
+    CurrentUser,
+    PerMachine,
+    Both,
+}
+
+impl From<NSISInstallerMode> 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 }
+    }
+}

+ 4 - 0
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()