1
0
Fork 0

Add tool-cache (#12)

* Add tool-cache

* Format

* Fix linux bug

* Update readme

* Package zip/unzip

* Reorganize

* Format

* unzip somehow got corrupted, fixing

* Resolve remaining todos

* Don't log everything

* Pass error codes

* linting

* Branding and nits

* Fix for mac

* clean up windows/nix logic

* Clean up tests

* Export HTTPError

* Add option for custom 7z tool

* Lint/format

* Dont wipe temp/tool directories
pull/14/head
Danny McCormick 2019-06-06 14:16:48 -04:00 committed by GitHub
parent 9bf86bb363
commit 71a9b2d3ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1142 additions and 3 deletions

View File

@ -6,7 +6,6 @@
"core",
"actions"
],
"author": "Bryan MacFarlane <bryanmac@microsoft.com>",
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core",
"license": "MIT",
"main": "lib/core.js",

View File

@ -6,7 +6,6 @@
"exec",
"actions"
],
"author": "Bryan MacFarlane <bryanmac@microsoft.com>",
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
"license": "MIT",
"main": "lib/exec.js",

View File

@ -6,7 +6,6 @@
"io",
"actions"
],
"author": "Danny McCormick <damccorm@microsoft.com>",
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io",
"license": "MIT",
"main": "lib/io.js",

View File

@ -0,0 +1,7 @@
# `@actions/tool-cache`
> Functions necessary for downloading and caching tools.
## Usage
See [src/tool-cache.ts](src/tool-cache.ts).

Binary file not shown.

BIN
packages/tool-cache/__tests__/externals/zip vendored Executable file

Binary file not shown.

View File

