Skip to content

Commit 63f24dc

Browse files
authored
Merge pull request #1838 from cachix/cli-options
Add --option CLI flag for overriding configuration options
2 parents edc1eba + 1654ff0 commit 63f24dc

File tree

10 files changed

+183
-25
lines changed

10 files changed

+183
-25
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
## Code Style Guidelines
1313
- **Imports**: Group by category (std lib first, then external crates, then internal)
1414
- **Naming**: Use `snake_case` for functions/variables, `CamelCase` for types/traits
15-
- **Error Handling**: Use `thiserror` crate with custom error types and `?` operator
15+
- **Error Handling**: Use `thiserror` crate with custom error types, `bail!` (instead of `panic~`) and `?` operator
1616
- **Types**: Prefer strong typing with descriptive names and appropriate generics
1717
- **Formatting**: Follow standard rustfmt rules, use pre-commit hooks
1818
- **Documentation**: Document public APIs with rustdoc comments

devenv-tasks/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,10 @@ mod test {
10451045
if let Err(Error::CycleDetected(task)) = result {
10461046
assert_eq!(task, "myapp:task_2".to_string());
10471047
} else {
1048-
panic!("Expected Error::CycleDetected, got {:?}", result);
1048+
return Err(Error::TaskNotFound(format!(
1049+
"Expected Error::CycleDetected, got {:?}",
1050+
result
1051+
)));
10491052
}
10501053
Ok(())
10511054
}

devenv/src/cli.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ pub struct GlobalOptions {
144144
help = "Override inputs in devenv.yaml."
145145
)]
146146
pub override_input: Vec<String>,
147+
148+
#[arg(
149+
long,
150+
global = true,
151+
num_args = 2,
152+
value_delimiter = ' ',
153+
help = "Override configuration options with typed values, e.g. --option languages.rust.version:string beta"
154+
)]
155+
pub option: Vec<String>,
147156
}
148157

149158
impl Default for GlobalOptions {
@@ -165,6 +174,7 @@ impl Default for GlobalOptions {
165174
nix_debugger: false,
166175
nix_option: vec![],
167176
override_input: vec![],
177+
option: vec![],
168178
}
169179
}
170180
}

