Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 38 additions & 1 deletion devenv-core/src/paths.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,3 +13,40 @@ pub struct DevenvPaths {
pub state: Option<PathBuf>,
pub git_root: Option<PathBuf>,
}

/// 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<PathBuf> {
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());
}
}
33 changes: 33 additions & 0 deletions devenv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ struct UiOptions {
log_level: devenv_tracing::Level,
tracing_specs: Vec<TraceOutputSpec>,
verbosity: VerbosityLevel,
discovered_root: Option<PathBuf>,
}

/// Options for the backend thread: resolved devenv config plus what to run.
Expand Down Expand Up @@ -208,6 +209,33 @@ impl TestDirs {
fn resolve(cli: Cli, shutdown: Arc<Shutdown>) -> 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);
Expand Down Expand Up @@ -244,6 +272,7 @@ fn resolve(cli: Cli, shutdown: Arc<Shutdown>) -> Result<(UiOptions, BackendOptio
log_level,
tracing_specs,
verbosity,
discovered_root,
};

// Backend options. Read before the `From` conversions consume `cli.nix_args`.
Expand Down Expand Up @@ -471,6 +500,10 @@ fn run(ui: UiOptions, backend: BackendOptions, shutdown: Arc<Shutdown>) -> 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;

Expand Down
1 change: 1 addition & 0 deletions tests/cli-subdir-discovery/.test-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use_shell: false
24 changes: 24 additions & 0 deletions tests/cli-subdir-discovery/.test.sh
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tests/cli-subdir-discovery/devenv.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ pkgs, ... }: {
packages = [ pkgs.hello ];
}
Loading