Skip to content

Commit 4432f30

Browse files
committed
Find Windows executable (#284)
1 parent ebe4531 commit 4432f30

File tree

2 files changed

+91
-3
lines changed

2 files changed

+91
-3
lines changed

common/src/dev-container-cli.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'path';
44
import {env} from 'process';
55
import {promisify} from 'util';
66
import {ExecFunction} from './exec';
7+
import {findWindowsExecutable} from './windows';
78

89
const cliVersion = "0"; // Use 'latest' to get latest CLI version, or pin to specific version e.g. '0.14.1' if required
910

@@ -27,7 +28,8 @@ function getSpecCliInfo() {
2728

2829
async function isCliInstalled(exec: ExecFunction): Promise<boolean> {
2930
try {
30-
const {exitCode} = await exec(getSpecCliInfo().command, ['--help'], {
31+
const command = await findWindowsExecutable(getSpecCliInfo().command);
32+
const {exitCode} = await exec(command, ['--help'], {
3133
silent: true,
3234
});
3335
return exitCode === 0;
@@ -121,7 +123,7 @@ async function runSpecCliJsonCommand<T>(options: {
121123
err: data => options.log(data),
122124
env: options.env ? {...process.env, ...options.env} : process.env,
123125
};
124-
const command = getSpecCliInfo().command;
126+
const command = await findWindowsExecutable(getSpecCliInfo().command);
125127
console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info
126128
await spawn(command, options.args, spawnOptions);
127129

@@ -138,7 +140,7 @@ async function runSpecCliNonJsonCommand(options: {
138140
err: data => options.log(data),
139141
env: options.env ? {...process.env, ...options.env} : process.env,
140142
};
141-
const command = getSpecCliInfo().command;
143+
const command = await findWindowsExecutable(getSpecCliInfo().command);
142144
console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info
143145
const result = await spawn(command, options.args, spawnOptions);
144146
return result.code

common/src/windows.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as fs from 'fs';
8+
9+
// From Dev Containers CLI
10+
export async function findWindowsExecutable(command: string): Promise<string> {
11+
if (process.platform !== 'win32') {
12+
return command;
13+
}
14+
15+
// If we have an absolute path then we take it.
16+
if (path.isAbsolute(command)) {
17+
return await findWindowsExecutableWithExtension(command) || command;
18+
}
19+
const cwd = process.cwd();
20+
if (/[/\\]/.test(command)) {
21+
// We have a directory and the directory is relative (see above). Make the path absolute
22+
// to the current working directory.
23+
const fullPath = path.join(cwd, command);
24+
return await findWindowsExecutableWithExtension(fullPath) || fullPath;
25+
}
26+
let pathValue: string | undefined = undefined;
27+
let paths: string[] | undefined = undefined;
28+
const env = process.env;
29+
// Path can be named in many different ways and for the execution it doesn't matter
30+
for (let key of Object.keys(env)) {
31+
if (key.toLowerCase() === 'path') {
32+
const value = env[key];
33+
if (typeof value === 'string') {
34+
pathValue = value;
35+
paths = value.split(path.delimiter)
36+
.filter(Boolean);
37+
}
38+
break;
39+
}
40+
}
41+
// No PATH environment. Bail out.
42+
if (paths === void 0 || paths.length === 0) {
43+
const err = new Error(`No PATH to look up executable '${command}'.`);
44+
(err as any).code = 'ENOENT';
45+
throw err;
46+
}
47+
// We have a simple file name. We get the path variable from the env
48+
// and try to find the executable on the path.
49+
for (let pathEntry of paths) {
50+
// The path entry is absolute.
51+
let fullPath: string;
52+
if (path.isAbsolute(pathEntry)) {
53+
fullPath = path.join(pathEntry, command);
54+
} else {
55+
fullPath = path.join(cwd, pathEntry, command);
56+
}
57+
const withExtension = await findWindowsExecutableWithExtension(fullPath);
58+
if (withExtension) {
59+
return withExtension;
60+
}
61+
}
62+
// Not found in PATH. Bail out.
63+
const err = new Error(`Exectuable '${command}' not found on PATH '${pathValue}'.`);
64+
(err as any).code = 'ENOENT';
65+
throw err;
66+
}
67+
68+
const pathext = process.env.PATHEXT;
69+
const executableExtensions = pathext ? pathext.toLowerCase().split(';') : ['.com', '.exe', '.bat', '.cmd'];
70+
71+
async function findWindowsExecutableWithExtension(fullPath: string) {
72+
if (executableExtensions.indexOf(path.extname(fullPath)) !== -1) {
73+
return await isFile(fullPath) ? fullPath : undefined;
74+
}
75+
for (const ext of executableExtensions) {
76+
const withExtension = fullPath + ext;
77+
if (await isFile(withExtension)) {
78+
return withExtension;
79+
}
80+
}
81+
return undefined;
82+
}
83+
84+
function isFile(filepath: string): Promise<boolean> {
85+
return new Promise(r => fs.stat(filepath, (err, stat) => r(!err && stat.isFile())));
86+
}

0 commit comments

Comments
 (0)