Skip to content

Commit 18ea31d

Browse files
committed
Fix issues with install-run on Windows.
1 parent 0b99d96 commit 18ea31d

File tree

3 files changed

+99
-60
lines changed

3 files changed

+99
-60
lines changed

libraries/rush-lib/src/scripts/install-run.ts

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as path from 'node:path';
1111
import type { IPackageJson } from '@rushstack/node-core-library';
1212

1313
import { syncNpmrc, type ILogger } from '../utilities/npmrcUtilities';
14+
import { convertCommandAndArgsToShell } from '../utilities/executionUtilities';
1415
import type { RushConstants } from '../logic/RushConstants';
1516

1617
export const RUSH_JSON_FILENAME: typeof RushConstants.rushJsonFilename = 'rush.json';
@@ -200,15 +201,14 @@ function _resolvePackageVersion(
200201
stdio: []
201202
};
202203
const platformNpmPath: string = _getPlatformPath(npmPath);
203-
const npmVersionSpawnResult: childProcess.SpawnSyncReturns<Buffer | string> = childProcess.spawnSync(
204-
platformNpmPath,
205-
['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'],
206-
spawnSyncOptions
207-
);
208-
209-
if (npmVersionSpawnResult.status !== 0) {
210-
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
211-
}
204+
205+
const npmVersionSpawnResult: childProcess.SpawnSyncReturns<Buffer | string> =
206+
_runAsShellCommandAndConfirmSuccess(
207+
platformNpmPath,
208+
['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'],
209+
spawnSyncOptions,
210+
'npm view'
211+
);
212212

