Add --depfile option

Add an option to output a depfile for outside build-systems to learn
the source file dependencies of the bindings.
This can be used by 3rd party build system integrations to only rerun
bindgen when necessary.

Testing is done via CMake integration tests, since CMake
is a 3rd party buildsystem which supports depfiles.
This commit is contained in:
Jonathan Schwender
2023-03-08 16:26:24 +01:00
committed by Emilio Cobos Álvarez
parent cb42a00ab6
commit 25132a3690
37 changed files with 674 additions and 9 deletions
+109
View File
@@ -0,0 +1,109 @@
use std::fs::read_to_string;
use std::path::PathBuf;
use std::process::Command;
static CBINDGEN_PATH: &str = env!("CARGO_BIN_EXE_cbindgen");
fn test_project(project_path: &str) {
let mut cmake_cmd = Command::new("cmake");
cmake_cmd.arg("--version");
cmake_cmd
.output()
.expect("CMake --version failed - Is CMake installed?");
let mut cmake_configure = Command::new("cmake");
let build_dir = PathBuf::from(project_path).join("build");
if build_dir.exists() {
std::fs::remove_dir_all(&build_dir).expect("Failed to remove old build directory");
}
let project_dir = PathBuf::from(project_path);
let cbindgen_define = format!("-DCBINDGEN_PATH={}", CBINDGEN_PATH);
cmake_configure
.arg("-S")
.arg(project_path)
.arg("-B")
.arg(&build_dir)
.arg(cbindgen_define);
let output = cmake_configure.output().expect("Failed to execute process");
let stdout_str = String::from_utf8(output.stdout).unwrap();
let stderr_str = String::from_utf8(output.stderr).unwrap();
assert!(
output.status.success(),
"Configuring test project failed: stdout: `{}`, stderr: `{}`",
stdout_str,
stderr_str
);
let depfile_path = build_dir.join("depfile.d");
assert!(
!depfile_path.exists(),
"depfile should not exist before building"
);
// Do the clean first build
let mut cmake_build = Command::new("cmake");
cmake_build.arg("--build").arg(&build_dir);
let output = cmake_build.output().expect("Failed to execute process");
assert!(output.status.success(), "Building test project failed");
let out_str = String::from_utf8(output.stdout).unwrap();
assert!(
out_str.contains("Running cbindgen"),
"cbindgen rule did not run. Output: {}",
out_str
);
assert!(
depfile_path.exists(),
"depfile does not exist after building"
);
let expected_dependencies_filepath = PathBuf::from(project_path)
.join("expectations")
.join("dependencies");
assert!(
expected_dependencies_filepath.exists(),
"Test did not define expected dependencies. Please read the Readme.md"
);
let expected_deps =
read_to_string(expected_dependencies_filepath).expect("Failed to read dependencies");
let depinfo = read_to_string(depfile_path).expect("Failed to read dependencies");
// Assumes a single rule in the file - all deps are listed to the rhs of the `:`.
let actual_deps = depinfo.split(':').collect::<Vec<_>>()[1];
// Strip the line breaks.
let actual_deps = actual_deps.replace("\\\n", " ");
// I don't want to deal with supporting escaped whitespace when splitting at whitespace,
// so the tests don't support being run in a directory containing whitespace.
assert!(
!actual_deps.contains("\\ "),
"The tests directory may not contain any whitespace"
);
let dep_list: Vec<&str> = actual_deps.split_ascii_whitespace().collect();
let expected_dep_list: Vec<String> = expected_deps
.lines()
.map(|dep| project_dir.join(dep).to_str().unwrap().to_string())
.collect();
assert_eq!(dep_list, expected_dep_list);
let output = cmake_build.output().expect("Failed to execute process");
assert!(output.status.success(), "Building test project failed");
let out_str = String::from_utf8(output.stdout).unwrap();
assert!(
!out_str.contains("Running cbindgen"),
"cbindgen rule ran on second build"
);
std::fs::remove_dir_all(build_dir).expect("Failed to remove old build directory");
()
}
macro_rules! test_file {
($test_function_name:ident, $name:expr, $file:tt) => {
#[test]
fn $test_function_name() {
test_project($file);
}
};
}
// This file is generated by build.rs
include!(concat!(env!("OUT_DIR"), "/depfile_tests.rs"));
+11
View File
@@ -0,0 +1,11 @@
This a folder containing tests for `--depfile` parameter.
Each test is in a subfolder and defines a minimum CMake project,
which uses cbindgen to generate Rust bindings and the `--depfile`
parameter to determine when to regenerate.
The outer test can the build the project, assert that rebuilding does not regenerate the
bindings, and then assert that touching the files involved does trigger rebuilding.
The test project must contain an `expectations` folder, containing a file `dependencies`.
This `dependencies` should list all files that should be listed as dependencies in the generated
depfile. The paths should be relative to the project folder (i.e. to the folder containing
`expectations`).
+27
View File
@@ -0,0 +1,27 @@
# Common code used across the different tests
if(NOT DEFINED CBINDGEN_PATH)
message(FATAL_ERROR "Path to cbindgen not specified")
endif()
# Promote to cache
set(CBINDGEN_PATH "${CBINDGEN_PATH}" CACHE INTERNAL "")
function(add_cbindgen_command custom_target_name header_destination)
# Place the depfile always at the same location, so the outer test framework can locate the file easily
set(depfile_destination "${CMAKE_BINARY_DIR}/depfile.d")
add_custom_command(
OUTPUT
"${header_destination}" "${depfile_destination}"
COMMAND
"${CBINDGEN_PATH}"
--output "${header_destination}"
--depfile "${depfile_destination}"
${ARGN}
DEPFILE "${depfile_destination}"
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
COMMENT "Running cbindgen"
COMMAND_EXPAND_LISTS
)
add_custom_target("${custom_target_name}" ALL DEPENDS "${header_destination}")
endfunction()
+1
View File
@@ -0,0 +1 @@
build
+12
View File
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.21.0)
project(depfile_test
LANGUAGES C
DESCRIPTION "A CMake Project to test the --depfile output from cbindgen"
)
include(../cbindgen_test.cmake)
add_cbindgen_command(gen_bindings
"${CMAKE_CURRENT_BINARY_DIR}/single_crate.h"
)
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "single_crate"
version = "0.1.0"
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "single_crate"
version = "0.1.0"
authors = ["cbindgen"]
[features]
cbindgen = []
@@ -0,0 +1,3 @@
src/alias/mod.rs
src/annotation.rs
src/lib.rs
@@ -0,0 +1,32 @@
#[repr(C)]
struct Dep {
a: i32,
b: f32,
}
#[repr(C)]
struct Foo<X> {
a: X,
b: X,
c: Dep,
}
#[repr(u32)]
enum Status {
Ok,
Err,
}
type IntFoo = Foo<i32>;
type DoubleFoo = Foo<f64>;
type Unit = i32;
type SpecialStatus = Status;
#[no_mangle]
pub extern "C" fn root(
x: IntFoo,
y: DoubleFoo,
z: Unit,
w: SpecialStatus
) { }
@@ -0,0 +1,43 @@
/// cbindgen:derive-lt=true
/// cbindgen:derive-lte=true
/// cbindgen:derive-constructor=true
/// cbindgen:rename-all=GeckoCase
#[repr(C)]
struct A(i32);
/// cbindgen:field-names=[x, y]
#[repr(C)]
struct B(i32, f32);
/// cbindgen:trailing-values=[Z, W]
#[repr(u32)]
enum C {
X = 2,
Y,
}
/// cbindgen:derive-helper-methods=true
#[repr(u8)]
enum F {
Foo(i16),
Bar { x: u8, y: i16 },
Baz
}
/// cbindgen:derive-helper-methods
#[repr(C, u8)]
enum H {
Hello(i16),
There { x: u8, y: i16 },
Everyone
}
#[no_mangle]
pub extern "C" fn root(
x: A,
y: B,
z: C,
f: F,
h: H,
) { }
+2
View File
@@ -0,0 +1,2 @@
mod alias;
mod annotation;
@@ -0,0 +1 @@
build
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.21.0)
project(depfile_test
LANGUAGES C
DESCRIPTION "A CMake Project to test the --depfile output from cbindgen"
)
include(../cbindgen_test.cmake)
add_cbindgen_command(gen_bindings
"${CMAKE_CURRENT_BINARY_DIR}/single_crate.h"
--config "${CMAKE_CURRENT_SOURCE_DIR}/config.toml"
)
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "single_crate"
version = "0.1.0"
@@ -0,0 +1,7 @@
[package]
name = "single_crate"
version = "0.1.0"
authors = ["cbindgen"]
[features]
cbindgen = []
@@ -0,0 +1,4 @@
config.toml
src/alias/mod.rs
src/annotation.rs
src/lib.rs
@@ -0,0 +1,32 @@
#[repr(C)]
struct Dep {
a: i32,
b: f32,
}
#[repr(C)]
struct Foo<X> {
a: X,
b: X,
c: Dep,
}
#[repr(u32)]
enum Status {
Ok,
Err,
}
type IntFoo = Foo<i32>;
type DoubleFoo = Foo<f64>;
type Unit = i32;
type SpecialStatus = Status;
#[no_mangle]
pub extern "C" fn root(
x: IntFoo,
y: DoubleFoo,
z: Unit,
w: SpecialStatus,
) {}
@@ -0,0 +1,43 @@
/// cbindgen:derive-lt=true
/// cbindgen:derive-lte=true
/// cbindgen:derive-constructor=true
/// cbindgen:rename-all=GeckoCase
#[repr(C)]
struct A(i32);
/// cbindgen:field-names=[x, y]
#[repr(C)]
struct B(i32, f32);
/// cbindgen:trailing-values=[Z, W]
#[repr(u32)]
enum C {
X = 2,
Y,
}
/// cbindgen:derive-helper-methods=true
#[repr(u8)]
enum F {
Foo(i16),
Bar { x: u8, y: i16 },
Baz,
}
/// cbindgen:derive-helper-methods
#[repr(C, u8)]
enum H {
Hello(i16),
There { x: u8, y: i16 },
Everyone,
}
#[no_mangle]
pub extern "C" fn root(
x: A,
y: B,
z: C,
f: F,
h: H,
) {}
@@ -0,0 +1,2 @@
mod alias;
mod annotation;
@@ -0,0 +1 @@
build
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.21.0)
project(depfile_test
LANGUAGES C
DESCRIPTION "A CMake Project to test the --depfile output from cbindgen"
)
include(../cbindgen_test.cmake)
add_cbindgen_command(gen_bindings
"${CMAKE_CURRENT_BINARY_DIR}/single_crate.h"
)
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "single_crate"
version = "0.1.0"
@@ -0,0 +1,7 @@
[package]
name = "single_crate"
version = "0.1.0"
authors = ["cbindgen"]
[features]
cbindgen = []
@@ -0,0 +1,4 @@
cbindgen.toml
src/alias/mod.rs
src/annotation.rs
src/lib.rs
@@ -0,0 +1,32 @@
#[repr(C)]
struct Dep {
a: i32,
b: f32,
}
#[repr(C)]
struct Foo<X> {
a: X,
b: X,
c: Dep,
}
#[repr(u32)]
enum Status {
Ok,
Err,
}
type IntFoo = Foo<i32>;
type DoubleFoo = Foo<f64>;
type Unit = i32;
type SpecialStatus = Status;
#[no_mangle]
pub extern "C" fn root(
x: IntFoo,
y: DoubleFoo,
z: Unit,
w: SpecialStatus,
) {}
@@ -0,0 +1,43 @@
/// cbindgen:derive-lt=true
/// cbindgen:derive-lte=true
/// cbindgen:derive-constructor=true
/// cbindgen:rename-all=GeckoCase
#[repr(C)]
struct A(i32);
/// cbindgen:field-names=[x, y]
#[repr(C)]
struct B(i32, f32);
/// cbindgen:trailing-values=[Z, W]
#[repr(u32)]
enum C {
X = 2,
Y,
}
/// cbindgen:derive-helper-methods=true
#[repr(u8)]
enum F {
Foo(i16),
Bar { x: u8, y: i16 },
Baz,
}
/// cbindgen:derive-helper-methods
#[repr(C, u8)]
enum H {
Hello(i16),
There { x: u8, y: i16 },
Everyone,
}
#[no_mangle]
pub extern "C" fn root(
x: A,
y: B,
z: C,
f: F,
h: H,
) {}
@@ -0,0 +1,2 @@
mod alias;
mod annotation;
+79 -4
View File
@@ -2,6 +2,8 @@ extern crate cbindgen;
use cbindgen::*;
use std::collections::HashSet;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::Command;
use std::{env, fs, str};
@@ -19,13 +21,29 @@ fn style_str(style: Style) -> &'static str {
fn run_cbindgen(
path: &Path,
output: &Path,
output: Option<&Path>,
language: Language,
cpp_compat: bool,
style: Option<Style>,
) -> Vec<u8> {
generate_depfile: bool,
) -> (Vec<u8>, Option<String>) {
assert!(
!(output.is_none() && generate_depfile),
"generating a depfile requires outputting to a path"
);
let program = Path::new(CBINDGEN_PATH);
let mut command = Command::new(program);
if let Some(output) = output {
command.arg("--output").arg(output);
}
let cbindgen_depfile = if generate_depfile {
let depfile = tempfile::NamedTempFile::new().unwrap();
command.arg("--depfile").arg(depfile.path());
Some(depfile)
} else {
None
};
match language {
Language::Cxx => {}
Language::C => {
@@ -53,13 +71,37 @@ fn run_cbindgen(
println!("Running: {:?}", command);
let cbindgen_output = command.output().expect("failed to execute process");
assert!(
cbindgen_output.status.success(),
"cbindgen failed: {:?} with error: {}",
output,
str::from_utf8(&cbindgen_output.stderr).unwrap_or_default()
);
cbindgen_output.stdout
let bindings = if let Some(output_path) = output {
let mut bindings = Vec::new();
// Ignore errors here, we have assertions on the expected output later.
let _ = File::open(output_path).map(|mut file| {
let _ = file.read_to_end(&mut bindings);
});
bindings
} else {
cbindgen_output.stdout
};
let depfile_contents = if let Some(mut depfile) = cbindgen_depfile {
let mut raw = Vec::new();
depfile.read_to_end(&mut raw).unwrap();
Some(
str::from_utf8(raw.as_slice())
.expect("Invalid encoding encountered in depfile")
.into(),
)
} else {
None
};
(bindings, depfile_contents)
}
fn compile(
@@ -183,7 +225,40 @@ fn run_compile_test(
generated_file.push(source_file);
let cbindgen_output = run_cbindgen(path, &generated_file, language, cpp_compat, style);
let (output_file, generate_depfile) = if env::var_os("CBINDGEN_TEST_VERIFY").is_some() {
(None, false)
} else {
(
Some(generated_file.as_path()),
// --depfile does not work in combination with expanding yet, so we blacklist expanding tests.
!(name.contains("expand") || name.contains("bitfield")),
)
};
let (cbindgen_output, depfile_contents) = run_cbindgen(
path,
output_file,
language,
cpp_compat,
style,
generate_depfile,
);
if generate_depfile {
let depfile = depfile_contents.expect("No depfile generated");
assert!(depfile.len() > 0);
let mut rules = depfile.split(':');
let target = rules.next().expect("No target found");
assert_eq!(target, generated_file.as_os_str().to_str().unwrap());
let sources = rules.next().unwrap();
// All the tests here only have one sourcefile.
assert!(
sources.contains(path.to_str().unwrap()),
"Path: {:?}, Depfile contents: {}",
path,
depfile
);
assert_eq!(rules.count(), 0, "More than 1 rule in the depfile");
}
if cbindgen_outputs.contains(&cbindgen_output) {
// We already generated an identical file previously.