This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
name: Release
|
||||
|
||||
# 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
|
||||
# artifacts attached and auto-generated notes.
|
||||
on:
|
||||
@@ -23,7 +23,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
name: Build Windows portable
|
||||
name: Build Windows installer
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -59,12 +59,12 @@ jobs:
|
||||
echo "Windows code signing secrets not configured; building unsigned."
|
||||
fi
|
||||
|
||||
- name: Package Windows portable .exe
|
||||
- name: Package Windows installer .exe
|
||||
run: npm run package:windows
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-portable
|
||||
name: windows-installer
|
||||
path: releases/*.exe
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
flags+=(--prerelease)
|
||||
fi
|
||||
gh release create "$TAG" \
|
||||
dist/windows-portable/* \
|
||||
dist/windows-installer/*.exe \
|
||||
dist/linux-artifacts/* \
|
||||
--title "StepForge $TAG" \
|
||||
--target "${{ github.sha }}" \
|
||||
|
||||
@@ -93,8 +93,8 @@ bash scripts/bootstrap-offline.sh # verify toolchain availability
|
||||
bash scripts/verify.sh # full test suite + smoke checks
|
||||
bash scripts/build-release.sh # assemble runnable app directory
|
||||
bash scripts/package-linux.sh # portable tar.gz + .deb (+ AppDir spec)
|
||||
npm run package:windows # portable Windows .exe in releases/
|
||||
pwsh scripts/package-windows.ps1 # same Windows portable build via PowerShell
|
||||
npm run package:windows # Windows installer .exe in releases/
|
||||
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
|
||||
|
||||
@@ -21,6 +21,12 @@ const { readLock } = require('../core/locks');
|
||||
const CaptureService = require('./capture');
|
||||
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
|
||||
// applies Power Throttling (EcoQoS) to background work — and StepForge records
|
||||
// with its window hidden, so the frame-capture worker renderer is exactly the
|
||||
|
||||
@@ -87,6 +87,6 @@ If you want to find commands quickly, press `Ctrl+/` for Quick Actions.
|
||||
## Optional builds
|
||||
|
||||
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/`.
|
||||
3. `bash scripts/package-linux.sh` creates Linux release artifacts.
|
||||
|
||||
@@ -109,7 +109,7 @@ ${toolRows}
|
||||
Fallback policy: when a packaging tool is missing the build still produces
|
||||
the runnable app (portable tarball with launcher) plus whatever package
|
||||
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.
|
||||
|
||||
## Offline guarantee
|
||||
|
||||
+46
-26
@@ -9,12 +9,9 @@ const { build, Platform } = require('electron-builder');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
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 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';
|
||||
const APP_ID = 'com.stepforge.app';
|
||||
|
||||
function findPortableExe(dir) {
|
||||
function findInstallerExe(dir) {
|
||||
if (!fs.existsSync(dir)) return null;
|
||||
const stack = [dir];
|
||||
while (stack.length) {
|
||||
@@ -31,15 +28,12 @@ function findPortableExe(dir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(RELEASE_DIR, { recursive: true });
|
||||
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
||||
|
||||
const config = {
|
||||
appId: 'com.stepforge.app',
|
||||
function createWindowsInstallerConfig(outputDir) {
|
||||
return {
|
||||
appId: APP_ID,
|
||||
productName: 'StepForge',
|
||||
directories: {
|
||||
output: OUTPUT_DIR,
|
||||
output: outputDir,
|
||||
},
|
||||
files: [
|
||||
'app/**/*',
|
||||
@@ -49,33 +43,59 @@ async function main() {
|
||||
],
|
||||
asar: true,
|
||||
compression: 'normal',
|
||||
artifactName: ARTIFACT_NAME,
|
||||
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 {
|
||||
await build({
|
||||
targets: Platform.WINDOWS.createTarget('portable'),
|
||||
targets: Platform.WINDOWS.createTarget('nsis'),
|
||||
config,
|
||||
});
|
||||
} 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);
|
||||
if (!builtExe) {
|
||||
throw new Error(`No .exe artifact was produced in ${OUTPUT_DIR}`);
|
||||
const builtInstaller = findInstallerExe(outputDir);
|
||||
if (!builtInstaller) {
|
||||
throw new Error(`No installer .exe artifact was produced in ${outputDir}`);
|
||||
}
|
||||
|
||||
const releaseExe = path.join(RELEASE_DIR, path.basename(builtExe));
|
||||
fs.copyFileSync(builtExe, releaseExe);
|
||||
const releaseInstaller = path.join(releaseDir, path.basename(builtInstaller));
|
||||
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) => {
|
||||
console.error(err.message || err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
if (require.main === module) {
|
||||
buildWindowsInstaller().catch((err) => {
|
||||
console.error(err.message || err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APP_ID,
|
||||
createWindowsInstallerConfig,
|
||||
findInstallerExe,
|
||||
buildWindowsInstaller,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user