From 418192507e4e1e428afedb9a4e8595e48931a2d0 Mon Sep 17 00:00:00 2001 From: sang-neo03 Date: Wed, 29 Apr 2026 17:50:46 +0800 Subject: [PATCH] fix(install): make Windows zip extraction resilient (issue #603) (#713) The Windows extraction step relied on `powershell -Command Expand-Archive`, which fails when: - Microsoft.PowerShell.Archive (a script module) cannot be loaded due to PSModulePath shadowing (Store-installed pwsh injecting WindowsApps paths) or ExecutionPolicy Restricted (issue #603), or - the temp directory contains characters that corrupt PowerShell string parsing (e.g. a single quote in TEMP). Switch to a two-tier extraction: 1. Primary: Add-Type System.IO.Compression.FileSystem + [ZipFile]::ExtractToDirectory. Bypasses the PowerShell module system entirely. .NET 4.5+, available on Win 8 / Server 2012 by default and widely on Win 7 SP1. 2. Fallback: Expand-Archive -LiteralPath, kept for the rare host without .NET 4.5 but with PS 5.0+ (e.g. Win 7 SP1 with WMF 5). Both paths pass file paths through env vars ($env:LARK_CLI_ARCHIVE / $env:LARK_CLI_DEST) so quoting / wildcard chars in the path can no longer break command parsing. -LiteralPath ensures Expand-Archive treats the value literally rather than as a wildcard pattern. $ErrorActionPreference='Stop' makes non-terminating cmdlet errors propagate as non-zero exit codes. Also drop `stdio: "ignore"` so the actual PowerShell error surfaces in the postinstall log when both paths fail, instead of leaving users with "Command failed: powershell ..." with no detail. Verified on Windows 10 + PS 5.1: - Reproduced #603 with shadow Microsoft.PowerShell.Archive + Restricted ExecutionPolicy: original install.js fails, patched install.js succeeds. - Reproduced single-quote-in-TEMP path corruption: original fails, patched succeeds. - Fallback path verified end-to-end with primary forced to fail. - Normal-environment install: no regression. --- scripts/install.js | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/scripts/install.js b/scripts/install.js index 334804170..249f4c0d1 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -125,6 +125,37 @@ function download(url, destPath) { execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] }); } +function extractZipWindows(archivePath, destDir) { + const psOpts = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"]; + const psStdio = ["ignore", "inherit", "inherit"]; + const psEnv = { + ...process.env, + LARK_CLI_ARCHIVE: archivePath, + LARK_CLI_DEST: destDir, + }; + + try { + const dotnet = + "$ErrorActionPreference='Stop';" + + "Add-Type -AssemblyName System.IO.Compression.FileSystem;" + + "[System.IO.Compression.ZipFile]::ExtractToDirectory($env:LARK_CLI_ARCHIVE,$env:LARK_CLI_DEST)"; + execFileSync("powershell.exe", [...psOpts, dotnet], { stdio: psStdio, env: psEnv }); + } catch (primaryErr) { + try { + const cmdlet = + "$ErrorActionPreference='Stop';" + + "Expand-Archive -LiteralPath $env:LARK_CLI_ARCHIVE -DestinationPath $env:LARK_CLI_DEST -Force"; + execFileSync("powershell.exe", [...psOpts, cmdlet], { stdio: psStdio, env: psEnv }); + } catch (fallbackErr) { + throw new Error( + `Failed to extract ${archivePath}. ` + + `.NET ZipFile attempt: ${primaryErr.message}. ` + + `Expand-Archive fallback: ${fallbackErr.message}` + ); + } + } +} + function install() { const mirrorUrls = getMirrorUrls(process.env); const downloadUrls = [GITHUB_URL, ...mirrorUrls]; @@ -156,10 +187,7 @@ function install() { verifyChecksum(archivePath, expectedHash); if (isWindows) { - execFileSync("powershell", [ - "-Command", - `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}'`, - ], { stdio: "ignore" }); + extractZipWindows(archivePath, tmpDir); } else { execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], { stdio: "ignore",