diff --git a/packages/cache/__tests__/tar.test.ts b/packages/cache/__tests__/tar.test.ts index 4145d9a946..7da098ea6c 100644 --- a/packages/cache/__tests__/tar.test.ts +++ b/packages/cache/__tests__/tar.test.ts @@ -134,6 +134,69 @@ test('zstd extract tar with windows BSDtar', async () => { } }) +test('zstd extract tar prefers BSDtar when opt-in env var set', async () => { + if (IS_WINDOWS) { + const mkdirMock = jest.spyOn(io, 'mkdirP') + const execMock = jest.spyOn(exec, 'exec') + const previousPreferBsdTar = + process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] + // GNU tar still available — without the env var this would take the GNU path. + jest + .spyOn(utils, 'getGnuTarPathOnWindows') + .mockReturnValue(Promise.resolve(GnuTarPathOnWindows)) + process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] = 'true' + + try { + const archivePath = `${process.env['windir']}\\fakepath\\cache.tar` + const workspace = process.env['GITHUB_WORKSPACE'] + const tarPath = SystemTarPathOnWindows + + await tar.extractTar(archivePath, CompressionMethod.Zstd) + + expect(mkdirMock).toHaveBeenCalledWith(workspace) + expect(execMock).toHaveBeenCalledTimes(2) + + expect(execMock).toHaveBeenNthCalledWith( + 1, + [ + 'zstd -d --long=30 --force -o', + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/') + ].join(' '), + undefined, + { + cwd: undefined, + env: expect.objectContaining(defaultEnv) + } + ) + + expect(execMock).toHaveBeenNthCalledWith( + 2, + [ + `"${tarPath}"`, + '-xf', + TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'), + '-P', + '-C', + workspace?.replace(/\\/g, '/') + ].join(' '), + undefined, + { + cwd: undefined, + env: expect.objectContaining(defaultEnv) + } + ) + } finally { + if (previousPreferBsdTar === undefined) { + delete process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] + } else { + process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] = + previousPreferBsdTar + } + } + } +}) + test('gzip extract tar', async () => { const mkdirMock = jest.spyOn(io, 'mkdirP') const execMock = jest.spyOn(exec, 'exec') diff --git a/packages/cache/src/internal/tar.ts b/packages/cache/src/internal/tar.ts index 73c126c7c2..8e03c92c0d 100644 --- a/packages/cache/src/internal/tar.ts +++ b/packages/cache/src/internal/tar.ts @@ -18,8 +18,19 @@ const IS_WINDOWS = process.platform === 'win32' async function getTarPath(): Promise { switch (process.platform) { case 'win32': { - const gnuTar = await utils.getGnuTarPathOnWindows() const systemTar = SystemTarPathOnWindows + // Opt-in: prefer BSD tar (libarchive, shipped as + // C:\Windows\System32\tar.exe on Windows 10+). Benchmarks on hosted + // runners show ~4x faster extract on many-small-files payloads + // compared to Git for Windows' MSYS GNU tar, likely due to native + // Win32/libarchive behavior and lower MSYS process/path translation + // overhead. See actions/cache#752 for context. + const preferBsdTar = + process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] === 'true' + if (preferBsdTar && existsSync(systemTar)) { + return {path: systemTar, type: ArchiveToolType.BSD} + } + const gnuTar = await utils.getGnuTarPathOnWindows() if (gnuTar) { // Use GNUtar as default on windows return {path: gnuTar, type: ArchiveToolType.GNU}