ensure CEF is installed (CLI)

This commit is contained in:
Lucas Nogueira
2025-11-04 12:56:13 -03:00
parent 2e291e1e85
commit 193d0edfb4
13 changed files with 393 additions and 18 deletions

View File

@@ -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",

View File

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

22
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"
}

View File

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

View File

@@ -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<String> = 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<String> = 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<String>,
pub mirror_url: Option<String>,
pub force: bool,
pub overwrite: bool,
pub archive: Option<PathBuf>,
}
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<bool> {
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<Option<PathBuf>> {
// 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))
}

View File

@@ -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<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
app_settings: &RustAppSettings,
pub fn run_dev_cef_macos<A: AppSettings, F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
app_settings: &A,
options: Options,
run_args: Vec<String>,
available_targets: &mut Option<Vec<RustupTarget>>,
@@ -121,8 +118,12 @@ pub fn run_dev_cef_macos<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>
on_exit: F,
) -> crate::Result<DevChild> {
// 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() {

View File

@@ -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;

View File

@@ -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<PathBuf> {
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<Vec<
Ok(watch_folders)
}
fn ensure_cef_directory_if_needed(
app_settings: &RustAppSettings,
config_features: Vec<String>,
target: Option<&str>,
features: &Option<Vec<String>>,
) -> 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,

View File

@@ -71,7 +71,7 @@ pub fn run_dev<F: Fn(Option<i32>, 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,

View File

@@ -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)]

View File

@@ -1295,7 +1295,7 @@ impl<R: Runtime> App<R> {
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}");