diff --git a/.changes/config.json b/.changes/config.json index a4ca328f7..d5824d1d2 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -171,6 +171,12 @@ "manager": "rust", "dependencies": ["tauri-utils", "tauri-runtime"] }, + "tauri-runtime-cef": { + "path": "./crates/tauri-runtime-cef", + "manager": "rust", + "dependencies": ["tauri-utils", "tauri-runtime"], + "postversion": "node ../../.scripts/ci/sync-cli-metadata.js ${ pkg.pkg } ${ release.type }" + }, "tauri-codegen": { "path": "./crates/tauri-codegen", "manager": "rust", diff --git a/.scripts/ci/sync-cli-metadata.js b/.scripts/ci/sync-cli-metadata.js index b47212d92..004eab1ca 100644 --- a/.scripts/ci/sync-cli-metadata.js +++ b/.scripts/ci/sync-cli-metadata.js @@ -59,12 +59,43 @@ const inc = (version) => { // read file into js object const metadata = JSON.parse(readFileSync(filePath, 'utf-8')) +// Extract CEF version from tauri-runtime-cef/Cargo.toml if it exists +// This runs whenever tauri-runtime-cef version is bumped +if (packageNickname === 'tauri-runtime-cef') { + try { + const cargoLockPath = resolve(__dirname, '../../Cargo.lock') + const cargoLock = readFileSync(cargoLockPath, 'utf-8') + // Find the [package] section for "cef" + // e.g.: + // [[package]] + // name = "cef" + // version = "141.6.0+141.0.11" + const pkgRegex = + /\[\[package\]\][^\[]+?name\s*=\s*"cef"[^\[]+?version\s*=\s*"([^"]+)"/m + const match = cargoLock.match(pkgRegex) + if (match && match[1]) { + const cefVersion = match[1] + metadata.cef = cefVersion + console.log(`Extracted CEF version ${cefVersion} from Cargo.lock`) + } else { + throw new Error('Could not find cef package and version in Cargo.lock') + } + } catch (error) { + throw new Error( + `Failed to extract CEF version from Cargo.lock: ${error.message}` + ) + } +} + // set field version let version if (packageNickname === '@tauri-apps/cli') { version = inc(metadata['cli.js'].version) metadata['cli.js'].version = version -} else { +} else if ( + packageNickname + !== 'tauri-runtime-cef' /* for cef we only update the cef version */ +) { version = inc(metadata[packageNickname]) metadata[packageNickname] = version } diff --git a/Cargo.lock b/Cargo.lock index 06fb446cf..0ccd16458 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1188,7 +1188,7 @@ source = "git+https://github.com/tauri-apps/cef-rs?branch=fix/interface-cast#6c3 dependencies = [ "anyhow", "cmake", - "download-cef", + "download-cef 2.2.0 (git+https://github.com/tauri-apps/cef-rs?branch=fix/interface-cast)", "serde_json", ] @@ -2102,6 +2102,25 @@ dependencies = [ "litrs", ] +[[package]] +name = "download-cef" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98178d9254efef0f69c1f584713d69c790ec00668cd98f783a5085fbefdbddc" +dependencies = [ + "bzip2 0.6.0", + "clap", + "indicatif", + "regex", + "semver", + "serde", + "serde_json", + "sha1_smol", + "tar", + "thiserror 2.0.12", + "ureq", +] + [[package]] name = "download-cef" version = "2.2.0" @@ -8758,6 +8777,7 @@ dependencies = [ "ctrlc", "dialoguer", "dirs 6.0.0", + "download-cef 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "duct", "dunce", "elf", diff --git a/crates/tauri-cli/Cargo.toml b/crates/tauri-cli/Cargo.toml index c9f4b7971..c4dfad968 100644 --- a/crates/tauri-cli/Cargo.toml +++ b/crates/tauri-cli/Cargo.toml @@ -113,6 +113,7 @@ uuid = { version = "1", features = ["v5"] } rand = "0.9" zip = { version = "4", default-features = false, features = ["deflate"] } which = "8" +download-cef = "2.2" [dev-dependencies] insta = "1" diff --git a/crates/tauri-cli/metadata-v2.json b/crates/tauri-cli/metadata-v2.json index 54650c005..373adffeb 100644 --- a/crates/tauri-cli/metadata-v2.json +++ b/crates/tauri-cli/metadata-v2.json @@ -5,5 +5,6 @@ }, "tauri": "2.9.2", "tauri-build": "2.5.1", - "tauri-plugin": "2.5.1" + "tauri-plugin": "2.5.1", + "cef": "141.6.0+141.0.11" } diff --git a/crates/tauri-cli/src/build.rs b/crates/tauri-cli/src/build.rs index 821915908..2c51b2f34 100644 --- a/crates/tauri-cli/src/build.rs +++ b/crates/tauri-cli/src/build.rs @@ -122,7 +122,7 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> { let bin_path = interface.build(interface_options)?; - log::info!(action ="Built"; "application at: {}", tauri_utils::display_path(bin_path)); + log::info!(action = "Built"; "application at: {}", tauri_utils::display_path(bin_path)); let app_settings = interface.app_settings(); diff --git a/crates/tauri-cli/src/cef/exporter.rs b/crates/tauri-cli/src/cef/exporter.rs new file mode 100644 index 000000000..151f5d8ac --- /dev/null +++ b/crates/tauri-cli/src/cef/exporter.rs @@ -0,0 +1,266 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::error::ErrorExt; +use crate::VersionMetadata; +use download_cef::{CefFile, CefIndex, OsAndArch, DEFAULT_TARGET}; +use std::{ + fs, + path::{Path, PathBuf}, + sync::OnceLock, + time::Duration, +}; + +fn default_version() -> &'static str { + static DEFAULT_VERSION: OnceLock = OnceLock::new(); + DEFAULT_VERSION.get_or_init(|| { + let metadata: VersionMetadata = serde_json::from_str(include_str!("../../metadata-v2.json")) + .expect("failed to parse metadata-v2.json"); + download_cef::default_version(&metadata.cef) + }) +} + +fn default_download_url() -> &'static str { + static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); + DEFAULT_DOWNLOAD_URL + .get_or_init(|| download_cef::default_download_url()) + .as_str() +} + +pub struct ExporterOptions { + pub output: PathBuf, + pub target: String, + pub version: Option, + pub mirror_url: Option, + pub force: bool, + pub overwrite: bool, + pub archive: Option, +} + +impl Default for ExporterOptions { + fn default() -> Self { + Self { + output: PathBuf::new(), + target: DEFAULT_TARGET.to_string(), + version: None, + mirror_url: None, + force: false, + overwrite: false, + archive: None, + } + } +} + +pub fn export_cef_directory(options: ExporterOptions) -> crate::Result<()> { + let output = &options.output; + let url = options + .mirror_url + .unwrap_or_else(|| default_download_url().to_string()); + let version = options + .version + .unwrap_or_else(|| default_version().to_string()); + + let parent = output.parent().ok_or_else(|| { + crate::Error::GenericError(format!("invalid target directory: {}", output.display())) + })?; + + // Check if directory exists and has valid archive.json + let archive_json_path = output.join("archive.json"); + let needs_update = if output.exists() && archive_json_path.exists() { + if options.force { + true + } else { + // Check if archive.json indicates we need an update + match check_archive_outdated(&archive_json_path, &version) { + Ok(true) => { + log::info!(action = "CEF"; "CEF directory is outdated, will update"); + true + } + Ok(false) => false, + Err(e) => { + log::warn!(action = "CEF"; "Failed to check archive.json: {e}. Will update."); + true + } + } + } + } else { + true + }; + + if !needs_update { + return Ok(()); + } + + if output.exists() { + if !options.overwrite { + return Err(crate::Error::GenericError(format!( + "target directory already exists: {}. Use overwrite=true to overwrite it.", + output.display() + ))); + } + + let dir = output + .file_name() + .and_then(|dir| dir.to_str()) + .ok_or_else(|| { + crate::Error::GenericError(format!("invalid target directory: {}", output.display())) + })?; + let old_output = parent.join(format!("old_{dir}")); + if old_output.exists() { + fs::remove_dir_all(&old_output) + .fs_context("failed to remove old output directory", old_output.clone())?; + } + fs::rename(output, &old_output) + .fs_context("failed to rename output directory", old_output.clone())?; + log::info!(action = "CEF"; "Cleaning up: {}", old_output.display()); + fs::remove_dir_all(&old_output) + .fs_context("failed to remove old output directory", old_output)?; + } + + let target_str = options.target.as_str(); + let os_arch = OsAndArch::try_from(target_str) + .map_err(|e| crate::Error::GenericError(format!("invalid target: {e}")))?; + let cef_dir = os_arch.to_string(); + let cef_dir = parent.join(&cef_dir); + + if cef_dir.exists() { + let dir = cef_dir + .file_name() + .and_then(|dir| dir.to_str()) + .ok_or_else(|| { + crate::Error::GenericError(format!("invalid target directory: {}", output.display())) + })?; + let old_cef_dir = parent.join(format!("old_{dir}")); + if old_cef_dir.exists() { + fs::remove_dir_all(&old_cef_dir) + .fs_context("failed to remove old cef directory", old_cef_dir.clone())?; + } + fs::rename(&cef_dir, &old_cef_dir) + .fs_context("failed to rename cef directory", old_cef_dir.clone())?; + log::info!(action = "CEF"; "Cleaning up: {}", old_cef_dir.display()); + fs::remove_dir_all(&old_cef_dir) + .fs_context("failed to remove old cef directory", old_cef_dir)?; + } + + let (archive, extracted_dir) = match options.archive { + Some(archive) => { + let extracted_dir = download_cef::extract_target_archive(target_str, &archive, parent, true) + .map_err(|e| crate::Error::GenericError(format!("failed to extract archive: {e}")))?; + let archive = CefFile::try_from(archive.as_path()) + .map_err(|e| crate::Error::GenericError(format!("invalid archive file: {e}")))?; + (archive, extracted_dir) + } + None => { + log::info!(action = "CEF"; "Downloading CEF version {version} for target {target_str}"); + let index = CefIndex::download_from(&url) + .map_err(|e| crate::Error::GenericError(format!("failed to download CEF index: {e}")))?; + let platform = index.platform(target_str).map_err(|e| { + crate::Error::GenericError(format!("platform not found for target {target_str}: {e}")) + })?; + let version = platform + .version(&version) + .map_err(|e| crate::Error::GenericError(format!("version {version} not found: {e}")))?; + + let archive = version + .download_archive_with_retry_from(&url, parent, true, Duration::from_secs(15), 3) + .map_err(|e| crate::Error::GenericError(format!("failed to download CEF archive: {e}")))?; + let extracted_dir = download_cef::extract_target_archive(target_str, &archive, parent, true) + .map_err(|e| crate::Error::GenericError(format!("failed to extract archive: {e}")))?; + + fs::remove_file(&archive).fs_context("failed to remove archive", archive.clone())?; + + let archive = version + .minimal() + .map_err(|e| { + crate::Error::GenericError(format!("failed to get minimal archive info: {e}")) + })? + .clone(); + (archive, extracted_dir) + } + }; + + if extracted_dir != cef_dir { + return Err(crate::Error::GenericError(format!( + "extracted dir {extracted_dir:?} does not match cef_dir {cef_dir:?}", + ))); + } + + archive + .write_archive_json(extracted_dir.as_path()) + .map_err(|e| crate::Error::GenericError(format!("failed to write archive.json: {e}")))?; + + if output != &cef_dir { + log::info!(action = "CEF"; "Renaming: {}", output.display()); + fs::rename(&cef_dir, output) + .fs_context("failed to rename cef directory to output", output.clone())?; + } + + // Set CEF_PATH environment variable + std::env::set_var("CEF_PATH", output.to_string_lossy().as_ref()); + log::info!(action = "CEF"; "CEF directory exported to: {}", output.display()); + + Ok(()) +} + +fn check_archive_outdated(archive_json_path: &Path, required_version: &str) -> crate::Result { + let content = fs::read_to_string(archive_json_path).fs_context( + "failed to read archive.json", + archive_json_path.to_path_buf(), + )?; + let archive_info: serde_json::Value = serde_json::from_str(&content) + .map_err(|e| crate::Error::GenericError(format!("failed to parse archive.json: {e}")))?; + + if let Some(name) = archive_info.get("name").and_then(|v| v.as_str()) { + Ok(!name.contains(required_version)) + } else { + Ok(true) + } +} + +pub fn ensure_cef_directory( + target: Option<&str>, + enabled_features: &[String], +) -> crate::Result> { + // Check if cef feature is enabled + let cef_enabled = enabled_features + .iter() + .any(|f| f == "cef" || f == "tauri/cef"); + + if !cef_enabled { + return Ok(None); + } + + let target = target.unwrap_or(DEFAULT_TARGET); + let os_arch = OsAndArch::try_from(target) + .map_err(|e| crate::Error::GenericError(format!("invalid target: {e}")))?; + + let cef_dir = if let Ok(cef_path) = std::env::var("CEF_PATH") { + let path = PathBuf::from(cef_path); + // If CEF_PATH is set but directory doesn't exist, we'll create it + if !path.exists() { + log::info!(action = "CEF"; "CEF_PATH set but directory doesn't exist, will export to {}", path.display()); + } + path + } else { + // Default to a directory in the user's cache or home directory + let base_dir = dirs::cache_dir() + .or_else(dirs::home_dir) + .ok_or_else(|| crate::Error::GenericError("failed to find cache or home directory".into()))?; + base_dir.join("tauri-cef").join(os_arch.to_string()) + }; + + export_cef_directory(ExporterOptions { + output: cef_dir.clone(), + target: target.to_string(), + version: None, + mirror_url: None, + force: false, + overwrite: true, + archive: None, + })?; + + std::env::set_var("CEF_PATH", cef_dir.to_string_lossy().as_ref()); + + Ok(Some(cef_dir)) +} diff --git a/crates/tauri-cli/src/interface/rust/cef.rs b/crates/tauri-cli/src/cef/macos_dev.rs similarity index 94% rename from crates/tauri-cli/src/interface/rust/cef.rs rename to crates/tauri-cli/src/cef/macos_dev.rs index e0e083344..1ad1cedfd 100644 --- a/crates/tauri-cli/src/interface/rust/cef.rs +++ b/crates/tauri-cli/src/cef/macos_dev.rs @@ -1,11 +1,8 @@ -// Copyright 2019-2024 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - #![cfg(target_os = "macos")] -use super::{desktop::DevChild, ExitReason, Options, RustAppSettings, RustupTarget}; -use crate::{error::ErrorExt, interface::AppSettings, CommandExt}; +use crate::interface::rust::desktop::DevChild; +use crate::interface::{AppSettings, ExitReason, Options, RustAppSettings, RustupTarget}; +use crate::{error::ErrorExt, CommandExt}; use serde::Serialize; use shared_child::SharedChild; @@ -112,8 +109,8 @@ fn copy_dir_all(src: &Path, dst: &Path) -> crate::Result<()> { Ok(()) } -pub fn run_dev_cef_macos, ExitReason) + Send + Sync + 'static>( - app_settings: &RustAppSettings, +pub fn run_dev_cef_macos, ExitReason) + Send + Sync + 'static>( + app_settings: &A, options: Options, run_args: Vec, available_targets: &mut Option>, @@ -121,8 +118,12 @@ pub fn run_dev_cef_macos, ExitReason) + Send + Sync + 'static> on_exit: F, ) -> crate::Result { // Build the app - let mut build_cmd = - super::desktop::cargo_command(false, options.clone(), available_targets, config_features)?; + let mut build_cmd = crate::interface::rust::desktop::cargo_command( + false, + options.clone(), + available_targets, + config_features, + )?; build_cmd.env("CARGO_TERM_PROGRESS_WIDTH", "80"); build_cmd.env("CARGO_TERM_PROGRESS_WHEN", "always"); match build_cmd.piped() { diff --git a/crates/tauri-cli/src/cef/mod.rs b/crates/tauri-cli/src/cef/mod.rs new file mode 100644 index 000000000..3ea1c2ae0 --- /dev/null +++ b/crates/tauri-cli/src/cef/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(target_os = "macos")] +pub mod macos_dev; + +pub mod exporter; diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index d183bf044..10fd856e8 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -39,8 +39,6 @@ use crate::{ use tauri_utils::{display_path, platform::Target as TargetPlatform}; mod cargo_config; -#[cfg(target_os = "macos")] -mod cef; mod desktop; pub mod installation; pub mod manifest; @@ -189,6 +187,12 @@ impl Interface for Rust { } fn build(&mut self, options: Options) -> crate::Result { + ensure_cef_directory_if_needed( + &self.app_settings, + self.config_features.clone(), + options.target.as_deref(), + &options.features, + )?; desktop::build( options, &self.app_settings, @@ -203,6 +207,12 @@ impl Interface for Rust { mut options: Options, on_exit: F, ) -> crate::Result<()> { + ensure_cef_directory_if_needed( + &self.app_settings, + self.config_features.clone(), + options.target.as_deref(), + &options.features, + )?; let on_exit = Arc::new(on_exit); let mut run_args = Vec::new(); @@ -499,6 +509,34 @@ fn get_watch_folders(additional_watch_folders: &[PathBuf]) -> crate::Result, + target: Option<&str>, + features: &Option>, +) -> crate::Result<()> { + let mut merged_features = config_features; + if let Some(f) = features { + merged_features.extend(f.clone()); + } + let enabled_features = app_settings + .manifest + .lock() + .unwrap() + .all_enabled_features(&merged_features); + let target_triple = target.or_else(|| { + app_settings + .cargo_config + .build() + .target() + .map(|t| t.as_ref()) + }); + if let Err(e) = crate::cef::exporter::ensure_cef_directory(target_triple, &enabled_features) { + log::warn!(action = "CEF"; "Failed to ensure CEF directory: {}. Continuing anyway.", e); + } + Ok(()) +} + impl Rust { pub fn build_options( &self, diff --git a/crates/tauri-cli/src/interface/rust/desktop.rs b/crates/tauri-cli/src/interface/rust/desktop.rs index 575bc9ab0..36115bf96 100644 --- a/crates/tauri-cli/src/interface/rust/desktop.rs +++ b/crates/tauri-cli/src/interface/rust/desktop.rs @@ -71,7 +71,7 @@ pub fn run_dev, ExitReason) + Send + Sync + 'static>( let cef_enabled = enabled_features.contains(&"cef".to_string()) || enabled_features.contains(&"tauri/cef".to_string()); if cef_enabled { - return super::cef::run_dev_cef_macos( + return crate::cef::macos_dev::run_dev_cef_macos( app_settings, options, run_args, diff --git a/crates/tauri-cli/src/lib.rs b/crates/tauri-cli/src/lib.rs index b473f63a7..cfb724c4c 100644 --- a/crates/tauri-cli/src/lib.rs +++ b/crates/tauri-cli/src/lib.rs @@ -14,6 +14,7 @@ mod acl; mod add; mod build; mod bundle; +mod cef; mod completions; mod dev; mod error; @@ -122,6 +123,8 @@ pub struct VersionMetadata { tauri_build: String, #[serde(rename = "tauri-plugin")] tauri_plugin: String, + #[serde(rename = "cef")] + cef: String, } #[derive(Deserialize)] diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index d52dc6956..b22de3de9 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -1295,7 +1295,7 @@ impl App { let app_handle = self.handle().clone(); let manager = self.manager.clone(); - move |event| match dbg!(&event) { + move |event| match &event { RuntimeRunEvent::Ready => { if let Err(e) = setup(&mut self) { panic!("Failed to setup app: {e}");