From 7e04cc8e89854f80eb66e88752588932ac92b867 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 17 Apr 2026 12:08:33 +0000 Subject: [PATCH 1/2] feat(cache): opt-in env var to prefer BSD tar on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS=true to route cache archive/extract through C:\Windows\System32\tar.exe (libarchive bsdtar) instead of Git-for-Windows' MSYS GNU tar. On hosted windows-latest runners, bsdtar extracts many-small-files payloads ~4x faster than MSYS tar because it makes native Win32 syscalls and has zstd built in (no fork-per-file to zstd.exe). Default behavior is unchanged — this only takes effect when the env var is explicitly set. The existing BSD_TAR_ZSTD two-command path handles the BSD tar case end-to-end. Refs: actions/cache#752 Signed-off-by: Gregor Zeitlinger --- packages/cache/__tests__/tar.test.ts | 56 ++++++++++++++++++++++++++++ packages/cache/src/internal/tar.ts | 11 ++++++ 2 files changed, 67 insertions(+) diff --git a/packages/cache/__tests__/tar.test.ts b/packages/cache/__tests__/tar.test.ts index 4145d9a946..d905dd461a 100644 --- a/packages/cache/__tests__/tar.test.ts +++ b/packages/cache/__tests__/tar.test.ts @@ -134,6 +134,62 @@ 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') + // 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 { + delete process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] + } + } +}) + 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..f7f757d973 100644 --- a/packages/cache/src/internal/tar.ts +++ b/packages/cache/src/internal/tar.ts @@ -20,6 +20,17 @@ async function getTarPath(): Promise { 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, because bsdtar makes + // native Win32 syscalls and has zstd built in (no external + // zstd.exe fork-per-file). 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} + } if (gnuTar) { // Use GNUtar as default on windows return {path: gnuTar, type: ArchiveToolType.GNU} From ed5f64c4717d7641a5a67c52c0be142f6ee6a3da Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 21 Apr 2026 07:28:09 +0000 Subject: [PATCH 2/2] Address cache tar review feedback Signed-off-by: Gregor Zeitlinger --- packages/cache/__tests__/tar.test.ts | 9 ++++++++- packages/cache/src/internal/tar.ts | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/cache/__tests__/tar.test.ts b/packages/cache/__tests__/tar.test.ts index d905dd461a..7da098ea6c 100644 --- a/packages/cache/__tests__/tar.test.ts +++ b/packages/cache/__tests__/tar.test.ts @@ -138,6 +138,8 @@ 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') @@ -185,7 +187,12 @@ test('zstd extract tar prefers BSDtar when opt-in env var set', async () => { } ) } finally { - delete process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] + if (previousPreferBsdTar === undefined) { + delete process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] + } else { + process.env['ACTIONS_CACHE_PREFER_BSD_TAR_ON_WINDOWS'] = + previousPreferBsdTar + } } } }) diff --git a/packages/cache/src/internal/tar.ts b/packages/cache/src/internal/tar.ts index f7f757d973..8e03c92c0d 100644 --- a/packages/cache/src/internal/tar.ts +++ b/packages/cache/src/internal/tar.ts @@ -18,19 +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, because bsdtar makes - // native Win32 syscalls and has zstd built in (no external - // zstd.exe fork-per-file). See actions/cache#752 for context. + // 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}