Skip to content

Commit 7d94382

Browse files
committed
fix(@angular/cli): resolve executables strictly from PATH
Update executable invocation logic to resolve system binaries (such as `git` and `which`) strictly from the `PATH` environment variable. This prevents bare command names passed to `execFileSync` / `execFile` from implicitly searching and resolving binaries relative to `process.cwd()` on Windows. Fixes #33755
1 parent b6ed402 commit 7d94382

4 files changed

Lines changed: 119 additions & 2 deletions

File tree

packages/angular/cli/src/commands/update/utilities/git.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
import { execFileSync } from 'node:child_process';
1010
import * as path from 'node:path';
11+
import { findExecutableOnPath } from '../../../utilities/executable';
12+
13+
let cachedGitPath: string | undefined;
1114

1215
/**
1316
* Execute a git command.
@@ -16,7 +19,15 @@ import * as path from 'node:path';
1619
* @returns The output of the command.
1720
*/
1821
function execGit(args: string[], input?: string): string {
19-
return execFileSync('git', args, { encoding: 'utf8', stdio: 'pipe', input });
22+
if (!cachedGitPath) {
23+
const gitPath = findExecutableOnPath('git');
24+
if (!gitPath) {
25+
throw new Error('Git executable not found on PATH.');
26+
}
27+
cachedGitPath = gitPath;
28+
}
29+
30+
return execFileSync(cachedGitPath, args, { encoding: 'utf8', stdio: 'pipe', input });
2031
}
2132

2233
/**

packages/angular/cli/src/utilities/completion.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getWorkspace } from '../utilities/config';
1616
import { forceAutocomplete } from '../utilities/environment-options';
1717
import { isTTY } from '../utilities/tty';
1818
import { assertIsError } from './error';
19+
import { findExecutableOnPath } from './executable';
1920
import { askConfirmation } from './prompt';
2021

2122
/** Interface for the autocompletion configuration stored in the global workspace. */
@@ -271,7 +272,14 @@ function getShellRunCommandCandidates(shell: string, home: string): string[] | u
271272
export function hasGlobalCliInstall(): Promise<boolean> {
272273
// List all binaries with the `ng` name on the user's `$PATH`.
273274
return new Promise<boolean>((resolve) => {
274-
execFile('which', ['-a', 'ng'], (error, stdout) => {
275+
const whichPath = findExecutableOnPath('which');
276+
if (!whichPath) {
277+
resolve(false);
278+
279+
return;
280+
}
281+
282+
execFile(whichPath, ['-a', 'ng'], (error, stdout) => {
275283
if (error) {
276284
// No instances of `ng` on the user's `$PATH`
277285

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { existsSync } from 'node:fs';
10+
import { delimiter, extname, join } from 'node:path';
11+
12+
/**
13+
* Searches the `PATH` environment variable for a given executable binary name.
14+
* On Windows, checks extensions in `PATHEXT` (e.g. `.exe`, `.cmd`) if no extension is given.
15+
* Returns the absolute path of the binary if found on `PATH`, or `undefined` if not found.
16+
*
17+
* This prevents `execFileSync` / `spawn` from implicitly resolving executables
18+
* relative to `process.cwd()` on Windows when passed bare command names.
19+
*
20+
* @param binaryName Name of the binary to search for (e.g. 'git').
21+
* @returns The absolute path to the binary if found on `PATH`, or `undefined`.
22+
*/
23+
export function findExecutableOnPath(binaryName: string): string | undefined {
24+
const envPath = process.env.PATH || process.env.Path || '';
25+
if (!envPath) {
26+
return undefined;
27+
}
28+
29+
const isWindows = process.platform === 'win32';
30+
const pathExt = process.env.PATHEXT
31+
? process.env.PATHEXT.split(delimiter)
32+
: ['.com', '.exe', '.bat', '.cmd'];
33+
34+
const hasExt = isWindows && extname(binaryName) !== '';
35+
const extensions = isWindows && !hasExt ? pathExt : [''];
36+
37+
for (const rawDir of envPath.split(delimiter)) {
38+
if (!rawDir) {
39+
continue;
40+
}
41+
42+
const dir = rawDir.startsWith('"') && rawDir.endsWith('"') ? rawDir.slice(1, -1) : rawDir;
43+
44+
for (const ext of extensions) {
45+
const candidate = join(dir, binaryName + ext);
46+
try {
47+
if (existsSync(candidate)) {
48+
return candidate;
49+
}
50+
} catch {
51+
// Ignore file system errors (e.g. invalid path or permission error)
52+
}
53+
}
54+
}
55+
56+
return undefined;
57+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { dirname } from 'node:path';
10+
import { findExecutableOnPath } from './executable';
11+
12+
describe('findExecutableOnPath', () => {
13+
it('should find executable on PATH when it exists', () => {
14+
// 'node' binary should be present on PATH in any Node test environment
15+
const nodePath = findExecutableOnPath('node');
16+
expect(nodePath).toBeDefined();
17+
expect(nodePath).toContain('node');
18+
});
19+
20+
it('should return undefined when binary does not exist on PATH', () => {
21+
const nonExistentPath = findExecutableOnPath('non_existent_binary_123456789');
22+
expect(nonExistentPath).toBeUndefined();
23+
});
24+
25+
it('should correctly handle PATH entries wrapped in double quotes', () => {
26+
const nodePath = findExecutableOnPath('node');
27+
if (!nodePath) {
28+
return;
29+
}
30+
31+
const originalPath = process.env.PATH;
32+
try {
33+
const dir = dirname(nodePath);
34+
process.env.PATH = `"${dir}"`;
35+
const resolved = findExecutableOnPath('node');
36+
expect(resolved).toBeDefined();
37+
} finally {
38+
process.env.PATH = originalPath;
39+
}
40+
});
41+
});

0 commit comments

Comments
 (0)