Repository: mozilla/cbindgen (language: rust)

Issue:
Add support for outputting dependencies
It would be nice if cbindgen could provide the full list of input files it used to generate proper dependencies for build systems. @emilio [is adding cbindgen to the Firefox build](https://phabricator.services.mozilla.com/D2447) and his glue code is manually providing dependencies which makes me worry that that list could be incorrect, which would result in broken builds.


Discussion / hints:
This should be easyish to implement, I could even try to give it a shot. @luser do you know what's the standard way to do it?

 * Just with a flag + stdout?
 * Providing a file we can read the dependencies from?

Thanks :)
I'm working on Rust support in [meson](https://github.com/mesonbuild/meson), and native support for cbindgen is on my radar as something I'll need, and internal dependency tracking would be excellent. There is a pretty standard format used by both ninja and make (among others), which is essentially a makefile snippet in the form:
```make
output: depends1 depends 2
```
or, with linebreaks:
```make
output: depends1\
depends2
```
This format is supported by both gcc and clang (and, because bindgen is ultimately a wrapper around clang, you can get this information out of bindgen by passing arguments to clang), and can automatically be consumed by ninja and make.

For me (and anyone else wanted to pass this information to make or ninja), having this dependency information writte to a file as a side effect of cbindgen is ideal, something in the form of: `cbindgen ... --dep-file=dependencies.d`.

