From c167bfd50aa54e81f7f3c27f4570a689af087250 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Mon, 30 Mar 2026 01:43:46 +0200 Subject: [PATCH] Meta: Make Rust FFI headers reproducible Teach import_rust_crate() to track RustFFI.h as a real build output, and teach the relevant Rust build scripts to rerun when their FFI inputs change. Also keep a copy of RustFFI.h in Cargo's own OUT_DIR and restore the configured FFI output from that cached copy after cargo rustc runs. This fixes the case where Ninja knows the header is missing, reruns the custom command, and Cargo exits without rerunning build.rs because the crate itself is already up to date. When Cargo leaves multiple hashed build-script outputs behind, pick the newest root-output before restoring RustFFI.h so we do not copy a stale header after Rust-side API changes. Finally, track the remaining Rust-side inputs that could leave build artifacts stale: LibUnicode and LibJS now rerun build.rs when src/ changes, and the asmintgen rule now depends on Cargo.lock, the BytecodeDef path dependency, and newly added Rust source files. --- Libraries/LibJS/CMakeLists.txt | 12 +++++-- Libraries/LibJS/Rust/build.rs | 9 +++++- Libraries/LibRegex/CMakeLists.txt | 2 +- Libraries/LibRegex/Rust/build.rs | 11 +++++-- Libraries/LibUnicode/CMakeLists.txt | 2 +- Libraries/LibUnicode/Rust/build.rs | 9 +++++- Meta/CMake/rust_crate.cmake | 16 ++++++++-- Meta/CMake/sync_rust_ffi_header.cmake | 45 +++++++++++++++++++++++++++ 8 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 Meta/CMake/sync_rust_ffi_header.cmake diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index 7e24d394ff..5a3ee44cc0 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -295,7 +295,7 @@ endif() target_link_libraries(LibJS PUBLIC JSClangPlugin) if (ENABLE_RUST) - import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libjs_rust) + import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libjs_rust FFI_HEADER RustFFI.h) target_link_libraries(LibJS PRIVATE libjs_rust) target_compile_definitions(LibJS PRIVATE ENABLE_RUST) @@ -352,7 +352,15 @@ if (DEFINED ASMINT_ARCH) COMMENT "Generating asm struct offsets" ) - file(GLOB ASMINTGEN_SOURCES "${ASMINTGEN_DIR}/src/*.rs" "${ASMINTGEN_DIR}/Cargo.toml") + file(GLOB ASMINTGEN_RUST_SOURCES CONFIGURE_DEPENDS "${ASMINTGEN_DIR}/src/*.rs") + file(GLOB ASMINTGEN_BYTECODE_DEF_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/BytecodeDef/src/*.rs") + set(ASMINTGEN_SOURCES + "${ASMINTGEN_DIR}/Cargo.toml" + "${ASMINTGEN_DIR}/Cargo.lock" + "${CMAKE_CURRENT_SOURCE_DIR}/BytecodeDef/Cargo.toml" + ${ASMINTGEN_RUST_SOURCES} + ${ASMINTGEN_BYTECODE_DEF_SOURCES} + ) # NB: Clear linker-related env vars so cargo uses the default host # linker. The CI may set CC/CXX or CARGO_TARGET_*_LINKER to a # compiler that isn't available in all build configurations (e.g. diff --git a/Libraries/LibJS/Rust/build.rs b/Libraries/LibJS/Rust/build.rs index 34de367434..c0decf0c2b 100644 --- a/Libraries/LibJS/Rust/build.rs +++ b/Libraries/LibJS/Rust/build.rs @@ -671,6 +671,8 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed={}", def_path.display()); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=cbindgen.toml"); + println!("cargo:rerun-if-env-changed=FFI_OUTPUT_DIR"); + println!("cargo:rerun-if-changed=src"); let out_dir = PathBuf::from(env::var("OUT_DIR")?); @@ -684,7 +686,12 @@ fn main() -> Result<(), Box> { e => panic!("{e:?}"), }, |bindings| { - bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + let header_path = out_dir.join("RustFFI.h"); + bindings.write_to_file(&header_path); + + if ffi_out_dir != out_dir { + bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + } }, ); diff --git a/Libraries/LibRegex/CMakeLists.txt b/Libraries/LibRegex/CMakeLists.txt index 5ce08544ac..fea21a3ecc 100644 --- a/Libraries/LibRegex/CMakeLists.txt +++ b/Libraries/LibRegex/CMakeLists.txt @@ -10,6 +10,6 @@ set(SOURCES ladybird_lib(LibRegex regex EXPLICIT_SYMBOL_EXPORT) target_link_libraries(LibRegex PRIVATE LibUnicode) -import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libregex_rust) +import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libregex_rust FFI_HEADER RustFFI.h) target_link_libraries(LibRegex PRIVATE libregex_rust) target_compile_definitions(LibRegex PRIVATE ENABLE_RUST) diff --git a/Libraries/LibRegex/Rust/build.rs b/Libraries/LibRegex/Rust/build.rs index a3a809cb54..bbb11fef6c 100644 --- a/Libraries/LibRegex/Rust/build.rs +++ b/Libraries/LibRegex/Rust/build.rs @@ -10,14 +10,16 @@ use std::path::PathBuf; fn main() -> Result<(), Box> { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); + let out_dir = PathBuf::from(env::var("OUT_DIR")?); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=cbindgen.toml"); + println!("cargo:rerun-if-env-changed=FFI_OUTPUT_DIR"); println!("cargo:rerun-if-changed=src"); let ffi_out_dir = env::var("FFI_OUTPUT_DIR") .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(env::var("OUT_DIR").unwrap())); + .unwrap_or_else(|_| out_dir.clone()); cbindgen::generate(manifest_dir).map_or_else( |error| match error { @@ -25,7 +27,12 @@ fn main() -> Result<(), Box> { e => panic!("{e:?}"), }, |bindings| { - bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + let header_path = out_dir.join("RustFFI.h"); + bindings.write_to_file(&header_path); + + if ffi_out_dir != out_dir { + bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + } }, ); diff --git a/Libraries/LibUnicode/CMakeLists.txt b/Libraries/LibUnicode/CMakeLists.txt index 2c7a490eac..f29697350e 100644 --- a/Libraries/LibUnicode/CMakeLists.txt +++ b/Libraries/LibUnicode/CMakeLists.txt @@ -30,7 +30,7 @@ ladybird_lib(LibUnicode unicode) find_package(ICU 78.2 EXACT REQUIRED COMPONENTS data i18n uc) target_link_libraries(LibUnicode PRIVATE ICU::i18n ICU::uc ICU::data) -import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libunicode_rust) +import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libunicode_rust FFI_HEADER RustFFI.h) target_link_libraries(LibUnicode PRIVATE libunicode_rust) # FIXME: Add support for building LibGfx in sanitize diff --git a/Libraries/LibUnicode/Rust/build.rs b/Libraries/LibUnicode/Rust/build.rs index 84d817c064..bbb11fef6c 100644 --- a/Libraries/LibUnicode/Rust/build.rs +++ b/Libraries/LibUnicode/Rust/build.rs @@ -14,6 +14,8 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=cbindgen.toml"); + println!("cargo:rerun-if-env-changed=FFI_OUTPUT_DIR"); + println!("cargo:rerun-if-changed=src"); let ffi_out_dir = env::var("FFI_OUTPUT_DIR") .map(PathBuf::from) @@ -25,7 +27,12 @@ fn main() -> Result<(), Box> { e => panic!("{e:?}"), }, |bindings| { - bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + let header_path = out_dir.join("RustFFI.h"); + bindings.write_to_file(&header_path); + + if ffi_out_dir != out_dir { + bindings.write_to_file(ffi_out_dir.join("RustFFI.h")); + } }, ); diff --git a/Meta/CMake/rust_crate.cmake b/Meta/CMake/rust_crate.cmake index 4f0fe04c9e..f34576bc63 100644 --- a/Meta/CMake/rust_crate.cmake +++ b/Meta/CMake/rust_crate.cmake @@ -7,12 +7,15 @@ # https://github.com/corrosion-rs/corrosion/issues/206 # https://github.com/corrosion-rs/corrosion/issues/624 function(import_rust_crate) - cmake_parse_arguments(PARSE_ARGV 0 ARG "" "MANIFEST_PATH;CRATE_NAME;FFI_OUTPUT_DIR" "") + cmake_parse_arguments(PARSE_ARGV 0 ARG "" "MANIFEST_PATH;CRATE_NAME;FFI_OUTPUT_DIR;FFI_HEADER" "") set(ARG_MANIFEST_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${ARG_MANIFEST_PATH}") if (NOT ARG_FFI_OUTPUT_DIR) set(ARG_FFI_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}") endif() + if (ARG_FFI_HEADER) + set(ffi_output "${ARG_FFI_OUTPUT_DIR}/${ARG_FFI_HEADER}") + endif() # Find the workspace Cargo.lock to track as a dependency. get_filename_component(workspace_dir "${ARG_MANIFEST_PATH}" DIRECTORY) @@ -77,7 +80,7 @@ function(import_rust_crate) endif() add_custom_command( - OUTPUT "${output_lib}" + OUTPUT "${output_lib}" ${ffi_output} COMMAND ${CMAKE_COMMAND} -E env ${cargo_env} "${RUST_CARGO}" @@ -91,6 +94,13 @@ function(import_rust_crate) -- -Cdefault-linker-libraries=yes --emit=dep-info + COMMAND + ${CMAKE_COMMAND} + -DCARGO_BUILD_SCRIPT_DIR=${cargo_output_dir}/build + -DCRATE_NAME=${ARG_CRATE_NAME} + -DFFI_HEADER=${ARG_FFI_HEADER} + -DFFI_OUTPUT_DIR=${ARG_FFI_OUTPUT_DIR} + -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/sync_rust_ffi_header.cmake" DEPENDS "${ARG_MANIFEST_PATH}" "${workspace_dir}/Cargo.lock" "${workspace_dir}/Cargo.toml" DEPFILE "${depfile}" @@ -99,7 +109,7 @@ function(import_rust_crate) COMMAND_EXPAND_LISTS ) - add_custom_target(${ARG_CRATE_NAME}-build DEPENDS "${output_lib}") + add_custom_target(${ARG_CRATE_NAME}-build DEPENDS "${output_lib}" ${ffi_output}) add_library(${ARG_CRATE_NAME} STATIC IMPORTED GLOBAL) set_target_properties(${ARG_CRATE_NAME} PROPERTIES diff --git a/Meta/CMake/sync_rust_ffi_header.cmake b/Meta/CMake/sync_rust_ffi_header.cmake new file mode 100644 index 0000000000..43a5f894fc --- /dev/null +++ b/Meta/CMake/sync_rust_ffi_header.cmake @@ -0,0 +1,45 @@ +#[[ + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause +]] + +if (NOT DEFINED CRATE_NAME OR NOT DEFINED CARGO_BUILD_SCRIPT_DIR OR NOT DEFINED FFI_HEADER OR NOT DEFINED FFI_OUTPUT_DIR) + message(FATAL_ERROR "sync_rust_ffi_header.cmake requires CRATE_NAME, CARGO_BUILD_SCRIPT_DIR, FFI_HEADER, and FFI_OUTPUT_DIR") +endif() + +if (FFI_HEADER STREQUAL "") + return() +endif() + +file(GLOB root_output_files "${CARGO_BUILD_SCRIPT_DIR}/${CRATE_NAME}-*/root-output") + +set(latest_source_header "") +set(latest_root_output_timestamp "") + +foreach(root_output_file IN LISTS root_output_files) + file(READ "${root_output_file}" out_dir) + string(STRIP "${out_dir}" out_dir) + + set(source_header "${out_dir}/${FFI_HEADER}") + if (NOT EXISTS "${source_header}") + continue() + endif() + + file(TIMESTAMP "${root_output_file}" root_output_timestamp "%s" UTC) + if (latest_source_header STREQUAL "" OR root_output_timestamp GREATER latest_root_output_timestamp) + set(latest_source_header "${source_header}") + set(latest_root_output_timestamp "${root_output_timestamp}") + endif() +endforeach() + +if (NOT latest_source_header STREQUAL "") + file(MAKE_DIRECTORY "${FFI_OUTPUT_DIR}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${latest_source_header}" "${FFI_OUTPUT_DIR}/${FFI_HEADER}" + COMMAND_ERROR_IS_FATAL ANY + ) + return() +endif() + +message(FATAL_ERROR "Failed to find ${FFI_HEADER} in Cargo build output for ${CRATE_NAME}")