rip out tauri bundle (v2), swc -> esbuild, blacksmith -> warpbuild, FAST CI 🚀 (#5328)
- inline mac, windows, linux bundling in the cli - remove tauri bundle from our dep tree - trim down the rpm crate for fewer dependencies in tree - migrate from blacksmith to warpbuild - move off swc and instead use esbuild via tool - use dev drives in windows CI - use fork of nix action for faster caching - test bundling in CI - fix asset handling in linux - add default APPDATA location for webview2 installs for dioxus apps on windows
This commit is contained in:
1 parent
be7296f4f0
commit
b2d01e6779
83 files changed
+7756
-5403
No files matched your search
@@ -145,15 +145,15 @@ pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
|
||||
/// - [x] iOS
|
||||
/// - [x] Windows
|
||||
/// - [x] Linux (appimage)
|
||||
/// - [ ] Linux (rpm)
|
||||
/// - [x] Linux (rpm)
|
||||
/// - [x] Linux (deb)
|
||||
/// - [ ] Android
|
||||
/// - [x] Android (returns current exe.)
|
||||
#[allow(unreachable_code)]
|
||||
fn get_asset_root() -> PathBuf {
|
||||
let cur_exe = std::env::current_exe().unwrap();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// In macOS bundles, the assets are in the `Resources` directory
|
||||
if cfg!(target_os = "macos") {
|
||||
return cur_exe
|
||||
.parent()
|
||||
.unwrap()
|
||||
@@ -162,20 +162,19 @@ fn get_asset_root() -> PathBuf {
|
||||
.join("Resources");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// In linux bundles, the assets are placed in the lib/$product_name directory
|
||||
// bin/
|
||||
// main
|
||||
// lib/
|
||||
// $product_name/
|
||||
// assets/
|
||||
// In linux bundles, the assets are placed in the lib/$product_name directory
|
||||
// bin/
|
||||
// main
|
||||
// lib/
|
||||
// $product_name/
|
||||
// assets/
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(product_name) = dioxus_cli_config::product_name() {
|
||||
let lib_asset_path = || {
|
||||
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
|
||||
path.exists().then_some(path)
|
||||
};
|
||||
if let Some(asset_dir) = lib_asset_path() {
|
||||
let path = cur_exe
|
||||
.parent()
|
||||
.and_then(|parent| parent.parent())
|
||||
.map(|root| root.join("lib").join(product_name));
|
||||
if let Some(asset_dir) = path.filter(|path| path.exists()) {
|
||||
return asset_dir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "dioxus-check"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Dioxus Labs"]
|
||||
description = "Checks Dioxus RSX files for issues"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["web", "desktop", "mobile", "gui", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||
quote = {workspace = true }
|
||||
syn = { workspace = true, features = ["full", "extra-traits", "visit"] }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.6"
|
||||
pretty_assertions = { workspace = true }
|
||||
@@ -1,39 +0,0 @@
|
||||
# dioxus-check
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-check
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/learn/0.7/) |
|
||||
[API Docs](https://docs.rs/dioxus-check) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/learn/0.7/essentials/basics/hooks#rules-of-hooks).
|
||||
|
||||
## Contributing
|
||||
|
||||
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/dioxuslabs/dioxus/blob/main/LICENSE-MIT
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
||||
@@ -1,10 +0,0 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
mod check;
|
||||
mod issues;
|
||||
mod metadata;
|
||||
|
||||
pub use check::check_file;
|
||||
pub use issues::{Issue, IssueReport};
|
||||
@@ -1,81 +0,0 @@
|
||||
[package]
|
||||
name = "dioxus-cli-opt"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
authors = ["Jonathan Kelley", "Evan Almloff"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
description = "CLI optimizations for dioxus-cli"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
manganis = { workspace = true }
|
||||
manganis-core = { workspace = true }
|
||||
object = { workspace = true, features = ["wasm"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
const-serialize = { workspace = true, features = ["serde"] }
|
||||
tracing = { workspace = true }
|
||||
rayon = "1.10.0"
|
||||
|
||||
# Image compression/conversion
|
||||
# - JPEG
|
||||
mozjpeg = { version = "0.10.13", default-features = false, features = [
|
||||
"parallel",
|
||||
] }
|
||||
# - PNG
|
||||
imagequant = "4.4.0"
|
||||
png = "0.17.16"
|
||||
|
||||
# Image format/conversion
|
||||
image = { version = "0.25", features = ["avif"] }
|
||||
|
||||
# CSS Minification
|
||||
lightningcss = { version = "1.0.0-alpha.67", features = [
|
||||
"browserslist",
|
||||
"into_owned",
|
||||
] }
|
||||
|
||||
# SCSS Processing
|
||||
grass = "0.13.4"
|
||||
codemap = "0.1.3"
|
||||
|
||||
# Js minification - swc has introduces minor versions with breaking changes in the past so we pin all of their crates
|
||||
# Root dependencies
|
||||
swc_bundler = { version = "=24.0.0", default-features = false }
|
||||
swc_common = { version = "=13.0.4", features = [
|
||||
"tty-emitter",
|
||||
], default-features = false }
|
||||
swc_ecma_ast = { version = "=13.0.1", default-features = false }
|
||||
swc_ecma_codegen = { version = "=15.0.2", default-features = false }
|
||||
swc_ecma_loader = { version = "=13.0.0", features = [
|
||||
"cache",
|
||||
"node",
|
||||
], default-features = false }
|
||||
swc_ecma_minifier = { version = "=25.0.0", default-features = false }
|
||||
swc_ecma_parser = { version = "=19.0.0", default-features = false }
|
||||
swc_ecma_transforms_base = { version = "=20.0.0", default-features = false }
|
||||
swc_ecma_visit = { version = "=13.0.0", default-features = false }
|
||||
|
||||
# Pinned from cargo tree --prefix none | grep -Eo 'swc[^ ]+ [^ ]+' | sort -u
|
||||
swc_allocator = { version = "4.0.1" }
|
||||
swc_atoms = { version = "6.0.1" }
|
||||
swc_config = { version = "3.1.1" }
|
||||
swc_config_macro = { version = "1.0.1" }
|
||||
swc_ecma_codegen_macros = { version = "2.0.2" }
|
||||
swc_ecma_lexer = { version = "19.0.1" }
|
||||
swc_ecma_transforms_optimization = { version = "21.0.0" }
|
||||
swc_ecma_usage_analyzer = { version = "19.0.2" }
|
||||
swc_ecma_utils = { version = "18.1.0" }
|
||||
swc_eq_ignore_macros = { version = "1.0.1" }
|
||||
swc_graph_analyzer = { version = "14.0.1" }
|
||||
swc_macros_common = { version = "1.0.1" }
|
||||
swc_sourcemap = { version = "9.3.2" }
|
||||
swc_timer = { version = "1.0.0" }
|
||||
swc_visit = { version = "2.0.1" }
|
||||
browserslist-rs = { version = "=0.19.0" }
|
||||
|
||||
[build-dependencies]
|
||||
built = { version = "0.8.0", features = ["git2"] }
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
built::write_built_file().expect("Failed to acquire build-time information");
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// The file has been placed there by the build script.
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
|
||||
pub(crate) fn version() -> String {
|
||||
format!(
|
||||
"{} ({})",
|
||||
PKG_VERSION,
|
||||
GIT_COMMIT_HASH_SHORT.unwrap_or("was built without git repository")
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use std::{
|
||||
io::{BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub(crate) fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> {
|
||||
let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX);
|
||||
let width = image.width() as usize;
|
||||
let height = image.height() as usize;
|
||||
|
||||
comp.set_size(width, height);
|
||||
let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work
|
||||
|
||||
comp.write_scanlines(image.to_rgba8().as_bytes())?;
|
||||
|
||||
let jpeg_bytes = comp.finish()?;
|
||||
|
||||
let file = std::fs::File::create(output_location)?;
|
||||
let w = &mut BufWriter::new(file);
|
||||
w.write_all(&jpeg_bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
use std::{io::BufWriter, path::Path};
|
||||
|
||||
use image::DynamicImage;
|
||||
|
||||
pub(crate) fn compress_png(image: DynamicImage, output_location: &Path) {
|
||||
// Image loading/saving is outside scope of this library
|
||||
let width = image.width() as usize;
|
||||
let height = image.height() as usize;
|
||||
let bitmap: Vec<_> = image
|
||||
.into_rgba8()
|
||||
.pixels()
|
||||
.map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3]))
|
||||
.collect();
|
||||
|
||||
// Configure the library
|
||||
let mut liq = imagequant::new();
|
||||
liq.set_speed(5).unwrap();
|
||||
liq.set_quality(0, 99).unwrap();
|
||||
|
||||
// Describe the bitmap
|
||||
let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap();
|
||||
|
||||
// The magic happens in quantize()
|
||||
let mut res = match liq.quantize(&mut img) {
|
||||
Ok(res) => res,
|
||||
Err(err) => panic!("Quantization failed, because: {err:?}"),
|
||||
};
|
||||
|
||||
let (palette, pixels) = res.remapped(&mut img).unwrap();
|
||||
|
||||
let file = std::fs::File::create(output_location).unwrap();
|
||||
let w = &mut BufWriter::new(file);
|
||||
|
||||
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
let mut flattened_palette = Vec::new();
|
||||
let mut alpha_palette = Vec::new();
|
||||
for px in palette {
|
||||
flattened_palette.push(px.r);
|
||||
flattened_palette.push(px.g);
|
||||
flattened_palette.push(px.b);
|
||||
alpha_palette.push(px.a);
|
||||
}
|
||||
encoder.set_palette(flattened_palette);
|
||||
encoder.set_trns(alpha_palette);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder.set_color(png::ColorType::Indexed);
|
||||
encoder.set_compression(png::Compression::Best);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
writer.write_image_data(&pixels).unwrap();
|
||||
writer.finish().unwrap();
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
use std::hash::Hasher;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use manganis_core::JsAssetOptions;
|
||||
use swc_common::errors::Emitter;
|
||||
use swc_common::errors::Handler;
|
||||
use swc_common::input::SourceFileInput;
|
||||
use swc_ecma_minifier::option::{ExtraOptions, MinifyOptions};
|
||||
use swc_ecma_parser::lexer::Lexer;
|
||||
use swc_ecma_parser::Parser;
|
||||
use swc_ecma_transforms_base::fixer::fixer;
|
||||
use swc_ecma_visit::VisitMutWith;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Error;
|
||||
use swc_bundler::{Bundler, Config, Load, ModuleData, ModuleRecord};
|
||||
use swc_common::{
|
||||
errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceMap, Span, GLOBALS,
|
||||
};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_codegen::text_writer::JsWriter;
|
||||
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
|
||||
use swc_ecma_parser::{parse_file_as_module, Syntax};
|
||||
|
||||
use crate::hash::hash_file_contents;
|
||||
|
||||
struct TracingEmitter;
|
||||
|
||||
impl Emitter for TracingEmitter {
|
||||
fn emit(&mut self, db: &mut swc_common::errors::DiagnosticBuilder<'_>) {
|
||||
match db.level {
|
||||
swc_common::errors::Level::Bug
|
||||
| swc_common::errors::Level::Fatal
|
||||
| swc_common::errors::Level::PhaseFatal
|
||||
| swc_common::errors::Level::Error => tracing::error!("{}", db.message()),
|
||||
swc_common::errors::Level::Warning
|
||||
| swc_common::errors::Level::FailureNote
|
||||
| swc_common::errors::Level::Cancelled => tracing::warn!("{}", db.message()),
|
||||
swc_common::errors::Level::Note | swc_common::errors::Level::Help => {
|
||||
tracing::trace!("{}", db.message())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a closure with the swc globals and handler set up
|
||||
fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
|
||||
let globals = Globals::new();
|
||||
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
|
||||
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
|
||||
GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
|
||||
}
|
||||
|
||||
fn bundle_js_to_writer(
|
||||
file: PathBuf,
|
||||
bundle: bool,
|
||||
minify: bool,
|
||||
write_to: &mut impl std::io::Write,
|
||||
) -> anyhow::Result<()> {
|
||||
inside_handler(|globals, cm| {
|
||||
bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_js_inside_handler(
|
||||
globals: &Globals,
|
||||
file: PathBuf,
|
||||
bundle: bool,
|
||||
cm: &Lrc<SourceMap>,
|
||||
) -> anyhow::Result<Module> {
|
||||
if bundle {
|
||||
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
|
||||
let mut bundler = Bundler::new(
|
||||
globals,
|
||||
cm.clone(),
|
||||
PathLoader { cm: cm.clone() },
|
||||
node_resolver,
|
||||
Config {
|
||||
require: true,
|
||||
..Default::default()
|
||||
},
|
||||
Box::new(Hook),
|
||||
);
|
||||
let mut entries = HashMap::default();
|
||||
entries.insert("main".to_string(), FileName::Real(file));
|
||||
|
||||
let mut bundles = bundler
|
||||
.bundle(entries)
|
||||
.context("failed to bundle javascript with swc")?;
|
||||
// Since we only inserted one entry, there should only be one bundle in the output
|
||||
let bundle = bundles
|
||||
.pop()
|
||||
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
|
||||
Ok(bundle.module)
|
||||
} else {
|
||||
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
|
||||
|
||||
let lexer = Lexer::new(
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
SourceFileInput::from(&*fm),
|
||||
None,
|
||||
);
|
||||
let mut parser = Parser::new_from(lexer);
|
||||
|
||||
parser.parse_module().map_err(|err| {
|
||||
HANDLER.with(|handler| {
|
||||
let mut error = err.into_diagnostic(handler);
|
||||
// swc errors panic on drop if you don't cancel them
|
||||
error.cancel();
|
||||
anyhow::anyhow!("{}", error.message())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_js_to_writer_inside_handler(
|
||||
globals: &Globals,
|
||||
cm: Lrc<SourceMap>,
|
||||
file: PathBuf,
|
||||
bundle: bool,
|
||||
minify: bool,
|
||||
write_to: &mut impl std::io::Write,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;
|
||||
|
||||
if minify {
|
||||
module = swc_ecma_minifier::optimize(
|
||||
std::mem::take(&mut module).into(),
|
||||
cm.clone(),
|
||||
None,
|
||||
None,
|
||||
&MinifyOptions {
|
||||
rename: true,
|
||||
compress: None,
|
||||
mangle: None,
|
||||
..Default::default()
|
||||
},
|
||||
&ExtraOptions {
|
||||
unresolved_mark: Mark::new(),
|
||||
top_level_mark: Mark::new(),
|
||||
mangle_name_cache: None,
|
||||
},
|
||||
)
|
||||
.expect_module();
|
||||
module.visit_mut_with(&mut fixer(None));
|
||||
}
|
||||
|
||||
let mut emitter = swc_ecma_codegen::Emitter {
|
||||
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
|
||||
cm: cm.clone(),
|
||||
comments: None,
|
||||
wr: Box::new(JsWriter::new(cm, "\n", write_to, None)),
|
||||
};
|
||||
|
||||
emitter.emit_module(&module)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct PathLoader {
|
||||
cm: Lrc<SourceMap>,
|
||||
}
|
||||
|
||||
impl Load for PathLoader {
|
||||
fn load(&self, file: &FileName) -> anyhow::Result<ModuleData> {
|
||||
let file = match file {
|
||||
FileName::Real(v) => v,
|
||||
_ => anyhow::bail!("Only real files are supported"),
|
||||
};
|
||||
|
||||
let fm = self.cm.load_file(file)?;
|
||||
|
||||
let module = HANDLER.with(|handler| {
|
||||
parse_file_as_module(
|
||||
&fm,
|
||||
Syntax::Es(Default::default()),
|
||||
Default::default(),
|
||||
None,
|
||||
&mut Vec::new(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
let mut error = err.into_diagnostic(handler);
|
||||
// swc errors panic on drop if you don't cancel them
|
||||
error.cancel();
|
||||
anyhow::anyhow!("{}", error.message())
|
||||
})
|
||||
.context("Failed to parse javascript")
|
||||
})?;
|
||||
|
||||
Ok(ModuleData {
|
||||
fm,
|
||||
module,
|
||||
helpers: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from https://github.com/swc-project/swc/blob/624680b7896cef9d8e30bd5ff910538298016974/bindings/binding_core_node/src/bundle.rs#L266-L302
|
||||
struct Hook;
|
||||
|
||||
impl swc_bundler::Hook for Hook {
|
||||
fn get_import_meta_props(
|
||||
&self,
|
||||
span: Span,
|
||||
module_record: &ModuleRecord,
|
||||
) -> Result<Vec<KeyValueProp>, Error> {
|
||||
let file_name = module_record.file_name.to_string();
|
||||
|
||||
Ok(vec![
|
||||
KeyValueProp {
|
||||
key: PropName::Ident(IdentName::new("url".into(), span)),
|
||||
value: Box::new(Expr::Lit(Lit::Str(Str {
|
||||
span,
|
||||
raw: None,
|
||||
value: file_name.into(),
|
||||
}))),
|
||||
},
|
||||
KeyValueProp {
|
||||
key: PropName::Ident(IdentName::new("main".into(), span)),
|
||||
value: Box::new(if module_record.is_entry {
|
||||
Expr::Member(MemberExpr {
|
||||
span,
|
||||
obj: Box::new(Expr::MetaProp(MetaPropExpr {
|
||||
span,
|
||||
kind: MetaPropKind::ImportMeta,
|
||||
})),
|
||||
prop: MemberProp::Ident(IdentName::new("main".into(), span)),
|
||||
})
|
||||
} else {
|
||||
Expr::Lit(Lit::Bool(Bool { span, value: false }))
|
||||
}),
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_js(
|
||||
js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
bundle: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
|
||||
if js_options.minified() {
|
||||
if let Err(err) = bundle_js_to_writer(source.to_path_buf(), bundle, true, &mut writer) {
|
||||
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let mut source_file = std::fs::File::open(source)?;
|
||||
std::io::copy(&mut source_file, &mut writer).with_context(|| {
|
||||
format!(
|
||||
"Failed to write js to output location: {}",
|
||||
output_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
|
||||
inside_handler(|globals, cm| {
|
||||
_ = resolve_js_inside_handler(globals, file, bundle, &cm)?;
|
||||
|
||||
for file in cm.files().iter() {
|
||||
let hash = file.src_hash;
|
||||
hasher.write(&hash.to_le_bytes());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn hash_js(
|
||||
js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
hasher: &mut impl Hasher,
|
||||
bundle: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if js_options.minified() {
|
||||
if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
|
||||
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
|
||||
hash_file_contents(source, hasher)?;
|
||||
}
|
||||
} else {
|
||||
hash_file_contents(source, hasher)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+23
-26
@@ -7,11 +7,10 @@ description = "CLI for building fullstack web, desktop, and mobile apps with a s
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["mobile", "gui", "cli", "dioxus", "wasm"]
|
||||
rust-version = "1.81.0"
|
||||
rust-version = "1.82.0"
|
||||
|
||||
[dependencies]
|
||||
dioxus-autofmt = { workspace = true }
|
||||
dioxus-check = { workspace = true }
|
||||
dioxus-rsx-rosetta = { workspace = true }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-rsx-hotreload = { workspace = true }
|
||||
@@ -20,8 +19,6 @@ dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-core-types = { workspace = true }
|
||||
dioxus-devtools-types = { workspace = true }
|
||||
dioxus-cli-config = { workspace = true }
|
||||
dioxus-cli-opt = { workspace = true }
|
||||
dioxus-fullstack = { workspace = true }
|
||||
dioxus-dx-wire-format = { workspace = true }
|
||||
wasm-split-cli = { workspace = true }
|
||||
depinfo = { workspace = true }
|
||||
@@ -33,10 +30,8 @@ bytes = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "cargo"] }
|
||||
convert_case = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
# swc uses serde internals that break with newer versions of serde. We should move off of swc eventually,
|
||||
# but pinning serde fixes the issue in the meantime
|
||||
serde = { version = "=1.0.221", features = ["derive"] }
|
||||
uuid = { workspace = true, features = ["v4", "v5", "v7"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
cargo_toml = { workspace = true, features = ["features"] }
|
||||
@@ -61,9 +56,10 @@ console = "0.16.0"
|
||||
ctrlc = "3.4.7"
|
||||
|
||||
axum = { workspace = true, default-features = true, features = ["ws"] }
|
||||
axum-server = { workspace = true, features = ["tls-rustls"] }
|
||||
axum-server = { workspace = true, features = ["tls-rustls-no-provider"] }
|
||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||
tower-http = { workspace = true, features = ["full"] }
|
||||
owo-colors = { workspace = true, features = ["supports-colors"] }
|
||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||
syn = { workspace = true, features = [
|
||||
"full",
|
||||
@@ -72,13 +68,12 @@ syn = { workspace = true, features = [
|
||||
"visit-mut",
|
||||
] }
|
||||
|
||||
headers = "0.4.1"
|
||||
walkdir = "2"
|
||||
dunce = { workspace = true }
|
||||
|
||||
# tools download
|
||||
dirs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls", "trust-dns", "json"] }
|
||||
reqwest = { workspace = true, default-features = false, features = ["rustls-tls", "json"] }
|
||||
tower = { workspace = true }
|
||||
git2 = "0.20.2"
|
||||
|
||||
@@ -95,9 +90,14 @@ toml_edit = "0.22.27"
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
# Assets
|
||||
lightningcss = { version = "1.0.0-alpha.67", features = ["browserslist", "into_owned"] }
|
||||
grass = "0.13.4"
|
||||
codemap = "0.1.3"
|
||||
mozjpeg = { version = "0.10.13", default-features = false, features = ["parallel"] }
|
||||
imagequant = "4.4.0"
|
||||
png = "0.17.16"
|
||||
brotli = "8.0.1"
|
||||
ignore = "0.4.23"
|
||||
env_logger = { workspace = true }
|
||||
const-serialize = { workspace = true, features = ["serde"] }
|
||||
const-serialize-07 = { package = "const-serialize", version = "=0.7.2", features = ["serde"] }
|
||||
|
||||
@@ -127,7 +127,6 @@ manganis = { workspace = true }
|
||||
manganis-core = { workspace = true }
|
||||
manganis-core-07 = { workspace = true }
|
||||
target-lexicon = { version = "0.13.2", features = ["serde", "serde_support"] }
|
||||
wasm-encoder = "0.235.0"
|
||||
|
||||
# Extracting data from an executable
|
||||
object = { workspace = true, features = ["all"] }
|
||||
@@ -136,26 +135,26 @@ itertools = { workspace = true }
|
||||
throbber-widgets-tui = "0.8.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
handlebars = "6.3.2"
|
||||
strum = { version = "0.27.1", features = ["derive"] }
|
||||
memmap = "0.7.0"
|
||||
walrus = { workspace = true, features = ["parallel"] }
|
||||
wasmparser = { workspace = true }
|
||||
|
||||
tauri-utils = { workspace = true }
|
||||
tauri-macos-sign = { workspace = true }
|
||||
tauri-bundler = { workspace = true }
|
||||
# bundler dependencies
|
||||
icns = { workspace = true }
|
||||
image = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
md5 = { workspace = true }
|
||||
rpm = { workspace = true }
|
||||
include_dir = "0.7.4"
|
||||
flate2 = "1.1.2"
|
||||
tar = "0.4.44"
|
||||
local-ip-address = "0.6.5"
|
||||
dircpy = "0.3.19"
|
||||
plist = "1.7.4"
|
||||
memoize = "0.5.1"
|
||||
backtrace = "0.3.74"
|
||||
ar = "0.9.0"
|
||||
wasm-bindgen-externref-xform = "0.2.100"
|
||||
pdb = "0.8.0"
|
||||
self_update = { version = "0.42.0", features = [
|
||||
self_update = { version = "0.42.0", default-features = false, features = [
|
||||
"archive-tar",
|
||||
"archive-zip",
|
||||
"compression-flate2",
|
||||
@@ -163,7 +162,6 @@ self_update = { version = "0.42.0", features = [
|
||||
] }
|
||||
self-replace = "1.5.0"
|
||||
cargo-config2 = { workspace = true }
|
||||
posthog-rs = "0.3.5"
|
||||
serde_json5 = "0.2.1"
|
||||
fs2 = "0.4.3"
|
||||
sentry-backtrace = "0.42.0"
|
||||
@@ -175,9 +173,6 @@ home = "=0.5.11"
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
git2 = { version = "0.20.2", features = ["vendored-libgit2"] }
|
||||
|
||||
[build-dependencies]
|
||||
built = { version = "0.8.0", features = ["git2"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
|
||||
@@ -190,6 +185,8 @@ name = "dx"
|
||||
|
||||
[dev-dependencies]
|
||||
escargot = "0.5"
|
||||
indoc = "2.0.6"
|
||||
pretty_assertions = { workspace = true }
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+40
-1
@@ -1,3 +1,42 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
built::write_built_file().expect("Failed to acquire build-time information");
|
||||
println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA");
|
||||
println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA_SHORT");
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs");
|
||||
|
||||
let full_hash = std::env::var("DIOXUS_CLI_GIT_SHA")
|
||||
.ok()
|
||||
.or_else(resolve_git_commit_hash);
|
||||
|
||||
if let Some(full_hash) = full_hash {
|
||||
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}");
|
||||
|
||||
let short_hash = std::env::var("DIOXUS_CLI_GIT_SHA_SHORT")
|
||||
.ok()
|
||||
.unwrap_or_else(|| full_hash.chars().take(7).collect());
|
||||
|
||||
println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}");
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_git_commit_hash() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let hash = String::from_utf8(output.stdout).ok()?;
|
||||
let hash = hash.trim().to_string();
|
||||
|
||||
if hash.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hash)
|
||||
}
|
||||
}
|
||||
@@ -3266,14 +3266,14 @@
|
||||
]
|
||||
},
|
||||
"upgrade_code": {
|
||||
"description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace. You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.\n\nIt is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code whenever you want to change your product name.",
|
||||
"description": "A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**, otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.\n\nBy default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.\n\nIt is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code whenever you want to change your product name.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
|
||||
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\nBecause a valid version is required for MSI installer, it will be derived from the package version if this field is not set.\n\nThe first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255. The third and fourth fields have a maximum value of 65,535.\n\nSee <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
||||
@@ -33,10 +33,10 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::opt::AssetManifest;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use const_serialize::{deserialize_const, serialize_const, ConstVec};
|
||||
use dioxus_cli_opt::AssetManifest;
|
||||
use manganis::{AssetOptions, AssetVariant, BundledAsset, ImageFormat, ImageSize};
|
||||
use manganis_core::{AndroidArtifactMetadata, SwiftPackageMetadata, SymbolData};
|
||||
use object::{File, Object, ObjectSection, ObjectSymbol, ReadCache, ReadRef, Section, Symbol};
|
||||
@@ -811,7 +811,7 @@ pub(crate) async fn extract_symbols_from_file(
|
||||
// Add the hash to each asset in parallel
|
||||
assets
|
||||
.par_iter_mut()
|
||||
.for_each(dioxus_cli_opt::add_hash_to_asset);
|
||||
.for_each(crate::opt::add_hash_to_asset);
|
||||
|
||||
// Write back only assets to the binary file (permissions are not modified)
|
||||
for entry in write_entries {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::opt::process_file_to;
|
||||
use crate::{
|
||||
build::cache::ObjectCache, serve::WebServer, verbosity_or_default, BuildArtifacts,
|
||||
BuildRequest, BuildStage, BuilderUpdate, BundleFormat, ProgressRx, ProgressTx, Result,
|
||||
RustcArgs, StructuredOutput,
|
||||
};
|
||||
use anyhow::{bail, Context, Error};
|
||||
use dioxus_cli_opt::process_file_to;
|
||||
use futures_util::{future::OptionFuture, pin_mut, FutureExt};
|
||||
use itertools::Itertools;
|
||||
use std::{
|
||||
@@ -475,9 +475,12 @@ impl AppBuilder {
|
||||
current,
|
||||
total,
|
||||
krate,
|
||||
fresh,
|
||||
..
|
||||
} => {
|
||||
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
|
||||
if !fresh {
|
||||
tracing::info!("Compiled [{current:>3}/{total}]: {krate}");
|
||||
}
|
||||
}
|
||||
BuildStage::RunningBindgen => tracing::info!("Running wasm-bindgen..."),
|
||||
BuildStage::CopyingAssets {
|
||||
@@ -509,7 +512,7 @@ impl AppBuilder {
|
||||
}
|
||||
BuilderUpdate::BuildFailed { err } => {
|
||||
// Flush remaining compiler messages
|
||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||
while let Ok(msg) = self.rx.try_recv() {
|
||||
if let BuilderUpdate::CompilerMessage { message } = msg {
|
||||
tracing::info!(json = %StructuredOutput::RustcOutput { message: message.clone() }, %message);
|
||||
}
|
||||
@@ -760,7 +763,8 @@ impl AppBuilder {
|
||||
let to = asset_dir.join(bundled.bundled_path());
|
||||
|
||||
tracing::debug!("Copying asset from patch: {}", from.display());
|
||||
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
|
||||
let esbuild = crate::esbuild::Esbuild::path_if_installed();
|
||||
if let Err(e) = process_file_to(bundled.options(), &from, &to, esbuild.as_deref()) {
|
||||
tracing::error!("Failed to copy asset: {e}");
|
||||
continue;
|
||||
}
|
||||
@@ -869,7 +873,8 @@ impl AppBuilder {
|
||||
// the asset would be in a new location because the contents and hash have changed. Since we are
|
||||
// hotreloading, we need to use the old asset location it was originally written to.
|
||||
let options = *resource.options();
|
||||
let res = process_file_to(&options, &changed_file, &output_path);
|
||||
let esbuild = crate::esbuild::Esbuild::path_if_installed();
|
||||
let res = process_file_to(&options, &changed_file, &output_path, esbuild.as_deref());
|
||||
let bundled_name = PathBuf::from(resource.bundled_path());
|
||||
if let Err(e) = res {
|
||||
tracing::debug!("Failed to hotreload asset {e}");
|
||||
|
||||
@@ -137,12 +137,19 @@ impl BuildContext {
|
||||
tracing::trace!(dx_src = ?TraceSrc::Cargo, "{line}");
|
||||
}
|
||||
|
||||
pub(crate) fn status_build_progress(&self, count: usize, total: usize, name: String) {
|
||||
pub(crate) fn status_build_progress(
|
||||
&self,
|
||||
count: usize,
|
||||
total: usize,
|
||||
name: String,
|
||||
fresh: bool,
|
||||
) {
|
||||
_ = self.tx.unbounded_send(BuilderUpdate::Progress {
|
||||
stage: BuildStage::Compiling {
|
||||
current: count,
|
||||
total,
|
||||
krate: name,
|
||||
fresh,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -144,9 +144,9 @@ pub async fn create_framework_bundle(
|
||||
std::fs::write(resources_dir.join("Info.plist"), info_plist)?;
|
||||
|
||||
// Create symbolic links (required for macOS framework structure)
|
||||
let versions_dir = framework_dir.join("Versions");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let versions_dir = framework_dir.join("Versions");
|
||||
std::os::unix::fs::symlink("A", versions_dir.join("Current"))?;
|
||||
std::os::unix::fs::symlink(
|
||||
format!("Versions/Current/{}", framework_name),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! Eventually, we might want to expand this to include more metadata about the build, including
|
||||
//! build time, target platform, etc.
|
||||
|
||||
use dioxus_cli_opt::AssetManifest;
|
||||
use crate::opt::AssetManifest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
|
||||
@@ -1420,8 +1420,25 @@ fn name_is_bindgen_symbol(name: &str) -> bool {
|
||||
|| name.contains("__wbindgen_externref")
|
||||
|| name.contains("wasm_bindgen8describe6inform")
|
||||
|| name.contains("wasm_bindgen..describe..WasmDescribe")
|
||||
|| name.contains("wasm_bindgen..closure..WasmClosure$GT$8describe")
|
||||
|| name.contains("wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe")
|
||||
|| (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe"))
|
||||
|| (name.contains("wasm_bindgen7closure16Closure") && name.contains("describe"))
|
||||
|| (name.contains("wasm_bindgen7convert8closures") && name.contains("describe_invoke"))
|
||||
}
|
||||
|
||||
// Test for bindgen symbols. As we find more bad symbols, add them here
|
||||
#[test]
|
||||
fn bindgen_symbol_catch() {
|
||||
let symbol = "_ZN12wasm_bindgen7convert8closures1_142_$LT$impl$u20$wasm_bindgen..closure..WasmClosure$u20$for$u20$dyn$u20$core..ops..function..FnMut$LT$$LP$$RP$$GT$$u2b$Output$u20$$u3d$$u20$R$GT$15describe_invoke17h4373f8b6570333dcE";
|
||||
assert!(name_is_bindgen_symbol(symbol));
|
||||
|
||||
// matches_legacy_wasm_bindgen_closure_describe_symbols
|
||||
let symbol = "_ZN12wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe17h1234567890abcdefE";
|
||||
assert!(name_is_bindgen_symbol(symbol));
|
||||
|
||||
// does_not_match_saved_runtime_exports
|
||||
assert!(!name_is_bindgen_symbol("__wbindgen_malloc"));
|
||||
assert!(!name_is_bindgen_symbol("__wbindgen_realloc"));
|
||||
assert!(!name_is_bindgen_symbol("__wbindgen_free"));
|
||||
}
|
||||
|
||||
/// Manually parse the data section from a wasm module
|
||||
|
||||
@@ -320,6 +320,7 @@
|
||||
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
||||
|
||||
use super::HotpatchModuleCache;
|
||||
use crate::opt::{process_file_to, AssetManifest};
|
||||
use crate::{
|
||||
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
||||
LinkAction, LinkerFlavor, ObjectCache, Platform, Renderer, Result, RustcArgs, TargetArgs,
|
||||
@@ -331,7 +332,6 @@ use cargo_toml::{Profile, Profiles, StripSetting};
|
||||
use depinfo::RustcDepInfo;
|
||||
use dioxus_cli_config::{format_base_path_meta_element, PRODUCT_NAME_ENV};
|
||||
use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
|
||||
use dioxus_cli_opt::{process_file_to, AssetManifest};
|
||||
use itertools::Itertools;
|
||||
use krates::{cm::TargetKind, NodeId};
|
||||
use manganis::{AssetOptions, BundledAsset, SwiftPackageMetadata};
|
||||
@@ -1306,7 +1306,12 @@ impl BuildRequest {
|
||||
}
|
||||
Message::CompilerArtifact(artifact) => {
|
||||
units_compiled += 1;
|
||||
ctx.status_build_progress(units_compiled, crate_count, artifact.target.name);
|
||||
ctx.status_build_progress(
|
||||
units_compiled,
|
||||
crate_count,
|
||||
artifact.target.name,
|
||||
artifact.fresh,
|
||||
);
|
||||
output_location = artifact.executable.map(Into::into);
|
||||
}
|
||||
// todo: this can occasionally swallow errors, so we should figure out what exactly is going wrong
|
||||
@@ -2280,6 +2285,7 @@ impl BuildRequest {
|
||||
// Parallel Copy over the assets and keep track of progress with an atomic counter
|
||||
let progress = ctx.tx.clone();
|
||||
let ws_dir = self.workspace_dir();
|
||||
let esbuild_path = crate::esbuild::Esbuild::path_if_installed();
|
||||
|
||||
// Optimizing assets is expensive and blocking, so we do it in a tokio spawn blocking task
|
||||
tokio::task::spawn_blocking(move || {
|
||||
@@ -2292,7 +2298,7 @@ impl BuildRequest {
|
||||
"Starting asset copy {processing}/{asset_count} from {from_:?}"
|
||||
);
|
||||
|
||||
let res = process_file_to(options, from, to);
|
||||
let res = process_file_to(options, from, to, esbuild_path.as_deref());
|
||||
if let Err(err) = res.as_ref() {
|
||||
tracing::error!("Failed to copy asset {from:?}: {err}");
|
||||
}
|
||||
@@ -5524,6 +5530,25 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
.join("app-debug.apk")
|
||||
}
|
||||
|
||||
pub(crate) fn release_apk_path(&self) -> PathBuf {
|
||||
self.root_dir()
|
||||
.join("app")
|
||||
.join("build")
|
||||
.join("outputs")
|
||||
.join("apk")
|
||||
.join("release")
|
||||
.join("app-release.apk")
|
||||
}
|
||||
|
||||
pub(crate) fn android_apk_path(&self) -> PathBuf {
|
||||
let assembled_release = self.release && self.config.bundle.android.is_some();
|
||||
if assembled_release {
|
||||
self.release_apk_path()
|
||||
} else {
|
||||
self.debug_apk_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// We only really currently care about:
|
||||
///
|
||||
/// - app dir (.app, .exe, .apk, etc)
|
||||
@@ -5683,6 +5708,9 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
|
||||
self.verify_toolchain_installed().await?;
|
||||
|
||||
// esbuild is used for JS asset processing on all platforms
|
||||
let _esbuild_path = crate::esbuild::Esbuild::get_or_install().await?;
|
||||
|
||||
match self.bundle {
|
||||
BundleFormat::Web => self.verify_web_tooling().await?,
|
||||
BundleFormat::Ios => self.verify_ios_tooling().await?,
|
||||
@@ -5970,7 +5998,7 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with(&prefix) && name.ends_with(".rlib") {
|
||||
let mtime = entry.metadata().ok()?.modified().ok()?;
|
||||
if best.as_ref().map_or(true, |(_, t)| mtime > *t) {
|
||||
if best.as_ref().is_none_or(|(_, t)| mtime > *t) {
|
||||
best = Some((entry.path(), mtime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
use crate::{
|
||||
config::BundleConfig, CustomSignCommandSettings, DebianSettings, MacOsSettings,
|
||||
NSISInstallerMode, NsisSettings, PackageType, WebviewInstallMode, WindowsSettings, WixSettings,
|
||||
};
|
||||
|
||||
impl From<NsisSettings> for tauri_bundler::NsisSettings {
|
||||
fn from(val: NsisSettings) -> Self {
|
||||
tauri_bundler::NsisSettings {
|
||||
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,
|
||||
custom_language_files: None,
|
||||
template: None,
|
||||
compression: tauri_utils::config::NsisCompression::None,
|
||||
start_menu_folder: val.start_menu_folder,
|
||||
installer_hooks: val.installer_hooks,
|
||||
minimum_webview2_version: val.minimum_webview2_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DebianSettings> for tauri_bundler::DebianSettings {
|
||||
fn from(val: DebianSettings) -> Self {
|
||||
tauri_bundler::DebianSettings {
|
||||
depends: val.depends,
|
||||
files: val.files,
|
||||
desktop_template: val.desktop_template,
|
||||
provides: val.provides,
|
||||
conflicts: val.conflicts,
|
||||
replaces: val.replaces,
|
||||
section: val.section,
|
||||
priority: val.priority,
|
||||
changelog: val.changelog,
|
||||
pre_install_script: val.pre_install_script,
|
||||
post_install_script: val.post_install_script,
|
||||
pre_remove_script: val.pre_remove_script,
|
||||
post_remove_script: val.post_remove_script,
|
||||
recommends: val.recommends,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
version: val.version,
|
||||
upgrade_code: val.upgrade_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
files: val.files,
|
||||
hardened_runtime: val.hardened_runtime,
|
||||
bundle_version: val.bundle_version,
|
||||
bundle_name: val.bundle_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
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),
|
||||
webview_install_mode: val.webview_install_mode.into(),
|
||||
allow_downgrades: val.allow_downgrades,
|
||||
nsis: val.nsis.map(Into::into),
|
||||
sign_command: val.sign_command.map(Into::into),
|
||||
|
||||
icon_path: val.icon_path.unwrap_or("./icons/icon.ico".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageType> for tauri_bundler::PackageType {
|
||||
fn from(value: PackageType) -> Self {
|
||||
match value {
|
||||
PackageType::MacOsBundle => Self::MacOsBundle,
|
||||
PackageType::IosBundle => Self::IosBundle,
|
||||
PackageType::WindowsMsi => Self::WindowsMsi,
|
||||
PackageType::Deb => Self::Deb,
|
||||
PackageType::Rpm => Self::Rpm,
|
||||
PackageType::AppImage => Self::AppImage,
|
||||
PackageType::Dmg => Self::Dmg,
|
||||
PackageType::Updater => Self::Updater,
|
||||
PackageType::Nsis => Self::Nsis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 From<CustomSignCommandSettings> for tauri_bundler::CustomSignCommandSettings {
|
||||
fn from(val: CustomSignCommandSettings) -> Self {
|
||||
tauri_bundler::CustomSignCommandSettings {
|
||||
cmd: val.cmd,
|
||||
args: val.args,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use super::{copy_dir_recursive, zip_dir_recursive, Arch, Bundle, BundleContext};
|
||||
use crate::PackageType;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::process::Command;
|
||||
|
||||
pub(crate) struct IosBundled {
|
||||
pub ipa: Vec<PathBuf>,
|
||||
#[allow(dead_code)]
|
||||
pub app: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl BundleContext<'_> {
|
||||
/// Return the already-assembled iOS `.app` bundle produced by the build pipeline.
|
||||
pub(crate) async fn bundle_ios_app(&self) -> Result<Vec<PathBuf>> {
|
||||
let app_path = self.build.root_dir();
|
||||
if !app_path.exists() {
|
||||
bail!(
|
||||
"iOS app bundle not found at {}. Ensure the iOS build completed successfully.",
|
||||
app_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(vec![app_path])
|
||||
}
|
||||
|
||||
/// Package a signed iOS `.app` into an App Store distributable `.ipa`.
|
||||
pub(crate) async fn bundle_ios_ipa(&self, bundles: &[Bundle]) -> Result<IosBundled> {
|
||||
validate_ios_ipa_target(&self.target(), self.binary_arch())?;
|
||||
|
||||
let (app_paths, synthesized_app_paths) = if let Some(app_bundle) = bundles
|
||||
.iter()
|
||||
.find(|bundle| bundle.package_type == PackageType::IosApp)
|
||||
{
|
||||
(app_bundle.bundle_paths.clone(), Vec::new())
|
||||
} else {
|
||||
let paths = self.bundle_ios_app().await?;
|
||||
(paths.clone(), paths)
|
||||
};
|
||||
|
||||
if app_paths.is_empty() {
|
||||
bail!("No iOS .app bundle found to package into an .ipa");
|
||||
}
|
||||
|
||||
let app_path = &app_paths[0];
|
||||
if !app_path.exists() {
|
||||
bail!(
|
||||
"iOS .app bundle does not exist at expected path: {}",
|
||||
app_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
verify_codesigned_app(app_path).await?;
|
||||
|
||||
let output_dir = self.project_out_directory().join("bundle").join("ipa");
|
||||
std::fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let ipa_name = format!(
|
||||
"{}_{}_{}.ipa",
|
||||
self.product_name(),
|
||||
self.version_string(),
|
||||
self.binary_arch()
|
||||
);
|
||||
let output_path = output_dir.join(ipa_name);
|
||||
|
||||
tracing::info!("Creating iOS IPA at {}", output_path.display());
|
||||
|
||||
let staging_dir =
|
||||
tempfile::tempdir().context("Failed to create temp dir for IPA staging")?;
|
||||
let payload_dir = staging_dir.path().join("Payload");
|
||||
std::fs::create_dir_all(&payload_dir)?;
|
||||
|
||||
let staged_app = payload_dir.join(
|
||||
app_path
|
||||
.file_name()
|
||||
.context("iOS .app bundle is missing a file name")?,
|
||||
);
|
||||
copy_dir_recursive(app_path, &staged_app)?;
|
||||
|
||||
if output_path.exists() {
|
||||
std::fs::remove_file(&output_path)?;
|
||||
}
|
||||
zip_dir_recursive(staging_dir.path(), &output_path)?;
|
||||
|
||||
tracing::info!("IPA created at {}", output_path.display());
|
||||
|
||||
Ok(IosBundled {
|
||||
ipa: vec![output_path],
|
||||
app: synthesized_app_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_ios_ipa_target(target: &str, arch: Arch) -> Result<()> {
|
||||
let is_device = arch == Arch::AArch64 && !target.contains("sim");
|
||||
if !is_device {
|
||||
bail!(
|
||||
"IPA packaging requires a physical-device iOS target. Resolved target was `{target}`."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn verify_codesigned_app(app_path: &Path) -> Result<()> {
|
||||
verify_codesigned_app_with(Path::new("codesign"), app_path).await
|
||||
}
|
||||
|
||||
async fn verify_codesigned_app_with(codesign: &Path, app_path: &Path) -> Result<()> {
|
||||
let output = Command::new(codesign)
|
||||
.args(["--verify", "--deep", "--strict"])
|
||||
.arg(app_path)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("Failed to run `{}`", codesign.display()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"iOS .app bundle must be codesigned before creating an .ipa: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,981 @@
|
||||
use crate::bundler::{copy_dir_recursive, AppCategory, Bundle, BundleContext};
|
||||
use crate::{MacOsSettings, PackageType};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufWriter;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command as StdCommand, Stdio};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::process::Command;
|
||||
|
||||
impl BundleContext<'_> {
|
||||
/// Create the final macOS `.app` bundle and apply Apple-specific metadata.
|
||||
///
|
||||
/// This is the primary macOS bundling routine. It takes the executable produced by
|
||||
/// the Dioxus build pipeline and assembles the canonical app bundle structure in
|
||||
/// `project_out_directory()/macos/<BundleName>.app`.
|
||||
///
|
||||
/// The method performs the following steps:
|
||||
/// 1. Resolve macOS settings and choose the bundle/display name.
|
||||
/// 2. Remove any existing output bundle to keep the result deterministic.
|
||||
/// 3. Create the `Contents/`, `Contents/MacOS`, `Contents/Resources`, and
|
||||
/// optionally `Contents/Frameworks` directories.
|
||||
/// 4. Copy the main executable into `Contents/MacOS/` and mark it executable.
|
||||
/// 5. Build or copy an `.icns` file into `Contents/Resources/`.
|
||||
/// 6. Copy configured resources and sidecar binaries into the bundle.
|
||||
/// 7. Copy configured frameworks and arbitrary extra files from
|
||||
/// `MacOsSettings::files`.
|
||||
/// 8. Generate an `Info.plist` from bundle metadata, unless a custom plist path
|
||||
/// was configured and exists.
|
||||
/// 9. Write the legacy `PkgInfo` file.
|
||||
/// 10. If signing is configured, sign frameworks first, then the main binary, and
|
||||
/// finally the enclosing `.app`.
|
||||
/// 11. If notarization credentials are available, zip the `.app`, submit it to
|
||||
/// Apple's notary service, staple the result, and delete the temporary zip.
|
||||
///
|
||||
/// Contributor notes:
|
||||
/// - The build system guarantees the executable exists, but this method is
|
||||
/// responsible for the final `.app` on-disk shape.
|
||||
/// - Framework paths may be absolute or relative to the crate directory. Missing
|
||||
/// paths are tolerated because some entries may refer to system frameworks.
|
||||
/// - Lack of signing or notarization credentials is not fatal; an unsigned `.app`
|
||||
/// is still returned as a valid bundle artifact.
|
||||
pub(crate) async fn bundle_macos_app(&self) -> Result<Vec<PathBuf>> {
|
||||
let product_name = self.product_name();
|
||||
let macos_settings = self.macos();
|
||||
|
||||
let bundle_name = macos_settings
|
||||
.bundle_name
|
||||
.as_deref()
|
||||
.unwrap_or(&product_name);
|
||||
|
||||
let output_dir = self.project_out_directory().join("macos");
|
||||
let app_dir = output_dir.join(format!("{bundle_name}.app"));
|
||||
|
||||
tracing::info!("Creating macOS .app bundle at {}", app_dir.display());
|
||||
|
||||
if app_dir.exists() {
|
||||
fs::remove_dir_all(&app_dir)
|
||||
.with_context(|| format!("Failed to clean existing .app: {}", app_dir.display()))?;
|
||||
}
|
||||
|
||||
let contents_dir = app_dir.join("Contents");
|
||||
let macos_dir = contents_dir.join("MacOS");
|
||||
let resources_dir = contents_dir.join("Resources");
|
||||
let frameworks_dir = contents_dir.join("Frameworks");
|
||||
|
||||
fs::create_dir_all(&macos_dir)?;
|
||||
fs::create_dir_all(&resources_dir)?;
|
||||
|
||||
let binary_src = self.main_binary_path();
|
||||
let binary_dest = macos_dir.join(self.main_binary_name());
|
||||
tracing::debug!(
|
||||
"Copying binary: {} -> {}",
|
||||
binary_src.display(),
|
||||
binary_dest.display()
|
||||
);
|
||||
fs::copy(&binary_src, &binary_dest)
|
||||
.with_context(|| format!("Failed to copy main binary to {}", binary_dest.display()))?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&binary_dest, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
|
||||
let icon_path = self.create_macos_icns_file(&resources_dir)?;
|
||||
let icon_filename = icon_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().to_string());
|
||||
|
||||
self.copy_resources(&resources_dir)?;
|
||||
let _external_bins = self.copy_external_binaries(&macos_dir)?;
|
||||
|
||||
if let Some(frameworks) = &macos_settings.frameworks {
|
||||
if !frameworks.is_empty() {
|
||||
fs::create_dir_all(&frameworks_dir)?;
|
||||
for framework in frameworks {
|
||||
let framework_path = PathBuf::from(framework);
|
||||
if !framework_path.exists() {
|
||||
let resolved = self.crate_dir().join(&framework_path);
|
||||
if resolved.exists() {
|
||||
copy_framework(&resolved, &frameworks_dir)?;
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Framework not found as file, assuming system framework: {}",
|
||||
framework
|
||||
);
|
||||
}
|
||||
} else {
|
||||
copy_framework(&framework_path, &frameworks_dir)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (bundle_path, source_path) in &macos_settings.files {
|
||||
let dest = contents_dir.join(bundle_path);
|
||||
let src = if source_path.is_relative() {
|
||||
self.crate_dir().join(source_path)
|
||||
} else {
|
||||
source_path.clone()
|
||||
};
|
||||
|
||||
if !src.exists() {
|
||||
tracing::warn!(
|
||||
"Custom file not found: {} (for bundle path {})",
|
||||
src.display(),
|
||||
bundle_path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
if src.is_dir() {
|
||||
copy_dir_recursive(&src, &dest)?;
|
||||
} else {
|
||||
fs::copy(&src, &dest).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy custom file {} -> {}",
|
||||
src.display(),
|
||||
dest.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let info_plist = self.create_macos_info_plist(&macos_settings, icon_filename.as_deref())?;
|
||||
let plist_path = contents_dir.join("Info.plist");
|
||||
|
||||
if let Some(custom_plist_path) = &macos_settings.info_plist_path {
|
||||
let custom_path = if custom_plist_path.is_relative() {
|
||||
self.crate_dir().join(custom_plist_path)
|
||||
} else {
|
||||
custom_plist_path.clone()
|
||||
};
|
||||
if custom_path.exists() {
|
||||
tracing::info!("Using custom Info.plist from {}", custom_path.display());
|
||||
fs::copy(&custom_path, &plist_path)?;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Custom Info.plist not found at {}, generating default",
|
||||
custom_path.display()
|
||||
);
|
||||
write_plist(&info_plist, &plist_path)?;
|
||||
}
|
||||
} else {
|
||||
write_plist(&info_plist, &plist_path)?;
|
||||
}
|
||||
|
||||
fs::write(contents_dir.join("PkgInfo"), "APPL????")?;
|
||||
|
||||
let signing_identity = setup_keychain(macos_settings.signing_identity.as_deref()).await?;
|
||||
|
||||
if let Some(identity) = &signing_identity {
|
||||
tracing::info!("Signing .app bundle with identity: {}", identity.identity);
|
||||
|
||||
let mut sign_targets = Vec::new();
|
||||
|
||||
if frameworks_dir.exists() {
|
||||
for entry in fs::read_dir(&frameworks_dir)? {
|
||||
let entry = entry?;
|
||||
sign_targets.push(SignTarget { path: entry.path() });
|
||||
}
|
||||
}
|
||||
|
||||
sign_targets.push(SignTarget {
|
||||
path: binary_dest.clone(),
|
||||
});
|
||||
sign_targets.push(SignTarget {
|
||||
path: app_dir.clone(),
|
||||
});
|
||||
|
||||
sign_paths(identity, sign_targets, &macos_settings).await?;
|
||||
|
||||
let should_notarize =
|
||||
std::env::var("APPLE_ID").is_ok() || std::env::var("APPLE_API_KEY").is_ok();
|
||||
|
||||
if should_notarize {
|
||||
let zip_path = output_dir.join(format!("{bundle_name}.zip"));
|
||||
tracing::info!("Creating zip for notarization: {}", zip_path.display());
|
||||
|
||||
let status = Command::new("ditto")
|
||||
.args([
|
||||
"-c",
|
||||
"-k",
|
||||
"--sequesterRsrc",
|
||||
"--keepParent",
|
||||
&app_dir.display().to_string(),
|
||||
&zip_path.display().to_string(),
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to run ditto for zip creation")?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("ditto failed to create zip for notarization");
|
||||
}
|
||||
|
||||
match notarize(&zip_path).await {
|
||||
Ok(()) => {
|
||||
let _ = fs::remove_file(&zip_path);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = fs::remove_file(&zip_path);
|
||||
return Err(e.context("Notarization failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("No signing identity found; skipping code signing");
|
||||
}
|
||||
|
||||
Ok(vec![app_dir])
|
||||
}
|
||||
|
||||
/// Package the macOS application bundle into a distributable `.dmg` disk image.
|
||||
///
|
||||
/// DMG generation depends on having a complete `.app` bundle first. If the
|
||||
/// current bundling pass already produced one, this method reuses it. Otherwise it
|
||||
/// calls [`BundleContext::bundle_macos_app`] internally and returns both the
|
||||
/// resulting `.dmg` and the intermediate `.app` paths so the higher-level
|
||||
/// orchestrator can decide whether the `.app` should be preserved or cleaned up.
|
||||
///
|
||||
/// The process is:
|
||||
/// 1. Resolve the `.app` input, either from `bundles` or by building it now.
|
||||
/// 2. Create a temporary staging directory containing the `.app` and an
|
||||
/// `Applications` symlink for drag-and-drop installation.
|
||||
/// 3. Invoke `hdiutil create -format UDZO` to build a compressed, read-only disk
|
||||
/// image in `project_out_directory()/macos`.
|
||||
/// 4. Optionally sign the generated `.dmg`.
|
||||
/// 5. Optionally notarize and staple the `.dmg` if Apple credentials are present.
|
||||
///
|
||||
/// Only the final `.dmg` and, when synthesized as a prerequisite, the `.app` are
|
||||
/// considered outputs. The temporary DMG staging directory is always discarded.
|
||||
pub(crate) async fn bundle_macos_dmg(&self, bundles: &[Bundle]) -> Result<DmgBundled> {
|
||||
let product_name = self.product_name();
|
||||
let macos_settings = self.macos();
|
||||
|
||||
let bundle_name = macos_settings
|
||||
.bundle_name
|
||||
.as_deref()
|
||||
.unwrap_or(&product_name);
|
||||
|
||||
let (app_paths, app_bundle_paths) = if let Some(app_bundle) = bundles
|
||||
.iter()
|
||||
.find(|b| b.package_type == PackageType::MacOsBundle)
|
||||
{
|
||||
(app_bundle.bundle_paths.clone(), Vec::new())
|
||||
} else {
|
||||
let paths = self.bundle_macos_app().await?;
|
||||
(paths.clone(), paths)
|
||||
};
|
||||
|
||||
if app_paths.is_empty() {
|
||||
bail!("No .app bundle found to package into a DMG");
|
||||
}
|
||||
|
||||
let app_path = &app_paths[0];
|
||||
if !app_path.exists() {
|
||||
bail!(
|
||||
".app bundle does not exist at expected path: {}",
|
||||
app_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let output_dir = self.project_out_directory().join("macos");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let dmg_filename = format!(
|
||||
"{}_{}_{}",
|
||||
bundle_name,
|
||||
self.version_string(),
|
||||
self.binary_arch()
|
||||
);
|
||||
let dmg_path = output_dir.join(format!("{dmg_filename}.dmg"));
|
||||
|
||||
tracing::info!("Creating DMG at {}", dmg_path.display());
|
||||
|
||||
let staging_dir =
|
||||
tempfile::tempdir().context("Failed to create temp dir for DMG staging")?;
|
||||
let staging_path = staging_dir.path();
|
||||
|
||||
let staged_app = staging_path.join(app_path.file_name().unwrap());
|
||||
copy_dir_recursive(app_path, &staged_app)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink("/Applications", staging_path.join("Applications"))
|
||||
.context("Failed to create /Applications symlink in DMG staging")?;
|
||||
}
|
||||
|
||||
if dmg_path.exists() {
|
||||
fs::remove_file(&dmg_path)?;
|
||||
}
|
||||
|
||||
let status = Command::new("hdiutil")
|
||||
.args([
|
||||
"create",
|
||||
"-volname",
|
||||
bundle_name,
|
||||
"-srcfolder",
|
||||
&staging_path.display().to_string(),
|
||||
"-ov",
|
||||
"-format",
|
||||
"UDZO",
|
||||
&dmg_path.display().to_string(),
|
||||
])
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to execute `hdiutil create`")?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("`hdiutil create` failed with exit code: {status}");
|
||||
}
|
||||
|
||||
tracing::info!("DMG created at {}", dmg_path.display());
|
||||
|
||||
let signing_identity = setup_keychain(macos_settings.signing_identity.as_deref()).await?;
|
||||
if let Some(identity) = &signing_identity {
|
||||
tracing::info!("Signing DMG with identity: {}", identity.identity);
|
||||
sign_paths(
|
||||
identity,
|
||||
vec![SignTarget {
|
||||
path: dmg_path.clone(),
|
||||
}],
|
||||
&macos_settings,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let should_notarize =
|
||||
std::env::var("APPLE_ID").is_ok() || std::env::var("APPLE_API_KEY").is_ok();
|
||||
|
||||
if should_notarize {
|
||||
notarize(&dmg_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(DmgBundled {
|
||||
dmg: vec![dmg_path],
|
||||
app: app_bundle_paths,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an ICNS file from the icon files configured in the bundle context.
|
||||
fn create_macos_icns_file(&self, out_dir: &Path) -> Result<Option<PathBuf>> {
|
||||
let icon_paths = self.icon_files()?;
|
||||
if icon_paths.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let dest_path = out_dir.join(format!("{}.icns", self.product_name()));
|
||||
|
||||
for icon_path in &icon_paths {
|
||||
if icon_path
|
||||
.extension()
|
||||
.map(|ext| ext.eq_ignore_ascii_case("icns"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
tracing::info!("Copying existing .icns file: {}", icon_path.display());
|
||||
std::fs::copy(icon_path, &dest_path).with_context(|| {
|
||||
format!(
|
||||
"Failed to copy .icns file from {} to {}",
|
||||
icon_path.display(),
|
||||
dest_path.display()
|
||||
)
|
||||
})?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
}
|
||||
|
||||
let mut family = icns::IconFamily::new();
|
||||
|
||||
for icon_path in &icon_paths {
|
||||
let ext = icon_path
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
if ext != "png" {
|
||||
tracing::debug!("Skipping non-PNG icon file: {}", icon_path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
let img = ImageReader::open(icon_path)
|
||||
.with_context(|| format!("Failed to open icon image: {}", icon_path.display()))?
|
||||
.decode()
|
||||
.with_context(|| format!("Failed to decode icon image: {}", icon_path.display()))?;
|
||||
|
||||
add_image_to_family(&mut family, &img)?;
|
||||
}
|
||||
|
||||
if family.is_empty() {
|
||||
tracing::warn!("No valid icon images found; skipping .icns generation");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let file = File::create(&dest_path)
|
||||
.with_context(|| format!("Failed to create {}", dest_path.display()))?;
|
||||
let writer = BufWriter::new(file);
|
||||
family
|
||||
.write(writer)
|
||||
.with_context(|| format!("Failed to write .icns to {}", dest_path.display()))?;
|
||||
|
||||
tracing::info!("Generated .icns at {}", dest_path.display());
|
||||
Ok(Some(dest_path))
|
||||
}
|
||||
|
||||
/// Build a `plist::Dictionary` for Info.plist.
|
||||
fn create_macos_info_plist(
|
||||
&self,
|
||||
macos_settings: &MacOsSettings,
|
||||
icon_filename: Option<&str>,
|
||||
) -> Result<plist::Dictionary> {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
|
||||
let product_name = self.product_name();
|
||||
let bundle_name = macos_settings
|
||||
.bundle_name
|
||||
.as_deref()
|
||||
.unwrap_or(&product_name);
|
||||
|
||||
dict.insert(
|
||||
"CFBundleDevelopmentRegion".into(),
|
||||
plist::Value::String("English".into()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleDisplayName".into(),
|
||||
plist::Value::String(bundle_name.to_string()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleExecutable".into(),
|
||||
plist::Value::String(self.main_binary_name().to_string()),
|
||||
);
|
||||
|
||||
if let Some(icon) = icon_filename {
|
||||
dict.insert(
|
||||
"CFBundleIconFile".into(),
|
||||
plist::Value::String(icon.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.insert(
|
||||
"CFBundleIdentifier".into(),
|
||||
plist::Value::String(self.bundle_identifier()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleInfoDictionaryVersion".into(),
|
||||
plist::Value::String("6.0".into()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleName".into(),
|
||||
plist::Value::String(bundle_name.to_string()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundlePackageType".into(),
|
||||
plist::Value::String("APPL".into()),
|
||||
);
|
||||
|
||||
let version = self.version_string();
|
||||
let bundle_version = macos_settings.bundle_version.as_deref().unwrap_or(&version);
|
||||
|
||||
dict.insert(
|
||||
"CFBundleShortVersionString".into(),
|
||||
plist::Value::String(version.clone()),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleVersion".into(),
|
||||
plist::Value::String(bundle_version.to_string()),
|
||||
);
|
||||
|
||||
let min_version = macos_settings
|
||||
.minimum_system_version
|
||||
.as_deref()
|
||||
.unwrap_or("10.13");
|
||||
dict.insert(
|
||||
"LSMinimumSystemVersion".into(),
|
||||
plist::Value::String(min_version.to_string()),
|
||||
);
|
||||
|
||||
if let Some(category_str) = self.app_category() {
|
||||
if let Ok(category) = category_str.parse::<AppCategory>() {
|
||||
dict.insert(
|
||||
"LSApplicationCategoryType".into(),
|
||||
plist::Value::String(category.macos_application_category_type().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(copyright) = self.copyright_string() {
|
||||
dict.insert(
|
||||
"NSHumanReadableCopyright".into(),
|
||||
plist::Value::String(copyright.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.insert(
|
||||
"NSHighResolutionCapable".into(),
|
||||
plist::Value::Boolean(true),
|
||||
);
|
||||
|
||||
if let Some(domain) = &macos_settings.exception_domain {
|
||||
let mut ats_dict = plist::Dictionary::new();
|
||||
let mut exception_dict = plist::Dictionary::new();
|
||||
let mut domain_dict = plist::Dictionary::new();
|
||||
domain_dict.insert(
|
||||
"NSExceptionAllowsInsecureHTTPLoads".into(),
|
||||
plist::Value::Boolean(true),
|
||||
);
|
||||
domain_dict.insert("NSIncludesSubdomains".into(), plist::Value::Boolean(true));
|
||||
exception_dict.insert(domain.clone(), plist::Value::Dictionary(domain_dict));
|
||||
ats_dict.insert(
|
||||
"NSExceptionDomains".into(),
|
||||
plist::Value::Dictionary(exception_dict),
|
||||
);
|
||||
dict.insert(
|
||||
"NSAppTransportSecurity".into(),
|
||||
plist::Value::Dictionary(ats_dict),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(provider) = &macos_settings.provider_short_name {
|
||||
dict.insert(
|
||||
"ITSAppUsesNonExemptEncryption".into(),
|
||||
plist::Value::Boolean(false),
|
||||
);
|
||||
let _ = provider;
|
||||
}
|
||||
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add all appropriate size variants of an image to the ICNS family.
|
||||
fn add_image_to_family(family: &mut icns::IconFamily, img: &DynamicImage) -> Result<()> {
|
||||
// The icon sizes (in points) we generate for the .icns file, along with their
|
||||
// densities. macOS expects both 1x and 2x variants.
|
||||
const ICON_SIZES: &[(u32, u32, u32)] = &[
|
||||
(16, 16, 1),
|
||||
(16, 16, 2),
|
||||
(32, 32, 1),
|
||||
(32, 32, 2),
|
||||
(64, 64, 1),
|
||||
(64, 64, 2),
|
||||
(128, 128, 1),
|
||||
(128, 128, 2),
|
||||
(256, 256, 1),
|
||||
(256, 256, 2),
|
||||
(512, 512, 1),
|
||||
(512, 512, 2),
|
||||
];
|
||||
|
||||
for &(width, height, density) in ICON_SIZES {
|
||||
let pixel_width = width * density;
|
||||
let pixel_height = height * density;
|
||||
|
||||
let icon_type =
|
||||
match icns::IconType::from_pixel_size_and_density(pixel_width, pixel_height, density) {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if family.has_icon_with_type(icon_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let resized = img.resize_exact(
|
||||
pixel_width,
|
||||
pixel_height,
|
||||
image::imageops::FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
let rgba = resized.to_rgba8();
|
||||
let icns_image = icns::Image::from_data(
|
||||
icns::PixelFormat::RGBA,
|
||||
pixel_width,
|
||||
pixel_height,
|
||||
rgba.into_raw(),
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("Failed to create icns::Image for {pixel_width}x{pixel_height}@{density}x")
|
||||
})?;
|
||||
|
||||
family
|
||||
.add_icon_with_type(&icns_image, icon_type)
|
||||
.with_context(|| format!("Failed to add icon type {icon_type:?} to ICNS family"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a plist dictionary to a file.
|
||||
fn write_plist(dict: &plist::Dictionary, path: &Path) -> Result<()> {
|
||||
plist::Value::Dictionary(dict.clone())
|
||||
.to_file_xml(path)
|
||||
.with_context(|| format!("Failed to write Info.plist to {}", path.display()))
|
||||
}
|
||||
|
||||
/// Copy a framework (directory or .dylib) to the Frameworks directory.
|
||||
fn copy_framework(src: &Path, frameworks_dir: &Path) -> Result<()> {
|
||||
let dest = frameworks_dir.join(src.file_name().context("Framework path has no filename")?);
|
||||
|
||||
tracing::debug!("Copying framework: {} -> {}", src.display(), dest.display());
|
||||
|
||||
if src.is_dir() {
|
||||
copy_dir_recursive(src, &dest)?;
|
||||
} else {
|
||||
fs::copy(src, &dest)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set up the signing identity.
|
||||
async fn setup_keychain(identity: Option<&str>) -> Result<Option<SigningIdentity>> {
|
||||
let certificate_encoded = std::env::var("APPLE_CERTIFICATE").ok();
|
||||
let certificate_password = std::env::var("APPLE_CERTIFICATE_PASSWORD")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(cert_base64) = certificate_encoded {
|
||||
tracing::info!("Setting up temporary keychain for code signing (CI mode)");
|
||||
let keychain = setup_temp_keychain(&cert_base64, &certificate_password).await?;
|
||||
let identity_name = find_identity_in_keychain(&keychain.path).await?;
|
||||
return Ok(Some(SigningIdentity {
|
||||
identity: identity_name,
|
||||
temp_keychain: Some(keychain),
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(id) = identity {
|
||||
if !id.is_empty() {
|
||||
return Ok(Some(SigningIdentity {
|
||||
identity: id.to_string(),
|
||||
temp_keychain: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Set up a temporary keychain and import the certificate into it.
|
||||
async fn setup_temp_keychain(cert_base64: &str, password: &str) -> Result<TempKeychain> {
|
||||
use std::io::Write;
|
||||
|
||||
let keychain_password = "dioxus-bundle-keychain";
|
||||
let keychain_path = std::env::temp_dir().join("dioxus-signing.keychain-db");
|
||||
|
||||
let cert_data = base64_decode(cert_base64)
|
||||
.await
|
||||
.context("Failed to decode APPLE_CERTIFICATE from base64")?;
|
||||
|
||||
let cert_file = std::env::temp_dir().join("dioxus-signing-cert.p12");
|
||||
let mut f = std::fs::File::create(&cert_file)?;
|
||||
f.write_all(&cert_data)?;
|
||||
drop(f);
|
||||
|
||||
let _ = Command::new("security")
|
||||
.args(["delete-keychain", &keychain_path.display().to_string()])
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let mut create_keychain_cmd = Command::new("security");
|
||||
create_keychain_cmd.args([
|
||||
"create-keychain",
|
||||
"-p",
|
||||
keychain_password,
|
||||
&keychain_path.display().to_string(),
|
||||
]);
|
||||
run_command(&mut create_keychain_cmd, "create-keychain").await?;
|
||||
|
||||
let mut import_cmd = Command::new("security");
|
||||
import_cmd.args([
|
||||
"import",
|
||||
&cert_file.display().to_string(),
|
||||
"-k",
|
||||
&keychain_path.display().to_string(),
|
||||
"-P",
|
||||
password,
|
||||
"-T",
|
||||
"/usr/bin/codesign",
|
||||
"-T",
|
||||
"/usr/bin/security",
|
||||
]);
|
||||
run_command(&mut import_cmd, "import certificate").await?;
|
||||
|
||||
let output = Command::new("security")
|
||||
.args(["list-keychains", "-d", "user"])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to list keychains")?;
|
||||
let current_keychains = String::from_utf8_lossy(&output.stdout);
|
||||
let mut keychains: Vec<String> = current_keychains
|
||||
.lines()
|
||||
.map(|l| l.trim().trim_matches('"').to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
keychains.insert(0, keychain_path.display().to_string());
|
||||
|
||||
let mut cmd = Command::new("security");
|
||||
cmd.args(["list-keychains", "-d", "user", "-s"]);
|
||||
for kc in &keychains {
|
||||
cmd.arg(kc);
|
||||
}
|
||||
run_command(&mut cmd, "list-keychains -s").await?;
|
||||
|
||||
let mut unlock_cmd = Command::new("security");
|
||||
unlock_cmd.args([
|
||||
"unlock-keychain",
|
||||
"-p",
|
||||
keychain_password,
|
||||
&keychain_path.display().to_string(),
|
||||
]);
|
||||
run_command(&mut unlock_cmd, "unlock-keychain").await?;
|
||||
|
||||
let mut set_partition_cmd = Command::new("security");
|
||||
set_partition_cmd.args([
|
||||
"set-key-partition-list",
|
||||
"-S",
|
||||
"apple-tool:,apple:",
|
||||
"-s",
|
||||
"-k",
|
||||
keychain_password,
|
||||
&keychain_path.display().to_string(),
|
||||
]);
|
||||
run_command(&mut set_partition_cmd, "set-key-partition-list").await?;
|
||||
|
||||
let _ = std::fs::remove_file(&cert_file);
|
||||
|
||||
Ok(TempKeychain {
|
||||
path: keychain_path,
|
||||
password: keychain_password.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the signing identity in a keychain using `security find-identity`.
|
||||
async fn find_identity_in_keychain(keychain_path: &Path) -> Result<String> {
|
||||
let output = Command::new("security")
|
||||
.args([
|
||||
"find-identity",
|
||||
"-v",
|
||||
"-p",
|
||||
"codesigning",
|
||||
&keychain_path.display().to_string(),
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Failed to run `security find-identity`")?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with("1)")
|
||||
|| line.contains("Developer ID")
|
||||
|| line.contains("Apple Development")
|
||||
{
|
||||
if let Some(start) = line.find('"') {
|
||||
if let Some(end) = line.rfind('"') {
|
||||
if end > start {
|
||||
return Ok(line[start + 1..end].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 && parts[1].len() == 40 {
|
||||
return Ok(parts[1].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"No valid signing identity found in keychain {}.\nOutput: {}",
|
||||
keychain_path.display(),
|
||||
stdout
|
||||
)
|
||||
}
|
||||
|
||||
/// Sign a list of paths with the given identity.
|
||||
async fn sign_paths(
|
||||
identity: &SigningIdentity,
|
||||
targets: Vec<SignTarget>,
|
||||
settings: &MacOsSettings,
|
||||
) -> Result<()> {
|
||||
for target in &targets {
|
||||
sign_path(identity, target, settings).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sign a single path with `codesign`.
|
||||
async fn sign_path(
|
||||
identity: &SigningIdentity,
|
||||
target: &SignTarget,
|
||||
settings: &MacOsSettings,
|
||||
) -> Result<()> {
|
||||
tracing::info!("Signing: {}", target.path.display());
|
||||
|
||||
let mut cmd = Command::new("codesign");
|
||||
cmd.args(["--force", "--sign", &identity.identity]);
|
||||
|
||||
if settings.hardened_runtime {
|
||||
cmd.arg("--options");
|
||||
cmd.arg("runtime");
|
||||
}
|
||||
|
||||
if target
|
||||
.path
|
||||
.extension()
|
||||
.map(|e| e == "app" || e == "framework")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
cmd.arg("--deep");
|
||||
}
|
||||
|
||||
if let Some(entitlements) = &settings.entitlements {
|
||||
cmd.args(["--entitlements", entitlements]);
|
||||
}
|
||||
|
||||
if let Some(keychain) = &identity.temp_keychain {
|
||||
cmd.args(["--keychain", &keychain.path.display().to_string()]);
|
||||
}
|
||||
|
||||
cmd.arg(&target.path);
|
||||
|
||||
run_command(&mut cmd, &format!("codesign {}", target.path.display())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notarize a .app or .dmg with Apple's notary service.
|
||||
async fn notarize(app_path: &Path) -> Result<()> {
|
||||
let apple_id = std::env::var("APPLE_ID").ok();
|
||||
let apple_password = std::env::var("APPLE_PASSWORD").ok();
|
||||
let apple_team_id = std::env::var("APPLE_TEAM_ID").ok();
|
||||
let api_key = std::env::var("APPLE_API_KEY").ok();
|
||||
let api_issuer = std::env::var("APPLE_API_ISSUER").ok();
|
||||
let api_key_path = std::env::var("APPLE_API_KEY_PATH").ok();
|
||||
|
||||
let mut cmd = Command::new("xcrun");
|
||||
cmd.args(["notarytool", "submit"]);
|
||||
cmd.arg(app_path);
|
||||
|
||||
if let (Some(key), Some(issuer), Some(key_path)) = (&api_key, &api_issuer, &api_key_path) {
|
||||
cmd.args(["--key", key_path]);
|
||||
cmd.args(["--key-id", key]);
|
||||
cmd.args(["--issuer", issuer]);
|
||||
} else if let (Some(id), Some(pwd), Some(team)) = (&apple_id, &apple_password, &apple_team_id) {
|
||||
cmd.args(["--apple-id", id]);
|
||||
cmd.args(["--password", pwd]);
|
||||
cmd.args(["--team-id", team]);
|
||||
} else {
|
||||
bail!(
|
||||
"Notarization requires either:\n\
|
||||
- APPLE_ID, APPLE_PASSWORD, and APPLE_TEAM_ID env vars, or\n\
|
||||
- APPLE_API_KEY, APPLE_API_ISSUER, and APPLE_API_KEY_PATH env vars"
|
||||
);
|
||||
}
|
||||
|
||||
cmd.arg("--wait");
|
||||
|
||||
tracing::info!("Submitting {} for notarization...", app_path.display());
|
||||
run_command(&mut cmd, "xcrun notarytool submit").await?;
|
||||
|
||||
tracing::info!("Stapling notarization ticket...");
|
||||
let mut staple_cmd = Command::new("xcrun");
|
||||
staple_cmd.args(["stapler", "staple"]).arg(app_path);
|
||||
run_command(&mut staple_cmd, "xcrun stapler staple").await?;
|
||||
|
||||
tracing::info!("Notarization complete for {}", app_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to run a command and return a nice error on failure.
|
||||
async fn run_command(cmd: &mut Command, description: &str) -> Result<()> {
|
||||
tracing::debug!("Running: {:?}", cmd);
|
||||
let status = cmd
|
||||
.status()
|
||||
.await
|
||||
.with_context(|| format!("Failed to execute `{description}`"))?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("`{description}` failed with exit code: {}", status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode base64 (standard or URL-safe).
|
||||
async fn base64_decode(input: &str) -> Result<Vec<u8>> {
|
||||
let mut child = Command::new("base64")
|
||||
.args(["--decode"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to decode base64 certificate")?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(input.as_bytes())
|
||||
.await
|
||||
.context("Failed writing base64 certificate to decoder stdin")?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.await
|
||||
.context("Failed to decode base64 certificate")?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"base64 --decode failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(output.stdout)
|
||||
}
|
||||
|
||||
/// The result of DMG bundling, which may include both the `.dmg` and `.app` outputs.
|
||||
pub(crate) struct DmgBundled {
|
||||
/// Paths to the generated `.dmg` file(s).
|
||||
pub dmg: Vec<PathBuf>,
|
||||
/// Paths to the generated `.app` bundle(s) (if the `.app` was built as a dependency).
|
||||
pub app: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// A target to be code-signed.
|
||||
struct SignTarget {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
/// A code signing identity, optionally backed by a temporary keychain.
|
||||
struct SigningIdentity {
|
||||
/// The identity string passed to `codesign --sign`.
|
||||
/// This is either a team/certificate name or a SHA-1 hash.
|
||||
identity: String,
|
||||
/// If we created a temporary keychain to import a certificate,
|
||||
/// this holds its path so we can clean it up later.
|
||||
temp_keychain: Option<TempKeychain>,
|
||||
}
|
||||
|
||||
/// A temporary keychain created for CI certificate import.
|
||||
#[allow(dead_code)]
|
||||
struct TempKeychain {
|
||||
path: PathBuf,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl Drop for TempKeychain {
|
||||
fn drop(&mut self) {
|
||||
tracing::debug!("Cleaning up temporary keychain: {}", self.path.display());
|
||||
let _ = StdCommand::new("security")
|
||||
.args(["delete-keychain", &self.path.display().to_string()])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
mod ios;
|
||||
mod linux;
|
||||
mod macos;
|
||||
mod tools;
|
||||
mod updater;
|
||||
mod windows;
|
||||
|
||||
use crate::PackageType;
|
||||
use crate::{BuildRequest, DebianSettings, MacOsSettings, WindowsSettings};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tools::ResolvedTools;
|
||||
|
||||
/// A completed bundle with its output paths.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Bundle {
|
||||
pub package_type: PackageType,
|
||||
pub bundle_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// BundleContext wraps a BuildRequest and provides the settings API
|
||||
/// that bundle format modules need. This is the adapter between
|
||||
/// Dioxus build infrastructure and the individual bundler implementations.
|
||||
pub(crate) struct BundleContext<'a> {
|
||||
pub(crate) build: &'a BuildRequest,
|
||||
|
||||
pub(crate) package_types: Vec<PackageType>,
|
||||
|
||||
/// Pre-computed resource map: source path -> target path in bundle
|
||||
pub(crate) resources_map: HashMap<String, String>,
|
||||
|
||||
/// Pre-resolved tool paths (NSIS, WiX, linuxdeploy, WebView2).
|
||||
pub(crate) tools: ResolvedTools,
|
||||
}
|
||||
|
||||
impl<'a> BundleContext<'a> {
|
||||
/// Create a new BundleContext from a BuildRequest and optional package types.
|
||||
pub(crate) async fn new(
|
||||
build: &'a BuildRequest,
|
||||
package_types: &Option<Vec<PackageType>>,
|
||||
) -> Result<Self> {
|
||||
let package_types = package_types.clone().unwrap_or_default();
|
||||
|
||||
// Build the resources map from assets + config resources
|
||||
let mut resources_map = HashMap::new();
|
||||
|
||||
let asset_dir = build.asset_dir();
|
||||
if asset_dir.exists() {
|
||||
for entry in walkdir::WalkDir::new(&asset_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let old = path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize {entry:?}"))?;
|
||||
let new =
|
||||
PathBuf::from("assets").join(path.strip_prefix(&asset_dir).unwrap_or(path));
|
||||
resources_map.insert(old.display().to_string(), new.display().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge in any custom resources from config
|
||||
if let Some(resources) = &build.config.bundle.resources {
|
||||
for resource_path in resources {
|
||||
resources_map.insert(resource_path.clone(), String::new());
|
||||
}
|
||||
}
|
||||
|
||||
let tools_dir = dirs::cache_dir()
|
||||
.unwrap_or_else(|| PathBuf::from(".cache"))
|
||||
.join("dioxus");
|
||||
let _ = std::fs::create_dir_all(&tools_dir);
|
||||
|
||||
let arch = {
|
||||
let target = build.triple.to_string();
|
||||
if target.starts_with("x86_64") {
|
||||
Arch::X86_64
|
||||
} else if target.starts_with('i') {
|
||||
Arch::X86
|
||||
} else if target.starts_with("arm") && target.ends_with("hf") {
|
||||
Arch::Armhf
|
||||
} else if target.starts_with("arm") {
|
||||
Arch::Armel
|
||||
} else if target.starts_with("aarch64") {
|
||||
Arch::AArch64
|
||||
} else if target.starts_with("riscv64") {
|
||||
Arch::Riscv64
|
||||
} else if target.starts_with("universal") {
|
||||
Arch::Universal
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
Arch::X86_64
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
Arch::AArch64
|
||||
} else {
|
||||
Arch::X86_64
|
||||
}
|
||||
};
|
||||
|
||||
let windows_settings = build.config.bundle.windows.clone().unwrap_or_default();
|
||||
let tools =
|
||||
tools::resolve_tools(&tools_dir, &package_types, &windows_settings, arch).await?;
|
||||
|
||||
Ok(Self {
|
||||
build,
|
||||
package_types,
|
||||
resources_map,
|
||||
tools,
|
||||
})
|
||||
}
|
||||
|
||||
/// Bundle the project for every configured package type, in dependency order.
|
||||
///
|
||||
/// This is the orchestration entrypoint for native packaging. It owns the
|
||||
/// sequencing rules between bundle formats, but delegates the actual format
|
||||
/// implementation to the per-platform `bundle_*` methods on `BundleContext`.
|
||||
///
|
||||
/// The method performs the following high-level workflow:
|
||||
/// 1. Read the resolved package types from the immutable context.
|
||||
/// 2. Sort them so prerequisite artifacts are built before dependents.
|
||||
/// In practice, this means raw distributable formats such as `.app`, `.deb`,
|
||||
/// `.rpm`, `.AppImage`, `.msi`, `.exe`, `.apk`, and `.aab` run before
|
||||
/// dependent archive formats like `.ipa`/`.dmg`, and `Updater` always runs last.
|
||||
/// 3. Dispatch to the top-level format-specific bundling method for each package
|
||||
/// type and collect the artifact paths it returns.
|
||||
/// 4. Reuse outputs where formats depend on each other. For example, DMG creation
|
||||
/// may synthesize a `.app` bundle and return it alongside the `.dmg`.
|
||||
/// 5. Remove intermediate macOS `.app` bundles when they were only needed as an
|
||||
/// implementation detail of `.dmg` or updater generation.
|
||||
///
|
||||
/// The returned [`Bundle`] values are the canonical record of what was emitted by
|
||||
/// the bundling pass. Later steps such as updater packaging rely on these paths
|
||||
/// rather than rediscovering artifacts on disk.
|
||||
pub(crate) async fn bundle_project(&self) -> Result<Vec<Bundle>> {
|
||||
let mut package_types = self.package_types();
|
||||
|
||||
// Sort so dependencies come first (e.g. .app before .dmg)
|
||||
package_types.sort_by_key(|ptype| match ptype {
|
||||
PackageType::MacOsBundle
|
||||
| PackageType::IosApp
|
||||
| PackageType::WindowsMsi
|
||||
| PackageType::Nsis
|
||||
| PackageType::Deb
|
||||
| PackageType::Rpm
|
||||
| PackageType::AppImage
|
||||
| PackageType::Apk
|
||||
| PackageType::Aab => 0,
|
||||
PackageType::Ipa | PackageType::Dmg => 1,
|
||||
PackageType::Updater => 2,
|
||||
});
|
||||
|
||||
let mut bundles = Vec::<Bundle>::new();
|
||||
|
||||
for package_type in &package_types {
|
||||
// Skip if already built (e.g. DMG already built .app)
|
||||
if bundles.iter().any(|b| b.package_type == *package_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let bundle_paths = match package_type {
|
||||
PackageType::MacOsBundle => self.bundle_macos_app().await?,
|
||||
PackageType::IosApp => self.bundle_ios_app().await?,
|
||||
PackageType::Ipa => self.bundle_ios_ipa(&bundles).await?.ipa,
|
||||
PackageType::Dmg => {
|
||||
let bundled = self.bundle_macos_dmg(&bundles).await?;
|
||||
if !bundled.app.is_empty() {
|
||||
bundles.push(Bundle {
|
||||
package_type: PackageType::MacOsBundle,
|
||||
bundle_paths: bundled.app,
|
||||
});
|
||||
}
|
||||
bundled.dmg
|
||||
}
|
||||
PackageType::Deb => self.bundle_linux_deb().await?,
|
||||
PackageType::Rpm => self.bundle_linux_rpm().await?,
|
||||
PackageType::AppImage => self.bundle_linux_appimage().await?,
|
||||
PackageType::WindowsMsi => self.bundle_windows_msi().await?,
|
||||
PackageType::Nsis => self.bundle_windows_nsis().await?,
|
||||
PackageType::Updater => self.bundle_updater(&bundles).await?,
|
||||
PackageType::Apk | PackageType::Aab => self.bundle_android(*package_type).await?,
|
||||
};
|
||||
|
||||
bundles.push(Bundle {
|
||||
package_type: *package_type,
|
||||
bundle_paths,
|
||||
});
|
||||
}
|
||||
|
||||
// On macOS, clean up .app if only building dmg or updater
|
||||
if !package_types.contains(&PackageType::MacOsBundle) {
|
||||
if let Some(idx) = bundles
|
||||
.iter()
|
||||
.position(|b| b.package_type == PackageType::MacOsBundle)
|
||||
{
|
||||
let app_bundle = bundles.remove(idx);
|
||||
for path in &app_bundle.bundle_paths {
|
||||
tracing::info!("Cleaning up intermediate .app: {}", path.display());
|
||||
if path.is_dir() {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
} else {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for bundle in &bundles {
|
||||
for path in &bundle.bundle_paths {
|
||||
tracing::info!("Bundled: {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
/// The package types to bundle.
|
||||
pub(crate) fn package_types(&self) -> Vec<PackageType> {
|
||||
self.package_types.clone()
|
||||
}
|
||||
|
||||
/// The product name (PascalCase).
|
||||
pub(crate) fn product_name(&self) -> String {
|
||||
self.build.bundled_app_name()
|
||||
}
|
||||
|
||||
/// The bundle identifier (e.g. "com.example.app").
|
||||
pub(crate) fn bundle_identifier(&self) -> String {
|
||||
self.build.bundle_identifier()
|
||||
}
|
||||
|
||||
/// The publisher name.
|
||||
pub(crate) fn publisher(&self) -> Option<&str> {
|
||||
self.build.config.bundle.publisher.as_deref()
|
||||
}
|
||||
|
||||
/// The version string from Cargo.toml.
|
||||
pub(crate) fn version_string(&self) -> String {
|
||||
self.build.package().version.to_string()
|
||||
}
|
||||
|
||||
/// The short description.
|
||||
pub(crate) fn short_description(&self) -> String {
|
||||
self.build
|
||||
.config
|
||||
.bundle
|
||||
.short_description
|
||||
.clone()
|
||||
.or_else(|| self.build.package().description.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The long description.
|
||||
pub(crate) fn long_description(&self) -> Option<&str> {
|
||||
self.build.config.bundle.long_description.as_deref()
|
||||
}
|
||||
|
||||
/// The copyright string.
|
||||
pub(crate) fn copyright_string(&self) -> Option<&str> {
|
||||
self.build.config.bundle.copyright.as_deref()
|
||||
}
|
||||
|
||||
/// The authors list.
|
||||
pub(crate) fn authors(&self) -> &[String] {
|
||||
&self.build.package().authors
|
||||
}
|
||||
|
||||
/// Authors as a comma-separated string.
|
||||
pub(crate) fn authors_comma_separated(&self) -> Option<String> {
|
||||
let names = self.authors();
|
||||
if names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(names.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
/// The homepage URL.
|
||||
pub(crate) fn homepage_url(&self) -> Option<String> {
|
||||
self.build
|
||||
.package()
|
||||
.homepage
|
||||
.as_ref()
|
||||
.filter(|hp| !hp.is_empty())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// The license string.
|
||||
pub(crate) fn license(&self) -> Option<&str> {
|
||||
self.build.package().license.as_deref()
|
||||
}
|
||||
|
||||
/// The app category string.
|
||||
pub(crate) fn app_category(&self) -> Option<&str> {
|
||||
self.build.config.bundle.category.as_deref()
|
||||
}
|
||||
|
||||
/// The main binary name (without extension).
|
||||
pub(crate) fn main_binary_name(&self) -> &str {
|
||||
self.build.executable_name()
|
||||
}
|
||||
|
||||
/// The path to the main binary.
|
||||
pub(crate) fn main_binary_path(&self) -> PathBuf {
|
||||
self.build.main_exe()
|
||||
}
|
||||
|
||||
/// The output directory for bundles.
|
||||
pub(crate) fn project_out_directory(&self) -> PathBuf {
|
||||
self.build.bundle_dir(self.build.bundle)
|
||||
}
|
||||
|
||||
/// The target triple string.
|
||||
pub(crate) fn target(&self) -> String {
|
||||
self.build.triple.to_string()
|
||||
}
|
||||
|
||||
/// The binary architecture.
|
||||
pub(crate) fn binary_arch(&self) -> Arch {
|
||||
let target = self.target();
|
||||
if target.starts_with("x86_64") {
|
||||
Arch::X86_64
|
||||
} else if target.starts_with('i') {
|
||||
Arch::X86
|
||||
} else if target.starts_with("arm") && target.ends_with("hf") {
|
||||
Arch::Armhf
|
||||
} else if target.starts_with("arm") {
|
||||
Arch::Armel
|
||||
} else if target.starts_with("aarch64") {
|
||||
Arch::AArch64
|
||||
} else if target.starts_with("riscv64") {
|
||||
Arch::Riscv64
|
||||
} else if target.starts_with("universal") {
|
||||
Arch::Universal
|
||||
} else {
|
||||
// Default to the host arch
|
||||
if cfg!(target_arch = "x86_64") {
|
||||
Arch::X86_64
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
Arch::AArch64
|
||||
} else {
|
||||
Arch::X86_64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon file paths from config, canonicalized relative to the crate dir.
|
||||
pub(crate) fn icon_files(&self) -> Result<Vec<PathBuf>> {
|
||||
let mut icons = Vec::new();
|
||||
if let Some(icon_paths) = &self.build.config.bundle.icon {
|
||||
for icon in icon_paths {
|
||||
let icon_path = self
|
||||
.build
|
||||
.crate_dir()
|
||||
.join(icon)
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize icon path: {icon}"))?;
|
||||
icons.push(icon_path);
|
||||
}
|
||||
}
|
||||
Ok(icons)
|
||||
}
|
||||
|
||||
/// Copy resources to the given path.
|
||||
pub(crate) fn copy_resources(&self, dest: &Path) -> Result<()> {
|
||||
for (src, target) in &self.resources_map {
|
||||
let src_path = PathBuf::from(src);
|
||||
if !src_path.exists() {
|
||||
tracing::warn!("Resource not found: {src}");
|
||||
continue;
|
||||
}
|
||||
let dest_path = if target.is_empty() {
|
||||
dest.join(src_path.file_name().unwrap_or_default())
|
||||
} else {
|
||||
dest.join(target)
|
||||
};
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::copy(&src_path, &dest_path).with_context(|| {
|
||||
format!("Failed to copy resource {src} -> {}", dest_path.display())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy external binaries to the given path.
|
||||
pub(crate) fn copy_external_binaries(&self, dest: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
if let Some(bins) = &self.build.config.bundle.external_bin {
|
||||
let target = self.target();
|
||||
for bin in bins {
|
||||
let src = PathBuf::from(format!("{bin}-{target}"));
|
||||
if src.exists() {
|
||||
let dest_name = src
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{target}"), "");
|
||||
let dest_path = dest.join(dest_name);
|
||||
std::fs::copy(&src, &dest_path)?;
|
||||
paths.push(dest_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// The crate directory (where Cargo.toml is).
|
||||
pub(crate) fn crate_dir(&self) -> PathBuf {
|
||||
self.build.crate_dir()
|
||||
}
|
||||
|
||||
/// Debian settings from config.
|
||||
pub(crate) fn deb(&self) -> DebianSettings {
|
||||
self.build.config.bundle.deb.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// macOS settings from config.
|
||||
pub(crate) fn macos(&self) -> MacOsSettings {
|
||||
self.build.config.bundle.macos.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Windows settings from config.
|
||||
pub(crate) fn windows(&self) -> WindowsSettings {
|
||||
self.build.config.bundle.windows.clone().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The architecture of the target binary.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum Arch {
|
||||
X86_64,
|
||||
X86,
|
||||
AArch64,
|
||||
Armhf,
|
||||
Armel,
|
||||
Riscv64,
|
||||
Universal,
|
||||
}
|
||||
|
||||
impl Arch {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x86_64",
|
||||
Arch::X86 => "x86",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhf",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
Arch::Universal => "universal",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deb_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "amd64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "arm64",
|
||||
Arch::Armhf => "armhf",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
Arch::Universal => "all",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rpm_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x86_64",
|
||||
Arch::X86 => "i686",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armv7hl",
|
||||
Arch::Armel => "armv6l",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
Arch::Universal => "noarch",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn appimage_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x86_64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhf",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
Arch::Universal => "x86_64",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn windows_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x64",
|
||||
Arch::X86 => "x86",
|
||||
Arch::AArch64 => "arm64",
|
||||
_ => "x64",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wix_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x64",
|
||||
Arch::X86 => "x86",
|
||||
Arch::AArch64 => "arm64",
|
||||
_ => "x64",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wix_program_files_folder(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86 => "ProgramFilesFolder",
|
||||
Arch::X86_64 | Arch::AArch64 => "ProgramFiles64Folder",
|
||||
_ => "ProgramFiles64Folder",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn linuxdeploy_arch(&self) -> &'static str {
|
||||
match self {
|
||||
Arch::X86_64 => "x86_64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf | Arch::Armel => "armhf",
|
||||
Arch::Riscv64 | Arch::Universal => "x86_64",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Arch {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
/// The possible app categories.
|
||||
/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum AppCategory {
|
||||
Business,
|
||||
DeveloperTool,
|
||||
Education,
|
||||
Entertainment,
|
||||
Finance,
|
||||
Game,
|
||||
ActionGame,
|
||||
AdventureGame,
|
||||
ArcadeGame,
|
||||
BoardGame,
|
||||
CardGame,
|
||||
CasinoGame,
|
||||
DiceGame,
|
||||
EducationalGame,
|
||||
FamilyGame,
|
||||
KidsGame,
|
||||
MusicGame,
|
||||
PuzzleGame,
|
||||
RacingGame,
|
||||
RolePlayingGame,
|
||||
SimulationGame,
|
||||
SportsGame,
|
||||
StrategyGame,
|
||||
TriviaGame,
|
||||
WordGame,
|
||||
GraphicsAndDesign,
|
||||
HealthcareAndFitness,
|
||||
Lifestyle,
|
||||
Medical,
|
||||
Music,
|
||||
News,
|
||||
Photography,
|
||||
Productivity,
|
||||
Reference,
|
||||
SocialNetworking,
|
||||
Sports,
|
||||
Travel,
|
||||
Utility,
|
||||
Video,
|
||||
Weather,
|
||||
}
|
||||
|
||||
impl FromStr for AppCategory {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(input: &str) -> Result<AppCategory, Self::Err> {
|
||||
const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
|
||||
|
||||
let mut input = input.to_ascii_lowercase();
|
||||
if input.starts_with(MACOS_APP_CATEGORY_PREFIX) {
|
||||
input = input
|
||||
.split_at(MACOS_APP_CATEGORY_PREFIX.len())
|
||||
.1
|
||||
.to_string();
|
||||
}
|
||||
input = input.replace(' ', "");
|
||||
input = input.replace('-', "");
|
||||
|
||||
for &(string, category) in CATEGORY_STRINGS.iter() {
|
||||
if input == string {
|
||||
return Ok(category);
|
||||
}
|
||||
}
|
||||
Err(format!("Unknown app category: {input}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl AppCategory {
|
||||
/// Map to closest set of Freedesktop categories.
|
||||
pub(crate) fn freedesktop_categories(self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "Office;",
|
||||
AppCategory::DeveloperTool => "Development;",
|
||||
AppCategory::Education => "Education;",
|
||||
AppCategory::Entertainment => "Network;",
|
||||
AppCategory::Finance => "Office;Finance;",
|
||||
AppCategory::Game => "Game;",
|
||||
AppCategory::ActionGame => "Game;ActionGame;",
|
||||
AppCategory::AdventureGame => "Game;AdventureGame;",
|
||||
AppCategory::ArcadeGame => "Game;ArcadeGame;",
|
||||
AppCategory::BoardGame => "Game;BoardGame;",
|
||||
AppCategory::CardGame => "Game;CardGame;",
|
||||
AppCategory::CasinoGame => "Game;",
|
||||
AppCategory::DiceGame => "Game;",
|
||||
AppCategory::EducationalGame => "Game;Education;",
|
||||
AppCategory::FamilyGame => "Game;",
|
||||
AppCategory::KidsGame => "Game;KidsGame;",
|
||||
AppCategory::MusicGame => "Game;",
|
||||
AppCategory::PuzzleGame => "Game;LogicGame;",
|
||||
AppCategory::RacingGame => "Game;",
|
||||
AppCategory::RolePlayingGame => "Game;RolePlaying;",
|
||||
AppCategory::SimulationGame => "Game;Simulation;",
|
||||
AppCategory::SportsGame => "Game;SportsGame;",
|
||||
AppCategory::StrategyGame => "Game;StrategyGame;",
|
||||
AppCategory::TriviaGame => "Game;",
|
||||
AppCategory::WordGame => "Game;",
|
||||
AppCategory::GraphicsAndDesign => "Graphics;",
|
||||
AppCategory::HealthcareAndFitness => "Science;",
|
||||
AppCategory::Lifestyle => "Education;",
|
||||
AppCategory::Medical => "Science;MedicalSoftware;",
|
||||
AppCategory::Music => "AudioVideo;Audio;Music;",
|
||||
AppCategory::News => "Network;News;",
|
||||
AppCategory::Photography => "Graphics;Photography;",
|
||||
AppCategory::Productivity => "Office;",
|
||||
AppCategory::Reference => "Education;",
|
||||
AppCategory::SocialNetworking => "Network;",
|
||||
AppCategory::Sports => "Education;Sports;",
|
||||
AppCategory::Travel => "Education;",
|
||||
AppCategory::Utility => "Utility;",
|
||||
AppCategory::Video => "AudioVideo;Video;",
|
||||
AppCategory::Weather => "Science;",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map to macOS LSApplicationCategoryType.
|
||||
pub(crate) fn macos_application_category_type(self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "public.app-category.business",
|
||||
AppCategory::DeveloperTool => "public.app-category.developer-tools",
|
||||
AppCategory::Education => "public.app-category.education",
|
||||
AppCategory::Entertainment => "public.app-category.entertainment",
|
||||
AppCategory::Finance => "public.app-category.finance",
|
||||
AppCategory::Game => "public.app-category.games",
|
||||
AppCategory::ActionGame => "public.app-category.action-games",
|
||||
AppCategory::AdventureGame => "public.app-category.adventure-games",
|
||||
AppCategory::ArcadeGame => "public.app-category.arcade-games",
|
||||
AppCategory::BoardGame => "public.app-category.board-games",
|
||||
AppCategory::CardGame => "public.app-category.card-games",
|
||||
AppCategory::CasinoGame => "public.app-category.casino-games",
|
||||
AppCategory::DiceGame => "public.app-category.dice-games",
|
||||
AppCategory::EducationalGame => "public.app-category.educational-games",
|
||||
AppCategory::FamilyGame => "public.app-category.family-games",
|
||||
AppCategory::KidsGame => "public.app-category.kids-games",
|
||||
AppCategory::MusicGame => "public.app-category.music-games",
|
||||
AppCategory::PuzzleGame => "public.app-category.puzzle-games",
|
||||
AppCategory::RacingGame => "public.app-category.racing-games",
|
||||
AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
|
||||
AppCategory::SimulationGame => "public.app-category.simulation-games",
|
||||
AppCategory::SportsGame => "public.app-category.sports-games",
|
||||
AppCategory::StrategyGame => "public.app-category.strategy-games",
|
||||
AppCategory::TriviaGame => "public.app-category.trivia-games",
|
||||
AppCategory::WordGame => "public.app-category.word-games",
|
||||
AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
|
||||
AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
|
||||
AppCategory::Lifestyle => "public.app-category.lifestyle",
|
||||
AppCategory::Medical => "public.app-category.medical",
|
||||
AppCategory::Music => "public.app-category.music",
|
||||
AppCategory::News => "public.app-category.news",
|
||||
AppCategory::Photography => "public.app-category.photography",
|
||||
AppCategory::Productivity => "public.app-category.productivity",
|
||||
AppCategory::Reference => "public.app-category.reference",
|
||||
AppCategory::SocialNetworking => "public.app-category.social-networking",
|
||||
AppCategory::Sports => "public.app-category.sports",
|
||||
AppCategory::Travel => "public.app-category.travel",
|
||||
AppCategory::Utility => "public.app-category.utilities",
|
||||
AppCategory::Video => "public.app-category.video",
|
||||
AppCategory::Weather => "public.app-category.weather",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
|
||||
("actiongame", AppCategory::ActionGame),
|
||||
("actiongames", AppCategory::ActionGame),
|
||||
("adventuregame", AppCategory::AdventureGame),
|
||||
("adventuregames", AppCategory::AdventureGame),
|
||||
("arcadegame", AppCategory::ArcadeGame),
|
||||
("arcadegames", AppCategory::ArcadeGame),
|
||||
("boardgame", AppCategory::BoardGame),
|
||||
("boardgames", AppCategory::BoardGame),
|
||||
("business", AppCategory::Business),
|
||||
("cardgame", AppCategory::CardGame),
|
||||
("cardgames", AppCategory::CardGame),
|
||||
("casinogame", AppCategory::CasinoGame),
|
||||
("casinogames", AppCategory::CasinoGame),
|
||||
("developer", AppCategory::DeveloperTool),
|
||||
("developertool", AppCategory::DeveloperTool),
|
||||
("developertools", AppCategory::DeveloperTool),
|
||||
("development", AppCategory::DeveloperTool),
|
||||
("dicegame", AppCategory::DiceGame),
|
||||
("dicegames", AppCategory::DiceGame),
|
||||
("education", AppCategory::Education),
|
||||
("educationalgame", AppCategory::EducationalGame),
|
||||
("educationalgames", AppCategory::EducationalGame),
|
||||
("entertainment", AppCategory::Entertainment),
|
||||
("familygame", AppCategory::FamilyGame),
|
||||
("familygames", AppCategory::FamilyGame),
|
||||
("finance", AppCategory::Finance),
|
||||
("fitness", AppCategory::HealthcareAndFitness),
|
||||
("game", AppCategory::Game),
|
||||
("games", AppCategory::Game),
|
||||
("graphicdesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsanddesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsdesign", AppCategory::GraphicsAndDesign),
|
||||
("healthcareandfitness", AppCategory::HealthcareAndFitness),
|
||||
("healthcarefitness", AppCategory::HealthcareAndFitness),
|
||||
("kidsgame", AppCategory::KidsGame),
|
||||
("kidsgames", AppCategory::KidsGame),
|
||||
("lifestyle", AppCategory::Lifestyle),
|
||||
("logicgame", AppCategory::PuzzleGame),
|
||||
("medical", AppCategory::Medical),
|
||||
("music", AppCategory::Music),
|
||||
("musicgame", AppCategory::MusicGame),
|
||||
("musicgames", AppCategory::MusicGame),
|
||||
("news", AppCategory::News),
|
||||
("photography", AppCategory::Photography),
|
||||
("productivity", AppCategory::Productivity),
|
||||
("puzzlegame", AppCategory::PuzzleGame),
|
||||
("puzzlegames", AppCategory::PuzzleGame),
|
||||
("racinggame", AppCategory::RacingGame),
|
||||
("racinggames", AppCategory::RacingGame),
|
||||
("reference", AppCategory::Reference),
|
||||
("roleplaying", AppCategory::RolePlayingGame),
|
||||
("roleplayinggame", AppCategory::RolePlayingGame),
|
||||
("roleplayinggames", AppCategory::RolePlayingGame),
|
||||
("rpg", AppCategory::RolePlayingGame),
|
||||
("simulationgame", AppCategory::SimulationGame),
|
||||
("simulationgames", AppCategory::SimulationGame),
|
||||
("socialnetwork", AppCategory::SocialNetworking),
|
||||
("socialnetworking", AppCategory::SocialNetworking),
|
||||
("sports", AppCategory::Sports),
|
||||
("sportsgame", AppCategory::SportsGame),
|
||||
("sportsgames", AppCategory::SportsGame),
|
||||
("strategygame", AppCategory::StrategyGame),
|
||||
("strategygames", AppCategory::StrategyGame),
|
||||
("travel", AppCategory::Travel),
|
||||
("triviagame", AppCategory::TriviaGame),
|
||||
("triviagames", AppCategory::TriviaGame),
|
||||
("utilities", AppCategory::Utility),
|
||||
("utility", AppCategory::Utility),
|
||||
("video", AppCategory::Video),
|
||||
("weather", AppCategory::Weather),
|
||||
("wordgame", AppCategory::WordGame),
|
||||
("wordgames", AppCategory::WordGame),
|
||||
];
|
||||
|
||||
/// Recursively copy a directory tree.
|
||||
///
|
||||
/// Preserves symlinks on unix targets and falls back to copying link targets on non-unix.
|
||||
pub(crate) fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
|
||||
std::fs::create_dir_all(dest)?;
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let source_path = entry.path();
|
||||
let dest_path = dest.join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
copy_dir_recursive(&source_path, &dest_path)?;
|
||||
} else if file_type.is_symlink() {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let target = std::fs::read_link(&source_path)?;
|
||||
std::os::unix::fs::symlink(&target, &dest_path)?;
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
std::fs::copy(&source_path, &dest_path)?;
|
||||
}
|
||||
} else {
|
||||
std::fs::copy(&source_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively zip a directory tree while preserving relative paths and Unix modes.
|
||||
pub(crate) fn zip_dir_recursive(src: &Path, dest: &Path) -> Result<()> {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
let file =
|
||||
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
for entry in walkdir::WalkDir::new(src) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let relative = path
|
||||
.strip_prefix(src)
|
||||
.with_context(|| format!("Failed to strip prefix for {}", path.display()))?;
|
||||
|
||||
if relative.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let archive_path = relative.to_string_lossy().replace('\\', "/");
|
||||
let metadata = entry.metadata()?;
|
||||
#[cfg(unix)]
|
||||
let mode = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
metadata.permissions().mode()
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let mode = if metadata.is_dir() { 0o755 } else { 0o644 };
|
||||
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated)
|
||||
.unix_permissions(mode);
|
||||
|
||||
if metadata.is_dir() {
|
||||
zip.add_directory(format!("{archive_path}/"), options)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
zip.start_file(&archive_path, options)?;
|
||||
let mut src_file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
src_file.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&buffer)?;
|
||||
}
|
||||
|
||||
zip.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::zip_dir_recursive;
|
||||
|
||||
#[test]
|
||||
fn zip_dir_preserves_layout_and_permissions() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let src = temp.path().join("src");
|
||||
let payload = src.join("Payload/Test.app");
|
||||
std::fs::create_dir_all(&payload).unwrap();
|
||||
|
||||
let exec = payload.join("runner");
|
||||
std::fs::write(&exec, "hello").unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&exec, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
let zip_path = temp.path().join("bundle.zip");
|
||||
zip_dir_recursive(&src, &zip_path).unwrap();
|
||||
|
||||
let file = std::fs::File::open(&zip_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(file).unwrap();
|
||||
|
||||
let entry = archive.by_name("Payload/Test.app/runner").unwrap();
|
||||
let expected_mode = if cfg!(unix) { 0o755 } else { 0o644 };
|
||||
assert_eq!(
|
||||
entry.unix_mode().map(|mode| mode & 0o777),
|
||||
Some(expected_mode)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//! Tool downloading and caching for external bundling tools.
|
||||
//!
|
||||
//! All downloads happen upfront via `resolve_tools()` before any bundling starts.
|
||||
//! This keeps tool downloads out of the bundle format modules.
|
||||
|
||||
use super::Arch;
|
||||
use crate::{PackageType, WebviewInstallMode, WindowsSettings};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
/// NSIS download URL and expected hash
|
||||
const NSIS_URL: &str =
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip";
|
||||
const NSIS_SHA1: &str = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D";
|
||||
|
||||
/// WiX download URL and expected hash
|
||||
const WIX_URL: &str =
|
||||
"https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip";
|
||||
const WIX_SHA256: &str = "6ac824e1642d6f7277d0ed7ea09411a508f6116ba6fae0aa5f2c7daa2ff43d31";
|
||||
|
||||
/// linuxdeploy download base URL
|
||||
const LINUXDEPLOY_URL_BASE: &str =
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy";
|
||||
|
||||
/// WebView2 download URLs.
|
||||
const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
|
||||
const WEBVIEW2_X64_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
|
||||
const WEBVIEW2_X86_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
|
||||
const WEBVIEW2_ARM64_INSTALLER_URL: &str = "https://go.microsoft.com/fwlink/?linkid=2099617";
|
||||
|
||||
/// Pre-resolved tool paths. All downloads happen before bundling starts.
|
||||
pub(crate) struct ResolvedTools {
|
||||
/// Path to the NSIS directory (contains makensis). Set if NSIS bundling is requested.
|
||||
pub nsis_dir: Option<PathBuf>,
|
||||
/// Path to the WiX directory (contains candle.exe, light.exe). Set if MSI bundling is requested.
|
||||
pub wix_dir: Option<PathBuf>,
|
||||
/// Path to the linuxdeploy binary. Set if AppImage bundling is requested.
|
||||
pub linuxdeploy: Option<PathBuf>,
|
||||
/// Path to a downloaded WebView2 bootstrapper or offline installer, if needed by NSIS.
|
||||
pub webview2_installer: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Resolve and download all tools needed for the given package types.
|
||||
///
|
||||
/// This must be called before `bundle_project()` so that no HTTP calls happen
|
||||
/// during the actual bundling phase.
|
||||
pub(crate) async fn resolve_tools(
|
||||
tools_dir: &Path,
|
||||
package_types: &[PackageType],
|
||||
windows_settings: &WindowsSettings,
|
||||
arch: Arch,
|
||||
) -> Result<ResolvedTools> {
|
||||
let mut resolved = ResolvedTools {
|
||||
nsis_dir: None,
|
||||
wix_dir: None,
|
||||
linuxdeploy: None,
|
||||
webview2_installer: None,
|
||||
};
|
||||
|
||||
for pt in package_types {
|
||||
match pt {
|
||||
PackageType::Nsis => {
|
||||
resolved.nsis_dir = Some(ensure_nsis(tools_dir).await?);
|
||||
resolved.webview2_installer =
|
||||
resolve_webview2(tools_dir, windows_settings, arch).await?;
|
||||
}
|
||||
PackageType::WindowsMsi => {
|
||||
resolved.wix_dir = Some(ensure_wix(tools_dir).await?);
|
||||
}
|
||||
PackageType::AppImage => {
|
||||
let linuxdeploy_arch = arch.linuxdeploy_arch();
|
||||
resolved.linuxdeploy = Some(ensure_linuxdeploy(tools_dir, linuxdeploy_arch).await?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// Determine if a WebView2 installer needs to be downloaded based on NSIS settings,
|
||||
/// and download it if so.
|
||||
async fn resolve_webview2(
|
||||
tools_dir: &Path,
|
||||
settings: &WindowsSettings,
|
||||
arch: Arch,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
let mode = &settings.webview_install_mode;
|
||||
|
||||
match mode {
|
||||
WebviewInstallMode::Skip | WebviewInstallMode::FixedRuntime { .. } => Ok(None),
|
||||
WebviewInstallMode::DownloadBootstrapper { .. }
|
||||
| WebviewInstallMode::EmbedBootstrapper { .. } => {
|
||||
Ok(Some(download_webview2_bootstrapper(tools_dir).await?))
|
||||
}
|
||||
WebviewInstallMode::OfflineInstaller { .. } => {
|
||||
let arch_str = arch.windows_arch();
|
||||
Ok(Some(
|
||||
download_webview2_offline_installer(tools_dir, arch_str).await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_nsis(tools_dir: &Path) -> Result<PathBuf> {
|
||||
let nsis_dir = tools_dir.join("nsis-3.11");
|
||||
let makensis = if cfg!(target_os = "windows") {
|
||||
nsis_dir.join("makensis.exe")
|
||||
} else {
|
||||
nsis_dir.join("makensis")
|
||||
};
|
||||
|
||||
if makensis.exists() {
|
||||
return Ok(nsis_dir);
|
||||
}
|
||||
|
||||
if cfg!(feature = "no-downloads") {
|
||||
bail!("NSIS not found and automatic downloads are disabled. Install NSIS manually.");
|
||||
}
|
||||
|
||||
tracing::info!("Downloading NSIS...");
|
||||
|
||||
let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgo::Sha1).await?;
|
||||
extract_zip(&data, tools_dir)?;
|
||||
|
||||
if !makensis.exists() {
|
||||
bail!(
|
||||
"NSIS extraction succeeded but makensis not found at {}",
|
||||
makensis.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let _ = std::fs::set_permissions(&makensis, std::fs::Permissions::from_mode(0o755));
|
||||
|
||||
Ok(nsis_dir)
|
||||
}
|
||||
|
||||
async fn ensure_wix(tools_dir: &Path) -> Result<PathBuf> {
|
||||
let wix_dir = tools_dir.join("wix314");
|
||||
let candle = wix_dir.join("candle.exe");
|
||||
|
||||
if candle.exists() {
|
||||
return Ok(wix_dir);
|
||||
}
|
||||
|
||||
if cfg!(feature = "no-downloads") {
|
||||
bail!("WiX not found and automatic downloads are disabled. Install WiX manually.");
|
||||
}
|
||||
|
||||
tracing::info!("Downloading WiX toolset...");
|
||||
let data = download_and_verify(WIX_URL, WIX_SHA256, HashAlgo::Sha256).await?;
|
||||
|
||||
std::fs::create_dir_all(&wix_dir)?;
|
||||
extract_zip(&data, &wix_dir)?;
|
||||
|
||||
if !candle.exists() {
|
||||
bail!(
|
||||
"WiX extraction succeeded but candle.exe not found at {}",
|
||||
candle.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(wix_dir)
|
||||
}
|
||||
|
||||
async fn ensure_linuxdeploy(tools_dir: &Path, arch: &str) -> Result<PathBuf> {
|
||||
let linuxdeploy_name = format!("linuxdeploy-{arch}.AppImage");
|
||||
let linuxdeploy_path = tools_dir.join(&linuxdeploy_name);
|
||||
|
||||
if linuxdeploy_path.exists() {
|
||||
return Ok(linuxdeploy_path);
|
||||
}
|
||||
|
||||
if cfg!(feature = "no-downloads") {
|
||||
bail!(
|
||||
"linuxdeploy not found and automatic downloads are disabled. Install linuxdeploy manually."
|
||||
);
|
||||
}
|
||||
|
||||
let url = format!("{LINUXDEPLOY_URL_BASE}/{linuxdeploy_name}");
|
||||
tracing::info!("Downloading linuxdeploy from {url}...");
|
||||
|
||||
let data = download_bytes(&url).await?;
|
||||
std::fs::create_dir_all(tools_dir)?;
|
||||
std::fs::write(&linuxdeploy_path, &data)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
std::fs::set_permissions(&linuxdeploy_path, std::fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
Ok(linuxdeploy_path)
|
||||
}
|
||||
|
||||
async fn download_webview2_bootstrapper(tools_dir: &Path) -> Result<PathBuf> {
|
||||
let path = tools_dir.join("MicrosoftEdgeWebview2Setup.exe");
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
tracing::info!("Downloading WebView2 bootstrapper...");
|
||||
let data = download_bytes(WEBVIEW2_BOOTSTRAPPER_URL).await?;
|
||||
std::fs::create_dir_all(tools_dir)?;
|
||||
std::fs::write(&path, &data).context("Failed to write WebView2 bootstrapper")?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn download_webview2_offline_installer(tools_dir: &Path, arch: &str) -> Result<PathBuf> {
|
||||
let name = format!("MicrosoftEdgeWebView2RuntimeInstaller_{arch}.exe");
|
||||
let path = tools_dir.join(&name);
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
let url = match arch {
|
||||
"x64" => WEBVIEW2_X64_INSTALLER_URL,
|
||||
"x86" => WEBVIEW2_X86_INSTALLER_URL,
|
||||
"arm64" => WEBVIEW2_ARM64_INSTALLER_URL,
|
||||
_ => bail!("Unsupported architecture for WebView2 offline installer: {arch}"),
|
||||
};
|
||||
tracing::info!("Downloading WebView2 offline installer for {arch}...");
|
||||
let data = download_bytes(url).await?;
|
||||
std::fs::create_dir_all(tools_dir)?;
|
||||
std::fs::write(&path, &data).context("Failed to write WebView2 offline installer")?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
enum HashAlgo {
|
||||
Sha1,
|
||||
Sha256,
|
||||
}
|
||||
|
||||
async fn download_and_verify(url: &str, expected_hash: &str, algo: HashAlgo) -> Result<Vec<u8>> {
|
||||
let data = download_bytes(url).await?;
|
||||
|
||||
let computed = match algo {
|
||||
HashAlgo::Sha1 => {
|
||||
use sha1::Digest;
|
||||
let mut hasher = sha1::Sha1::new();
|
||||
hasher.update(&data);
|
||||
format!("{:X}", hasher.finalize())
|
||||
}
|
||||
HashAlgo::Sha256 => {
|
||||
use sha2::Digest;
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&data);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
};
|
||||
|
||||
if computed.to_uppercase() != expected_hash.to_uppercase() {
|
||||
bail!("Hash mismatch for {url}: expected {expected_hash}, got {computed}");
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Download bytes from a URL using the async reqwest client.
|
||||
pub(crate) async fn download_bytes(url: &str) -> Result<Vec<u8>> {
|
||||
let response = reqwest::get(url)
|
||||
.await
|
||||
.with_context(|| format!("Failed to download {url}"))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
bail!("Download failed with status {}: {url}", response.status());
|
||||
}
|
||||
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map(|b| b.to_vec())
|
||||
.with_context(|| format!("Failed to read response body from {url}"))
|
||||
}
|
||||
|
||||
/// Extract a zip archive to a directory.
|
||||
fn extract_zip(data: &[u8], dest: &Path) -> Result<()> {
|
||||
use std::io::Cursor;
|
||||
|
||||
let cursor = Cursor::new(data);
|
||||
let mut archive = zip::ZipArchive::new(cursor).context("Failed to read zip archive")?;
|
||||
|
||||
std::fs::create_dir_all(dest)?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let outpath = dest.join(file.mangled_name());
|
||||
|
||||
if file.name().ends_with('/') {
|
||||
std::fs::create_dir_all(&outpath)?;
|
||||
} else {
|
||||
if let Some(parent) = outpath.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut outfile = std::fs::File::create(&outpath)?;
|
||||
std::io::copy(&mut file, &mut outfile)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Updater bundle creation.
|
||||
//!
|
||||
//! Creates zip/tar.gz archives of bundle artifacts for auto-update distribution.
|
||||
|
||||
use super::{Bundle, BundleContext};
|
||||
use crate::PackageType;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
fs::File,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
impl BundleContext<'_> {
|
||||
/// Repackage previously-built bundle artifacts into updater-friendly archives.
|
||||
///
|
||||
/// This step never builds the application itself. Instead, it consumes the
|
||||
/// [`Bundle`] records produced earlier in the main bundling pass and wraps those
|
||||
/// artifacts into archive formats that are convenient for update distribution.
|
||||
///
|
||||
/// Archive mapping:
|
||||
/// - macOS `.app` bundles become `.app.tar.gz` archives so the bundle directory
|
||||
/// layout is preserved exactly.
|
||||
/// - Windows installers (`.msi` and NSIS `.exe`) become single-file `.zip`
|
||||
/// archives.
|
||||
/// - Linux artifacts (`.AppImage` and `.deb`) become single-file `.tar.gz`
|
||||
/// archives.
|
||||
///
|
||||
/// All outputs are written to `project_out_directory()/bundle/updater`. This
|
||||
/// method assumes the input bundles are already finalized and signed as needed; it
|
||||
/// performs no platform-specific mutation beyond wrapping them in the selected
|
||||
/// archive container.
|
||||
pub(crate) async fn bundle_updater(&self, bundles: &[Bundle]) -> Result<Vec<PathBuf>> {
|
||||
let mut updater_paths = Vec::new();
|
||||
let output_dir = self.project_out_directory().join("bundle").join("updater");
|
||||
std::fs::create_dir_all(&output_dir)?;
|
||||
|
||||
for bundle in bundles {
|
||||
match bundle.package_type {
|
||||
PackageType::MacOsBundle => {
|
||||
// Create .tar.gz of the .app bundle
|
||||
for app_path in &bundle.bundle_paths {
|
||||
let tar_path = output_dir.join(format!(
|
||||
"{}_{}.app.tar.gz",
|
||||
self.product_name(),
|
||||
self.version_string()
|
||||
));
|
||||
create_tar_gz(app_path, &tar_path)?;
|
||||
tracing::info!("Created updater archive: {}", tar_path.display());
|
||||
updater_paths.push(tar_path);
|
||||
}
|
||||
}
|
||||
PackageType::Nsis | PackageType::WindowsMsi => {
|
||||
// Create .zip of the installer
|
||||
for installer_path in &bundle.bundle_paths {
|
||||
let zip_path = output_dir.join(format!(
|
||||
"{}.zip",
|
||||
installer_path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
));
|
||||
create_zip(installer_path, &zip_path)?;
|
||||
tracing::info!("Created updater archive: {}", zip_path.display());
|
||||
updater_paths.push(zip_path);
|
||||
}
|
||||
}
|
||||
PackageType::AppImage | PackageType::Deb => {
|
||||
// Create .tar.gz of the artifact
|
||||
for artifact_path in &bundle.bundle_paths {
|
||||
let tar_path = output_dir.join(format!(
|
||||
"{}.tar.gz",
|
||||
artifact_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
));
|
||||
create_tar_gz_single_file(artifact_path, &tar_path)?;
|
||||
tracing::info!("Created updater archive: {}", tar_path.display());
|
||||
updater_paths.push(tar_path);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updater_paths)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a .tar.gz of a directory (e.g., a .app bundle).
|
||||
fn create_tar_gz(src_dir: &Path, dest: &Path) -> Result<()> {
|
||||
let file =
|
||||
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
|
||||
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let mut tar = tar::Builder::new(enc);
|
||||
|
||||
let dir_name = src_dir
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
tar.append_dir_all(&dir_name, src_dir)
|
||||
.with_context(|| format!("Failed to add {} to tar", src_dir.display()))?;
|
||||
|
||||
tar.into_inner()?.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a .tar.gz containing a single file.
|
||||
fn create_tar_gz_single_file(src_file: &Path, dest: &Path) -> Result<()> {
|
||||
let file =
|
||||
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
|
||||
let enc = flate2::write::GzEncoder::new(file, flate2::Compression::default());
|
||||
let mut tar = tar::Builder::new(enc);
|
||||
|
||||
let file_name = src_file
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
tar.append_path_with_name(src_file, &file_name)
|
||||
.with_context(|| format!("Failed to add {} to tar", src_file.display()))?;
|
||||
|
||||
tar.into_inner()?.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a .zip containing a single file.
|
||||
fn create_zip(src_file: &Path, dest: &Path) -> Result<()> {
|
||||
let file =
|
||||
File::create(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
|
||||
let mut zip = zip::ZipWriter::new(file);
|
||||
|
||||
let file_name = src_file
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
zip.start_file(&file_name, options)?;
|
||||
let mut src = File::open(src_file)?;
|
||||
io::copy(&mut src, &mut zip)?;
|
||||
|
||||
zip.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -67,43 +67,43 @@ fn extend_manifest_config_toml(manifest: &mut Manifest, path: &Path) {
|
||||
/// Merge the new profiles into the target profiles. Keep the existing values if they exist.
|
||||
fn merge_profiles(target: &mut Profiles, new: Profiles) {
|
||||
if let Some(new_release) = new.release {
|
||||
if target.release.is_none() {
|
||||
target.release = Some(new_release);
|
||||
if let Some(profile) = target.release.as_mut() {
|
||||
merge_profile(profile, new_release);
|
||||
} else {
|
||||
merge_profile(target.release.as_mut().unwrap(), new_release);
|
||||
target.release = Some(new_release);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_dev) = new.dev {
|
||||
if target.dev.is_none() {
|
||||
target.dev = Some(new_dev);
|
||||
if let Some(profile) = target.dev.as_mut() {
|
||||
merge_profile(profile, new_dev);
|
||||
} else {
|
||||
merge_profile(target.dev.as_mut().unwrap(), new_dev);
|
||||
target.dev = Some(new_dev);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_test) = new.test {
|
||||
if target.test.is_none() {
|
||||
target.test = Some(new_test);
|
||||
if let Some(profile) = target.test.as_mut() {
|
||||
merge_profile(profile, new_test);
|
||||
} else {
|
||||
merge_profile(target.test.as_mut().unwrap(), new_test);
|
||||
target.test = Some(new_test);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_bench) = new.bench {
|
||||
if target.bench.is_none() {
|
||||
target.bench = Some(new_bench);
|
||||
if let Some(profile) = target.bench.as_mut() {
|
||||
merge_profile(profile, new_bench);
|
||||
} else {
|
||||
merge_profile(target.bench.as_mut().unwrap(), new_bench);
|
||||
target.bench = Some(new_bench);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
if let Some(new_doc) = new.doc {
|
||||
if target.doc.is_none() {
|
||||
target.doc = Some(new_doc);
|
||||
if let Some(profile) = target.doc.as_mut() {
|
||||
merge_profile(profile, new_doc);
|
||||
} else {
|
||||
merge_profile(target.doc.as_mut().unwrap(), new_doc);
|
||||
target.doc = Some(new_doc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::metadata::{
|
||||
use super::metadata::{
|
||||
AnyLoopInfo, AsyncInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo,
|
||||
WhileInfo,
|
||||
};
|
||||
@@ -229,7 +229,7 @@ impl std::fmt::Display for Issue {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::check_file;
|
||||
use crate::check::check_file;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -168,6 +168,7 @@ pub struct Span {
|
||||
}
|
||||
|
||||
impl Span {
|
||||
#[cfg(test)]
|
||||
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
|
||||
let mut lines = source_text.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
mod issues;
|
||||
mod metadata;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use syn::{spanned::Spanned, visit::Visit, Pat};
|
||||
|
||||
use crate::{
|
||||
use {
|
||||
issues::{Issue, IssueReport},
|
||||
metadata::{
|
||||
AnyLoopInfo, AsyncInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo,
|
||||
@@ -289,7 +291,7 @@ impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::metadata::{
|
||||
use crate::check::metadata::{
|
||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
|
||||
MatchInfo, Span, WhileInfo,
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{fs::create_dir_all, path::PathBuf};
|
||||
|
||||
use crate::opt::process_file_to;
|
||||
use crate::{extract_assets_from_file, Result, StructuredOutput};
|
||||
use clap::Parser;
|
||||
use dioxus_cli_opt::process_file_to;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
@@ -28,7 +28,7 @@ impl BuildAssets {
|
||||
destination_path.display(),
|
||||
asset
|
||||
);
|
||||
process_file_to(asset.options(), &source_path, &destination_path)?;
|
||||
process_file_to(asset.options(), &source_path, &destination_path, None)?;
|
||||
}
|
||||
|
||||
Ok(StructuredOutput::Success)
|
||||
|
||||
+177
-162
@@ -1,10 +1,10 @@
|
||||
use crate::{AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat};
|
||||
use anyhow::{bail, Context};
|
||||
use crate::{
|
||||
AppBuilder, BuildArgs, BuildId, BuildMode, BuildRequest, BundleFormat, PackageType, Platform,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use path_absolutize::Absolutize;
|
||||
use std::collections::HashMap;
|
||||
use tauri_bundler::{BundleBinary, BundleSettings, PackageSettings, SettingsBuilder};
|
||||
|
||||
use walkdir::WalkDir;
|
||||
use std::path::PathBuf;
|
||||
use target_lexicon::Triple;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -15,7 +15,7 @@ use super::*;
|
||||
pub struct Bundle {
|
||||
/// The package types to bundle
|
||||
#[clap(long)]
|
||||
pub package_types: Option<Vec<crate::PackageType>>,
|
||||
pub package_types: Option<Vec<PackageType>>,
|
||||
|
||||
/// The directory in which the final bundle will be placed.
|
||||
///
|
||||
@@ -37,15 +37,14 @@ impl Bundle {
|
||||
pub(crate) async fn bundle(mut self) -> Result<StructuredOutput> {
|
||||
tracing::info!("Bundling project...");
|
||||
|
||||
self.force_ios_bundle_device_target_if_needed()?;
|
||||
let BuildTargets { client, server } = self.args.into_targets().await?;
|
||||
|
||||
let mut server_artifacts = None;
|
||||
let client_artifacts =
|
||||
AppBuilder::started(&client, BuildMode::Base { run: false }, BuildId::PRIMARY)?
|
||||
.finish_build()
|
||||
.await?;
|
||||
|
||||
tracing::info!(path = ?client.root_dir(), "Client build completed successfully! 🚀");
|
||||
let mut server_artifacts = None;
|
||||
|
||||
if let Some(server) = server.as_ref() {
|
||||
// If the server is present, we need to build it as well
|
||||
@@ -58,11 +57,13 @@ impl Bundle {
|
||||
tracing::info!(path = ?client.root_dir(), "Server build completed successfully! 🚀");
|
||||
}
|
||||
|
||||
// If we're building for iOS, we need to bundle the iOS bundle
|
||||
if client.bundle == BundleFormat::Ios && self.package_types.is_none() {
|
||||
self.package_types = Some(vec![crate::PackageType::IosBundle]);
|
||||
// Fill platform-specific defaults for package types when omitted.
|
||||
if self.package_types.is_none() {
|
||||
self.package_types = Self::default_package_types(client.bundle);
|
||||
}
|
||||
|
||||
Self::validate_package_types_for_bundle(client.bundle, self.package_types.as_deref())?;
|
||||
|
||||
let mut bundles = vec![];
|
||||
|
||||
// Copy the server over if it exists
|
||||
@@ -70,31 +71,24 @@ impl Bundle {
|
||||
bundles.push(server.main_exe());
|
||||
}
|
||||
|
||||
// Create a list of bundles that we might need to copy
|
||||
// Create a list of bundles that we might need to copy.
|
||||
// Package-type based bundling is handled by the bundler module.
|
||||
match client.bundle {
|
||||
// By default, mac/win/linux work with tauri bundle
|
||||
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows => {
|
||||
tracing::info!("Running desktop bundler...");
|
||||
for bundle in Self::bundle_desktop(&client, &self.package_types)? {
|
||||
// Desktop and Android platforms use package-type dispatch in the bundler module.
|
||||
BundleFormat::MacOS
|
||||
| BundleFormat::Ios
|
||||
| BundleFormat::Linux
|
||||
| BundleFormat::Windows
|
||||
| BundleFormat::Android => {
|
||||
tracing::info!("Running package bundler...");
|
||||
for bundle in Self::bundle_with_package_types(&client, &self.package_types).await? {
|
||||
bundles.extend(bundle.bundle_paths);
|
||||
}
|
||||
}
|
||||
|
||||
// Web/ios can just use their root_dir
|
||||
// Web can use its root_dir directly.
|
||||
BundleFormat::Web => bundles.push(client.root_dir()),
|
||||
BundleFormat::Ios => {
|
||||
tracing::warn!("iOS bundles are not currently codesigned! You will need to codesign the app before distributing.");
|
||||
bundles.push(client.root_dir())
|
||||
}
|
||||
BundleFormat::Server => bundles.push(client.root_dir()),
|
||||
|
||||
BundleFormat::Android => {
|
||||
let aab = client
|
||||
.android_gradle_bundle()
|
||||
.await
|
||||
.context("Failed to run gradle bundleRelease")?;
|
||||
bundles.push(aab);
|
||||
}
|
||||
};
|
||||
|
||||
// Copy the bundles to the output directory if one was specified
|
||||
@@ -148,147 +142,45 @@ impl Bundle {
|
||||
})
|
||||
}
|
||||
|
||||
fn bundle_desktop(
|
||||
async fn bundle_with_package_types(
|
||||
build: &BuildRequest,
|
||||
package_types: &Option<Vec<crate::PackageType>>,
|
||||
) -> Result<Vec<tauri_bundler::Bundle>, Error> {
|
||||
let krate = &build;
|
||||
let exe = build.main_exe();
|
||||
package_types: &Option<Vec<PackageType>>,
|
||||
) -> Result<Vec<crate::bundler::Bundle>, Error> {
|
||||
use anyhow::bail;
|
||||
|
||||
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
|
||||
if matches!(
|
||||
build.bundle,
|
||||
BundleFormat::MacOS | BundleFormat::Linux | BundleFormat::Windows
|
||||
) {
|
||||
let krate = &build;
|
||||
let exe = build.main_exe();
|
||||
|
||||
let package = krate.package();
|
||||
let mut name: PathBuf = krate.executable_name().into();
|
||||
if cfg!(windows) {
|
||||
name.set_extension("exe");
|
||||
}
|
||||
std::fs::create_dir_all(krate.bundle_dir(build.bundle))
|
||||
.context("Failed to create bundle directory")?;
|
||||
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name))
|
||||
.with_context(|| "Failed to copy the output executable into the bundle directory")?;
|
||||
_ = std::fs::remove_dir_all(krate.bundle_dir(build.bundle));
|
||||
|
||||
let binaries = vec![
|
||||
// We use the name of the exe but it has to be in the same directory
|
||||
BundleBinary::new(krate.executable_name().to_string(), true)
|
||||
.set_src_path(Some(exe.display().to_string())),
|
||||
];
|
||||
let mut name: PathBuf = krate.executable_name().into();
|
||||
if cfg!(windows) {
|
||||
name.set_extension("exe");
|
||||
}
|
||||
std::fs::create_dir_all(krate.bundle_dir(build.bundle))
|
||||
.context("Failed to create bundle directory")?;
|
||||
std::fs::copy(&exe, krate.bundle_dir(build.bundle).join(&name)).with_context(|| {
|
||||
"Failed to copy the output executable into the bundle directory"
|
||||
})?;
|
||||
|
||||
let mut bundle_settings: BundleSettings = krate.config.bundle.clone().into();
|
||||
|
||||
// Check if required fields are provided instead of failing silently.
|
||||
if bundle_settings.identifier.is_none() {
|
||||
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
|
||||
}
|
||||
if bundle_settings.publisher.is_none() {
|
||||
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
|
||||
}
|
||||
|
||||
/// Resolve an icon path relative to the crate dir
|
||||
fn canonicalize_icon_path(build: &BuildRequest, icon: &mut String) -> Result<(), Error> {
|
||||
let icon_path = build
|
||||
.crate_dir()
|
||||
.join(&icon)
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize path to icon {icon:?}"))?;
|
||||
*icon = icon_path.to_string_lossy().to_string();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Resolve bundle.icon relative to the crate dir
|
||||
if let Some(icons) = bundle_settings.icon.as_mut() {
|
||||
for icon in icons.iter_mut() {
|
||||
canonicalize_icon_path(build, icon)?;
|
||||
// Check if required fields are provided instead of failing silently.
|
||||
if build.config.bundle.identifier.is_none() {
|
||||
bail!("\n\nBundle identifier was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\nidentifier = \"com.mycompany\"\n\n");
|
||||
}
|
||||
if build.config.bundle.publisher.is_none() {
|
||||
bail!("\n\nBundle publisher was not provided in `Dioxus.toml`. Add it as:\n\n[bundle]\npublisher = \"MyCompany\"\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
if cfg!(windows) {
|
||||
// Resolve bundle.windows.icon_path relative to the crate dir
|
||||
let mut windows_icon_path = bundle_settings
|
||||
.windows
|
||||
.icon_path
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
canonicalize_icon_path(build, &mut windows_icon_path)?;
|
||||
bundle_settings.windows.icon_path = PathBuf::from(&windows_icon_path);
|
||||
let ctx = crate::bundler::BundleContext::new(build, package_types).await?;
|
||||
|
||||
let windows_icon_override = krate.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());
|
||||
tracing::debug!("Bundling project for {:?}", ctx.package_types());
|
||||
|
||||
if let Some(icon_path) = icon_path {
|
||||
bundle_settings.icon = Some(vec![icon_path.into()]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if bundle_settings.resources_map.is_none() {
|
||||
bundle_settings.resources_map = Some(HashMap::new());
|
||||
}
|
||||
|
||||
let asset_dir = build.asset_dir();
|
||||
if asset_dir.exists() {
|
||||
for entry in WalkDir::new(&asset_dir) {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() {
|
||||
let old = path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize {entry:?}"))?;
|
||||
let new =
|
||||
PathBuf::from("assets").join(path.strip_prefix(&asset_dir).unwrap_or(path));
|
||||
|
||||
tracing::debug!("Bundled asset: {old:?} -> {new:?}");
|
||||
bundle_settings
|
||||
.resources_map
|
||||
.as_mut()
|
||||
.expect("to be set")
|
||||
.insert(old.display().to_string(), new.display().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for resource_path in bundle_settings.resources.take().into_iter().flatten() {
|
||||
bundle_settings
|
||||
.resources_map
|
||||
.as_mut()
|
||||
.expect("to be set")
|
||||
.insert(resource_path, "".to_string());
|
||||
}
|
||||
|
||||
let mut settings = SettingsBuilder::new()
|
||||
.project_out_directory(krate.bundle_dir(build.bundle))
|
||||
.package_settings(PackageSettings {
|
||||
product_name: krate.bundled_app_name(),
|
||||
version: package.version.to_string(),
|
||||
description: package.description.clone().unwrap_or_default(),
|
||||
homepage: Some(package.homepage.clone().unwrap_or_default()),
|
||||
authors: Some(package.authors.clone()),
|
||||
default_run: Some(name.display().to_string()),
|
||||
})
|
||||
.log_level(log::Level::Debug)
|
||||
.binaries(binaries)
|
||||
.bundle_settings(bundle_settings);
|
||||
|
||||
if let Some(packages) = &package_types {
|
||||
settings = settings.package_types(packages.iter().map(|p| (*p).into()).collect());
|
||||
}
|
||||
|
||||
settings = settings.target(build.triple.to_string());
|
||||
|
||||
let settings = settings
|
||||
.build()
|
||||
.context("failed to bundle tauri bundle settings")?;
|
||||
tracing::debug!("Bundling project with settings: {:#?}", settings);
|
||||
if cfg!(target_os = "macos") {
|
||||
std::env::set_var("CI", "true");
|
||||
}
|
||||
|
||||
let bundles = tauri_bundler::bundle::bundle_project(&settings).inspect_err(|err| {
|
||||
let bundles = ctx.bundle_project().await.inspect_err(|err| {
|
||||
tracing::error!("Failed to bundle project: {:#?}", err);
|
||||
if cfg!(target_os = "macos") {
|
||||
tracing::error!("Make 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)");
|
||||
@@ -297,4 +189,127 @@ impl Bundle {
|
||||
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
fn default_package_types(bundle: BundleFormat) -> Option<Vec<PackageType>> {
|
||||
match bundle {
|
||||
BundleFormat::Ios => Some(vec![crate::PackageType::Ipa]),
|
||||
BundleFormat::Android => Some(vec![crate::PackageType::Aab]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn force_ios_bundle_device_target_if_needed(&mut self) -> Result<()> {
|
||||
let client_args = self
|
||||
.args
|
||||
.client
|
||||
.as_mut()
|
||||
.map(|args| &mut args.build_arguments)
|
||||
.unwrap_or(&mut self.args.shared.build_arguments);
|
||||
|
||||
if client_args.platform == Platform::Ios && client_args.target.is_none() {
|
||||
client_args.target = Some("aarch64-apple-ios".parse::<Triple>()?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_package_types_for_bundle(
|
||||
bundle: BundleFormat,
|
||||
package_types: Option<&[PackageType]>,
|
||||
) -> Result<(), Error> {
|
||||
let Some(package_types) = package_types else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let is_package_supported = |package_type: &PackageType| -> bool {
|
||||
match bundle {
|
||||
BundleFormat::MacOS => matches!(
|
||||
package_type,
|
||||
PackageType::MacOsBundle | PackageType::Dmg | PackageType::Updater
|
||||
),
|
||||
BundleFormat::Linux => matches!(
|
||||
package_type,
|
||||
PackageType::Deb
|
||||
| PackageType::Rpm
|
||||
| PackageType::AppImage
|
||||
| PackageType::Updater
|
||||
),
|
||||
BundleFormat::Windows => {
|
||||
matches!(
|
||||
package_type,
|
||||
PackageType::WindowsMsi | PackageType::Nsis | PackageType::Updater
|
||||
)
|
||||
}
|
||||
BundleFormat::Android => {
|
||||
matches!(package_type, PackageType::Apk | PackageType::Aab)
|
||||
}
|
||||
BundleFormat::Ios => matches!(package_type, PackageType::IosApp | PackageType::Ipa),
|
||||
BundleFormat::Web | BundleFormat::Server => false,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(invalid) = package_types.iter().find(|pt| !is_package_supported(pt)) {
|
||||
anyhow::bail!(
|
||||
"Package type '{invalid:?}' is not supported for bundle format '{bundle}'."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Bundle;
|
||||
use crate::{
|
||||
cli::CommandWithPlatformOverrides, BuildArgs, BundleFormat, PackageType, Platform,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn ios_bundle_defaults_to_ipa() {
|
||||
assert_eq!(
|
||||
Bundle::default_package_types(BundleFormat::Ios),
|
||||
Some(vec![PackageType::Ipa])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_ios_package_types() {
|
||||
assert!(Bundle::validate_package_types_for_bundle(
|
||||
BundleFormat::Ios,
|
||||
Some(&[PackageType::IosApp, PackageType::Ipa]),
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
assert!(Bundle::validate_package_types_for_bundle(
|
||||
BundleFormat::Ios,
|
||||
Some(&[PackageType::Dmg]),
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundle_ios_defaults_to_device_target() {
|
||||
let mut bundle = Bundle {
|
||||
package_types: None,
|
||||
out_dir: None,
|
||||
args: CommandWithPlatformOverrides {
|
||||
shared: BuildArgs {
|
||||
build_arguments: crate::TargetArgs {
|
||||
platform: Platform::Ios,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
|
||||
bundle.force_ios_bundle_device_target_if_needed().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bundle.args.shared.build_arguments.target,
|
||||
Some("aarch64-apple-ios".parse().unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
|
||||
let res = tokio::spawn(async move {
|
||||
tokio::fs::read_to_string(&_path)
|
||||
.await
|
||||
.map(|contents| dioxus_check::check_file(_path, &contents))
|
||||
.map(|contents| crate::check::check_file(_path, &contents))
|
||||
})
|
||||
.await;
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ where
|
||||
{
|
||||
fn augment_args(cmd: clap::Command) -> clap::Command {
|
||||
T::augment_args(cmd).defer(|cmd| {
|
||||
PlatformOverrides::<Self>::augment_subcommands(cmd.disable_help_subcommand(true))
|
||||
PlatformOverrides::<T>::augment_subcommands(cmd.disable_help_subcommand(true))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,3 +182,62 @@ where
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn cli_parses_bundle_with_package_types_without_recursing() {
|
||||
let cli = crate::Cli::try_parse_from([
|
||||
"dx",
|
||||
"bundle",
|
||||
"--platform",
|
||||
"desktop",
|
||||
"--package-types",
|
||||
"msi",
|
||||
])
|
||||
.expect("bundle command should parse");
|
||||
|
||||
assert!(matches!(cli.action, crate::Commands::Bundle(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_parses_platform_override_subcommands_without_recursing() {
|
||||
let cli = crate::Cli::try_parse_from([
|
||||
"dx",
|
||||
"bundle",
|
||||
"--platform",
|
||||
"desktop",
|
||||
"@client",
|
||||
"--release",
|
||||
"@server",
|
||||
"--platform",
|
||||
"server",
|
||||
])
|
||||
.expect("platform override subcommands should parse");
|
||||
|
||||
let crate::Commands::Bundle(bundle) = cli.action else {
|
||||
panic!("expected bundle command");
|
||||
};
|
||||
|
||||
let client = bundle
|
||||
.args
|
||||
.client
|
||||
.as_ref()
|
||||
.expect("@client override should be captured");
|
||||
let server = bundle
|
||||
.args
|
||||
.server
|
||||
.as_ref()
|
||||
.expect("@server override should be captured");
|
||||
|
||||
assert!(!bundle.args.shared.build_arguments.release);
|
||||
assert!(client.build_arguments.release);
|
||||
assert_eq!(
|
||||
server.build_arguments.platform,
|
||||
crate::Platform::Server,
|
||||
"@server should override the platform"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -178,8 +178,6 @@ impl Print {
|
||||
///
|
||||
/// This function comes from shell-escape
|
||||
fn escape_windows(s: Cow<str>) -> Cow<str> {
|
||||
use std::iter::repeat;
|
||||
|
||||
let mut needs_escape = s.is_empty();
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
@@ -202,15 +200,15 @@ impl Print {
|
||||
|
||||
match chars.next() {
|
||||
Some('"') => {
|
||||
es.extend(repeat('\\').take(nslashes * 2 + 1));
|
||||
es.extend(std::iter::repeat_n('\\', nslashes * 2 + 1));
|
||||
es.push('"');
|
||||
}
|
||||
Some(c) => {
|
||||
es.extend(repeat('\\').take(nslashes));
|
||||
es.extend(std::iter::repeat_n('\\', nslashes));
|
||||
es.push(c);
|
||||
}
|
||||
None => {
|
||||
es.extend(repeat('\\').take(nslashes * 2));
|
||||
es.extend(std::iter::repeat_n('\\', nslashes * 2));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,10 +82,13 @@ impl RunArgs {
|
||||
current,
|
||||
total,
|
||||
krate,
|
||||
fresh,
|
||||
} => {
|
||||
tracing::debug!(
|
||||
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
|
||||
)
|
||||
if !fresh {
|
||||
tracing::debug!(
|
||||
"[{bundle_format}] ({current}/{total}) Compiling {krate} ",
|
||||
)
|
||||
}
|
||||
}
|
||||
BuildStage::RunningBindgen => {
|
||||
tracing::info!("[{bundle_format}] Running WASM bindgen")
|
||||
|
||||
@@ -121,7 +121,7 @@ pub(crate) struct WixSettings {
|
||||
pub(crate) fips_compliant: bool,
|
||||
/// MSI installer version in the format `major.minor.patch.build` (build is optional).
|
||||
///
|
||||
/// Because a valid version is required for MSI installer, it will be derived from [`tauri_bundler::PackageSettings::version`] if this field is not set.
|
||||
/// Because a valid version is required for MSI installer, it will be derived from the package version if this field is not set.
|
||||
///
|
||||
/// The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.
|
||||
/// The third and fourth fields have a maximum value of 65,535.
|
||||
@@ -132,10 +132,9 @@ pub(crate) struct WixSettings {
|
||||
/// A GUID upgrade code for MSI installer. This code **_must stay the same across all of your updates_**,
|
||||
/// otherwise, Windows will treat your update as a different app and your users will have duplicate versions of your app.
|
||||
///
|
||||
/// By default, tauri generates this code by generating a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
|
||||
/// You can use Tauri's CLI to generate and print this code for you by running `tauri inspect wix-upgrade-code`.
|
||||
/// By default, this code is generated as a Uuid v5 using the string `<productName>.exe.app.x64` in the DNS namespace.
|
||||
///
|
||||
/// It is recommended that you set this value in your tauri config file to avoid accidental changes in your upgrade code
|
||||
/// It is recommended that you set this value in your Dioxus.toml to avoid accidental changes in your upgrade code
|
||||
/// whenever you want to change your product name.
|
||||
#[serde(default)]
|
||||
#[schemars(with = "Option<String>")]
|
||||
@@ -146,28 +145,39 @@ pub(crate) struct WixSettings {
|
||||
pub(crate) struct MacOsSettings {
|
||||
#[serde(default)]
|
||||
pub(crate) bundle_version: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) frameworks: Option<Vec<String>>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) minimum_system_version: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) license: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) exception_domain: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) signing_identity: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) provider_short_name: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) entitlements: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) info_plist_path: Option<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) bundle_name: Option<String>,
|
||||
|
||||
/// List of custom files to add to the application bundle.
|
||||
/// Maps the path in the Contents directory in the app to the path of the file to include (relative to the current working directory).
|
||||
#[serde(default)]
|
||||
pub files: HashMap<PathBuf, PathBuf>,
|
||||
|
||||
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
|
||||
///
|
||||
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
|
||||
@@ -290,15 +300,19 @@ pub struct CustomSignCommandSettings {
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, clap::ValueEnum, Serialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum, Serialize)]
|
||||
pub(crate) enum PackageType {
|
||||
/// The macOS application bundle (.app).
|
||||
#[clap(name = "macos")]
|
||||
MacOsBundle,
|
||||
|
||||
/// The iOS app bundle.
|
||||
/// The raw iOS application bundle (.app).
|
||||
#[clap(name = "ios")]
|
||||
IosBundle,
|
||||
IosApp,
|
||||
|
||||
/// The distributable iOS application archive (.ipa).
|
||||
#[clap(name = "ipa")]
|
||||
Ipa,
|
||||
|
||||
/// The Windows bundle (.msi).
|
||||
#[clap(name = "msi")]
|
||||
@@ -327,6 +341,14 @@ pub(crate) enum PackageType {
|
||||
/// The Updater bundle (a patch of an existing app)
|
||||
#[clap(name = "updater")]
|
||||
Updater,
|
||||
|
||||
/// Android application package (.apk).
|
||||
#[clap(name = "apk")]
|
||||
Apk,
|
||||
|
||||
/// Android App Bundle (.aab).
|
||||
#[clap(name = "aab")]
|
||||
Aab,
|
||||
}
|
||||
|
||||
impl FromStr for PackageType {
|
||||
@@ -335,7 +357,8 @@ impl FromStr for PackageType {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"macos" => Ok(PackageType::MacOsBundle),
|
||||
"ios" => Ok(PackageType::IosBundle),
|
||||
"ios" => Ok(PackageType::IosApp),
|
||||
"ipa" => Ok(PackageType::Ipa),
|
||||
"msi" => Ok(PackageType::WindowsMsi),
|
||||
"nsis" => Ok(PackageType::Nsis),
|
||||
"deb" => Ok(PackageType::Deb),
|
||||
@@ -343,6 +366,8 @@ impl FromStr for PackageType {
|
||||
"appimage" => Ok(PackageType::AppImage),
|
||||
"dmg" => Ok(PackageType::Dmg),
|
||||
"updater" => Ok(PackageType::Updater),
|
||||
"apk" => Ok(PackageType::Apk),
|
||||
"aab" => Ok(PackageType::Aab),
|
||||
_ => Err(format!("{s} is not a valid package type")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
// The file has been placed there by the build script.
|
||||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
pub const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const PKG_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
|
||||
pub const PKG_VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
|
||||
pub const PKG_VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
|
||||
pub const PKG_VERSION_PRE: &str = env!("CARGO_PKG_VERSION_PRE");
|
||||
|
||||
pub const GIT_COMMIT_HASH: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA");
|
||||
pub const GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("DIOXUS_CLI_GIT_SHA_SHORT");
|
||||
@@ -0,0 +1,207 @@
|
||||
//! esbuild binary download, caching, and invocation.
|
||||
//!
|
||||
//! esbuild is used for JavaScript bundling and minification, replacing the
|
||||
//! previous SWC Rust library dependencies. The standalone Go binary is
|
||||
//! downloaded from the npm registry and cached locally.
|
||||
|
||||
use crate::{CliSettings, Workspace};
|
||||
use anyhow::{anyhow, Context};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Pinned esbuild version.
|
||||
const ESBUILD_VERSION: &str = "0.27.3";
|
||||
|
||||
pub(crate) struct Esbuild;
|
||||
|
||||
impl Esbuild {
|
||||
/// Ensure the esbuild binary is available, downloading if needed.
|
||||
/// Returns the path to the binary.
|
||||
pub(crate) async fn get_or_install() -> anyhow::Result<PathBuf> {
|
||||
if CliSettings::prefer_no_downloads() {
|
||||
which::which("esbuild")
|
||||
.map_err(|_| anyhow!("esbuild not found on PATH and downloads are disabled"))
|
||||
} else {
|
||||
let path = Self::installed_bin_path();
|
||||
if !path.exists() {
|
||||
Self::install_from_npm().await?;
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the esbuild binary path if it's already been installed or is on PATH.
|
||||
/// This is a sync check intended for use after `verify_tooling` has been called.
|
||||
pub(crate) fn path_if_installed() -> Option<PathBuf> {
|
||||
let path = Self::installed_bin_path();
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
} else {
|
||||
which::which("esbuild").ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// The path where we cache the esbuild binary.
|
||||
fn installed_bin_path() -> PathBuf {
|
||||
let dir = Workspace::dioxus_data_dir().join("esbuild");
|
||||
let name = if cfg!(windows) {
|
||||
format!("esbuild-{ESBUILD_VERSION}.exe")
|
||||
} else {
|
||||
format!("esbuild-{ESBUILD_VERSION}")
|
||||
};
|
||||
dir.join(name)
|
||||
}
|
||||
|
||||
/// Download esbuild from the npm registry and extract the binary.
|
||||
///
|
||||
/// esbuild publishes platform-specific packages to npm as tar.gz archives.
|
||||
/// Each archive contains `package/bin/esbuild[.exe]`.
|
||||
async fn install_from_npm() -> anyhow::Result<()> {
|
||||
let platform = Self::npm_platform_package()
|
||||
.ok_or_else(|| anyhow!("No esbuild binary available for this platform"))?;
|
||||
|
||||
let url = format!(
|
||||
"https://registry.npmjs.org/@esbuild/{platform}/-/{platform}-{ESBUILD_VERSION}.tgz"
|
||||
);
|
||||
|
||||
tracing::info!("Installing esbuild v{ESBUILD_VERSION} from {url}...");
|
||||
|
||||
let bytes = reqwest::get(&url)
|
||||
.await
|
||||
.with_context(|| format!("Failed to download esbuild from {url}"))?
|
||||
.bytes()
|
||||
.await
|
||||
.context("Failed to read esbuild download response")?;
|
||||
|
||||
// Extract `package/bin/esbuild[.exe]` from the tar.gz
|
||||
let binary_data = Self::extract_binary_from_tgz(&bytes)?;
|
||||
|
||||
let binary_path = Self::installed_bin_path();
|
||||
std::fs::create_dir_all(binary_path.parent().unwrap())
|
||||
.context("Failed to create esbuild cache directory")?;
|
||||
|
||||
std::fs::write(&binary_path, &binary_data).context("Failed to write esbuild binary")?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = binary_path.metadata()?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&binary_path, perms)?;
|
||||
}
|
||||
|
||||
tracing::info!("esbuild v{ESBUILD_VERSION} installed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract the esbuild binary from an npm tar.gz package.
|
||||
///
|
||||
/// The binary is at `package/bin/esbuild` (or `package/bin/esbuild.exe` on Windows).
|
||||
fn extract_binary_from_tgz(tgz_bytes: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
let bin_name = if cfg!(windows) {
|
||||
"esbuild.exe"
|
||||
} else {
|
||||
"esbuild"
|
||||
};
|
||||
Self::extract_binary_named_from_tgz(tgz_bytes, bin_name)
|
||||
}
|
||||
|
||||
fn extract_binary_named_from_tgz(tgz_bytes: &[u8], bin_name: &str) -> anyhow::Result<Vec<u8>> {
|
||||
use flate2::read::GzDecoder;
|
||||
use std::io::Read;
|
||||
use tar::Archive;
|
||||
|
||||
let decoder = GzDecoder::new(tgz_bytes);
|
||||
let mut archive = Archive::new(decoder);
|
||||
|
||||
let expected_paths = [
|
||||
format!("package/bin/{bin_name}"),
|
||||
format!("package/{bin_name}"),
|
||||
];
|
||||
let mut archive_entries = Vec::new();
|
||||
|
||||
for entry in archive.entries().context("Failed to read tar entries")? {
|
||||
let mut entry = entry.context("Failed to read tar entry")?;
|
||||
let path = entry.path().context("Failed to read entry path")?;
|
||||
let path_string = path.to_string_lossy().replace('\\', "/");
|
||||
|
||||
if expected_paths.contains(&path_string) {
|
||||
let mut data = Vec::new();
|
||||
entry
|
||||
.read_to_end(&mut data)
|
||||
.context("Failed to read esbuild binary from archive")?;
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
archive_entries.push(path_string);
|
||||
}
|
||||
|
||||
archive_entries.sort();
|
||||
archive_entries.truncate(10);
|
||||
|
||||
anyhow::bail!(
|
||||
"esbuild binary not found in archive (expected one of {}). Found entries: {}",
|
||||
expected_paths.join(", "),
|
||||
archive_entries.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
/// Map the host platform to the npm package name for esbuild.
|
||||
///
|
||||
/// esbuild publishes per-platform packages under `@esbuild/{name}`:
|
||||
/// - darwin-arm64, darwin-x64
|
||||
/// - linux-x64, linux-arm64
|
||||
/// - win32-x64, win32-arm64
|
||||
fn npm_platform_package() -> Option<&'static str> {
|
||||
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
||||
Some("darwin-arm64")
|
||||
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
|
||||
Some("darwin-x64")
|
||||
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
|
||||
Some("linux-x64")
|
||||
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
|
||||
Some("linux-arm64")
|
||||
} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
|
||||
Some("win32-x64")
|
||||
} else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
|
||||
Some("win32-arm64")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Esbuild;
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
|
||||
fn tgz_with_entries(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
{
|
||||
let mut tar = tar::Builder::new(&mut encoder);
|
||||
for (path, contents) in entries {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(contents.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
tar.append_data(&mut header, *path, *contents).unwrap();
|
||||
}
|
||||
tar.finish().unwrap();
|
||||
}
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_binary_from_bin_layout() {
|
||||
let tgz = tgz_with_entries(&[("package/bin/esbuild.exe", b"windows-binary")]);
|
||||
let extracted = Esbuild::extract_binary_named_from_tgz(&tgz, "esbuild.exe").unwrap();
|
||||
assert_eq!(extracted, b"windows-binary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_binary_from_package_root_layout() {
|
||||
let tgz = tgz_with_entries(&[("package/esbuild.exe", b"windows-binary")]);
|
||||
let extracted = Esbuild::extract_binary_named_from_tgz(&tgz, "esbuild.exe").unwrap();
|
||||
assert_eq!(extracted, b"windows-binary");
|
||||
}
|
||||
}
|
||||
+78
-17
@@ -42,14 +42,14 @@ use futures_util::FutureExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{any::Any, io::Read, str::FromStr, sync::Arc, time::SystemTime};
|
||||
use std::{any::Any, io::Read, pin::Pin, str::FromStr, sync::Arc, time::SystemTime};
|
||||
use std::{borrow::Cow, sync::OnceLock};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fmt::{Debug, Display, Write as _},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
time::Instant,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{future::Future, panic::AssertUnwindSafe};
|
||||
use tracing::{field::Visit, Level, Subscriber};
|
||||
@@ -139,10 +139,9 @@ impl TraceController {
|
||||
///
|
||||
/// We pass the TraceController around the CLI in a few places, namely the serve command so the TUI
|
||||
/// can access things like the logs.
|
||||
pub async fn main<F>(run_app: impl FnOnce(Commands, Self) -> F) -> StructuredOutput
|
||||
where
|
||||
F: Future<Output = Result<StructuredOutput>>,
|
||||
{
|
||||
pub async fn main(
|
||||
run_app: impl FnOnce(Commands, Self) -> Pin<Box<dyn Future<Output = Result<StructuredOutput>>>>,
|
||||
) -> StructuredOutput {
|
||||
let args = Cli::parse();
|
||||
let tui_active = Arc::new(AtomicBool::new(false));
|
||||
let is_serve_cmd = matches!(args.action, Commands::Serve(_));
|
||||
@@ -178,7 +177,7 @@ impl TraceController {
|
||||
|
||||
// We complete filter out a few fields that are not relevant to the user, like `dx_src` and `json`
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(args.verbosity.verbose)
|
||||
.with_target(false)
|
||||
.fmt_fields(
|
||||
format::debug_fn(move |writer, field, value| {
|
||||
if field.name() == "json" && !args.verbosity.json_output {
|
||||
@@ -521,7 +520,7 @@ impl TraceController {
|
||||
error_handled,
|
||||
} = event;
|
||||
|
||||
let mut ph_event = posthog_rs::Event::new(action, distinct_id.to_string());
|
||||
let mut ph_event = PosthogEvent::new(action, distinct_id.to_string());
|
||||
|
||||
// The reporter's fields
|
||||
_ = ph_event.insert_prop("is_ci", CliSettings::is_ci());
|
||||
@@ -649,7 +648,7 @@ impl TraceController {
|
||||
reset_cursor();
|
||||
|
||||
// re-emit any remaining messages in case they're useful.
|
||||
while let Ok(Some(msg)) = self.tui_rx.lock().await.try_next() {
|
||||
while let Ok(msg) = self.tui_rx.lock().await.try_recv() {
|
||||
let content = match msg.content {
|
||||
TraceContent::Text(text) => text,
|
||||
TraceContent::Cargo(msg) => msg.message.to_string(),
|
||||
@@ -729,7 +728,7 @@ impl TraceController {
|
||||
true => {
|
||||
let mut msgs = self.telemetry_rx.lock().await;
|
||||
|
||||
let request_body = std::iter::from_fn(|| msgs.try_next().ok().flatten())
|
||||
let request_body = std::iter::from_fn(|| msgs.try_recv().ok())
|
||||
.filter_map(|msg| serde_json::to_value(msg).ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -740,8 +739,7 @@ impl TraceController {
|
||||
false => {
|
||||
let mut msgs = self.telemetry_rx.lock().await;
|
||||
|
||||
let msg_list =
|
||||
std::iter::from_fn(|| msgs.try_next().ok().flatten()).collect::<Vec<_>>();
|
||||
let msg_list = std::iter::from_fn(|| msgs.try_recv().ok()).collect::<Vec<_>>();
|
||||
|
||||
if !msg_list.is_empty() {
|
||||
let dest = Workspace::dioxus_data_dir()
|
||||
@@ -1336,10 +1334,43 @@ impl Default for PrettyUptime {
|
||||
|
||||
impl FormatTime for PrettyUptime {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
|
||||
let e = self.epoch.elapsed();
|
||||
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
|
||||
Self::write_elapsed(self.epoch.elapsed(), w)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrettyUptime {
|
||||
fn write_elapsed(elapsed: Duration, mut w: impl std::fmt::Write) -> std::fmt::Result {
|
||||
write!(
|
||||
w,
|
||||
"{:3}.{:02}s",
|
||||
elapsed.as_secs(),
|
||||
elapsed.subsec_millis() / 10
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod pretty_uptime_tests {
|
||||
use super::PrettyUptime;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn pretty_uptime_pads_centiseconds_to_keep_a_stable_width() {
|
||||
let cases = [
|
||||
(Duration::from_millis(92_993), " 92.99s"),
|
||||
(Duration::from_millis(93_085), " 93.08s"),
|
||||
(Duration::from_millis(93_130), " 93.13s"),
|
||||
(Duration::from_millis(999_999), "999.99s"),
|
||||
];
|
||||
|
||||
for (elapsed, expected) in cases {
|
||||
let mut rendered = String::new();
|
||||
PrettyUptime::write_elapsed(elapsed, &mut rendered).unwrap();
|
||||
assert_eq!(rendered, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the provided future and wait for it to complete, handling Ctrl-C gracefully.
|
||||
///
|
||||
/// If ctrl-c is pressed twice, it exits immediately, skipping our telemetry flush.
|
||||
@@ -1372,7 +1403,37 @@ async fn run_with_ctrl_c(
|
||||
/// We only store non-pii information in telemetry to track issues and performance
|
||||
/// across the CLI.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Reporter {
|
||||
pub session_id: Uuid,
|
||||
pub distinct_id: Uuid,
|
||||
struct Reporter {
|
||||
session_id: Uuid,
|
||||
distinct_id: Uuid,
|
||||
}
|
||||
|
||||
/// A minimal PostHog event, replacing the `posthog-rs` crate dependency.
|
||||
/// See <https://posthog.com/docs/api/capture>
|
||||
#[derive(Serialize, Debug)]
|
||||
struct PosthogEvent {
|
||||
event: String,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
distinct_id: String,
|
||||
properties: std::collections::HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl PosthogEvent {
|
||||
fn new(event: impl Into<String>, distinct_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
event: event.into(),
|
||||
distinct_id: distinct_id.into(),
|
||||
properties: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_prop<V: Serialize>(
|
||||
&mut self,
|
||||
key: impl Into<String>,
|
||||
value: V,
|
||||
) -> Result<(), serde_json::Error> {
|
||||
self.properties
|
||||
.insert(key.into(), serde_json::to_value(value)?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+25
-20
@@ -5,15 +5,18 @@
|
||||
#![allow(clippy::doc_overindented_list_items)]
|
||||
|
||||
mod build;
|
||||
mod bundle_utils;
|
||||
mod bundler;
|
||||
mod cargo_toml;
|
||||
mod check;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod devcfg;
|
||||
mod dx_build_info;
|
||||
mod error;
|
||||
mod esbuild;
|
||||
mod fastfs;
|
||||
mod logging;
|
||||
mod opt;
|
||||
mod platform;
|
||||
mod rustcwrapper;
|
||||
mod serve;
|
||||
@@ -53,25 +56,27 @@ async fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
// Run under the tracing collector so we can capture errors/panics.
|
||||
let result = TraceController::main(|args, tracer| async move {
|
||||
match args {
|
||||
Commands::Serve(opts) => opts.serve(&tracer).await,
|
||||
Commands::Translate(opts) => opts.translate(),
|
||||
Commands::New(opts) => opts.create().await,
|
||||
Commands::Init(opts) => opts.init().await,
|
||||
Commands::Config(opts) => opts.config().await,
|
||||
Commands::Autoformat(opts) => opts.autoformat().await,
|
||||
Commands::Check(opts) => opts.check().await,
|
||||
Commands::Build(opts) => opts.build().await,
|
||||
Commands::Bundle(opts) => opts.bundle().await,
|
||||
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,
|
||||
Commands::Components(opts) => opts.run().await,
|
||||
}
|
||||
let result = TraceController::main(|args, tracer| {
|
||||
Box::pin(async move {
|
||||
match args {
|
||||
Commands::Serve(opts) => opts.serve(&tracer).await,
|
||||
Commands::Translate(opts) => opts.translate(),
|
||||
Commands::New(opts) => opts.create().await,
|
||||
Commands::Init(opts) => opts.init().await,
|
||||
Commands::Config(opts) => opts.config().await,
|
||||
Commands::Autoformat(opts) => opts.autoformat().await,
|
||||
Commands::Check(opts) => opts.check().await,
|
||||
Commands::Build(opts) => opts.build().await,
|
||||
Commands::Bundle(opts) => opts.bundle().await,
|
||||
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,
|
||||
Commands::Components(opts) => opts.run().await,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Print the structured output in JSON format for third-party tools to consume.
|
||||
|
||||
File renamed without changes.
@@ -3,7 +3,7 @@ use manganis::{AssetOptions, CssModuleAssetOptions, FolderAssetOptions};
|
||||
use manganis_core::{AssetVariant, CssAssetOptions, ImageAssetOptions, JsAssetOptions};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::css::{process_css_module, process_scss};
|
||||
use crate::opt::css::{process_css_module, process_scss};
|
||||
|
||||
use super::{
|
||||
css::process_css, folder::process_folder, image::process_image, js::process_js,
|
||||
@@ -11,12 +11,13 @@ use super::{
|
||||
};
|
||||
|
||||
/// Process a specific file asset with the given options reading from the source and writing to the output path
|
||||
pub fn process_file_to(
|
||||
pub(crate) fn process_file_to(
|
||||
options: &AssetOptions,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
process_file_to_with_options(options, source, output_path, false)
|
||||
process_file_to_with_options(options, source, output_path, false, esbuild_path)
|
||||
}
|
||||
|
||||
/// Process a specific file asset with additional options
|
||||
@@ -25,6 +26,7 @@ pub(crate) fn process_file_to_with_options(
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
in_folder: bool,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
// If the file already exists and this is a hashed asset, then we must have a file
|
||||
// with the same hash already. The hash has the file contents and options, so if we
|
||||
@@ -61,7 +63,7 @@ pub(crate) fn process_file_to_with_options(
|
||||
process_scss(options, source, &temp_path)?;
|
||||
}
|
||||
ResolvedAssetType::Js(options) => {
|
||||
process_js(options, source, &temp_path, !in_folder)?;
|
||||
process_js(options, source, &temp_path, !in_folder, esbuild_path)?;
|
||||
}
|
||||
ResolvedAssetType::Image(options) => {
|
||||
process_image(options, source, &temp_path)?;
|
||||
@@ -70,7 +72,7 @@ pub(crate) fn process_file_to_with_options(
|
||||
process_json(source, &temp_path)?;
|
||||
}
|
||||
ResolvedAssetType::Folder(_) => {
|
||||
process_folder(source, &temp_path)?;
|
||||
process_folder(source, &temp_path, esbuild_path)?;
|
||||
}
|
||||
ResolvedAssetType::File => {
|
||||
let source_file = std::fs::File::open(source)?;
|
||||
@@ -2,10 +2,14 @@ use std::path::Path;
|
||||
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
use crate::file::process_file_to_with_options;
|
||||
use crate::opt::file::process_file_to_with_options;
|
||||
|
||||
/// Process a folder, optimizing and copying all assets into the output folder
|
||||
pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()> {
|
||||
pub fn process_folder(
|
||||
source: &Path,
|
||||
output_folder: &Path,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Create the folder
|
||||
std::fs::create_dir_all(output_folder)?;
|
||||
|
||||
@@ -21,9 +25,9 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
|
||||
let metadata = file.metadata()?;
|
||||
let output_path = output_folder.join(file.strip_prefix(source)?);
|
||||
if metadata.is_dir() {
|
||||
process_folder(&file, &output_path)
|
||||
process_folder(&file, &output_path, esbuild_path)
|
||||
} else {
|
||||
process_file_minimal(&file, &output_path)
|
||||
process_file_minimal(&file, &output_path, esbuild_path)
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -31,12 +35,17 @@ pub fn process_folder(source: &Path, output_folder: &Path) -> anyhow::Result<()>
|
||||
}
|
||||
|
||||
/// Optimize a file without changing any of its contents significantly (e.g. by changing the extension)
|
||||
fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> {
|
||||
fn process_file_minimal(
|
||||
input_path: &Path,
|
||||
output_path: &Path,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
process_file_to_with_options(
|
||||
&manganis_core::AssetOptions::builder().into_asset_options(),
|
||||
input_path,
|
||||
output_path,
|
||||
true,
|
||||
esbuild_path,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
use crate::opt::{
|
||||
css::hash_scss,
|
||||
file::{resolve_asset_options, ResolvedAssetType},
|
||||
js::hash_js,
|
||||
@@ -48,8 +48,8 @@ fn hash_file(options: &AssetOptions, source: &Path) -> anyhow::Result<AssetHash>
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
options.hash(&mut hash);
|
||||
|
||||
// Hash the version of CLI opt
|
||||
hash.write(crate::build_info::version().as_bytes());
|
||||
// Hash the version of the CLI
|
||||
hash.write(crate::dx_build_info::PKG_VERSION.as_bytes());
|
||||
hash_file_with_options(options, source, &mut hash, false)?;
|
||||
|
||||
let hash = hash.finish();
|
||||
@@ -128,7 +128,7 @@ pub(crate) fn hash_file_contents(source: &Path, hasher: &mut impl Hasher) -> any
|
||||
}
|
||||
|
||||
/// Add a hash to the asset, or log an error if it fails
|
||||
pub fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||
pub(crate) fn add_hash_to_asset(asset: &mut BundledAsset) {
|
||||
let source = asset.absolute_source_path();
|
||||
match AssetHash::hash_file_contents(asset.options(), source) {
|
||||
Ok(hash) => {
|
||||
@@ -1,12 +1,8 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use jpg::compress_jpg;
|
||||
use image::{DynamicImage, EncodableLayout};
|
||||
use manganis_core::{ImageAssetOptions, ImageFormat, ImageSize};
|
||||
use png::compress_png;
|
||||
|
||||
mod jpg;
|
||||
mod png;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn process_image(
|
||||
image_options: &ImageAssetOptions,
|
||||
@@ -71,3 +67,70 @@ pub(crate) fn process_image(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn compress_png(image: DynamicImage, output_location: &Path) {
|
||||
// Image loading/saving is outside scope of this library
|
||||
let width = image.width() as usize;
|
||||
let height = image.height() as usize;
|
||||
let bitmap: Vec<_> = image
|
||||
.into_rgba8()
|
||||
.pixels()
|
||||
.map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3]))
|
||||
.collect();
|
||||
|
||||
// Configure the library
|
||||
let mut liq = imagequant::new();
|
||||
liq.set_speed(5).unwrap();
|
||||
liq.set_quality(0, 99).unwrap();
|
||||
|
||||
// Describe the bitmap
|
||||
let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap();
|
||||
|
||||
// The magic happens in quantize()
|
||||
let mut res = match liq.quantize(&mut img) {
|
||||
Ok(res) => res,
|
||||
Err(err) => panic!("Quantization failed, because: {err:?}"),
|
||||
};
|
||||
|
||||
let (palette, pixels) = res.remapped(&mut img).unwrap();
|
||||
|
||||
let file = std::fs::File::create(output_location).unwrap();
|
||||
let w = &mut BufWriter::new(file);
|
||||
|
||||
let mut encoder = png::Encoder::new(w, width as u32, height as u32);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
let mut flattened_palette = Vec::new();
|
||||
let mut alpha_palette = Vec::new();
|
||||
for px in palette {
|
||||
flattened_palette.push(px.r);
|
||||
flattened_palette.push(px.g);
|
||||
flattened_palette.push(px.b);
|
||||
alpha_palette.push(px.a);
|
||||
}
|
||||
encoder.set_palette(flattened_palette);
|
||||
encoder.set_trns(alpha_palette);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder.set_color(png::ColorType::Indexed);
|
||||
encoder.set_compression(png::Compression::Best);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
writer.write_image_data(&pixels).unwrap();
|
||||
writer.finish().unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> {
|
||||
let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX);
|
||||
let width = image.width() as usize;
|
||||
let height = image.height() as usize;
|
||||
|
||||
comp.set_size(width, height);
|
||||
let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work
|
||||
|
||||
comp.write_scanlines(image.to_rgba8().as_bytes())?;
|
||||
|
||||
let jpeg_bytes = comp.finish()?;
|
||||
|
||||
let file = std::fs::File::create(output_location)?;
|
||||
let w = &mut BufWriter::new(file);
|
||||
w.write_all(&jpeg_bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
use manganis_core::JsAssetOptions;
|
||||
|
||||
use crate::opt::hash::hash_file_contents;
|
||||
|
||||
pub(crate) fn process_js(
|
||||
js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
bundle: bool,
|
||||
esbuild_path: Option<&Path>,
|
||||
) -> anyhow::Result<()> {
|
||||
let minify = js_options.minified();
|
||||
let needs_esbuild = minify || bundle;
|
||||
|
||||
if needs_esbuild {
|
||||
if let Some(esbuild) = esbuild_path {
|
||||
match run_esbuild(esbuild, source, output_path, bundle, minify) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Failed to process JS with esbuild. Falling back to copy: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!("esbuild binary path not set. Copying JS without processing.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: copy unprocessed
|
||||
let mut source_file = std::fs::File::open(source)?;
|
||||
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
|
||||
std::io::copy(&mut source_file, &mut writer).with_context(|| {
|
||||
format!(
|
||||
"Failed to write JS to output location: {}",
|
||||
output_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run esbuild to bundle and/or minify a JavaScript file.
|
||||
fn run_esbuild(
|
||||
esbuild: &Path,
|
||||
source: &Path,
|
||||
output_path: &Path,
|
||||
bundle: bool,
|
||||
minify: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut cmd = std::process::Command::new(esbuild);
|
||||
cmd.arg(source);
|
||||
cmd.arg(format!("--outfile={}", output_path.display()));
|
||||
cmd.arg("--log-level=warning");
|
||||
|
||||
if bundle {
|
||||
cmd.arg("--bundle");
|
||||
cmd.arg("--format=esm");
|
||||
}
|
||||
|
||||
if minify {
|
||||
cmd.arg("--minify");
|
||||
}
|
||||
|
||||
tracing::debug!("Running esbuild: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().context("Failed to run esbuild")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("esbuild failed: {stderr}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn hash_js(
|
||||
_js_options: &JsAssetOptions,
|
||||
source: &Path,
|
||||
hasher: &mut impl std::hash::Hasher,
|
||||
_bundle: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
hash_file_contents(source, hasher)
|
||||
}
|
||||
File renamed without changes.
@@ -1,13 +1,8 @@
|
||||
use anyhow::Context;
|
||||
use manganis::AssetOptions;
|
||||
use manganis_core::BundledAsset;
|
||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
mod build_info;
|
||||
mod css;
|
||||
mod file;
|
||||
mod folder;
|
||||
@@ -16,14 +11,14 @@ mod image;
|
||||
mod js;
|
||||
mod json;
|
||||
|
||||
pub use file::process_file_to;
|
||||
pub use hash::add_hash_to_asset;
|
||||
pub(crate) use file::process_file_to;
|
||||
pub(crate) use hash::add_hash_to_asset;
|
||||
|
||||
/// A manifest of all assets collected from dependencies
|
||||
///
|
||||
/// This will be filled in primarily by incremental compilation artifacts.
|
||||
#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct AssetManifest {
|
||||
pub(crate) struct AssetManifest {
|
||||
/// Map of bundled asset name to the asset itself
|
||||
assets: BTreeMap<PathBuf, HashSet<BundledAsset>>,
|
||||
}
|
||||
@@ -88,41 +83,4 @@ impl AssetManifest {
|
||||
.flat_map(|assets| assets.iter())
|
||||
.filter(move |asset| seen.insert(asset.bundled_path()))
|
||||
}
|
||||
|
||||
pub fn load_from_file(path: &Path) -> anyhow::Result<Self> {
|
||||
let src = std::fs::read_to_string(path)?;
|
||||
|
||||
serde_json::from_str(&src)
|
||||
.with_context(|| format!("Failed to parse asset manifest from {path:?}\n{src}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimize a list of assets in parallel
|
||||
pub fn optimize_all_assets(
|
||||
assets_to_transfer: Vec<(PathBuf, PathBuf, AssetOptions)>,
|
||||
on_optimization_start: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
||||
on_optimization_end: impl FnMut(&Path, &Path, &AssetOptions) + Sync + Send,
|
||||
) -> anyhow::Result<()> {
|
||||
let on_optimization_start = Arc::new(RwLock::new(on_optimization_start));
|
||||
let on_optimization_end = Arc::new(RwLock::new(on_optimization_end));
|
||||
assets_to_transfer
|
||||
.par_iter()
|
||||
.try_for_each(|(from, to, options)| {
|
||||
{
|
||||
let mut on_optimization_start = on_optimization_start.write().unwrap();
|
||||
on_optimization_start(from, to, options);
|
||||
}
|
||||
|
||||
let res = process_file_to(options, from, to);
|
||||
if let Err(err) = res.as_ref() {
|
||||
tracing::error!("Failed to copy asset {from:?}: {err}");
|
||||
}
|
||||
|
||||
{
|
||||
let mut on_optimization_end = on_optimization_end.write().unwrap();
|
||||
on_optimization_end(from, to, options);
|
||||
}
|
||||
|
||||
res.map(|_| ())
|
||||
})
|
||||
}
|
||||
@@ -439,6 +439,7 @@ impl Output {
|
||||
/// Render the current state of everything to the console screen
|
||||
pub fn render(&mut self, runner: &AppServer, server: &WebServer) {
|
||||
if !self.interactive {
|
||||
self.emit_logs_to_tracing();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1094,11 +1095,31 @@ impl Output {
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
/// Drains the current log queue to tracing so they still get printed even if the output is non-interactive
|
||||
fn emit_logs_to_tracing(&mut self) {
|
||||
// In non-interactive mode (CI, piped output), drain pending logs to tracing
|
||||
// so they still appear in stdout/stderr instead of being silently dropped.
|
||||
while let Some(log) = self.pending_logs.pop_back() {
|
||||
let msg = match &log.content {
|
||||
TraceContent::Text(s) => s.as_str(),
|
||||
TraceContent::Cargo(d) => d.message.as_str(),
|
||||
};
|
||||
match log.level {
|
||||
Level::ERROR => tracing::error!("{msg}"),
|
||||
Level::WARN => tracing::warn!("{msg}"),
|
||||
Level::INFO => tracing::info!("{msg}"),
|
||||
Level::DEBUG => tracing::debug!("{msg}"),
|
||||
Level::TRACE => tracing::trace!("{msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for Output {
|
||||
fn drop(&mut self) {
|
||||
if !self.interactive {
|
||||
self.emit_logs_to_tracing();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ impl AppServer {
|
||||
let mut changes: Vec<_> = event.into_iter().collect();
|
||||
|
||||
// Dequeue in bulk if we can, we might've received a lot of events in one go
|
||||
while let Some(event) = self.watcher_rx.try_next().ok().flatten() {
|
||||
while let Ok(event) = self.watcher_rx.try_recv() {
|
||||
changes.push(event);
|
||||
}
|
||||
|
||||
@@ -1313,7 +1313,7 @@ impl AppServer {
|
||||
let depth = relative.components().count();
|
||||
let is_better = best_match
|
||||
.as_ref()
|
||||
.map_or(true, |(_, best_depth)| depth < *best_depth);
|
||||
.is_none_or(|(_, best_depth)| depth < *best_depth);
|
||||
if is_better {
|
||||
best_match = Some((krate.name.replace('-', "_"), depth));
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use futures_util::{
|
||||
StreamExt,
|
||||
};
|
||||
use hyper::HeaderMap;
|
||||
use rustls::crypto::{aws_lc_rs::default_provider, CryptoProvider};
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
|
||||
@@ -135,15 +135,15 @@ async fn find_latest_wasm_opt_download_url() -> anyhow::Result<String> {
|
||||
// Find the platform identifier based on the current OS and architecture
|
||||
// hardcoded for now to get around github api rate limits
|
||||
if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-windows.tar.gz".to_string());
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-windows.tar.gz".to_string());
|
||||
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-linux.tar.gz".to_string());
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-linux.tar.gz".to_string());
|
||||
} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) {
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-aarch64-linux.tar.gz".to_string());
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-aarch64-linux.tar.gz".to_string());
|
||||
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-x86_64-macos.tar.gz".to_string());
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-x86_64-macos.tar.gz".to_string());
|
||||
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_123/binaryen-version_123-arm64-macos.tar.gz".to_string());
|
||||
return Ok("https://github.com/WebAssembly/binaryen/releases/download/version_127/binaryen-version_127-arm64-macos.tar.gz".to_string());
|
||||
};
|
||||
|
||||
let url = "https://api.github.com/repos/WebAssembly/binaryen/releases/latest";
|
||||
|
||||
@@ -479,7 +479,7 @@ impl VirtualDom {
|
||||
/// Queue any pending events
|
||||
fn queue_events(&mut self) {
|
||||
// Prevent a task from deadlocking the runtime by repeatedly queueing itself
|
||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||
while let Ok(msg) = self.rx.try_recv() {
|
||||
match msg {
|
||||
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
|
||||
|
||||
@@ -131,9 +131,16 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
/// set the directory where data will be stored in release mode.
|
||||
/// Set the directory where WebView2 stores its user data (cookies, cache, IndexedDB, etc.).
|
||||
///
|
||||
/// > Note: This **must** be set when bundling on Windows.
|
||||
/// If not set, Dioxus automatically chooses a sensible default:
|
||||
/// - **Windows:** `%LOCALAPPDATA%/<exe_name>` — this avoids the WebView2 default of
|
||||
/// placing data next to the executable, which fails in read-only locations like
|
||||
/// `Program Files` or on certain drive types (e.g. ReFS dev drives).
|
||||
/// - **macOS/Linux:** managed by WebKit automatically.
|
||||
///
|
||||
/// You typically only need this if you want multiple apps to share a WebView2 profile
|
||||
/// or need the data in a specific location for compliance/IT policy reasons.
|
||||
pub fn with_data_directory(mut self, path: impl Into<PathBuf>) -> Self {
|
||||
self.data_dir = Some(path.into());
|
||||
self
|
||||
|
||||
@@ -231,6 +231,7 @@ impl EditWebsocket {
|
||||
let hex_encoded_server_key = encode_key_string(¤t_server_location.server_key);
|
||||
let mut location = None;
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
let on_request = |req: &Request, res| {
|
||||
// Try to parse the webview id and key from the path
|
||||
let path = req.uri().path();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![allow(unexpected_cfgs)]
|
||||
|
||||
mod android_sync_lock;
|
||||
mod app;
|
||||
|
||||
@@ -260,12 +260,23 @@ impl WebviewInstance {
|
||||
|
||||
unsafe {
|
||||
let window: id = window.ns_window() as id;
|
||||
#[allow(unexpected_cfgs)]
|
||||
let _: () = msg_send![window, setCollectionBehavior: NSWindowCollectionBehavior::NSWindowCollectionBehaviorManaged];
|
||||
}
|
||||
}
|
||||
|
||||
let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
let mut web_context = WebContext::new(cfg.data_dir.clone().or_else(|| {
|
||||
// On Windows, WebView2 defaults to storing its data next to the executable.
|
||||
// This fails on certain drives (e.g. ReFS dev drives, Program Files) where the
|
||||
// directory may not be writable. Fall back to %LOCALAPPDATA%/<exe_name> automatically.
|
||||
if cfg!(windows) {
|
||||
let exe = std::env::current_exe().ok()?;
|
||||
let name = exe.file_stem()?.to_str()?;
|
||||
let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
|
||||
Some(std::path::PathBuf::from(local_app_data).join(name))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
let edit_queue = shared.websocket.create_queue();
|
||||
let asset_handlers = AssetHandlerRegistry::new();
|
||||
let edits = WebviewEdits::new(dom.runtime(), edit_queue.clone());
|
||||
|
||||
@@ -94,6 +94,7 @@ pub enum BuildStage {
|
||||
current: usize,
|
||||
total: usize,
|
||||
krate: String,
|
||||
fresh: bool,
|
||||
},
|
||||
RunningBindgen,
|
||||
SplittingBundle,
|
||||
|
||||
@@ -1,251 +1,112 @@
|
||||
// @ts-check
|
||||
const { defineConfig, devices } = require("@playwright/test");
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
// Copy a directory to a temp location for tests that modify source files (hot-patch tests).
|
||||
// Done in JS so the webServer command is a simple `dx serve` with a `cwd`, which keeps
|
||||
// stdout/stderr piping clean on both Unix and Windows cmd.exe.
|
||||
function copyToTemp(src, dest) {
|
||||
const absSrc = path.resolve(__dirname, src);
|
||||
const absDest = path.resolve(__dirname, dest);
|
||||
fs.rmSync(absDest, { recursive: true, force: true });
|
||||
fs.cpSync(absSrc, absDest, { recursive: true });
|
||||
}
|
||||
|
||||
let webServer = [];
|
||||
let grep = undefined;
|
||||
let grepInvert = undefined;
|
||||
const repoRoot = path.resolve(__dirname, "..", "..");
|
||||
const dx = path.join(repoRoot, "target", "release", process.platform === "win32" ? "dx.exe" : "dx");
|
||||
|
||||
const ALL_SERVERS = [
|
||||
{
|
||||
specs: ["liveview.spec.js"], port: 3030,
|
||||
command: "cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
|
||||
env: { CARGO_TERM_PROGRESS_WHEN: "never" }
|
||||
},
|
||||
{ specs: ["web.spec.js"], port: 9990, cwd: "web", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 9990` },
|
||||
{ specs: ["web-routing.spec.js"], port: 2020, cwd: "web-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2020` },
|
||||
{ specs: ["web-hash-routing.spec.js"], port: 2021, cwd: "web-hash-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 2021` },
|
||||
{ specs: ["fullstack.spec.js"], port: 3333, cwd: "fullstack", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3333 --release` },
|
||||
{ specs: ["fullstack-errors.spec.js"], port: 3232, cwd: "fullstack-errors", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 3232` },
|
||||
{ specs: ["fullstack-mounted.spec.js"], port: 7777, cwd: "fullstack-mounted", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7777` },
|
||||
{ specs: ["fullstack-routing.spec.js"], port: 8888, cwd: "fullstack-routing", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 8888` },
|
||||
{ specs: ["fullstack-spread.spec.js"], port: 7980, cwd: "fullstack-spread", command: `${dx} run --verbose --force-sequential --web --addr 127.0.0.1 --port 7980` },
|
||||
{ specs: ["fullstack-hydration-order.spec.js"], port: 7979, cwd: "fullstack-hydration-order", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 7979` },
|
||||
{ specs: ["fullstack-error-codes.spec.js"], port: 8124, cwd: "fullstack-error-codes", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8124` },
|
||||
{ specs: ["nested-suspense.spec.js", "nested-suspense-no-js.spec.js"], port: 5050, cwd: "nested-suspense", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 5050` },
|
||||
{ specs: ["nested-suspense-ssg.spec.js"], port: 6060, cwd: "nested-suspense", command: `${dx} run --bin nested-suspense-ssg --force-sequential --web --ssg --addr 127.0.0.1 --port 6060` },
|
||||
{ specs: ["suspense-carousel.spec.js"], port: 4040, cwd: "suspense-carousel", command: `${dx} run --force-sequential --web --addr 127.0.0.1 --port 4040` },
|
||||
{ specs: ["cli-optimization.spec.js"], port: 8989, cwd: "cli-optimization", command: `${dx} run --addr 127.0.0.1 --port 8989` },
|
||||
{ specs: ["wasm-split.spec.js"], port: 8001, cwd: "wasm-split-harness", command: `${dx} run --bin wasm-split-harness --web --addr 127.0.0.1 --port 8001 --wasm-split --profile wasm-split-release` },
|
||||
{ specs: ["default-features-disabled.spec.js"], port: 8002, cwd: "default-features-disabled", command: `${dx} run --force-sequential --addr 127.0.0.1 --port 8002` },
|
||||
{ specs: ["web-patch.spec.js"], port: 9980, cwd: "web-hot-patch-temp", setup: () => copyToTemp("web-hot-patch", "web-hot-patch-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9980 --hot-patch --exit-on-error` },
|
||||
{ specs: ["web-patch-fullstack.spec.js"], port: 9981, cwd: "web-hot-patch-fullstack-temp", setup: () => copyToTemp("web-hot-patch-fullstack", "web-hot-patch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --web --addr 127.0.0.1 --port 9981 --hot-patch --exit-on-error` },
|
||||
];
|
||||
|
||||
if (process.platform === "win32") {
|
||||
webServer = [
|
||||
{
|
||||
cwd: path.join(process.cwd(), "windows-headless"),
|
||||
command:
|
||||
"cargo run --package dioxus-cli --release -- run --force-sequential",
|
||||
port: 8787,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
];
|
||||
grep = /windows/;
|
||||
} else {
|
||||
webServer = [
|
||||
{
|
||||
command:
|
||||
"cargo run --package dioxus-playwright-liveview-test --bin dioxus-playwright-liveview-test",
|
||||
port: 3030,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "web"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 9990',
|
||||
port: 9990,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "web-routing"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2020',
|
||||
port: 2020,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "web-hash-routing"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 2021',
|
||||
port: 2021,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3333 --release',
|
||||
port: 3333,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-errors"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 3232',
|
||||
port: 3232,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-mounted"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7777',
|
||||
port: 7777,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-spread"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --verbose --force-sequential --web --addr "127.0.0.1" --port 7980',
|
||||
port: 7980,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-routing"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 8888',
|
||||
port: 8888,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "suspense-carousel"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 4040',
|
||||
port: 4040,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "nested-suspense"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 5050',
|
||||
port: 5050,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "nested-suspense"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --bin nested-suspense-ssg --force-sequential --web --ssg --addr "127.0.0.1" --port 6060',
|
||||
port: 6060,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-hydration-order"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --web --addr "127.0.0.1" --port 7979',
|
||||
port: 7979,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "cli-optimization"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --addr "127.0.0.1" --port 8989',
|
||||
port: 8989,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "wasm-split-harness"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --bin wasm-split-harness --web --addr "127.0.0.1" --port 8001 --wasm-split --profile wasm-split-release',
|
||||
port: 8001,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "default-features-disabled"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8002',
|
||||
port: 8002,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "barebones-template"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8123',
|
||||
port: 8123,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
command:
|
||||
'rm -rf ./web-hot-patch-temp && cp -r ./web-hot-patch ./web-hot-patch-temp && cd web-hot-patch-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9980 --hot-patch --exit-on-error',
|
||||
port: 9980,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
command:
|
||||
'rm -rf ./web-hot-patch-fullstack-temp && cp -r ./web-hot-patch-fullstack ./web-hot-patch-fullstack-temp && cd web-hot-patch-fullstack-temp && cargo run --manifest-path ../../cli/Cargo.toml --release -- serve --verbose --force-sequential --web --addr "127.0.0.1" --port 9981 --hot-patch --exit-on-error',
|
||||
port: 9981,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
{
|
||||
cwd: path.join(process.cwd(), "fullstack-error-codes"),
|
||||
command:
|
||||
'cargo run --package dioxus-cli --release -- run --force-sequential --addr "127.0.0.1" --port 8124',
|
||||
port: 8124,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
},
|
||||
];
|
||||
ALL_SERVERS.push({ specs: ["windows.spec.js"], port: 8787, cwd: "windows-headless", command: `${dx} run --force-sequential` });
|
||||
ALL_SERVERS.push({ specs: ["windows-hotpatch-fullstack.spec.js"], port: 8788, cwd: "windows-hotpatch-fullstack-temp", setup: () => copyToTemp("windows-hotpatch-fullstack", "windows-hotpatch-fullstack-temp"), command: `${dx} serve --verbose --force-sequential --hot-patch --exit-on-error` });
|
||||
}
|
||||
|
||||
// Determine which servers to start based on spec files in argv and platform.
|
||||
// On Windows only windows.spec.js runs (grep filter below), so don't bother
|
||||
// starting servers for tests that will never execute — they just crash and
|
||||
// pollute the logs.
|
||||
const specArgs = process.argv.filter((a) => a.endsWith(".spec.js")).map((a) => path.basename(a));
|
||||
const isWindows = process.platform === "win32";
|
||||
const activeServers = specArgs.length > 0
|
||||
? ALL_SERVERS.filter((s) => s.specs.some((spec) => specArgs.includes(spec)))
|
||||
: isWindows
|
||||
? ALL_SERVERS.filter((s) => s.specs.some((spec) => spec.includes("windows")))
|
||||
: ALL_SERVERS.filter((s) => !s.specs.some((spec) => spec.includes("windows")));
|
||||
|
||||
// Run any setup functions (e.g. copying source to temp dirs for hot-patch tests)
|
||||
// Only happens on initialize
|
||||
//
|
||||
// Build dx once: main process builds and sets env var; workers inherit it and skip.
|
||||
// todo: implement proper fixtures https://www.youtube.com/watch?v=3i6cJUFO_m4
|
||||
if (!process.env._DX_BUILT) {
|
||||
for (const s of activeServers) {
|
||||
if (s.setup) s.setup();
|
||||
}
|
||||
|
||||
execSync("cargo build --package dioxus-cli --release", {
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env, CARGO_TERM_PROGRESS_WHEN: "never" },
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
grepInvert = /windows/;
|
||||
process.env._DX_BUILT = "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: ".",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "retain-on-failure",
|
||||
// Increase the timeout for navigations to give dx time to build the project
|
||||
navigationTimeout: 50 * 60 * 1000,
|
||||
},
|
||||
|
||||
timeout: 50 * 60 * 1000,
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
webServer: activeServers.map((s) => ({
|
||||
command: s.command,
|
||||
port: s.port,
|
||||
cwd: s.cwd ? path.join(__dirname, s.cwd) : __dirname,
|
||||
timeout: 50 * 60 * 1000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: s.env,
|
||||
})),
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
grep,
|
||||
grepInvert,
|
||||
grep: process.platform === "win32" ? /windows/ : undefined,
|
||||
grepInvert: process.platform !== "win32" ? /windows/ : undefined,
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer,
|
||||
});
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../dioxus", features = ["web"] }
|
||||
wasm-bindgen = { version = "0.2.100" }
|
||||
wasm-bindgen = { version = "0.2.114" }
|
||||
web-sys = { version = "*", features = ["MouseEvent"] }
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,78 @@
|
||||
// @ts-check
|
||||
const { test: base, expect } = require("@playwright/test");
|
||||
const fs = require("fs");
|
||||
|
||||
const test = base.extend({
|
||||
browser: async ({ playwright }, use) => {
|
||||
const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:8788");
|
||||
await use(browser);
|
||||
await browser.close();
|
||||
},
|
||||
context: async ({ browser }, use) => {
|
||||
const context = browser.contexts()[0];
|
||||
await use(context);
|
||||
},
|
||||
page: async ({ context }, use) => {
|
||||
const page = context.pages()[0];
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
const hotPatchTimeout = { timeout: 1000 * 60 * 2 };
|
||||
|
||||
test("windows fullstack hotpatch", async ({ page }) => {
|
||||
const mainPath = "windows-hotpatch-fullstack-temp/src/main.rs";
|
||||
const stylePath = "windows-hotpatch-fullstack-temp/assets/style.css";
|
||||
let mainContent = fs.readFileSync(mainPath, "utf8");
|
||||
let styleContent = fs.readFileSync(stylePath, "utf8");
|
||||
|
||||
// Reset any changes from prior runs.
|
||||
mainContent = mainContent.replace(/Ok\(\s*2\s*\)/g, "Ok(1)");
|
||||
mainContent = mainContent.replace("Click button! Count:", "Click me! Count:");
|
||||
fs.writeFileSync(mainPath, mainContent);
|
||||
styleContent = styleContent.replace("background-color: blue;", "background-color: red;");
|
||||
fs.writeFileSync(stylePath, styleContent);
|
||||
|
||||
// ** Verify the initial fat build is working **
|
||||
const main = page.locator("#main");
|
||||
await expect(main).toContainText("Click me! Count: 0");
|
||||
|
||||
// Click and verify the server function works.
|
||||
const button = page.locator("button#increment-button");
|
||||
await button.click();
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(main).toContainText("Click me! Count: 1");
|
||||
|
||||
// Verify initial CSS is applied.
|
||||
await expect(main).toHaveCSS("background-color", "rgb(255, 0, 0)");
|
||||
|
||||
// ** Hot patch: change server fn return value and button text **
|
||||
const updatedContent = mainContent
|
||||
.replace(/Ok\(\s*1\s*\)/g, "Ok(2)")
|
||||
.replace("Click me! Count:", "Click button! Count:");
|
||||
fs.writeFileSync(mainPath, updatedContent);
|
||||
|
||||
// Wait for the hot patch to apply (text should update).
|
||||
await expect(main).toContainText("Click button! Count: 1", hotPatchTimeout);
|
||||
|
||||
// Click and verify the new increment amount from the patched server fn.
|
||||
await button.click();
|
||||
await page.waitForTimeout(5000);
|
||||
await expect(main).toContainText("Click button! Count: 3");
|
||||
|
||||
// ** Hot patch CSS **
|
||||
const updatedStyle = styleContent.replace(
|
||||
"background-color: red;",
|
||||
"background-color: blue;"
|
||||
);
|
||||
fs.writeFileSync(stylePath, updatedStyle);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Wait for the CSS hot patch to apply.
|
||||
await expect(main).toHaveCSS(
|
||||
"background-color",
|
||||
"rgb(0, 0, 255)",
|
||||
hotPatchTimeout
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "windows-hotpatch-fullstack"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../../dioxus", features = ["fullstack"] }
|
||||
|
||||
[features]
|
||||
desktop = ["dioxus/desktop"]
|
||||
server = ["dioxus/server"]
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,3 @@
|
||||
#main {
|
||||
background-color: red;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
const CSS: Asset = asset!("/assets/style.css");
|
||||
|
||||
fn app() -> Element {
|
||||
let mut num = use_signal(|| 0);
|
||||
|
||||
rsx! {
|
||||
document::Link {
|
||||
href: CSS,
|
||||
rel: "stylesheet",
|
||||
}
|
||||
button {
|
||||
id: "increment-button",
|
||||
onclick: move |_| async move {
|
||||
let increment_amount = get_count().await.unwrap();
|
||||
*num.write() += increment_amount;
|
||||
},
|
||||
"Click me! Count: {num}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/api/get_count")]
|
||||
async fn get_count() -> Result<i32> {
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let builder = dioxus::LaunchBuilder::new();
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
let builder = builder.with_cfg(
|
||||
dioxus::desktop::Config::new().with_windows_browser_args("--remote-debugging-port=8788"),
|
||||
);
|
||||
|
||||
builder.launch(app);
|
||||
}
|
||||
@@ -76,7 +76,7 @@ impl<T> Memo<T> {
|
||||
spawn_isomorphic(async move {
|
||||
while rx.next().await.is_some() {
|
||||
// Remove any pending updates
|
||||
while rx.try_next().is_ok() {}
|
||||
while rx.try_recv().is_ok() {}
|
||||
memo.recompute();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,9 +17,9 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
|
||||
let visibility = args
|
||||
.visibility
|
||||
.unwrap_or_else(|| syn::Visibility::Inherited);
|
||||
if input.trait_.is_some() {
|
||||
if let Some(input_trait) = input.trait_.as_ref() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
input.trait_.unwrap().1,
|
||||
input_trait.1.clone(),
|
||||
"The `store` attribute can only be used on `impl Store<T> { ... }` blocks, not trait implementations.",
|
||||
));
|
||||
}
|
||||
@@ -86,7 +86,8 @@ pub(crate) fn extend_store(args: ExtendArgs, mut input: ItemImpl) -> syn::Result
|
||||
let trait_definition = impl_to_trait_body(&extension_name, &input, &visibility)?;
|
||||
|
||||
// Reformat the type to be generic over the lens
|
||||
input.self_ty = Box::new(parse_quote!(#store_path<#item, #lens_generic>));
|
||||
input.self_ty = parse_quote!(#store_path<#item, #lens_generic>);
|
||||
|
||||
// Change the standalone impl block to a trait impl block
|
||||
let (_, trait_generics, _) = input.generics.split_for_impl();
|
||||
input.trait_ = Some((
|
||||
|
||||
Reference in new issue
Block a user