fix(packaging): make App ZIPs readable in Windows Explorer

## Problem and outcome / 问题与结果

The ZIP writer on Windows and macOS archived `.` as a root entry and prefixed members with `./`. Windows File Explorer enumerated zero entries in the published Windows ZIP, although bsdtar could extract it. Archive the source directory's children directly, preserving hidden files and Unix executable modes. Each Windows package now verifies that File Explorer can resolve every source file and directory before publication.

The archive regression checks normalized member paths as well as extraction. This fixes the shared packaging boundary without changing App media or UI behavior.

## Release notes

### 中文

v1.5.1 是一次 ZIP 兼容性修复。修复 Windows 文件资源管理器打开 App 下载包时显示为空或无法解压的问题,并在 Windows 打包时逐项检查资源管理器能否读取文件。

遇到该问题,请重新下载 v1.5.1 的 ZIP 并解压。此前已正常解压、运行的 App 不受此问题影响。

### English

v1.5.1 fixes a ZIP compatibility issue that could make App downloads appear empty or fail to extract in Windows File Explorer. Windows packaging now checks that File Explorer can read every file before release.

If affected, download and extract the v1.5.1 ZIP again. Apps that were already extracted and running are unaffected by this issue.

## Verification / 验证

- Reproduced with the original v1.5.0 Windows archive: File Explorer listed zero entries; the Linux archive remained readable.
- Repacked the original Windows package using the corrected entry layout: File Explorer listed all root entries and extracted all seven files; every SHA-256 matched the original.
- Independent .NET extraction and 7-Zip integrity checks passed.
- `npm run check`: type checking, 998 tests and Web production build passed.
- Windows archive tests include hidden files, Unicode names and spaced paths; Unix checks retain executable permissions.
- Full CI packaging verifies Windows, Linux and macOS before publication.

## Impact / 影响

Packaging-only patch release v1.5.1. Standard Store/Deflate compression, existing dependencies and product protocols are retained. Existing published archives remain immutable.
This commit is contained in:
AAA authored and GitHub committed 2026-09-17 13:44:13 +08:00
1 parent 49bd6e288a
commit 08da234b8f
4 files changed
+39 -3

No files matched your search

+22 -2
View File
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { existsSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import { join, resolve } from "node:path";
export function tarExecutable() {
@@ -15,10 +15,30 @@ export function createZip(source, archive) {
if (process.platform === "linux") {
execFileSync("zip", ["-qry", archive, "."], { cwd: source, stdio: "pipe" });
} else {
execFileSync(tarExecutable(), ["--format=zip", "-cf", archive, "."], {
// Archiving "." adds ./ members that Windows Explorer treats as an empty ZIP.
execFileSync(tarExecutable(), ["--format=zip", "-cf", archive, "--", ...readdirSync(source).sort()], {
cwd: source, stdio: "pipe", windowsHide: true,
});
}
if (process.platform === "win32") {
// The writer can read its own incompatible ZIPs. Check Explorer separately,
// including nested helpers and hidden files, before publishing the archive.
execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `
$ErrorActionPreference = 'Stop'
$shell = New-Object -ComObject Shell.Application
function Check-Folder($folder, [string]$source) {
if ($null -eq $folder) { throw 'Windows Explorer cannot open the ZIP folder' }
foreach ($entry in Get-ChildItem -LiteralPath $source -Force) {
$item = $folder.ParseName($entry.Name)
if ($null -eq $item) { throw "Windows Explorer cannot read ZIP entry: $($entry.Name)" }
if ($entry.PSIsContainer) { Check-Folder $item.GetFolder $entry.FullName }
}
}
Check-Folder $shell.NameSpace($env:PIIK_ZIP_ARCHIVE) $env:PIIK_ZIP_SOURCE
`], { stdio: "pipe", windowsHide: true, timeout: 30_000,
env: { ...process.env, PIIK_ZIP_ARCHIVE: archive, PIIK_ZIP_SOURCE: resolve(source) },
});
}
}
export function extractZip(archive, destination) {