rerun_except/
lib.rs

1//! Specify which files should *not* trigger a `cargo` rebuild.
2//!
3//! In normal operation, `cargo` rebuilds a project when any potentially relevant file changes. One
4//! can use the
5//! [`rerun-if-changed`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#change-detection)
6//! instruction to tell `cargo` to only rebuild if certain files are changed. However, it is easy
7//! to forget to add new files when using `rerun-if-changed`, causing `cargo` not to rebuild a
8//! project when it should.
9//!
10//! `rerun_except` inverts this logic, causing `cargo` to rebuild a project when a file changes
11//! *unless you explicitly ignored that file*. This is safer than `rerun-if-changed` because if you
12//! forget to explicitly ignore files, then `cargo` will still rebuild your project.
13//!
14//! `rerun_except` uses the [`ignore`](https://crates.io/crates/ignore) library to specify which
15//! files to ignore in `gitignore` format. Note that explicit ignore files in your project (e.g.
16//! `.gitignore`) are implicitly added to the list of ignored files.
17//!
18//! For example if you have the following file layout:
19//!
20//! ```text
21//! proj/
22//!   .gitignore
23//!   Cargo.toml
24//!   src/
25//!     lib.rs
26//!   lang_tests/
27//!     run.rs
28//!     test1.lang
29//!     test2.lang
30//!   target/
31//!     ...
32//! ```
33//!
34//! and you do not want the two `.lang` files to trigger a rebuild then you would tell
35//! `rerun_except` to exclude `lang_tests/*.lang`. Assuming, as is common, that your `.gitignore`
36//! file also  the `target/` directory, then `rerun_except` will also ignore the `target`
37//! directory.
38//!
39//! Adding a new file such as `lang_tests/test3.lang` will not trigger a rebuild (since it is
40//! covered by the ignore glob `lang_tests/*.lang`), but adding a new file such as `build.rs` will
41//! trigger a rebuild (since it is not covered by an ignore glob).
42//!
43//! To use `rerun_except` in this manner you simply need to call `rerun_except::rerun_except` with
44//! an array of ignore globs in [`gitignore` format](https://git-scm.com/docs/gitignore) as part of
45//! your `build.rs` file:
46//!
47//! ```rust,ignore
48//! use rerun_except::rerun_except;
49//!
50//! fn main() {
51//!     rerun_except(&["lang_tests/*.lang"]).unwrap();
52//! }
53//! ```
54
55#![allow(clippy::needless_doctest_main)]
56
57use std::env;
58use std::error::Error;
59
60use ignore::{overrides::OverrideBuilder, WalkBuilder};
61
62/// Specify which files should not cause `cargo` to rebuild a project. `globs` is an array of
63/// ignore globs. Each entry must be in [`gitignore` format](https://git-scm.com/docs/gitignore)
64/// with the minor exception that entries must not begin with a `!`.
65pub fn rerun_except(globs: &[&str]) -> Result<(), Box<dyn Error>> {
66    check_globs(globs)?;
67
68    let mdir = env::var("CARGO_MANIFEST_DIR").unwrap();
69    let mut overb = OverrideBuilder::new(&mdir);
70    for g in globs {
71        overb.add(&format!("!{}", g))?;
72    }
73    for e in WalkBuilder::new(&mdir)
74        .overrides(overb.build()?)
75        .build()
76        .filter(|x| x.is_ok())
77    {
78        let e_uw = e?;
79        let path = e_uw.path();
80        if path.is_dir() {
81            continue;
82        }
83        if let Some(path_str) = path.to_str() {
84            if path_str == mdir {
85                continue;
86            }
87            println!("cargo:rerun-if-changed={}", path_str);
88        }
89    }
90
91    Ok(())
92}
93
94fn check_globs(globs: &[&str]) -> Result<(), Box<dyn Error>> {
95    for g in globs {
96        if g.starts_with('!') {
97            return Err(Box::<dyn Error>::from("Glob '%s' starts with a '!'"));
98        }
99    }
100    Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_check_globs() {
109        assert!(check_globs(&["a"]).is_ok());
110        assert!(check_globs(&["^a"]).is_ok());
111        assert!(check_globs(&["!a"]).is_err());
112    }
113}