feat: egui + cef + osr

This commit is contained in:
Louis 2025-10-09 22:44:12 +07:00
parent 01050f3103
commit 8a5d7a0906
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
59 changed files with 492484 additions and 0 deletions

46
browser/Cargo.toml Normal file
View File

@ -0,0 +1,46 @@
[workspace]
resolver = "2"
members = [
"download-cef",
"get-latest",
"update-bindings",
"export-cef-dir",
"sys",
"cef",
"app",
]
[workspace.package]
version = "140.3.1+140.1.14"
edition = "2021"
license = "Apache-2.0 OR MIT"
authors = []
repository = "https://github.com/menloresearch/jan"
[workspace.dependencies]
cef = { path = "cef" }
cef-dll-sys = { version = "140.3.1", path = "sys" }
download-cef = { version = "2.2", path = "download-cef" }
anyhow = "1"
bindgen = "0.72"
clap = { version = "4", features = ["derive"] }
cmake = "0.1.52"
convert_case = "0.8"
git-cliff = "2"
git-cliff-core = "2"
plist = "1"
proc-macro2 = "1"
quote = "1"
regex = "1"
semver = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
syn = { version = "2", features = ["full"] }
thiserror = "2"
toml_edit = "0.23"
[workspace.dependencies.windows-sys]
version = "0.61"
features = ["Win32_System_Environment", "Win32_System_LibraryLoader"]

74
browser/README.md Normal file
View File

@ -0,0 +1,74 @@
# cef-rs
Use CEF in Rust.
## Supported Targets
| Target | Linux | macOS | Windows |
| ------ | ----- | ----- | ------- |
| x86_64 | ✅ | ✅ | ✅ |
| ARM64 | ✅ | ✅ | ✅ |
## Usage
### Install Shared CEF Binaries
This step is optional, but it will make all other builds of the `cef` crate much faster. If you don't do this, the `cef-dll-sys` crate `build.rs` script will download and extract the same files under its `OUT_DIR` directory. You should repeat this step each time you upgrade to a new version of the `cef` crate.
#### Linux or macOS:
```sh
cargo run -p export-cef-dir -- --force $HOME/.local/share/cef
```
#### Windows (using PowerShell)
```pwsh
cargo run -p export-cef-dir -- --force $env:USERPROFILE/.local/share/cef
```
### Set Environment Variables
#### Linux
```sh
export CEF_PATH="$HOME/.local/share/cef"
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$CEF_PATH"
```
#### macOS
```sh
export CEF_PATH="$HOME/.local/share/cef"
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$CEF_PATH:$CEF_PATH/Chromium Embedded Framework.framework/Libraries"
```
#### Windows (using PowerShell)
```pwsh
$env:CEF_PATH="$env:USERPROFILE/.local/share/cef"
$env:PATH="$env:PATH;$env:CEF_PATH"
```
### Run the `app`
#### Linux
```sh
cargo run --bin app
```
#### macOS
```sh
cargo run --bin bundle_app
open target/debug/app.app
```
#### Windows (using PowerShell)
```pwsh
cp ./app/win/jan.exe.manifest ./target/debug/
cargo run --bin app
```

38
browser/app/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "app"
edition = "2024"
publish = false
[[bin]]
name = "app"
[[bin]]
name = "app_helper"
path = "src/mac/helper.rs"
[[bin]]
name = "bundle_app"
path = "src/mac/bundle_app.rs"
[features]
default = ["sandbox"]
sandbox = ["cef/sandbox"]
[dependencies]
cef.workspace = true
cef-dll-sys.workspace = true
parking_lot = "0.12"
once_cell = "1.19"
tokio = { version = "1", features = ["time"] }
egui = "0.29"
egui-wgpu = "0.29"
egui-winit = "0.29"
wgpu = "22"
winit = { version = "0.30", features = [] }
pollster = "0.4"
env_logger = "0.11"
[target.'cfg(target_os = "macos")'.dependencies]
plist.workspace = true
serde.workspace = true

View File

@ -0,0 +1,182 @@
#[cfg(target_os = "macos")]
mod mac {
use serde::Serialize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Serialize)]
struct InfoPlist {
#[serde(rename = "CFBundleDevelopmentRegion")]
cf_bundle_development_region: String,
#[serde(rename = "CFBundleDisplayName")]
cf_bundle_display_name: String,
#[serde(rename = "CFBundleExecutable")]
cf_bundle_executable: String,
#[serde(rename = "CFBundleIdentifier")]
cf_bundle_identifier: String,
#[serde(rename = "CFBundleInfoDictionaryVersion")]
cf_bundle_info_dictionary_version: String,
#[serde(rename = "CFBundleName")]
cf_bundle_name: String,
#[serde(rename = "CFBundlePackageType")]
cf_bundle_package_type: String,
#[serde(rename = "CFBundleSignature")]
cf_bundle_signature: String,
#[serde(rename = "CFBundleVersion")]
cf_bundle_version: String,
#[serde(rename = "CFBundleShortVersionString")]
cf_bundle_short_version_string: String,
#[serde(rename = "LSEnvironment")]
ls_environment: HashMap<String, String>,
#[serde(rename = "LSFileQuarantineEnabled")]
ls_file_quarantine_enabled: bool,
#[serde(rename = "LSMinimumSystemVersion")]
ls_minimum_system_version: String,
#[serde(rename = "LSUIElement")]
ls_ui_element: Option<String>,
#[serde(rename = "NSBluetoothAlwaysUsageDescription")]
ns_bluetooth_always_usage_description: String,
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
ns_supports_automatic_graphics_switching: bool,
#[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")]
ns_web_browser_publickey_credential_usage_description: String,
#[serde(rename = "NSCameraUsageDescription")]
ns_camera_usage_description: String,
#[serde(rename = "NSMicrophoneUsageDescription")]
ns_microphone_usage_description: String,
}
const EXEC_PATH: &str = "Contents/MacOS";
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
const RESOURCES_PATH: &str = "Contents/Resources";
const FRAMEWORK: &str = "Chromium Embedded Framework.framework";
const HELPERS: &[&str] = &[
"app Helper (GPU)",
"app Helper (Renderer)",
"app Helper (Plugin)",
"app Helper (Alerts)",
"app Helper",
];
fn create_app_layout(app_path: &Path) -> PathBuf {
[EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH]
.iter()
.for_each(|p| fs::create_dir_all(app_path.join(p)).unwrap());
app_path.join("Contents")
}
fn create_app(app_path: &Path, exec_name: &str, bin: &Path, is_helper: bool) -> PathBuf {
let app_path = app_path.join(exec_name).with_extension("app");
let contents_path = create_app_layout(&app_path);
create_info_plist(&contents_path, exec_name, is_helper).unwrap();
fs::copy(bin, app_path.join(EXEC_PATH).join(exec_name)).unwrap();
app_path
}
// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos
fn bundle(app_path: &Path) {
let example_path = PathBuf::from(app_path);
let main_app_path = create_app(
app_path,
"app",
&example_path.join("app"),
false,
);
let cef_path = cef_dll_sys::get_cef_dir().unwrap();
let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK);
if to.exists() {
fs::remove_dir_all(&to).unwrap();
}
copy_directory(&cef_path.join(FRAMEWORK), &to);
HELPERS.iter().for_each(|helper| {
create_app(
&main_app_path.join(FRAMEWORKS_PATH),
helper,
&example_path.join("app_helper"),
true,
);
});
}
fn create_info_plist(
contents_path: &Path,
exec_name: &str,
is_helper: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let info_plist = InfoPlist {
cf_bundle_development_region: "en".to_string(),
cf_bundle_display_name: exec_name.to_string(),
cf_bundle_executable: exec_name.to_string(),
cf_bundle_identifier: "app.jan.ai.helper".to_string(),
cf_bundle_info_dictionary_version: "6.0".to_string(),
cf_bundle_name: "cef-rs".to_string(),
cf_bundle_package_type: "APPL".to_string(),
cf_bundle_signature: "????".to_string(),
cf_bundle_version: "1.0.0".to_string(),
cf_bundle_short_version_string: "1.0".to_string(),
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())]
.iter()
.cloned()
.collect(),
ls_file_quarantine_enabled: true,
ls_minimum_system_version: "11.0".to_string(),
ls_ui_element: if is_helper {
Some("1".to_string())
} else {
None
},
ns_bluetooth_always_usage_description: exec_name.to_string(),
ns_supports_automatic_graphics_switching: true,
ns_web_browser_publickey_credential_usage_description: exec_name.to_string(),
ns_camera_usage_description: exec_name.to_string(),
ns_microphone_usage_description: exec_name.to_string(),
};
plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?;
Ok(())
}
fn copy_directory(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_directory(&entry.path(), &dst_path);
} else {
fs::copy(&entry.path(), &dst_path).unwrap();
}
}
}
fn run_command(args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new("cargo")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()?;
if !status.success() {
std::process::exit(1);
}
Ok(())
}
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
let app_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../target/debug");
run_command(&["build", "--bin", "app"])?;
run_command(&["build", "--bin", "app_helper"])?;
bundle(&app_path);
Ok(())
}
}
#[cfg(target_os = "macos")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
mac::main()
}
#[cfg(not(target_os = "macos"))]
fn main() {}

View File

@ -0,0 +1,25 @@
use cef::{args::Args, *};
fn main() {
let args = Args::new();
#[cfg(all(target_os = "macos", feature = "sandbox"))]
let _sandbox = {
let mut sandbox = cef::sandbox::Sandbox::new();
sandbox.initialize(args.as_main_args());
sandbox
};
#[cfg(target_os = "macos")]
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true);
assert!(loader.load());
loader
};
execute_process(
Some(args.as_main_args()),
None::<&mut App>,
std::ptr::null_mut(),
);
}

782
browser/app/src/main.rs Normal file
View File

