From 95301577e8234bda862d4bfae85f1c1346540374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Kerekes?= Date: Thu, 22 Oct 2026 17:02:57 +0200 Subject: [PATCH 1/2] fix(update): use the system tar on Windows --- apps/server/src/update/install.ts | 10 ++++++++-- apps/server/src/update/update.test.ts | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/server/src/update/install.ts b/apps/server/src/update/install.ts index ff0fa22..8e20744 100644 --- a/apps/server/src/update/install.ts +++ b/apps/server/src/update/install.ts @@ -20,6 +20,12 @@ import { activeBundle, Build, bundlePaths, renderShim, type InstalledLayout } fr import { assetUrl } from "./release.js"; import { compareVersions, parseVersion } from "./version.js"; +// Windows archives are zip files that only the bundled bsdtar reads, and a PATH lookup can pick another tar. +export const tarProgram = + process.platform === "win32" + ? join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tar.exe") + : "tar"; + async function download(url: string, limit: number, fetcher: typeof fetch, signal?: AbortSignal) { const response = await fetcher(url, { signal: AbortSignal.any([AbortSignal.timeout(10 * 60_000), ...(signal ? [signal] : [])]), @@ -147,7 +153,7 @@ export async function prepareUpdate( if (!existing) { const file = join(work, archive); await writeFile(file, bytes, { mode: 0o600 }); - const listing = await run("tar", ["-tf", file]); + const listing = await run(tarProgram, ["-tf", file]); if ( listing.code !== 0 || !listing.stdout.trim() || @@ -162,7 +168,7 @@ export async function prepareUpdate( ) ) throw new UpdateError("Archive contains an unsafe path or cannot be read."); - if ((await run("tar", ["-xf", file, "-C", work])).code !== 0) + if ((await run(tarProgram, ["-xf", file, "-C", work])).code !== 0) throw new UpdateError("Release extraction failed."); await verifyBundle(join(work, "inbot"), layout, version, run); await writeFile(join(work, "inbot", ".archive-sha256"), `${hash}\n`, { mode: 0o600 }); diff --git a/apps/server/src/update/update.test.ts b/apps/server/src/update/update.test.ts index 2853206..8049b14 100644 --- a/apps/server/src/update/update.test.ts +++ b/apps/server/src/update/update.test.ts @@ -5,7 +5,7 @@ import { dirname, join } from "node:path"; import { expect, test, vi } from "vitest"; import { runCommand, type CommandRunner } from "../../../cli/src/service.js"; import { runCli } from "../../../cli/src/cli.js"; -import { prepareUpdate } from "./install.js"; +import { prepareUpdate, tarProgram } from "./install.js"; import { activeBundle, bundlePaths, @@ -43,7 +43,7 @@ export async function updateFixture(target = `${process.platform}-${process.arch await createBundle(join(stage, "inbot"), "0.2.0"); const archive = `inbot-0.2.0-${target}.${windows ? "zip" : "tar.gz"}`; const archivePath = join(root, archive); - const packed = await runCommand("tar", [ + const packed = await runCommand(tarProgram, [ windows ? "-acf" : "-czf", archivePath, "-C", @@ -251,7 +251,7 @@ test("CLI update reinstalls the service on the new runtime and preserves its por username: "synthetic", health: async () => registered, run: async (program: string, args: readonly string[]) => { - if (program === "tar" || program.includes("codesign")) return f.run(program, args); + if (program === tarProgram || program.includes("codesign")) return f.run(program, args); managerCalls.push([...args]); if (args.includes("bootout") || args.includes("disable") || args.includes("/Delete")) registered = false; From 8ce08afa589629e7a56b5774828f4cf5037b01ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Kerekes?= Date: Thu, 22 Oct 2026 17:14:31 +0200 Subject: [PATCH 2/2] test(installer): run Windows PowerShell without PowerShell 7 module paths --- scripts/install-windows.test.mjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/install-windows.test.mjs b/scripts/install-windows.test.mjs index 0c97e4d..75bbcdc 100644 --- a/scripts/install-windows.test.mjs +++ b/scripts/install-windows.test.mjs @@ -11,6 +11,8 @@ import { promisify } from "node:util"; const exec = promisify(execFile); const root = fileURLToPath(new URL("../", import.meta.url)); const windows = process.platform === "win32"; +// PowerShell 7 exports its own module path, which stops Windows PowerShell from loading its cmdlets. +const { PSModulePath: _, ...baseEnv } = process.env; const architecture = windows ? ( await powershell( @@ -20,7 +22,7 @@ const architecture = windows : undefined; const target = `win32-${architecture}`; -function powershell(command, env = process.env) { +function powershell(command, env = baseEnv) { return exec("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], { env }); } @@ -49,7 +51,7 @@ function runShim(argument, env) { function installEnv(temporary, release) { return { - ...process.env, + ...baseEnv, INBOT_HOME: join(temporary, "home"), INBOT_TEST_MEMORY_KEYCHAIN: "1", INBOT_INSTALL_ROOT: join(temporary, "installed app"), @@ -107,7 +109,7 @@ test( } finally { $zip.Dispose() } `, { - ...process.env, + ...baseEnv, INBOT_TEST_BUNDLE: bundle, INBOT_TEST_ARCHIVE: join(release, archive), }, @@ -184,7 +186,7 @@ test( $zip = [System.IO.Compression.ZipFile]::Open($env:INBOT_TEST_ARCHIVE, 'Create') try { $zip.CreateEntry($env:INBOT_TEST_ENTRY) | Out-Null } finally { $zip.Dispose() } `, - { ...process.env, INBOT_TEST_ARCHIVE: archivePath, INBOT_TEST_ENTRY: entry }, + { ...baseEnv, INBOT_TEST_ARCHIVE: archivePath, INBOT_TEST_ENTRY: entry }, ); const digest = createHash("sha256") .update(await readFile(archivePath))