Sign the macOS bundle, or it arrives "damaged"
The first release could not be opened: macOS said "WarpEngine Store is damaged and can't be opened. You should move it to the Bin." Not a wording problem — an integrity one. electron-builder found no signing identity and skipped signing, so the bundle kept only the linker's ad-hoc signature on its main executable, with no resource seal. `codesign --verify` said "code has no resources but signature indicates they must be present", and Gatekeeper reports that as damaged and offers no way past it, unlike an un-notarised app which can at least be approved. `scripts/after-pack.js` now signs the bundle itself during packaging. Measured on a copy unzipped from the artifact with the quarantine flag set by hand: before code has no resources but signature indicates they must be present after valid on disk; satisfies its Designated Requirement and the identifier is ours rather than `Electron`. `syspolicy_check` is down to its expected "adhoc signed" warning. A downloaded copy still has to be approved — that is Gatekeeper policy for anything un-notarised, and notarisation needs a paid Developer ID — so the README and the release notes lead with the one command that does it. Two smaller things the failure turned up: - The self-test was passing silently. With a copy of the app already open, the second process lost the single-instance lock and exited 0 with no output, which reads exactly like success. It now uses its own user-data directory and skips the lock, and it caught a real launch failure immediately afterwards. - The README claimed right-click ▸ Open was enough. It was not, and I had not checked it — replaced with what the measurements support. v1.0.0's attachments are withdrawn rather than left downloadable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,10 +27,27 @@ Grab the package for your machine from the
|
|||||||
and open it. On first run, if there is no store on the machine yet, the window
|
and open it. On first run, if there is no store on the machine yet, the window
|
||||||
offers to download one — that is the whole setup.
|
offers to download one — that is the whole setup.
|
||||||
|
|
||||||
The macOS build is **not signed or notarised**, so the first open needs
|
### Opening it on macOS
|
||||||
*right-click ▸ Open* (or *System Settings ▸ Privacy & Security*). Nothing the
|
|
||||||
store itself downloads is affected: those files are fetched by Python, which does
|
The build is ad-hoc signed but **not notarised**, so macOS asks before running a
|
||||||
not set the quarantine flag.
|
copy that came from a browser. The reliable way through:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xattr -dr com.apple.quarantine "/Applications/WarpEngine Store.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
If macOS offers *Open Anyway* under **System Settings ▸ Privacy & Security** after
|
||||||
|
a blocked attempt, that works as well. Notarisation is the only thing that removes
|
||||||
|
the step entirely, and it needs a paid Apple Developer ID.
|
||||||
|
|
||||||
|
Nothing the store itself downloads is affected: Python fetches those files, and
|
||||||
|
Python does not set the quarantine flag.
|
||||||
|
|
||||||
|
**v1.0.0 could not be opened at all** — it reported *"is damaged"*. The bundle had
|
||||||
|
never been signed; only its main executable carried the linker's ad-hoc signature,
|
||||||
|
so there was no resource seal and Gatekeeper refused it outright rather than
|
||||||
|
asking. `scripts/after-pack.js` signs the bundle during the build now, and the
|
||||||
|
result verifies as `valid on disk`.
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
@@ -61,6 +78,10 @@ npm run dist:mac # or dist:win / dist:linux
|
|||||||
is ESM-only, and older Node cannot `require()` it. The packaged app carries its
|
is ESM-only, and older Node cannot `require()` it. The packaged app carries its
|
||||||
own runtime.
|
own runtime.
|
||||||
|
|
||||||
|
`npm run uitest` runs with its own user-data directory and without the
|
||||||
|
single-instance lock. Otherwise a copy the user already has open swallows the test
|
||||||
|
process, which exits 0 and reads as a pass.
|
||||||
|
|
||||||
Both test scripts accept a sandbox store instead of the real one, which is how
|
Both test scripts accept a sandbox store instead of the real one, which is how
|
||||||
this repository is tested without touching a working installation:
|
this repository is tested without touching a working installation:
|
||||||
|
|
||||||
@@ -95,10 +116,17 @@ too — that is why the indirection is there.
|
|||||||
|
|
||||||
## Verified, and not
|
## Verified, and not
|
||||||
|
|
||||||
Exercised on macOS (arm64): the store is discovered, the catalog lists, a sync
|
Exercised on macOS (arm64), with the packaged app from the release rather than a
|
||||||
installs, the window renders the installed state, and `npm run uitest` passes with
|
dev run: the store is discovered, the catalog lists, a sync installs, the window
|
||||||
the grid rendered and both languages in the picker. The bootstrap download was run
|
renders the installed state, and `npm run uitest` passes with the grid rendered
|
||||||
into an empty directory and the resulting store answered the bridge.
|
and both languages in the picker. The bootstrap download was run into an empty
|
||||||
|
directory and the resulting store answered the bridge.
|
||||||
|
|
||||||
|
The signing was measured rather than assumed, by setting the quarantine flag on a
|
||||||
|
copy unzipped from the release artifact: `codesign --verify --deep --strict` is
|
||||||
|
clean, and `syspolicy_check` reports only the expected *"adhoc signed"* warning.
|
||||||
|
A quarantined copy is still stopped until it is approved — that part is Gatekeeper
|
||||||
|
policy, not a fault in the package.
|
||||||
|
|
||||||
**Not tried on Linux or Windows.** The paths and the launch behaviour are written
|
**Not tried on Linux or Windows.** The paths and the launch behaviour are written
|
||||||
for them, and the store CLI itself has the same gap — `.desktop` and `.lnk`
|
for them, and the store CLI itself has the same gap — `.desktop` and `.lnk`
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ async function guarded (fn) {
|
|||||||
// would otherwise be noticed: the main process log stays empty.
|
// would otherwise be noticed: the main process log stays empty.
|
||||||
const SELFTEST = process.argv.includes('--selftest')
|
const SELFTEST = process.argv.includes('--selftest')
|
||||||
|
|
||||||
|
// A test run must never be swallowed by a copy the user already has open: it gets
|
||||||
|
// its own user-data directory and skips the single-instance lock. Without this the
|
||||||
|
// second process exits silently with status 0, which reads as a passing test.
|
||||||
|
if (SELFTEST) {
|
||||||
|
app.setPath('userData', path.join(app.getPath('temp'), 'warpstore-gui-selftest'))
|
||||||
|
}
|
||||||
|
|
||||||
async function selftest () {
|
async function selftest () {
|
||||||
const result = await win.webContents.executeJavaScript(`(() => ({
|
const result = await win.webContents.executeJavaScript(`(() => ({
|
||||||
cards: document.querySelectorAll('.card').length,
|
cards: document.querySelectorAll('.card').length,
|
||||||
@@ -215,7 +222,7 @@ ipcMain.handle('app:openExternal', async (_event, url) => {
|
|||||||
|
|
||||||
// --- lifecycle ------------------------------------------------------------
|
// --- lifecycle ------------------------------------------------------------
|
||||||
|
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!SELFTEST && !app.requestSingleInstanceLock()) {
|
||||||
app.quit()
|
app.quit()
|
||||||
} else {
|
} else {
|
||||||
app.on('second-instance', () => {
|
app.on('second-instance', () => {
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "warp-engine-desktop-gui",
|
"name": "warp-engine-desktop-gui",
|
||||||
"productName": "WarpEngine Store",
|
"productName": "WarpEngine Store",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Graphical client for a WarpEngine desktop store: install the catalog into your own application menu.",
|
"description": "Graphical client for a WarpEngine desktop store: install the catalog into your own application menu.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Teletype Games <games@teletype.hu>",
|
"author": "Teletype Games <games@teletype.hu>",
|
||||||
@@ -51,7 +51,8 @@
|
|||||||
"AppImage",
|
"AppImage",
|
||||||
"deb"
|
"deb"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"afterPack": "scripts/after-pack.js"
|
||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"electron@43.4.0": true
|
"electron@43.4.0": true
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
'use strict'
|
||||||
|
// Ad-hoc sign the macOS bundle after packing.
|
||||||
|
//
|
||||||
|
// Without this the bundle carries only the linker's ad-hoc signature on the main
|
||||||
|
// executable, with no resource seal — `codesign --verify` says "code has no
|
||||||
|
// resources but signature indicates they must be present". That runs fine
|
||||||
|
// locally, but a browser download adds the quarantine flag, Gatekeeper evaluates
|
||||||
|
// the broken seal, and macOS reports the app as *damaged* rather than merely
|
||||||
|
// unverified. The first release shipped exactly that.
|
||||||
|
//
|
||||||
|
// An ad-hoc signature is not a Developer ID and does not notarise anything: the
|
||||||
|
// user still has to right-click ▸ Open the first time. It is the difference
|
||||||
|
// between "unidentified developer" and "move it to the Bin".
|
||||||
|
|
||||||
|
const { execFileSync } = require('node:child_process')
|
||||||
|
const path = require('node:path')
|
||||||
|
|
||||||
|
exports.default = async function afterPack (context) {
|
||||||
|
if (context.electronPlatformName !== 'darwin') return
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
console.log(' • ad-hoc signing skipped reason=codesign only exists on macOS')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
|
||||||
|
// --deep is the pragmatic choice for ad-hoc signing a bundle with nested
|
||||||
|
// frameworks and helpers; Apple discourages it for real identities, where the
|
||||||
|
// inner-to-outer order matters.
|
||||||
|
execFileSync('codesign', ['--force', '--deep', '--sign', '-', app], { stdio: 'inherit' })
|
||||||
|
execFileSync('codesign', ['--verify', '--deep', '--strict', '--verbose=1', app], { stdio: 'inherit' })
|
||||||
|
console.log(` • ad-hoc signed ${app}`)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user