Skip to content

Commit 0fad495

Browse files
committed
direnv: fix excessive reloads
When watching generated files, we need to take care not to touch the files unless they change. direnv only compares the modification time, not the file contents, so it's triggered every time we write these files. Even worse, the vscode extension is happy to re-evaluate the shell without waiting for the previous one to finish loading.
1 parent db41761 commit 0fad495

File tree

7 files changed

+102
-31
lines changed

7 files changed

+102
-31
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[workspace]
22
resolver = "2"
33
members = [
4-
"devenv",
5-
"devenv-generate",
6-
"devenv-eval-cache",
7-
"devenv-run-tests",
8-
"devenv-tasks",
9-
"http-client-tls",
10-
"nix-conf-parser",
11-
"xtask",
4+
"devenv",
5+
"devenv-generate",
6+
"devenv-eval-cache",
7+
"devenv-run-tests",
8+
"devenv-tasks",
9+
"http-client-tls",
10+
"nix-conf-parser",
11+
"xtask",
1212
]
1313

1414
[workspace.package]
@@ -35,6 +35,7 @@ cli-table = "0.4.9"
3535
console = "0.15.8"
3636
dotlock = "0.5.0"
3737
dialoguer = "0.11.0"
38+
fd-lock = "4"
3839
futures = "0.3.30"
3940
hex = "0.4.3"
4041
include_dir = "0.7.3"
@@ -51,10 +52,10 @@ pretty_assertions = { version = "1.4.0", features = ["unstable"] }
5152
regex = "1.10.3"
5253
schemars = "0.8.16"
5354
schematic = { version = "0.17.11", features = [
54-
"schema",
55-
"yaml",
56-
"renderer_template",
57-
"renderer_json_schema",
55+
"schema",
56+
"yaml",
57+
"renderer_template",
58+
"renderer_json_schema",
5859
] }
5960
serde = { version = "1.0.197", features = ["derive"] }
6061
serde_json = "1.0.114"
@@ -69,13 +70,13 @@ tracing = "0.1.40"
6970
tracing-core = "0.1.32"
7071
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
7172
tokio = { version = "1.39.3", features = [
72-
"process",
73-
"fs",
74-
"io-util",
75-
"macros",
76-
"rt-multi-thread",
77-
"sync",
78-
"time",
73+
"process",
74+
"fs",
75+
"io-util",
76+
"macros",
77+
"rt-multi-thread",
78+
"sync",
79+
"time",
7980
] }
8081
tokio-util = { version = "0.7.12", features = ["io"] }
8182
which = "7.0.2"

devenv-eval-cache/src/command.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ impl<'a> CachedCommand<'a> {
4545
}
4646

4747
/// Watch additional paths for changes.
48+
///
49+
/// WARN: Be careful watching generated files.
50+
/// External tools like direnv are triggered solely by the modification date and don't compare file contents.
51+
/// Use [util::write_file_with_lock] to safely write such files.
4852
pub fn watch_path<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
4953
self.extra_paths.push(path.as_ref().to_path_buf());
5054
self

devenv/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ cli-table.workspace = true
2020
console.workspace = true
2121
dialoguer.workspace = true
2222
dotlock.workspace = true
23+
fd-lock.workspace = true
2324
hex.workspace = true
2425
include_dir.workspace = true
2526
indoc.workspace = true

devenv/src/devenv.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{cli, cnix, config, tasks};
1+
use super::{cli, cnix, config, tasks, util};
22
use clap::crate_version;
33
use cli_table::Table;
44
use cli_table::{print_stderr, WithTitle};
@@ -866,11 +866,10 @@ impl Devenv {
866866
}
867867
}
868868
}
869-
fs::write(
869+
util::write_file_with_lock(
870870
self.devenv_dotfile.join("flake.json"),
871-
serde_json::to_string(&flake_inputs).unwrap(),
872-
)
873-
.expect("Failed to write flake.json");
871+
&serde_json::to_string(&flake_inputs).unwrap(),
872+
)?;
874873
fs::write(
875874
self.devenv_dotfile.join("devenv.json"),
876875
serde_json::to_string(&self.config).unwrap(),
@@ -916,13 +915,7 @@ impl Devenv {
916915
);
917916
let flake = FLAKE_TMPL.replace("__DEVENV_VARS__", &vars);
918917
let flake_path = self.devenv_root.join(DEVENV_FLAKE);
919-
920-
// Avoid writing the flake if it hasn't changed.
921-
// direnv's watch_file triggers a reload based solely on mtime, which becomes annoying if we constantly touch this file.
922-
let existing_flake = fs::read_to_string(&flake_path).unwrap_or_default();
923-
if flake != existing_flake {
924-
fs::write(flake_path, flake).expect("Failed to write flake.nix");
925-
}
918+
util::write_file_with_lock(&flake_path, &flake)?;
926919

927920
self.assembled = true;
928921
Ok(())

devenv/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) mod cnix;
33
pub mod config;
44
mod devenv;
55
pub mod log;
6+
mod util;
67

78
pub use cli::{default_system, GlobalOptions};
89
pub use devenv::{Devenv, DevenvOptions, DIRENVRC, DIRENVRC_VERSION};

devenv/src/util.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use fd_lock::RwLock;
2+
use miette::{miette, IntoDiagnostic, Result};
3+
use std::fs;
4+
use std::io::Write;
5+
use std::path::Path;
6+
7+
/// Safely write a file with locking, avoiding writing if the content hasn't changed.
8+
///
9+
/// Returns Ok(true) if the file was written, Ok(false) if no write was needed.
10+
pub fn write_file_with_lock<P: AsRef<Path>>(path: P, content: &str) -> Result<bool> {
11+
let path = path.as_ref();
12+
13+
// Create parent directories if they don't exist
14+
if let Some(parent) = path.parent() {
15+
if !parent.exists() {
16+
fs::create_dir_all(parent)
17+
.into_diagnostic()
18+
.map_err(|e| miette!("Failed to create directory {}: {}", parent.display(), e))?;
19+
}
20+
}
21+
22+
// Open or create the file with locking
23+
let file = fs::OpenOptions::new()
24+
.read(true)
25+
.write(true)
26+
.create(true)
27+
.open(path)
28+
.into_diagnostic()
29+
.map_err(|e| miette!("Failed to open file {}: {}", path.display(), e))?;
30+
31+
// Acquire an exclusive lock on the file
32+
let mut file_lock = RwLock::new(file);
33+
let mut file_handle = file_lock
34+
.write()
35+
.into_diagnostic()
36+
.map_err(|e| miette!("Failed to lock file {}: {}", path.display(), e))?;
37+
38+
// Read existing content
39+
let existing_content = fs::read_to_string(path).unwrap_or_default();
40+
41+
// Compare and write only if different
42+
if content != existing_content {
43+
file_handle
44+
.set_len(0)
45+
.into_diagnostic()
46+
.map_err(|e| miette!("Failed to truncate file {}: {}", path.display(), e))?;
47+
48+
file_handle
49+
.write_all(content.as_bytes())
50+
.into_diagnostic()
51+
.map_err(|e| miette!("Failed to write to file {}: {}", path.display(), e))?;
52+
53+
// File was written
54+
Ok(true)
55+
} else {
56+
// No write needed
57+
Ok(false)
58+
}
59+
}

0 commit comments

Comments
 (0)