Skip to content

Commit 4e44ec1

Browse files
committed
fix(command): resolve relative PATH entries against cwd
1 parent a7fee4e commit 4e44ec1

6 files changed

Lines changed: 148 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "exec-relative-path-cwd",
3+
"workspaces": [
4+
"packages/*"
5+
]
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"name": "app"
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const fs = require('fs');
2+
3+
fs.mkdirSync('packages/app/tools', { recursive: true });
4+
fs.writeFileSync(
5+
'packages/app/tools/fake-node',
6+
'#!/usr/bin/env node\nconsole.log("resolved from package cwd");\n',
7+
{ mode: 0o755 },
8+
);
9+
fs.writeFileSync('packages/app/tools/fake-node.cmd', '@node "%~dp0\\fake-node" %*\n');
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[[case]]
2+
name = "command_exec_relative_path_cwd"
3+
vp = "local"
4+
skip-platforms = ["windows"]
5+
comment = "A relative PATH entry must resolve against the selected package cwd, not the vp process cwd."
6+
steps = [
7+
{ argv = ["node", "setup.js"], snapshot = false, continue-on-failure = true },
8+
{ argv = ["vp", "exec", "--filter", "app", "--", "fake-node"], envs = [["PATH", "./tools:${PATH}"]], comment = "relative PATH entry resolves from the selected package", continue-on-failure = true },
9+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# command_exec_relative_path_cwd
2+
3+
A relative PATH entry must resolve against the selected package cwd, not the vp process cwd.
4+
5+
## `node setup.js`
6+
7+
8+
## `PATH=./tools:${PATH} vp exec --filter app -- fake-node`
9+
10+
relative PATH entry resolves from the selected package
11+
12+
```
13+
resolved from package cwd
14+
```

crates/vp_command/src/lib.rs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ use vt_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf};
2121

2222
mod ps1_shim;
2323