@ -0,0 +1,782 @@
use cef::{args::Args, rc::*, *};
use parking_lot::RwLock;
use std::sync::Arc;
use std::time::{Duration, Instant};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
platform::pump_events::{EventLoopExtPumpEvents, PumpStatus},
window::{Window, WindowAttributes, WindowId},
};
// Shared state between CEF and rendering
static TEXTURE_BUFFER: once_cell::sync::Lazy<Arc<RwLock<Vec<u8>>>> =
once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(Vec::new())));
static TEXTURE_SIZE: once_cell::sync::Lazy<Arc<RwLock<(u32, u32)>>> =
once_cell::sync::Lazy::new(|| Arc::new(RwLock::new((1024, 768))));
static CEF_INITIALIZED: once_cell::sync::Lazy<Arc<RwLock<bool>>> =
once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(false)));
static BROWSER_INSTANCE: once_cell::sync::Lazy<Arc<RwLock<Option<Browser>>>> =
once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(None)));
// CEF App implementation
struct DemoApp {
object: *mut RcImpl<cef_dll_sys::_cef_app_t, Self>,
}
impl DemoApp {
fn new_app() -> App {
App::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl WrapApp for DemoApp {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_app_t, Self>) {
self.object = object;
}
}
impl Clone for DemoApp {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
}
}
}
impl Rc for DemoApp {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplApp for DemoApp {
fn get_raw(&self) -> *mut cef_dll_sys::_cef_app_t {
self.object.cast()
}
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(DemoBrowserProcessHandler::new_browser_process_handler())
}
}
// Browser Process Handler
struct DemoBrowserProcessHandler {
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
}
impl DemoBrowserProcessHandler {
fn new_browser_process_handler() -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl Rc for DemoBrowserProcessHandler {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapBrowserProcessHandler for DemoBrowserProcessHandler {
fn wrap_rc(
&mut self,
object: *mut RcImpl<cef_dll_sys::_cef_browser_process_handler_t, Self>,
) {
self.object = object;
}
}
impl Clone for DemoBrowserProcessHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
}
}
}
impl ImplBrowserProcessHandler for DemoBrowserProcessHandler {
fn get_raw(&self) -> *mut cef_dll_sys::_cef_browser_process_handler_t {
self.object.cast()
}
fn on_context_initialized(&self) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
println!("CEF context initialized");
*CEF_INITIALIZED.write() = true;
let mut client = DemoClient::new_client();
let url = CefString::from("https://www.google.com");
let window_info = WindowInfo {
windowless_rendering_enabled: 1,
..Default::default()
};
let settings = BrowserSettings::default();
let _browser = browser_host_create_browser(
Some(&window_info),
Some(&mut client),
Some(&url),
Some(&settings),
Option::<&mut DictionaryValue>::None,
Option::<&mut RequestContext>::None,
);
}));
}
}
// Client
struct DemoClient {
object: *mut RcImpl<cef_dll_sys::_cef_client_t, Self>,
}
impl DemoClient {
fn new_client() -> Client {
Client::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl WrapClient for DemoClient {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_client_t, Self>) {
self.object = object;
}
}
impl Clone for DemoClient {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
}
}
}
impl Rc for DemoClient {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplClient for DemoClient {
fn get_raw(&self) -> *mut cef_dll_sys::_cef_client_t {
self.object.cast()
}
fn render_handler(&self) -> Option<RenderHandler> {
Some(DemoRenderHandler::new_render_handler())
}
fn life_span_handler(&self) -> Option<LifeSpanHandler> {
Some(DemoLifeSpanHandler::new_life_span_handler())
}
}
// LifeSpan Handler to capture browser instance
struct DemoLifeSpanHandler {
object: *mut RcImpl<cef_dll_sys::_cef_life_span_handler_t, Self>,
}
impl DemoLifeSpanHandler {
fn new_life_span_handler() -> LifeSpanHandler {
LifeSpanHandler::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl WrapLifeSpanHandler for DemoLifeSpanHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_life_span_handler_t, Self>) {
self.object = object;
}
}
impl Clone for DemoLifeSpanHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
}
}
}
impl Rc for DemoLifeSpanHandler {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplLifeSpanHandler for DemoLifeSpanHandler {
fn get_raw(&self) -> *mut cef_dll_sys::_cef_life_span_handler_t {
self.object.cast()
}
fn on_after_created(&self, browser: Option<&mut Browser>) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if let Some(browser) = browser {
// Store the browser instance for later use
*BROWSER_INSTANCE.write() = Some(browser.clone());
println!("Browser created and stored!");
}
}));
}
}
// Render Handler
struct DemoRenderHandler {
object: *mut RcImpl<cef_dll_sys::_cef_render_handler_t, Self>,
}
impl DemoRenderHandler {
fn new_render_handler() -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl WrapRenderHandler for DemoRenderHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for DemoRenderHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
}
}
}
impl Rc for DemoRenderHandler {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplRenderHandler for DemoRenderHandler {
fn get_raw(&self) -> *mut cef_dll_sys::_cef_render_handler_t {
self.object.cast()
}
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if let Some(rect) = rect {
let size = TEXTURE_SIZE.read();
rect.x = 0;
rect.y = 0;
rect.width = size.0 as i32;
rect.height = size.1 as i32;
}
}));
}
fn on_paint(
&self,
_browser: Option<&mut Browser>,
_type_: PaintElementType,
_dirty_rects_count: usize,
_dirty_rects: Option<&Rect>,
buffer: *const u8,
width: ::std::os::raw::c_int,
height: ::std::os::raw::c_int,
) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if buffer.is_null() || width <= 0 || height <= 0 {
return;
}
let size = (width * height * 4) as usize;
let slice = unsafe { std::slice::from_raw_parts(buffer, size) };
let mut texture_buffer = TEXTURE_BUFFER.write();
texture_buffer.clear();
texture_buffer.extend_from_slice(slice);
let mut texture_size = TEXTURE_SIZE.write();
*texture_size = (width as u32, height as u32);
}));
}
}
// egui + wgpu Application
struct EguiCefApp {
window: Option<Arc<Window>>,
egui_state: Option<EguiState>,
url_input: String,
frame_count: u64,
last_update: Instant,
}
struct EguiState {
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
egui_renderer: egui_wgpu::Renderer,
egui_state: egui_winit::State,
cef_texture: Option<wgpu::Texture>,
}
impl EguiCefApp {
fn new() -> Self {
Self {
window: None,
egui_state: None,
url_input: String::from("https://www.google.com"),
frame_count: 0,
last_update: Instant::now(),
}
}
}
impl ApplicationHandler for EguiCefApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.window.is_some() {
return;
}
let window_attrs = WindowAttributes::default()
.with_title("egui + CEF Browser")
.with_inner_size(winit::dpi::LogicalSize::new(1024, 768));
let window = Arc::new(event_loop.create_window(window_attrs).unwrap());
// Initialize wgpu and egui
let egui_state = pollster::block_on(async {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("Device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: Default::default(),
},
None,
)
.await
.unwrap();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(surface_caps.formats[0]);
let size = window.inner_size();
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
let egui_ctx = egui::Context::default();
let viewport_id = egui_ctx.viewport_id();
let max_texture_side = Some(device.limits().max_texture_dimension_2d as usize);
let egui_state = egui_winit::State::new(
egui_ctx,
viewport_id,
&window,
Some(window.scale_factor() as f32),
Some(winit::window::Theme::Dark),
max_texture_side,
);
let egui_renderer = egui_wgpu::Renderer::new(&device, surface_format, None, 1, false);
EguiState {
device,
queue,
surface,
surface_config,
egui_renderer,
egui_state,
cef_texture: None,
}
});
self.egui_state = Some(egui_state);
self.window = Some(window);
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let Some(window) = &self.window else {
return;
};
let Some(state) = &mut self.egui_state else {
return;
};
// Let egui handle the event first
let response = state.egui_state.on_window_event(window, &event);
if response.repaint {
window.request_redraw();
}
if response.consumed {
return;
}
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::Resized(new_size) => {
if new_size.width > 0 && new_size.height > 0 {
state.surface_config.width = new_size.width;
state.surface_config.height = new_size.height;
state.surface.configure(&state.device, &state.surface_config);
window.request_redraw();
}
}
WindowEvent::RedrawRequested => {
self.render();
}
_ => {}
}
}
}
impl EguiCefApp {
fn render(&mut self) {
let Some(window) = &self.window else {
return;
};
let Some(state) = &mut self.egui_state else {
return;
};
self.frame_count += 1;
// Build egui UI
let raw_input = state.egui_state.take_egui_input(window);
let egui_output = state.egui_state.egui_ctx().run(raw_input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("🦀 egui + CEF Browser Integration");
ui.separator();
ui.horizontal(|ui| {
ui.label("URL:");
ui.text_edit_singleline(&mut self.url_input);
if ui.button("Navigate").clicked() {
// Navigate to the URL
let url = self.url_input.clone();
if let Some(browser) = BROWSER_INSTANCE.read().as_ref() {
if let Some(main_frame) = browser.main_frame() {
let cef_url = CefString::from(url.as_str());
main_frame.load_url(Some(&cef_url));
}
}
}
});
ui.separator();
let cef_ready = *CEF_INITIALIZED.read();
ui.label(format!("CEF Status: {}", if cef_ready { "✅ Ready" } else { "⏳ Initializing..." }));
ui.label(format!("Frame: {}", self.frame_count));
ui.label(format!("FPS: {:.1}", 1.0 / self.last_update.elapsed().as_secs_f32()));
ui.separator();
// Display CEF texture
let texture_buffer = TEXTURE_BUFFER.read();
let size = TEXTURE_SIZE.read();
if !texture_buffer.is_empty() && size.0 > 0 && size.1 > 0 {
// Update or create wgpu texture from CEF buffer
if state.cef_texture.is_none()
|| state.cef_texture.as_ref().unwrap().width() != size.0
|| state.cef_texture.as_ref().unwrap().height() != size.1
{
state.cef_texture = Some(state.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width: size.0,
height: size.1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}));
}
// Convert BGRA to RGBA
let mut rgba_buffer = Vec::with_capacity(texture_buffer.len());
for chunk in texture_buffer.chunks_exact(4) {
rgba_buffer.push(chunk[2]); // R
rgba_buffer.push(chunk[1]); // G
rgba_buffer.push(chunk[0]); // B
rgba_buffer.push(chunk[3]); // A
}
if let Some(texture) = &state.cef_texture {
state.queue.write_texture(
wgpu::ImageCopyTexture {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&rgba_buffer,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * size.0),
rows_per_image: Some(size.1),
},
wgpu::Extent3d {
width: size.0,
height: size.1,
depth_or_array_layers: 1,
},
);
// Register texture with egui
let texture_id = state.egui_renderer.register_native_texture(
&state.device,
&texture.create_view(&wgpu::TextureViewDescriptor::default()),
wgpu::FilterMode::Linear,
);
ui.image(egui::ImageSource::Texture(egui::load::SizedTexture::new(
texture_id,
[size.0 as f32, size.1 as f32],
)));
}
} else {
ui.label("⏳ Waiting for CEF to render...");
}
});
});
state.egui_state.handle_platform_output(window, egui_output.platform_output);
// Render
let output = state.surface.get_current_texture().unwrap();
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = state.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
let screen_descriptor = egui_wgpu::ScreenDescriptor {
size_in_pixels: [state.surface_config.width, state.surface_config.height],
pixels_per_point: window.scale_factor() as f32,
};
let primitives = state.egui_state.egui_ctx().tessellate(egui_output.shapes, egui_output.pixels_per_point);
// Update textures
for (id, image_delta) in &egui_output.textures_delta.set {
state.egui_renderer.update_texture(&state.device, &state.queue, *id, image_delta);
}
// Update buffers
state.egui_renderer.update_buffers(
&state.device,
&state.queue,
&mut encoder,
&primitives,
&screen_descriptor,
);
// Render
{
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.1,
g: 0.1,
b: 0.1,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
// Render egui
state.egui_renderer.render(
&mut render_pass.forget_lifetime(),
&primitives,
&screen_descriptor,
);
} // render_pass dropped here automatically
for id in &egui_output.textures_delta.free {
state.egui_renderer.free_texture(id);
}
state.queue.submit(std::iter::once(encoder.finish()));
output.present();
self.last_update = Instant::now();
window.request_redraw();
}
}
fn main() {
env_logger::init();
#[cfg(target_os = "macos")]
let _loader = {
let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
if !loader.load() {
eprintln!("Failed to load CEF library");
return;
}
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = Args::new();
let cmd = args.as_cmd_line().unwrap();
let switch = CefString::from("type");
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
let mut app = DemoApp::new_app();
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
// Handle non-browser processes (renderer, GPU, etc.)
if !is_browser_process {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
println!("launch process {process_type}");
assert!(ret >= 0, "cannot execute non-browser process");
return;
}
// Browser process - initialize CEF with external message pump
println!("launch browser process");
assert!(ret == -1, "cannot execute browser process");
let mut settings = Settings {
no_sandbox: !cfg!(feature = "sandbox") as _,
windowless_rendering_enabled: 1,
external_message_pump: 1, // Key: allows manual event loop control
..Default::default()
};
let init_result = initialize(
Some(args.as_main_args()),
Some(&mut settings),
Some(&mut app),
std::ptr::null_mut(),
);
if init_result != 1 {
eprintln!("Failed to initialize CEF: {}", init_result);
return;
}
println!("CEF initialized successfully with external message pump");
// Run event loop with manual pumping
let mut event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let mut egui_app = EguiCefApp::new();
loop {
// Pump CEF messages
do_message_loop_work();
// Pump winit events
let status = event_loop.pump_app_events(Some(Duration::ZERO), &mut egui_app);
if let PumpStatus::Exit(code) = status {
println!("Exiting with code: {}", code);
break;
}
// Small sleep to prevent busy-waiting
std::thread::sleep(Duration::from_millis(1));
}
shutdown();
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- https://cef-builds.spotifycdn.com/cef_binary_132.3.2+g4997b2f+chromium-132.0.6834.161_windows64.tar.bz2 -->
<!-- This compatibility section is from the standar pre - built binary package, specifically from %CEF_ROOT%/tests/app/win/compatibility.manifest -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!--The ID below indicates application support for Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!--The ID below indicates application support for Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!--The ID below indicates application support for Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!--The ID below indicates application support for Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!--The ID below indicates application support for Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- This tag is required for XAML islands usage in the process for media scenarios. -->
<!-- This version corresponds to the Windows 10 May 2019 Update. -->
<maxversiontested Id="10.0.18362.0"/>
</application>
</compatibility>
<!--The compatibility section will be merged from build/win/compatibility.manifest -->
<dependency>
<dependentAssembly>
<assemblyIdentity type="Win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

215
browser/cef/CHANGELOG.md Normal file
View File

@ -0,0 +1,215 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [140.3.1+140.1.14](https://github.com/tauri-apps/cef-rs/compare/cef-v140.3.0+140.1.14...cef-v140.3.1+140.1.14) - 2025-10-03
### Fixed
- copy wrapped out-params back to pointers ([#224](https://github.com/tauri-apps/cef-rs/pull/224))
### Other
- update bindings
- *(test)* test out-params
## [140.2.0+140.1.14](https://github.com/tauri-apps/cef-rs/compare/cef-v140.1.0+140.1.13...cef-v140.2.0+140.1.14) - 2025-09-21
### Other
- *(release)* update CEF version to 140.1.14
## [140.1.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/cef-v140.0.0+140.1.13...cef-v140.1.0+140.1.13) - 2025-09-19
### Other
- release
## [140.0.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/cef-v139.8.0+139.0.40...cef-v140.0.0+140.1.13) - 2025-09-19
### Other
- update bindings
- *(release)* update CEF version to 140.1.13
## [139.8.0+139.0.40](https://github.com/tauri-apps/cef-rs/compare/cef-v139.7.2+139.0.38...cef-v139.8.0+139.0.40) - 2025-09-12
### Other
- *(release)* update CEF version to 139.0.40
## [139.7.2+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-v139.7.1+139.0.38...cef-v139.7.2+139.0.38) - 2025-09-08
### Fixed
- cleanup logic for copying back out-params
- handle out-params ([#173](https://github.com/tauri-apps/cef-rs/issues/173))
### Other
- release v139.7.2+139.0.38
- update bindings
## [139.7.1+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-v139.7.0+139.0.38...cef-v139.7.1+139.0.38) - 2025-09-07
### Other
- release v139.7.1+139.0.38
- *(deps)* update rust crate windows-sys to 0.61
## [139.7.0+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-v139.6.0+139.0.37...cef-v139.7.0+139.0.38) - 2025-08-31
### Other
- *(release)* update CEF version to 139.0.38
## [139.6.0+139.0.37](https://github.com/tauri-apps/cef-rs/compare/cef-v139.5.0+139.0.30...cef-v139.6.0+139.0.37) - 2025-08-29
### Other
- *(release)* update CEF version to 139.0.37
## [139.5.0+139.0.30](https://github.com/tauri-apps/cef-rs/compare/cef-v139.4.0+139.0.28...cef-v139.5.0+139.0.30) - 2025-08-28
### Other
- *(release)* update CEF version to 139.0.30
## [139.4.0+139.0.28](https://github.com/tauri-apps/cef-rs/compare/cef-v139.3.0+139.0.26...cef-v139.4.0+139.0.28) - 2025-08-23
### Other
- *(release)* update CEF version to 139.0.28
## [139.3.0+139.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-v139.2.1+139.0.23...cef-v139.3.0+139.0.26) - 2025-08-22
### Other
- *(release)* update CEF version to 139.0.26
## [139.2.1+139.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-v139.2.0+139.0.23...cef-v139.2.1+139.0.23) - 2025-08-16
### Fixed
- warnings about usize < 0 comparisons
### Other
- release
## [139.2.0+139.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-v139.1.0+139.0.20...cef-v139.2.0+139.0.23) - 2025-08-16
### Other
- *(release)* update CEF version to 139.0.23
## [139.1.0+139.0.20](https://github.com/tauri-apps/cef-rs/compare/cef-v139.0.1+139.0.17...cef-v139.1.0+139.0.20) - 2025-08-15
### Other
- *(release)* update CEF version to 139.0.20
## [139.0.1+139.0.17](https://github.com/tauri-apps/cef-rs/compare/cef-v139.0.0+139.0.17...cef-v139.0.1+139.0.17) - 2025-08-08
### Other
- release v139.0.1+139.0.17
## [139.0.0+139.0.17](https://github.com/tauri-apps/cef-rs/compare/cef-v138.9.0+138.0.36...cef-v139.0.0+139.0.17) - 2025-08-08
### Other
- *(release)* update CEF version to 139.0.17
## [138.9.0+138.0.36](https://github.com/tauri-apps/cef-rs/compare/cef-v138.8.0+138.0.34...cef-v138.9.0+138.0.36) - 2025-08-07
### Other
- *(release)* update CEF version to 138.0.36
## [138.8.0+138.0.34](https://github.com/tauri-apps/cef-rs/compare/cef-v138.7.1+138.0.33...cef-v138.8.0+138.0.34) - 2025-08-02
### Fixed
- remove cef version from example dependencies
### Other
- *(release)* update CEF version to 138.0.34
## [138.7.1+138.0.33](https://github.com/tauri-apps/cef-rs/compare/cef-v138.7.0+138.0.33...cef-v138.7.1+138.0.33) - 2025-07-29
### Other
- release v138.7.1+138.0.33
- move examples into separate crates
## [138.7.0+138.0.33](https://github.com/tauri-apps/cef-rs/compare/cef-v138.6.1+138.0.27...cef-v138.7.0+138.0.33) - 2025-07-29
### Other
- *(release)* update CEF version to 138.0.33
## [138.6.1+138.0.27](https://github.com/tauri-apps/cef-rs/compare/cef-v138.6.0+138.0.27...cef-v138.6.1+138.0.27) - 2025-07-28
### Fixed
- embed git-cliff as a library in get-latest
### Other
- *(release)* bump version for get-latest updates
## [138.6.0+138.0.27](https://github.com/tauri-apps/cef-rs/compare/cef-v138.5.1+138.0.26...cef-v138.6.0+138.0.27) - 2025-07-28
### Added
- update CEF version to 138.0.27
### Fixed
- bump version for release
## [138.5.1+138.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-v138.5.0+138.0.26...cef-v138.5.1+138.0.26) - 2025-07-22
### Other
- release
- *(doc)* regenerate CHANGELOG.md
## [138.5.0+138.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-v138.4.0+138.0.25...cef-v138.5.0+138.0.26) - 2025-07-19
### Other
- update CEF version
## [138.4.0+138.0.25](https://github.com/tauri-apps/cef-rs/compare/cef-v138.3.0+138.0.23...cef-v138.4.0+138.0.25) - 2025-07-18
### Other
- update CEF version
## [138.3.0+138.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-v138.2.2+138.0.21...cef-v138.3.0+138.0.23) - 2025-07-17
### Other
- update CEF version
## [138.2.2+138.0.21](https://github.com/tauri-apps/cef-rs/compare/cef-v138.2.1+138.0.21...cef-v138.2.2+138.0.21) - 2025-07-14
### Other
- release
- seed CHANGELOG.md files
## [138.2.1+138.0.21](https://github.com/tauri-apps/cef-rs/compare/cef-v138.2.0+138.0.21...cef-v138.2.1+138.0.21) - 2025-07-14
### Fixed
- bump major version of download-cef [#145](https://github.com/tauri-apps/cef-rs/issues/145)

26
browser/cef/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "cef"
description = "Use cef in Rust"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[features]
default = ["sandbox"]
dox = ["cef-dll-sys/dox"]
sandbox = ["cef-dll-sys/sandbox"]
[package.metadata.docs.rs]
features = ["dox"]
[dependencies]
cef-dll-sys.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
libloading = "0.8"

3
browser/cef/README.md Normal file
View File

@ -0,0 +1,3 @@
# cef
Use the [Chromium Embedded Framework](https://github.com/chromiumembedded/cef) in Rust.

83
browser/cef/src/args.rs Normal file
View File

@ -0,0 +1,83 @@
#[cfg(not(target_os = "windows"))]
use std::ffi::{c_char, CString};
use crate::*;
#[derive(Clone, Default)]
pub struct Args {
#[cfg(not(target_os = "windows"))]
_source: Vec<CString>,
#[cfg(not(target_os = "windows"))]
_argv: Vec<*const c_char>,
main_args: MainArgs,
}
impl Args {
#[cfg(target_os = "windows")]
pub fn new() -> Self {
let main_args = MainArgs {
instance: cef_dll_sys::HINSTANCE(
unsafe {
windows_sys::Win32::System::LibraryLoader::GetModuleHandleW(std::ptr::null())
}
.cast(),
),
};
Self { main_args }
}
#[cfg(not(target_os = "windows"))]
pub fn new() -> Self {
let args = std::env::args();
let _source = args
.into_iter()
.map(|arg| CString::new(arg).unwrap())
.collect::<Vec<CString>>();
let _argv = _source
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<*const c_char>>();
let main_args = MainArgs {
argc: _argv.len() as i32,
argv: _argv.as_ptr() as *mut *mut _,
};
Self {
_source,
_argv,
main_args,
}
}
pub fn as_main_args(&self) -> &MainArgs {
&self.main_args
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn as_cmd_line(&self) -> Option<CommandLine> {
let Some(cmd_line) = command_line_create() else {
return None;
};
cmd_line.init_from_argv(self.as_main_args().argc, self.as_main_args().argv.cast());
Some(cmd_line)
}
#[cfg(target_os = "windows")]
pub fn as_cmd_line(&self) -> Option<CommandLine> {
let cmd_line = command_line_create().and_then(|cmd_line| {
unsafe {
std::ffi::CStr::from_ptr(
windows_sys::Win32::System::Environment::GetCommandLineA().cast(),
)
}
.to_str()
.ok()
.map(|args| {
cmd_line.init_from_string(Some(&CefString::from(args)));
cmd_line
})
});
cmd_line
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod x86_64_unknown_linux_gnu;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub use x86_64_unknown_linux_gnu::*;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
mod aarch64_unknown_linux_gnu;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
pub use aarch64_unknown_linux_gnu::*;
#[cfg(all(target_os = "linux", target_arch = "arm"))]
mod arm_unknown_linux_gnueabi;
#[cfg(all(target_os = "linux", target_arch = "arm"))]
pub use arm_unknown_linux_gnueabi::*;
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
mod x86_64_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub use x86_64_pc_windows_msvc::*;
#[cfg(all(target_os = "windows", target_arch = "x86"))]
mod i686_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "x86"))]
pub use i686_pc_windows_msvc::*;
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
mod aarch64_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
pub use aarch64_pc_windows_msvc::*;
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
mod x86_64_apple_darwin;
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
pub use x86_64_apple_darwin::*;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod aarch64_apple_darwin;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub use aarch64_apple_darwin::*;
#[cfg(test)]
mod test {
use super::*;
use crate::{rc::*, sys};
use std::{cell::RefCell, ptr};
#[derive(Default)]
struct CallInfo {
extra_info: RefCell<Option<DictionaryValue>>,
}
struct TestLifeSpanHandler {
object: *mut RcImpl<sys::_cef_life_span_handler_t, Self>,
call_info: std::rc::Rc<CallInfo>,
}
impl ImplLifeSpanHandler for TestLifeSpanHandler {
fn get_raw(&self) -> *mut sys::_cef_life_span_handler_t {
self.object.cast()
}
fn on_before_popup(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_popup_id: ::std::os::raw::c_int,
_target_url: Option<&CefString>,
_target_frame_name: Option<&CefString>,
_target_disposition: WindowOpenDisposition,
_user_gesture: ::std::os::raw::c_int,
_popup_features: Option<&PopupFeatures>,
_window_info: Option<&mut WindowInfo>,
_client: Option<&mut Option<Client>>,
_settings: Option<&mut BrowserSettings>,
extra_info: Option<&mut Option<DictionaryValue>>,
_no_javascript_access: Option<&mut ::std::os::raw::c_int>,
) -> ::std::os::raw::c_int {
let extra_info = extra_info.expect("extra_info is required");
*extra_info = self.call_info.extra_info.take();
1
}
}
impl WrapLifeSpanHandler for TestLifeSpanHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_life_span_handler_t, Self>) {
self.object = object;
}
}
impl TestLifeSpanHandler {
fn new(call_info: std::rc::Rc<CallInfo>) -> LifeSpanHandler {
LifeSpanHandler::new(Self {
object: std::ptr::null_mut(),
call_info,
})
}
}
impl Clone for TestLifeSpanHandler {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
self.object
};
Self {
object,
call_info: self.call_info.clone(),
}
}
}
impl Rc for TestLifeSpanHandler {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
#[test]
fn dictionary_value_out_param() {
#[cfg(target_os = "macos")]
unsafe {
use std::{ffi::CString, os::unix::ffi::OsStrExt};
let cef_dir = sys::get_cef_dir().expect("CEF not found");
let framework_dir = cef_dir
.join(sys::FRAMEWORK_PATH)
.canonicalize()
.expect("failed to get framework path");
let framework_dir =
CString::new(framework_dir.as_os_str().as_bytes()).expect("invalid path");
assert_eq!(sys::cef_load_library(framework_dir.as_ptr().cast()), 1);
}
assert_eq!(initialize(None, None, None, ptr::null_mut()), 0);
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let call_info = std::rc::Rc::new(CallInfo::default());
let extra_info = dictionary_value_create().expect("failed to create dictionary");
let test_key = CefString::from("testKey");
let test_value = CefString::from("testValue");
extra_info.set_string(Some(&test_key), Some(&test_value));
*call_info.extra_info.borrow_mut() = Some(extra_info);
let mut extra_info = None;
let handler = TestLifeSpanHandler::new(call_info);
assert_eq!(
1,
handler.on_before_popup(
None,
None,
1,
None,
None,
sys::cef_window_open_disposition_t::CEF_WOD_CURRENT_TAB.into(),
0,
None,
None,
None,
None,
Some(&mut extra_info),
None,
)
);
let extra_info = extra_info.as_ref().unwrap();
assert_eq!(
"testValue",
CefString::from(&extra_info.string(Some(&test_key))).to_string()
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

17
browser/cef/src/lib.rs Normal file
View File

@ -0,0 +1,17 @@
#![doc = include_str!("../README.md")]
pub mod args;
pub mod rc;
pub mod string;
#[cfg(target_os = "macos")]
pub mod library_loader;
#[cfg(target_os = "macos")]
pub mod sandbox;
#[rustfmt::skip]
mod bindings;
pub use bindings::*;
pub use cef_dll_sys as sys;

View File

@ -0,0 +1,46 @@
use crate::{load_library, unload_library};
pub struct LibraryLoader {
path: std::path::PathBuf,
}
impl LibraryLoader {
const FRAMEWORK_PATH: &str =
"Chromium Embedded Framework.framework/Chromium Embedded Framework";
pub fn new(path: &std::path::Path, helper: bool) -> Self {
let resolver = if helper {
"../../../.."
} else {
"../../Frameworks"
};
let path = path
.join(resolver)
.join(Self::FRAMEWORK_PATH)
.canonicalize()
.unwrap();
Self { path }
}
// See [cef_load_library] for more documentation.
pub fn load(&self) -> bool {
Self::load_library(&self.path)
}
fn load_library(name: &std::path::Path) -> bool {
use std::os::unix::ffi::OsStrExt;
let Ok(name) = std::ffi::CString::new(name.as_os_str().as_bytes()) else {
return false;
};
unsafe { load_library(Some(&*name.as_ptr().cast())) == 1 }
}
}
impl Drop for LibraryLoader {
fn drop(&mut self) {
if unload_library() != 1 {
eprintln!("cannot unload framework {}", self.path.display());
}
}
}

380
browser/cef/src/rc.rs Normal file
View File

@ -0,0 +1,380 @@
//! Reference counted module
//!
//! Many cef types are reference counted, this module is the building block to create them. Users
//! typically don't need to uses these types, the `update-bindings` tool generates all the code
//! which should ever call them.
use std::{
fmt::Debug,
mem,
ops::Deref,
ptr::{self, NonNull},
sync::atomic::{fence, AtomicUsize, Ordering},
};
use cef_dll_sys::cef_base_ref_counted_t;
/// Reference counted trait for types has [`cef_base_ref_counted_t`].
pub trait Rc {
/// Increase the reference count by 1.
///
/// # Safety
///
/// Calling this method when you need to manually handle the reference count.
/// Otherwise, these methods shouldn't be called externally in most cases.
unsafe fn add_ref(&self) {
self.as_base().add_ref();
}
/// Decrease reference count by 1 and release the value if the count meets 0.
/// Reuturn `True` if it is released.
///
/// # Safety
///
/// Calling this method when you need to manually handle the reference count.
/// Otherwise, these methods shouldn't be called externally in most cases.
unsafe fn release(&self) -> bool {
self.as_base().release()
}
/// `True` if the reference count is exactly 1.
fn has_one_ref(&self) -> bool {
self.as_base().has_one_ref()
}
/// `True` if the reference count is larger than 0.
fn has_at_least_one_ref(&self) -> bool {
self.as_base().has_at_least_one_ref()
}
/// Get the reference of [cef_base_ref_counted_t].
fn as_base(&self) -> &cef_base_ref_counted_t;
}
impl Rc for cef_base_ref_counted_t {
unsafe fn add_ref(&self) {
if let Some(add_ref) = self.add_ref {
add_ref(ptr::from_ref(self) as *mut _);
}
}
fn has_one_ref(&self) -> bool {
if let Some(has_one_ref) = self.has_one_ref {
let result = unsafe { has_one_ref(ptr::from_ref(self) as *mut _) };
return result == 1;
}
false
}
fn has_at_least_one_ref(&self) -> bool {
if let Some(has_at_least_one_ref) = self.has_at_least_one_ref {
let result = unsafe { has_at_least_one_ref(ptr::from_ref(self) as *mut _) };
return result == 1;
}
false
}
unsafe fn release(&self) -> bool {
if let Some(release) = self.release {
return release(ptr::from_ref(self) as *mut _) == 1;
}
false
}
fn as_base(&self) -> &Self {
self
}
}
pub trait ConvertParam<T: Sized> {
fn into_raw(self) -> T;
}
impl<T, U> ConvertParam<U> for T
where
T: Sized + Into<U>,
U: Sized,
{
fn into_raw(self) -> U {
self.into()
}
}
impl<T> ConvertParam<*mut T> for &RefGuard<T>
where
T: Sized + Rc,
{
/// Access the [RefGuard] and return the raw pointer without decreasing the reference count.
///
/// # Safety
///
/// This should be used when you need to pass wrapper type to the FFI function as **parameter**, and it is **not**
/// the `self` type (usually the first parameter). This means we pass the ownership of the
/// value to the function call. Using this method elsewehre may cause incorrect reference count
/// and memory safety issues.
fn into_raw(self) -> *mut T {
unsafe { self.into_raw() }
}
}
pub struct WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
value: T,
output: Option<NonNull<P>>,
}
impl<T, P> Drop for WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
fn drop(&mut self) {
if let Some(output) = &mut self.output {
let output = unsafe { output.as_mut() };
let mut value = unsafe { mem::zeroed() };
mem::swap(&mut self.value, &mut value);
*output = value.into();
}
}
}
impl<T, P> From<*mut P> for WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
fn from(value: *mut P) -> Self {
let mut output = NonNull::new(value);
let value = output
.as_mut()
.map(|p| {
let mut value = unsafe { mem::zeroed() };
mem::swap(unsafe { p.as_mut() }, &mut value);
value.into()
})
.unwrap_or_else(|| unsafe { mem::zeroed() });
Self { value, output }
}
}
impl<T, P> From<*const P> for WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn from(value: *const P) -> Self {
let value = unsafe { value.as_ref() }
.map(|value| (*value).into())
.unwrap_or_else(|| unsafe { mem::zeroed() });
Self {
value,
output: None,
}
}
}
impl<T, P> AsMut<T> for WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
fn as_mut(&mut self) -> &mut T {
&mut self.value
}
}
impl<T, P> AsRef<T> for WrapParamRef<T, P>
where
T: Sized + Into<P>,
P: Sized + Copy + Into<T>,
{
fn as_ref(&self) -> &T {
&self.value
}
}
pub trait ConvertReturnValue<T: Sized> {
fn wrap_result(self) -> T;
}
impl<T, U> ConvertReturnValue<U> for T
where
T: Sized + Into<U>,
U: Sized,
{
fn wrap_result(self) -> U {
self.into()
}
}
/// A smart pointer for types from cef library.
pub struct RefGuard<T: Rc> {
object: *mut T,
}
impl<T: Debug + Rc> Debug for RefGuard<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let object_ref = unsafe { self.object.as_ref() };
write!(f, "RefGuard({object_ref:#?})")
}
}
impl<T: Rc> RefGuard<T> {
/// Create [RefGuard] from a raw C pointer.
///
/// # Safety
///
/// This should be used to get the **return value** of the FFI function. This means we get the
/// ownership of the value. The reference count of the return value is already increased when
/// you get it. So we don't need to increase it again manually. Using this method elsewhere may
/// cause incorrect reference count and memory safety issues.
pub unsafe fn from_raw(ptr: *mut T) -> RefGuard<T> {
RefGuard { object: ptr }
}
/// Create [RefGuard] from a raw C pointer and increase a reference count. This should be used
/// when you want to copy the value and create another wrapper type.
///
/// # Safety
///
/// THis should be used when you want to manually increase the reference count upon getting the
/// raw pointer. Using this method elsewhere may cause incorrect reference count and memory
/// safety issues.
pub unsafe fn from_raw_add_ref(ptr: *mut T) -> RefGuard<T> {
let guard = RefGuard { object: ptr };
guard.add_ref();
guard
}
// Get the raw pointer of [RefGuard].
//
/// # Safety
///
/// This should be used when you need to pass wrapper type to the FFI function as **parameter**, and it **is**
/// the `self` type (usually the first parameter). This means we pass the ownership of the
/// value to the function call. Using this method elsewhere may cause incorrect reference count
/// and memory safety issues.
pub unsafe fn into_raw(&self) -> *mut T {
self.object
}
/// Convert the value to another value that is also reference counted.
///
/// # Safety
///
/// This should be used when the type has type `U` as its base type. Using this method
/// elsewhere may cause memory safety issues.
pub unsafe fn convert<U: Rc>(&self) -> RefGuard<U> {
RefGuard::from_raw_add_ref(self.into_raw().cast())
}
}
unsafe impl<T: Rc> Send for RefGuard<T> {}
unsafe impl<T: Rc> Sync for RefGuard<T> {}
impl<T: Rc> Clone for RefGuard<T> {
fn clone(&self) -> RefGuard<T> {
unsafe { self.add_ref() };
RefGuard {
object: self.object,
}
}
}
impl<T: Rc> Deref for RefGuard<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.object }
}
}
impl<T: Rc> Drop for RefGuard<T> {
fn drop(&mut self) {
unsafe { self.release() };
}
}
/// There are some types require users to implement one their own in Rust and then create a raw type around it to
/// pass to sys level api. This is the wrapper type for it.
#[repr(C)]
pub struct RcImpl<T, I> {
/// Raw cef types
pub cef_object: T,
/// Rust interface of such type
pub interface: I,
ref_count: AtomicUsize,
}
impl<T, I> RcImpl<T, I> {
pub fn new(mut cef_object: T, interface: I) -> *mut RcImpl<T, I> {
let base: &mut cef_base_ref_counted_t =
unsafe { &mut *(ptr::from_mut(&mut cef_object).cast()) };
base.size = std::mem::size_of::<T>();
base.add_ref = Some(add_ref::<T, I>);
base.has_one_ref = Some(has_one_ref::<T, I>);
base.has_at_least_one_ref = Some(has_at_least_one_ref::<T, I>);
base.release = Some(release::<T, I>);
Box::into_raw(Box::new(RcImpl {
cef_object,
interface,
ref_count: AtomicUsize::new(1),
}))
}
pub fn get<'a>(ptr: *mut T) -> &'a mut RcImpl<T, I> {
unsafe { &mut *(ptr.cast()) }
}
}
extern "C" fn add_ref<T, I>(this: *mut cef_base_ref_counted_t) {
let obj = RcImpl::<T, I>::get(this.cast());
obj.ref_count.fetch_add(1, Ordering::Relaxed);
}
extern "C" fn has_one_ref<T, I>(this: *mut cef_base_ref_counted_t) -> i32 {
let obj = RcImpl::<T, I>::get(this.cast());
if obj.ref_count.load(Ordering::Relaxed) == 1 {
1
} else {
0
}
}
extern "C" fn has_at_least_one_ref<T, I>(this: *mut cef_base_ref_counted_t) -> i32 {
let obj = RcImpl::<T, I>::get(this.cast());
if obj.ref_count.load(Ordering::Relaxed) >= 1 {
1
} else {
0
}
}
pub extern "C" fn release<T, I>(this: *mut cef_base_ref_counted_t) -> i32 {
let obj = RcImpl::<T, I>::get(this.cast());
if obj.ref_count.fetch_sub(1, Ordering::Release) != 1 {
0
} else {
fence(Ordering::Acquire);
let _: Box<RcImpl<T, I>> = unsafe { Box::from_raw(this.cast()) };
1
}
}

