UNPKG

azure-pipelines-tool-lib

Version:
604 lines (603 loc) 25.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.scrape = exports.extractZip = exports.extractTar = exports.extract7z = exports.cacheFile = exports.cacheDir = exports.downloadToolWithRetries = exports.downloadTool = exports.findLocalToolVersions = exports.findLocalTool = exports.evaluateVersions = exports.cleanVersion = exports.isExplicitVersion = exports.prependPath = exports.debug = void 0; const httpm = require("typed-rest-client/HttpClient"); const path = require("path"); const os = require("os"); const process = require("process"); const fs = require("fs"); const semver = require("semver"); const tl = require("azure-pipelines-task-lib/task"); const cmp = require('semver-compare'); const uuidV4 = require('uuid/v4'); let pkg = require(path.join(__dirname, 'package.json')); let userAgent = 'vsts-task-installer/' + pkg.version; let requestOptions = { // ignoreSslError: true, proxy: tl.getHttpProxyConfiguration(), cert: tl.getHttpCertConfiguration(), allowRedirects: true, allowRetries: true, maxRetries: 2 }; tl.setResourcePath(path.join(__dirname, 'lib.json')); function debug(message) { tl.debug(message); } exports.debug = debug; function prependPath(toolPath) { tl.assertAgent('2.115.0'); if (!toolPath) { throw new Error('Parameter toolPath must not be null or empty'); } else if (!tl.exist(toolPath) || !tl.stats(toolPath).isDirectory()) { throw new Error('Directory does not exist: ' + toolPath); } // todo: add a test for path console.log(tl.loc('TOOL_LIB_PrependPath', toolPath)); let newPath = toolPath + path.delimiter + process.env['PATH']; tl.debug('new Path: ' + newPath); process.env['PATH'] = newPath; // instruct the agent to set this path on future tasks console.log('##vso[task.prependpath]' + toolPath); } exports.prependPath = prependPath; function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } //----------------------------- // Version Functions //----------------------------- /** * Checks if a version spec is an explicit version (e.g. 1.0.1 or v1.0.1) * As opposed to a version spec like 1.x * * @param versionSpec */ function isExplicitVersion(versionSpec) { let c = semver.clean(versionSpec); tl.debug('isExplicit: ' + c); let valid = semver.valid(c) != null; tl.debug('explicit? ' + valid); return valid; } exports.isExplicitVersion = isExplicitVersion; /** * Returns cleaned (removed leading/trailing whitespace, remove '=v' prefix) * and parsed version, or null if version is invalid. */ function cleanVersion(version) { tl.debug('cleaning: ' + version); return semver.clean(version); } exports.cleanVersion = cleanVersion; /** * evaluates a list of versions and returns the latest version matching the version spec * * @param versions an array of versions to evaluate * @param versionSpec a version spec (e.g. 1.x) */ function evaluateVersions(versions, versionSpec) { let version; tl.debug('evaluating ' + versions.length + ' versions'); versions = versions.sort(cmp); for (let i = versions.length - 1; i >= 0; i--) { let potential = versions[i]; let satisfied = semver.satisfies(potential, versionSpec); if (satisfied) { version = potential; break; } } if (version) { tl.debug('matched: ' + version); } else { tl.debug('match not found'); } return version; } exports.evaluateVersions = evaluateVersions; //----------------------------- // Local Tool Cache Functions //----------------------------- /** * finds the path to a tool in the local installed tool cache * * @param toolName name of the tool * @param versionSpec version of the tool * @param arch optional arch. defaults to arch of computer */ function findLocalTool(toolName, versionSpec, arch) { if (!toolName) { throw new Error('toolName parameter is required'); } if (!versionSpec) { throw new Error('versionSpec parameter is required'); } arch = arch || os.arch(); // attempt to resolve an explicit version if (!isExplicitVersion(versionSpec)) { let localVersions = findLocalToolVersions(toolName, arch); let match = evaluateVersions(localVersions, versionSpec); versionSpec = match; } // check for the explicit version in the cache let toolPath; if (versionSpec) { versionSpec = semver.clean(versionSpec); let cacheRoot = _getCacheRoot(); let cachePath = path.join(cacheRoot, toolName, versionSpec, arch); tl.debug('checking cache: ' + cachePath); if (tl.exist(cachePath) && tl.exist(`${cachePath}.complete`)) { console.log(tl.loc('TOOL_LIB_FoundInCache', toolName, versionSpec, arch)); toolPath = cachePath; } else { tl.debug('not found'); } } return toolPath; } exports.findLocalTool = findLocalTool; /** * Retrieves the versions of a tool that is intalled in the local tool cache * * @param toolName name of the tool * @param arch optional arch. defaults to arch of computer */ function findLocalToolVersions(toolName, arch) { let versions = []; arch = arch || os.arch(); let toolPath = path.join(_getCacheRoot(), toolName); if (tl.exist(toolPath)) { let children = tl.ls('', [toolPath]); children.forEach((child) => { if (isExplicitVersion(child)) { let fullPath = path.join(toolPath, child, arch); if (tl.exist(fullPath) && tl.exist(`${fullPath}.complete`)) { versions.push(child); } } }); } return versions; } exports.findLocalToolVersions = findLocalToolVersions; //--------------------- // Download Functions //--------------------- // // TODO: keep extension intact // /** * Download a tool from an url and stream it into a file * * @param url url of tool to download * @param fileName optional fileName. Should typically not use (will be a guid for reliability). Can pass fileName with an absolute path. * @param handlers optional handlers array. Auth handlers to pass to the HttpClient for the tool download. * @param additionalHeaders optional custom HTTP headers. This is passed to the REST client that downloads the tool. */ function downloadTool(url, fileName, handlers, additionalHeaders) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { try { handlers = handlers || null; let http = new httpm.HttpClient(userAgent, handlers, requestOptions); tl.debug(fileName); fileName = fileName || uuidV4(); // check if it's an absolute path already var destPath; if (path.isAbsolute(fileName)) { destPath = fileName; } else { destPath = path.join(_getAgentTemp(), fileName); } // make sure that the folder exists tl.mkdirP(path.dirname(destPath)); console.log(tl.loc('TOOL_LIB_Downloading', url.replace(/sig=[^&]*/, "sig=-REDACTED-"))); tl.debug('destination ' + destPath); if (fs.existsSync(destPath)) { throw new Error("Destination file path already exists"); } tl.debug('downloading'); let response = yield http.get(url, additionalHeaders); if (response.message.statusCode != 200) { let err = new Error('Unexpected HTTP response: ' + response.message.statusCode); err['httpStatusCode'] = response.message.statusCode; tl.debug(`Failed to download "${fileName}" from "${url}". Code(${response.message.statusCode}) Message(${response.message.statusMessage})`); throw err; } let downloadedContentLength = _getContentLengthOfDownloadedFile(response); if (!isNaN(downloadedContentLength)) { tl.debug(`Content-Length of downloaded file: ${downloadedContentLength}`); } else { tl.debug(`Content-Length header missing`); } tl.debug('creating stream'); const file = fs.createWriteStream(destPath); file .on('open', (fd) => __awaiter(this, void 0, void 0, function* () { tl.debug('file write stream opened. fd: ' + fd); const messageStream = response.message; if (messageStream.aborted || messageStream.destroyed) { file.end(); reject(new Error('Incoming message read stream was Aborted or Destroyed before download was complete')); return; } tl.debug('subscribing to message read stream events...'); try { messageStream .on('error', (err) => { file.end(); reject(err); }) .on('aborted', () => { // this block is for Node10 compatibility since it doesn't emit 'error' event after 'aborted' one file.end(); reject(new Error('Aborted')); }) .pipe(file); } catch (err) { reject(err); } tl.debug('successfully subscribed to message read stream events'); })) .on('close', () => { tl.debug('download complete'); let fileSizeInBytes; try { fileSizeInBytes = _getFileSizeOnDisk(destPath); } catch (err) { fileSizeInBytes = NaN; tl.warning(`Unable to check file size of ${destPath} due to error: ${err.Message}`); } if (!isNaN(fileSizeInBytes)) { tl.debug(`Downloaded file size: ${fileSizeInBytes} bytes`); } else { tl.debug(`File size on disk was not found`); } if (!isNaN(downloadedContentLength) && !isNaN(fileSizeInBytes) && fileSizeInBytes !== downloadedContentLength) { tl.warning(`Content-Length (${downloadedContentLength} bytes) did not match downloaded file size (${fileSizeInBytes} bytes).`); } resolve(destPath); }) .on('error', (err) => { file.end(); reject(err); }); } catch (error) { reject(error); } })); }); } exports.downloadTool = downloadTool; function downloadToolWithRetries(url, fileName, handlers, additionalHeaders, maxAttempts = 3, retryInterval = 500) { return __awaiter(this, void 0, void 0, function* () { let attempt = 1; let destinationPath = ''; while (attempt <= maxAttempts && destinationPath == '') { try { destinationPath = yield downloadTool(url, fileName, handlers, additionalHeaders); } catch (err) { if (attempt === maxAttempts) throw err; const attemptInterval = attempt * retryInterval; // Error will be shown in downloadTool. tl.debug(`Attempt ${attempt} failed. Retrying after ${attemptInterval} ms`); yield delay(attemptInterval); attempt++; } } return destinationPath; }); } exports.downloadToolWithRetries = downloadToolWithRetries; //--------------------- // Size functions //--------------------- /** * Gets size of downloaded file from "Content-Length" header * * @param response response for request to get the file * @returns number if the 'content-length' is not empty, otherwise NaN */ function _getContentLengthOfDownloadedFile(response) { let contentLengthHeader = response.message.headers['content-length']; let parsedContentLength = parseInt(contentLengthHeader); return parsedContentLength; } /** * Gets size of file saved to disk * * @param filePath the path to the file, saved to the disk * @returns size of file saved to disk */ function _getFileSizeOnDisk(filePath) { let fileStats = fs.statSync(filePath); let fileSizeInBytes = fileStats.size; return fileSizeInBytes; } //--------------------- // Install Functions //--------------------- function _createToolPath(tool, version, arch) { // todo: add test for clean let folderPath = path.join(_getCacheRoot(), tool, semver.clean(version), arch); tl.debug('destination ' + folderPath); let markerPath = `${folderPath}.complete`; tl.rmRF(folderPath); tl.rmRF(markerPath); tl.mkdirP(folderPath); return folderPath; } function _completeToolPath(tool, version, arch) { let folderPath = path.join(_getCacheRoot(), tool, semver.clean(version), arch); let markerPath = `${folderPath}.complete`; tl.writeFile(markerPath, ''); tl.debug('finished caching tool'); } /** * Caches a directory and installs it into the tool cacheDir * * @param sourceDir the directory to cache into tools * @param tool tool name * @param version version of the tool. semver format * @param arch architecture of the tool. Optional. Defaults to machine architecture */ function cacheDir(sourceDir, tool, version, arch) { return __awaiter(this, void 0, void 0, function* () { version = semver.clean(version); arch = arch || os.arch(); console.log(tl.loc('TOOL_LIB_CachingTool', tool, version, arch)); tl.debug('source dir: ' + sourceDir); if (!tl.stats(sourceDir).isDirectory()) { throw new Error('sourceDir is not a directory'); } // create the tool dir let destPath = _createToolPath(tool, version, arch); // copy each child item. do not move. move can fail on Windows // due to anti-virus software having an open handle on a file. for (let itemName of fs.readdirSync(sourceDir)) { let s = path.join(sourceDir, itemName); tl.cp(s, destPath + '/', '-r'); } // write .complete _completeToolPath(tool, version, arch); return destPath; }); } exports.cacheDir = cacheDir; /** * Caches a downloaded file (GUID) and installs it * into the tool cache with a given targetName * * @param sourceFile the file to cache into tools. Typically a result of downloadTool which is a guid. * @param targetFile the name of the file name in the tools directory * @param tool tool name * @param version version of the tool. semver format * @param arch architecture of the tool. Optional. Defaults to machine architecture */ function cacheFile(sourceFile, targetFile, tool, version, arch) { return __awaiter(this, void 0, void 0, function* () { version = semver.clean(version); arch = arch || os.arch(); console.log(tl.loc('TOOL_LIB_CachingTool', tool, version, arch)); tl.debug('source file:' + sourceFile); if (!tl.stats(sourceFile).isFile()) { throw new Error('sourceFile is not a file'); } // create the tool dir let destFolder = _createToolPath(tool, version, arch); // copy instead of move. move can fail on Windows due to // anti-virus software having an open handle on a file. let destPath = path.join(destFolder, targetFile); tl.debug('destination file' + destPath); tl.cp(sourceFile, destPath); // write .complete _completeToolPath(tool, version, arch); return destFolder; }); } exports.cacheFile = cacheFile; //--------------------- // Extract Functions //--------------------- /** * Extract a .7z file * * @param file path to the .7z file * @param dest destination directory. Optional. * @param _7zPath path to 7zr.exe. Optional, for long path support. Most .7z archives do not have this * problem. If your .7z archive contains very long paths, you can pass the path to 7zr.exe which will * gracefully handle long paths. By default 7z.exe is used because it is a very small program and is * bundled with the tool lib. However it does not support long paths. 7z.exe is the reduced command line * interface, it is smaller than the full command line interface, and it does support long paths. At the * time of this writing, it is freely available from the LZMA SDK that is available on the 7zip website. * Be sure to check the current license agreement. If 7z.exe is bundled with your task, then the path * to 7z.exe can be pass to this function. * @param overwriteDest Overwrite files in destination catalog. Optional. * @returns path to the destination directory */ function extract7z(file, dest, _7zPath, overwriteDest) { return __awaiter(this, void 0, void 0, function* () { if (process.platform != 'win32') { throw new Error('extract7z() not supported on current OS'); } if (!file) { throw new Error("parameter 'file' is required"); } console.log(tl.loc('TOOL_LIB_ExtractingArchive')); dest = _createExtractFolder(dest); let originalCwd = process.cwd(); try { process.chdir(dest); if (_7zPath) { // extract const _7z = tl.tool(_7zPath); if (overwriteDest) { _7z.arg('-aoa'); } _7z.arg('x') // eXtract files with full paths .arg('-bb1') // -bb[0-3] : set output log level .arg('-bd') // disable progress indicator .arg('-sccUTF-8') // set charset for for console input/output .arg(file); yield _7z.exec(); } else { // extract let escapedScript = path.join(__dirname, 'Invoke-7zdec.ps1').replace(/'/g, "''").replace(/"|\n|\r/g, ''); // double-up single quotes, remove double quotes and newlines let escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, ''); let escapedTarget = dest.replace(/'/g, "''").replace(/"|\n|\r/g, ''); const overrideDestDirectory = overwriteDest ? 1 : 0; const command = `& '${escapedScript}' -Source '${escapedFile}' -Target '${escapedTarget}' -OverrideDestDirectory ${overrideDestDirectory}`; let powershellPath = tl.which('powershell', true); let powershell = tl.tool(powershellPath) .line('-NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command') .arg(command); powershell.on('stdout', (buffer) => { process.stdout.write(buffer); }); powershell.on('stderr', (buffer) => { process.stderr.write(buffer); }); yield powershell.exec({ silent: true }); } } finally { process.chdir(originalCwd); } return dest; }); } exports.extract7z = extract7z; /** * installs a tool from a tar by extracting the tar and installing it into the tool cache * * @param file file path of the tar * @param tool name of tool in the tool cache * @param version version of the tool * @param arch arch of the tool. optional. defaults to the arch of the machine * @param options IExtractOptions * @param destination destination directory. optional. */ function extractTar(file, destination) { return __awaiter(this, void 0, void 0, function* () { // mkdir -p node/4.7.0/x64 // tar xzC ./node/4.7.0/x64 -f node-v4.7.0-darwin-x64.tar.gz --strip-components 1 console.log(tl.loc('TOOL_LIB_ExtractingArchive')); let dest = _createExtractFolder(destination); let tr = tl.tool('tar'); tr.arg(['xC', dest, '-f', file]); yield tr.exec(); return dest; }); } exports.extractTar = extractTar; function extractZip(file, destination) { return __awaiter(this, void 0, void 0, function* () { if (!file) { throw new Error("parameter 'file' is required"); } console.log(tl.loc('TOOL_LIB_ExtractingArchive')); let dest = _createExtractFolder(destination); if (process.platform == 'win32') { // build the powershell command let escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, ''); // double-up single quotes, remove double quotes and newlines let escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, ''); let command = `$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}')`; // change the console output code page to UTF-8. // TODO: FIX WHICH: let chcpPath = tl.which('chcp.com', true); let chcpPath = path.join(process.env.windir, "system32", "chcp.com"); yield tl.exec(chcpPath, '65001'); // run powershell let powershell = tl.tool('powershell') .line('-NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command') .arg(command); yield powershell.exec(); } else { let unzip = tl.tool('unzip') .arg(file); yield unzip.exec({ cwd: dest }); } return dest; }); } exports.extractZip = extractZip; function _createExtractFolder(dest) { if (!dest) { // create a temp dir dest = path.join(_getAgentTemp(), uuidV4()); } tl.mkdirP(dest); return dest; } //--------------------- // Query Functions //--------------------- // default input will be >= LTS version. drop label different than value. // v4 (LTS) would have a value of 4.x // option to always download? (not cache), TTL? /** * Scrape a web page for versions by regex * * @param url url to scrape * @param regex regex to use for version matches * @param handlers optional handlers array. Auth handlers to pass to the HttpClient for the tool download. */ function scrape(url, regex, handlers) { return __awaiter(this, void 0, void 0, function* () { handlers = handlers || null; let http = new httpm.HttpClient(userAgent, handlers, requestOptions); let output = yield (yield http.get(url)).readBody(); let matches = output.match(regex); let seen = {}; let versions = []; for (let i = 0; i < matches.length; i++) { let ver = semver.clean(matches[i]); if (!seen.hasOwnProperty(ver)) { seen[ver] = true; versions.push(ver); } } return versions; }); } exports.scrape = scrape; function _getCacheRoot() { tl.assertAgent('2.115.0'); let cacheRoot = tl.getVariable('Agent.ToolsDirectory'); if (!cacheRoot) { throw new Error('Agent.ToolsDirectory is not set'); } return cacheRoot; } function _getAgentTemp() { tl.assertAgent('2.115.0'); let tempDirectory = tl.getVariable('Agent.TempDirectory'); if (!tempDirectory) { throw new Error('Agent.TempDirectory is not set'); } return tempDirectory; }