@ -0,0 +1,338 @@
import * as fs from 'fs'
import * as nock from 'nock'
import * as path from 'path'
import * as io from '@actions/io'
import * as exec from '@actions/exec'
const cachePath = path.join(__dirname, 'CACHE')
const tempPath = path.join(__dirname, 'TEMP')
// Set temp and tool directories before importing (used to set global state)
process.env['RUNNER_TEMPDIRECTORY'] = tempPath
process.env['RUNNER_TOOLSDIRECTORY'] = cachePath
// eslint-disable-next-line import/first
import * as tc from '../src/tool-cache'
const IS_WINDOWS = process.platform === 'win32'
describe('@actions/tool-cache', function() {
beforeAll(function() {
nock('http://example.com')
.persist()
.get('/bytes/35')
.reply(200, {
username: 'abc',
password: 'def'
})
})
beforeEach(async function() {
await io.rmRF(cachePath)
await io.rmRF(tempPath)
await io.mkdirP(cachePath)
await io.mkdirP(tempPath)
})
afterAll(async function() {
await io.rmRF(tempPath)
await io.rmRF(cachePath)
})
it('downloads a 35 byte file', async () => {
const downPath: string = await tc.downloadTool(
'http://example.com/bytes/35'
)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
})
it('downloads a 35 byte file after a redirect', async () => {
nock('http://example.com')
.get('/redirect-to')
.reply(303, undefined, {
location: 'http://example.com/bytes/35'
})
const downPath: string = await tc.downloadTool(
'http://example.com/redirect-to'
)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
})
it('has status code in exception dictionary for HTTP error code responses', async () => {
nock('http://example.com')
.get('/bytes/bad')
.reply(400, {
username: 'bad',
password: 'file'
})
expect.assertions(2)
try {
const errorCodeUrl = 'http://example.com/bytes/bad'
await tc.downloadTool(errorCodeUrl)
} catch (err) {
expect(err.toString()).toContain('Unexpected HTTP response: 400')
expect(err['httpStatusCode']).toBe(400)
}
})
it('works with redirect code 302', async function() {
nock('http://example.com')
.get('/redirect-to')
.reply(302, undefined, {
location: 'http://example.com/bytes/35'
})
const downPath: string = await tc.downloadTool(
'http://example.com/redirect-to'
)
expect(fs.existsSync(downPath)).toBeTruthy()
expect(fs.statSync(downPath).size).toBe(35)
})
it('installs a binary tool and finds it', async () => {
const downPath: string = await tc.downloadTool(
'http://example.com/bytes/35'
)
expect(fs.existsSync(downPath)).toBeTruthy()
await tc.cacheFile(downPath, 'foo', 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
const binaryPath: string = path.join(toolPath, 'foo')
expect(fs.existsSync(binaryPath)).toBeTruthy()
})
if (IS_WINDOWS) {
it('installs a 7z and finds it', async () => {
const tempDir = path.join(__dirname, 'test-install-7z')
try {
await io.mkdirP(tempDir)
// copy the 7z file to the test dir
const _7zFile: string = path.join(tempDir, 'test.7z')
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
// extract/cache
const extPath: string = await tc.extract7z(_7zFile)
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
const toolPath: string = tc.find('my-7z-contents', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
}
})
it('extract 7z using custom 7z tool', async function() {
const tempDir = path.join(
__dirname,
'test-extract-7z-using-custom-7z-tool'
)
try {
await io.mkdirP(tempDir)
// create mock7zr.cmd
const mock7zrPath: string = path.join(tempDir, 'mock7zr.cmd')
fs.writeFileSync(
mock7zrPath,
[
'echo %* > "%~dp0mock7zr-args.txt"',
`"${path.join(
__dirname,
'..',
'scripts',
'externals',
'7zdec.exe'
)}" x %5`
].join('\r\n')
)
// copy the 7z file to the test dir
const _7zFile: string = path.join(tempDir, 'test.7z')
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
// extract
const extPath: string = await tc.extract7z(
_7zFile,
undefined,
mock7zrPath
)
expect(fs.existsSync(extPath)).toBeTruthy()
expect(
fs.existsSync(path.join(tempDir, 'mock7zr-args.txt'))
).toBeTruthy()
expect(
fs
.readFileSync(path.join(tempDir, 'mock7zr-args.txt'))
.toString()
.trim()
).toBe(`x -bb1 -bd -sccUTF-8 ${_7zFile}`)
expect(fs.existsSync(path.join(extPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(extPath, 'file-with-ç-character.txt'))
).toBeTruthy()
expect(
fs.existsSync(path.join(extPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
}
})
}
it('installs a zip and finds it', async () => {
const tempDir = path.join(__dirname, 'test-install-zip')
try {
await io.mkdirP(tempDir)
// stage the layout for a zip file:
// file.txt
// folder/nested-file.txt
const stagingDir = path.join(tempDir, 'zip-staging')
await io.mkdirP(path.join(stagingDir, 'folder'))
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
// create the zip
const zipFile = path.join(tempDir, 'test.zip')
await io.rmRF(zipFile)
if (IS_WINDOWS) {
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
const escapedZipFile = zipFile.replace(/'/g, "''")
const powershellPath = await io.which('powershell', true)
const args = [
'-NoLogo',
'-Sta',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
]
await exec.exec(`"${powershellPath}"`, args)
} else {
const zipPath: string = path.join(__dirname, 'externals', 'zip')
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
}
const extPath: string = await tc.extractZip(zipFile)
await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(tempDir)
}
})
it('installs a zip and extracts it to specified directory', async function() {
const tempDir = path.join(__dirname, 'test-install-zip')
try {
await io.mkdirP(tempDir)
// stage the layout for a zip file:
// file.txt
// folder/nested-file.txt
const stagingDir = path.join(tempDir, 'zip-staging')
await io.mkdirP(path.join(stagingDir, 'folder'))
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
// create the zip
const zipFile = path.join(tempDir, 'test.zip')
await io.rmRF(zipFile)
if (IS_WINDOWS) {
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
const escapedZipFile = zipFile.replace(/'/g, "''")
const powershellPath = await io.which('powershell', true)
const args = [
'-NoLogo',
'-Sta',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
]
await exec.exec(`"${powershellPath}"`, args)
} else {
const zipPath = path.join(__dirname, 'externals', 'zip')
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
}
const destDir = path.join(__dirname, 'unzip-dest')
await io.rmRF(destDir)
await io.mkdirP(destDir)
try {
const extPath: string = await tc.extractZip(zipFile, destDir)
await tc.cacheDir(extPath, 'foo', '1.1.0')
const toolPath: string = tc.find('foo', '1.1.0')
expect(fs.existsSync(toolPath)).toBeTruthy()
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
expect(
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
).toBeTruthy()
} finally {
await io.rmRF(destDir)
}
} finally {
await io.rmRF(tempDir)
}
})
it('works with a 502 temporary failure', async function() {
nock('http://example.com')
.get('/temp502')
.twice()
.reply(502, undefined)
nock('http://example.com')
.get('/temp502')
.reply(200, undefined)
const statusCodeUrl = 'http://example.com/temp502'
await tc.downloadTool(statusCodeUrl)
})
it("doesn't retry 502s more than 3 times", async function() {
nock('http://example.com')
.get('/perm502')
.times(3)
.reply(502, undefined)
expect.assertions(1)
try {
const statusCodeUrl = 'http://example.com/perm502'
await tc.downloadTool(statusCodeUrl)
} catch (err) {
expect(err.toString()).toContain('502')
}
})
})

205
packages/tool-cache/package-lock.json generated Normal file
View File

@ -0,0 +1,205 @@
{
"name": "@actions/tool-cache",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/nock": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz",
"integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true
},
"@types/semver": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.0.tgz",
"integrity": "sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ==",
"dev": true
},
"@types/uuid": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz",
"integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"chai": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
"dev": true,
"requires": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.2",
"deep-eql": "^3.0.1",
"get-func-name": "^2.0.0",
"pathval": "^1.1.0",
"type-detect": "^4.0.5"
}
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"dev": true
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
},
"nock": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
"integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==",
"dev": true,
"requires": {
"chai": "^4.1.2",
"debug": "^4.1.0",
"deep-equal": "^1.0.0",
"json-stringify-safe": "^5.0.1",
"lodash": "^4.17.5",
"mkdirp": "^0.5.0",
"propagate": "^1.0.0",
"qs": "^6.5.1",
"semver": "^5.5.0"
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
}
}
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"propagate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
"integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
"dev": true
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true
},
"semver": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.1.0.tgz",
"integrity": "sha512-kCqEOOHoBcFs/2Ccuk4Xarm/KiWRSLEX9CAZF8xkJ6ZPlIoTZ8V5f7J16vYLJqDbR7KrxTJpR2lqjIEm2Qx9cQ=="
},
"tunnel": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
},
"type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"typed-rest-client": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.4.0.tgz",
"integrity": "sha512-f+3+X13CIpkv0WvFERkXq4aH5BYzyeYclf8t+X7oa/YaE80EjYW12kphY0aEQBaL9RzChP0MSbsVhB4X+bzyDw==",
"requires": {
"tunnel": "0.0.4",
"underscore": "1.8.3"
}
},
"underscore": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
}

