@azure-tools/extension
Version:
Yarn-Based extension aquisition (for Azure Open Source Projects)
475 lines • 21.1 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtensionManager = exports.fetchPackageMetadata = void 0;
const child_process_1 = require("child_process");
const os_1 = require("os");
const path_1 = require("path");
const system_requirements_1 = require("@autorest/system-requirements");
const async_io_1 = require("@azure-tools/async-io");
const tasks_1 = require("@azure-tools/tasks");
const npm_package_arg_1 = require("npm-package-arg");
const pacote = __importStar(require("pacote"));
const semver = __importStar(require("semver"));
const exceptions_1 = require("./exceptions");
const extension_1 = require("./extension");
const async_lock_1 = require("./locks/async-lock");
const logger_1 = require("./logger");
const npm_1 = require("./npm");
const yarn_1 = require("./yarn");
function quoteIfNecessary(text) {
if (text && text.indexOf(" ") > -1 && text.charAt(0) != '"') {
return `"${text}"`;
}
return text;
}
const nodePath = quoteIfNecessary(process.execPath);
function cmdlineToArray(text, result = [], matcher = /[^\s"]+|"([^"]*)"/gi, count = 0) {
text = text.replace(/\\"/g, "\ufffe");
const match = matcher.exec(text);
return match
? cmdlineToArray(text, result, matcher, result.push(match[1] ? match[1].replace(/\ufffe/g, '\\"') : match[0].replace(/\ufffe/g, '\\"')))
: result;
}
function getPathVariableName() {
// windows calls it's path 'Path' usually, but this is not guaranteed.
if (process.platform === "win32") {
let PATH = "Path";
Object.keys(process.env).forEach(function (e) {
if (e.match(/^PATH$/i)) {
PATH = e;
}
});
return PATH;
}
return "PATH";
}
async function realPathWithExtension(command) {
const pathExt = (process.env.pathext || ".EXE").split(";");
for (const each of pathExt) {
const filename = `${command}${each}`;
if (await (0, async_io_1.isFile)(filename)) {
return filename;
}
}
return undefined;
}
async function getFullPath(command, searchPath) {
command = command.replace(/"/g, "");
const ext = (0, path_1.extname)(command);
if ((0, path_1.isAbsolute)(command)) {
// if the file has an extension, or we're not on win32, and this is an actual file, use it.
if (ext || process.platform !== "win32") {
if (await (0, async_io_1.isFile)(command)) {
return command;
}
}
// if we're on windows, look for a file with an acceptable extension.
if (process.platform === "win32") {
// try all the PATHEXT extensions to see if it is a recognized program
const cmd = await realPathWithExtension(command);
if (cmd) {
return cmd;
}
}
return undefined;
}
if (searchPath) {
const folders = searchPath.split(path_1.delimiter);
for (const each of folders) {
const fullPath = await getFullPath((0, path_1.resolve)(each, command));
if (fullPath) {
return fullPath;
}
}
}
return undefined;
}
/**
* Resolve given package metadata.
* @param spec This can be a package name with version, the url to a tgz or a local folder.
* @returns Package metadata.
*/
async function fetchPackageMetadata(spec) {
try {
return await pacote.manifest(spec, {
cache: `${(0, os_1.tmpdir)()}/cache`,
registry: process.env.autorest_registry || "https://registry.npmjs.org",
"full-metadata": true,
});
}
catch (error) {
logger_1.logger.error(`Error resolving package ${spec}`, error);
throw new exceptions_1.UnresolvedPackageException(spec);
}
}
exports.fetchPackageMetadata = fetchPackageMetadata;
function resolveName(name, version) {
try {
return (0, npm_package_arg_1.resolve)(name, version);
}
catch (e) {
if (e instanceof Error) {
throw new exceptions_1.InvalidPackageIdentityException(name, version, e.message);
}
else {
throw e;
}
}
}
class ExtensionManager {
static async Create(installationPath, packageManagerType = "yarn", packageManagerPath = undefined) {
if (!(await (0, async_io_1.exists)(installationPath))) {
await (0, async_io_1.mkdir)(installationPath);
}
if (!(await (0, async_io_1.isDirectory)(installationPath))) {
throw new tasks_1.Exception(`Extension folder '${installationPath}' is not a valid directory`);
}
const lock = new tasks_1.SharedLock(installationPath);
const packageManager = packageManagerType === "yarn" ? new yarn_1.Yarn(packageManagerPath) : new npm_1.Npm();
return new ExtensionManager(installationPath, lock, await lock.acquire(), packageManager);
}
async dispose() {
await this.disposeLock();
this.disposeLock = async () => { };
this.sharedLock = null;
}
async reset() {
if (!this.sharedLock) {
throw new tasks_1.Exception("Extension manager has been disposed.");
}
// get the exclusive lock
const release = await this.sharedLock.exclusive();
try {
// nuke the folder
await (0, async_io_1.rmdir)(this.installationPath);
// recreate the folder
await (0, async_io_1.mkdir)(this.installationPath);
await this.packageManager.clean(this.installationPath);
}
catch (e) {
throw new exceptions_1.ExtensionFolderLocked(this.installationPath);
}
finally {
// drop the lock
await release();
}
}
constructor(installationPath, sharedLock, disposeLock, packageManager) {
this.installationPath = installationPath;
this.sharedLock = sharedLock;
this.disposeLock = disposeLock;
this.packageManager = packageManager;
this.dotnetPath = (0, path_1.normalize)(`${(0, os_1.homedir)()}/.dotnet`);
}
/**
* Return the list of version for the given package name [+ version range]
*
* @param name Name of the package with or without version range.
* @returns List of semver versions
*/
async getPackageVersions(name) {
const packument = await pacote.packument(name);
return Object.keys(packument.versions);
}
async findPackage(name, version = "latest") {
if (version.endsWith(".tgz")) {
// get the package metadata
const pm = await fetchPackageMetadata(version);
return new extension_1.Package(pm, pm, this);
}
try {
// version can be a version or any one of the formats that
// npm accepts (path, targz, git repo)
const resolved = resolveName(name, version);
const resolvedName = resolved.raw;
// get the package metadata
const pm = await fetchPackageMetadata(resolvedName);
return new extension_1.Package(resolved, pm, this);
}
catch (E) {
// in the event that there isn't a matching package by that name
// we can try a fallback to see if a gh release has a package
// (if it is an autorest.<whatever> project)
// https://github.com/Azure/${PROJECT}/releases/download/v${VERSION}/autorest/${PROJECT}-${VERSION}.tgz
if (name.startsWith("@autorest/")) {
const githubRepo = name.replace("@autorest/", "autorest.");
const githubPkgName = name.replace("@", "").replace("autorest/", "autorest-");
const githubVersion = version
.replace(/^[~|^]/g, "") // Use the exact version instead of range
.replace(/_/g, "-"); // Replace _ with - ;
const ghurl = `https://github.com/Azure/${githubRepo}/releases/download/v${githubVersion}/${githubPkgName}-${githubVersion}.tgz`;
try {
const pm = await fetchPackageMetadata(ghurl);
if (pm) {
return new extension_1.Package(pm, pm, this);
}
}
catch (_a) {
// no worries, return the previous error
}
}
throw E;
}
}
async getInstalledExtension(name, version) {
if (!semver.validRange(version)) {
// if they asked for something that isn't a valid range, we have to find out what version
// the target package actually is.
const pkg = await this.findPackage(name, version);
version = pkg.version;
}
const installed = await this.getInstalledExtensions();
for (const each of installed) {
if (name === each.name && semver.satisfies(each.version, version)) {
return each;
}
}
return null;
}
async getInstalledExtensions() {
const results = new Array();
// iterate thru the folders.
// the folder name should have the pattern @ORG#NAME@VER or NAME@VER
for (const folder of await (0, async_io_1.readdir)(this.installationPath)) {
const fullpath = (0, path_1.join)(this.installationPath, folder);
if (await (0, async_io_1.isDirectory)(fullpath)) {
const split = /((@.+)_)?(.+)@(.+)/.exec(folder);
if (split) {
try {
const org = split[2];
const name = split[3];
const version = split[4];
const actualPath = org
? (0, path_1.normalize)(`${fullpath}/node_modules/${org}/${name}`)
: (0, path_1.normalize)(`${fullpath}/node_modules/${name}`);
const pm = await fetchPackageMetadata(actualPath);
const ext = new extension_1.Extension(new extension_1.Package(null, pm, this), this.installationPath);
if (fullpath !== ext.location) {
// eslint-disable-next-line no-console
console.trace(`WARNING: Not reporting '${fullpath}' since its package.json claims it should be at '${ext.location}' (probably symlinked once and modified later)`);
continue;
}
results.push([ext, version]);
}
catch (e) {
// ignore things that don't look right.
}
}
}
}
// each folder will contain a node_modules folder, which should have a folder by
// in the node_modules folder there should be a folder by the name of the
return results.sort((a, b) => semver.compare(b[1], a[1])).map((each) => each[0]);
}
async installPackage(pkg, force, maxWait = 5 * 60 * 1000, reportProgress = () => { }) {
if (!this.sharedLock) {
throw new tasks_1.Exception("Extension manager has been disposed.");
}
// will throw if the CriticalSection lock can't be acquired.
// we need this so that only one extension at a time can start installing
// in this process (since to use NPM right, we have to do a change dir before runinng it)
// if we ran NPM out-of-proc, this probably wouldn't be necessary.
const extensionRelease = await ExtensionManager.lock.acquire(maxWait);
if (!(await (0, async_io_1.exists)(this.installationPath))) {
await (0, async_io_1.mkdir)(this.installationPath);
}
const extension = new extension_1.Extension(pkg, this.installationPath);
const release = await new tasks_1.Mutex(extension.location).acquire(maxWait / 2);
try {
// change directory
process.chdir(this.installationPath);
if (await (0, async_io_1.isDirectory)(extension.location)) {
if (!force) {
// already installed
// if the target folder is created, we're going to make the naive assumption that the package is installed. (--force will blow away)
return extension;
}
// force removal first
try {
// progress.NotifyMessage(`Removing existing extension ${extension.location}`);
await (0, tasks_1.Delay)(100);
await (0, async_io_1.rmdir)(extension.location);
}
catch (e) {
// no worries.
}
}
// create the folder
await (0, async_io_1.mkdir)(extension.location);
const pkgRef = getPkgRef(pkg.packageMetadata);
const promise = this.packageManager.install(extension.location, [pkgRef], { force }, (progress) => {
reportProgress({ pkg, ...progress });
});
await extensionRelease();
const result = await promise;
if (result.success) {
return extension;
}
else {
const message = [result.error.message, "", "Installation logs: ", formatLogEntries(result.error.logs)];
throw new exceptions_1.PackageInstallationException(pkg.name, pkg.version, message.join("\n"));
}
}
catch (e) {
// clean up the attempted install directory
if (await (0, async_io_1.isDirectory)(extension.location)) {
await (0, tasks_1.Delay)(100);
await (0, async_io_1.rmdir)(extension.location);
}
if (e instanceof tasks_1.Exception) {
throw e;
}
if (e instanceof exceptions_1.PackageInstallationException) {
throw e;
}
if (e instanceof Error) {
throw new exceptions_1.PackageInstallationException(pkg.name, pkg.version, e.message + e.stack);
}
throw new exceptions_1.PackageInstallationException(pkg.name, pkg.version, `${e}`);
}
finally {
await Promise.all([extensionRelease(), release()]);
}
}
async removeExtension(extension) {
if (await (0, async_io_1.isDirectory)(extension.location)) {
const release = await new tasks_1.Mutex(extension.location).acquire();
await (0, async_io_1.rmdir)(extension.location);
await release();
}
}
async start(extension, enableDebugger = false) {
const PathVar = getPathVariableName();
await this.validateExtensionSystemRequirements(extension);
if (!extension.definition.scripts) {
throw new exceptions_1.MissingStartCommandException(extension);
}
const script = enableDebugger && extension.definition.scripts.debug
? extension.definition.scripts.debug
: extension.definition.scripts.start;
// look at the extension for the
if (!script) {
throw new exceptions_1.MissingStartCommandException(extension);
}
const command = cmdlineToArray(script);
if (command.length === 0) {
throw new exceptions_1.MissingStartCommandException(extension);
}
// add each engine into the front of the path.
const env = { ...process.env };
// add potential .bin folders (depends on platform and npm version)
env[PathVar] = `${(0, path_1.join)(extension.modulePath, "node_modules", ".bin")}${path_1.delimiter}${env[PathVar]}`;
env[PathVar] = `${(0, path_1.join)(extension.location, "node_modules", ".bin")}${path_1.delimiter}${env[PathVar]}`;
// find appropriate path for interpreter
switch (command[0].toLowerCase()) {
case "node":
case "node.exe":
command[0] = nodePath;
break;
case "python":
case "python.exe":
case "python3":
case "python3.exe":
await (0, system_requirements_1.patchPythonPath)(command, { version: ">=3.6" });
break;
}
// ensure parameters requiring quotes have them.
for (let i = 0; i < command.length; i++) {
command[i] = quoteIfNecessary(command[i]);
}
// spawn the command via the shell (since that how npm would have done it anyway.)
const fullCommandPath = await getFullPath(command[0], env[getPathVariableName()]);
if (!fullCommandPath) {
throw new tasks_1.Exception(`Unable to resolve full path for executable '${command[0]}' -- (cmdline '${command.join(" ")}')`);
}
// == special case ==
// on Windows, if this command has a space in the name, and it's not an .EXE
// then we're going to have to add the folder to the PATH
// and execute it by just the filename
// and set the path back when we're done.
if (process.platform === "win32" && fullCommandPath.indexOf(" ") > -1 && !/.exe$/gi.exec(fullCommandPath)) {
// preserve the current path
const originalPath = process.env[PathVar];
try {
// insert the dir into the path
process.env[PathVar] = `${(0, path_1.dirname)(fullCommandPath)}${path_1.delimiter}${env[PathVar]}`;
// call spawn and return
return (0, child_process_1.spawn)((0, path_1.basename)(fullCommandPath), command.slice(1), {
env,
cwd: extension.modulePath,
stdio: ["pipe", "pipe", "pipe"],
});
}
finally {
// regardless, restore the original path on the way out!
process.env[PathVar] = originalPath;
}
}
return (0, child_process_1.spawn)(fullCommandPath, command.slice(1), {
env,
cwd: extension.modulePath,
stdio: ["pipe", "pipe", "pipe"],
});
}
/**
* Validate if present the extension system requirements.
* @param extension Extension to validate.
*/
async validateExtensionSystemRequirements(extension) {
const systemRequirements = extension.definition.systemRequirements;
if (!systemRequirements) {
return;
}
const errors = await (0, system_requirements_1.validateExtensionSystemRequirements)(systemRequirements);
if (errors.length > 0) {
throw new exceptions_1.UnsatisfiedSystemRequirementException(extension, errors);
}
}
}
exports.ExtensionManager = ExtensionManager;
ExtensionManager.lock = new async_lock_1.AsyncLock();
function formatLogEntries(entries) {
const lines = ["```", ...entries.map(formatLogEntry), "```"];
return lines.join("\n");
}
function formatLogEntry(entry) {
const [first, ...lines] = entry.message.split("\n");
const spacing = " ".repeat(entry.severity.length);
return [`${entry.severity}: ${first}`, ...lines.map((x) => `${spacing} ${x}`)].join("\n");
}
function getPkgRef(pkg) {
// Change in pacote https://github.com/npm/pacote/issues/20
// Issue with git+ssh in yarn + performance of git+https is much worse that github https://github.com/yarnpkg/yarn/issues/6417
// if (pkg._from.startsWith("github:")) {
// return pkg._from;
// }
return pkg._resolved;
}
//# sourceMappingURL=main.js.map