View File

@ -0,0 +1,53 @@
use libloading::Library;
use std::{ffi::c_void, ptr::NonNull};
use crate::MainArgs;
pub struct Sandbox {
lib: Library,
context: Option<NonNull<c_void>>,
}
impl Sandbox {
const LIBCEF_SANDBOX_PATH: &str =
"../../../../Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib";
pub fn new() -> Self {
unsafe {
let lib = Library::new(
std::env::current_exe()
.unwrap()
.join(Self::LIBCEF_SANDBOX_PATH)
.canonicalize()
.unwrap(),
)
.unwrap();
Self { lib, context: None }
}
}
pub fn initialize(&mut self, args: &MainArgs) {
let inner = unsafe {
self.lib
.get::<extern "C" fn(
argc: std::os::raw::c_int,
argv: *mut *mut ::std::os::raw::c_char,
) -> *mut c_void>(b"cef_sandbox_initialize")
.unwrap()(args.argc, args.argv)
};
self.context = NonNull::new(inner);
assert!(self.context.is_some());
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
unsafe {
if let Some(inner) = self.context {
self.lib
.get::<extern "C" fn(context: *mut c_void)>(b"cef_sandbox_destroy")
.unwrap()(inner.as_ptr());
}
}
}
}

1415
browser/cef/src/string.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.2.0](https://github.com/tauri-apps/cef-rs/compare/download-cef-v2.1.0...download-cef-v2.2.0) - 2025-09-23
### Added
- add --mirror-url cli args
- Allow to download CEF binaries with custom base url set in env variable
## [2.1.0](https://github.com/tauri-apps/cef-rs/compare/download-cef-v2.0.3...download-cef-v2.1.0) - 2025-09-19
### Added
- support pre-downloaded archive ([#197](https://github.com/tauri-apps/cef-rs/pull/197))
## [2.0.3](https://github.com/tauri-apps/cef-rs/compare/download-cef-v2.0.2...download-cef-v2.0.3) - 2025-08-16
### Fixed
- #183
## [2.0.2](https://github.com/tauri-apps/cef-rs/compare/download-cef-v2.0.1...download-cef-v2.0.2) - 2025-07-22
### Other
- *(doc)* regenerate CHANGELOG.md
## [2.0.1](https://github.com/tauri-apps/cef-rs/compare/download-cef-v2.0.0...download-cef-v2.0.1) - 2025-07-14
### Other
- release
## [2.0.0](https://github.com/tauri-apps/cef-rs/compare/download-cef-v1.6.0...download-cef-v2.0.0) - 2025-07-14
### Fixed
- bump major version of download-cef [#145](https://github.com/tauri-apps/cef-rs/issues/145)
### Other
- update CEF version

View File

@ -0,0 +1,23 @@
[package]
name = "download-cef"
description = "Download and extract pre-built CEF (Chromium Embedded Framework) archives."
version = "2.2.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
clap.workspace = true
regex.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
bzip2 = "0.6"
indicatif = "0.18"
sha1_smol = "1"
tar = "0.4"
ureq = { version = "3", features = ["json", "socks-proxy"] }

View File

@ -0,0 +1,4 @@
# download-cef
Utility functions to download and extract prebuilt [Chromium Embedded Framework](https://github.com/chromiumembedded/cef)
archives on any supported platform.

View File

@ -0,0 +1,644 @@
#![doc = include_str!("../README.md")]
use bzip2::bufread::BzDecoder;
use clap::ValueEnum;
use regex::Regex;
use semver::Version;
use serde::{Deserialize, Serialize};
use sha1_smol::Sha1;
use std::{
collections::HashMap,
env,
fmt::{self, Display},
fs::{self, File},
io::{self, BufReader, IsTerminal, Write},
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
thread,
time::Duration,
};
#[macro_use]
extern crate thiserror;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unsupported target triplet: {0}")]
UnsupportedTarget(String),
#[error("HTTP request error: {0}")]
Request(#[from] ureq::Error),
#[error("Invalid version: {0}")]
InvalidVersion(#[from] semver::Error),
#[error("Version not found: {0}")]
VersionNotFound(String),
#[error("Missing Content-Length header")]
MissingContentLength,
#[error("Opaque Content-Length header: {0}")]
OpaqueContentLength(#[from] ureq::http::header::ToStrError),
#[error("Invalid Content-Length header: {0}")]
InvalidContentLength(String),
#[error("File I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Unexpected file size: downloaded {downloaded} expected {expected}")]
UnexpectedFileSize { downloaded: u64, expected: u64 },
#[error("Bad SHA1 file hash: {0}")]
CorruptedFile(String),
#[error("Invalid archive file path: {0}")]
InvalidArchiveFile(String),
#[error("JSON serialization error: {0}")]
Json(#[from] serde_json::Error),
#[error(
"Undexpected archive version: location: {location} archive {archive} expected {expected}"
)]
VersionMismatch {
location: String,
archive: String,
expected: String,
},
#[error("Invalid regex pattern: {0}")]
InvalidRegexPattern(#[from] regex::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
pub const LINUX_TARGETS: &[&str] = &[
"x86_64-unknown-linux-gnu",
"arm-unknown-linux-gnueabi",
"aarch64-unknown-linux-gnu",
];
pub const MACOS_TARGETS: &[&str] = &["aarch64-apple-darwin", "x86_64-apple-darwin"];
pub const WINDOWS_TARGETS: &[&str] = &[
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"i686-pc-windows-msvc",
];
pub fn default_version(version: &str) -> String {
unwrap_cef_version(version).unwrap_or_else(|_| version.to_string())
}
fn unwrap_cef_version(version: &str) -> Result<String> {
static VERSIONS: OnceLock<Mutex<HashMap<Version, String>>> = OnceLock::new();
let mut versions = VERSIONS
.get_or_init(Default::default)
.lock()
.expect("Lock error");
Ok(versions
.entry(Version::parse(version)?)
.or_insert_with_key(|v| {
if v.build.is_empty() {
version.to_string()
} else {
v.build.to_string()
}
})
.clone())
}
pub fn check_archive_json(version: &str, location: &str) -> Result<()> {
let expected = Version::parse(&unwrap_cef_version(version)?)?;
static PATTERN: OnceLock<core::result::Result<Regex, regex::Error>> = OnceLock::new();
let pattern = PATTERN
.get_or_init(|| Regex::new(r"^cef_binary_([^+]+)(:?\+.+)?$"))
.as_ref()
.map_err(Clone::clone)?;
let archive_json: CefFile = serde_json::from_reader(File::open(archive_json_path(location))?)?;
let archive_version = pattern.replace(&archive_json.name, "$1");
let archive = Version::parse(&archive_version)?;
if archive <= expected {
Ok(())
} else {
Err(Error::VersionMismatch {
location: location.to_string(),
expected: expected.to_string(),
archive: archive.to_string(),
})
}
}
fn archive_json_path<P>(location: P) -> PathBuf
where
P: AsRef<Path>,
{
location.as_ref().join("archive.json")
}
pub const DEFAULT_CDN_URL: &str = "https://cef-builds.spotifycdn.com";
pub fn default_download_url() -> String {
env::var("CEF_DOWNLOAD_URL").unwrap_or(DEFAULT_CDN_URL.to_owned())
}
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum Channel {
Stable,
Beta,
}
impl Display for Channel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Channel::Stable => write!(f, "stable"),
Channel::Beta => write!(f, "beta"),
}
}
}
#[derive(Deserialize, Serialize, Default)]
pub struct CefIndex {
pub macosarm64: CefPlatform,
pub macosx64: CefPlatform,
pub windows64: CefPlatform,
pub windowsarm64: CefPlatform,
pub windows32: CefPlatform,
pub linux64: CefPlatform,
pub linuxarm64: CefPlatform,
pub linuxarm: CefPlatform,
}
impl CefIndex {
pub fn download() -> Result<Self> {
Self::download_from(DEFAULT_CDN_URL)
}
pub fn download_from(url: &str) -> Result<Self> {
Ok(ureq::get(&format!("{url}/index.json"))
.call()?
.into_body()
.read_json()?)
}
pub fn platform(&self, target: &str) -> Result<&CefPlatform> {
match target {
"aarch64-apple-darwin" => Ok(&self.macosarm64),
"x86_64-apple-darwin" => Ok(&self.macosx64),
"x86_64-pc-windows-msvc" => Ok(&self.windows64),
"aarch64-pc-windows-msvc" => Ok(&self.windowsarm64),
"i686-pc-windows-msvc" => Ok(&self.windows32),
"x86_64-unknown-linux-gnu" => Ok(&self.linux64),
"aarch64-unknown-linux-gnu" => Ok(&self.linuxarm64),
"arm-unknown-linux-gnueabi" => Ok(&self.linuxarm),
v => Err(Error::UnsupportedTarget(v.to_string())),
}
}
}
#[derive(Deserialize, Serialize, Default)]
pub struct CefPlatform {
pub versions: Vec<CefVersion>,
}
impl CefPlatform {
pub fn version(&self, cef_version: &str) -> Result<&CefVersion> {
let version_prefix = format!("{cef_version}+");
self.versions
.iter()
.find(|v| v.cef_version.starts_with(&version_prefix))
.ok_or_else(|| Error::VersionNotFound(cef_version.to_string()))
}
pub fn latest(&self, channel: Channel) -> Result<&CefVersion> {
static PATTERN: OnceLock<core::result::Result<Regex, regex::Error>> = OnceLock::new();
let pattern = PATTERN
.get_or_init(|| Regex::new(r"^([^+]+)(:?\+.+)?$"))
.as_ref()
.map_err(Clone::clone)?;
self.versions
.iter()
.filter_map(|value| {
if value.channel == channel {
let key = Version::parse(&pattern.replace(&value.cef_version, "$1")).ok()?;
Some((key, value))
} else {
None
}
})
.max_by(|(a, _), (b, _)| a.cmp(b))
.map(|(_, v)| v)
.ok_or_else(|| Error::VersionNotFound("latest".to_string()))
}
}
#[derive(Deserialize, Serialize)]
pub struct CefVersion {
pub channel: Channel,
pub cef_version: String,
pub files: Vec<CefFile>,
}
impl CefVersion {
pub fn download_archive<P>(&self, location: P, show_progress: bool) -> Result<PathBuf>
where
P: AsRef<Path>,
{
self.download_archive_from(DEFAULT_CDN_URL, location, show_progress)
}
pub fn download_archive_from<P>(
&self,
url: &str,
location: P,
show_progress: bool,
) -> Result<PathBuf>
where
P: AsRef<Path>,
{
let file = self.minimal()?;
let (file, sha) = (file.name.as_str(), file.sha1.as_str());
fs::create_dir_all(&location)?;
let download_file = location.as_ref().join(file);
if download_file.exists() {
if calculate_file_sha1(&download_file) == sha {
if show_progress {
println!("Verified archive: {}", download_file.display());
}
return Ok(download_file);
}
if show_progress {
println!("Cleaning corrupted archive: {}", download_file.display());
}
let corrupted_file = location.as_ref().join(format!("corrupted_{file}"));
fs::rename(&download_file, &corrupted_file)?;
fs::remove_file(&corrupted_file)?;
}
let cef_url = format!("{url}/{file}");
if show_progress {
println!("Using archive url: {cef_url}");
}
let mut file = File::create(&download_file)?;
let resp = ureq::get(&cef_url).call()?;
let expected = resp
.headers()
.get("Content-Length")
.ok_or(Error::MissingContentLength)?;
let expected = expected.to_str()?;
let expected = expected
.parse::<u64>()
.map_err(|_| Error::InvalidContentLength(expected.to_owned()))?;
let downloaded = if show_progress && io::stdout().is_terminal() {
const DOWNLOAD_TEMPLATE: &str = "{msg} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})";
let bar = indicatif::ProgressBar::new(expected);
bar.set_style(
indicatif::ProgressStyle::with_template(DOWNLOAD_TEMPLATE)
.expect("invalid template")
.progress_chars("##-"),
);
bar.set_message("Downloading");
std::io::copy(
&mut bar.wrap_read(resp.into_body().into_reader()),
&mut file,
)
} else {
let mut reader = resp.into_body().into_reader();
std::io::copy(&mut reader, &mut file)
}?;
if downloaded != expected {
return Err(Error::UnexpectedFileSize {
downloaded,
expected,
});
}
if show_progress {
println!("Verifying SHA1 hash: {sha}...");
}
if calculate_file_sha1(&download_file) != sha {
return Err(Error::CorruptedFile(download_file.display().to_string()));
}
if show_progress {
println!("Downloaded archive: {}", download_file.display());
}
Ok(download_file)
}
pub fn download_archive_with_retry<P>(
&self,
location: P,
show_progress: bool,
retry_delay: Duration,
max_retries: u32,
) -> Result<PathBuf>
where
P: AsRef<Path>,
{
self.download_archive_with_retry_from(
DEFAULT_CDN_URL,
location,
show_progress,
retry_delay,
max_retries,
)
}
pub fn download_archive_with_retry_from<P>(
&self,
url: &str,
location: P,
show_progress: bool,
retry_delay: Duration,
max_retries: u32,
) -> Result<PathBuf>
where
P: AsRef<Path>,
{
let mut result = self.download_archive_from(&url, &location, show_progress);
let mut retry = 0;
while let Err(Error::Io(_)) = &result {
if retry >= max_retries {
break;
}
retry += 1;
thread::sleep(retry_delay * retry);
result = self.download_archive_from(&url, &location, show_progress);
}
result
}
pub fn minimal(&self) -> Result<&CefFile> {
self.files
.iter()
.find(|f| f.file_type == "minimal")
.ok_or_else(|| Error::VersionNotFound(self.cef_version.clone()))
}
pub fn write_archive_json<P>(&self, location: P) -> Result<()>
where
P: AsRef<Path>,
{
self.minimal()?.write_archive_json(location)
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct CefFile {
#[serde(rename = "type")]
pub file_type: String,
pub name: String,
pub sha1: String,
}
impl CefFile {
pub fn write_archive_json<P>(&self, location: P) -> Result<()>
where
P: AsRef<Path>,
{
let archive_version = serde_json::to_string_pretty(self)?;
let mut archive_json = File::create(archive_json_path(location))?;
archive_json.write_all(archive_version.as_bytes())?;
Ok(())
}
}
impl TryFrom<&Path> for CefFile {
type Error = Error;
fn try_from(location: &Path) -> Result<Self> {
let file_type = "minimal".to_string();
let name = location
.file_name()
.map(|f| f.display().to_string())
.ok_or_else(|| Error::InvalidArchiveFile(location.display().to_string()))?;
let sha1 = calculate_file_sha1(location);
Ok(Self {
file_type,
name,
sha1,
})
}
}
pub fn download_target_archive<P>(
target: &str,
cef_version: &str,
location: P,
show_progress: bool,
) -> Result<PathBuf>
where
P: AsRef<Path>,
{
download_target_archive_from(
DEFAULT_CDN_URL,
target,
cef_version,
location,
show_progress,
)
}
pub fn download_target_archive_from<P>(
url: &str,
target: &str,
cef_version: &str,
location: P,
show_progress: bool,
) -> Result<PathBuf>
where
P: AsRef<Path>,
{
if show_progress {
println!("Downloading CEF archive for {target}...");
}
let index = CefIndex::download_from(&url)?;
let platform = index.platform(target)?;
let version = platform.version(cef_version)?;
version.download_archive_with_retry_from(
url,
location,
show_progress,
Duration::from_secs(15),
3,
)
}
pub fn extract_target_archive<P, Q>(
target: &str,
archive: P,
location: Q,
show_progress: bool,
) -> Result<PathBuf>
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
if show_progress {
println!("Extracting archive: {}", archive.as_ref().display());
}
let decoder = BzDecoder::new(BufReader::new(File::open(&archive)?));
tar::Archive::new(decoder).unpack(&location)?;
let extracted_dir = archive.as_ref().display().to_string();
let extracted_dir = extracted_dir
.strip_suffix(".tar.bz2")
.map(PathBuf::from)
.ok_or(Error::InvalidArchiveFile(extracted_dir))?;
let os_and_arch = OsAndArch::try_from(target)?;
let OsAndArch { os, arch } = os_and_arch;
let cef_dir = os_and_arch.to_string();
let cef_dir = location.as_ref().join(cef_dir);
if cef_dir.exists() {
let old_dir = location.as_ref().join(format!("old_{os}_{arch}"));
if show_progress {
println!("Cleaning up: {}", old_dir.display());
}
fs::rename(&cef_dir, &old_dir)?;
fs::remove_dir_all(old_dir)?;
}
const RELEASE_DIR: &str = "Release";
fs::rename(extracted_dir.join(RELEASE_DIR), &cef_dir)?;
if os != "macos" {
let resources = extracted_dir.join("Resources");
for entry in fs::read_dir(&resources)? {
let entry = entry?;
fs::rename(entry.path(), cef_dir.join(entry.file_name()))?;
}
}
const CMAKE_LISTS_TXT: &str = "CMakeLists.txt";
fs::rename(
extracted_dir.join(CMAKE_LISTS_TXT),
cef_dir.join(CMAKE_LISTS_TXT),
)?;
const CMAKE_DIR: &str = "cmake";
fs::rename(extracted_dir.join(CMAKE_DIR), cef_dir.join(CMAKE_DIR))?;
const INCLUDE_DIR: &str = "include";
fs::rename(extracted_dir.join(INCLUDE_DIR), cef_dir.join(INCLUDE_DIR))?;
const LIBCEF_DLL_DIR: &str = "libcef_dll";
fs::rename(
extracted_dir.join(LIBCEF_DLL_DIR),
cef_dir.join(LIBCEF_DLL_DIR),
)?;
if show_progress {
println!("Moved contents to: {}", cef_dir.display());
}
// Cleanup whatever is left in the extracted directory.
let old_dir = extracted_dir
.parent()
.map(|parent| parent.join(format!("extracted_{os}_{arch}")))
.ok_or_else(|| Error::InvalidArchiveFile(extracted_dir.display().to_string()))?;
if show_progress {
println!("Cleaning up: {}", old_dir.display());
}
fs::rename(&extracted_dir, &old_dir)?;
fs::remove_dir_all(old_dir)?;
Ok(cef_dir)
}
fn calculate_file_sha1(path: &Path) -> String {
use std::io::Read;
let mut file = BufReader::new(File::open(path).unwrap());
let mut sha1 = Sha1::new();
let mut buffer = [0; 8192];
loop {
let count = file.read(&mut buffer).unwrap();
if count == 0 {
break;
}
sha1.update(&buffer[..count]);
}
sha1.digest().to_string()
}
pub struct OsAndArch {
pub os: &'static str,
pub arch: &'static str,
}
impl Display for OsAndArch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let os = self.os;
let arch = self.arch;
write!(f, "cef_{os}_{arch}")
}
}
impl TryFrom<&str> for OsAndArch {
type Error = Error;
fn try_from(target: &str) -> Result<Self> {
match target {
"aarch64-apple-darwin" => Ok(OsAndArch {
os: "macos",
arch: "aarch64",
}),
"x86_64-apple-darwin" => Ok(OsAndArch {
os: "macos",
arch: "x86_64",
}),
"x86_64-pc-windows-msvc" => Ok(OsAndArch {
os: "windows",
arch: "x86_64",
}),
"aarch64-pc-windows-msvc" => Ok(OsAndArch {
os: "windows",
arch: "aarch64",
}),
"i686-pc-windows-msvc" => Ok(OsAndArch {
os: "windows",
arch: "x86",
}),
"x86_64-unknown-linux-gnu" => Ok(OsAndArch {
os: "linux",
arch: "x86_64",
}),
"aarch64-unknown-linux-gnu" => Ok(OsAndArch {
os: "linux",
arch: "aarch64",
}),
"arm-unknown-linux-gnueabi" => Ok(OsAndArch {
os: "linux",
arch: "arm",
}),
v => Err(Error::UnsupportedTarget(v.to_string())),
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu";
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
pub const DEFAULT_TARGET: &str = "aarch64-unknown-linux-gnu";
#[cfg(all(target_os = "linux", target_arch = "arm"))]
pub const DEFAULT_TARGET: &str = "arm-unknown-linux-gnueabi";
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub const DEFAULT_TARGET: &str = "x86_64-pc-windows-msvc";
#[cfg(all(target_os = "windows", target_arch = "x86"))]
pub const DEFAULT_TARGET: &str = "i686-pc-windows-msvc";
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
pub const DEFAULT_TARGET: &str = "aarch64-pc-windows-msvc";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
pub const DEFAULT_TARGET: &str = "x86_64-apple-darwin";
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub const DEFAULT_TARGET: &str = "aarch64-apple-darwin";

View File

@ -0,0 +1,208 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [140.3.0+140.1.14](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v140.2.0+140.1.14...export-cef-dir-v140.3.0+140.1.14) - 2025-09-23
### Added
- add --mirror-url cli args
- Allow to download CEF binaries with custom base url set in env variable
## [140.2.0+140.1.14](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v140.1.0+140.1.13...export-cef-dir-v140.2.0+140.1.14) - 2025-09-21
### Other
- *(release)* update CEF version to 140.1.14
## [140.1.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v140.0.0+140.1.13...export-cef-dir-v140.1.0+140.1.13) - 2025-09-19
### Added
- support pre-downloaded archive ([#197](https://github.com/tauri-apps/cef-rs/issues/197))
### Other
- release
## [140.0.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.8.0+139.0.40...export-cef-dir-v140.0.0+140.1.13) - 2025-09-19
### Other
- *(release)* update CEF version to 140.1.13
## [139.8.0+139.0.40](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.7.2+139.0.38...export-cef-dir-v139.8.0+139.0.40) - 2025-09-12
### Other
- *(release)* update CEF version to 139.0.40
## [139.7.2+139.0.38](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.7.1+139.0.38...export-cef-dir-v139.7.2+139.0.38) - 2025-09-08
### Other
- release v139.7.2+139.0.38
## [139.7.1+139.0.38](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.7.0+139.0.38...export-cef-dir-v139.7.1+139.0.38) - 2025-09-07
### Other
- release v139.7.1+139.0.38
- *(deps)* update rust crate windows-sys to 0.61
## [139.7.0+139.0.38](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.6.0+139.0.37...export-cef-dir-v139.7.0+139.0.38) - 2025-08-31
### Other
- *(release)* update CEF version to 139.0.38
## [139.6.0+139.0.37](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.5.0+139.0.30...export-cef-dir-v139.6.0+139.0.37) - 2025-08-29
### Other
- *(release)* update CEF version to 139.0.37
## [139.5.0+139.0.30](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.4.0+139.0.28...export-cef-dir-v139.5.0+139.0.30) - 2025-08-28
### Other
- *(release)* update CEF version to 139.0.30
## [139.4.0+139.0.28](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.3.0+139.0.26...export-cef-dir-v139.4.0+139.0.28) - 2025-08-23
### Other
- *(release)* update CEF version to 139.0.28
## [139.3.0+139.0.26](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.2.1+139.0.23...export-cef-dir-v139.3.0+139.0.26) - 2025-08-22
### Other
- *(release)* update CEF version to 139.0.26
## [139.2.1+139.0.23](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.2.0+139.0.23...export-cef-dir-v139.2.1+139.0.23) - 2025-08-16
### Fixed
- [#183](https://github.com/tauri-apps/cef-rs/issues/183)
### Other
- release
## [139.2.0+139.0.23](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.1.0+139.0.20...export-cef-dir-v139.2.0+139.0.23) - 2025-08-16
### Other
- *(release)* update CEF version to 139.0.23
## [139.1.0+139.0.20](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.0.1+139.0.17...export-cef-dir-v139.1.0+139.0.20) - 2025-08-15
### Other
- *(release)* update CEF version to 139.0.20
## [139.0.1+139.0.17](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v139.0.0+139.0.17...export-cef-dir-v139.0.1+139.0.17) - 2025-08-08
### Other
- release v139.0.1+139.0.17
## [139.0.0+139.0.17](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.9.0+138.0.36...export-cef-dir-v139.0.0+139.0.17) - 2025-08-08
### Other
- *(release)* update CEF version to 139.0.17
## [138.9.0+138.0.36](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.8.0+138.0.34...export-cef-dir-v138.9.0+138.0.36) - 2025-08-07
### Other
- *(release)* update CEF version to 138.0.36
## [138.8.0+138.0.34](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.7.1+138.0.33...export-cef-dir-v138.8.0+138.0.34) - 2025-08-02
### Fixed
- remove cef version from example dependencies
### Other
- *(release)* update CEF version to 138.0.34
## [138.7.1+138.0.33](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.7.0+138.0.33...export-cef-dir-v138.7.1+138.0.33) - 2025-07-29
### Other
- release v138.7.1+138.0.33
- move examples into separate crates
## [138.7.0+138.0.33](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.6.1+138.0.27...export-cef-dir-v138.7.0+138.0.33) - 2025-07-29
### Other
- *(release)* update CEF version to 138.0.33
## [138.6.1+138.0.27](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.6.0+138.0.27...export-cef-dir-v138.6.1+138.0.27) - 2025-07-28
### Fixed
- embed git-cliff as a library in get-latest
### Other
- *(release)* bump version for get-latest updates
## [138.6.0+138.0.27](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.5.1+138.0.26...export-cef-dir-v138.6.0+138.0.27) - 2025-07-28
### Added
- update CEF version to 138.0.27
### Fixed
- bump version for release
## [138.5.1+138.0.26](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.5.0+138.0.26...export-cef-dir-v138.5.1+138.0.26) - 2025-07-22
### Other
- release
- *(doc)* regenerate CHANGELOG.md
## [138.5.0+138.0.26](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.4.0+138.0.25...export-cef-dir-v138.5.0+138.0.26) - 2025-07-19
### Other
- update CEF version
## [138.4.0+138.0.25](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.3.0+138.0.23...export-cef-dir-v138.4.0+138.0.25) - 2025-07-18
### Other
- update CEF version
## [138.3.0+138.0.23](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.2.2+138.0.21...export-cef-dir-v138.3.0+138.0.23) - 2025-07-17
### Other
- update CEF version
## [138.2.2+138.0.21](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.2.1+138.0.21...export-cef-dir-v138.2.2+138.0.21) - 2025-07-14
### Other
- release
- seed CHANGELOG.md files
## [138.2.1+138.0.21](https://github.com/tauri-apps/cef-rs/compare/export-cef-dir-v138.2.0+138.0.21...export-cef-dir-v138.2.1+138.0.21) - 2025-07-14
### Fixed
- bump major version of download-cef [#145](https://github.com/tauri-apps/cef-rs/issues/145)

View File

@ -0,0 +1,16 @@
[package]
name = "export-cef-dir"
description = "Export pre-built CEF (Chromium Embedded Framework) archives."
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
download-cef.workspace = true
semver.workspace = true
serde_json.workspace = true

View File

@ -0,0 +1,28 @@
# export-cef-dir
Export files from the prebuilt [Chromium Embedded Framework](https://github.com/chromiumembedded/cef)
archive on any supported platform. The structure of the exported directory matches the way that
the `cef-dll-sys` crate expects to see them.
To use the target directory when building, set the `CEF_PATH` environment variable to the path of the
exported directory, e.g., `~/.local/share/cef`.
To use the DLLs in this directory at runtime, the library loader path varies by platform:
- Linux
```sh
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$CEF_PATH"
```
- macOS
```sh
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$CEF_PATH"
```
- Windows (using PowerShell)
```pwsh
$env:PATH = "$env:PATH;$env:CEF_PATH"
```

View File

@ -0,0 +1,136 @@
#![doc = include_str!("../README.md")]
use clap::Parser;
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(|| download_cef::default_version(env!("CARGO_PKG_VERSION")))
.as_str()
}
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()
}
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
#[arg(short, long)]
force: bool,
#[arg(short, long)]
save_archive: bool,
#[arg(short, long, default_value = DEFAULT_TARGET)]
target: String,
#[arg(short, long, default_value = default_version())]
version: String,
#[arg(short, long, default_value = default_download_url())]
mirror_url: String,
#[arg(short, long)]
archive: Option<String>,
output: String,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let output = PathBuf::from(args.output);
let url = args.mirror_url.as_str();
let parent = PathBuf::from(
output
.parent()
.ok_or_else(|| anyhow::anyhow!("invalid target directory: {}", output.display()))?,
);
if fs::exists(&output)? {
if !args.force {
return Err(anyhow::anyhow!(
"target directory already exists: {}",
output.display()
));
}
let dir = output
.file_name()
.and_then(|dir| dir.to_str())
.ok_or_else(|| anyhow::anyhow!("invalid target directory: {}", output.display()))?;
let old_output = parent.join(format!("old_{dir}"));
fs::rename(&output, &old_output)?;
println!("Cleaning up: {}", old_output.display());
fs::remove_dir_all(old_output)?
}
let target = args.target.as_str();
let os_arch = OsAndArch::try_from(target)?;
let cef_dir = os_arch.to_string();
let cef_dir = parent.join(&cef_dir);
if fs::exists(&cef_dir)? {
let dir = cef_dir
.file_name()
.and_then(|dir| dir.to_str())
.ok_or_else(|| anyhow::anyhow!("invalid target directory: {}", output.display()))?;
let old_cef_dir = parent.join(format!("old_{dir}"));
fs::rename(&cef_dir, &old_cef_dir)?;
println!("Cleaning up: {}", old_cef_dir.display());
fs::remove_dir_all(old_cef_dir)?
}
let (archive, extracted_dir) = match args.archive {
Some(archive) => {
let extracted_dir =
download_cef::extract_target_archive(target, &archive, &parent, true)?;
let archive = CefFile::try_from(Path::new(&archive))?;
(archive, extracted_dir)
}
None => {
let cef_version = args.version.as_str();
let index = CefIndex::download_from(url)?;
let platform = index.platform(target)?;
let version = platform.version(cef_version)?;
let archive = version.download_archive_with_retry_from(
url,
&parent,
true,
Duration::from_secs(15),
3,
)?;
let extracted_dir =
download_cef::extract_target_archive(target, &archive, &parent, true)?;
if !args.save_archive {
println!("Cleaning up: {}", archive.display());
fs::remove_file(archive)?;
}
let archive = version.minimal()?.clone();
(archive, extracted_dir)
}
};
if extracted_dir != cef_dir {
return Err(anyhow::anyhow!(
"extracted dir {extracted_dir:?} does not match cef_dir {cef_dir:?}",
));
}
archive.write_archive_json(extracted_dir)?;
if output != cef_dir {
println!("Renaming: {}", output.display());
fs::rename(cef_dir, output)?;
}
Ok(())
}

View File

@ -0,0 +1,21 @@
[package]
name = "get-latest"
publish = false
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
download-cef.workspace = true
clap.workspace = true
git-cliff.workspace = true
git-cliff-core.workspace = true
regex.workspace = true
semver.workspace = true
serde.workspace = true
thiserror.workspace = true
toml_edit.workspace = true

View File

@ -0,0 +1,5 @@
# get-latest
Download the list of versions available from the [Chromium Embedded Framework (CEF) Automated
Builds](https://cef-builds.spotifycdn.com/index.html) page and determine the latest version that is
available for every platform we support.

View File

@ -0,0 +1,112 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# A Tera template to be rendered as the changelog's footer.
# See https://keats.github.io/tera/docs/#introduction
header = """# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
"""
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{%- if version %}
## [{{ version }}]\
{%- if previous.version == version -%}\
(<REPO>/releases/tag/{{ version }})\
{%- else -%}\
(<REPO>/compare/{{ previous.version }}...{{ version }})\
{% endif %} \
- {{ timestamp | date(format="%Y-%m-%d") }}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
{%- if commit.scope -%}
- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message }}\
{%- if commit.links %} \
({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%})\
{% endif %}
{% else -%}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}
{% endif -%}
{% endfor -%}
{% endfor %}\
{%- else -%}
{% endif -%}
"""
# A Tera template to be rendered as the changelog's footer.
# See https://keats.github.io/tera/docs/#introduction
footer = ""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
{ pattern = '<REPO>', replace = "https://github.com/tauri-apps/cef-rs" },
{ pattern = '\[[\w\-]+-v([0-9.+]+)\]', replace = "[${1}]" },
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
{ pattern = '(\w+\s)?#([0-9]+)', replace = "${1}[#${2}](<REPO>/issues/${2})" },
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))" },
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "added" },
{ message = "^changed", group = "changed" },
{ message = "^deprecated", group = "deprecated" },
{ message = "^fix", group = "fixed" },
{ message = "^security", group = "security" },
{ message = "^.*", group = "other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order releases topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "newest"
# Process submodules commits
recurse_submodules = false

View File

@ -0,0 +1,235 @@
#![doc = include_str!("../README.md")]
#[macro_use]
extern crate thiserror;
use clap::Parser;
use download_cef::{CefIndex, Channel, LINUX_TARGETS, MACOS_TARGETS, WINDOWS_TARGETS};
use git_cliff::args::*;
use regex::Regex;
use semver::{BuildMetadata, Version};
use std::{
env, fs,
io::Write,
path::PathBuf,
process::{Command, ExitStatus},
sync::OnceLock,
};
use toml_edit::{value, DocumentMut};
#[derive(Debug, Error)]
enum Error {
#[error("Download error: {0}")]
Download(#[from] download_cef::Error),
#[error("Invalid regex pattern: {0}")]
InvalidRegexPattern(#[from] regex::Error),
#[error("Invalid version: {0}")]
InvalidVersion(#[from] semver::Error),
#[error("No versions found")]
NoVersionsFound,
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid manifest file: {0}")]
InvalidManifest(#[from] toml_edit::TomlError),
#[error("Error invoking git: {0:?}")]
GitInvocation(ExitStatus),
#[error("Error running git-cliff: {0:?}")]
InvalidGitCliffArgs(#[from] clap::Error),
#[error("Error updating change log: {0:?}")]
UpdateChangeLog(#[from] git_cliff_core::error::Error),
}
type Result<T> = std::result::Result<T, Error>;
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()
}
#[derive(Parser)]
#[command(about, long_about = None)]
struct Args {
#[arg(short, long, default_value = default_download_url())]
mirror_url: String,
#[arg(short, long, default_value = "stable")]
channel: Channel,
#[arg(short, long)]
update_version: bool,
}
fn main() -> Result<()> {
let pattern = Regex::new(r"^([^+]+)(:?\+.+)?$")?;
let args = Args::parse();
let channel = args.channel;
let url = args.mirror_url.as_str();
let index = CefIndex::download_from(url)?;
let latest_versions: Vec<_> = LINUX_TARGETS
.iter()
.chain(MACOS_TARGETS.iter())
.chain(WINDOWS_TARGETS.iter())
.map(|target| {
index
.platform(target)
.and_then(|platform| platform.latest(channel.clone()))
.map(|version| pattern.replace(&version.cef_version, "$1"))
})
.collect::<download_cef::Result<Vec<_>>>()?;
let latest_versions = latest_versions
.into_iter()
.map(|version| Ok(Version::parse(&version)?))
.collect::<Result<Vec<_>>>()?;
let latest_version = latest_versions
.into_iter()
.min()
.ok_or(Error::NoVersionsFound)?;
println!("Latest available {channel} version: {latest_version}");
if args.update_version {
let current_version =
Version::parse(&download_cef::default_version(env!("CARGO_PKG_VERSION")))?;
if current_version < latest_version {
let latest_build = BuildMetadata::new(&latest_version.to_string())?;
let mut next_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
if next_version.major < latest_version.major {
next_version.major = latest_version.major;
next_version.minor = 0;
} else {
next_version.minor += 1;
}
next_version.patch = 0;
next_version.build = latest_build.clone();
let mut manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest.pop();
let manifest = manifest.join("Cargo.toml");
let mut doc = fs::read_to_string(&manifest)?.parse::<DocumentMut>()?;
doc["workspace"]["package"]["version"] = value(next_version.to_string());
let workspace_version = Version {
build: BuildMetadata::EMPTY,
..next_version.clone()
};
doc["workspace"]["dependencies"]["cef-dll-sys"]["version"] =
value(workspace_version.to_string());
fs::write(&manifest, doc.to_string().as_bytes())?;
if let Ok(output) = env::var("GITHUB_OUTPUT") {
let mut output = fs::OpenOptions::new()
.create(true)
.append(true)
.open(output)?;
let commit_message =
format!("chore(release): update CEF version to {latest_version}");
writeln!(output, "commit-message={commit_message}",)?;
let output = Command::new("git")
.args(["commit", "-a", "-m", commit_message.as_str()])
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
if !output.status.success() {
return Err(Error::GitInvocation(output.status));
}
let export_cef_dir_tag = format!("export-cef-dir-v{next_version}");
let output = Command::new("git")
.args(["tag", "--no-sign", "-f", export_cef_dir_tag.as_str()])
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
if !output.status.success() {
return Err(Error::GitInvocation(output.status));
}
let cef_dll_sys_tag = format!("cef-dll-sys-v{next_version}");
let output = Command::new("git")
.args(["tag", "--no-sign", "-f", cef_dll_sys_tag.as_str()])
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
if !output.status.success() {
return Err(Error::GitInvocation(output.status));
}
let cef_tag = format!("cef-v{next_version}");
let output = Command::new("git")
.args(["tag", "--no-sign", "-f", cef_tag.as_str()])
.output()?;
std::io::stdout().write_all(&output.stdout)?;
std::io::stderr().write_all(&output.stderr)?;
if !output.status.success() {
return Err(Error::GitInvocation(output.status));
}
let mut config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
config_path.push("cliff.toml");
let common_opts = ["--strip", "footer", "--include-path", "Cargo.toml"];
let export_cef_dir_opts = Opt {
config: config_path.clone(),
range: Some("export-cef-dir-v138.2.0+138.0.21..".to_string()),
..Opt::try_parse_from(
common_opts.iter().chain(
[
"--include-path",
"export-cef-dir/*",
"--tag-pattern",
"^export-cef-dir-v",
"--output",
"export-cef-dir/CHANGELOG.md",
]
.iter(),
),
)?
};
git_cliff::run(export_cef_dir_opts)?;
let cef_dll_sys_opts = Opt {
config: config_path.clone(),
range: Some("cef-dll-sys-v138.2.0+138.0.21..".to_string()),
..Opt::try_parse_from(
common_opts.iter().chain(
[
"--include-path",
"sys/*",
"--tag-pattern",
"^cef-dll-sys-v",
"--output",
"sys/CHANGELOG.md",
]
.iter(),
),
)?
};
git_cliff::run(cef_dll_sys_opts)?;
let cef_opts = Opt {
config: config_path,
range: Some("cef-v138.2.0+138.0.21..".to_string()),
..Opt::try_parse_from(
common_opts.iter().chain(
[
"--include-path",
"cef/*",
"--tag-pattern",
"^cef-v",
"--output",
"cef/CHANGELOG.md",
]
.iter(),
),
)?
};
git_cliff::run(cef_opts)?;
}
}
}
Ok(())
}

202
browser/sys/CHANGELOG.md Normal file
View File

@ -0,0 +1,202 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [140.2.0+140.1.14](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v140.1.0+140.1.13...cef-dll-sys-v140.2.0+140.1.14) - 2025-09-21
### Other
- *(release)* update CEF version to 140.1.14
## [140.1.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v140.0.0+140.1.13...cef-dll-sys-v140.1.0+140.1.13) - 2025-09-19
### Other
- release
## [140.0.0+140.1.13](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.8.0+139.0.40...cef-dll-sys-v140.0.0+140.1.13) - 2025-09-19
### Other
- update bindings
- *(release)* update CEF version to 140.1.13
## [139.8.0+139.0.40](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.7.2+139.0.38...cef-dll-sys-v139.8.0+139.0.40) - 2025-09-12
### Other
- update bindings
- *(release)* update CEF version to 139.0.40
## [139.7.2+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.7.1+139.0.38...cef-dll-sys-v139.7.2+139.0.38) - 2025-09-08
### Fixed
- handle out-params ([#173](https://github.com/tauri-apps/cef-rs/issues/173))
### Other
- release v139.7.2+139.0.38
- update bindings
## [139.7.1+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.7.0+139.0.38...cef-dll-sys-v139.7.1+139.0.38) - 2025-09-07
### Other
- release v139.7.1+139.0.38
- *(deps)* update rust crate windows-sys to 0.61
## [139.7.0+139.0.38](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.6.0+139.0.37...cef-dll-sys-v139.7.0+139.0.38) - 2025-08-31
### Other
- *(release)* update CEF version to 139.0.38
## [139.6.0+139.0.37](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.5.0+139.0.30...cef-dll-sys-v139.6.0+139.0.37) - 2025-08-29
### Other
- *(release)* update CEF version to 139.0.37
## [139.5.0+139.0.30](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.4.0+139.0.28...cef-dll-sys-v139.5.0+139.0.30) - 2025-08-28
### Other
- *(release)* update CEF version to 139.0.30
## [139.4.0+139.0.28](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.3.0+139.0.26...cef-dll-sys-v139.4.0+139.0.28) - 2025-08-23
### Other
- *(release)* update CEF version to 139.0.28
## [139.3.0+139.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.2.1+139.0.23...cef-dll-sys-v139.3.0+139.0.26) - 2025-08-22
### Other
- *(release)* update CEF version to 139.0.26
## [139.2.1+139.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.2.0+139.0.23...cef-dll-sys-v139.2.1+139.0.23) - 2025-08-16
### Other
- release
## [139.2.0+139.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.1.0+139.0.20...cef-dll-sys-v139.2.0+139.0.23) - 2025-08-16
### Other
- *(release)* update CEF version to 139.0.23
## [139.1.0+139.0.20](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.0.1+139.0.17...cef-dll-sys-v139.1.0+139.0.20) - 2025-08-15
### Other
- *(release)* update CEF version to 139.0.20
## [139.0.1+139.0.17](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v139.0.0+139.0.17...cef-dll-sys-v139.0.1+139.0.17) - 2025-08-08
### Other
- release v139.0.1+139.0.17
## [139.0.0+139.0.17](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.9.0+138.0.36...cef-dll-sys-v139.0.0+139.0.17) - 2025-08-08
### Other
- update bindings
- *(release)* update CEF version to 139.0.17
## [138.9.0+138.0.36](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.8.0+138.0.34...cef-dll-sys-v138.9.0+138.0.36) - 2025-08-07
### Other
- *(release)* update CEF version to 138.0.36
## [138.8.0+138.0.34](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.7.1+138.0.33...cef-dll-sys-v138.8.0+138.0.34) - 2025-08-02
### Fixed
- remove cef version from example dependencies
### Other
- *(release)* update CEF version to 138.0.34
## [138.7.1+138.0.33](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.7.0+138.0.33...cef-dll-sys-v138.7.1+138.0.33) - 2025-07-29
### Other
- release v138.7.1+138.0.33
- move examples into separate crates
## [138.7.0+138.0.33](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.6.1+138.0.27...cef-dll-sys-v138.7.0+138.0.33) - 2025-07-29
### Other
- update bindings
- *(release)* update CEF version to 138.0.33
## [138.6.1+138.0.27](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.6.0+138.0.27...cef-dll-sys-v138.6.1+138.0.27) - 2025-07-28
### Fixed
- embed git-cliff as a library in get-latest
### Other
- *(release)* bump version for get-latest updates
## [138.6.0+138.0.27](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.5.1+138.0.26...cef-dll-sys-v138.6.0+138.0.27) - 2025-07-28
### Added
- update CEF version to 138.0.27
### Fixed
- bump version for release
## [138.5.1+138.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.5.0+138.0.26...cef-dll-sys-v138.5.1+138.0.26) - 2025-07-22
### Other
- release
- *(doc)* regenerate CHANGELOG.md
## [138.5.0+138.0.26](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.4.0+138.0.25...cef-dll-sys-v138.5.0+138.0.26) - 2025-07-19
### Other
- update CEF version
## [138.4.0+138.0.25](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.3.0+138.0.23...cef-dll-sys-v138.4.0+138.0.25) - 2025-07-18
### Other
- update CEF version
## [138.3.0+138.0.23](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.2.2+138.0.21...cef-dll-sys-v138.3.0+138.0.23) - 2025-07-17
### Other
- update CEF version
## [138.2.2+138.0.21](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.2.1+138.0.21...cef-dll-sys-v138.2.2+138.0.21) - 2025-07-14
### Other
- release
- seed CHANGELOG.md files
## [138.2.1+138.0.21](https://github.com/tauri-apps/cef-rs/compare/cef-dll-sys-v138.2.0+138.0.21...cef-dll-sys-v138.2.1+138.0.21) - 2025-07-14
### Fixed
- bump major version of download-cef [#145](https://github.com/tauri-apps/cef-rs/issues/145)

26
browser/sys/Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "cef-dll-sys"
description = "cef-rs sys crate"
links = "cef_dll_wrapper"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[lib]
doctest = false
[features]
dox = []
sandbox = []
[package.metadata.docs.rs]
features = ["dox"]
[build-dependencies]
anyhow.workspace = true
cmake.workspace = true
download-cef.workspace = true
serde_json.workspace = true

4
browser/sys/README.md Normal file
View File

@ -0,0 +1,4 @@
# cef-dll-sys
Generated bindings for the prebuilt [Chromium Embedded Framework](https://github.com/chromiumembedded/cef)
C API on any supported platform.

131
browser/sys/build.rs Normal file
View File

@ -0,0 +1,131 @@
#[cfg(not(feature = "dox"))]
fn main() -> anyhow::Result<()> {
use download_cef::{CefIndex, OsAndArch};
use std::{env, fs, path::PathBuf};
println!("cargo::rerun-if-changed=build.rs");
let target = env::var("TARGET")?;
let os_arch = OsAndArch::try_from(target.as_str())?;
println!("cargo::rerun-if-env-changed=FLATPAK");
println!("cargo::rerun-if-env-changed=CEF_PATH");
let cef_path_env = env::var("FLATPAK")
.map(|_| String::from("/usr/lib"))
.or_else(|_| env::var("CEF_PATH"));
let cef_dir = match cef_path_env {
Ok(cef_path) => {
// Allow overriding the CEF path with environment variables.
println!("Using CEF path from environment: {cef_path}");
download_cef::check_archive_json(&env::var("CARGO_PKG_VERSION")?, &cef_path)?;
PathBuf::from(cef_path)
}
Err(_) => {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let cef_dir = os_arch.to_string();
let cef_dir = out_dir.join(&cef_dir);
if !fs::exists(&cef_dir)? {
let cef_version = download_cef::default_version(&env::var("CARGO_PKG_VERSION")?);
let index = CefIndex::download()?;
let platform = index.platform(&target)?;
let version = platform.version(&cef_version)?;
let archive = version.download_archive(&out_dir, false)?;
let extracted_dir =
download_cef::extract_target_archive(&target, &archive, &out_dir, false)?;
if extracted_dir != cef_dir {
return Err(anyhow::anyhow!(
"extracted dir {extracted_dir:?} does not match cef_dir {cef_dir:?}",
));
}
version.write_archive_json(extracted_dir)?;
}
cef_dir
}
};
let cef_dir = cef_dir.display().to_string();
println!("cargo::metadata=CEF_DIR={cef_dir}");
println!("cargo::rustc-link-search=native={cef_dir}");
let mut cef_dll_wrapper = cmake::Config::new(&cef_dir);
cef_dll_wrapper
.generator("Ninja")
.profile("RelWithDebInfo")
.build_target("libcef_dll_wrapper");
let project_arch = match os_arch.arch {
"aarch64" => "arm64",
arch => arch,
};
let sandbox = if cfg!(feature = "sandbox") {
"ON"
} else {
"OFF"
};
match os_arch.os {
"linux" => {
println!("cargo::rustc-link-lib=dylib=cef");
}
"windows" => {
let sdk_libs = [
"comctl32.lib",
"delayimp.lib",
"mincore.lib",
"powrprof.lib",
"propsys.lib",
"runtimeobject.lib",
"setupapi.lib",
"shcore.lib",
"shell32.lib",
"shlwapi.lib",
"user32.lib",
"version.lib",
"wbemuuid.lib",
"winmm.lib",
]
.join(" ");
let build_dir = cef_dll_wrapper
.define("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreaded")
.define("CMAKE_OBJECT_PATH_MAX", "500")
.define("CMAKE_STATIC_LINKER_FLAGS", &sdk_libs)
.define("PROJECT_ARCH", project_arch)
.define("USE_SANDBOX", sandbox)
.build()
.display()
.to_string();
println!("cargo::rustc-link-search=native={build_dir}/build/libcef_dll_wrapper");
println!("cargo::rustc-link-lib=static=libcef_dll_wrapper");
println!("cargo::rustc-link-lib=dylib=libcef");
}
"macos" => {
println!("cargo::rustc-link-lib=framework=AppKit");
let build_dir = cef_dll_wrapper
.no_default_flags(true)
.define("PROJECT_ARCH", project_arch)
.define("USE_SANDBOX", sandbox)
.build()
.display()
.to_string();
println!("cargo::rustc-link-search=native={build_dir}/build/libcef_dll_wrapper");
println!("cargo::rustc-link-lib=static=cef_dll_wrapper");
}
os => unimplemented!("unknown target {os}"),
}
Ok(())
}
#[cfg(feature = "dox")]
fn main() {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod x86_64_unknown_linux_gnu;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub use x86_64_unknown_linux_gnu::*;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
mod aarch64_unknown_linux_gnu;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
pub use aarch64_unknown_linux_gnu::*;
#[cfg(all(target_os = "linux", target_arch = "arm"))]
mod arm_unknown_linux_gnueabi;
#[cfg(all(target_os = "linux", target_arch = "arm"))]
pub use arm_unknown_linux_gnueabi::*;
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
mod x86_64_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
pub use x86_64_pc_windows_msvc::*;
#[cfg(all(target_os = "windows", target_arch = "x86"))]
mod i686_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "x86"))]
pub use i686_pc_windows_msvc::*;
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
mod aarch64_pc_windows_msvc;
#[cfg(all(target_os = "windows", target_arch = "aarch64"))]
pub use aarch64_pc_windows_msvc::*;
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
mod x86_64_apple_darwin;
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
pub use x86_64_apple_darwin::*;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
mod aarch64_apple_darwin;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub use aarch64_apple_darwin::*;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

91
browser/sys/src/lib.rs Normal file
View File

@ -0,0 +1,91 @@
#![doc = include_str!("../README.md")]
#[allow(
non_snake_case,
non_camel_case_types,
non_upper_case_globals,
dead_code,
clippy::all
)]
mod bindings;
pub use bindings::*;
#[cfg(target_os = "windows")]
impl Default for HWND {
fn default() -> Self {
Self(std::ptr::null_mut())
}
}
#[cfg(target_os = "windows")]
impl Default for HINSTANCE {
fn default() -> Self {
Self(std::ptr::null_mut())
}
}
#[cfg(target_os = "macos")]
pub const FRAMEWORK_PATH: &str =
"Chromium Embedded Framework.framework/Chromium Embedded Framework";
use std::{
env::{
self,
consts::{ARCH, OS},
},
fs,
path::PathBuf,
};
pub fn get_cef_dir() -> Option<PathBuf> {
let cef_path_env = env::var("FLATPAK")
.map(|_| String::from("/usr/lib"))
.or_else(|_| env::var("CEF_PATH"));
match cef_path_env {
Ok(cef_path) => {
// Allow overriding the CEF path with environment variables.
PathBuf::from(cef_path).canonicalize().ok()
}
Err(_) => {
let out_dir = PathBuf::from(env::var("OUT_DIR").ok()?);
let cef_dir = format!("cef_{OS}_{ARCH}");
let cef_dir = out_dir.join(&cef_dir).canonicalize().ok()?;
fs::exists(&cef_dir).ok()?.then_some(cef_dir)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_cef_dir() {
let _ = get_cef_dir().expect("CEF not found");
}
#[test]
fn test_init() {
use std::ptr::*;
unsafe {
#[cfg(target_os = "macos")]
{
use std::os::unix::ffi::OsStrExt;
let cef_dir = get_cef_dir().expect("CEF not found");
let framework_dir = cef_dir
.join(FRAMEWORK_PATH)
.canonicalize()
.expect("failed to get framework path");
let framework_dir = std::ffi::CString::new(framework_dir.as_os_str().as_bytes())
.expect("invalid path");
assert_eq!(cef_load_library(framework_dir.as_ptr().cast()), 1);
}
assert_eq!(cef_initialize(null(), null(), null_mut(), null_mut()), 0);
};
}
}

33
browser/sys/wrapper.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef CEF_RUST_SYS_WRAPPER_H
#define CEF_RUST_SYS_WRAPPER_H
#ifdef __APPLE__
#include "include/wrapper/cef_library_loader.h"
#include "include/cef_sandbox_mac.h"
#endif
#include "include/cef_api_hash.h"
#include "include/cef_version.h"
#include "include/capi/cef_base_capi.h"
#include "include/capi/cef_app_capi.h"
#include "include/capi/cef_client_capi.h"
#include "include/capi/cef_urlrequest_capi.h"
#include "include/capi/views/cef_layout_capi.h"
#include "include/capi/views/cef_box_layout_capi.h"
#include "include/capi/views/cef_fill_layout_capi.h"
#include "include/capi/views/cef_button_capi.h"
#include "include/capi/views/cef_label_button_capi.h"
#include "include/capi/views/cef_menu_button_capi.h"
#include "include/capi/views/cef_textfield_capi.h"
#include "include/capi/views/cef_browser_view_capi.h"
#include "include/capi/views/cef_scroll_view_capi.h"
#include "include/capi/views/cef_window_capi.h"
#endif

View File

@ -0,0 +1,22 @@
[package]
name = "update-bindings"
publish = false
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
download-cef.workspace = true
bindgen.workspace = true
clap.workspace = true
convert_case.workspace = true
proc-macro2.workspace = true
quote.workspace = true
regex.workspace = true
semver.workspace = true
syn.workspace = true
thiserror.workspace = true

View File

@ -0,0 +1,8 @@
# update-bindings
Download the prebuilt [Chromium Embedded Framework](https://github.com/chromiumembedded/cef)
archive on any supported platform and run `bindgen` on the C API for the `cef-dll-sys` crate,
then regenerate the safe bindings in the `cef` crate.
You can find the latest version of the prebuilt CEF archives on the [Chromium Embedded Framework
(CEF) Automated Builds](https://cef-builds.spotifycdn.com/index.html).

View File

@ -0,0 +1,2 @@
// Empty build.rs file to set OUT_DIR environment variable
fn main() {}

View File

@ -0,0 +1,39 @@
use std::{convert::From, env, path::PathBuf};
pub fn get_manifest_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
pub fn get_out_dir() -> PathBuf {
PathBuf::from(env!("OUT_DIR"))
}
pub fn get_cef_root(os: &str, arch: &str) -> PathBuf {
env::var(format!("CEF_PATH_{os}_{arch}"))
.map(PathBuf::from)
.unwrap_or_else(|_| {
let mut out_dir = get_out_dir();
out_dir.push(format!("cef_{os}_{arch}"));
out_dir
})
}
pub fn get_sys_dir() -> crate::Result<PathBuf> {
let manifest_dir = get_manifest_dir();
let mut bindings_dir = get_manifest_dir().parent().map_or_else(
|| Err(crate::Error::MissingParent(manifest_dir)),
|parent| Ok(PathBuf::from(parent)),
)?;
bindings_dir.push("sys");
Ok(bindings_dir)
}
pub fn get_cef_dir() -> crate::Result<PathBuf> {
let manifest_dir = get_manifest_dir();
let mut webview2_com_dir = get_manifest_dir().parent().map_or_else(
|| Err(crate::Error::MissingParent(manifest_dir)),
|parent| Ok(PathBuf::from(parent)),
)?;
webview2_com_dir.push("cef");
Ok(webview2_com_dir)
}

View File

@ -0,0 +1,102 @@
#![doc = include_str!("../README.md")]
#[macro_use]
extern crate thiserror;
use clap::Parser;
use download_cef::DEFAULT_TARGET;
use std::{fs, io::Read, path::Path, sync::OnceLock};
#[derive(Debug, Error)]
pub enum Error {
#[error("Missing Parent")]
MissingParent(std::path::PathBuf),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Bindgen(#[from] bindgen::BindgenError),
#[error(transparent)]
Regex(#[from] regex::Error),
#[error(transparent)]
Syn(#[from] syn::Error),
#[error("Parsing bindgen output failed")]
Parse(#[from] parse_tree::Unrecognized),
#[error("Missing Path")]
MissingPath(std::path::PathBuf),
}
pub type Result<T> = std::result::Result<T, Error>;
mod dirs;
mod parse_tree;
mod upgrade;
fn default_version() -> &'static str {
static DEFAULT_VERSION: OnceLock<String> = OnceLock::new();
DEFAULT_VERSION
.get_or_init(|| download_cef::default_version(env!("CARGO_PKG_VERSION")))
.as_str()
}
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()
}
#[derive(Parser, Debug)]
#[command(about, long_about = None)]
struct Args {
#[arg(short, long)]
download: bool,
#[arg(short, long)]
bindgen: bool,
#[arg(short, long, default_value = DEFAULT_TARGET)]
target: String,
#[arg(short, long, default_value = default_version())]
version: String,
#[arg(short, long, default_value = default_download_url())]
mirror_url: String,
}
fn main() -> Result<()> {
let args = Args::parse();
let target = args.target.as_str();
if args.bindgen {
if args.download {
let _ = upgrade::download(args.mirror_url.as_str(), target, args.version.as_str());
}
upgrade::sys_bindgen(target)?;
}
let bindings_file = upgrade::get_target_bindings(target);
let mut sys_bindings = dirs::get_sys_dir()?;
sys_bindings.push("src");
sys_bindings.push("bindings");
sys_bindings.push(&bindings_file);
let mut cef_bindings = dirs::get_cef_dir()?;
cef_bindings.push("src");
cef_bindings.push("bindings");
cef_bindings.push(&bindings_file);
let bindings = parse_tree::generate_bindings(&sys_bindings)?;
let source = read_bindings(&bindings)?;
let dest = read_bindings(&cef_bindings).unwrap_or_default();
if source != dest {
fs::copy(&bindings, &cef_bindings)?;
println!("Updated: {}", cef_bindings.display());
}
Ok(())
}
fn read_bindings(source_path: &Path) -> crate::Result<String> {
let mut source_file = fs::File::open(source_path)?;
let mut updated = String::default();
source_file.read_to_string(&mut updated)?;
Ok(updated)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
use crate::dirs;
use std::{
path::{Path, PathBuf},
process::Command,
};
const TARGETS: &[&str] = &[
// macos
"aarch64-apple-darwin",
"x86_64-apple-darwin",
// windows
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
"i686-pc-windows-msvc",
// linux
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"arm-unknown-linux-gnueabi",
];
pub fn download(url: &str, target: &str, version: &str) -> PathBuf {
assert!(TARGETS.contains(&target), "unsupported target {target}");
let archive =
download_cef::download_target_archive_from(url, target, version, dirs::get_out_dir(), true)
.expect("download failed");
download_cef::extract_target_archive(target, &archive, dirs::get_out_dir(), true)
.expect("extraction failed")
}
pub fn sys_bindgen(target: &str) -> crate::Result<()> {
assert!(TARGETS.contains(&target), "unsupported target {target}");
let (os, arch) = target_to_os_arch(target);
let cef_path = dirs::get_cef_root(os, arch);
bindgen(target, &cef_path)
}
pub fn get_target_bindings(target: &str) -> String {
assert!(TARGETS.contains(&target), "unsupported target {target}");
format!("{}.rs", target.replace('-', "_"))
}
fn bindgen(target: &str, cef_path: &Path) -> crate::Result<()> {
let mut sys_bindings = dirs::get_sys_dir()?;
let mut wrapper = sys_bindings.clone();
sys_bindings.push("src");
sys_bindings.push("bindings");
sys_bindings.push(format!("{}.rs", target.replace('-', "_")));
wrapper.push("wrapper.h");
let mut bindings = bindgen::Builder::default()
.header(wrapper.display().to_string())
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: true,
})
.allowlist_type("cef_.*")
.allowlist_function("cef_.*")
.allowlist_item("CEF_API_VERSION(_.+)?")
.allowlist_item("CEF_VERSION(_.+)?")
.allowlist_item("CHROME_VERSION(_.+)?")
.default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
.bitfield_enum(".*_mask_t")
.clang_args([
format!("-I{}", cef_path.display()),
format!("--target={target}"),
]);
if target.contains("windows") {
bindings = bindings.new_type_alias("HINSTANCE").new_type_alias("HWND");
} else if target.contains("apple") {
let sdk_path = Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.unwrap()
.stdout;
bindings = bindings.clang_arg(format!(
"--sysroot={}",
String::from_utf8_lossy(&sdk_path).trim()
));
}
let bindings = bindings.generate()?;
bindings.write_to_file(&sys_bindings)?;
Ok(())
}
fn target_to_os_arch(target: &str) -> (&str, &str) {
match target {
"aarch64-apple-darwin" => ("macos", "aarch64"),
"x86_64-apple-darwin" => ("macos", "x86_64"),
"x86_64-pc-windows-msvc" => ("windows", "x86_64"),
"aarch64-pc-windows-msvc" => ("windows", "aarch64"),
"i686-pc-windows-msvc" => ("windows", "x86"),
"x86_64-unknown-linux-gnu" => ("linux", "x86_64"),
"aarch64-unknown-linux-gnu" => ("linux", "aarch64"),
"arm-unknown-linux-gnueabi" => ("linux", "arm"),
v => panic!("unsupported {v:?}"),
}
}