mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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:
parent
709fc0a497
commit
bfcafc81fd
19 changed files with 218 additions and 26 deletions
29
.github/scripts/release/assets/win.ps1
vendored
29
.github/scripts/release/assets/win.ps1
vendored
|
|
@ -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:'
|
||||
|
|
|
|||
12
.github/scripts/release/r2/publish-platform.ts
vendored
12
.github/scripts/release/r2/publish-platform.ts
vendored
|
|
@ -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",
|
||||
|
|
|
|||
28
.github/scripts/release/r2/publish.sh
vendored
28
.github/scripts/release/r2/publish.sh
vendored
|
|
@ -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")) {
|
||||
|
|
|
|||
6
.github/scripts/release/r2/summary.sh
vendored
6
.github/scripts/release/r2/summary.sh
vendored
|
|
@ -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),
|
||||
],
|
||||
[
|
||||
|
|
|
|||
3
.github/scripts/release/r2/verify.sh
vendored
3
.github/scripts/release/r2/verify.sh
vendored
|
|
@ -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"
|
||||
|
|
|
|||
4
.github/workflows/release-beta.yml
vendored
4
.github/workflows/release-beta.yml
vendored
|
|
@ -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"
|
||||
|
|
|
|||
6
.github/workflows/release-preview.yml
vendored
6
.github/workflows/release-preview.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
6
.github/workflows/release-stable.yml
vendored
6
.github/workflows/release-stable.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
41
tools/pack/src/win/zip.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
43
tools/pack/tests/win-targets.test.ts
Normal file
43
tools/pack/tests/win-targets.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue