Add Windows installer packaging
Template tests / tests (push) Successful in 1m40s

This commit is contained in:
2026-06-23 11:01:43 -05:00
parent f4278d118c
commit 058582d7af
7 changed files with 93 additions and 35 deletions
+5 -5
View File
@@ -1,7 +1,7 @@
name: Release name: Release
# Manual release: from the GitHub "Actions" tab pick "Release", click # Manual release: from the GitHub "Actions" tab pick "Release", click
# "Run workflow", enter the version tag, and it builds the Windows portable # "Run workflow", enter the version tag, and it builds the Windows installer
# .exe and the Linux tarball + .deb, then publishes a GitHub Release with all # .exe and the Linux tarball + .deb, then publishes a GitHub Release with all
# artifacts attached and auto-generated notes. # artifacts attached and auto-generated notes.
on: on:
@@ -23,7 +23,7 @@ permissions:
jobs: jobs:
build-windows: build-windows:
name: Build Windows portable name: Build Windows installer
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -59,12 +59,12 @@ jobs:
echo "Windows code signing secrets not configured; building unsigned." echo "Windows code signing secrets not configured; building unsigned."
fi fi
- name: Package Windows portable .exe - name: Package Windows installer .exe
run: npm run package:windows run: npm run package:windows
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: windows-portable name: windows-installer
path: releases/*.exe path: releases/*.exe
if-no-files-found: error if-no-files-found: error
@@ -126,7 +126,7 @@ jobs:
flags+=(--prerelease) flags+=(--prerelease)
fi fi
gh release create "$TAG" \ gh release create "$TAG" \
dist/windows-portable/* \ dist/windows-installer/*.exe \
dist/linux-artifacts/* \ dist/linux-artifacts/* \
--title "StepForge $TAG" \ --title "StepForge $TAG" \
--target "${{ github.sha }}" \ --target "${{ github.sha }}" \
+2 -2
View File
@@ -93,8 +93,8 @@ bash scripts/bootstrap-offline.sh # verify toolchain availability
bash scripts/verify.sh # full test suite + smoke checks bash scripts/verify.sh # full test suite + smoke checks
bash scripts/build-release.sh # assemble runnable app directory bash scripts/build-release.sh # assemble runnable app directory
bash scripts/package-linux.sh # portable tar.gz + .deb (+ AppDir spec) bash scripts/package-linux.sh # portable tar.gz + .deb (+ AppDir spec)
npm run package:windows # portable Windows .exe in releases/ npm run package:windows # Windows installer .exe in releases/
pwsh scripts/package-windows.ps1 # same Windows portable build via PowerShell pwsh scripts/package-windows.ps1 # same Windows installer build via PowerShell
``` ```
See [build/build_report.md](build/build_report.md) for what was produced on See [build/build_report.md](build/build_report.md) for what was produced on
+6
View File
@@ -21,6 +21,12 @@ const { readLock } = require('../core/locks');
const CaptureService = require('./capture'); const CaptureService = require('./capture');
const { keepProcessesResponsive } = require('./win-power'); const { keepProcessesResponsive } = require('./win-power');
const APP_ID = 'com.stepforge.app';
if (process.platform === 'win32') {
app.setAppUserModelId(APP_ID);
}
// Keep capture working on battery. In a power-saving plan on DC power, Windows // Keep capture working on battery. In a power-saving plan on DC power, Windows
// applies Power Throttling (EcoQoS) to background work — and StepForge records // applies Power Throttling (EcoQoS) to background work — and StepForge records
// with its window hidden, so the frame-capture worker renderer is exactly the // with its window hidden, so the frame-capture worker renderer is exactly the
+1 -1
View File
@@ -87,6 +87,6 @@ If you want to find commands quickly, press `Ctrl+/` for Quick Actions.
## Optional builds ## Optional builds
1. `bash scripts/build-release.sh` assembles the offline release layout. 1. `bash scripts/build-release.sh` assembles the offline release layout.
2. `npm run package:windows` creates the portable Windows `.exe` in 2. `npm run package:windows` creates the Windows installer `.exe` in
`releases/`. `releases/`.
3. `bash scripts/package-linux.sh` creates Linux release artifacts. 3. `bash scripts/package-linux.sh` creates Linux release artifacts.
+1 -1
View File
@@ -109,7 +109,7 @@ ${toolRows}
Fallback policy: when a packaging tool is missing the build still produces Fallback policy: when a packaging tool is missing the build still produces
the runnable app (portable tarball with launcher) plus whatever package the runnable app (portable tarball with launcher) plus whatever package
formats the available tools allow. Windows artifacts are produced by formats the available tools allow. Windows artifacts are produced by
\`npm run package:windows\` (electron-builder, portable .exe); .msi/.rpm/ \`npm run package:windows\` (electron-builder, installer .exe); .msi/.rpm/
AppImage require the tools listed above and are skipped on this host. AppImage require the tools listed above and are skipped on this host.
## Offline guarantee ## Offline guarantee
+44 -24
View File
@@ -9,12 +9,9 @@ const { build, Platform } = require('electron-builder');
const ROOT_DIR = path.resolve(__dirname, '..'); const ROOT_DIR = path.resolve(__dirname, '..');
const PACKAGE_JSON = require(path.join(ROOT_DIR, 'package.json')); const PACKAGE_JSON = require(path.join(ROOT_DIR, 'package.json'));
const RELEASE_DIR = path.resolve(process.env.STEPFORGE_RELEASE_DIR || path.join(ROOT_DIR, 'releases')); const APP_ID = 'com.stepforge.app';
const WORK_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'stepforge-win-'));
const OUTPUT_DIR = path.join(WORK_DIR, 'output');
const ARTIFACT_NAME = 'stepforge-windows-x64-portable.exe';
function findPortableExe(dir) { function findInstallerExe(dir) {
if (!fs.existsSync(dir)) return null; if (!fs.existsSync(dir)) return null;
const stack = [dir]; const stack = [dir];
while (stack.length) { while (stack.length) {
@@ -31,15 +28,12 @@ function findPortableExe(dir) {
return null; return null;
} }
async function main() { function createWindowsInstallerConfig(outputDir) {
fs.mkdirSync(RELEASE_DIR, { recursive: true }); return {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true }); appId: APP_ID,
const config = {
appId: 'com.stepforge.app',
productName: 'StepForge', productName: 'StepForge',
directories: { directories: {
output: OUTPUT_DIR, output: outputDir,
}, },
files: [ files: [
'app/**/*', 'app/**/*',
@@ -49,33 +43,59 @@ async function main() {
], ],
asar: true, asar: true,
compression: 'normal', compression: 'normal',
artifactName: ARTIFACT_NAME,
win: { win: {
target: ['portable'], target: ['nsis'],
},
nsis: {
oneClick: false,
allowToChangeInstallationDirectory: true,
createDesktopShortcut: true,
createStartMenuShortcut: true,
shortcutName: 'StepForge',
}, },
}; };
}
async function buildWindowsInstaller() {
const releaseDir = path.resolve(process.env.STEPFORGE_RELEASE_DIR || path.join(ROOT_DIR, 'releases'));
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stepforge-win-'));
const outputDir = path.join(workDir, 'output');
fs.mkdirSync(releaseDir, { recursive: true });
fs.rmSync(outputDir, { recursive: true, force: true });
const config = createWindowsInstallerConfig(outputDir);
try { try {
await build({ await build({
targets: Platform.WINDOWS.createTarget('portable'), targets: Platform.WINDOWS.createTarget('nsis'),
config, config,
}); });
} catch (err) { } catch (err) {
throw new Error(`Windows portable build failed: ${err.message}`); throw new Error(`Windows installer build failed: ${err.message}`);
} }
const builtExe = findPortableExe(OUTPUT_DIR); const builtInstaller = findInstallerExe(outputDir);
if (!builtExe) { if (!builtInstaller) {
throw new Error(`No .exe artifact was produced in ${OUTPUT_DIR}`); throw new Error(`No installer .exe artifact was produced in ${outputDir}`);
} }
const releaseExe = path.join(RELEASE_DIR, path.basename(builtExe)); const releaseInstaller = path.join(releaseDir, path.basename(builtInstaller));
fs.copyFileSync(builtExe, releaseExe); fs.copyFileSync(builtInstaller, releaseInstaller);
console.log(`StepForge ${PACKAGE_JSON.version} Windows portable build written to ${releaseExe}`); console.log(`StepForge ${PACKAGE_JSON.version} Windows installer written to ${releaseInstaller}`);
} }
main().catch((err) => { if (require.main === module) {
buildWindowsInstaller().catch((err) => {
console.error(err.message || err); console.error(err.message || err);
process.exitCode = 1; process.exitCode = 1;
}); });
}
module.exports = {
APP_ID,
createWindowsInstallerConfig,
findInstallerExe,
buildWindowsInstaller,
};
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const assert = require('node:assert/strict');
const { makeTmpDir, rmrf } = require('./helpers');
const { createWindowsInstallerConfig, findInstallerExe } = require('../../scripts/package-windows');
test('Windows packaging uses an assisted NSIS installer', (t) => {
const config = createWindowsInstallerConfig('/tmp/stepforge-output');
assert.deepEqual(config.win.target, ['nsis']);
assert.equal(config.nsis.oneClick, false);
assert.equal(config.nsis.allowToChangeInstallationDirectory, true);
assert.equal(config.nsis.createDesktopShortcut, true);
assert.equal(config.nsis.createStartMenuShortcut, true);
assert.equal(config.nsis.shortcutName, 'StepForge');
assert.equal(config.asar, true);
assert.ok(config.files.includes('app/**/*'));
assert.ok(config.files.includes('core/**/*'));
assert.ok(config.files.includes('exporters/**/*'));
assert.ok(!config.files.includes('assets/**/*'));
const tmp = makeTmpDir('windows-installer');
t.after(() => rmrf(tmp));
fs.mkdirSync(path.join(tmp, 'nested', 'deeper'), { recursive: true });
const installer = path.join(tmp, 'nested', 'deeper', 'StepForge Setup 0.2.0.exe');
fs.writeFileSync(installer, Buffer.from('fake installer'));
assert.equal(findInstallerExe(tmp), installer);
});