mirror of
https://github.com/aphrody-labs/cef-rs.git
synced 2026-10-13 21:06:46 +00:00
feat(sys): support cross-compiling to Windows with cargo-xwin (#462)
* feat(sys): support cross-compiling to Windows with cargo-xwin
Building `cef-dll-sys` for `x86_64-pc-windows-msvc` from a Linux host with
cargo-xwin failed while compiling `libcef_dll_wrapper` with clang-cl, for
four reasons that are now handled in the build script:
- CEF's CMake clears `CMAKE_CXX_FLAGS` for the Ninja generator, dropping
the toolchain's `--target` and `/imsvc` include paths. They are now passed
through `CEF_C_COMPILER_FLAGS`/`CEF_CXX_COMPILER_FLAGS` instead, which CEF
appends after its own flags.
- clang-cl reports diagnostics that MSVC doesn't (`/MP` is unsupported,
`-Wmissing-field-initializers`, `-Wundefined-var-template`) and CEF's `/WX`
turned them into errors. Those are silenced and `/WX-` is appended.
- The Windows SDK import libraries were merged into the wrapper archive
through `CMAKE_STATIC_LINKER_FLAGS`, which llvm-lib can't do because it
rejects the XFG hash map member of current import libraries. They are
now emitted as `cargo::rustc-link-lib` directives for the final link.
- The SDK expects case-insensitive header lookups (`Softpub.h` vs
`SoftPub.h`). Mismatched headers get a correctly-cased copy in an extra
include directory under `OUT_DIR`.
The `cc` crate is used to detect clang-cl and to read the target CXXFLAGS,
so this is a no-op for native MSVC builds. A CI job cross-compiles the
`cefsimple` example on ubuntu-latest to keep this working.
* fix(sys): join clang-cl options that take a separate argument
cargo-xwin 0.23 exports the SDK include paths as `/imsvc <dir>` (two
tokens) where 0.19 used `/imsvc<dir>`. CMake de-duplicates compile
options, so every repeated `/imsvc` after the first was dropped and the
bare paths were passed to clang-cl as additional source files:
clang-cl: error: cannot specify '/Fo...' when compiling multiple source files
Join such option/value pairs into a single token before handing the flags
to CMake, and only add the clang-cl specific flags that aren't already
present so de-duplication has nothing left to remove.
* ci: use LLVM 20 for the Windows cross-compile job
The MSVC STL headers that cargo-xwin downloads now refuse compilers older
than Clang 19 (STL1000), and ubuntu-latest ships Clang 18. Install LLVM 20
from apt.llvm.org and put it first on PATH: cargo-xwin symlinks its
`clang-cl` to the `clang` found there, so only the SDK directory is cached.
* cleanup
---------
Co-authored-by: Lucas Nogueira <lucas@crabnebula.dev>
This commit is contained in:
1 parent
207bdd2917
commit
a7a4451b19
5 files changed
+230
-22
No files matched your search
@@ -68,3 +68,45 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
|
||||
cross-compile-windows:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# The MSVC STL headers require Clang 19 or newer, ubuntu-latest ships 18.
|
||||
wget -qO llvm.sh https://apt.llvm.org/llvm.sh
|
||||
sudo bash llvm.sh 20
|
||||
sudo apt-get install -yq llvm-20 ninja-build
|
||||
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
pipx install cargo-xwin
|
||||
echo "CARGO_XWIN_VERSION=$(cargo xwin --version | awk '{print $NF}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache CEF
|
||||
id: cache-cef
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ~/.local/share/cef
|
||||
key: cef-cross-windows-${{ hashFiles('Cargo.toml') }}
|
||||
|
||||
# Like the native jobs, export the distribution ahead of the build so the cache
|
||||
# holds only the extracted files and not the downloaded archive as well.
|
||||
- name: Export CEF
|
||||
if: steps.cache-cef.outputs.cache-hit != 'true'
|
||||
run: cargo run -p export-cef-dir -- --target x86_64-pc-windows-msvc "$HOME/.local/share/cef"
|
||||
|
||||
# Only the SDK itself: cargo-xwin symlinks `clang-cl` to the `clang` found on PATH
|
||||
# next to it, and that must be recreated for the toolchain installed above. The
|
||||
# SDK and CRT versions come from the cargo-xwin release, so key on it.
|
||||
- name: Cache Windows SDK
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ~/.cache/cargo-xwin/xwin
|
||||
key: cargo-xwin-sdk-${{ runner.os }}-${{ env.CARGO_XWIN_VERSION }}
|
||||
|
||||
- name: Build
|
||||
run: CEF_PATH="$HOME/.local/share/cef" cargo xwin build -p cefsimple --target x86_64-pc-windows-msvc --verbose
|
||||
@@ -31,6 +31,7 @@ anyhow = "1"
|
||||
ash = "0.38"
|
||||
bindgen = "0.73"
|
||||
cargo_metadata = "0.23.1"
|
||||
cc = "1.2.8"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
cmake = "0.1.52"
|
||||
convert_case = "0.12"
|
||||
|
||||
@@ -116,6 +116,18 @@ cargo run --bin bundle-cef-app -- cefsimple -o ./target/bundle
|
||||
./target/bundle/cefsimple.exe
|
||||
```
|
||||
|
||||
### Cross-compiling to Windows
|
||||
|
||||
The `cef-dll-sys` crate can be cross-compiled to `x86_64-pc-windows-msvc` from Linux with [cargo-xwin](https://github.com/rust-cross/cargo-xwin), which downloads the Windows SDK and sets up a `clang-cl` toolchain for both Rust and CMake. Install `clang`, `lld`, `llvm` and `ninja` from your package manager (the MSVC STL headers require Clang 19 or newer; on Ubuntu 24.04 use [apt.llvm.org](https://apt.llvm.org/)), then:
|
||||
|
||||
```sh
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
cargo install cargo-xwin
|
||||
cargo xwin build --target x86_64-pc-windows-msvc
|
||||
```
|
||||
|
||||
The `libcef_dll_wrapper` static library is built with `clang-cl` and the CEF runtime files are copied next to the output binaries, exactly like a native Windows build. The `CEF_PATH` environment variable works the same way as well: the Windows CEF binaries are downloaded into their own `cef_windows_x86_64` directory next to the host ones.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
||||
@@ -21,6 +21,7 @@ features = ["dox"]
|
||||
|
||||
[build-dependencies]
|
||||
anyhow.workspace = true
|
||||
cc.workspace = true
|
||||
cmake.workspace = true
|
||||
download-cef.workspace = true
|
||||
serde_json.workspace = true
|
||||
+174
-22
@@ -175,33 +175,38 @@ fn main() -> anyhow::Result<()> {
|
||||
// On macOS it's more complicated so we'll leave it to tools like tauri-cli for now.
|
||||
copy_cef_runtime_files(&cef_dir, target_dir)?;
|
||||
|
||||
// Windows SDK import libraries used by the wrapper. These used to be merged into
|
||||
// libcef_dll_wrapper.lib through CMAKE_STATIC_LINKER_FLAGS, but llvm-lib (used when
|
||||
// cross-compiling with cargo-xwin) can't read the XFG hash map member of current SDK
|
||||
// import libraries, so pass them to the final link through cargo instead.
|
||||
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(" ");
|
||||
"comctl32",
|
||||
"delayimp",
|
||||
"mincore",
|
||||
"powrprof",
|
||||
"propsys",
|
||||
"runtimeobject",
|
||||
"setupapi",
|
||||
"shcore",
|
||||
"shell32",
|
||||
"shlwapi",
|
||||
"user32",
|
||||
"version",
|
||||
"wbemuuid",
|
||||
"winmm",
|
||||
];
|
||||
for lib in sdk_libs {
|
||||
println!("cargo::rustc-link-lib=dylib={lib}");
|
||||
}
|
||||
|
||||
let build_dir = cef_dll_wrapper
|
||||
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()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
.define("USE_SANDBOX", sandbox);
|
||||
configure_clang_cl(&mut cef_dll_wrapper, &cef_dir, &out_dir)?;
|
||||
|
||||
let build_dir = cef_dll_wrapper.build().to_string_lossy().into_owned();
|
||||
|
||||
println!("cargo::rustc-link-search=native={build_dir}/build/libcef_dll_wrapper");
|
||||
println!("cargo::rustc-link-lib=static=libcef_dll_wrapper");
|
||||
@@ -227,6 +232,153 @@ fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cross-compiling to Windows from another host (e.g. with `cargo xwin`) uses `clang-cl`, which
|
||||
/// needs a few adjustments to CEF's MSVC-oriented CMake configuration. This is a no-op when the
|
||||
/// C++ compiler for the target isn't `clang-cl`.
|
||||
#[cfg(not(feature = "dox"))]
|
||||
fn configure_clang_cl(
|
||||
config: &mut cmake::Config,
|
||||
cef_dir: &std::path::Path,
|
||||
out_dir: &std::path::Path,
|
||||
) -> anyhow::Result<()> {
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
// Only the flags from the CXXFLAGS environment (e.g. cargo-xwin's `--target` and `/imsvc`
|
||||
// include paths) are needed here; the cmake crate takes care of everything else.
|
||||
let compiler = cc::Build::new()
|
||||
.cpp(true)
|
||||
.no_default_flags(true)
|
||||
.try_get_compiler()?;
|
||||
if !compiler.is_like_clang_cl() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// CMake de-duplicates compile options, which would drop the repeated `/imsvc` of options that
|
||||
// take their value as a separate argument (`/imsvc <dir>`), so join those into one token.
|
||||
let mut flags: Vec<String> = Vec::new();
|
||||
let mut args = compiler
|
||||
.args()
|
||||
.iter()
|
||||
.map(|arg| arg.to_string_lossy().into_owned());
|
||||
while let Some(arg) = args.next() {
|
||||
let flag = match arg.as_str() {
|
||||
"/imsvc" | "-imsvc" | "/I" | "-I" | "-isystem" | "/external:I" => match args.next() {
|
||||
Some(value) => format!("{arg}{value}"),
|
||||
None => arg,
|
||||
},
|
||||
_ => arg,
|
||||
};
|
||||
flags.push(flag);
|
||||
}
|
||||
|
||||
// clang-cl reports diagnostics that MSVC doesn't (`/MP` is unsupported, `/W4` enables extra
|
||||
// warnings), which CEF's `/WX` would turn into errors.
|
||||
for flag in [
|
||||
"-Wno-unused-command-line-argument",
|
||||
"-Wno-missing-field-initializers",
|
||||
"-Wno-undefined-var-template",
|
||||
"/WX-",
|
||||
] {
|
||||
if !flags.iter().any(|existing| existing == flag) {
|
||||
flags.push(flag.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// The Windows SDK relies on case-insensitive file names, which doesn't hold on other hosts:
|
||||
// e.g. the wrapper includes `Softpub.h` while the SDK ships `SoftPub.h`. Provide copies with
|
||||
// the expected casing in an extra include directory.
|
||||
let include_dirs: Vec<PathBuf> = flags
|
||||
.iter()
|
||||
.filter_map(|flag| {
|
||||
flag.strip_prefix("/imsvc")
|
||||
.or_else(|| flag.strip_prefix("-imsvc"))
|
||||
})
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
let shim_dir = out_dir.join("include-shim");
|
||||
fs::create_dir_all(&shim_dir)?;
|
||||
for header in system_includes(cef_dir)? {
|
||||
if include_dirs.iter().any(|dir| dir.join(&header).is_file()) {
|
||||
continue;
|
||||
}
|
||||
if let Some(found) = include_dirs
|
||||
.iter()
|
||||
.find_map(|dir| find_case_insensitive(dir, &header))
|
||||
{
|
||||
fs::copy(found, shim_dir.join(&header))?;
|
||||
}
|
||||
}
|
||||
flags.push(format!("/imsvc{}", shim_dir.display()));
|
||||
|
||||
// CEF clears CMAKE_CXX_FLAGS when using the Ninja generator (see cef_variables.cmake), which
|
||||
// would drop all of the above, so pass them through CEF's own flag lists instead. Those are
|
||||
// appended after CEF's flags, so they can also override `/WX`.
|
||||
let flags = flags.join(";");
|
||||
config
|
||||
.define("CEF_C_COMPILER_FLAGS", &flags)
|
||||
.define("CEF_CXX_COMPILER_FLAGS", &flags);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collects the file names used in `#include <...>` directives (without directory components) by
|
||||
/// the CEF headers and wrapper sources.
|
||||
#[cfg(not(feature = "dox"))]
|
||||
fn system_includes(
|
||||
cef_dir: &std::path::Path,
|
||||
) -> Result<std::collections::BTreeSet<String>, std::io::Error> {
|
||||
fn visit(
|
||||
dir: &std::path::Path,
|
||||
includes: &mut std::collections::BTreeSet<String>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
for entry in std::fs::read_dir(dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_dir() {
|
||||
visit(&path, includes)?;
|
||||
continue;
|
||||
}
|
||||
let Ok(source) = std::fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
for line in source.lines() {
|
||||
let Some(directive) = line.trim_start().strip_prefix("#include") else {
|
||||
continue;
|
||||
};
|
||||
let Some((name, _)) = directive
|
||||
.trim_start()
|
||||
.strip_prefix('<')
|
||||
.and_then(|rest| rest.split_once('>'))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !name.contains('/') {
|
||||
includes.insert(name.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut includes = std::collections::BTreeSet::new();
|
||||
for dir in ["include", "libcef_dll"] {
|
||||
visit(&cef_dir.join(dir), &mut includes)?;
|
||||
}
|
||||
Ok(includes)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "dox"))]
|
||||
fn find_case_insensitive(dir: &std::path::Path, name: &str) -> Option<std::path::PathBuf> {
|
||||
std::fs::read_dir(dir)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.find(|path| {
|
||||
path.file_name()
|
||||
.and_then(|file_name| file_name.to_str())
|
||||
.is_some_and(|file_name| file_name.eq_ignore_ascii_case(name))
|
||||
})
|
||||
}
|
||||
|
||||
/// `CEF_API_VERSION_LAST` from the distribution's generated
|
||||
/// `include/cef_api_versions.h`: the newest versioned (non-experimental) API
|
||||
/// it supports, written there as `#define CEF_API_VERSION_LAST
|
||||
|
||||
Reference in new issue
Block a user