Files
toolkit/packages/cache/src/internal/tar.ts
T

285 lines
8.2 KiB
TypeScript
Raw Normal View History

import {exec} from '@actions/exec'
import * as io from '@actions/io'
import {existsSync, writeFileSync} from 'fs'
import * as path from 'path'
import * as utils from './cacheUtils'
2022-11-28 10:24:40 +00:00
import {ArchiveTool} from './contracts'
import {
CompressionMethod,
SystemTarPathOnWindows,
2022-11-29 10:35:22 +00:00
ArchiveToolType,
TarFilename,
ManifestFilename
2022-11-28 10:24:40 +00:00
} from './constants'
const IS_WINDOWS = process.platform === 'win32'
2022-11-16 20:18:31 +00:00
// Function also mutates the args array. For non-mutation call with passing an empty array.
2022-11-28 10:24:40 +00:00
async function getTarPath(): Promise<ArchiveTool> {
2021-02-02 20:09:10 +01:00
switch (process.platform) {
case 'win32': {
2022-11-15 08:41:04 +00:00
const gnuTar = await utils.getGnuTarPathOnWindows()
2022-11-23 07:39:15 +00:00
const systemTar = SystemTarPathOnWindows
2022-11-15 08:41:04 +00:00
if (gnuTar) {
2022-11-08 08:47:35 +00:00
// Use GNUtar as default on windows
2022-11-28 10:24:40 +00:00
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU}
2021-02-02 20:09:10 +01:00
} else if (existsSync(systemTar)) {
2022-11-28 10:24:40 +00:00
return <ArchiveTool>{path: systemTar, type: ArchiveToolType.BSD}
2021-02-02 20:09:10 +01:00
}
break
}
2021-02-02 20:09:10 +01:00
case 'darwin': {
const gnuTar = await io.which('gtar', false)
if (gnuTar) {
2021-04-09 13:38:55 -05:00
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
2022-11-28 10:24:40 +00:00
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU}
} else {
return <ArchiveTool>{
path: await io.which('tar', true),
type: ArchiveToolType.BSD
}
2021-02-02 20:09:10 +01:00
}
}
default:
break
}
2022-11-28 10:24:40 +00:00
return <ArchiveTool>{
path: await io.which('tar', true),
type: ArchiveToolType.GNU
}
}
2022-11-28 10:24:40 +00:00
// Return arguments for tar as per tarPath, compressionMethod, method type and os
2022-11-23 07:39:15 +00:00
async function getTarArgs(
2022-11-28 10:24:40 +00:00
tarPath: ArchiveTool,
2022-11-23 07:39:15 +00:00
compressionMethod: CompressionMethod,
type: string,
archivePath = ''
): Promise<string[]> {
2022-11-29 12:16:17 +00:00
const args = [`"${tarPath.path}"`]
2022-11-23 07:39:15 +00:00
const cacheFileName = utils.getCacheFileName(compressionMethod)
const tarFile = 'cache.tar'
const workingDirectory = getWorkingDirectory()
2022-11-23 10:44:38 +00:00
const BSD_TAR_ZSTD =
2022-11-28 10:24:40 +00:00
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
2022-11-23 07:39:15 +00:00
// Method specific args
switch (type) {
case 'create':
args.push(
'--posix',
'-cf',
2022-11-23 10:44:38 +00:00
BSD_TAR_ZSTD
2022-11-23 07:39:15 +00:00
? tarFile
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'--exclude',
2022-11-23 10:44:38 +00:00
BSD_TAR_ZSTD
2022-11-23 07:39:15 +00:00
? tarFile
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'--files-from',
2022-11-29 10:35:22 +00:00
ManifestFilename
2022-11-23 07:39:15 +00:00
)
break
case 'extract':
args.push(
'-xf',
2022-11-23 10:44:38 +00:00
BSD_TAR_ZSTD
2022-11-23 07:39:15 +00:00
? tarFile
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P',
'-C',
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
)
break
case 'list':
args.push(
'-tf',
2022-11-23 10:44:38 +00:00
BSD_TAR_ZSTD
2022-11-23 07:39:15 +00:00
? tarFile
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
'-P'
)
break
}
// Platform specific args
2022-11-28 10:24:40 +00:00
if (tarPath.type === ArchiveToolType.GNU) {
switch (process.platform) {
case 'win32':
2022-11-23 07:39:15 +00:00
args.push('--force-local')
2022-11-28 10:24:40 +00:00
break
case 'darwin':
2022-11-23 07:39:15 +00:00
args.push('--delay-directory-restore')
2022-11-28 10:24:40 +00:00
break
2022-11-23 07:39:15 +00:00
}
}
return args
}
async function getCommands(
2022-11-28 10:24:40 +00:00
compressionMethod: CompressionMethod,
type: string,
archivePath = ''
): Promise<string[]> {
let args
2022-12-09 09:33:59 +00:00
2022-11-28 10:24:40 +00:00
const tarPath = await getTarPath()
const tarArgs = await getTarArgs(
tarPath,
compressionMethod,
type,
archivePath
)
const compressionArgs =
type !== 'create'
? await getDecompressionProgram(tarPath, compressionMethod, archivePath)
: await getCompressionProgram(tarPath, compressionMethod)
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
2022-12-09 09:33:59 +00:00
2022-11-28 10:24:40 +00:00
if (BSD_TAR_ZSTD && type !== 'create') {
args = [...compressionArgs, ...tarArgs]
2022-11-28 10:24:40 +00:00
} else {
args = [...tarArgs, ...compressionArgs]
}
2022-12-09 09:33:59 +00:00
if (BSD_TAR_ZSTD) {
return args
2022-12-09 09:33:59 +00:00
}
return [args.join(' ')]
}
function getWorkingDirectory(): string {
return process.env['GITHUB_WORKSPACE'] ?? process.cwd()
}
// Common function for extractTar and listTar to get the compression method
2022-11-28 10:24:40 +00:00
async function getDecompressionProgram(
tarPath: ArchiveTool,
2022-11-23 11:40:11 +00:00
compressionMethod: CompressionMethod,
archivePath: string
2022-11-17 09:12:53 +00:00
): Promise<string[]> {
// -d: Decompress.
// unzstd is equivalent to 'zstd -d'
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
2022-11-23 10:44:38 +00:00
const BSD_TAR_ZSTD =
2022-11-28 10:24:40 +00:00
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
switch (compressionMethod) {
case CompressionMethod.Zstd:
2022-11-23 10:44:38 +00:00
return BSD_TAR_ZSTD
? [
'zstd -d --long=30 -o',
2022-11-29 10:35:22 +00:00
TarFilename,
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
2022-11-23 10:44:38 +00:00
]
: [
'--use-compress-program',
2022-11-30 10:11:07 +00:00
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
2022-11-23 10:44:38 +00:00
]
case CompressionMethod.ZstdWithoutLong:
2022-11-23 10:44:38 +00:00
return BSD_TAR_ZSTD
? [
'zstd -d -o',
2022-11-29 10:35:22 +00:00
TarFilename,
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
2022-11-23 10:44:38 +00:00
]
2022-11-30 10:11:07 +00:00
: ['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd']
default:
return ['-z']
}
}
2022-11-28 10:24:40 +00:00
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
// zstdmt is equivalent to 'zstd -T0'
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
// Using 30 here because we also support 32-bit self-hosted runners.
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
async function getCompressionProgram(
tarPath: ArchiveTool,
compressionMethod: CompressionMethod
): Promise<string[]> {
const cacheFileName = utils.getCacheFileName(compressionMethod)
const BSD_TAR_ZSTD =
tarPath.type === ArchiveToolType.BSD &&
compressionMethod !== CompressionMethod.Gzip &&
IS_WINDOWS
switch (compressionMethod) {
case CompressionMethod.Zstd:
return BSD_TAR_ZSTD
? [
'zstd -T0 --long=30 -o',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
2022-11-29 10:35:22 +00:00
TarFilename
2022-11-28 10:24:40 +00:00
]
: [
'--use-compress-program',
2022-11-30 10:11:07 +00:00
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
2022-11-28 10:24:40 +00:00
]
case CompressionMethod.ZstdWithoutLong:
return BSD_TAR_ZSTD
? [
'zstd -T0 -o',
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
2022-11-29 10:35:22 +00:00
TarFilename
2022-11-28 10:24:40 +00:00
]
2022-11-30 10:11:07 +00:00
: ['--use-compress-program', IS_WINDOWS ? '"zstd -T0"' : 'zstdmt']
2022-11-28 10:24:40 +00:00
default:
return ['-z']
}
}
async function execCommands(commands: string[], cwd?: string): Promise<void> {
for (const command of commands) {
try {
await exec(command, undefined, {cwd})
} catch (error) {
throw new Error(`${command[0]} failed with error: ${error?.message}`)
}
}
}
export async function listTar(
archivePath: string,
compressionMethod: CompressionMethod
): Promise<void> {
const commands = await getCommands(compressionMethod, 'list', archivePath)
await execCommands(commands)
}
export async function extractTar(
archivePath: string,
compressionMethod: CompressionMethod
): Promise<void> {
// Create directory to extract tar into
const workingDirectory = getWorkingDirectory()
await io.mkdirP(workingDirectory)
const commands = await getCommands(compressionMethod, 'extract', archivePath)
await execCommands(commands)
}
export async function createTar(
archiveFolder: string,
sourceDirectories: string[],
compressionMethod: CompressionMethod
): Promise<void> {
// Write source directories to manifest.txt to avoid command length limits
writeFileSync(
2022-11-29 12:16:17 +00:00
path.join(archiveFolder, ManifestFilename),
sourceDirectories.join('\n')
)
const commands = await getCommands(compressionMethod, 'create')
await execCommands(commands, archiveFolder)
2022-08-18 11:44:15 +00:00
}