This commit is contained in:
@@ -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 }}" \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+43
-23
@@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -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