diff --git a/src/builder.rs b/src/builder.rs index 520d6c94..3c9ea211 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -103,9 +103,21 @@ fn instrumented_instantiation( .into_iter() .fold((vec![], vec![]), |(mut paths, mut log_lines), result| { match result { - LogDatum::CopiedSource(src) - | LogDatum::NixSourceFile(src) - | LogDatum::ReadFileOrDir(src) => { + LogDatum::CopiedSource(src) | LogDatum::ReadFileOrDir(src) => { + paths.push(src); + } + LogDatum::NixSourceFile(mut src) => { + // We need to emulate nix’s `default.nix` mechanism here. + // That is, if the user uses something like + // `import ./foo` + // and `foo` is a directory, nix will actually import + // `./foo/default.nix` + // but still print `./foo`. + // Since this is the only time directories are printed, + // we can just manually re-implement that behavior. + if src.is_dir() { + src.push("default.nix"); + } paths.push(src); } LogDatum::Text(line) => log_lines.push(line), @@ -405,4 +417,100 @@ in {} Ok(()) } + /// Helper to recursively print the contents of files of a directory tree. + /// No error handling is done. + fn pretty_print_files_in_dir(dir: &Path) -> String { + std::fs::read_dir(&dir) + .unwrap() + .map(|path| { + let p = path.unwrap().path(); + format!( + "{:?}: {}\n", + p, + if !p.is_dir() { + String::from_utf8(std::fs::read(&p).unwrap()).unwrap() + } else { + "\n".to_string() + &pretty_print_files_in_dir(&p) + } + ) + }) + .collect::() + } + + // TODO: builtins.fetchTarball and the like? What happens with those? + // Are they directories and if yes, should we watch them? + /// The paths that are returned by the nix-instantiate call + /// must not contain directories, otherwise the watcher will + /// watch those recursively, which leads to a lot of wasted resources + /// and often exhausts the amount of available file handles + /// (especially on macOS). + #[test] + fn no_unnecessary_files_or_directories_watched() -> std::io::Result<()> { + let root_tmp = tempfile::tempdir()?; + let cas_tmp = tempfile::tempdir()?; + let root = root_tmp.path(); + let shell = root.join("shell.nix"); + std::fs::write( + &shell, + drv( + "shell", + r##" +# The `foo/default.nix` is implicitely imported +# (we only want to watch that one, not the whole directory) +foo = import ./foo; +# `dir` is imported as source directory (no `import`). +# We *do* want to watch this directory, because we need to react +# when the user updates it. +dir-as-source = ./dir; +"##, + ), + )?; + + // ./foo + // ./foo/default.nix + // ./foo/bar <- should not be watched + // ./foo/baz <- should be watched + // ./dir <- should be watched, because imported as source + let foo = root.join("foo"); + std::fs::create_dir(&foo)?; + let dir = root.join("dir"); + std::fs::create_dir(&dir)?; + let foo_default = &foo.join("default.nix"); + std::fs::write(&foo_default, "import ./baz")?; + let foo_bar = &foo.join("bar"); + std::fs::write(&foo_bar, "This file should not be watched")?; + let foo_baz = &foo.join("baz"); + std::fs::write(&foo_baz, "\"This file should be watched\"")?; + + let cas = ContentAddressable::new(cas_tmp.path().join("cas"))?; + + match instrumented_instantiation(&NixFile::from(shell), &cas).unwrap() { + Info::Success(s) => { + assert!( + s.paths.iter().any(|p| p.ends_with("foo/default.nix")), + "foo/default.nix should be watched!" + ); + assert!( + !s.paths.iter().any(|p| p.ends_with("foo/bar")), + "foo/bar should not be watched!" + ); + assert!( + s.paths.iter().any(|p| p.ends_with("foo/baz")), + "foo/baz should be watched!" + ); + assert!( + s.paths.iter().any(|p| p.ends_with("dir")), + "dir should be watched!" + ); + assert!( + !s.paths.iter().any(|p| p.ends_with("foo")), + "No imported directories must exist in watched paths: {:#?}", + s.paths + ); + } + Info::Failure(f) => panic!("{:#?}\n{}", f, pretty_print_files_in_dir(root)), + }; + + Ok(()) + } }