View File

@ -0,0 +1,48 @@
{
"name": "@actions/tool-cache",
"version": "1.0.0",
"description": "Actions tool-cache lib",
"keywords": [
"exec",
"actions"
],
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
"license": "MIT",
"main": "lib/tool-cache.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib",
"scripts"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/actions/toolkit.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
"tsc": "tsc"
},
"bugs": {
"url": "https://github.com/actions/toolkit/issues"
},
"dependencies": {
"semver": "^6.1.0",
"typed-rest-client": "^1.4.0",
"uuid": "^3.3.2",
"@actions/core": "^0.1.0",
"@actions/io": "^1.0.0",
"@actions/exec": "^1.0.0"
},
"devDependencies": {
"nock": "^10.0.6",
"@types/nock": "^10.0.3",
"@types/semver": "^6.0.0",
"@types/uuid": "^3.4.4"
}
}

View File

@ -0,0 +1,60 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[Parameter(Mandatory = $true)]
[string]$Target)
# This script translates the output from 7zdec into UTF8. Node has limited
# built-in support for encodings.
#
# 7zdec uses the system default code page. The system default code page varies
# depending on the locale configuration. On an en-US box, the system default code
# page is Windows-1252.
#
# Note, on a typical en-US box, testing with the 'ç' character is a good way to
# determine whether data is passed correctly between processes. This is because
# the 'ç' character has a different code point across each of the common encodings
# on a typical en-US box, i.e.
# 1) the default console-output code page (IBM437)
# 2) the system default code page (i.e. CP_ACP) (Windows-1252)
# 3) UTF8
$ErrorActionPreference = 'Stop'
# Redefine the wrapper over STDOUT to use UTF8. Node expects UTF8 by default.
$stdout = [System.Console]::OpenStandardOutput()
$utf8 = New-Object System.Text.UTF8Encoding($false) # do not emit BOM
$writer = New-Object System.IO.StreamWriter($stdout, $utf8)
[System.Console]::SetOut($writer)
# All subsequent output must be written using [System.Console]::WriteLine(). In
# PowerShell 4, Write-Host and Out-Default do not consider the updated stream writer.
Set-Location -LiteralPath $Target
# Print the ##command.
$_7zdec = Join-Path -Path "$PSScriptRoot" -ChildPath "externals/7zdec.exe"
[System.Console]::WriteLine("##[command]$_7zdec x `"$Source`"")
# The $OutputEncoding variable instructs PowerShell how to interpret the output
# from the external command.
$OutputEncoding = [System.Text.Encoding]::Default
# Note, the output from 7zdec.exe needs to be iterated over. Otherwise PowerShell.exe
# will launch the external command in such a way that it inherits the streams.
& $_7zdec x $Source 2>&1 |
ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) {
[System.Console]::WriteLine($_.Exception.Message)
}
else {
[System.Console]::WriteLine($_)
}
}
[System.Console]::WriteLine("##[debug]7zdec.exe exit code '$LASTEXITCODE'")
[System.Console]::Out.Flush()
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}

Binary file not shown.

BIN
packages/tool-cache/scripts/externals/unzip vendored Executable file

Binary file not shown.

View File

@ -0,0 +1,473 @@
import * as core from '@actions/core'
import * as io from '@actions/io'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as httpm from 'typed-rest-client/HttpClient'
import * as semver from 'semver'
import * as uuidV4 from 'uuid/v4'
import {exec} from '@actions/exec/lib/exec'
import {ExecOptions} from '@actions/exec/lib/interfaces'
import {ok} from 'assert'
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'
const userAgent = 'actions/tool-cache'
// On load grab temp directory and cache directory and remove them from env (currently don't want to expose this)
let tempDirectory: string = process.env['RUNNER_TEMPDIRECTORY'] || ''
let cacheRoot: string = process.env['RUNNER_TOOLSDIRECTORY'] || ''
// If directories not found, place them in common temp locations
if (!tempDirectory || !cacheRoot) {
let baseLocation: string
if (IS_WINDOWS) {
// On windows use the USERPROFILE env variable
baseLocation = process.env['USERPROFILE'] || 'C:\\'
} else {
if (process.platform === 'darwin') {
baseLocation = '/Users'
} else {
baseLocation = '/home'
}
}
if (!tempDirectory) {
tempDirectory = path.join(baseLocation, 'actions', 'temp')
}
if (!cacheRoot) {
cacheRoot = path.join(baseLocation, 'actions', 'cache')
}
}
/**
* Download a tool from an url and stream it into a file
*
* @param url url of tool to download
* @returns path to downloaded tool
*/
export async function downloadTool(url: string): Promise<string> {
// Wrap in a promise so that we can resolve from within stream callbacks
return new Promise<string>(async (resolve, reject) => {
try {
const http = new httpm.HttpClient(userAgent, [], {
allowRetries: true,
maxRetries: 3
})
const destPath = path.join(tempDirectory, uuidV4())
await io.mkdirP(tempDirectory)
core.debug(`Downloading ${url}`)
core.debug(`Downloading ${destPath}`)
if (fs.existsSync(destPath)) {
throw new Error(`Destination file path ${destPath} already exists`)
}
const response: httpm.HttpClientResponse = await http.get(url)
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
}
const file: NodeJS.WritableStream = fs.createWriteStream(destPath)
file.on('open', async () => {
try {
const stream = response.message.pipe(file)
stream.on('close', () => {
core.debug('download complete')
resolve(destPath)
})
} catch (err) {
core.debug(
`Failed to download from "${url}". Code(${
response.message.statusCode
}) Message(${response.message.statusMessage})`
)
reject(err)
}
})
file.on('error', err => {
file.end()
reject(err)
})
} catch (err) {
reject(err)
}
})
}
/**
* 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')
dest = dest || (await _createExtractFolder(dest))
const originalCwd = process.cwd()
process.chdir(dest)
if (_7zPath) {
try {
const args: string[] = [
'x', // eXtract files with full paths
'-bb1', // -bb[0-3] : set output log level
'-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
}
/**
* Extract a tar
*
* @param file path to the tar
* @param dest destination directory. Optional.
* @returns path to the destination directory
*/
export async function extractTar(file: string, dest?: string): Promise<string> {
if (!file) {
throw new Error("parameter 'file' is required")
}
dest = dest || (await _createExtractFolder(dest))
const tarPath: string = await io.which('tar', true)
await exec(`"${tarPath}"`, ['xzC', dest, '-f', file])
return dest
}
/**
* 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")
}
dest = dest || (await _createExtractFolder(dest))
if (IS_WINDOWS) {
await extractZipWin(file, dest)
} else {
await extractZipNix(file, dest)
}
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, '')
const command = `$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}')`
// run powershell
const powershellPath = await io.which('powershell')
const args = [
'-NoLogo',
'-Sta',
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy',
'Unrestricted',
'-Command',
command
]
await exec(`"${powershellPath}"`, args)
}
async function extractZipNix(file: string, dest: string): Promise<void> {
const unzipPath = path.join(__dirname, '..', 'scripts', 'externals', 'unzip')
await exec(`"${unzipPath}"`, [file], {cwd: dest})
}
/**
* 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
}
/**
* 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
*/
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
if (!_isExplicitVersion(versionSpec)) {
const localVersions: string[] = _findLocalToolVersions(toolName, arch)
const match = _evaluateVersions(localVersions, versionSpec)
versionSpec = match
}
// check for the explicit version in the cache
let toolPath = ''
if (versionSpec) {
versionSpec = semver.clean(versionSpec) || ''
const cachePath = path.join(cacheRoot, toolName, versionSpec, arch)
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
}
async function _createExtractFolder(dest?: string): Promise<string> {
if (!dest) {
// create a temp dir
dest = path.join(tempDirectory, uuidV4())
}
await io.mkdirP(dest)
return dest
}
async function _createToolPath(
tool: string,
version: string,
arch?: string
): Promise<string> {
const folderPath = path.join(
cacheRoot,
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(
cacheRoot,
tool,
semver.clean(version) || version,
arch || ''
)
const markerPath = `${folderPath}.complete`
fs.writeFileSync(markerPath, '')
core.debug('finished caching tool')
}
function _isExplicitVersion(versionSpec: string): boolean {
const c = semver.clean(versionSpec) || ''
core.debug(`isExplicit: ${c}`)
const valid = semver.valid(c) != null
core.debug(`explicit? ${valid}`)
return valid
}
function _evaluateVersions(versions: string[], versionSpec: string): string {
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
}
function _findLocalToolVersions(toolName: string, arch?: string): string[] {
const versions: string[] = []
arch = arch || os.arch()
const toolPath = path.join(cacheRoot, toolName)
if (fs.existsSync(toolPath)) {
const children: string[] = fs.readdirSync(toolPath)
for (const child of children) {
if (_isExplicitVersion(child)) {
const fullPath = path.join(toolPath, child, arch || '')
if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) {
versions.push(child)
}
}
}
}
return versions
}

View File

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "./lib",
"rootDir": "./src"
},
"include": [
"./src"
]
}