feat(pack): add Windows portable zip target alongside NSIS installer (#2937)

Adds a new `--to zip` (and `--to all`) tools-pack Windows build target that
produces a portable `.zip` from the cached `win-unpacked` tree using the
bundled 7z. The zip lays files at the archive root so users can extract it
anywhere and launch `Open Design.exe` without going through the NSIS
installer, addressing the no-install download request.

Release plumbing is updated to publish the portable zip and its sha256
beside the existing installer on R2 for beta, preview, and stable channels
(default on, gated by `WINDOWS_INCLUDE_ZIP`/`WIN_INCLUDE_ZIP`). The
electron-updater `latest.yml` feed continues to point only at the
installer; the zip is a manual-download convenience and is intentionally
excluded from the in-app updater.

Closes #1121

Generated-By: looper 0.0.0-dev (runner=worker, agent=claude-code)

Co-authored-by: libertecode <libertecode@proton.me>
This commit is contained in:
PerishFire 2026-05-26 14:14:44 +08:00 committed by GitHub
parent 709fc0a497
commit bfcafc81fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 218 additions and 26 deletions

View file

@ -8,21 +8,37 @@ foreach ($name in @("CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN", "RELEASE_CHANNEL", "
$assetSuffix = if ($null -eq $env:WINDOWS_ASSET_SUFFIX) { "" } else { $env:WINDOWS_ASSET_SUFFIX }
$versionPathSuffix = if ($null -eq $env:ASSET_VERSION_SUFFIX) { "" } else { $env:ASSET_VERSION_SUFFIX }
$includeZip = if ([string]::IsNullOrWhiteSpace($env:WINDOWS_INCLUDE_ZIP)) { $true } else { $env:WINDOWS_INCLUDE_ZIP -ne "false" }
$releaseDir = Join-Path $env:RUNNER_TEMP "release-assets"
New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
$sourceInstaller = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/${env:TOOLS_PACK_NAMESPACE}/builder/Open Design-${env:TOOLS_PACK_NAMESPACE}-setup.exe"
$builderDir = Join-Path $env:RUNNER_TEMP "tools-pack/out/win/namespaces/${env:TOOLS_PACK_NAMESPACE}/builder"
$sourceInstaller = Join-Path $builderDir "Open Design-${env:TOOLS_PACK_NAMESPACE}-setup.exe"
$sourceZip = Join-Path $builderDir "Open Design-${env:TOOLS_PACK_NAMESPACE}-portable.zip"
if (!(Test-Path $sourceInstaller)) {
throw "expected installer not found at $sourceInstaller"
}
if ($includeZip -and !(Test-Path $sourceZip)) {
throw "expected portable zip not found at $sourceZip (build with --to all or --to zip, or set WINDOWS_INCLUDE_ZIP=false to skip)"
}
$versionedInstaller = "open-design-${env:RELEASE_VERSION}$assetSuffix-win-x64-setup.exe"
$checksumFile = "$versionedInstaller.sha256"
Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller)
$versionedZip = "open-design-${env:RELEASE_VERSION}$assetSuffix-win-x64-portable.zip"
$installerChecksumFile = "$versionedInstaller.sha256"
$zipChecksumFile = "$versionedZip.sha256"
Copy-Item $sourceInstaller (Join-Path $releaseDir $versionedInstaller)
$installerPath = Join-Path $releaseDir $versionedInstaller
$hash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $checksumFile)
$installerHash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLowerInvariant()
"$installerHash $versionedInstaller" | Set-Content -Path (Join-Path $releaseDir $installerChecksumFile)
if ($includeZip) {
Copy-Item $sourceZip (Join-Path $releaseDir $versionedZip)
$zipPath = Join-Path $releaseDir $versionedZip
$zipHash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLowerInvariant()
"$zipHash $versionedZip" | Set-Content -Path (Join-Path $releaseDir $zipChecksumFile)
}
$installerBytes = [System.IO.File]::ReadAllBytes($installerPath)
$installerSha512 = [System.Convert]::ToBase64String([System.Security.Cryptography.SHA512]::Create().ComputeHash($installerBytes))
$installerSize = (Get-Item $installerPath).Length
@ -39,6 +55,9 @@ $releaseNotes = if ([string]::IsNullOrWhiteSpace($env:RELEASE_NOTES)) {
} else {
$env:RELEASE_NOTES
}
# latest.yml is the electron-updater auto-update feed; it only references the
# NSIS installer because the portable zip is a manual-download convenience and
# is not consumed by the in-app updater.
@(
"version: `"${env:RELEASE_VERSION}`""
'files:'

View file

@ -175,10 +175,18 @@ if (platform === "mac") {
} else if (platform === "win") {
const suffix = optional("WIN_ASSET_SUFFIX", assetVersionSuffix);
const installer = `open-design-${releaseVersion}${suffix}-win-x64-setup.exe`;
const portableZip = `open-design-${releaseVersion}${suffix}-win-x64-portable.zip`;
const includeZip = optional("WIN_INCLUDE_ZIP", "true") !== "false";
const artifacts = { installer: fileEntry(installer, contentType(installer)) };
const assetNames = [installer, `${installer}.sha256`, "latest.yml"];
if (includeZip) {
artifacts.portableZip = fileEntry(portableZip, contentType(portableZip));
assetNames.push(portableZip, `${portableZip}.sha256`);
}
config = {
arch: "x64",
artifacts: { installer: fileEntry(installer, contentType(installer)) },
assetNames: [installer, `${installer}.sha256`, "latest.yml"],
artifacts,
assetNames,
feed: {
latestUrl: publicUrl(latestPrefix, "latest.yml"),
name: "latest.yml",

View file

@ -109,8 +109,17 @@ mac_zip="open-design-$RELEASE_VERSION$asset_version_suffix-mac-arm64.zip"
mac_intel_dmg="open-design-$RELEASE_VERSION$mac_intel_asset_suffix-mac-x64.dmg"
mac_intel_zip="open-design-$RELEASE_VERSION$mac_intel_asset_suffix-mac-x64.zip"
win_installer="open-design-$RELEASE_VERSION$win_asset_suffix-win-x64-setup.exe"
win_portable_zip="open-design-$RELEASE_VERSION$win_asset_suffix-win-x64-portable.zip"
linux_appimage="open-design-$RELEASE_VERSION$linux_asset_suffix-linux-x64.AppImage"
metadata_path="$release_root/metadata.json"
win_include_zip="${WIN_INCLUDE_ZIP:-true}"
case "$win_include_zip" in
true | false) ;;
*)
echo "unsupported WIN_INCLUDE_ZIP: $win_include_zip" >&2
exit 1
;;
esac
if [ "$ENABLE_MAC" = "true" ]; then
upload "$release_root/mac/$mac_dmg" "$version_prefix/$mac_dmg" "application/x-apple-diskimage" "public, max-age=31536000, immutable"
@ -139,6 +148,13 @@ if [ "$ENABLE_WIN" = "true" ]; then
echo "win_installer_url=$public_origin/$version_prefix/$win_installer"
echo "win_feed_url=$public_origin/$latest_prefix/latest.yml"
} >> "$GITHUB_OUTPUT"
if [ "$win_include_zip" = "true" ]; then
upload "$release_root/win/$win_portable_zip" "$version_prefix/$win_portable_zip" "application/zip" "public, max-age=31536000, immutable"
upload "$release_root/win/$win_portable_zip.sha256" "$version_prefix/$win_portable_zip.sha256" "text/plain; charset=utf-8" "public, max-age=31536000, immutable"
{
echo "win_portable_zip_url=$public_origin/$version_prefix/$win_portable_zip"
} >> "$GITHUB_OUTPUT"
fi
fi
if [ "$ENABLE_MAC_INTEL" = "true" ]; then
@ -177,6 +193,8 @@ MAC_ZIP="$mac_zip" \
MAC_INTEL_DMG="$mac_intel_dmg" \
MAC_INTEL_ZIP="$mac_intel_zip" \
WIN_INSTALLER="$win_installer" \
WIN_PORTABLE_ZIP="$win_portable_zip" \
WIN_INCLUDE_ZIP="$win_include_zip" \
LINUX_APPIMAGE="$linux_appimage" \
MAC_ARTIFACT_MODE="$mac_artifact_mode" \
METADATA_PATH="$metadata_path" \
@ -236,6 +254,12 @@ if (enabled("ENABLE_MAC")) {
};
}
if (enabled("ENABLE_WIN")) {
const winArtifacts = {
installer: fileEntry("win", env.WIN_INSTALLER, "application/vnd.microsoft.portable-executable"),
};
if (env.WIN_INCLUDE_ZIP !== "false") {
winArtifacts.portableZip = fileEntry("win", env.WIN_PORTABLE_ZIP, "application/zip");
}
platforms.win = {
arch: "x64",
enabled: true,
@ -245,9 +269,7 @@ if (enabled("ENABLE_WIN")) {
url: url(versionPrefix, "latest.yml"),
},
signed: false,
artifacts: {
installer: fileEntry("win", env.WIN_INSTALLER, "application/vnd.microsoft.portable-executable"),
},
artifacts: winArtifacts,
};
}
if (enabled("ENABLE_LINUX")) {

View file

@ -65,6 +65,7 @@ if (platforms.mac.enabled) {
if (platforms.win.enabled) {
platforms.win.artifacts = {
installer: optional("R2_WIN_INSTALLER_URL"),
portableZip: optional("R2_WIN_PORTABLE_ZIP_URL"),
};
platforms.win.feed = optional("R2_WIN_FEED_URL");
platforms.win.e2e = platformReport("win");
@ -142,7 +143,10 @@ const platformRows = [
[
"Windows x64",
platformStatus(platforms.win, "Published"),
linkList([{ label: "Installer", url: platforms.win.artifacts?.installer }]),
linkList([
{ label: "Installer", url: platforms.win.artifacts?.installer },
{ label: "Portable ZIP", url: platforms.win.artifacts?.portableZip },
]),
link("latest.yml", platforms.win.feed),
],
[

View file

@ -134,6 +134,9 @@ if [ "$ENABLE_WIN" = "true" ]; then
grep -F "version: \"$RELEASE_VERSION\"" "$downloaded_feed"
grep -F "$R2_WIN_INSTALLER_URL" "$downloaded_feed"
curl -fsSI "$R2_WIN_INSTALLER_URL" >/dev/null
if [ -n "${R2_WIN_PORTABLE_ZIP_URL:-}" ]; then
curl -fsSI "$R2_WIN_PORTABLE_ZIP_URL" >/dev/null
fi
require_report_file "win/manifest.json"
require_report_file "win/screenshots/open-design-win-smoke.png"
require_report_file "win/suite-result.json"

View file

@ -459,7 +459,7 @@ jobs:
"--namespace", "release-beta-win",
"--portable",
"--app-version", "${{ needs.metadata.outputs.beta_version }}",
"--to", "nsis",
"--to", "all",
"--json"
)
"cache_failed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
@ -477,7 +477,7 @@ jobs:
--namespace release-beta-win `
--portable `
--app-version "${{ needs.metadata.outputs.beta_version }}" `
--to nsis `
--to all `
--json
if ($LASTEXITCODE -ne 0) {
throw "Windows tools-pack uncached fallback build exited with code $LASTEXITCODE"

View file

@ -456,7 +456,7 @@ jobs:
"--namespace", "release-preview-win",
"--portable",
"--app-version", "${{ needs.metadata.outputs.release_version }}",
"--to", "nsis",
"--to", "all",
"--json"
)
"cache_failed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
@ -474,7 +474,7 @@ jobs:
--namespace release-preview-win `
--portable `
--app-version "${{ needs.metadata.outputs.release_version }}" `
--to nsis `
--to all `
--json
if ($LASTEXITCODE -ne 0) {
throw "Windows tools-pack uncached fallback build exited with code $LASTEXITCODE"
@ -727,6 +727,7 @@ jobs:
R2_REPORT_ZIP_URL: ${{ steps.r2.outputs.report_zip_url }}
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
R2_WIN_PORTABLE_ZIP_URL: ${{ steps.r2.outputs.win_portable_zip_url }}
run: bash .github/scripts/release/r2/verify.sh
- name: Publish summary
@ -743,6 +744,7 @@ jobs:
R2_VERSION_PREFIX: ${{ steps.r2.outputs.version_prefix }}
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
R2_WIN_PORTABLE_ZIP_URL: ${{ steps.r2.outputs.win_portable_zip_url }}
run: bash .github/scripts/release/r2/summary.sh
- name: Cleanup workflow artifacts

View file

@ -503,7 +503,7 @@ jobs:
"--namespace", "${{ needs.metadata.outputs.win_namespace }}",
"--portable",
"--app-version", "${{ needs.metadata.outputs.release_version }}",
"--to", "nsis",
"--to", "all",
"--json"
)
"cache_failed=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
@ -521,7 +521,7 @@ jobs:
--namespace "${{ needs.metadata.outputs.win_namespace }}" `
--portable `
--app-version "${{ needs.metadata.outputs.release_version }}" `
--to nsis `
--to all `
--json
if ($LASTEXITCODE -ne 0) {
throw "Windows tools-pack uncached fallback build exited with code $LASTEXITCODE"
@ -888,6 +888,7 @@ jobs:
R2_REPORT_ZIP_URL: ${{ steps.r2.outputs.report_zip_url }}
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
R2_WIN_PORTABLE_ZIP_URL: ${{ steps.r2.outputs.win_portable_zip_url }}
run: bash .github/scripts/release/r2/verify.sh
- name: Promote draft to published latest
@ -921,6 +922,7 @@ jobs:
R2_VERSION_PREFIX: ${{ steps.r2.outputs.version_prefix }}
R2_WIN_FEED_URL: ${{ steps.r2.outputs.win_feed_url }}
R2_WIN_INSTALLER_URL: ${{ steps.r2.outputs.win_installer_url }}
R2_WIN_PORTABLE_ZIP_URL: ${{ steps.r2.outputs.win_portable_zip_url }}
run: bash .github/scripts/release/r2/summary.sh
- name: Cleanup workflow artifacts

View file

@ -120,7 +120,7 @@ export type ToolPackConfig = {
function resolveToolPackBuildOutput(platform: ToolPackPlatform, value: string | undefined): ToolPackBuildOutput {
if (value == null || value.length === 0) return platform === "win" ? "nsis" : "all";
if (platform === "mac" && (value === "all" || value === "app" || value === "dmg" || value === "zip")) return value;
if (platform === "win" && (value === "all" || value === "dir" || value === "nsis")) return value;
if (platform === "win" && (value === "all" || value === "dir" || value === "nsis" || value === "zip")) return value;
if (platform === "linux" && (value === "all" || value === "appimage" || value === "dir")) return value;
throw new Error(`unsupported ${platform} --to target: ${value}`);
}

View file

@ -77,7 +77,7 @@ function addSharedOptions(command: CacCommand) {
const TO_HELP_BY_PLATFORM: Record<ToolPackPlatform, string> = {
linux: "build target: all|appimage|dir (default: all)",
mac: "build target: all|app|dmg|zip (default: all)",
win: "build target: all|dir|nsis (default: nsis)",
win: "build target: all|dir|nsis|zip (default: nsis). `zip` produces a portable zip from the unpacked build; `all` produces dir+nsis+zip.",
};
function addBuildOptions(command: CacCommand, platform: ToolPackPlatform) {

View file

@ -89,6 +89,7 @@ export async function packWin(config: ToolPackConfig): Promise<WinPackResult> {
installerPath: (await pathExists(paths.setupPath)) ? paths.setupPath : null,
latestYmlPath: (await pathExists(paths.latestYmlPath)) ? paths.latestYmlPath : null,
outputRoot: config.roots.output.namespaceRoot,
portableZipPath: (await pathExists(paths.setupZipPath)) ? paths.setupZipPath : null,
resourceRoot: builtApp == null ? paths.resourceRoot : join(builtApp.unpackedRoot, "resources", "open-design"),
runtimeNamespaceRoot: config.roots.runtime.namespaceRoot,
cacheReport: cache.report(),

View file

@ -33,8 +33,13 @@ import {
} from "./manifest.js";
import { ensureNsisPersianLanguageAlias, writeNsisInclude } from "./nsis.js";
import { sanitizeNamespace } from "./paths.js";
import { resolveWinTargets } from "./report.js";
import {
resolveElectronBuilderWinTargets,
shouldBuildWinNsisInstaller,
shouldBuildWinPortableZip,
} from "./report.js";
import type { ResourceTreeResult } from "./resources.js";
import { buildWinPortableZip } from "./zip.js";
import type {
ElectronBuilderDirCacheMetadata,
WinBuiltAppManifest,
@ -141,7 +146,7 @@ async function runElectronBuilderRaw(config: ToolPackConfig, paths: WinPaths, pr
win: {
artifactName: `${PRODUCT_NAME}-${namespaceToken}.\${ext}`,
icon: paths.winIconPath,
target: resolveWinTargets(config.to).map((target) => ({ arch: ["x64"], target })),
target: resolveElectronBuilderWinTargets(config.to).map((target) => ({ arch: ["x64"], target })),
},
};
@ -337,7 +342,13 @@ export async function runElectronBuilder(
unpackedRoot: cachedUnpackedRoot,
webStandaloneHookAuditPath: (await pathExists(paths.webStandaloneHookAuditPath)) ? paths.webStandaloneHookAuditPath : null,
});
if (config.to === "nsis" || config.to === "all") {
await buildCustomWinNsisInstaller(config, paths, await materializeCachedUnpackedForInstaller(cachedUnpackedRoot, paths, packagedVersion));
if (shouldBuildWinNsisInstaller(config.to) || shouldBuildWinPortableZip(config.to)) {
const materialized = await materializeCachedUnpackedForInstaller(cachedUnpackedRoot, paths, packagedVersion);
if (shouldBuildWinNsisInstaller(config.to)) {
await buildCustomWinNsisInstaller(config, paths, materialized);
}
if (shouldBuildWinPortableZip(config.to)) {
await buildWinPortableZip(config, paths, materialized);
}
}
}

View file

@ -56,6 +56,7 @@ export function resolveWinPaths(config: ToolPackConfig): WinPaths {
packagedMainPrebundlePath: join(namespaceRoot, "assembled", WIN_PREBUNDLED_PACKAGED_MAIN_RELATIVE_PATH),
resourceRoot: join(namespaceRoot, "resources", "open-design"),
setupPath: join(namespaceRoot, "builder", `${PRODUCT_NAME}-${namespaceToken}-setup.exe`),
setupZipPath: join(namespaceRoot, "builder", `${PRODUCT_NAME}-${namespaceToken}-portable.zip`),
startMenuShortcutPath: join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", identity.shortcutName),
tarballsRoot: join(namespaceRoot, "tarballs"),
userDesktopShortcutPath: join(homedir(), "Desktop", identity.shortcutName),

View file

@ -22,19 +22,40 @@ function isBetterSqlite3SourceResidue(path: string): boolean {
);
}
export function resolveWinTargets(to: ToolPackConfig["to"]): Array<"dir" | "nsis"> {
export function resolveWinTargets(to: ToolPackConfig["to"]): Array<"dir" | "nsis" | "zip"> {
switch (to) {
case "dir":
return ["dir"];
case "all":
return ["dir", "nsis"];
return ["dir", "nsis", "zip"];
case "nsis":
return ["nsis"];
case "zip":
return ["zip"];
default:
throw new Error(`unsupported win target: ${to}`);
}
}
// electron-builder only knows how to produce the unpacked `dir` and the NSIS
// installer; the portable zip is assembled afterwards from the same unpacked
// content using bundled 7z, so we filter the zip target out before handing the
// list to electron-builder. When the user asks for `zip` alone we still need
// the unpacked dir, so the helper substitutes `dir` in its place.
export function resolveElectronBuilderWinTargets(to: ToolPackConfig["to"]): Array<"dir" | "nsis"> {
const filtered = resolveWinTargets(to).filter((target): target is "dir" | "nsis" => target !== "zip");
if (filtered.length === 0) return ["dir"];
return filtered;
}
export function shouldBuildWinNsisInstaller(to: ToolPackConfig["to"]): boolean {
return resolveWinTargets(to).includes("nsis");
}
export function shouldBuildWinPortableZip(to: ToolPackConfig["to"]): boolean {
return resolveWinTargets(to).includes("zip");
}
export async function collectWinSizeReport(
config: ToolPackConfig,
paths: WinPaths,
@ -68,6 +89,7 @@ export async function collectWinSizeReport(
generatedAt: new Date().toISOString(),
installerBytes: await sizeExistingFileBytes(paths.setupPath),
outputRootBytes: namespaceSizeIndex.sizePathBytes(config.roots.output.namespaceRoot),
portableZipBytes: await sizeExistingFileBytes(paths.setupZipPath),
resourceRootBytes: sizeIndex.sizePathBytes(join(appResourcesRoot, "open-design")),
runtimeNamespaceRoot: config.roots.runtime.namespaceRoot,
topLevel: {

View file

@ -76,6 +76,7 @@ export type WinPaths = {
packagedMainPrebundlePath: string;
resourceRoot: string;
setupPath: string;
setupZipPath: string;
startMenuShortcutPath: string;
tarballsRoot: string;
userDesktopShortcutPath: string;
@ -96,6 +97,7 @@ export type WinPackResult = {
installerPath: string | null;
latestYmlPath: string | null;
outputRoot: string;
portableZipPath: string | null;
resourceRoot: string;
runtimeNamespaceRoot: string;
cacheReport: CacheReport;
@ -123,12 +125,13 @@ export type WinSizeReport = {
};
nodeGypRebuild: boolean;
npmRebuild: boolean;
targets: Array<"dir" | "nsis">;
targets: Array<"dir" | "nsis" | "zip">;
webOutputMode: ToolPackConfig["webOutputMode"];
};
generatedAt: string;
installerBytes: number | null;
outputRootBytes: number;
portableZipBytes: number | null;
resourceRootBytes: number;
runtimeNamespaceRoot: string;
topLevel: {

41
tools/pack/src/win/zip.ts Normal file
View file

@ -0,0 +1,41 @@
import { execFile } from "node:child_process";
import { mkdir, rm, stat } from "node:fs/promises";
import { dirname } from "node:path";
import { promisify } from "node:util";
import type { ToolPackConfig } from "../config.js";
import { winResources } from "../resources.js";
import type { WinBuiltAppManifest, WinPaths } from "./types.js";
const execFileAsync = promisify(execFile);
// Produces a portable zip from the unpacked Electron build using the same 7z
// binary that ships with tools-pack for the NSIS payload. The zip lays files
// flat at the archive root so that users can extract it anywhere on Windows
// and run `Open Design.exe` without going through the NSIS installer.
//
// We deliberately do not delegate this to electron-builder's native `zip`
// target: the existing tools-pack flow forces electron-builder to `to: "dir"`
// so the cached `win-unpacked` output can be shared across cache hits and
// post-processed into the custom NSIS installer. Producing the zip from that
// same cached unpacked tree keeps the build deterministic and avoids a
// second electron-builder pass.
export async function buildWinPortableZip(
_config: ToolPackConfig,
paths: WinPaths,
builtApp: WinBuiltAppManifest,
): Promise<void> {
if (process.platform !== "win32") throw new Error("Windows portable zip build must run on Windows");
await mkdir(dirname(paths.setupZipPath), { recursive: true });
await rm(paths.setupZipPath, { force: true });
await execFileAsync(
winResources.sevenZipExe,
["a", "-tzip", "-mx=5", paths.setupZipPath, ".\\*"],
{
cwd: builtApp.unpackedRoot,
windowsHide: true,
},
);
await stat(paths.setupZipPath);
}

View file

@ -24,6 +24,15 @@ afterEach(() => {
}
});
describe("resolveToolPackConfig win build target", () => {
it("accepts the portable zip target and rejects unsupported values", () => {
expect(resolveToolPackConfig("win", { to: "zip" }).to).toBe("zip");
expect(resolveToolPackConfig("win", { to: "all" }).to).toBe("all");
expect(resolveToolPackConfig("win", { to: "nsis" }).to).toBe("nsis");
expect(() => resolveToolPackConfig("win", { to: "dmg" })).toThrow(/unsupported win --to target: dmg/);
});
});
describe("resolveToolPackConfig namespace defaults", () => {
it("keeps ordinary local builds on the default namespace", () => {
expect(resolveToolPackConfig("mac").namespace).toBe("default");

View file

@ -40,6 +40,7 @@ function createPaths(root: string): WinPaths {
packagedMainPrebundlePath: join(namespaceRoot, "assembled", "app", "prebundled", "packaged-main.mjs"),
resourceRoot: join(namespaceRoot, "resources", "open-design"),
setupPath: join(namespaceRoot, "builder", "Open Design-second-setup.exe"),
setupZipPath: join(namespaceRoot, "builder", "Open Design-second-portable.zip"),
startMenuShortcutPath: join(namespaceRoot, "start-menu.lnk"),
tarballsRoot: join(namespaceRoot, "tarballs"),
userDesktopShortcutPath: join(namespaceRoot, "desktop", "user.lnk"),

View file

@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
resolveElectronBuilderWinTargets,
resolveWinTargets,
shouldBuildWinNsisInstaller,
shouldBuildWinPortableZip,
} from "../src/win/report.js";
describe("resolveWinTargets", () => {
it("returns the full target set including the portable zip for `all`", () => {
expect(resolveWinTargets("all")).toEqual(["dir", "nsis", "zip"]);
});
it("returns only the requested single target", () => {
expect(resolveWinTargets("dir")).toEqual(["dir"]);
expect(resolveWinTargets("nsis")).toEqual(["nsis"]);
expect(resolveWinTargets("zip")).toEqual(["zip"]);
});
});
describe("resolveElectronBuilderWinTargets", () => {
it("hides the portable zip from electron-builder because it is built from the cached unpacked dir", () => {
expect(resolveElectronBuilderWinTargets("zip")).toEqual(["dir"]);
expect(resolveElectronBuilderWinTargets("all")).toEqual(["dir", "nsis"]);
expect(resolveElectronBuilderWinTargets("nsis")).toEqual(["nsis"]);
expect(resolveElectronBuilderWinTargets("dir")).toEqual(["dir"]);
});
});
describe("shouldBuildWinNsisInstaller / shouldBuildWinPortableZip", () => {
it("flags the NSIS installer and portable zip independently", () => {
expect(shouldBuildWinNsisInstaller("nsis")).toBe(true);
expect(shouldBuildWinNsisInstaller("all")).toBe(true);
expect(shouldBuildWinNsisInstaller("zip")).toBe(false);
expect(shouldBuildWinNsisInstaller("dir")).toBe(false);
expect(shouldBuildWinPortableZip("zip")).toBe(true);
expect(shouldBuildWinPortableZip("all")).toBe(true);
expect(shouldBuildWinPortableZip("nsis")).toBe(false);
expect(shouldBuildWinPortableZip("dir")).toBe(false);
});
});