UNPKG

@azure-tools/extension

Version:

Yarn-Based extension aquisition (for Azure Open Source Projects)

475 lines 21.1 kB
"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