azure-pipelines-tool-lib
Version:
Azure Pipelines Tool Installer Lib for CI/CD Tasks
604 lines (603 loc) • 25.7 kB
JavaScript
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;
}
;