2019-06-06 18:16:48 +00:00
|
|
|
import * as core from '@actions/core'
|
|
|
|
import * as io from '@actions/io'
|
|
|
|
import * as fs from 'fs'
|
2020-05-19 17:25:57 +00:00
|
|
|
import * as mm from './manifest'
|
2019-06-06 18:16:48 +00:00
|
|
|
import * as os from 'os'
|
|
|
|
import * as path from 'path'
|
2020-01-10 21:37:48 +00:00
|
|
|
import * as httpm from '@actions/http-client'
|
2019-06-06 18:16:48 +00:00
|
|
|
import * as semver from 'semver'
|
2020-03-05 17:05:27 +00:00
|
|
|
import * as stream from 'stream'
|
|
|
|
import * as util from 'util'
|
2022-05-11 21:14:25 +00:00
|
|
|
import {ok} from 'assert'
|
|
|
|
import {OutgoingHttpHeaders} from 'http'
|
2020-01-18 19:28:37 +00:00
|
|
|
import uuidV4 from 'uuid/v4'
|
2019-06-06 18:16:48 +00:00
|
|
|
import {exec} from '@actions/exec/lib/exec'
|
|
|
|
import {ExecOptions} from '@actions/exec/lib/interfaces'
|
2020-03-05 17:05:27 +00:00
|
|
|
import {RetryHelper} from './retry-helper'
|
2019-06-06 18:16:48 +00:00
|
|
|
|
|
|
|
export class HTTPError extends Error {
|
|
|
|
constructor(readonly httpStatusCode: number | undefined) {
|
|
|
|
super(`Unexpected HTTP response: ${httpStatusCode}`)
|
|
|
|
Object.setPrototypeOf(this, new.target.prototype)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const IS_WINDOWS = process.platform === 'win32'
|
2020-07-15 18:49:23 +00:00
|
|
|
const IS_MAC = process.platform === 'darwin'
|
2019-06-06 18:16:48 +00:00
|
|
|
const userAgent = 'actions/tool-cache'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Download a tool from an url and stream it into a file
|
|
|
|
*
|
|
|
|
* @param url url of tool to download
|
2019-12-16 16:59:48 +00:00
|
|
|
* @param dest path to download tool
|
2020-05-19 17:25:57 +00:00
|
|
|
* @param auth authorization header
|
2021-04-28 18:39:15 +00:00
|
|
|
* @param headers other headers
|
2019-06-06 18:16:48 +00:00
|
|
|
* @returns path to downloaded tool
|
|
|
|
*/
|
2019-12-16 16:59:48 +00:00
|
|
|
export async function downloadTool(
|
|
|
|
url: string,
|
2020-05-19 17:25:57 +00:00
|
|
|
dest?: string,
|
2021-04-28 18:39:15 +00:00
|
|
|
auth?: string,
|
2022-05-11 21:14:25 +00:00
|
|
|
headers?: OutgoingHttpHeaders
|
2019-12-16 16:59:48 +00:00
|
|
|
): Promise<string> {
|
2020-03-09 18:35:53 +00:00
|
|
|
dest = dest || path.join(_getTempDirectory(), uuidV4())
|
2020-03-05 17:05:27 +00:00
|
|
|
await io.mkdirP(path.dirname(dest))
|
|
|
|
core.debug(`Downloading ${url}`)
|
|
|
|
core.debug(`Destination ${dest}`)
|
|
|
|
|
|
|
|
const maxAttempts = 3
|
2020-03-09 18:35:53 +00:00
|
|
|
const minSeconds = _getGlobal<number>(
|
2020-03-05 17:05:27 +00:00
|
|
|
'TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS',
|
|
|
|
10
|
|
|
|
)
|
2020-03-09 18:35:53 +00:00
|
|
|
const maxSeconds = _getGlobal<number>(
|
2020-03-05 17:05:27 +00:00
|
|
|
'TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS',
|
|
|
|
20
|
|
|
|
)
|
|
|
|
const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds)
|
|
|
|
return await retryHelper.execute(
|
2020-03-09 18:35:53 +00:00
|
|
|
async () => {
|
2021-04-28 18:39:15 +00:00
|
|
|
return await downloadToolAttempt(url, dest || '', auth, headers)
|
2020-03-09 18:35:53 +00:00
|
|
|
},
|
|
|
|
(err: Error) => {
|
|
|
|
if (err instanceof HTTPError && err.httpStatusCode) {
|
|
|
|
// Don't retry anything less than 500, except 408 Request Timeout and 429 Too Many Requests
|
|
|
|
if (
|
|
|
|
err.httpStatusCode < 500 &&
|
|
|
|
err.httpStatusCode !== 408 &&
|
|
|
|
err.httpStatusCode !== 429
|
|
|
|
) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise retry
|
|
|
|
return true
|
|
|
|
}
|
2020-03-05 17:05:27 +00:00
|
|
|
)
|
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
|
2020-05-19 17:25:57 +00:00
|
|
|
async function downloadToolAttempt(
|
|
|
|
url: string,
|
|
|
|
dest: string,
|
2021-04-28 18:39:15 +00:00
|
|
|
auth?: string,
|
2022-05-11 21:14:25 +00:00
|
|
|
headers?: OutgoingHttpHeaders
|
2020-05-19 17:25:57 +00:00
|
|
|
): Promise<string> {
|
2020-03-05 17:05:27 +00:00
|
|
|
if (fs.existsSync(dest)) {
|
|
|
|
throw new Error(`Destination file path ${dest} already exists`)
|
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
|
2020-03-05 17:05:27 +00:00
|
|
|
// Get the response headers
|
|
|
|
const http = new httpm.HttpClient(userAgent, [], {
|
|
|
|
allowRetries: false
|
|
|
|
})
|
2020-05-19 17:25:57 +00:00
|
|
|
|
|
|
|
if (auth) {
|
|
|
|
core.debug('set auth')
|
2021-04-28 18:39:15 +00:00
|
|
|
if (headers === undefined) {
|
|
|
|
headers = {}
|
2020-05-19 17:25:57 +00:00
|
|
|
}
|
2021-04-28 18:39:15 +00:00
|
|
|
headers.authorization = auth
|
2020-05-19 17:25:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const response: httpm.HttpClientResponse = await http.get(url, headers)
|
2020-03-05 17:05:27 +00:00
|
|
|
if (response.message.statusCode !== 200) {
|
|
|
|
const err = new HTTPError(response.message.statusCode)
|
|
|
|
core.debug(
|
|
|
|
`Failed to download from "${url}". Code(${response.message.statusCode}) Message(${response.message.statusMessage})`
|
|
|
|
)
|
|
|
|
throw err
|
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
|
2020-03-05 17:05:27 +00:00
|
|
|
// Download the response body
|
|
|
|
const pipeline = util.promisify(stream.pipeline)
|
2020-03-09 18:35:53 +00:00
|
|
|
const responseMessageFactory = _getGlobal<() => stream.Readable>(
|
2020-03-05 17:05:27 +00:00
|
|
|
'TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY',
|
|
|
|
() => response.message
|
|
|
|
)
|
|
|
|
const readStream = responseMessageFactory()
|
|
|
|
let succeeded = false
|
|
|
|
try {
|
|
|
|
await pipeline(readStream, fs.createWriteStream(dest))
|
|
|
|
core.debug('download complete')
|
|
|
|
succeeded = true
|
|
|
|
return dest
|
|
|
|
} finally {
|
|
|
|
// Error, delete dest before retry
|
|
|
|
if (!succeeded) {
|
|
|
|
core.debug('download failed')
|
|
|
|
try {
|
|
|
|
await io.rmRF(dest)
|
|
|
|
} catch (err) {
|
|
|
|
core.debug(`Failed to delete '${dest}'. ${err.message}`)
|
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
}
|
2020-03-05 17:05:27 +00:00
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 7zdec.exe is used because it is a very small program and is
|
|
|
|
* bundled with the tool lib. However it does not support long paths. 7zr.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 7zr.exe is bundled with your action, then the path
|
|
|
|
* to 7zr.exe can be pass to this function.
|
|
|
|
* @returns path to the destination directory
|
|
|
|
*/
|
|
|
|
export async function extract7z(
|
|
|
|
file: string,
|
|
|
|
dest?: string,
|
|
|
|
_7zPath?: string
|
|
|
|
): Promise<string> {
|
|
|
|
ok(IS_WINDOWS, 'extract7z() not supported on current OS')
|
|
|
|
ok(file, 'parameter "file" is required')
|
|
|
|
|
2019-12-09 19:26:48 +00:00
|
|
|
dest = await _createExtractFolder(dest)
|
2019-06-06 18:16:48 +00:00
|
|
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
process.chdir(dest)
|
|
|
|
if (_7zPath) {
|
|
|
|
try {
|
2020-04-29 15:33:01 +00:00
|
|
|
const logLevel = core.isDebug() ? '-bb1' : '-bb0'
|
2019-06-06 18:16:48 +00:00
|
|
|
const args: string[] = [
|
|
|
|
'x', // eXtract files with full paths
|
2020-04-29 15:33:01 +00:00
|
|
|
logLevel, // -bb[0-3] : set output log level
|
2019-06-06 18:16:48 +00:00
|
|
|
'-bd', // disable progress indicator
|
|
|
|
'-sccUTF-8', // set charset for for console input/output
|
|
|
|
file
|
|
|
|
]
|
|
|
|
const options: ExecOptions = {
|
|
|
|
silent: true
|
|
|
|
}
|
|
|
|
await exec(`"${_7zPath}"`, args, options)
|
|
|
|
} finally {
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const escapedScript = path
|
|
|
|
.join(__dirname, '..', 'scripts', 'Invoke-7zdec.ps1')
|
|
|
|
.replace(/'/g, "''")
|
|
|
|
.replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
|
|
|
|
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
|
|
|
const escapedTarget = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
|
|
|
const command = `& '${escapedScript}' -Source '${escapedFile}' -Target '${escapedTarget}'`
|
|
|
|
const args: string[] = [
|
|
|
|
'-NoLogo',
|
|
|
|
'-Sta',
|
|
|
|
'-NoProfile',
|
|
|
|
'-NonInteractive',
|
|
|
|
'-ExecutionPolicy',
|
|
|
|
'Unrestricted',
|
|
|
|
'-Command',
|
|
|
|
command
|
|
|
|
]
|
|
|
|
const options: ExecOptions = {
|
|
|
|
silent: true
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
const powershellPath: string = await io.which('powershell', true)
|
|
|
|
await exec(`"${powershellPath}"`, args, options)
|
|
|
|
} finally {
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-11-15 21:12:36 +00:00
|
|
|
* Extract a compressed tar archive
|
2019-06-06 18:16:48 +00:00
|
|
|
*
|
|
|
|
* @param file path to the tar
|
|
|
|
* @param dest destination directory. Optional.
|
2019-11-15 21:12:36 +00:00
|
|
|
* @param flags flags for the tar command to use for extraction. Defaults to 'xz' (extracting gzipped tars). Optional.
|
2019-06-06 18:16:48 +00:00
|
|
|
* @returns path to the destination directory
|
|
|
|
*/
|
2019-08-13 18:26:14 +00:00
|
|
|
export async function extractTar(
|
|
|
|
file: string,
|
|
|
|
dest?: string,
|
2020-05-19 17:25:57 +00:00
|
|
|
flags: string | string[] = 'xz'
|
2019-08-13 18:26:14 +00:00
|
|
|
): Promise<string> {
|
2019-06-06 18:16:48 +00:00
|
|
|
if (!file) {
|
|
|
|
throw new Error("parameter 'file' is required")
|
|
|
|
}
|
|
|
|
|
2019-12-19 15:44:08 +00:00
|
|
|
// Create dest
|
2019-12-09 19:26:48 +00:00
|
|
|
dest = await _createExtractFolder(dest)
|
2019-12-19 15:44:08 +00:00
|
|
|
|
|
|
|
// Determine whether GNU tar
|
2020-02-13 14:54:56 +00:00
|
|
|
core.debug('Checking tar --version')
|
2019-12-19 15:44:08 +00:00
|
|
|
let versionOutput = ''
|
|
|
|
await exec('tar --version', [], {
|
|
|
|
ignoreReturnCode: true,
|
2020-02-13 14:54:56 +00:00
|
|
|
silent: true,
|
2019-12-19 15:44:08 +00:00
|
|
|
listeners: {
|
|
|
|
stdout: (data: Buffer) => (versionOutput += data.toString()),
|
|
|
|
stderr: (data: Buffer) => (versionOutput += data.toString())
|
|
|
|
}
|
|
|
|
})
|
2020-02-13 14:54:56 +00:00
|
|
|
core.debug(versionOutput.trim())
|
2019-12-19 15:44:08 +00:00
|
|
|
const isGnuTar = versionOutput.toUpperCase().includes('GNU TAR')
|
|
|
|
|
|
|
|
// Initialize args
|
2020-05-19 17:25:57 +00:00
|
|
|
let args: string[]
|
|
|
|
if (flags instanceof Array) {
|
|
|
|
args = flags
|
|
|
|
} else {
|
|
|
|
args = [flags]
|
|
|
|
}
|
2019-12-19 15:44:08 +00:00
|
|
|
|
2020-04-29 15:33:01 +00:00
|
|
|
if (core.isDebug() && !flags.includes('v')) {
|
|
|
|
args.push('-v')
|
|
|
|
}
|
|
|
|
|
2019-12-19 15:44:08 +00:00
|
|
|
let destArg = dest
|
|
|
|
let fileArg = file
|
|
|
|
if (IS_WINDOWS && isGnuTar) {
|
|
|
|
args.push('--force-local')
|
|
|
|
destArg = dest.replace(/\\/g, '/')
|
|
|
|
|
|
|
|
// Technically only the dest needs to have `/` but for aesthetic consistency
|
|
|
|
// convert slashes in the file arg too.
|
|
|
|
fileArg = file.replace(/\\/g, '/')
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isGnuTar) {
|
|
|
|
// Suppress warnings when using GNU tar to extract archives created by BSD tar
|
|
|
|
args.push('--warning=no-unknown-keyword')
|
2021-05-21 21:01:42 +00:00
|
|
|
args.push('--overwrite')
|
2019-12-19 15:44:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
args.push('-C', destArg, '-f', fileArg)
|
|
|
|
await exec(`tar`, args)
|
2019-06-06 18:16:48 +00:00
|
|
|
|
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
2020-07-15 18:49:23 +00:00
|
|
|
/**
|
|
|
|
* Extract a xar compatible archive
|
|
|
|
*
|
|
|
|
* @param file path to the archive
|
|
|
|
* @param dest destination directory. Optional.
|
|
|
|
* @param flags flags for the xar. Optional.
|
|
|
|
* @returns path to the destination directory
|
|
|
|
*/
|
|
|
|
export async function extractXar(
|
|
|
|
file: string,
|
|
|
|
dest?: string,
|
|
|
|
flags: string | string[] = []
|
|
|
|
): Promise<string> {
|
|
|
|
ok(IS_MAC, 'extractXar() not supported on current OS')
|
|
|
|
ok(file, 'parameter "file" is required')
|
|
|
|
|
|
|
|
dest = await _createExtractFolder(dest)
|
|
|
|
|
|
|
|
let args: string[]
|
|
|
|
if (flags instanceof Array) {
|
|
|
|
args = flags
|
|
|
|
} else {
|
|
|
|
args = [flags]
|
|
|
|
}
|
|
|
|
|
|
|
|
args.push('-x', '-C', dest, '-f', file)
|
|
|
|
|
|
|
|
if (core.isDebug()) {
|
|
|
|
args.push('-v')
|
|
|
|
}
|
|
|
|
|
|
|
|
const xarPath: string = await io.which('xar', true)
|
|
|
|
await exec(`"${xarPath}"`, _unique(args))
|
|
|
|
|
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
2019-06-06 18:16:48 +00:00
|
|
|
/**
|
|
|
|
* Extract a zip
|
|
|
|
*
|
|
|
|
* @param file path to the zip
|
|
|
|
* @param dest destination directory. Optional.
|
|
|
|
* @returns path to the destination directory
|
|
|
|
*/
|
|
|
|
export async function extractZip(file: string, dest?: string): Promise<string> {
|
|
|
|
if (!file) {
|
|
|
|
throw new Error("parameter 'file' is required")
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:26:48 +00:00
|
|
|
dest = await _createExtractFolder(dest)
|
2019-06-06 18:16:48 +00:00
|
|
|
|
|
|
|
if (IS_WINDOWS) {
|
|
|
|
await extractZipWin(file, dest)
|
|
|
|
} else {
|
2019-09-24 21:07:08 +00:00
|
|
|
await extractZipNix(file, dest)
|
2019-06-06 18:16:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
|
|
|
async function extractZipWin(file: string, dest: string): Promise<void> {
|
|
|
|
// build the powershell command
|
|
|
|
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
|
|
|
|
const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
2021-05-21 21:01:42 +00:00
|
|
|
const pwshPath = await io.which('pwsh', false)
|
|
|
|
|
|
|
|
//To match the file overwrite behavior on nix systems, we use the overwrite = true flag for ExtractToDirectory
|
|
|
|
//and the -Force flag for Expand-Archive as a fallback
|
|
|
|
if (pwshPath) {
|
|
|
|
//attempt to use pwsh with ExtractToDirectory, if this fails attempt Expand-Archive
|
|
|
|
const pwshCommand = [
|
|
|
|
`$ErrorActionPreference = 'Stop' ;`,
|
|
|
|
`try { Add-Type -AssemblyName System.IO.Compression.ZipFile } catch { } ;`,
|
|
|
|
`try { [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`,
|
|
|
|
`catch { if (($_.Exception.GetType().FullName -eq 'System.Management.Automation.MethodException') -or ($_.Exception.GetType().FullName -eq 'System.Management.Automation.RuntimeException') ){ Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force } else { throw $_ } } ;`
|
|
|
|
].join(' ')
|
|
|
|
|
|
|
|
const args = [
|
|
|
|
'-NoLogo',
|
|
|
|
'-NoProfile',
|
|
|
|
'-NonInteractive',
|
|
|
|
'-ExecutionPolicy',
|
|
|
|
'Unrestricted',
|
|
|
|
'-Command',
|
|
|
|
pwshCommand
|
|
|
|
]
|
|
|
|
|
|
|
|
core.debug(`Using pwsh at path: ${pwshPath}`)
|
|
|
|
await exec(`"${pwshPath}"`, args)
|
|
|
|
} else {
|
|
|
|
const powershellCommand = [
|
|
|
|
`$ErrorActionPreference = 'Stop' ;`,
|
|
|
|
`try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ;`,
|
|
|
|
`if ((Get-Command -Name Expand-Archive -Module Microsoft.PowerShell.Archive -ErrorAction Ignore)) { Expand-Archive -LiteralPath '${escapedFile}' -DestinationPath '${escapedDest}' -Force }`,
|
|
|
|
`else {[System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}', $true) }`
|
|
|
|
].join(' ')
|
|
|
|
|
|
|
|
const args = [
|
|
|
|
'-NoLogo',
|
|
|
|
'-Sta',
|
|
|
|
'-NoProfile',
|
|
|
|
'-NonInteractive',
|
|
|
|
'-ExecutionPolicy',
|
|
|
|
'Unrestricted',
|
|
|
|
'-Command',
|
|
|
|
powershellCommand
|
|
|
|
]
|
|
|
|
|
|
|
|
const powershellPath = await io.which('powershell', true)
|
|
|
|
core.debug(`Using powershell at path: ${powershellPath}`)
|
|
|
|
|
|
|
|
await exec(`"${powershellPath}"`, args)
|
|
|
|
}
|
2019-06-06 18:16:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function extractZipNix(file: string, dest: string): Promise<void> {
|
2020-04-28 14:36:49 +00:00
|
|
|
const unzipPath = await io.which('unzip', true)
|
2020-04-29 15:33:01 +00:00
|
|
|
const args = [file]
|
|
|
|
if (!core.isDebug()) {
|
|
|
|
args.unshift('-q')
|
|
|
|
}
|
2021-05-21 21:01:42 +00:00
|
|
|
args.unshift('-o') //overwrite with -o, otherwise a prompt is shown which freezes the run
|
2020-04-29 15:33:01 +00:00
|
|
|
await exec(`"${unzipPath}"`, args, {cwd: dest})
|
2019-08-13 16:39:01 +00:00
|
|
|
}
|
|
|
|
|
2019-06-06 18:16:48 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
export async function cacheDir(
|
|
|
|
sourceDir: string,
|
|
|
|
tool: string,
|
|
|
|
version: string,
|
|
|
|
arch?: string
|
|
|
|
): Promise<string> {
|
|
|
|
version = semver.clean(version) || version
|
|
|
|
arch = arch || os.arch()
|
|
|
|
core.debug(`Caching tool ${tool} ${version} ${arch}`)
|
|
|
|
|
|
|
|
core.debug(`source dir: ${sourceDir}`)
|
|
|
|
if (!fs.statSync(sourceDir).isDirectory()) {
|
|
|
|
throw new Error('sourceDir is not a directory')
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the tool dir
|
|
|
|
const destPath: string = await _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 (const itemName of fs.readdirSync(sourceDir)) {
|
|
|
|
const s = path.join(sourceDir, itemName)
|
|
|
|
await io.cp(s, destPath, {recursive: true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// write .complete
|
|
|
|
_completeToolPath(tool, version, arch)
|
|
|
|
|
|
|
|
return destPath
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
export async function cacheFile(
|
|
|
|
sourceFile: string,
|
|
|
|
targetFile: string,
|
|
|
|
tool: string,
|
|
|
|
version: string,
|
|
|
|
arch?: string
|
|
|
|
): Promise<string> {
|
|
|
|
version = semver.clean(version) || version
|
|
|
|
arch = arch || os.arch()
|
|
|
|
core.debug(`Caching tool ${tool} ${version} ${arch}`)
|
|
|
|
|
|
|
|
core.debug(`source file: ${sourceFile}`)
|
|
|
|
if (!fs.statSync(sourceFile).isFile()) {
|
|
|
|
throw new Error('sourceFile is not a file')
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the tool dir
|
|
|
|
const destFolder: string = await _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.
|
|
|
|
const destPath: string = path.join(destFolder, targetFile)
|
|
|
|
core.debug(`destination file ${destPath}`)
|
|
|
|
await io.cp(sourceFile, destPath)
|
|
|
|
|
|
|
|
// write .complete
|
|
|
|
_completeToolPath(tool, version, arch)
|
|
|
|
|
|
|
|
return destFolder
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-06-07 19:17:27 +00:00
|
|
|
* Finds the path to a tool version in the local installed tool cache
|
2019-06-06 18:16:48 +00:00
|
|
|
*
|
|
|
|
* @param toolName name of the tool
|
|
|
|
* @param versionSpec version of the tool
|
|
|
|
* @param arch optional arch. defaults to arch of computer
|
|
|
|
*/
|
|
|
|
export function find(
|
|
|
|
toolName: string,
|
|
|
|
versionSpec: string,
|
|
|
|
arch?: string
|
|
|
|
): string {
|
|
|
|
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
|
2021-05-07 20:13:26 +00:00
|
|
|
if (!isExplicitVersion(versionSpec)) {
|
2019-06-07 19:17:27 +00:00
|
|
|
const localVersions: string[] = findAllVersions(toolName, arch)
|
2021-05-07 20:13:26 +00:00
|
|
|
const match = evaluateVersions(localVersions, versionSpec)
|
2019-06-06 18:16:48 +00:00
|
|
|
versionSpec = match
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for the explicit version in the cache
|
|
|
|
let toolPath = ''
|
|
|
|
if (versionSpec) {
|
|
|
|
versionSpec = semver.clean(versionSpec) || ''
|
2020-03-09 18:35:53 +00:00
|
|
|
const cachePath = path.join(
|
|
|
|
_getCacheDirectory(),
|
|
|
|
toolName,
|
|
|
|
versionSpec,
|
|
|
|
arch
|
|
|
|
)
|
2019-06-06 18:16:48 +00:00
|
|
|
core.debug(`checking cache: ${cachePath}`)
|
|
|
|
if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) {
|
|
|
|
core.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`)
|
|
|
|
toolPath = cachePath
|
|
|
|
} else {
|
|
|
|
core.debug('not found')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return toolPath
|
|
|
|
}
|
|
|
|
|
2019-06-07 19:17:27 +00:00
|
|
|
/**
|
|
|
|
* Finds the paths to all versions of a tool that are installed in the local tool cache
|
|
|
|
*
|
|
|
|
* @param toolName name of the tool
|
|
|
|
* @param arch optional arch. defaults to arch of computer
|
|
|
|
*/
|
|
|
|
export function findAllVersions(toolName: string, arch?: string): string[] {
|
|
|
|
const versions: string[] = []
|
|
|
|
|
|
|
|
arch = arch || os.arch()
|
2020-03-09 18:35:53 +00:00
|
|
|
const toolPath = path.join(_getCacheDirectory(), toolName)
|
2019-06-07 19:17:27 +00:00
|
|
|
|
|
|
|
if (fs.existsSync(toolPath)) {
|
|
|
|
const children: string[] = fs.readdirSync(toolPath)
|
|
|
|
for (const child of children) {
|
2021-05-07 20:13:26 +00:00
|
|
|
if (isExplicitVersion(child)) {
|
2019-06-07 19:17:27 +00:00
|
|
|
const fullPath = path.join(toolPath, child, arch || '')
|
|
|
|
if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) {
|
|
|
|
versions.push(child)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return versions
|
|
|
|
}
|
|
|
|
|
2020-05-19 17:25:57 +00:00
|
|
|
// versions-manifest
|
|
|
|
//
|
|
|
|
// typical pattern of a setup-* action that supports JIT would be:
|
|
|
|
// 1. resolve semver against local cache
|
|
|
|
//
|
|
|
|
// 2. if no match, download
|
|
|
|
// a. query versions manifest to match
|
|
|
|
// b. if no match, fall back to source if exists (tool distribution)
|
|
|
|
// c. with download url, download, install and preprent path
|
|
|
|
|
|
|
|
export type IToolRelease = mm.IToolRelease
|
|
|
|
export type IToolReleaseFile = mm.IToolReleaseFile
|
|
|
|
|
|
|
|
interface GitHubTreeItem {
|
|
|
|
path: string
|
|
|
|
size: string
|
|
|
|
url: string
|
|
|
|
}
|
|
|
|
|
|
|
|
interface GitHubTree {
|
|
|
|
tree: GitHubTreeItem[]
|
|
|
|
truncated: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getManifestFromRepo(
|
|
|
|
owner: string,
|
|
|
|
repo: string,
|
|
|
|
auth?: string,
|
|
|
|
branch = 'master'
|
|
|
|
): Promise<IToolRelease[]> {
|
|
|
|
let releases: IToolRelease[] = []
|
|
|
|
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}`
|
|
|
|
|
|
|
|
const http: httpm.HttpClient = new httpm.HttpClient('tool-cache')
|
2022-05-11 21:14:25 +00:00
|
|
|
const headers: OutgoingHttpHeaders = {}
|
2020-05-19 17:25:57 +00:00
|
|
|
if (auth) {
|
|
|
|
core.debug('set auth')
|
|
|
|
headers.authorization = auth
|
|
|
|
}
|
|
|
|
|
|
|
|
const response = await http.getJson<GitHubTree>(treeUrl, headers)
|
|
|
|
if (!response.result) {
|
|
|
|
return releases
|
|
|
|
}
|
|
|
|
|
|
|
|
let manifestUrl = ''
|
|
|
|
for (const item of response.result.tree) {
|
|
|
|
if (item.path === 'versions-manifest.json') {
|
|
|
|
manifestUrl = item.url
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
headers['accept'] = 'application/vnd.github.VERSION.raw'
|
|
|
|
let versionsRaw = await (await http.get(manifestUrl, headers)).readBody()
|
|
|
|
|
|
|
|
if (versionsRaw) {
|
|
|
|
// shouldn't be needed but protects against invalid json saved with BOM
|
|
|
|
versionsRaw = versionsRaw.replace(/^\uFEFF/, '')
|
|
|
|
try {
|
|
|
|
releases = JSON.parse(versionsRaw)
|
|
|
|
} catch {
|
|
|
|
core.debug('Invalid json')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return releases
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function findFromManifest(
|
|
|
|
versionSpec: string,
|
|
|
|
stable: boolean,
|
|
|
|
manifest: IToolRelease[],
|
|
|
|
archFilter: string = os.arch()
|
|
|
|
): Promise<IToolRelease | undefined> {
|
|
|
|
// wrap the internal impl
|
|
|
|
const match: mm.IToolRelease | undefined = await mm._findMatch(
|
|
|
|
versionSpec,
|
|
|
|
stable,
|
|
|
|
manifest,
|
|
|
|
archFilter
|
|
|
|
)
|
|
|
|
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
|
2019-06-06 18:16:48 +00:00
|
|
|
async function _createExtractFolder(dest?: string): Promise<string> {
|
|
|
|
if (!dest) {
|
|
|
|
// create a temp dir
|
2020-03-09 18:35:53 +00:00
|
|
|
dest = path.join(_getTempDirectory(), uuidV4())
|
2019-06-06 18:16:48 +00:00
|
|
|
}
|
|
|
|
await io.mkdirP(dest)
|
|
|
|
return dest
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _createToolPath(
|
|
|
|
tool: string,
|
|
|
|
version: string,
|
|
|
|
arch?: string
|
|
|
|
): Promise<string> {
|
|
|
|
const folderPath = path.join(
|
2020-03-09 18:35:53 +00:00
|
|
|
_getCacheDirectory(),
|
2019-06-06 18:16:48 +00:00
|
|
|
tool,
|
|
|
|
semver.clean(version) || version,
|
|
|
|
arch || ''
|
|
|
|
)
|
|
|
|
core.debug(`destination ${folderPath}`)
|
|
|
|
const markerPath = `${folderPath}.complete`
|
|
|
|
await io.rmRF(folderPath)
|
|
|
|
await io.rmRF(markerPath)
|
|
|
|
await io.mkdirP(folderPath)
|
|
|
|
return folderPath
|
|
|
|
}
|
|
|
|
|
|
|
|
function _completeToolPath(tool: string, version: string, arch?: string): void {
|
|
|
|
const folderPath = path.join(
|
2020-03-09 18:35:53 +00:00
|
|
|
_getCacheDirectory(),
|
2019-06-06 18:16:48 +00:00
|
|
|
tool,
|
|
|
|
semver.clean(version) || version,
|
|
|
|
arch || ''
|
|
|
|
)
|
|
|
|
const markerPath = `${folderPath}.complete`
|
|
|
|
fs.writeFileSync(markerPath, '')
|
|
|
|
core.debug('finished caching tool')
|
|
|
|
}
|
|
|
|
|
2021-05-07 20:13:26 +00:00
|
|
|
/**
|
|
|
|
* Check if version string is explicit
|
|
|
|
*
|
|
|
|
* @param versionSpec version string to check
|
|
|
|
*/
|
|
|
|
export function isExplicitVersion(versionSpec: string): boolean {
|
2019-06-06 18:16:48 +00:00
|
|
|
const c = semver.clean(versionSpec) || ''
|
|
|
|
core.debug(`isExplicit: ${c}`)
|
|
|
|
|
|
|
|
const valid = semver.valid(c) != null
|
|
|
|
core.debug(`explicit? ${valid}`)
|
|
|
|
|
|
|
|
return valid
|
|
|
|
}
|
|
|
|
|
2021-05-07 20:13:26 +00:00
|
|
|
/**
|
|
|
|
* Get the highest satisfiying semantic version in `versions` which satisfies `versionSpec`
|
|
|
|
*
|
|
|
|
* @param versions array of versions to evaluate
|
|
|
|
* @param versionSpec semantic version spec to satisfy
|
|
|
|
*/
|
|
|
|
|
|
|
|
export function evaluateVersions(
|
|
|
|
versions: string[],
|
|
|
|
versionSpec: string
|
|
|
|
): string {
|
2019-06-06 18:16:48 +00:00
|
|
|
let version = ''
|
|
|
|
core.debug(`evaluating ${versions.length} versions`)
|
|
|
|
versions = versions.sort((a, b) => {
|
|
|
|
if (semver.gt(a, b)) {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return -1
|
|
|
|
})
|
|
|
|
for (let i = versions.length - 1; i >= 0; i--) {
|
|
|
|
const potential: string = versions[i]
|
|
|
|
const satisfied: boolean = semver.satisfies(potential, versionSpec)
|
|
|
|
if (satisfied) {
|
|
|
|
version = potential
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (version) {
|
|
|
|
core.debug(`matched: ${version}`)
|
|
|
|
} else {
|
|
|
|
core.debug('match not found')
|
|
|
|
}
|
|
|
|
|
|
|
|
return version
|
|
|
|
}
|
2020-03-05 17:05:27 +00:00
|
|
|
|
2020-03-09 18:35:53 +00:00
|
|
|
/**
|
|
|
|
* Gets RUNNER_TOOL_CACHE
|
|
|
|
*/
|
|
|
|
function _getCacheDirectory(): string {
|
|
|
|
const cacheDirectory = process.env['RUNNER_TOOL_CACHE'] || ''
|
|
|
|
ok(cacheDirectory, 'Expected RUNNER_TOOL_CACHE to be defined')
|
|
|
|
return cacheDirectory
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets RUNNER_TEMP
|
|
|
|
*/
|
|
|
|
function _getTempDirectory(): string {
|
|
|
|
const tempDirectory = process.env['RUNNER_TEMP'] || ''
|
|
|
|
ok(tempDirectory, 'Expected RUNNER_TEMP to be defined')
|
|
|
|
return tempDirectory
|
|
|
|
}
|
|
|
|
|
2020-03-05 17:05:27 +00:00
|
|
|
/**
|
|
|
|
* Gets a global variable
|
|
|
|
*/
|
2020-03-09 18:35:53 +00:00
|
|
|
function _getGlobal<T>(key: string, defaultValue: T): T {
|
2020-03-05 17:05:27 +00:00
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
const value = (global as any)[key] as T | undefined
|
|
|
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
|
|
return value !== undefined ? value : defaultValue
|
|
|
|
}
|
2020-07-15 18:49:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an array of unique values.
|
|
|
|
* @param values Values to make unique.
|
|
|
|
*/
|
|
|
|
function _unique<T>(values: T[]): T[] {
|
|
|
|
return Array.from(new Set(values))
|
|
|
|
}
|