diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3edc5e5..81cb383 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,10 +11,10 @@ jobs: matrix: # note: we're using ubuntu-latest as a stand-in for all Linux # distributions. If we find we need more, we should do Docker stuff. - os: [ubuntu-latest, macos-11] + os: [ubuntu-latest, macos-latest] runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@v2 - - uses: cachix/install-nix-action@v16 + - uses: actions/checkout@v7 + - uses: cachix/install-nix-action@v31 - run: nix-build diff --git a/Cargo.lock b/Cargo.lock index 0a8fc8f..ced915c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "firstaide" -version = "0.1.6" +version = "0.1.7" dependencies = [ "anyhow", "atty", diff --git a/Cargo.toml b/Cargo.toml index 1aa563d..05d2d99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "firstaide" -version = "0.1.6" +version = "0.1.7" authors = ["Gavin Panella "] edition = "2018" description = "Bootstrap and cache Nix environments; works with direnv." diff --git a/src/cmds/build.rs b/src/cmds/build.rs index 7b54979..12efef2 100644 --- a/src/cmds/build.rs +++ b/src/cmds/build.rs @@ -92,7 +92,7 @@ fn build(config: config::Config) -> Result { // 5. Calculate checksums. log::info!("Calculate file checksums."); - let checksums = spin(|| sums::Checksums::from(&config.watch_files()?)) + let checksums = spin(|| sums::Checksums::from(&config.build_dir, &config.watch_files()?)) .context("could not calculate checksums")?; let cache_file = config.cache_file(&checksums); diff --git a/src/cmds/hook.rs b/src/cmds/hook.rs index 9a2977d..f49aaa0 100644 --- a/src/cmds/hook.rs +++ b/src/cmds/hook.rs @@ -75,7 +75,7 @@ impl Command { .write_all(&chunk("Helpers.", include_bytes!("hook/helpers.sh"))) .context("could not write helpers")?; - let sums_now = sums::Checksums::from(&config.watch_files()?)?; + let sums_now = sums::Checksums::from(&config.build_dir, &config.watch_files()?)?; let cache_file = config.cache_file(&sums_now); let cache_file_fallback = config.cache_file_most_recent(); @@ -116,8 +116,10 @@ impl Command { { let mut watches = Vec::with_capacity(8192); // 8kB enough? watches.extend(b"watch_file \\\n "); + // Checksum paths are relative to the build directory (or + // absolute, in caches written by older versions). for watch in cache.sums.into_iter() { - bash::escape_into(watch.path(), &mut watches); + bash::escape_into(&config.abspath(watch.path()), &mut watches); watches.extend(b" \\\n "); } // Also watch the cache file, the build log, the build diff --git a/src/cmds/status.rs b/src/cmds/status.rs index 79f3c42..e36c673 100644 --- a/src/cmds/status.rs +++ b/src/cmds/status.rs @@ -23,9 +23,11 @@ impl Command { let stdout = io::stdout(); let mut handle = stdout.lock(); - let sums_now = - sums::Checksums::from(&config.watch_files().context("could not get watch files")?) - .context("could not calculate checksums")?; + let sums_now = sums::Checksums::from( + &config.build_dir, + &config.watch_files().context("could not get watch files")?, + ) + .context("could not calculate checksums")?; let cache_file = config.cache_file(&sums_now); let cache_file_fallback = config.cache_file_most_recent(); diff --git a/src/config.rs b/src/config.rs index 23089cc..ac53aee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -181,7 +181,7 @@ impl Config { } /// Return an absolute path, resolved relative to `self.build_dir`. - fn abspath>(&self, path: T) -> PathBuf { + pub fn abspath>(&self, path: T) -> PathBuf { let p = path.as_ref(); if p.is_relative() { self.build_dir.join(p) diff --git a/src/sums.rs b/src/sums.rs index 97446fd..e17e5a9 100644 --- a/src/sums.rs +++ b/src/sums.rs @@ -1,4 +1,3 @@ - use crypto_hash::{hex_digest, Algorithm}; use serde::{Deserialize, Serialize}; use std::fs; @@ -9,13 +8,14 @@ use std::path::{Path, PathBuf}; pub struct Checksums(Vec); impl Checksums { - pub fn from(filenames: &[T]) -> io::Result + pub fn from(root: R, filenames: &[T]) -> io::Result where + R: AsRef, T: AsRef, { let mut sums = Vec::new(); for filename in filenames { - let sum = Checksum::from(filename)?; + let sum = Checksum::from(root.as_ref(), filename)?; sums.push(sum); } Ok(Self(sums)) @@ -44,11 +44,15 @@ pub enum Checksum { } impl Checksum { - pub fn from(filename: T) -> io::Result + pub fn from(root: &Path, filename: T) -> io::Result where T: AsRef, { - let path = filename.as_ref().to_path_buf(); + // Store the path relative to `root` so that checksums – and the cache + // signature derived from them – do not depend on where the working + // tree lives. Paths outside `root` are kept as they are. + let path = filename.as_ref(); + let path = path.strip_prefix(root).unwrap_or(path).to_path_buf(); match Sha1::from(&filename) { Ok(sha1) => Ok(Checksum::Found(path, sha1)), Err(ref err) if err.kind() == io::ErrorKind::NotFound => Ok(Checksum::NotFound(path)), @@ -79,3 +83,58 @@ impl Sha1 { pub fn equal(a: &Checksums, b: &Checksums) -> bool { a.0.iter().eq(b.0.iter()) } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn checksums_of_identical_trees_at_different_roots_are_equal() { + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + for dir in [dir_a.path(), dir_b.path()] { + fs::create_dir(dir.join("sub")).unwrap(); + fs::write(dir.join("shell.nix"), b"{ }: 12345").unwrap(); + fs::write(dir.join("sub").join("deps.nix"), b"{ }: 67890").unwrap(); + } + let files_a = [ + dir_a.path().join("shell.nix"), + dir_a.path().join("sub/deps.nix"), + ]; + let files_b = [ + dir_b.path().join("shell.nix"), + dir_b.path().join("sub/deps.nix"), + ]; + + let sums_a = Checksums::from(dir_a.path(), &files_a).unwrap(); + let sums_b = Checksums::from(dir_b.path(), &files_b).unwrap(); + + assert!(equal(&sums_a, &sums_b)); + assert_eq!(sums_a.sig(), sums_b.sig()); + } + + #[test] + fn checksums_of_missing_files_are_also_root_relative() { + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + + let sums_a = Checksums::from(dir_a.path(), &[dir_a.path().join("nope.nix")]).unwrap(); + let sums_b = Checksums::from(dir_b.path(), &[dir_b.path().join("nope.nix")]).unwrap(); + + assert!(equal(&sums_a, &sums_b)); + assert_eq!(sums_a.sig(), sums_b.sig()); + } + + #[test] + fn checksums_keep_paths_outside_the_root_absolute() { + let root = tempfile::tempdir().unwrap(); + let elsewhere = tempfile::tempdir().unwrap(); + fs::write(elsewhere.path().join("other.nix"), b"{ }: 1").unwrap(); + let outside = elsewhere.path().join("other.nix"); + + let sums = Checksums::from(root.path(), &[outside.clone()]).unwrap(); + + assert_eq!(sums.0[0].path(), outside.as_path()); + } +}