I'd be willing to work on this if you're not, but I'd appreciate some pointers on where to get started.
@dcbaker I have a PR for this (#820), and initial testing shows that it works as expected with CMake. Maybe you could have a look if it also works for meson and share some feedback.

Fix patch:
diff --git a/build.rs b/build.rs
index 5644b02c1..8390b5f62 100644
--- a/build.rs
+++ b/build.rs
@@ -16,19 +16,6 @@ fn generate_tests() {
 
     println!("cargo:rerun-if-changed={}", tests_dir.display());
 
-    // Try to make a decent guess at where our binary will end up in.
-    //
-    // TODO(emilio): Ideally running tests will just use the library-version of
-    // cbindgen instead of the built binary.
-    let cbindgen_path = out_dir
-        .parent()
-        .unwrap()
-        .parent()
-        .unwrap()
-        .parent()
-        .unwrap()
-        .join("cbindgen");
-
     for entry in entries {
         let path_segment = if entry.file_type().unwrap().is_file() {
             match entry.path().extension().and_then(OsStr::to_str) {
@@ -53,8 +40,47 @@ fn generate_tests() {
 
         writeln!(
             dst,
-            "test_file!({:?}, test_{}, {:?}, {:?});",
-            cbindgen_path,
+            "test_file!(test_{}, {:?}, {:?});",
+            identifier,
+            path_segment,
+            entry.path(),
+        )
+        .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(),
@@ -67,4 +93,5 @@ fn generate_tests() {
 
 fn main() {
     generate_tests();
+    generate_depfile_tests();
 }
diff --git a/src/bindgen/bindings.rs b/src/bindgen/bindings.rs
index a03b2826d..cdacdadca 100644
--- a/src/bindgen/bindings.rs
+++ b/src/bindgen/bindings.rs
@@ -29,6 +29,7 @@ pub struct Bindings {
     constants: Vec<Constant>,
     items: Vec<ItemContainer>,
     functions: Vec<Function>,
+    source_files: Vec<path::PathBuf>,
     /// Bindings are generated by a recursive call to cbindgen
     /// and shouldn't do anything when written anywhere.
     noop: bool,
@@ -50,6 +51,7 @@ impl Bindings {
         globals: Vec<Static>,
         items: Vec<ItemContainer>,
         functions: Vec<Function>,
+        source_files: Vec<path::PathBuf>,
         noop: bool,
     ) -> Bindings {
         Bindings {
@@ -61,6 +63,7 @@ impl Bindings {
             constants,
             items,
             functions,
+            source_files,
             noop,
         }
     }
@@ -128,6 +131,47 @@ impl Bindings {
         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 {
         if self.noop {
             return false;
diff --git a/src/bindgen/builder.rs b/src/bindgen/builder.rs
index 17d4ad1df..a0328b409 100644
--- a/src/bindgen/builder.rs
+++ b/src/bindgen/builder.rs
@@ -359,6 +359,7 @@ impl Builder {
                 Default::default(),
                 Default::default(),
                 Default::default(),
+                Default::default(),
                 true,
             ));
         }
@@ -391,6 +392,8 @@ impl Builder {
             result.extend_with(&parser::parse_lib(cargo, &self.config)?);
         }
 
+        result.source_files.extend_from_slice(self.srcs.as_slice());
+
         Library::new(
             self.config,
             result.constants,
@@ -401,6 +404,7 @@ impl Builder {
             result.opaque_items,
             result.typedefs,
             result.functions,
+            result.source_files,
         )
         .generate()
     }
diff --git a/src/bindgen/config.rs b/src/bindgen/config.rs
index fbf35cbe1..c7590355a 100644
--- a/src/bindgen/config.rs
+++ b/src/bindgen/config.rs
@@ -5,7 +5,7 @@
 use std::collections::{BTreeMap, HashMap};
 use std::default::Default;
 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::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
@@ -1003,6 +1003,8 @@ pub struct Config {
     pub only_target_dependencies: bool,
     /// Configuration options specific to Cython.
     pub cython: CythonConfig,
+    #[serde(skip)]
+    pub(crate) config_path: Option<StdPathBuf>,
 }
 
 impl Default for Config {
@@ -1045,6 +1047,7 @@ impl Default for Config {
             pointer: PtrConfig::default(),
             only_target_dependencies: false,
             cython: CythonConfig::default(),
+            config_path: None,
         }
     }
 }
@@ -1086,10 +1089,10 @@ impl Config {
             )
         })?;
 
-        match toml::from_str::<Config>(&config_text) {
-            Ok(x) => Ok(x),
-            Err(e) => Err(format!("Couldn't parse config file: {}.", e)),
-        }
+        let mut config = toml::from_str::<Config>(&config_text)
+            .map_err(|e| 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 {
diff --git a/src/bindgen/library.rs b/src/bindgen/library.rs
index c00a6ebc6..cb4cfbd3a 100644
--- a/src/bindgen/library.rs
+++ b/src/bindgen/library.rs
@@ -3,6 +3,7 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use std::collections::HashMap;
+use std::path::PathBuf;
 
 use crate::bindgen::bindings::Bindings;
 use crate::bindgen::config::{Config, Language, SortKey};
@@ -25,6 +26,7 @@ pub struct Library {
     opaque_items: ItemMap<OpaqueItem>,
     typedefs: ItemMap<Typedef>,
     functions: Vec<Function>,
+    source_files: Vec<PathBuf>,
 }
 
 impl Library {
@@ -39,6 +41,7 @@ impl Library {
         opaque_items: ItemMap<OpaqueItem>,
         typedefs: ItemMap<Typedef>,
         functions: Vec<Function>,
+        source_files: Vec<PathBuf>,
     ) -> Library {
         Library {
             config,
@@ -50,6 +53,7 @@ impl Library {
             opaque_items,
             typedefs,
             functions,
+            source_files,
         }
     }
 
@@ -135,6 +139,7 @@ impl Library {
             globals,
             items,
             functions,
+            self.source_files,
             false,
         ))
     }
diff --git a/src/bindgen/parser.rs b/src/bindgen/parser.rs
index a964cd2d8..e8add192a 100644
--- a/src/bindgen/parser.rs
+++ b/src/bindgen/parser.rs
@@ -57,6 +57,7 @@ pub fn parse_src(src_file: &FilePath, config: &Config) -> ParseResult {
     };
 
     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)
 }
 
@@ -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();
     context.parse_crate(&binding_crate)?;
+    context.out.source_files = context.cache_src.keys().map(|k| k.to_owned()).collect();
     Ok(context.out)
 }
 
@@ -406,6 +408,7 @@ pub struct Parse {
     pub opaque_items: ItemMap<OpaqueItem>,
     pub typedefs: ItemMap<Typedef>,
     pub functions: Vec<Function>,
+    pub source_files: Vec<FilePathBuf>,
 }
 
 impl Parse {
@@ -419,6 +422,7 @@ impl Parse {
             opaque_items: ItemMap::default(),
             typedefs: ItemMap::default(),
             functions: Vec::new(),
+            source_files: Vec::new(),
         }
     }
 
@@ -466,6 +470,7 @@ impl Parse {
         self.opaque_items.extend_with(&other.opaque_items);
         self.typedefs.extend_with(&other.typedefs);
         self.functions.extend_from_slice(&other.functions);
+        self.source_files.extend_from_slice(&other.source_files);
     }
 
     fn load_syn_crate_mod<'a>(
diff --git a/src/main.rs b/src/main.rs
index 812366f49..ea2ef88f9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -262,6 +262,20 @@ fn main() {
                 .help("Report errors only (overrides verbosity options).")
                 .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();
 
     if !matches.is_present("out") && matches.is_present("verify") {
@@ -306,6 +320,9 @@ fn main() {
                 error!("Bindings changed: {}", file);
                 std::process::exit(2);
             }
+            if let Some(depfile) = matches.value_of("depfile") {
+                bindings.generate_depfile(file, depfile)
+            }
         }
         _ => {
             bindings.write(io::stdout());


Test patch:
diff --git a/tests/depfile.rs b/tests/depfile.rs
new file mode 100644
index 000000000..8dc1859f3
--- /dev/null
+++ b/tests/depfile.rs
@@ -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"));
diff --git a/tests/depfile/Readme.md b/tests/depfile/Readme.md
new file mode 100644
index 000000000..d7f8b1a73
--- /dev/null
+++ b/tests/depfile/Readme.md
@@ -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`).
diff --git a/tests/depfile/cbindgen_test.cmake b/tests/depfile/cbindgen_test.cmake
new file mode 100644
index 000000000..752c3c3b6
--- /dev/null
+++ b/tests/depfile/cbindgen_test.cmake
@@ -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()
\ No newline at end of file
diff --git a/tests/depfile/single_crate/.gitignore b/tests/depfile/single_crate/.gitignore
new file mode 100644
index 000000000..378eac25d
--- /dev/null
+++ b/tests/depfile/single_crate/.gitignore
@@ -0,0 +1,1 @@
+build
diff --git a/tests/depfile/single_crate/CMakeLists.txt b/tests/depfile/single_crate/CMakeLists.txt
new file mode 100644
index 000000000..3c7862478
--- /dev/null
+++ b/tests/depfile/single_crate/CMakeLists.txt
@@ -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"
+)
diff --git a/tests/depfile/single_crate/Cargo.lock b/tests/depfile/single_crate/Cargo.lock
new file mode 100644
index 000000000..26d416263
--- /dev/null
+++ b/tests/depfile/single_crate/Cargo.lock
@@ -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"
diff --git a/tests/depfile/single_crate/Cargo.toml b/tests/depfile/single_crate/Cargo.toml
new file mode 100644
index 000000000..66eff1125
--- /dev/null
+++ b/tests/depfile/single_crate/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "single_crate"
+version = "0.1.0"
+authors = ["cbindgen"]
+
+[features]
+cbindgen = []
diff --git a/tests/depfile/single_crate/expectations/dependencies b/tests/depfile/single_crate/expectations/dependencies
new file mode 100644
index 000000000..7a9b14c93
--- /dev/null
+++ b/tests/depfile/single_crate/expectations/dependencies
@@ -0,0 +1,3 @@
+src/alias/mod.rs
+src/annotation.rs
+src/lib.rs
diff --git a/tests/depfile/single_crate/src/alias/mod.rs b/tests/depfile/single_crate/src/alias/mod.rs
new file mode 100644
index 000000000..86e3e0049
--- /dev/null
+++ b/tests/depfile/single_crate/src/alias/mod.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
+) { }
diff --git a/tests/depfile/single_crate/src/annotation.rs b/tests/depfile/single_crate/src/annotation.rs
new file mode 100644
index 000000000..70705923e
--- /dev/null
+++ b/tests/depfile/single_crate/src/annotation.rs
@@ -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,
+) { }
+
diff --git a/tests/depfile/single_crate/src/lib.rs b/tests/depfile/single_crate/src/lib.rs
new file mode 100644
index 000000000..c604cbd38
--- /dev/null
+++ b/tests/depfile/single_crate/src/lib.rs
@@ -0,0 +1,2 @@
+mod alias;
+mod annotation;
diff --git a/tests/depfile/single_crate_config/.gitignore b/tests/depfile/single_crate_config/.gitignore
new file mode 100644
index 000000000..378eac25d
--- /dev/null
+++ b/tests/depfile/single_crate_config/.gitignore
@@ -0,0 +1,1 @@
+build
diff --git a/tests/depfile/single_crate_config/CMakeLists.txt b/tests/depfile/single_crate_config/CMakeLists.txt
new file mode 100644
index 000000000..db7229460
--- /dev/null
+++ b/tests/depfile/single_crate_config/CMakeLists.txt
@@ -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"
+        )
diff --git a/tests/depfile/single_crate_config/Cargo.lock b/tests/depfile/single_crate_config/Cargo.lock
new file mode 100644
index 000000000..26d416263
--- /dev/null
+++ b/tests/depfile/single_crate_config/Cargo.lock
@@ -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"
diff --git a/tests/depfile/single_crate_config/Cargo.toml b/tests/depfile/single_crate_config/Cargo.toml
new file mode 100644
index 000000000..66eff1125
--- /dev/null
+++ b/tests/depfile/single_crate_config/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "single_crate"
+version = "0.1.0"
+authors = ["cbindgen"]
+
+[features]
+cbindgen = []
diff --git a/tests/depfile/single_crate_config/config.toml b/tests/depfile/single_crate_config/config.toml
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/depfile/single_crate_config/expectations/dependencies b/tests/depfile/single_crate_config/expectations/dependencies
new file mode 100644
index 000000000..bf8ccb39e
--- /dev/null
+++ b/tests/depfile/single_crate_config/expectations/dependencies
@@ -0,0 +1,4 @@
+config.toml
+src/alias/mod.rs
+src/annotation.rs
+src/lib.rs
diff --git a/tests/depfile/single_crate_config/src/alias/mod.rs b/tests/depfile/single_crate_config/src/alias/mod.rs
new file mode 100644
index 000000000..6c045d294
--- /dev/null
+++ b/tests/depfile/single_crate_config/src/alias/mod.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,
+) {}
diff --git a/tests/depfile/single_crate_config/src/annotation.rs b/tests/depfile/single_crate_config/src/annotation.rs
new file mode 100644
index 000000000..54cd52662
--- /dev/null
+++ b/tests/depfile/single_crate_config/src/annotation.rs
@@ -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,
+) {}
+
diff --git a/tests/depfile/single_crate_config/src/lib.rs b/tests/depfile/single_crate_config/src/lib.rs
new file mode 100644
index 000000000..c604cbd38
--- /dev/null
+++ b/tests/depfile/single_crate_config/src/lib.rs
@@ -0,0 +1,2 @@
+mod alias;
+mod annotation;
diff --git a/tests/depfile/single_crate_default_config/.gitignore b/tests/depfile/single_crate_default_config/.gitignore
new file mode 100644
index 000000000..378eac25d
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/.gitignore
@@ -0,0 +1,1 @@
+build
diff --git a/tests/depfile/single_crate_default_config/CMakeLists.txt b/tests/depfile/single_crate_default_config/CMakeLists.txt
new file mode 100644
index 000000000..7a2b0ab9b
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/CMakeLists.txt
@@ -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"
+    )
diff --git a/tests/depfile/single_crate_default_config/Cargo.lock b/tests/depfile/single_crate_default_config/Cargo.lock
new file mode 100644
index 000000000..26d416263
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/Cargo.lock
@@ -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"
diff --git a/tests/depfile/single_crate_default_config/Cargo.toml b/tests/depfile/single_crate_default_config/Cargo.toml
new file mode 100644
index 000000000..66eff1125
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "single_crate"
+version = "0.1.0"
+authors = ["cbindgen"]
+
+[features]
+cbindgen = []
diff --git a/tests/depfile/single_crate_default_config/cbindgen.toml b/tests/depfile/single_crate_default_config/cbindgen.toml
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/depfile/single_crate_default_config/expectations/dependencies b/tests/depfile/single_crate_default_config/expectations/dependencies
new file mode 100644
index 000000000..693c6f3c0
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/expectations/dependencies
@@ -0,0 +1,4 @@
+cbindgen.toml
+src/alias/mod.rs
+src/annotation.rs
+src/lib.rs
diff --git a/tests/depfile/single_crate_default_config/src/alias/mod.rs b/tests/depfile/single_crate_default_config/src/alias/mod.rs
new file mode 100644
index 000000000..6c045d294
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/src/alias/mod.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,
+) {}
diff --git a/tests/depfile/single_crate_default_config/src/annotation.rs b/tests/depfile/single_crate_default_config/src/annotation.rs
new file mode 100644
index 000000000..54cd52662
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/src/annotation.rs
@@ -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,
+) {}
+
diff --git a/tests/depfile/single_crate_default_config/src/lib.rs b/tests/depfile/single_crate_default_config/src/lib.rs
new file mode 100644
index 000000000..c604cbd38
--- /dev/null
+++ b/tests/depfile/single_crate_default_config/src/lib.rs
@@ -0,0 +1,2 @@
+mod alias;
+mod annotation;
diff --git a/tests/profile.rs b/tests/profile.rs
index 4e8b489d2..69433a2be 100644
--- a/tests/profile.rs
+++ b/tests/profile.rs
@@ -40,26 +40,14 @@ fn build_using_bin(extra_args: &[&str]) -> tempfile::TempDir {
         .tempdir()
         .expect("Creating tmp dir failed");
 
-    let cbindgen_path = match option_env!("CARGO_BIN_EXE_cbindgen") {
-        Some(path) => path.into(),
-        None => {
-            // We must be on an older version of Rust.
-            // Guess where cbindgen would be relative to OUT_DIR.
-            let mut path = PathBuf::from(env!("OUT_DIR"));
-            path.pop();
-            path.pop();
-            path.pop();
-            path.push("cbindgen");
-            path.into_os_string()
-        }
-    };
+    let cbindgen_path = env!("CARGO_BIN_EXE_cbindgen");
 
     Command::new(cbindgen_path)
         .current_dir(expand_dep_test_dir)
         .env("CARGO_EXPAND_TARGET_DIR", tmp_dir.path())
         .args(extra_args)
         .output()
-        .expect("build should succed");
+        .expect("build should succeed");
 
     tmp_dir
 }
diff --git a/tests/tests.rs b/tests/tests.rs
index 88a1c400e..ec389d6e4 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -2,10 +2,15 @@ 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};
 
+// Set automatically by cargo for integration tests
+static CBINDGEN_PATH: &str = env!("CARGO_BIN_EXE_cbindgen");
+
 fn style_str(style: Style) -> &'static str {
     match style {
         Style::Both => "both",
@@ -15,15 +20,30 @@ fn style_str(style: Style) -> &'static str {
 }
 
 fn run_cbindgen(
-    cbindgen_path: &'static str,
     path: &Path,
-    output: &Path,
+    output: Option<&Path>,
     language: Language,
     cpp_compat: bool,
     style: Option<Style>,
-) -> Vec<u8> {
-    let program = Path::new(cbindgen_path);
+    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 => {
@@ -51,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(
@@ -144,7 +188,6 @@ const SKIP_WARNING_AS_ERROR_SUFFIX: &str = ".skip_warning_as_error";
 
 #[allow(clippy::too_many_arguments)]
 fn run_compile_test(
-    cbindgen_path: &'static str,
     name: &'static str,
     path: &Path,
     tmp_dir: &Path,
@@ -182,14 +225,40 @@ fn run_compile_test(
 
     generated_file.push(source_file);
 
-    let cbindgen_output = run_cbindgen(
-        cbindgen_path,
+    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,
-        &generated_file,
+        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.
@@ -234,7 +303,7 @@ fn run_compile_test(
     }
 }
 
-fn test_file(cbindgen_path: &'static str, name: &'static str, filename: &'static str) {
+fn test_file(name: &'static str, filename: &'static str) {
     let test = Path::new(filename);
     let tmp_dir = tempfile::Builder::new()
         .prefix("cbindgen-test-output")
@@ -247,7 +316,6 @@ fn test_file(cbindgen_path: &'static str, name: &'static str, filename: &'static
     for cpp_compat in &[true, false] {
         for style in &[Style::Type, Style::Tag, Style::Both] {
             run_compile_test(
-                cbindgen_path,
                 name,
                 test,
                 tmp_dir,
@@ -260,7 +328,6 @@ fn test_file(cbindgen_path: &'static str, name: &'static str, filename: &'static
     }
 
     run_compile_test(
-        cbindgen_path,
         name,
         test,
         tmp_dir,
@@ -274,7 +341,6 @@ fn test_file(cbindgen_path: &'static str, name: &'static str, filename: &'static
     let mut cbindgen_outputs = HashSet::new();
     for style in &[Style::Type, Style::Tag] {
         run_compile_test(
-            cbindgen_path,
             name,
             test,
             tmp_dir,
@@ -287,10 +353,10 @@ fn test_file(cbindgen_path: &'static str, name: &'static str, filename: &'static
 }
 
 macro_rules! test_file {
-    ($cbindgen_path:expr, $test_function_name:ident, $name:expr, $file:tt) => {
+    ($test_function_name:ident, $name:expr, $file:tt) => {
         #[test]
         fn $test_function_name() {
-            test_file($cbindgen_path, $name, $file);
+            test_file($name, $file);
         }
     };
 }