From e16ad58bf4e52e40007d75ccedaca7ac449fe0ba Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 11 Apr 2024 09:07:37 +0200 Subject: [PATCH] Find Windows executable (#284) --- common/src/dev-container-cli.ts | 8 +-- common/src/windows.ts | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 common/src/windows.ts diff --git a/common/src/dev-container-cli.ts b/common/src/dev-container-cli.ts index a68a0323..0cc2c920 100644 --- a/common/src/dev-container-cli.ts +++ b/common/src/dev-container-cli.ts @@ -4,6 +4,7 @@ import path from 'path'; import {env} from 'process'; import {promisify} from 'util'; import {ExecFunction} from './exec'; +import {findWindowsExecutable} from './windows'; const cliVersion = "0"; // Use 'latest' to get latest CLI version, or pin to specific version e.g. '0.14.1' if required @@ -27,7 +28,8 @@ function getSpecCliInfo() { async function isCliInstalled(exec: ExecFunction): Promise { try { - const {exitCode} = await exec(getSpecCliInfo().command, ['--help'], { + const command = await findWindowsExecutable(getSpecCliInfo().command); + const {exitCode} = await exec(command, ['--help'], { silent: true, }); return exitCode === 0; @@ -121,7 +123,7 @@ async function runSpecCliJsonCommand(options: { err: data => options.log(data), env: options.env ? {...process.env, ...options.env} : process.env, }; - const command = getSpecCliInfo().command; + const command = await findWindowsExecutable(getSpecCliInfo().command); console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info await spawn(command, options.args, spawnOptions); @@ -138,7 +140,7 @@ async function runSpecCliNonJsonCommand(options: { err: data => options.log(data), env: options.env ? {...process.env, ...options.env} : process.env, }; - const command = getSpecCliInfo().command; + const command = await findWindowsExecutable(getSpecCliInfo().command); console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info const result = await spawn(command, options.args, spawnOptions); return result.code diff --git a/common/src/windows.ts b/common/src/windows.ts new file mode 100644 index 00000000..21063bf4 --- /dev/null +++ b/common/src/windows.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as fs from 'fs'; + +// From Dev Containers CLI +export async function findWindowsExecutable(command: string): Promise { + if (process.platform !== 'win32') { + return command; + } + + // If we have an absolute path then we take it. + if (path.isAbsolute(command)) { + return await findWindowsExecutableWithExtension(command) || command; + } + const cwd = process.cwd(); + if (/[/\\]/.test(command)) { + // We have a directory and the directory is relative (see above). Make the path absolute + // to the current working directory. + const fullPath = path.join(cwd, command); + return await findWindowsExecutableWithExtension(fullPath) || fullPath; + } + let pathValue: string | undefined = undefined; + let paths: string[] | undefined = undefined; + const env = process.env; + // Path can be named in many different ways and for the execution it doesn't matter + for (let key of Object.keys(env)) { + if (key.toLowerCase() === 'path') { + const value = env[key]; + if (typeof value === 'string') { + pathValue = value; + paths = value.split(path.delimiter) + .filter(Boolean); + } + break; + } + } + // No PATH environment. Bail out. + if (paths === void 0 || paths.length === 0) { + const err = new Error(`No PATH to look up executable '${command}'.`); + (err as any).code = 'ENOENT'; + throw err; + } + // We have a simple file name. We get the path variable from the env + // and try to find the executable on the path. + for (let pathEntry of paths) { + // The path entry is absolute. + let fullPath: string; + if (path.isAbsolute(pathEntry)) { + fullPath = path.join(pathEntry, command); + } else { + fullPath = path.join(cwd, pathEntry, command); + } + const withExtension = await findWindowsExecutableWithExtension(fullPath); + if (withExtension) { + return withExtension; + } + } + // Not found in PATH. Bail out. + const err = new Error(`Exectuable '${command}' not found on PATH '${pathValue}'.`); + (err as any).code = 'ENOENT'; + throw err; +} + +const pathext = process.env.PATHEXT; +const executableExtensions = pathext ? pathext.toLowerCase().split(';') : ['.com', '.exe', '.bat', '.cmd']; + +async function findWindowsExecutableWithExtension(fullPath: string) { + if (executableExtensions.indexOf(path.extname(fullPath)) !== -1) { + return await isFile(fullPath) ? fullPath : undefined; + } + for (const ext of executableExtensions) { + const withExtension = fullPath + ext; + if (await isFile(withExtension)) { + return withExtension; + } + } + return undefined; +} + +function isFile(filepath: string): Promise { + return new Promise(r => fs.stat(filepath, (err, stat) => r(!err && stat.isFile()))); +}