213213
const npmViewVersionOutput: string = npmVersionSpawnResult.stdout.toString();
214214
const parsedVersionOutput: string | string[] = JSON.parse(npmViewVersionOutput);
@@ -355,22 +355,21 @@ function _installPackage(
355355
packageInstallFolder: string,
356356
name: string,
357357
version: string,
358-
command: 'install' | 'ci'
358+
npmCommand: 'install' | 'ci'
359359
): void {
360360
try {
361361
logger.info(`Installing ${name}...`);
362362
const npmPath: string = getNpmPath();
363-
const platformNpmPath: string = _getPlatformPath(npmPath);
364-
const result: childProcess.SpawnSyncReturns<Buffer> = childProcess.spawnSync(platformNpmPath, [command], {
365-
stdio: 'inherit',
366-
cwd: packageInstallFolder,
367-
env: process.env
368-
});
369-
370-
if (result.status !== 0) {
371-
throw new Error(`"npm ${command}" encountered an error`);
372-
}
373-
363+
_runAsShellCommandAndConfirmSuccess(
364+
npmPath,
365+
[npmCommand],
366+
{
367+
stdio: 'inherit',
368+
cwd: packageInstallFolder,
369+
env: process.env
370+
},
371+
`npm ${npmCommand}`
372+
);
374373
logger.info(`Successfully installed ${name}@${version}`);
375374
} catch (e) {
376375
throw new Error(`Unable to install package: ${e}`);
@@ -409,6 +408,40 @@ function _writeFlagFile(packageInstallFolder: string): void {
409408
}
410409
}
411410

411+
/**
412+
* Run the specified command under the platform's shell and throw if it didn't succeed.
413+
*/
414+
function _runAsShellCommandAndConfirmSuccess(
415+
command: string,
416+
args: string[],
417+
options: childProcess.SpawnSyncOptions,
418+
commandNameForLogging: string
419+
): childProcess.SpawnSyncReturns<string | Buffer<ArrayBufferLike>> {
420+
if (_isWindows()) {
421+
({ command, args } = convertCommandAndArgsToShell({ command, args }));
422+
}
423+
424+
const result: childProcess.SpawnSyncReturns<string | Buffer<ArrayBufferLike>> = childProcess.spawnSync(
425+
command,
426+
args,
427+
options
428+
);
429+
430+
if (result.status !== 0) {
431+
if (result.status === undefined) {
432+
if (result.error) {
433+
throw new Error(`"${commandNameForLogging}" failed: ${result.error.message.toString()}`);
434+
} else {
435+
throw new Error(`"${commandNameForLogging}" failed for an unknown reason`);
436+
}
437+
} else {
438+
throw new Error(`"${commandNameForLogging}" returned error code ${result.status}`);
439+
}
440+
}
441+
442+
return result;
443+
}
444+
412445
export function installAndRun(
413446
logger: ILogger,
414447
packageName: string,
@@ -456,12 +489,14 @@ export function installAndRun(
456489
const originalEnvPath: string = process.env.PATH || '';
457490
let result: childProcess.SpawnSyncReturns<Buffer>;
458491
try {
459-
// `npm` bin stubs on Windows are `.cmd` files
460-
// Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true`
461-
const platformBinPath: string = _getPlatformPath(binPath);
492+
let command: string = binPath;
493+
let args: string[] = packageBinArgs;
494+
if (_isWindows()) {
495+
({ command, args } = convertCommandAndArgsToShell({ command, args }));
496+
}
462497

463498
process.env.PATH = [binFolderPath, originalEnvPath].join(path.delimiter);
464-
result = childProcess.spawnSync(platformBinPath, packageBinArgs, {
499+
result = childProcess.spawnSync(command, args, {
465500
stdio: 'inherit',
466501
windowsVerbatimArguments: false,
467502
cwd: process.cwd(),
@@ -501,7 +536,8 @@ function _run(): void {
501536
throw new Error('Unexpected exception: could not detect node path');
502537
}
503538

504-
if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') {
539+
const scriptFileName: string = path.basename(scriptPath).toLowerCase();
540+
if (scriptFileName !== 'install-run.js' && scriptFileName !== 'install-run') {
505541
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
506542
// to the script that (presumably) imported this file
507543

libraries/rush-lib/src/utilities/Utilities.ts

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { RushConfiguration } from '../api/RushConfiguration';
2424
import { syncNpmrc } from './npmrcUtilities';
2525
import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration';
2626
import { RushConstants } from '../logic/RushConstants';
27+
import { convertCommandAndArgsToShell } from './executionUtilities';
2728

2829
export type UNINITIALIZED = 'UNINITIALIZED';
2930
// eslint-disable-next-line @typescript-eslint/no-redeclare
@@ -178,11 +179,6 @@ type IExecuteCommandInternalOptions = Omit<IExecuteCommandOptions, 'suppressOutp
178179
captureOutput: boolean;
179180
};
180181

181-
interface ICommandAndArgs {
182-
command: string;
183-
args: string[];
184-
}
185-
186182
export class Utilities {
187183
public static syncNpmrc: typeof syncNpmrc = syncNpmrc;
188184

@@ -691,7 +687,7 @@ export class Utilities {
691687
Object.assign(spawnOptions, SubprocessTerminator.RECOMMENDED_OPTIONS);
692688
}
693689

694-
const { command, args } = Utilities._convertCommandAndArgsToShell(commandAndArgs);
690+
const { command, args } = convertCommandAndArgsToShell(commandAndArgs);
695691
return spawnFunction(command, args, spawnOptions);
696692
}
697693

@@ -832,7 +828,7 @@ export class Utilities {
832828
};
833829

834830
if (shell) {
835-
({ command, args } = Utilities._convertCommandAndArgsToShell({ command, args }));
831+
({ command, args } = convertCommandAndArgsToShell({ command, args }));
836832
}
837833

838834
const childProcess: child_process.ChildProcess = child_process.spawn(command, args, options);
@@ -884,31 +880,4 @@ export class Utilities {
884880
throw new Error(`The command failed with exit code ${status}\n${stderr}`);
885881
}
886882
}
887-
888-
private static _convertCommandAndArgsToShell(command: string): ICommandAndArgs;
889-
private static _convertCommandAndArgsToShell(options: ICommandAndArgs): ICommandAndArgs;
890-
private static _convertCommandAndArgsToShell(options: ICommandAndArgs | string): ICommandAndArgs {
891-
let shellCommand: string;
892-
let commandFlags: string[];
893-
if (process.platform !== 'win32') {
894-
shellCommand = 'sh';
895-
commandFlags = ['-c'];
896-
} else {
897-
shellCommand = process.env.comspec || 'cmd';
898-
commandFlags = ['/d', '/s', '/c'];
899-
}
900-
901-
let commandToRun: string;
902-
if (typeof options === 'string') {
903-
commandToRun = options;
904-
} else {
905-
const { command, args } = options;
906-
commandToRun = [command, ...args].join(' ');
907-
}
908-
909-
return {
910-
command: shellCommand,
911-
args: [...commandFlags, commandToRun]
912-
};
913-
}
914883
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
export interface ICommandAndArgs {
5+
command: string;
6+
args: string[];
7+
}
8+
9+
export function convertCommandAndArgsToShell(command: string): ICommandAndArgs;
10+
export function convertCommandAndArgsToShell(options: ICommandAndArgs): ICommandAndArgs;
11+
export function convertCommandAndArgsToShell(options: ICommandAndArgs | string): ICommandAndArgs {
12+
let shellCommand: string;
13+
let commandFlags: string[];
14+
if (process.platform !== 'win32') {
15+
shellCommand = 'sh';
16+
commandFlags = ['-c'];
17+
} else {
18+
shellCommand = process.env.comspec || 'cmd';
19+
commandFlags = ['/d', '/s', '/c'];
20+
}
21+
22+
let commandToRun: string;
23+
if (typeof options === 'string') {
24+
commandToRun = options;
25+
} else {
26+
const { command, args } = options;
27+
commandToRun = [command, ...args].join(' ');
28+
}
29+
30+
return {
31+
command: shellCommand,
32+
args: [...commandFlags, commandToRun]
33+
};
34+
}

0 commit comments

Comments
 (0)