24+
fn normalize_path_env(
25+
path_env: &OsStr,
26+
cwd: &AbsolutePath,
27+
) -> Result<OsString, std::env::JoinPathsError> {
28+
std::env::join_paths(std::env::split_paths(path_env).map(|path| {
29+
if path.is_absolute() || path.starts_with("~") { path } else { cwd.as_path().join(path) }
30+
}))
31+
}
32+
2433
/// Result of running a command with fspy tracking.
2534
#[derive(Debug)]
2635
pub struct FspyCommandResult {
@@ -39,15 +48,22 @@ pub fn resolve_bin(
3948
path_env: Option<&OsStr>,
4049
cwd: impl AsRef<AbsolutePath>,
4150
) -> Result<AbsolutePathBuf, Error> {
51+
let cwd = cwd.as_ref();
4252
let current_path;
4353
let path_env = if let Some(p) = path_env {
4454
p
4555
} else {
4656
current_path = std::env::var_os("PATH").unwrap_or_default();
4757
&current_path
4858
};
49-
let path = which::which_in(bin_name, Some(path_env), cwd.as_ref())
59+
// `which` resolves relative PATH entries against the process cwd instead of the supplied
60+
// command cwd. Commands are spawned with `cwd`, so resolve the entries the same way first;
61+
// leave `~` entries for `which` to expand against the user's home directory.
62+
let path_env = normalize_path_env(path_env, cwd)
5063
.map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?;
64+
let path = which::which_in(bin_name, Some(&path_env), cwd)
65+
.map_err(|_| Error::CannotFindBinaryPath(bin_name.into()))?;
66+
let path = if path.is_absolute() { path } else { cwd.as_path().join(path) };
5167
AbsolutePathBuf::new(path).ok_or_else(|| Error::CannotFindBinaryPath(bin_name.into()))
5268
}
5369

@@ -399,6 +415,96 @@ mod tests {
399415
tempdir().expect("Failed to create temp directory")
400416
}
401417

418+
#[cfg(unix)]
419+
fn create_executable(path: &std::path::Path) {
420+
use std::{fs, os::unix::fs::PermissionsExt};
421+
422+
fs::create_dir_all(path.parent().unwrap()).unwrap();
423+
fs::write(path, "#!/bin/sh\nexit 0\n").unwrap();
424+
let mut permissions = fs::metadata(path).unwrap().permissions();
425+
permissions.set_mode(0o755);
426+
fs::set_permissions(path, permissions).unwrap();
427+
}
428+
429+
#[cfg(unix)]
430+
#[test]
431+
fn test_resolve_bin_with_relative_path_entry() {
432+
use std::path::PathBuf;
433+
434+
let temp_dir = create_temp_dir();
435+
let cwd_path = temp_dir.path().canonicalize().unwrap();
436+
let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap();
437+
let bin_dir = cwd_path.join("node_modules/.bin");
438+
let bin_path = bin_dir.join("fake-node");
439+
let fallback_bin_dir = cwd_path.join("fallback-bin");
440+
let fallback_bin_path = fallback_bin_dir.join("fake-node");
441+
442+
create_executable(&bin_path);
443+
create_executable(&fallback_bin_path);
444+
445+
let path_env =
446+
std::env::join_paths([PathBuf::from("./node_modules/.bin"), fallback_bin_dir]).unwrap();
447+
let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap();
448+
449+
assert_eq!(resolved.into_path_buf(), bin_path);
450+
}
451+
452+
#[cfg(unix)]
453+
#[test]
454+
fn test_resolve_bin_continues_after_missing_relative_path_entry() {
455+
use std::path::PathBuf;
456+
457+
let temp_dir = create_temp_dir();
458+
let cwd_path = temp_dir.path().canonicalize().unwrap();
459+
let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap();
460+
let fallback_bin_dir = cwd_path.join("fallback-bin");
461+
let fallback_bin_path = fallback_bin_dir.join("fake-node");
462+
463+
create_executable(&fallback_bin_path);
464+
465+
let path_env =
466+
std::env::join_paths([PathBuf::from("./missing-bin"), fallback_bin_dir]).unwrap();
467+
let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap();
468+
469+
assert_eq!(resolved.into_path_buf(), fallback_bin_path);
470+
}
471+
472+
#[cfg(unix)]
473+
#[test]
474+
fn test_resolve_bin_with_empty_path_entry() {
475+
use std::path::PathBuf;
476+
477+
let temp_dir = create_temp_dir();
478+
let cwd_path = temp_dir.path().canonicalize().unwrap();
479+
let cwd = AbsolutePathBuf::new(cwd_path.clone()).unwrap();
480+
let bin_path = cwd_path.join("fake-node");
481+
482+
create_executable(&bin_path);
483+
484+
let path_env = std::env::join_paths([PathBuf::new()]).unwrap();
485+
let resolved = resolve_bin("fake-node", Some(&path_env), &cwd).unwrap();
486+
487+
assert_eq!(resolved.into_path_buf(), bin_path);
488+
}
489+
490+
#[cfg(unix)]
491+
#[test]
492+
fn test_normalize_path_env_preserves_tilde_entry() {
493+
use std::path::PathBuf;
494+
495+
let temp_dir = create_temp_dir();
496+
let cwd_path = temp_dir.path().canonicalize().unwrap();
497+
let cwd = AbsolutePathBuf::new(cwd_path).unwrap();
498+
let path_env = std::env::join_paths([PathBuf::from("~/bin")]).unwrap();
499+
500+
let normalized = normalize_path_env(&path_env, &cwd).unwrap();
501+
502+
assert_eq!(
503+
std::env::split_paths(&normalized).collect::<Vec<_>>(),
504+
[PathBuf::from("~/bin")]
505+
);
506+
}
507+
402508
mod run_command_tests {
403509

404510
use super::*;

0 commit comments

Comments
 (0)