From 3c457273e9bb2515facde50601f1a83237cdec56 Mon Sep 17 00:00:00 2001 From: yashau Date: Tue, 19 May 2026 20:07:56 +0500 Subject: [PATCH] fix npm windows installer extraction --- .github/workflows/release.yml | 7 +- .../scripts/patch-kittylitter-npm-package.js | 157 ++++++++++++++++++ 2 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 tools/scripts/patch-kittylitter-npm-package.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dcec6baa7..90edb669e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -314,6 +314,7 @@ jobs: PLAN: ${{ needs.plan.outputs.val }} if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }} steps: + - uses: actions/checkout@v5 - name: Fetch npm packages uses: actions/download-artifact@v7 with: @@ -322,8 +323,10 @@ jobs: merge-multiple: true - uses: actions/setup-node@v6 with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org' + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + - name: Patch npm package Windows installer + run: node tools/scripts/patch-kittylitter-npm-package.js - run: | for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do pkg=$(echo "$release" | jq '.artifacts[] | select(endswith("-npm-package.tar.gz"))' --raw-output) diff --git a/tools/scripts/patch-kittylitter-npm-package.js b/tools/scripts/patch-kittylitter-npm-package.js new file mode 100644 index 000000000..0805c953f --- /dev/null +++ b/tools/scripts/patch-kittylitter-npm-package.js @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const root = path.resolve(__dirname, "..", ".."); +const defaultPackageDir = path.join(root, "npm"); +const packagePaths = process.argv.slice(2); + +function fail(message) { + console.error(message); + process.exit(1); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + stdio: "pipe", + ...options, + }); + if (result.status !== 0 || result.error) { + fail( + [ + `command failed: ${command} ${args.join(" ")}`, + result.error ? `error: ${result.error.message}` : "", + result.stdout ? `stdout:\n${result.stdout}` : "", + result.stderr ? `stderr:\n${result.stderr}` : "", + ] + .filter(Boolean) + .join("\n"), + ); + } + return result; +} + +function findPackages() { + if (packagePaths.length > 0) { + return packagePaths.map((p) => path.resolve(p)); + } + if (!fs.existsSync(defaultPackageDir)) { + fail(`npm package directory not found: ${defaultPackageDir}`); + } + return fs + .readdirSync(defaultPackageDir) + .filter((name) => name.endsWith("-npm-package.tar.gz")) + .map((name) => path.join(defaultPackageDir, name)); +} + +function patchBinaryInstaller(source) { + const windowsExtractor = `if (this.platform.artifactName.includes("windows")) { + // Prefer bsdtar on Windows. It is available on modern Windows + // installs and avoids PowerShell execution-policy failures + // when importing Microsoft.PowerShell.Archive. + result = spawnSync("tar", [ + "-xf", + tempFile, + "-C", + this.installDirectory, + ]); + + if (result.error || result.status !== 0) { + result = spawnSync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + \`& { + param([string]$LiteralPath, [string]$DestinationPath) + $ErrorActionPreference = "Stop" + Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force + }\`, + tempFile, + this.installDirectory, + ]); + } + }`; + + const originalWindowsExtractor = `if (this.platform.artifactName.includes("windows")) { + // Windows does not have "unzip" by default on many installations, instead + // we use Expand-Archive from powershell + result = spawnSync("powershell.exe", [ + "-NoProfile", + "-NonInteractive", + "-Command", + \`& { + param([string]$LiteralPath, [string]$DestinationPath) + Expand-Archive -LiteralPath $LiteralPath -DestinationPath $DestinationPath -Force + }\`, + tempFile, + this.installDirectory, + ]); + }`; + + if (!source.includes(originalWindowsExtractor)) { + fail("could not find generated Windows zip extractor block"); + } + source = source.replace(originalWindowsExtractor, windowsExtractor); + + const installSuccessBlock = `.then(() => { + if (!suppressLogs) { + console.error(\`${"${this.name}"} has been installed!\`); + } + })`; + + const verifiedInstallSuccessBlock = `.then(() => { + if (!this.exists()) { + const missing = Object.values(this.binaries) + .map((binRelPath) => join(this.installDirectory, binRelPath)) + .filter((binPath) => !existsSync(binPath)); + throw new Error( + \`${"${this.name}"} install failed: missing expected binaries: ${'${missing.join(", ")}'}\`, + ); + } + if (!suppressLogs) { + console.error(\`${"${this.name}"} has been installed!\`); + } + })`; + + if (!source.includes(installSuccessBlock)) { + fail("could not find generated install success block"); + } + return source.replace(installSuccessBlock, verifiedInstallSuccessBlock); +} + +function patchPackage(packagePath) { + if (!fs.existsSync(packagePath)) { + fail(`npm package not found: ${packagePath}`); + } + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "kittylitter-npm-")); + try { + run("tar", ["-xzf", packagePath, "-C", tempDir]); + const packageRoot = path.join(tempDir, "package"); + const installerPath = path.join(packageRoot, "binary-install.js"); + if (!fs.existsSync(installerPath)) { + fail(`binary-install.js not found in ${packagePath}`); + } + + const source = fs.readFileSync(installerPath, "utf8"); + const patched = patchBinaryInstaller(source); + fs.writeFileSync(installerPath, patched); + + const repacked = `${packagePath}.patched`; + run("tar", ["-czf", repacked, "-C", tempDir, "package"]); + fs.renameSync(repacked, packagePath); + console.error(`patched npm package: ${packagePath}`); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +} + +for (const packagePath of findPackages()) { + patchPackage(packagePath); +}