Skip to content
56 changes: 34 additions & 22 deletions packages/cache/__tests__/tar.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as exec from '@actions/exec'
import * as io from '@actions/io'
import * as path from 'path'
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
import {
CacheFilename,
CompressionMethod,
GnuTarPathOnWindows
} from '../src/internal/constants'
import * as tar from '../src/internal/tar'
import * as utils from '../src/internal/cacheUtils'
// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand All @@ -28,6 +32,10 @@ beforeAll(async () => {
await jest.requireActual('@actions/io').rmRF(getTempDir())
})

beforeEach(async () => {
jest.restoreAllMocks()
})

afterAll(async () => {
delete process.env['GITHUB_WORKSPACE']
await jest.requireActual('@actions/io').rmRF(getTempDir())
Expand All @@ -41,13 +49,14 @@ test('zstd extract tar', async () => {
? `${process.env['windir']}\\fakepath\\cache.tar`
: 'cache.tar'
const workspace = process.env['GITHUB_WORKSPACE']
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath

await tar.extractTar(archivePath, CompressionMethod.Zstd)

expect(mkdirMock).toHaveBeenCalledWith(workspace)
expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
`"${tarPath}"`,
[
'--use-compress-program',
IS_WINDOWS ? 'zstd -d --long=30' : 'unzstd --long=30',
Expand All @@ -74,9 +83,7 @@ test('gzip extract tar', async () => {
await tar.extractTar(archivePath, CompressionMethod.Gzip)

expect(mkdirMock).toHaveBeenCalledWith(workspace)
const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
Expand All @@ -87,18 +94,19 @@ test('gzip extract tar', async () => {
'-P',
'-C',
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
]
Comment thread
Phantsure marked this conversation as resolved.
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{cwd: undefined}
)
})

test('gzip extract GNU tar on windows', async () => {
test('gzip extract GNU tar on windows with GNUtar in path', async () => {
if (IS_WINDOWS) {
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false)

// GNU tar present in path but not at default location
const isGnuMock = jest
.spyOn(utils, 'isGnuTarInstalled')
.mockReturnValue(Promise.resolve(true))
.spyOn(utils, 'getGnuTarPathOnWindows')
.mockReturnValue(Promise.resolve('C:\\Program Files\\gnutar\\tar.exe'))
const execMock = jest.spyOn(exec, 'exec')
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
const workspace = process.env['GITHUB_WORKSPACE']
Expand Down Expand Up @@ -134,9 +142,11 @@ test('zstd create tar', async () => {

await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd)

const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath

expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
`"${tarPath}"`,
[
'--posix',
'--use-compress-program',
Expand Down Expand Up @@ -170,9 +180,7 @@ test('gzip create tar', async () => {

await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip)

const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath

expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
Expand All @@ -189,7 +197,9 @@ test('gzip create tar', async () => {
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
'--files-from',
'manifest.txt'
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
]
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{
cwd: archiveFolder
}
Expand All @@ -205,9 +215,10 @@ test('zstd list tar', async () => {

await tar.listTar(archivePath, CompressionMethod.Zstd)

const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
`"${tarPath}"`,
[
'--use-compress-program',
IS_WINDOWS ? 'zstd -d --long=30' : 'unzstd --long=30',
Expand All @@ -230,9 +241,10 @@ test('zstdWithoutLong list tar', async () => {

await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)

const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${defaultTarPath}"`,
`"${tarPath}"`,
[
'--use-compress-program',
IS_WINDOWS ? 'zstd -d' : 'unzstd',
Expand All @@ -254,9 +266,7 @@ test('gzip list tar', async () => {

await tar.listTar(archivePath, CompressionMethod.Gzip)

const tarPath = IS_WINDOWS
? `${process.env['windir']}\\System32\\tar.exe`
: defaultTarPath
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
expect(execMock).toHaveBeenCalledTimes(1)
expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
Expand All @@ -265,7 +275,9 @@ test('gzip list tar', async () => {
'-tf',
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
'-P'
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
]
.concat(IS_WINDOWS ? ['--force-local'] : [])
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
{cwd: undefined}
)
})
15 changes: 11 additions & 4 deletions packages/cache/src/internal/cacheUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import * as path from 'path'
import * as semver from 'semver'
import * as util from 'util'
import {v4 as uuidV4} from 'uuid'
import {CacheFilename, CompressionMethod} from './constants'
import {
CacheFilename,
CompressionMethod,
GnuTarPathOnWindows
} from './constants'

// From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23
export async function createTempDirectory(): Promise<string> {
Expand Down Expand Up @@ -90,7 +94,7 @@ async function getVersion(app: string): Promise<string> {

// Use zstandard if possible to maximize cache performance
export async function getCompressionMethod(): Promise<CompressionMethod> {
if (process.platform === 'win32' && !(await isGnuTarInstalled())) {
if (process.platform === 'win32' && !(await getGnuTarPathOnWindows())) {
// Disable zstd due to bug https://github.com/actions/cache/issues/301
return CompressionMethod.Gzip
}
Expand All @@ -116,9 +120,12 @@ export function getCacheFileName(compressionMethod: CompressionMethod): string {
: CacheFilename.Zstd
}

export async function isGnuTarInstalled(): Promise<boolean> {
export async function getGnuTarPathOnWindows(): Promise<string> {
Comment thread
Phantsure marked this conversation as resolved.
Comment thread
Phantsure marked this conversation as resolved.
if (fs.existsSync(GnuTarPathOnWindows)) {
return GnuTarPathOnWindows
}
const versionOutput = await getVersion('tar')
return versionOutput.toLowerCase().includes('gnu tar')
return versionOutput.toLowerCase().includes('gnu tar') ? io.which('tar') : ''
}

export function assertDefined<T>(name: string, value?: T): T {
Expand Down
3 changes: 3 additions & 0 deletions packages/cache/src/internal/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ export const DefaultRetryDelay = 5000
// over the socket during this period, the socket is destroyed and the download
// is aborted.
export const SocketTimeout = 5000

// The default path of GNUtar on hosted Windows runners
export const GnuTarPathOnWindows = 'C:\\Program Files\\Git\\usr\\bin\\tar.exe'
16 changes: 8 additions & 8 deletions packages/cache/src/internal/tar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ async function getTarPath(
args: string[],
compressionMethod: CompressionMethod
): Promise<string> {
let tarPath = await io.which('tar', true)
Comment thread
bishal-pdMSFT marked this conversation as resolved.
Outdated
switch (process.platform) {
case 'win32': {
const gnuTar = await utils.getGnuTarPathOnWindows()
const systemTar = `${process.env['windir']}\\System32\\tar.exe`
if (compressionMethod !== CompressionMethod.Gzip) {
// We only use zstandard compression on windows when gnu tar is installed due to
// a bug with compressing large files with bsdtar + zstd
if (gnuTar) {
// Use GNUtar as default on windows
args.push('--force-local')
tarPath = gnuTar
Comment thread
bishal-pdMSFT marked this conversation as resolved.
Outdated
Comment thread
Phantsure marked this conversation as resolved.
Outdated
} else if (existsSync(systemTar)) {
Comment thread
Phantsure marked this conversation as resolved.
return systemTar
} else if (await utils.isGnuTarInstalled()) {
args.push('--force-local')
tarPath = systemTar
}
break
}
Expand All @@ -30,14 +30,14 @@ async function getTarPath(
if (gnuTar) {
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
args.push('--delay-directory-restore')
return gnuTar
tarPath = gnuTar
}
break
}
default:
break
}
return await io.which('tar', true)
return tarPath
}

async function execTar(
Expand Down