diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eeefbe51..22e72939f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ ### Improvements - `devenv shell` now registers zsh completions from packages in the devenv profile. A generated `.zshenv` prepends `$DEVENV_PROFILE/share/zsh/site-functions` to `fpath` before `/etc/zshrc` runs, so the system `compinit` picks up the new directory. +- `devenv` now walks up parent directories to find `devenv.nix`, so commands like `devenv shell` work from any subdirectory of a project ([#2232](https://github.com/cachix/devenv/issues/2232)). - Bumped `secretspec` to `v0.10.1`. The new `bws` (Bitwarden Secrets Manager) feature is not enabled because its transitive `bitwarden` crate conflicts with `sqlx` 0.8 on `libsqlite3-sys` and pins `typenum` to 1.18. - Bumped `iocraft` to `0.8.2` and switched the `[patch.crates-io]` entry from the `cachix/iocraft` fork to upstream `ccbrown/iocraft` `main`, now that the row-level diff and stderr rendering patches are merged upstream. - `devenv.yaml` options are now documented in `snake_case` (e.g. `allow_unfree`, `clean_env`). The previous `camelCase` spellings remain supported for backward compatibility. diff --git a/devenv-core/src/paths.rs b/devenv-core/src/paths.rs index dd1d0b52a..151117afd 100644 --- a/devenv-core/src/paths.rs +++ b/devenv-core/src/paths.rs @@ -1,6 +1,6 @@ //! On-disk layout for a devenv project. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] pub struct DevenvPaths { @@ -13,3 +13,40 @@ pub struct DevenvPaths { pub state: Option, pub git_root: Option, } + +/// Walk up from `start` looking for a directory containing `devenv.nix`. +/// Returns the first ancestor (including `start` itself) that contains it, +/// or `None` if none is found before reaching the filesystem root. +pub fn find_project_root(start: &Path) -> Option { + start + .ancestors() + .find(|d| d.join("devenv.nix").exists()) + .map(PathBuf::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn finds_marker_in_start_dir() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("devenv.nix"), "").unwrap(); + assert_eq!(find_project_root(tmp.path()).as_deref(), Some(tmp.path())); + } + + #[test] + fn walks_up_to_parent() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("devenv.nix"), "").unwrap(); + let nested = tmp.path().join("a/b/c"); + std::fs::create_dir_all(&nested).unwrap(); + assert_eq!(find_project_root(&nested).as_deref(), Some(tmp.path())); + } + + #[test] + fn returns_none_when_no_marker() { + let tmp = tempfile::tempdir().unwrap(); + assert!(find_project_root(tmp.path()).is_none()); + } +} diff --git a/devenv/src/main.rs b/devenv/src/main.rs index 07b111f0a..6bb3e5c5e 100644 --- a/devenv/src/main.rs +++ b/devenv/src/main.rs @@ -117,6 +117,7 @@ struct UiOptions { log_level: devenv_tracing::Level, tracing_specs: Vec, verbosity: VerbosityLevel, + discovered_root: Option, } /// Options for the backend thread: resolved devenv config plus what to run. @@ -208,6 +209,33 @@ impl TestDirs { fn resolve(cli: Cli, shutdown: Arc) -> Result<(UiOptions, BackendOptions)> { let command = cli.command; + // Walk up parent directories to find devenv.nix. Skip when the user has + // explicitly chosen a source (`--from`) or is constructing a project via + // module-option overrides (`-O`). Has to run before Config::load() reads + // "./devenv.yaml". + let discovered_root = + if cli.from.is_none() && cli.input_overrides.nix_module_options.is_empty() { + env::current_dir() + .ok() + .and_then(|cwd| devenv_core::paths::find_project_root(&cwd).filter(|r| r != &cwd)) + } else { + None + }; + if let Some(root) = &discovered_root { + env::set_current_dir(root) + .into_diagnostic() + .wrap_err_with(|| { + format!( + "Failed to chdir to discovered project root: {}", + root.display() + ) + })?; + // Safety: resolve() runs single-threaded before tokio starts. + unsafe { + env::set_var("PWD", root); + } + } + // UI options: verbosity, log level, tracing, TUI. Pure CLI + env, no config. let verbosity = resolve_verbosity(&cli.cli_options); let quiet = matches!(verbosity, VerbosityLevel::Quiet); @@ -244,6 +272,7 @@ fn resolve(cli: Cli, shutdown: Arc) -> Result<(UiOptions, BackendOptio log_level, tracing_specs, verbosity, + discovered_root, }; // Backend options. Read before the `From` conversions consume `cli.nix_args`. @@ -471,6 +500,10 @@ fn run(ui: UiOptions, backend: BackendOptions, shutdown: Arc) -> Resul let _tracing_guard = devenv_tracing::init_tracing(ui.log_level, &ui.tracing_specs); + if let Some(root) = &ui.discovered_root { + tracing::info!("Discovered devenv.nix in {}", root.display()); + } + let tui = ui.tui; let verbosity = ui.verbosity; diff --git a/tests/cli-subdir-discovery/.test-config.yml b/tests/cli-subdir-discovery/.test-config.yml new file mode 100644 index 000000000..13c16cea3 --- /dev/null +++ b/tests/cli-subdir-discovery/.test-config.yml @@ -0,0 +1 @@ +use_shell: false diff --git a/tests/cli-subdir-discovery/.test.sh b/tests/cli-subdir-discovery/.test.sh new file mode 100755 index 000000000..5447c09d0 --- /dev/null +++ b/tests/cli-subdir-discovery/.test.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -xe +set -o pipefail + +proj_root="$(pwd)" +mkdir -p sub/nested + +# Discovery from one level deep +(cd sub && devenv print-paths | grep -Fxq "DEVENV_ROOT=\"$proj_root\"") + +# Discovery from two levels deep +(cd sub/nested && devenv print-paths | grep -Fxq "DEVENV_ROOT=\"$proj_root\"") + +# Negative case: outside any project, original error preserved +outside="$(mktemp -d)" +trap 'rm -rf "$outside"' EXIT +output="$(cd "$outside" && devenv print-paths 2>&1 || true)" +if echo "$output" | grep -q "devenv.nix does not exist"; then + echo "✓ negative case: error preserved outside any project" +else + echo "expected 'devenv.nix does not exist' outside any project, got:" + echo "$output" + exit 1 +fi diff --git a/tests/cli-subdir-discovery/devenv.nix b/tests/cli-subdir-discovery/devenv.nix new file mode 100644 index 000000000..54d65c0af --- /dev/null +++ b/tests/cli-subdir-discovery/devenv.nix @@ -0,0 +1,3 @@ +{ pkgs, ... }: { + packages = [ pkgs.hello ]; +}