diff --git a/.gitignore b/.gitignore index c13ba4b5..f543c3ae 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ packages/*/node_modules/ packages/*/lib/ packages/*/__tests__/_temp/ .DS_Store +*.xar packages/*/audit.json diff --git a/packages/tool-cache/README.md b/packages/tool-cache/README.md index f45cedd8..d3da0c35 100644 --- a/packages/tool-cache/README.md +++ b/packages/tool-cache/README.md @@ -29,6 +29,10 @@ if (process.platform === 'win32') { const node12Path = await tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-win-x64.7z'); const node12ExtractedFolder = await tc.extract7z(node12Path, 'path/to/extract/to'); } +else if (process.platform === 'darwin') { + const node12Path = await tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0.pkg'); + const node12ExtractedFolder = await tc.extractXar(node12Path, 'path/to/extract/to'); +} else { const node12Path = await tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-linux-x64.tar.gz'); const node12ExtractedFolder = await tc.extractTar(node12Path, 'path/to/extract/to'); diff --git a/packages/tool-cache/__tests__/data/archive-content/file-with-ç-character.txt b/packages/tool-cache/__tests__/data/archive-content/file-with-ç-character.txt new file mode 100644 index 00000000..5803ea15 --- /dev/null +++ b/packages/tool-cache/__tests__/data/archive-content/file-with-ç-character.txt @@ -0,0 +1 @@ +file-with-ç-character.txt \ No newline at end of file diff --git a/packages/tool-cache/__tests__/data/archive-content/file.txt b/packages/tool-cache/__tests__/data/archive-content/file.txt new file mode 100644 index 00000000..f2da98ad --- /dev/null +++ b/packages/tool-cache/__tests__/data/archive-content/file.txt @@ -0,0 +1 @@ +file.txt contents \ No newline at end of file diff --git a/packages/tool-cache/__tests__/data/archive-content/folder/nested-file.txt b/packages/tool-cache/__tests__/data/archive-content/folder/nested-file.txt new file mode 100644 index 00000000..37922426 --- /dev/null +++ b/packages/tool-cache/__tests__/data/archive-content/folder/nested-file.txt @@ -0,0 +1 @@ +folder/nested-file.txt contents \ No newline at end of file diff --git a/packages/tool-cache/__tests__/tool-cache.test.ts b/packages/tool-cache/__tests__/tool-cache.test.ts index 7dc17c67..3722d18b 100644 --- a/packages/tool-cache/__tests__/tool-cache.test.ts +++ b/packages/tool-cache/__tests__/tool-cache.test.ts @@ -15,6 +15,7 @@ process.env['RUNNER_TOOL_CACHE'] = cachePath import * as tc from '../src/tool-cache' const IS_WINDOWS = process.platform === 'win32' +const IS_MAC = process.platform === 'darwin' describe('@actions/tool-cache', function() { beforeAll(function() { @@ -346,6 +347,110 @@ describe('@actions/tool-cache', function() { await io.rmRF(tempDir) } }) + } else if (IS_MAC) { + it('extract .xar', async () => { + const tempDir = path.join(tempPath, 'test-install.xar') + const sourcePath = path.join(__dirname, 'data', 'archive-content') + const targetPath = path.join(tempDir, 'test.xar') + await io.mkdirP(tempDir) + + // Create test archive + const xarPath = await io.which('xar', true) + await exec.exec(`${xarPath}`, ['-cf', targetPath, '.'], { + cwd: sourcePath + }) + + // extract/cache + const extPath: string = await tc.extractXar(targetPath, undefined, '-x') + await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0') + const toolPath: string = tc.find('my-xar-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() + expect( + fs.readFileSync( + path.join(toolPath, 'folder', 'nested-file.txt'), + 'utf8' + ) + ).toBe('folder/nested-file.txt contents') + }) + + it('extract .xar to a directory that does not exist', async () => { + const tempDir = path.join(tempPath, 'test-install.xar') + const sourcePath = path.join(__dirname, 'data', 'archive-content') + const targetPath = path.join(tempDir, 'test.xar') + await io.mkdirP(tempDir) + + const destDir = path.join(tempDir, 'not-exist') + + // Create test archive + const xarPath = await io.which('xar', true) + await exec.exec(`${xarPath}`, ['-cf', targetPath, '.'], { + cwd: sourcePath + }) + + // extract/cache + const extPath: string = await tc.extractXar(targetPath, destDir, '-x') + await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0') + const toolPath: string = tc.find('my-xar-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() + expect( + fs.readFileSync( + path.join(toolPath, 'folder', 'nested-file.txt'), + 'utf8' + ) + ).toBe('folder/nested-file.txt contents') + }) + + it('extract .xar without flags', async () => { + const tempDir = path.join(tempPath, 'test-install.xar') + const sourcePath = path.join(__dirname, 'data', 'archive-content') + const targetPath = path.join(tempDir, 'test.xar') + await io.mkdirP(tempDir) + + // Create test archive + const xarPath = await io.which('xar', true) + await exec.exec(`${xarPath}`, ['-cf', targetPath, '.'], { + cwd: sourcePath + }) + + // extract/cache + const extPath: string = await tc.extractXar(targetPath, undefined) + await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0') + const toolPath: string = tc.find('my-xar-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() + expect( + fs.readFileSync( + path.join(toolPath, 'folder', 'nested-file.txt'), + 'utf8' + ) + ).toBe('folder/nested-file.txt contents') + }) } it('extract .tar.gz', async () => { diff --git a/packages/tool-cache/src/tool-cache.ts b/packages/tool-cache/src/tool-cache.ts index 385569ca..3e9ecab4 100644 --- a/packages/tool-cache/src/tool-cache.ts +++ b/packages/tool-cache/src/tool-cache.ts @@ -23,6 +23,7 @@ export class HTTPError extends Error { } const IS_WINDOWS = process.platform === 'win32' +const IS_MAC = process.platform === 'darwin' const userAgent = 'actions/tool-cache' /** @@ -276,6 +277,43 @@ export async function extractTar( return dest } +/** + * 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 { + 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 +} + /** * Extract a zip * @@ -675,3 +713,11 @@ function _getGlobal(key: string, defaultValue: T): T { /* eslint-enable @typescript-eslint/no-explicit-any */ return value !== undefined ? value : defaultValue } + +/** + * Returns an array of unique values. + * @param values Values to make unique. + */ +function _unique(values: T[]): T[] { + return Array.from(new Set(values)) +}