create cli bundle command

This commit is contained in:
Evan Almloff 2023-07-26 12:40:50 -07:00
parent d637ef187c
commit 489338d642
8 changed files with 510 additions and 4 deletions

View file

@ -72,10 +72,14 @@ mlua = { version = "0.8.1", features = [
ctrlc = "3.2.3" ctrlc = "3.2.3"
gitignore = "1.0.7" gitignore = "1.0.7"
open = "4.1.0" 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" toml_edit = "0.19.11"
# dioxus-rsx = "0.0.1" # 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-autofmt = { workspace = true }
dioxus-check = { workspace = true } dioxus-check = { workspace = true }
rsx-rosetta = { workspace = true } rsx-rosetta = { workspace = true }

View file

@ -45,3 +45,30 @@ script = []
available = true available = true
required = [] 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.
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -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(())
}
}

View file

@ -107,3 +107,33 @@ pub fn parse_public_url(val: &str) -> String {
let suffix = if !val.ends_with('/') { "/" } else { "" }; let suffix = if !val.ends_with('/') { "/" } else { "" };
format!("{}{}{}", prefix, val, suffix) 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>>,
}

View file

@ -1,5 +1,6 @@
pub mod autoformat; pub mod autoformat;
pub mod build; pub mod build;
pub mod bundle;
pub mod cfg; pub mod cfg;
pub mod check; pub mod check;
pub mod clean; pub mod clean;
@ -60,6 +61,9 @@ pub enum Commands {
/// Clean output artifacts. /// Clean output artifacts.
Clean(clean::Clean), Clean(clean::Clean),
/// Bundle the Rust desktop app and all of its assets.
Bundle(bundle::Bundle),
/// Print the version of this extension /// Print the version of this extension
#[clap(name = "version")] #[clap(name = "version")]
Version(version::Version), Version(version::Version),
@ -93,6 +97,7 @@ impl Display for Commands {
Commands::Version(_) => write!(f, "version"), Commands::Version(_) => write!(f, "version"),
Commands::Autoformat(_) => write!(f, "fmt"), Commands::Autoformat(_) => write!(f, "fmt"),
Commands::Check(_) => write!(f, "check"), Commands::Check(_) => write!(f, "check"),
Commands::Bundle(_) => write!(f, "bundle"),
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
Commands::Plugin(_) => write!(f, "plugin"), Commands::Plugin(_) => write!(f, "plugin"),

View file

@ -11,6 +11,9 @@ pub struct DioxusConfig {
pub web: WebConfig, pub web: WebConfig,
#[serde(default)]
pub bundle: BundleConfig,
#[serde(default = "default_plugin")] #[serde(default = "default_plugin")]
pub plugin: toml::Value, pub plugin: toml::Value,
} }
@ -40,7 +43,7 @@ impl DioxusConfig {
}; };
let dioxus_conf_file = dioxus_conf_file.as_path(); 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| { .map_err(|err| {
let error_location = dioxus_conf_file let error_location = dioxus_conf_file
.strip_prefix(crate_dir) .strip_prefix(crate_dir)
@ -48,7 +51,20 @@ impl DioxusConfig {
.display(); .display();
crate::Error::Unique(format!("{error_location} {err}")) 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 { impl Default for DioxusConfig {
fn default() -> Self { fn default() -> Self {
let name = "name";
Self { Self {
application: ApplicationConfig { application: ApplicationConfig {
name: "dioxus".into(), name: name.into(),
default_platform: Platform::Web, default_platform: Platform::Web,
out_dir: Some(PathBuf::from("dist")), out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")), asset_dir: Some(PathBuf::from("public")),
@ -107,6 +124,11 @@ impl Default for DioxusConfig {
cert_path: None, 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()), plugin: toml::Value::Table(toml::map::Map::new()),
} }
} }
@ -310,3 +332,251 @@ impl CrateConfig {
self 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,
desktop_template: None,
}
}
}
#[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 {
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<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 }
}
}

View file

@ -92,6 +92,10 @@ async fn main() -> anyhow::Result<()> {
.config() .config()
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), .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")] #[cfg(feature = "plugin")]
Plugin(opts) => opts Plugin(opts) => opts
.plugin() .plugin()