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}