@gjsify/cli 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +144 -135
- package/lib/commands/onboard.js +11 -0
- package/lib/commands/publish.d.ts +41 -0
- package/lib/commands/publish.js +222 -2
- package/lib/commands/ship.js +89 -76
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +64 -0
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/publish-readback.d.ts +132 -0
- package/lib/utils/publish-readback.js +273 -0
- package/lib/utils/ship/discover.js +85 -0
- package/lib/utils/ship/formats.d.ts +23 -0
- package/lib/utils/ship/formats.js +44 -0
- package/lib/utils/ship/launcher.js +57 -0
- package/lib/utils/ship/layout.d.ts +64 -2
- package/lib/utils/ship/layout.js +169 -12
- package/lib/utils/ship/msi.js +24 -7
- package/lib/utils/ship/payload.js +22 -3
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.js +35 -0
- package/lib/utils/ship/plist.d.ts +41 -1
- package/lib/utils/ship/plist.js +59 -1
- package/lib/utils/ship/rpm.js +7 -0
- package/lib/utils/ship/settings.d.ts +76 -5
- package/lib/utils/ship/settings.js +89 -8
- package/lib/utils/ship/share-dirs.d.ts +30 -0
- package/lib/utils/ship/share-dirs.js +30 -0
- package/lib/utils/ship/signing.d.ts +122 -6
- package/lib/utils/ship/signing.js +256 -67
- package/lib/utils/ship/stage-writer.js +20 -1
- package/lib/utils/ship/types.d.ts +34 -2
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/package.json +19 -19
|
@@ -30,38 +30,53 @@
|
|
|
30
30
|
// exist before the validation, because they are computed from its output,
|
|
31
31
|
// and the arriving stage is never written to at all.
|
|
32
32
|
//
|
|
33
|
-
//
|
|
33
|
+
// THREE THINGS THIS FILE USED TO REFUSE TO DO, and the measurement that changed
|
|
34
|
+
// each of them (ADR 0040). Every one is marked UNVERIFIED where it is: this
|
|
35
|
+
// repository has no macOS host of its own, so what follows is argv and file
|
|
36
|
+
// structure decided from Apple's documentation and held by unit tests, with the
|
|
37
|
+
// darwin CI leg as the only thing that has ever run `codesign` here.
|
|
34
38
|
//
|
|
35
|
-
// *
|
|
36
|
-
//
|
|
37
|
-
// a
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// the
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// *
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
// * IT NOW SEALS THE `<App>.app` BUNDLE, and the reason it did not was WRONG.
|
|
40
|
+
// The header used to say a bundle seal "would not survive into the `.zip`"
|
|
41
|
+
// because a script main executable's signature lives in an extended attribute
|
|
42
|
+
// and the payload round trip carries none. That is Apple's rule for a LOOSE
|
|
43
|
+
// file, not for a bundle. Apple's TN3126 gives four cases and ours is the
|
|
44
|
+
// second: *"If the item is a bundle without a Mach-O image, the code signature
|
|
45
|
+
// is stored in the bundle's _CodeSignature directory"* — regular files, mode
|
|
46
|
+
// 0644, which a plain zip carries like any other. TN2206 states the same from
|
|
47
|
+
// the other side: *"a properly-signed app that has all of its files in the
|
|
48
|
+
// correct places will not contain any signatures stored as extended
|
|
49
|
+
// attributes."* So the seal is reachable, and what was actually blocking it was
|
|
50
|
+
// this file's own refusal of a changed file set. {@link signPayload} now allows
|
|
51
|
+
// exactly the additions the seal makes and still refuses every other one.
|
|
52
|
+
// * IT NOW PASSES `--options runtime`, `--entitlements` and (for a real identity)
|
|
53
|
+
// `--timestamp`. The hardened runtime is a bit in the CODE DIRECTORY, which an
|
|
54
|
+
// ad-hoc signature has, so the flag is accepted with `--sign -` as it is with a
|
|
55
|
+
// Developer ID. `--timestamp` is not: a timestamp lives in the CMS structure
|
|
56
|
+
// and an ad-hoc signature has no CMS and no certificate to pin, so it is passed
|
|
57
|
+
// only for a named identity — and Apple documents nothing about what
|
|
58
|
+
// `codesign --sign - --timestamp` does, which is a second reason not to send it.
|
|
59
|
+
// * IT NOW STAPLES what `stapler` accepts. `stapler(1)` is explicit —
|
|
60
|
+
// *"stapler works only with UDIF disk images, signed \"flat\" installer
|
|
61
|
+
// packages, and certain code-signed executable bundles such as \".app\""* — and
|
|
62
|
+
// Apple's notarisation guide adds the exception that decides our zip:
|
|
63
|
+
// *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
64
|
+
// Instead, run stapler against each item that you added to the archive."*
|
|
65
|
+
// `canCarryTicket` in `utils/ship/formats.ts` is that fact per format, so the
|
|
66
|
+
// `.dmg` is stapled and the `.zip` is told what a user has to do instead.
|
|
67
|
+
//
|
|
68
|
+
// WHAT IS STILL NOT DONE, deliberately: `signtool` is never given a timestamp URL
|
|
69
|
+
// (see the win32 row), and the entitlement § A16 is about —
|
|
70
|
+
// `com.apple.security.cs.disable-library-validation` — is NOT granted, because
|
|
71
|
+
// § A4's re-sign of every image in the closure is the design of record and
|
|
72
|
+
// granting the entitlement would quietly make it look unnecessary.
|
|
73
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
74
|
+
import { dirname, join, sep } from 'node:path';
|
|
61
75
|
import { isOnPath } from '../check-system-deps.js';
|
|
62
76
|
import { describeExit, spawnToCompletion } from '../spawn.js';
|
|
63
77
|
import { listFilesRecursive } from './discover.js';
|
|
64
78
|
import { classifyBinary } from './payload.js';
|
|
79
|
+
import { renderEntitlements } from './plist.js';
|
|
65
80
|
import { writePayload } from './stage-writer.js';
|
|
66
81
|
/**
|
|
67
82
|
* The signers, keyed by the LAYOUT they sign — never by the host they run on.
|
|
@@ -87,10 +102,63 @@ export const SIGNERS = {
|
|
|
87
102
|
// ad-hoc (§ A4: 106 of 106) — `install_name_tool` invalidates the original
|
|
88
103
|
// during relocation and the relocator re-signs. Without it `codesign`
|
|
89
104
|
// refuses a file that already carries a signature.
|
|
90
|
-
|
|
105
|
+
//
|
|
106
|
+
// `--options runtime` on BOTH identities and `--timestamp` on only one:
|
|
107
|
+
// the hardened runtime is a code-directory bit, which an ad-hoc signature
|
|
108
|
+
// has, while a timestamp is a CMS countersignature over a CERTIFICATE, and
|
|
109
|
+
// an ad-hoc signature has neither ("Ad-hoc signing does not use an identity
|
|
110
|
+
// at all", `codesign(1)`). Apple documents nothing about what
|
|
111
|
+
// `--sign - --timestamp` does, so it is not sent rather than guessed at —
|
|
112
|
+
// and the one thing `codesign(1)` does promise about the flag is that "if
|
|
113
|
+
// the timestamp authority service cannot be contacted … the signing
|
|
114
|
+
// operation will fail", which is not a failure worth inventing for a
|
|
115
|
+
// signature that cannot carry the result.
|
|
116
|
+
args: ({ identity, file, entitlements, adhoc }) => [
|
|
117
|
+
'--force',
|
|
118
|
+
'--sign',
|
|
119
|
+
identity,
|
|
120
|
+
'--options',
|
|
121
|
+
'runtime',
|
|
122
|
+
...(entitlements === undefined ? [] : ['--entitlements', entitlements]),
|
|
123
|
+
...(adhoc ? [] : ['--timestamp']),
|
|
124
|
+
file,
|
|
125
|
+
],
|
|
126
|
+
// FOUR OF THE REFERENCE'S SIX, and both omissions are decisions:
|
|
127
|
+
//
|
|
128
|
+
// * `com.apple.security.cs.get-task-allow` is granted by
|
|
129
|
+
// `refs/node/tools/osx-entitlements.plist:15` and is a DEBUGGING
|
|
130
|
+
// entitlement — it lets another process attach to this one. Apple's
|
|
131
|
+
// notarisation rules refuse a Developer-ID artifact carrying it, so
|
|
132
|
+
// shipping it would trade a working debug build for an artifact that
|
|
133
|
+
// cannot be distributed. The reference signs its own build; we sign what
|
|
134
|
+
// a stranger downloads.
|
|
135
|
+
// * `com.apple.security.cs.disable-library-validation` is § A16's open
|
|
136
|
+
// question and stays open. § A4's design of record is that every Mach-O
|
|
137
|
+
// in the closure is re-signed with the SAME identity as the launcher, so
|
|
138
|
+
// library validation has nothing to object to — and granting the
|
|
139
|
+
// entitlement anyway would make that re-sign look optional the first
|
|
140
|
+
// time somebody reads this list.
|
|
141
|
+
//
|
|
142
|
+
// The four that remain are what a shipped V8 needs under a hardened
|
|
143
|
+
// runtime, plus the one our own loader story needs:
|
|
144
|
+
// `@gjsify/node-gi`'s `maybeReexecForGtkRuntime` sets `DYLD_*` for its
|
|
145
|
+
// child on darwin, and a hardened process may not do that without
|
|
146
|
+
// `allow-dyld-environment-variables`.
|
|
147
|
+
entitlements: [
|
|
148
|
+
'com.apple.security.cs.allow-jit',
|
|
149
|
+
'com.apple.security.cs.allow-unsigned-executable-memory',
|
|
150
|
+
'com.apple.security.cs.disable-executable-page-protection',
|
|
151
|
+
'com.apple.security.cs.allow-dyld-environment-variables',
|
|
152
|
+
],
|
|
153
|
+
// The `.app` is a bundle, and a bundle's signature is the seal over its
|
|
154
|
+
// whole tree — the images alone leave `codesign --verify` with nothing to
|
|
155
|
+
// read at the bundle level.
|
|
156
|
+
sealsBundle: true,
|
|
157
|
+
sealAddPrefix: (bundleRoot) => `${bundleRoot}/Contents/_CodeSignature/`,
|
|
91
158
|
configKey: 'gjsify.ship.sign.darwin.identity',
|
|
92
|
-
gap: 'the images are signed
|
|
93
|
-
'
|
|
159
|
+
gap: 'the images are signed and the bundle is sealed; UNVERIFIED end to end — no macOS host in this ' +
|
|
160
|
+
'repository has run a `--sign` over a real `.app`, and ADR 0024 § A16 still leaves library ' +
|
|
161
|
+
'validation unmeasured',
|
|
94
162
|
},
|
|
95
163
|
win32: {
|
|
96
164
|
layoutOs: 'win32',
|
|
@@ -108,7 +176,17 @@ export const SIGNERS = {
|
|
|
108
176
|
// records that SmartScreen only WARNS until per-file-hash reputation
|
|
109
177
|
// accrues — signed or not. The argv is unit-tested, the skip and the
|
|
110
178
|
// refusals are e2e-tested, and the invocation itself has no proof.
|
|
111
|
-
args: (identity, file) => ['sign', '/n', identity, '/fd', 'sha256', file],
|
|
179
|
+
args: ({ identity, file }) => ['sign', '/n', identity, '/fd', 'sha256', file],
|
|
180
|
+
// Windows has no equivalent concept. An Authenticode signature carries no
|
|
181
|
+
// capability claims — what a hardened runtime and an entitlement do on
|
|
182
|
+
// macOS is done on Windows by the manifest and by AppContainer, neither of
|
|
183
|
+
// which this signer touches.
|
|
184
|
+
entitlements: [],
|
|
185
|
+
// A Windows program directory is a DIRECTORY and nothing more: there is no
|
|
186
|
+
// manifest to seal and no per-directory signature format. `signtool` signs
|
|
187
|
+
// images, one at a time, which is what the loop above already does.
|
|
188
|
+
sealsBundle: false,
|
|
189
|
+
sealAddPrefix: () => '',
|
|
112
190
|
configKey: 'gjsify.ship.sign.win32.identity',
|
|
113
191
|
gap: 'no timestamp countersignature is requested, so the signature expires with the certificate ' +
|
|
114
192
|
'(UNVERIFIED: no run in this repository has ever invoked signtool)',
|
|
@@ -174,6 +252,31 @@ export function assertHostCanSign(signer) {
|
|
|
174
252
|
'a legitimate output (ADR 0024 § A13); silently producing one while --sign was asked for is not.');
|
|
175
253
|
}
|
|
176
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* What a signing run did to the payload's FILE SET, split three ways.
|
|
257
|
+
*
|
|
258
|
+
* PURE, and extracted from {@link signPayload} for one reason: it is the whole of
|
|
259
|
+
* the seal decision and the only half of it that can be checked without a macOS
|
|
260
|
+
* host. Everything else about a bundle seal needs `codesign`, which this
|
|
261
|
+
* repository has on exactly one CI leg; this function needs nothing.
|
|
262
|
+
*
|
|
263
|
+
* `sealPrefix` absent means "this signer seals nothing", and then EVERY addition
|
|
264
|
+
* is unexpected — which is what the file did before ADR 0040 and is still the
|
|
265
|
+
* right answer for `signtool`.
|
|
266
|
+
*/
|
|
267
|
+
export function partitionSignedFileSet(input) {
|
|
268
|
+
const planned = new Set(input.planned);
|
|
269
|
+
const arrived = new Set(input.arrived);
|
|
270
|
+
const added = [...arrived].filter((path) => !planned.has(path)).sort();
|
|
271
|
+
const prefix = input.sealPrefix;
|
|
272
|
+
return {
|
|
273
|
+
sealed: prefix === undefined ? [] : added.filter((path) => path.startsWith(prefix)),
|
|
274
|
+
unexpected: prefix === undefined ? added : added.filter((path) => !path.startsWith(prefix)),
|
|
275
|
+
removed: [...planned].filter((path) => !arrived.has(path)).sort(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** `codesign --sign -`: the one identity value that names no identity at all. */
|
|
279
|
+
export const ADHOC_IDENTITY = '-';
|
|
177
280
|
/**
|
|
178
281
|
* Re-sign every signable image in the payload, and hand back the new bytes.
|
|
179
282
|
*
|
|
@@ -184,28 +287,29 @@ export function assertHostCanSign(signer) {
|
|
|
184
287
|
* same tree fail `readStage`'s size comparison (measured — see the header), and
|
|
185
288
|
* `writeStage`'s wipe would delete the payload it was about to pack.
|
|
186
289
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
290
|
+
* TWO STEPS ON DARWIN, ONE EVERYWHERE ELSE: every Mach-O in the closure, then the
|
|
291
|
+
* bundle itself. The second is what `codesign --verify <App>.app` reads, and it
|
|
292
|
+
* writes five regular files into `Contents/_CodeSignature/` that the returned
|
|
293
|
+
* payload carries — so the seal reaches the `.zip` and the `.dmg` alike, which is
|
|
294
|
+
* the correction ADR 0040 makes to this file's previous claim that it could not.
|
|
295
|
+
*
|
|
296
|
+
* THE FILE SET MAY ONLY GROW WHERE THE SEAL WRITES, and that is asserted rather
|
|
297
|
+
* than assumed. Both tools rewrite an image in place, so a payload that gained a
|
|
298
|
+
* path outside `sealAddPrefix` — or lost one anywhere — means something else ran,
|
|
299
|
+
* and its files would be packed with no mode from the plan.
|
|
192
300
|
*/
|
|
193
301
|
export async function signPayload(input) {
|
|
194
302
|
const { payload, signer, workDir } = input;
|
|
195
303
|
writePayload(workDir, payload, '');
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
input.log(`nothing in this payload is a ${signer.signs === 'macho' ? 'Mach-O' : 'PE'} image, so ` +
|
|
203
|
-
`${signer.tool} signed 0 file(s). The artifact carries no signature.`);
|
|
204
|
-
return [...payload];
|
|
304
|
+
const adhoc = input.identity === ADHOC_IDENTITY;
|
|
305
|
+
let entitlements;
|
|
306
|
+
if (signer.entitlements.length > 0) {
|
|
307
|
+
mkdirSync(dirname(input.entitlementsPath), { recursive: true });
|
|
308
|
+
writeFileSync(input.entitlementsPath, renderEntitlements(signer.entitlements));
|
|
309
|
+
entitlements = input.entitlementsPath;
|
|
205
310
|
}
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const args = signer.args(input.identity, file);
|
|
311
|
+
const run = async (file, what) => {
|
|
312
|
+
const args = signer.args({ identity: input.identity, file, adhoc, ...(entitlements ? { entitlements } : {}) });
|
|
209
313
|
if (input.verbose)
|
|
210
314
|
input.log(`${signer.tool} ${args.join(' ')}`);
|
|
211
315
|
const result = await spawnToCompletion(signer.tool, args, {
|
|
@@ -215,33 +319,75 @@ export async function signPayload(input) {
|
|
|
215
319
|
notFound: () => new Error(`gjsify ship: \`${signer.tool}\` is not on PATH — ${signer.installHint}.`),
|
|
216
320
|
});
|
|
217
321
|
if (result.code !== 0) {
|
|
218
|
-
throw new Error(`gjsify ship: ${signer.tool} failed on ${
|
|
322
|
+
throw new Error(`gjsify ship: ${signer.tool} failed on ${what} with ${describeExit(result)}. ` +
|
|
219
323
|
'Nothing is packed from a payload whose signature failed: an artifact that is half ' +
|
|
220
324
|
'signed installs and is refused at first launch, which is worse than not shipping.');
|
|
221
325
|
}
|
|
326
|
+
};
|
|
327
|
+
const signable = payload.filter((entry) => classifyBinary(entry.data) === signer.signs);
|
|
328
|
+
if (signable.length === 0 && !signer.sealsBundle) {
|
|
329
|
+
// NOT silent, and not an error either. A `--app gjs` payload really is
|
|
330
|
+
// JavaScript and a launcher, so there is nothing for the loader to
|
|
331
|
+
// validate — but a run that asked for a signature and made none has to
|
|
332
|
+
// say so, or "signed" and "signed nothing" look identical in the log.
|
|
333
|
+
//
|
|
334
|
+
// `&& !sealsBundle` is what keeps the message TRUE rather than what
|
|
335
|
+
// narrows it: a darwin run with no image still seals the bundle, which is
|
|
336
|
+
// a signature, so printing "the artifact carries no signature" there would
|
|
337
|
+
// be the log contradicting the artifact.
|
|
338
|
+
input.log(`nothing in this payload is a ${signer.signs === 'macho' ? 'Mach-O' : 'PE'} image, so ` +
|
|
339
|
+
`${signer.tool} signed 0 file(s). The artifact carries no signature.`);
|
|
340
|
+
return [...payload];
|
|
222
341
|
}
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
342
|
+
for (const entry of signable) {
|
|
343
|
+
await run(join(workDir, entry.path.split('/').join(sep)), entry.path);
|
|
344
|
+
}
|
|
345
|
+
// THE IMAGES FIRST, THE SEAL LAST, and the order is not stylistic: a bundle
|
|
346
|
+
// seal hashes the files it seals, so sealing before the closure is signed
|
|
347
|
+
// would record the digests of the images this run is about to replace and
|
|
348
|
+
// produce a bundle that verifies against a tree it no longer contains.
|
|
349
|
+
let sealPrefix;
|
|
350
|
+
if (signer.sealsBundle) {
|
|
351
|
+
if (input.bundleRoot === '') {
|
|
352
|
+
throw new Error(`gjsify ship: the ${signer.layoutOs} signer seals a bundle and this layout has no root to ` +
|
|
353
|
+
'seal. `Layout.root` is what names the artifact directory; a signer that sealed the whole ' +
|
|
354
|
+
'stage would sign the payload of every other format beside it.');
|
|
355
|
+
}
|
|
356
|
+
sealPrefix = signer.sealAddPrefix(input.bundleRoot);
|
|
357
|
+
await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`);
|
|
358
|
+
}
|
|
359
|
+
// THE FILE SET MAY ONLY GROW WHERE THE SEAL WRITES. Both tools rewrite an
|
|
360
|
+
// image in place, so any other new path means something else ran — and a file
|
|
361
|
+
// that appeared unnoticed would be packed with no mode from the plan, while
|
|
362
|
+
// one that vanished would be packed as absent.
|
|
363
|
+
const changes = partitionSignedFileSet({
|
|
364
|
+
arrived: listFilesRecursive(workDir),
|
|
365
|
+
planned: payload.map((entry) => entry.path),
|
|
366
|
+
...(sealPrefix === undefined ? {} : { sealPrefix }),
|
|
367
|
+
});
|
|
368
|
+
if (changes.unexpected.length > 0 || changes.removed.length > 0) {
|
|
228
369
|
throw new Error(`gjsify ship: ${signer.tool} changed the payload's file set — ` +
|
|
229
|
-
`${
|
|
230
|
-
`${removed.length} removed (${removed.slice(0, 3).join(', ') || '—'}). ` +
|
|
231
|
-
'Signing is a mutation of the files that are already in the payload (ADR 0024 § A4)
|
|
232
|
-
|
|
233
|
-
'vanished would be packed as absent.');
|
|
370
|
+
`${changes.unexpected.length} added (${changes.unexpected.slice(0, 3).join(', ') || '—'}), ` +
|
|
371
|
+
`${changes.removed.length} removed (${changes.removed.slice(0, 3).join(', ') || '—'}). ` +
|
|
372
|
+
'Signing is a mutation of the files that are already in the payload (ADR 0024 § A4), plus ' +
|
|
373
|
+
`exactly the seal's own directory${sealPrefix === undefined ? '' : ` (\`${sealPrefix}\`)`}.`);
|
|
234
374
|
}
|
|
375
|
+
const sealed = changes.sealed;
|
|
235
376
|
input.log(`${signer.tool} signed ${signable.length} of ${payload.length} payload file(s) as ` +
|
|
236
|
-
`${
|
|
377
|
+
`${adhoc ? 'ad-hoc (`-`)' : `\`${input.identity}\``}` +
|
|
378
|
+
`${sealPrefix === undefined ? '' : ` and sealed ${input.bundleRoot} (${sealed.length} seal file(s))`}` +
|
|
379
|
+
` — ${signer.gap}.`);
|
|
237
380
|
// Modes come from the PLAN, exactly as `readStage` insists: the signer
|
|
238
381
|
// rewrites bytes and this re-read must not turn a 0755 launcher into
|
|
239
|
-
// whatever the scratch directory's umask produced.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
382
|
+
// whatever the scratch directory's umask produced. The seal's own files have
|
|
383
|
+
// no plan entry — they did not exist when the plan was made — so they take
|
|
384
|
+
// 0644, which is what `codesign` writes them as
|
|
385
|
+
// (`bundlediskrep.cpp`: `O_WRONLY | O_CREAT | O_TRUNC, 0644`).
|
|
386
|
+
const read = (path) => new Uint8Array(readFileSync(join(workDir, path.split('/').join(sep))));
|
|
387
|
+
return [
|
|
388
|
+
...payload.map((entry) => ({ path: entry.path, mode: entry.mode, data: read(entry.path) })),
|
|
389
|
+
...sealed.map((path) => ({ path, mode: 0o644, data: read(path) })),
|
|
390
|
+
].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
245
391
|
}
|
|
246
392
|
export const NOTARIES = {
|
|
247
393
|
darwin: {
|
|
@@ -313,5 +459,48 @@ export async function notarizeArtifact(input) {
|
|
|
313
459
|
if (result.code !== 0) {
|
|
314
460
|
throw new Error(`gjsify ship: ${notary.tool} notarytool failed with ${describeExit(result)}.`);
|
|
315
461
|
}
|
|
316
|
-
input.log(`${notary.tool} notarytool accepted ${input.artifact}
|
|
462
|
+
input.log(`${notary.tool} notarytool accepted ${input.artifact}.`);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Attach the notarisation ticket to an artifact that can hold one.
|
|
466
|
+
*
|
|
467
|
+
* WHY IT IS A SEPARATE CALL AND NOT THE TAIL OF {@link notarizeArtifact}: not
|
|
468
|
+
* every artifact the notary service ACCEPTS can carry a ticket, and the pair that
|
|
469
|
+
* comes apart is exactly the one this command produces. Apple:
|
|
470
|
+
* *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
471
|
+
* Instead, run stapler against each item that you added to the archive. Then
|
|
472
|
+
* create a new ZIP file containing the stapled items for distribution."*
|
|
473
|
+
* `stapler(1)` gives the accepting side of the same list: *"stapler works only
|
|
474
|
+
* with UDIF disk images, signed \"flat\" installer packages, and certain
|
|
475
|
+
* code-signed executable bundles such as \".app\"."* So the `.dmg` is stapled
|
|
476
|
+
* here and the `.zip` is told, in the artifact log, what a person has to do
|
|
477
|
+
* instead — which is a fact about the CONTAINER and therefore lives on the format
|
|
478
|
+
* (`canCarryTicket`), not in a branch here.
|
|
479
|
+
*
|
|
480
|
+
* ⚠️ UNVERIFIED END TO END, for the same reason `notarizeArtifact` is: stapling
|
|
481
|
+
* needs a real notarisation, which needs an Apple account (ADR 0024 § A15, § A17).
|
|
482
|
+
* The argv is unit-tested and the invocation has never run in this repository.
|
|
483
|
+
*
|
|
484
|
+
* `stapler(1)` also states the constraint that decides the ORDER: *"Stapling does
|
|
485
|
+
* not invalidate the code signature and must be run after an executable or archive
|
|
486
|
+
* has been code-signed and notarized … Code-signing a supported file format
|
|
487
|
+
* invalidates any stapled tickets"* — so nothing may re-sign after this, and
|
|
488
|
+
* nothing in `packOne` does.
|
|
489
|
+
*/
|
|
490
|
+
export async function stapleArtifact(input) {
|
|
491
|
+
const args = ['stapler', 'staple', input.artifact];
|
|
492
|
+
if (input.verbose)
|
|
493
|
+
input.log(`${input.notary.tool} ${args.join(' ')}`);
|
|
494
|
+
const result = await spawnToCompletion(input.notary.tool, args, {
|
|
495
|
+
completion: 'return',
|
|
496
|
+
stdio: 'inherit',
|
|
497
|
+
notFound: () => new Error(`gjsify ship: \`${input.notary.tool}\` is not on PATH — ${input.notary.installHint}.`),
|
|
498
|
+
});
|
|
499
|
+
if (result.code !== 0) {
|
|
500
|
+
throw new Error(`gjsify ship: ${input.notary.tool} stapler failed with ${describeExit(result)}. The artifact is ` +
|
|
501
|
+
'notarised and NOT stapled, which means every launch asks Gatekeeper to check online — and ' +
|
|
502
|
+
'fails closed on a machine with no network. Re-run the finish phase; `stapler` requires ' +
|
|
503
|
+
'internet access and the ticket may not have propagated yet.');
|
|
504
|
+
}
|
|
505
|
+
input.log(`${input.notary.tool} stapler attached the ticket to ${input.artifact}.`);
|
|
317
506
|
}
|
|
@@ -45,7 +45,26 @@ export function writeStage(root, files) {
|
|
|
45
45
|
for (const file of files) {
|
|
46
46
|
const target = join(root, file.path.split('/').join(sep));
|
|
47
47
|
mkdirSync(dirname(target), { recursive: true });
|
|
48
|
-
|
|
48
|
+
// A `switch` with a `never` guard rather than a ternary chain: the third
|
|
49
|
+
// source kind arrived because a LAYOUT started owning a binary, and a
|
|
50
|
+
// fourth would otherwise be written as whatever the last branch happens
|
|
51
|
+
// to be — the same closed-vocabulary hazard `packOne` uses one for.
|
|
52
|
+
let data;
|
|
53
|
+
switch (file.source.kind) {
|
|
54
|
+
case 'text':
|
|
55
|
+
data = new TextEncoder().encode(file.source.text);
|
|
56
|
+
break;
|
|
57
|
+
case 'file':
|
|
58
|
+
data = new Uint8Array(readFileSync(file.source.path));
|
|
59
|
+
break;
|
|
60
|
+
case 'bytes':
|
|
61
|
+
data = file.source.data;
|
|
62
|
+
break;
|
|
63
|
+
default: {
|
|
64
|
+
const unhandled = file.source;
|
|
65
|
+
throw new Error(`gjsify ship: no writer for the "${JSON.stringify(unhandled)}" source kind.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
49
68
|
writeFileSync(target, data);
|
|
50
69
|
chmodSync(target, file.mode);
|
|
51
70
|
}
|
|
@@ -7,13 +7,23 @@ export interface StagedMode {
|
|
|
7
7
|
}
|
|
8
8
|
/** One file in the payload, addressed relative to the install prefix. */
|
|
9
9
|
export interface StagedFile extends StagedMode {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Where the bytes come from.
|
|
12
|
+
*
|
|
13
|
+
* `bytes` is the third kind because a layout can now OWN a binary: the
|
|
14
|
+
* windows row emits a GUI-subsystem launcher (`utils/ship/pe-launcher.ts`)
|
|
15
|
+
* that exists nowhere on disk and is not text. Spelling it as `text` would
|
|
16
|
+
* round-trip it through UTF-8 and rewrite every byte above 0x7f.
|
|
17
|
+
*/
|
|
11
18
|
source: {
|
|
12
19
|
kind: 'text';
|
|
13
20
|
text: string;
|
|
14
21
|
} | {
|
|
15
22
|
kind: 'file';
|
|
16
23
|
path: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'bytes';
|
|
26
|
+
data: Uint8Array;
|
|
17
27
|
};
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
@@ -286,6 +296,12 @@ export interface ShipMimeType {
|
|
|
286
296
|
* `share/locale/<rel>`, and no packer reads the field. So translations
|
|
287
297
|
* cross in the payload, where they belong, and the `abs` path does not
|
|
288
298
|
* cross at all. This list being explicit is what caught that.
|
|
299
|
+
* - `fontFiles` — the same shape one milestone later (ADR 0038). `planStage`
|
|
300
|
+
* copies each face into `share/fonts/<appId>/`, no packer reads the field, and
|
|
301
|
+
* the only pack-time question — "does this payload carry fonts" — is answered
|
|
302
|
+
* by the payload's own paths. There is no scriptlet to key off it either:
|
|
303
|
+
* fontconfig scans the directory and caches lazily per user, measured rather
|
|
304
|
+
* than assumed.
|
|
289
305
|
* - `kind`, `execArgs` — read by the planner and the metadata renderers, all of
|
|
290
306
|
* which have run by the time a stage exists. `appId` was on this line and
|
|
291
307
|
* MOVED: the Flatpak packer needs it at pack time twice over — it is the
|
|
@@ -342,7 +358,13 @@ export interface PackSettings {
|
|
|
342
358
|
typelibPackages: Record<string, Record<DistroFormatId, string>>;
|
|
343
359
|
/**
|
|
344
360
|
* The interpreter the launcher execs, and therefore the one the package
|
|
345
|
-
* depends on.
|
|
361
|
+
* depends on. THIS TARGET's runtime, resolved once by `resolveShipApp` out of
|
|
362
|
+
* `gjsify.ship.app.<os>` falling back to `gjsify.app`; default `'gjs'`.
|
|
363
|
+
*
|
|
364
|
+
* PER TARGET and never the project field, which is #1486's fix and whose
|
|
365
|
+
* measurement `resolveShipApp` carries. That the stage manifest records the
|
|
366
|
+
* RESOLVED value is what keeps `--from-stage` honest: the packing host has no
|
|
367
|
+
* project and needs none.
|
|
346
368
|
*
|
|
347
369
|
* ON `PackSettings`, not on `ShipSettings` alone, because BOTH halves need it
|
|
348
370
|
* and they must not answer it differently: `renderLauncher` writes
|
|
@@ -432,6 +454,16 @@ export interface ShipSettings extends PackSettings {
|
|
|
432
454
|
rel: string;
|
|
433
455
|
abs: string;
|
|
434
456
|
}[];
|
|
457
|
+
/**
|
|
458
|
+
* Absolute paths of the font faces the application ships (`gjsify.ship.fonts`).
|
|
459
|
+
*
|
|
460
|
+
* Read by `planStage` alone, which copies each into `share/fonts/<appId>/` — the
|
|
461
|
+
* one directory fontconfig reaches through the launcher's existing
|
|
462
|
+
* `XDG_DATA_DIRS` (ADR 0038). Nothing in the pack path reads it, which is why it
|
|
463
|
+
* is not on {@link PackSettings}; the fonts cross in the payload, like the
|
|
464
|
+
* catalogues.
|
|
465
|
+
*/
|
|
466
|
+
fontFiles: string[];
|
|
435
467
|
/** `<dest relative to prefix>` → absolute source path. */
|
|
436
468
|
extraFiles: Record<string, string>;
|
|
437
469
|
/** Arguments appended to the launcher's `exec` line. */
|
package/lib/utils/system-gi.d.ts
CHANGED
|
@@ -28,11 +28,25 @@ export declare const PROBED_GI_LIBDIRS: Record<string, readonly string[]>;
|
|
|
28
28
|
/**
|
|
29
29
|
* The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API.
|
|
30
30
|
*
|
|
31
|
-
* Exported so `gi-typelib.ts` shares it rather than keeping a
|
|
32
|
-
* one string this whole layout rule turns on
|
|
33
|
-
* agreement suite
|
|
31
|
+
* Exported so `gi-typelib.ts` shares it rather than keeping a fourth copy of the
|
|
32
|
+
* one string this whole layout rule turns on, and compared across all three copies
|
|
33
|
+
* by the agreement suite rather than trusting three literals to stay equal.
|
|
34
34
|
*/
|
|
35
35
|
export declare const TYPELIB_SUBDIR = "girepository-1.0";
|
|
36
|
+
/**
|
|
37
|
+
* The library directories a typelib directory implies, most specific first.
|
|
38
|
+
*
|
|
39
|
+
* TWO LAYOUTS, and a path alone cannot always say which: GI's INSTALL layout keeps
|
|
40
|
+
* the libraries in the PARENT (`<libdir>/girepository-1.0/…`), while a STAGED pair —
|
|
41
|
+
* an ADR 0017 prebuild directory, `gjsify.ship.bundledTypelibs` — keeps them in the
|
|
42
|
+
* DIRECTORY ITSELF. {@link TYPELIB_SUBDIR} as the basename is a positive signal for
|
|
43
|
+
* the first; absent it, both readings are offered and the caller's `existsDir` keeps
|
|
44
|
+
* whichever is real.
|
|
45
|
+
*
|
|
46
|
+
* The measurement, and why the ORDER of the two is the load-bearing half, are in the
|
|
47
|
+
* ORIGINAL, `packages/node-gi/node-gi/system-gi.js`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function giLibraryDirsForTypelibDir(typelibDir: string): string[];
|
|
36
50
|
/** Split an OS search-path variable, dropping empty segments. */
|
|
37
51
|
export declare function splitSearchPath(value: string | undefined, separator?: string): string[];
|
|
38
52
|
/**
|
|
@@ -52,8 +66,9 @@ export declare function pkgConfigSearchDirs(env: Record<string, string | undefin
|
|
|
52
66
|
*
|
|
53
67
|
* Three sources, in precedence order:
|
|
54
68
|
* 1. `GI_TYPELIB_PATH` — the host stating outright where typelibs live; the
|
|
55
|
-
* libraries are
|
|
56
|
-
* alone: an explicit statement is
|
|
69
|
+
* libraries are wherever {@link giLibraryDirsForTypelibDir} says the layout
|
|
70
|
+
* puts them. Accepted on directory existence alone: an explicit statement is
|
|
71
|
+
* not second-guessed.
|
|
57
72
|
* 2. `pkg-config`'s `.pc` search path → each `<dir>/pkgconfig`'s parent. The
|
|
58
73
|
* general mechanism (any prefix, including a custom one).
|
|
59
74
|
* 3. {@link PROBED_GI_LIBDIRS} — the standard macOS prefixes, for the common
|
package/lib/utils/system-gi.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// dynamic loader can find the ones a typelib names by BARE LEAF
|
|
4
4
|
// (`libgtk-4.1.dylib`, `libgdk_pixbuf-2.0.0.dylib`, …).
|
|
5
5
|
//
|
|
6
|
-
// A PINNED MIRROR of `packages/node-gi/node-gi/system-gi.js
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// A PINNED MIRROR of `packages/node-gi/node-gi/system-gi.js`, and of
|
|
7
|
+
// `@gjsify/utils/core`'s `system-gi-dirs.ts` — all three must agree and
|
|
8
|
+
// `system-gi.spec.ts` asserts it over a table of injected host shapes. Why a mirror
|
|
9
|
+
// and not an import, and what is owed to retire THIS copy (a delegation to
|
|
10
|
+
// `@gjsify/utils`, which the CLI already depends on):
|
|
11
|
+
// `status/open-todos.md` § "systemGiLibraryDirs() lives in three places".
|
|
10
12
|
//
|
|
11
13
|
// WHY THIS EXISTS. A failed `dlopen('libgtk-4.1.dylib')` surfaces one layer up as
|
|
12
14
|
// something that reads like a type-system bug — `TypeError: Gtk.DrawingArea is not
|
|
@@ -32,6 +34,8 @@
|
|
|
32
34
|
// `<libdir>/libgtk-4.1.dylib`. A directory holding `girepository-1.0/` therefore IS
|
|
33
35
|
// a GI library directory, which is what makes this package-manager-agnostic — it
|
|
34
36
|
// lands on Homebrew, MacPorts or a custom prefix without knowing any of them exist.
|
|
37
|
+
// THAT IS ONE LAYOUT, NOT THE LAYOUT — {@link giLibraryDirsForTypelibDir}; a staged
|
|
38
|
+
// pair keeps typelib and backer in ONE directory, and the incident is in the ORIGINAL.
|
|
35
39
|
//
|
|
36
40
|
// PURE FUNCTION of injected host facts (platform / env / fs / pkg-config), like
|
|
37
41
|
// `resolvePrebuildDirName()` and `buildNativeEnv()` beside it, so the darwin branch
|
|
@@ -74,11 +78,28 @@ export const PROBED_GI_LIBDIRS = {
|
|
|
74
78
|
/**
|
|
75
79
|
* The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API.
|
|
76
80
|
*
|
|
77
|
-
* Exported so `gi-typelib.ts` shares it rather than keeping a
|
|
78
|
-
* one string this whole layout rule turns on
|
|
79
|
-
* agreement suite
|
|
81
|
+
* Exported so `gi-typelib.ts` shares it rather than keeping a fourth copy of the
|
|
82
|
+
* one string this whole layout rule turns on, and compared across all three copies
|
|
83
|
+
* by the agreement suite rather than trusting three literals to stay equal.
|
|
80
84
|
*/
|
|
81
85
|
export const TYPELIB_SUBDIR = 'girepository-1.0';
|
|
86
|
+
/**
|
|
87
|
+
* The library directories a typelib directory implies, most specific first.
|
|
88
|
+
*
|
|
89
|
+
* TWO LAYOUTS, and a path alone cannot always say which: GI's INSTALL layout keeps
|
|
90
|
+
* the libraries in the PARENT (`<libdir>/girepository-1.0/…`), while a STAGED pair —
|
|
91
|
+
* an ADR 0017 prebuild directory, `gjsify.ship.bundledTypelibs` — keeps them in the
|
|
92
|
+
* DIRECTORY ITSELF. {@link TYPELIB_SUBDIR} as the basename is a positive signal for
|
|
93
|
+
* the first; absent it, both readings are offered and the caller's `existsDir` keeps
|
|
94
|
+
* whichever is real.
|
|
95
|
+
*
|
|
96
|
+
* The measurement, and why the ORDER of the two is the load-bearing half, are in the
|
|
97
|
+
* ORIGINAL, `packages/node-gi/node-gi/system-gi.js`.
|
|
98
|
+
*/
|
|
99
|
+
export function giLibraryDirsForTypelibDir(typelibDir) {
|
|
100
|
+
const parent = dirname(typelibDir);
|
|
101
|
+
return basename(typelibDir) === TYPELIB_SUBDIR ? [parent] : [typelibDir, parent];
|
|
102
|
+
}
|
|
82
103
|
/** Memoized `pkg-config` answer: the spawn happens at most once per process. */
|
|
83
104
|
let pkgConfigDirsCache = null;
|
|
84
105
|
/** Split an OS search-path variable, dropping empty segments. */
|
|
@@ -124,8 +145,9 @@ export function pkgConfigSearchDirs(env) {
|
|
|
124
145
|
*
|
|
125
146
|
* Three sources, in precedence order:
|
|
126
147
|
* 1. `GI_TYPELIB_PATH` — the host stating outright where typelibs live; the
|
|
127
|
-
* libraries are
|
|
128
|
-
* alone: an explicit statement is
|
|
148
|
+
* libraries are wherever {@link giLibraryDirsForTypelibDir} says the layout
|
|
149
|
+
* puts them. Accepted on directory existence alone: an explicit statement is
|
|
150
|
+
* not second-guessed.
|
|
129
151
|
* 2. `pkg-config`'s `.pc` search path → each `<dir>/pkgconfig`'s parent. The
|
|
130
152
|
* general mechanism (any prefix, including a custom one).
|
|
131
153
|
* 3. {@link PROBED_GI_LIBDIRS} — the standard macOS prefixes, for the common
|
|
@@ -155,11 +177,12 @@ export function systemGiLibraryDirs({ platform = process.platform, env = process
|
|
|
155
177
|
if (dir && dir !== '/' && !out.includes(dir))
|
|
156
178
|
out.push(dir);
|
|
157
179
|
};
|
|
158
|
-
// 1. Explicit: the typelib dir
|
|
180
|
+
// 1. Explicit: the libdirs each typelib dir implies, both layouts.
|
|
159
181
|
for (const typelibDir of splitSearchPath(env['GI_TYPELIB_PATH'])) {
|
|
160
|
-
const libDir
|
|
161
|
-
|
|
162
|
-
|
|
182
|
+
for (const libDir of giLibraryDirsForTypelibDir(typelibDir)) {
|
|
183
|
+
if (existsDir(libDir))
|
|
184
|
+
add(libDir);
|
|
185
|
+
}
|
|
163
186
|
}
|
|
164
187
|
// 2 + 3. Candidate prefixes, believed only on the girepository-1.0 marker.
|
|
165
188
|
const candidates = [];
|