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:
parent
cb42a00ab6
commit
25132a3690
41
build.rs
41
build.rs
@ -51,6 +51,47 @@ fn generate_tests() {
|
|||||||
dst.flush().unwrap();
|
dst.flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_depfile_tests() {
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
let mut dst = File::create(Path::new(&out_dir).join("depfile_tests.rs")).unwrap();
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let tests_dir = manifest_dir.join("tests").join("depfile");
|
||||||
|
let tests = fs::read_dir(&tests_dir).unwrap();
|
||||||
|
|
||||||
|
let entries = tests.map(|t| t.expect("Couldn't read test file"));
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed={}", tests_dir.display());
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
if entry.file_type().unwrap().is_file() {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let path_segment = entry.file_name().to_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
let identifier = path_segment
|
||||||
|
.replace(|c| !char::is_alphanumeric(c), "_")
|
||||||
|
.replace("__", "_");
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
dst,
|
||||||
|
"test_file!(test_depfile_{}, {:?}, {:?});",
|
||||||
|
identifier,
|
||||||
|
path_segment,
|
||||||
|
entry.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
generate_tests();
|
generate_tests();
|
||||||
|
generate_depfile_tests();
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ pub struct Bindings {
|
|||||||
constants: Vec<Constant>,
|
constants: Vec<Constant>,
|
||||||
items: Vec<ItemContainer>,
|
items: Vec<ItemContainer>,
|
||||||
functions: Vec<Function>,
|
functions: Vec<Function>,
|
||||||
|
source_files: Vec<path::PathBuf>,
|
||||||
/// Bindings are generated by a recursive call to cbindgen
|
/// Bindings are generated by a recursive call to cbindgen
|
||||||
/// and shouldn't do anything when written anywhere.
|
/// and shouldn't do anything when written anywhere.
|
||||||
noop: bool,
|
noop: bool,
|
||||||
@ -50,6 +51,7 @@ impl Bindings {
|
|||||||
globals: Vec<Static>,
|
globals: Vec<Static>,
|
||||||
items: Vec<ItemContainer>,
|
items: Vec<ItemContainer>,
|
||||||
functions: Vec<Function>,
|
functions: Vec<Function>,
|
||||||
|
source_files: Vec<path::PathBuf>,
|
||||||
noop: bool,
|
noop: bool,
|
||||||
) -> Bindings {
|
) -> Bindings {
|
||||||
Bindings {
|
Bindings {
|
||||||
@ -61,6 +63,7 @@ impl Bindings {
|
|||||||
constants,
|
constants,
|
||||||
items,
|
items,
|
||||||
functions,
|
functions,
|
||||||
|
source_files,
|
||||||
noop,
|
noop,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,6 +131,47 @@ impl Bindings {
|
|||||||
fields
|
fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_depfile<P: AsRef<path::Path>>(&self, header_path: P, depfile_path: P) {
|
||||||
|
if let Some(dir) = depfile_path.as_ref().parent() {
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir_all(dir).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let canon_header_path = header_path.as_ref().canonicalize().unwrap();
|
||||||
|
let mut canon_source_files: Vec<_> = self
|
||||||
|
.source_files
|
||||||
|
.iter()
|
||||||
|
.chain(self.config.config_path.as_ref().into_iter())
|
||||||
|
.map(|p| p.canonicalize().unwrap())
|
||||||
|
.collect();
|
||||||
|
// Sorting makes testing easier by ensuring the output is ordered.
|
||||||
|
canon_source_files.sort_unstable();
|
||||||
|
|
||||||
|
// When writing the depfile we must escape whitespace in paths to avoid it being interpreted
|
||||||
|
// as a seperator.
|
||||||
|
// It is not clear how to otherwise _correctly_ replace whitespace in a non-unicode
|
||||||
|
// compliant slice, without knowing the encoding, so we lossy convert such cases,
|
||||||
|
// to avoid panics.
|
||||||
|
let mut depfile = File::create(depfile_path).unwrap();
|
||||||
|
write!(
|
||||||
|
&mut depfile,
|
||||||
|
"{}:",
|
||||||
|
canon_header_path.to_string_lossy().replace(' ', "\\ ")
|
||||||
|
)
|
||||||
|
.expect("Writing header name to depfile failed");
|
||||||
|
canon_source_files.into_iter().for_each(|source_file| {
|
||||||
|
// Add line-continue and line-break and then indent with 4 spaces.
|
||||||
|
// This makes the output more human-readable.
|
||||||
|
depfile.write_all(b" \\\n ").unwrap();
|
||||||
|
let escaped_path = source_file.to_string_lossy().replace(' ', "\\ ");
|
||||||
|
depfile.write_all(escaped_path.as_bytes()).unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
writeln!(&mut depfile).unwrap();
|
||||||
|
|
||||||
|
depfile.flush().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_to_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
pub fn write_to_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
||||||
if self.noop {
|
if self.noop {
|
||||||
return false;
|
return false;
|
||||||
|
@ -359,6 +359,7 @@ impl Builder {
|
|||||||
Default::default(),
|
Default::default(),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -391,6 +392,8 @@ impl Builder {
|
|||||||
result.extend_with(&parser::parse_lib(cargo, &self.config)?);
|
result.extend_with(&parser::parse_lib(cargo, &self.config)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.source_files.extend_from_slice(self.srcs.as_slice());
|
||||||
|
|
||||||
Library::new(
|
Library::new(
|
||||||
self.config,
|
self.config,
|
||||||
result.constants,
|
result.constants,
|
||||||
@ -401,6 +404,7 @@ impl Builder {
|
|||||||
result.opaque_items,
|
result.opaque_items,
|
||||||
result.typedefs,
|
result.typedefs,
|
||||||
result.functions,
|
result.functions,
|
||||||
|
result.source_files,
|
||||||
)
|
)
|
||||||
.generate()
|
.generate()
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::default::Default;
|
use std::default::Default;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fmt, fs, path::Path as StdPath};
|
use std::{fmt, fs, path::Path as StdPath, path::PathBuf as StdPathBuf};
|
||||||
|
|
||||||
use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer};
|
use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer};
|
||||||
use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
|
use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||||
@ -1003,6 +1003,8 @@ pub struct Config {
|
|||||||
pub only_target_dependencies: bool,
|
pub only_target_dependencies: bool,
|
||||||
/// Configuration options specific to Cython.
|
/// Configuration options specific to Cython.
|
||||||
pub cython: CythonConfig,
|
pub cython: CythonConfig,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) config_path: Option<StdPathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -1045,6 +1047,7 @@ impl Default for Config {
|
|||||||
pointer: PtrConfig::default(),
|
pointer: PtrConfig::default(),
|
||||||
only_target_dependencies: false,
|
only_target_dependencies: false,
|
||||||
cython: CythonConfig::default(),
|
cython: CythonConfig::default(),
|
||||||
|
config_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1086,10 +1089,10 @@ impl Config {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match toml::from_str::<Config>(&config_text) {
|
let mut config = toml::from_str::<Config>(&config_text)
|
||||||
Ok(x) => Ok(x),
|
.map_err(|e| format!("Couldn't parse config file: {}.", e))?;
|
||||||
Err(e) => Err(format!("Couldn't parse config file: {}.", e)),
|
config.config_path = Some(StdPathBuf::from(file_name.as_ref()));
|
||||||
}
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_root_or_default<P: AsRef<StdPath>>(root: P) -> Config {
|
pub fn from_root_or_default<P: AsRef<StdPath>>(root: P) -> Config {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::bindgen::bindings::Bindings;
|
use crate::bindgen::bindings::Bindings;
|
||||||
use crate::bindgen::config::{Config, Language, SortKey};
|
use crate::bindgen::config::{Config, Language, SortKey};
|
||||||
@ -25,6 +26,7 @@ pub struct Library {
|
|||||||
opaque_items: ItemMap<OpaqueItem>,
|
opaque_items: ItemMap<OpaqueItem>,
|
||||||
typedefs: ItemMap<Typedef>,
|
typedefs: ItemMap<Typedef>,
|
||||||
functions: Vec<Function>,
|
functions: Vec<Function>,
|
||||||
|
source_files: Vec<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Library {
|
impl Library {
|
||||||
@ -39,6 +41,7 @@ impl Library {
|
|||||||
opaque_items: ItemMap<OpaqueItem>,
|
opaque_items: ItemMap<OpaqueItem>,
|
||||||
typedefs: ItemMap<Typedef>,
|
typedefs: ItemMap<Typedef>,
|
||||||
functions: Vec<Function>,
|
functions: Vec<Function>,
|
||||||
|
source_files: Vec<PathBuf>,
|
||||||
) -> Library {
|
) -> Library {
|
||||||
Library {
|
Library {
|
||||||
config,
|
config,
|
||||||
@ -50,6 +53,7 @@ impl Library {
|
|||||||
opaque_items,
|
opaque_items,
|
||||||
typedefs,
|
typedefs,
|
||||||
functions,
|
functions,
|
||||||
|
source_files,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +139,7 @@ impl Library {
|
|||||||
globals,
|
globals,
|
||||||
items,
|
items,
|
||||||
functions,
|
functions,
|
||||||
|
self.source_files,
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ pub fn parse_src(src_file: &FilePath, config: &Config) -> ParseResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
context.parse_mod(&pkg_ref, src_file, 0)?;
|
context.parse_mod(&pkg_ref, src_file, 0)?;
|
||||||
|
context.out.source_files = context.cache_src.keys().map(|k| k.to_owned()).collect();
|
||||||
Ok(context.out)
|
Ok(context.out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ pub(crate) fn parse_lib(lib: Cargo, config: &Config) -> ParseResult {
|
|||||||
|
|
||||||
let binding_crate = context.lib.as_ref().unwrap().binding_crate_ref();
|
let binding_crate = context.lib.as_ref().unwrap().binding_crate_ref();
|
||||||
context.parse_crate(&binding_crate)?;
|
context.parse_crate(&binding_crate)?;
|
||||||
|
context.out.source_files = context.cache_src.keys().map(|k| k.to_owned()).collect();
|
||||||
Ok(context.out)
|
Ok(context.out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,6 +408,7 @@ pub struct Parse {
|
|||||||
pub opaque_items: ItemMap<OpaqueItem>,
|
pub opaque_items: ItemMap<OpaqueItem>,
|
||||||
pub typedefs: ItemMap<Typedef>,
|
pub typedefs: ItemMap<Typedef>,
|
||||||
pub functions: Vec<Function>,
|
pub functions: Vec<Function>,
|
||||||
|
pub source_files: Vec<FilePathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse {
|
impl Parse {
|
||||||
@ -419,6 +422,7 @@ impl Parse {
|
|||||||
opaque_items: ItemMap::default(),
|
opaque_items: ItemMap::default(),
|
||||||
typedefs: ItemMap::default(),
|
typedefs: ItemMap::default(),
|
||||||
functions: Vec::new(),
|
functions: Vec::new(),
|
||||||
|
source_files: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -466,6 +470,7 @@ impl Parse {
|
|||||||
self.opaque_items.extend_with(&other.opaque_items);
|
self.opaque_items.extend_with(&other.opaque_items);
|
||||||
self.typedefs.extend_with(&other.typedefs);
|
self.typedefs.extend_with(&other.typedefs);
|
||||||
self.functions.extend_from_slice(&other.functions);
|
self.functions.extend_from_slice(&other.functions);
|
||||||
|
self.source_files.extend_from_slice(&other.source_files);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_syn_crate_mod<'a>(
|
fn load_syn_crate_mod<'a>(
|
||||||
|
17
src/main.rs
17
src/main.rs
@ -262,6 +262,20 @@ fn main() {
|
|||||||
.help("Report errors only (overrides verbosity options).")
|
.help("Report errors only (overrides verbosity options).")
|
||||||
.required(false),
|
.required(false),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("depfile")
|
||||||
|
.value_name("PATH")
|
||||||
|
.long("depfile")
|
||||||
|
.takes_value(true)
|
||||||
|
.min_values(1)
|
||||||
|
.max_values(1)
|
||||||
|
.required(false)
|
||||||
|
.help("Generate a depfile at the given Path listing the source files \
|
||||||
|
cbindgen traversed when generating the bindings. Useful when \
|
||||||
|
integrating cbindgen into 3rd party build-systems. \
|
||||||
|
This option is ignored if `--out` is missing."
|
||||||
|
)
|
||||||
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
if !matches.is_present("out") && matches.is_present("verify") {
|
if !matches.is_present("out") && matches.is_present("verify") {
|
||||||
@ -306,6 +320,9 @@ fn main() {
|
|||||||
error!("Bindings changed: {}", file);
|
error!("Bindings changed: {}", file);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
|
if let Some(depfile) = matches.value_of("depfile") {
|
||||||
|
bindings.generate_depfile(file, depfile)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
bindings.write(io::stdout());
|
bindings.write(io::stdout());
|
||||||
|
109
tests/depfile.rs
Normal file
109
tests/depfile.rs
Normal 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
tests/depfile/Readme.md
Normal file
11
tests/depfile/Readme.md
Normal 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
tests/depfile/cbindgen_test.cmake
Normal file
27
tests/depfile/cbindgen_test.cmake
Normal 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
tests/depfile/single_crate/.gitignore
vendored
Normal file
1
tests/depfile/single_crate/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
12
tests/depfile/single_crate/CMakeLists.txt
Normal file
12
tests/depfile/single_crate/CMakeLists.txt
Normal 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
tests/depfile/single_crate/Cargo.lock
generated
Normal file
7
tests/depfile/single_crate/Cargo.lock
generated
Normal 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
tests/depfile/single_crate/Cargo.toml
Normal file
7
tests/depfile/single_crate/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "single_crate"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["cbindgen"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cbindgen = []
|
3
tests/depfile/single_crate/expectations/dependencies
Normal file
3
tests/depfile/single_crate/expectations/dependencies
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
src/alias/mod.rs
|
||||||
|
src/annotation.rs
|
||||||
|
src/lib.rs
|
32
tests/depfile/single_crate/src/alias/mod.rs
Normal file
32
tests/depfile/single_crate/src/alias/mod.rs
Normal file
@ -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
|
||||||
|
) { }
|
43
tests/depfile/single_crate/src/annotation.rs
Normal file
43
tests/depfile/single_crate/src/annotation.rs
Normal file
@ -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
tests/depfile/single_crate/src/lib.rs
Normal file
2
tests/depfile/single_crate/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod alias;
|
||||||
|
mod annotation;
|
1
tests/depfile/single_crate_config/.gitignore
vendored
Normal file
1
tests/depfile/single_crate_config/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
13
tests/depfile/single_crate_config/CMakeLists.txt
Normal file
13
tests/depfile/single_crate_config/CMakeLists.txt
Normal file
@ -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
tests/depfile/single_crate_config/Cargo.lock
generated
Normal file
7
tests/depfile/single_crate_config/Cargo.lock
generated
Normal 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
tests/depfile/single_crate_config/Cargo.toml
Normal file
7
tests/depfile/single_crate_config/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "single_crate"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["cbindgen"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
cbindgen = []
|
0
tests/depfile/single_crate_config/config.toml
Normal file
0
tests/depfile/single_crate_config/config.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
config.toml
|
||||||
|
src/alias/mod.rs
|
||||||
|
src/annotation.rs
|
||||||
|
src/lib.rs
|
32
tests/depfile/single_crate_config/src/alias/mod.rs
Normal file
32
tests/depfile/single_crate_config/src/alias/mod.rs
Normal file
@ -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,
|
||||||
|
) {}
|
43
tests/depfile/single_crate_config/src/annotation.rs
Normal file
43
tests/depfile/single_crate_config/src/annotation.rs
Normal file
@ -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
tests/depfile/single_crate_config/src/lib.rs
Normal file
2
tests/depfile/single_crate_config/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod alias;
|
||||||
|
mod annotation;
|
1
tests/depfile/single_crate_default_config/.gitignore
vendored
Normal file
1
tests/depfile/single_crate_default_config/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
build
|
12
tests/depfile/single_crate_default_config/CMakeLists.txt
Normal file
12
tests/depfile/single_crate_default_config/CMakeLists.txt
Normal 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
tests/depfile/single_crate_default_config/Cargo.lock
generated
Normal file
7
tests/depfile/single_crate_default_config/Cargo.lock
generated
Normal 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
tests/depfile/single_crate_default_config/Cargo.toml
Normal file
7
tests/depfile/single_crate_default_config/Cargo.toml
Normal file
@ -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
|
32
tests/depfile/single_crate_default_config/src/alias/mod.rs
Normal file
32
tests/depfile/single_crate_default_config/src/alias/mod.rs
Normal file
@ -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,
|
||||||
|
) {}
|
43
tests/depfile/single_crate_default_config/src/annotation.rs
Normal file
43
tests/depfile/single_crate_default_config/src/annotation.rs
Normal file
@ -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
tests/depfile/single_crate_default_config/src/lib.rs
Normal file
2
tests/depfile/single_crate_default_config/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
mod alias;
|
||||||
|
mod annotation;
|
@ -2,6 +2,8 @@ extern crate cbindgen;
|
|||||||
|
|
||||||
use cbindgen::*;
|
use cbindgen::*;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::{env, fs, str};
|
use std::{env, fs, str};
|
||||||
@ -19,13 +21,29 @@ fn style_str(style: Style) -> &'static str {
|
|||||||
|
|
||||||
fn run_cbindgen(
|
fn run_cbindgen(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
output: &Path,
|
output: Option<&Path>,
|
||||||
language: Language,
|
language: Language,
|
||||||
cpp_compat: bool,
|
cpp_compat: bool,
|
||||||
style: Option<Style>,
|
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 program = Path::new(CBINDGEN_PATH);
|
||||||
let mut command = Command::new(program);
|
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 {
|
match language {
|
||||||
Language::Cxx => {}
|
Language::Cxx => {}
|
||||||
Language::C => {
|
Language::C => {
|
||||||
@ -53,13 +71,37 @@ fn run_cbindgen(
|
|||||||
|
|
||||||
println!("Running: {:?}", command);
|
println!("Running: {:?}", command);
|
||||||
let cbindgen_output = command.output().expect("failed to execute process");
|
let cbindgen_output = command.output().expect("failed to execute process");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
cbindgen_output.status.success(),
|
cbindgen_output.status.success(),
|
||||||
"cbindgen failed: {:?} with error: {}",
|
"cbindgen failed: {:?} with error: {}",
|
||||||
output,
|
output,
|
||||||
str::from_utf8(&cbindgen_output.stderr).unwrap_or_default()
|
str::from_utf8(&cbindgen_output.stderr).unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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
|
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(
|
fn compile(
|
||||||
@ -183,7 +225,40 @@ fn run_compile_test(
|
|||||||
|
|
||||||
generated_file.push(source_file);
|
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) {
|
if cbindgen_outputs.contains(&cbindgen_output) {
|
||||||
// We already generated an identical file previously.
|
// We already generated an identical file previously.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user