devenv/src/cnix.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ impl Nix {
159159
let now_ns = get_now_with_nanoseconds();
160160
let target = format!("{}-shell", now_ns);
161161
if let Ok(resolved_gc_root) = fs::canonicalize(gc_root) {
162-
symlink_force(&resolved_gc_root, &self.devenv_home_gc.join(target));
162+
symlink_force(&resolved_gc_root, &self.devenv_home_gc.join(target))?;
163163
} else {
164164
warn!(
165165
"Failed to resolve the GC root path to the Nix store: {}. Try running devenv again with --refresh-eval-cache.",
@@ -185,7 +185,7 @@ impl Nix {
185185
let link_path = self
186186
.devenv_dot_gc
187187
.join(format!("{}-{}", name, get_now_with_nanoseconds()));
188-
symlink_force(path, &link_path);
188+
symlink_force(path, &link_path)?;
189189
Ok(())
190190
}
191191

@@ -387,8 +387,9 @@ impl Nix {
387387
cached_cmd.watch_path(self.devenv_root.join("devenv.yaml"));
388388
cached_cmd.watch_path(self.devenv_root.join("devenv.lock"));
389389
cached_cmd.watch_path(self.devenv_dotfile.join("flake.json"));
390+
cached_cmd.watch_path(self.devenv_dotfile.join("cli-options.nix"));
390391

391-
// Ignore anything in .devenv.
392+
// Ignore anything in .devenv except for the specifically watched files above.
392393
cached_cmd.unwatch_path(&self.devenv_dotfile);
393394

394395
if self.global_options.refresh_eval_cache || options.refresh_cached_output {
@@ -859,7 +860,7 @@ impl Nix {
859860
}
860861
}
861862

862-
fn symlink_force(link_path: &Path, target: &Path) {
863+
fn symlink_force(link_path: &Path, target: &Path) -> Result<()> {
863864
let _lock = dotlock::Dotlock::create(target.with_extension("lock")).unwrap();
864865

865866
debug!(
@@ -869,16 +870,20 @@ fn symlink_force(link_path: &Path, target: &Path) {
869870
);
870871

871872
if target.exists() {
872-
fs::remove_file(target).unwrap_or_else(|_| panic!("Failed to remove {}", target.display()));
873+
fs::remove_file(target)
874+
.map_err(|e| miette::miette!("Failed to remove {}: {}", target.display(), e))?;
873875
}
874876

875-
symlink(link_path, target).unwrap_or_else(|_| {
876-
panic!(
877-
"Failed to create symlink: {} -> {}",
877+
symlink(link_path, target).map_err(|e| {
878+
miette::miette!(
879+
"Failed to create symlink: {} -> {}: {}",
878880
link_path.display(),
879-
target.display()
881+
target.display(),
882+
e
880883
)
881-
});
884+
})?;
885+
886+
Ok(())
882887
}
883888

884889
fn get_now_with_nanoseconds() -> String {

devenv/src/devenv.rs

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ impl Devenv {
160160

161161
let path = PROJECT_DIR
162162
.get_file(filename)
163-
.unwrap_or_else(|| panic!("missing {} in the executable", filename));
163+
.ok_or_else(|| miette::miette!("missing {} in the executable", filename))?;
164164

165165
// write path.contents to target/filename
166166
let target_path = target.join(filename);
@@ -862,8 +862,9 @@ impl Devenv {
862862
"});
863863
}
864864

865-
fs::create_dir_all(&self.devenv_dot_gc)
866-
.unwrap_or_else(|_| panic!("Failed to create {}", self.devenv_dot_gc.display()));
865+
fs::create_dir_all(&self.devenv_dot_gc).map_err(|e| {
866+
miette::miette!("Failed to create {}: {}", self.devenv_dot_gc.display(), e)
867+
})?;
867868

868869
// Initialise any Nix state
869870
self.nix.assemble().await?;
@@ -897,8 +898,55 @@ impl Devenv {
897898
)
898899
.expect("Failed to write imports.txt");
899900

900-
fs::create_dir_all(&self.devenv_runtime)
901-
.unwrap_or_else(|_| panic!("Failed to create {}", self.devenv_runtime.display()));
901+
fs::create_dir_all(&self.devenv_runtime).map_err(|e| {
902+
miette::miette!("Failed to create {}: {}", self.devenv_runtime.display(), e)
903+
})?;
904+
905+
// Create cli-options.nix if there are CLI options
906+
if !self.global_options.option.is_empty() {
907+
let mut cli_options = String::from("{\n");
908+
909+
const SUPPORTED_TYPES: &[&str] = &["string", "int", "float", "bool", "path"];
910+
911+
for chunk in self.global_options.option.chunks_exact(2) {
912+
// Parse the path and type from the first value
913+
let key_parts: Vec<&str> = chunk[0].split(':').collect();
914+
if key_parts.len() < 2 {
915+
miette::bail!("Invalid option format: '{}'. Must include type, e.g. 'languages.rust.version:string'. Supported types: {}",
916+
chunk[0], SUPPORTED_TYPES.join(", "));
917+
}
918+
919+
let path = key_parts[0];
920+
let type_name = key_parts[1];
921+
922+
// Format value based on type
923+
let value = match type_name {
924+
"string" => format!("\"{}\"", &chunk[1]),
925+
"int" => chunk[1].clone(),
926+
"float" => chunk[1].clone(),
927+
"bool" => chunk[1].clone(), // true/false will work directly in Nix
928+
"path" => format!("./{}", &chunk[1]), // relative path
929+
_ => miette::bail!(
930+
"Unsupported type: '{}'. Supported types: {}",
931+
type_name,
932+
SUPPORTED_TYPES.join(", ")
933+
),
934+
};
935+
936+
cli_options.push_str(&format!(" {} = {};\n", path, value));
937+
}
938+
939+
cli_options.push_str("}\n");
940+
941+
fs::write(self.devenv_dotfile.join("cli-options.nix"), cli_options)
942+
.expect("Failed to write cli-options.nix");
943+
} else {
944+
// Remove the file if it exists but there are no CLI options
945+
let cli_options_path = self.devenv_dotfile.join("cli-options.nix");
946+
if cli_options_path.exists() {
947+
fs::remove_file(&cli_options_path).expect("Failed to remove cli-options.nix");
948+
}
949+
}
902950

903951
// create flake.devenv.nix
904952
let vars = indoc::formatdoc!(

devenv/src/flake.tmpl.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
./devenv.nix
9292
(devenv.devenv or { })
9393
(if builtins.pathExists ./devenv.local.nix then ./devenv.local.nix else { })
94+
(if builtins.pathExists (devenv_dotfile + "/cli-options.nix") then import (devenv_dotfile + "/cli-options.nix") else { })
9495
];
9596
};
9697
config = project.config;

docs/basics.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,16 @@ jq-1.6
3636
hello
3737
```
3838

39-
40-
41-
42-
See [Nix language tutorial](https://nix.dev/tutorials/first-steps/nix-language) for a 1-2 hour deep dive
39+
See [Nix language tutorial](https://nix.dev/tutorials/first-steps/nix-language) for a 1-2 hour deep dive
4340
that will allow you to read any Nix file.
4441

45-
!!! note
46-
47-
We're running [a fundraiser to improve the developer experience around error messages](https://opencollective.com/nix-errors-enhancement), with the goal of lowering the barrier to learning Nix.
4842

4943
## Environment Summary
5044

5145
If you'd like to print the summary of the current environment:
5246

5347
```shell-session
54-
$ devenv info
48+
$ devenv info
5549
...
5650
5751
# env
@@ -68,3 +62,23 @@ $ devenv info
6862
# processes
6963
7064
```
65+
66+
## CLI Options Overrides
67+
68+
!!! info "New in 1.6"
69+
70+
You can override configuration options temporarily using the `--option` flag:
71+
72+
```shell-session
73+
$ devenv shell --option env.GREET:string Hello --option languages.rust.enable:bool true
74+
```
75+
76+
The option requires you to specify the inferred Nix type:
77+
78+
- `:string` for string values
79+
- `:int` for integer values
80+
- `:float` for floating-point values
81+
- `:bool` for boolean values (true/false)
82+
- `:path` for file paths (interpreted as relative paths)
83+
84+
This is useful for temporarily changing the configuration without modifying your `devenv.nix` file, such as when testing different configurations or creating option matrices.

tests/cli-options/.test.sh

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env bash
2+
set -eu -o pipefail
3+
4+
# Test CLI options feature
5+
cd "$(dirname "$0")"
6+
7+
# Test basic string and bool types
8+
OUTPUT=$(devenv --option languages.rust.channel:string beta --option services.redis.enable:bool true info)
9+
10+
# Check for expected RUST_VERSION
11+
echo "$OUTPUT" | grep -E "RUST_VERSION:.*beta" || {
12+
echo "ERROR: Expected CLI option override for RUST_VERSION to be 'beta'"
13+
echo $OUTPUT
14+
exit 1
15+
}
16+
17+
# Check for expected REDIS_ENABLED
18+
echo "$OUTPUT" | grep -E "REDIS_ENABLED:.*1" || {
19+
echo "ERROR: Expected CLI option override for REDIS_ENABLED to be '1'"
20+
echo $OUTPUT
21+
exit 1
22+
}
23+
24+
# Test int type
25+
OUTPUT=$(devenv --option env.TEST_INT:int 42 info)
26+
echo "$OUTPUT" | grep -E "TEST_INT:.*42" || {
27+
echo "ERROR: Expected CLI option override for TEST_INT to be '42'"
28+
echo $OUTPUT
29+
exit 1
30+
}
31+
32+
# Test float type
33+
OUTPUT=$(devenv --option env.TEST_FLOAT:float 3.14 info)
34+
echo "$OUTPUT" | grep -E "TEST_FLOAT:.*3.14" || {
35+
echo "ERROR: Expected CLI option override for TEST_FLOAT to be '3.14'"
36+
echo $OUTPUT
37+
exit 1
38+
}
39+
40+
# Test path type
41+
OUTPUT=$(devenv --option env.TEST_PATH:path somepath info)
42+
echo "$OUTPUT" | grep -E "TEST_PATH:.*/somepath" || {
43+
echo "ERROR: Expected CLI option override for TEST_PATH to include 'somepath'"
44+
echo $OUTPUT
45+
exit 1
46+
}
47+
48+
# Test invalid type (should fail)
49+
if devenv --option languages.rust.version:invalid value info &> /dev/null; then
50+
echo "ERROR: Expected CLI option with invalid type to fail"
51+
echo $OUTPUT
52+
exit 1
53+
fi
54+
55+
# Test missing type (should fail)
56+
if devenv --option languages.rust.version value info &> /dev/null; then
57+
echo "ERROR: Expected CLI option without type specification to fail"
58+
echo $OUTPUT
59+
exit 1
60+
fi

tests/cli-options/devenv.nix

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{ pkgs, lib, config, ... }:
2+
3+
{
4+
languages.rust.enable = true;
5+
languages.rust.channel = lib.mkDefault "stable";
6+
7+
env = {
8+
RUST_VERSION = config.languages.rust.channel;
9+
REDIS_ENABLED = builtins.toString config.services.redis.enable;
10+
};
11+
}

tests/cli-options/devenv.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
inputs:
2+
fenix:
3+
url: github:nix-community/fenix
4+
inputs:
5+
nixpkgs:
6+
follows: nixpkgs

0 commit comments

Comments
 (0)