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:
committed by
Emilio Cobos Álvarez
parent
cb42a00ab6
commit
25132a3690
@@ -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"));
|
||||
@@ -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`).
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
)
|
||||
Generated
+7
@@ -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,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,
|
||||
) { }
|
||||
|
||||
@@ -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
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user