@gjsify/cli 0.64.19 → 0.64.21
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/README.md +3 -1
- package/dist/cli.gjs.mjs +330 -329
- package/lib/commands/flatpak/check.d.ts +3 -0
- package/lib/commands/flatpak/check.js +14 -4
- package/lib/commands/ship.js +22 -1
- package/lib/types/config-data.d.ts +45 -0
- package/lib/utils/ship/app-runtime.d.ts +21 -0
- package/lib/utils/ship/app-runtime.js +1 -1
- package/lib/utils/ship/layout.d.ts +11 -0
- package/lib/utils/ship/layout.js +12 -1
- package/lib/utils/ship/nested-bundles.d.ts +60 -0
- package/lib/utils/ship/nested-bundles.js +140 -0
- package/lib/utils/ship/release-record.d.ts +30 -0
- package/lib/utils/ship/release-record.js +1 -0
- package/lib/utils/ship/settings.d.ts +26 -1
- package/lib/utils/ship/settings.js +148 -0
- package/lib/utils/ship/signing.d.ts +45 -5
- package/lib/utils/ship/signing.js +74 -24
- package/lib/utils/ship/stage-manifest.js +19 -0
- package/lib/utils/ship/types.d.ts +73 -0
- package/package.json +20 -20
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// only) > derived from package.json. Never a yargs `default:`, which is
|
|
11
11
|
// indistinguishable from a value the user typed and would clobber the config.
|
|
12
12
|
import { validateMimeTypes } from './mime.js';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
13
14
|
import { basename, isAbsolute, posix, relative, resolve, sep } from 'node:path';
|
|
14
15
|
import { DEFAULT_GJS_FLOOR, DEFAULT_NODE_FLOOR } from './depends.js';
|
|
15
16
|
import { resolveShipFlatpakSettings } from './flatpak-config.js';
|
|
@@ -227,6 +228,16 @@ export function resolveShipSettings(input) {
|
|
|
227
228
|
projectDir: input.projectDir,
|
|
228
229
|
bundleDir: discovered.bundleDir,
|
|
229
230
|
}),
|
|
231
|
+
nestedBundles: resolveShipNestedBundles({
|
|
232
|
+
declared: ship.nestedBundles,
|
|
233
|
+
projectDir: input.projectDir,
|
|
234
|
+
layoutOs: input.cli.layoutOs,
|
|
235
|
+
}),
|
|
236
|
+
helperExecutables: resolveShipHelperExecutables({
|
|
237
|
+
declared: ship.helperExecutables,
|
|
238
|
+
projectDir: input.projectDir,
|
|
239
|
+
layoutOs: input.cli.layoutOs,
|
|
240
|
+
}),
|
|
230
241
|
outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
|
|
231
242
|
arch: input.cli.arch ?? process.arch,
|
|
232
243
|
app,
|
|
@@ -292,6 +303,143 @@ export function resolveShipExecutables(input) {
|
|
|
292
303
|
return { name, entry: rel.split(sep).join(posix.sep), gui: gui === true };
|
|
293
304
|
});
|
|
294
305
|
}
|
|
306
|
+
/** A `Contents/PlugIns` or `Contents/Helpers` leaf: no path separator, no NUL, not empty. */
|
|
307
|
+
function assertUsableBundleLeaf(name, at, under) {
|
|
308
|
+
if (name === '' ||
|
|
309
|
+
name === '.' ||
|
|
310
|
+
name === '..' ||
|
|
311
|
+
name.includes('/') ||
|
|
312
|
+
name.includes('\\') ||
|
|
313
|
+
name.includes('\u0000')) {
|
|
314
|
+
throw new Error(`gjsify ship: \`${at}.name\` "${name}" is not usable as a leaf under \`${under}\` — empty and ` +
|
|
315
|
+
'".", "..", and anything containing "/", "\\" or a NUL byte are refused.');
|
|
316
|
+
}
|
|
317
|
+
return name;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* `gjsify.ship.nestedBundles` → the further bundles the darwin container embeds
|
|
321
|
+
* under `Contents/PlugIns/` — e.g. a Safari Web Extension `.appex`.
|
|
322
|
+
*
|
|
323
|
+
* Darwin-only by construction: `layoutOs !== 'darwin'` refuses a non-empty list
|
|
324
|
+
* rather than silently dropping it, because `Contents/PlugIns` has no
|
|
325
|
+
* equivalent on the other two layouts (ADR 0024 amendment, "nested bundles and
|
|
326
|
+
* helper executables"; the beifahrer ADR this answers: 0026 § 6).
|
|
327
|
+
*/
|
|
328
|
+
export function resolveShipNestedBundles(input) {
|
|
329
|
+
const { declared } = input;
|
|
330
|
+
if (declared === undefined)
|
|
331
|
+
return [];
|
|
332
|
+
if (!Array.isArray(declared)) {
|
|
333
|
+
throw new Error('gjsify ship: `gjsify.ship.nestedBundles` must be a list of `{ "source", "name", "entitlements"? }` ' +
|
|
334
|
+
'objects.');
|
|
335
|
+
}
|
|
336
|
+
if (declared.length > 0 && input.layoutOs !== 'darwin') {
|
|
337
|
+
throw new Error(`gjsify ship: \`gjsify.ship.nestedBundles\` is set and this run assembles the ${input.layoutOs} ` +
|
|
338
|
+
'layout, which has no `Contents/PlugIns` or equivalent — a Flatpak sandbox has no App ' +
|
|
339
|
+
"Extension model and a Windows program directory has nothing Apple's extension points name. " +
|
|
340
|
+
'Nested bundles are staged only for `gjsify ship darwin`.');
|
|
341
|
+
}
|
|
342
|
+
const seen = new Set();
|
|
343
|
+
return declared.map((row, index) => {
|
|
344
|
+
const at = `gjsify.ship.nestedBundles[${index}]`;
|
|
345
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
346
|
+
throw new Error(`gjsify ship: \`${at}\` must be an object with \`source\` and \`name\`.`);
|
|
347
|
+
}
|
|
348
|
+
const { source, name, entitlements, ...unknown } = row;
|
|
349
|
+
const stray = Object.keys(unknown);
|
|
350
|
+
if (stray.length > 0) {
|
|
351
|
+
throw new Error(`gjsify ship: \`${at}\` has unknown key(s) ${stray.join(', ')}; known: source, name, entitlements.`);
|
|
352
|
+
}
|
|
353
|
+
if (typeof source !== 'string' || source === '') {
|
|
354
|
+
throw new Error(`gjsify ship: \`${at}.source\` must be the project-relative directory of a built bundle.`);
|
|
355
|
+
}
|
|
356
|
+
if (typeof name !== 'string') {
|
|
357
|
+
throw new Error(`gjsify ship: \`${at}.name\` must be a string — the leaf under \`Contents/PlugIns/\`.`);
|
|
358
|
+
}
|
|
359
|
+
assertUsableBundleLeaf(name, at, 'Contents/PlugIns');
|
|
360
|
+
if (seen.has(name)) {
|
|
361
|
+
throw new Error(`gjsify ship: \`${at}.name\` "${name}" is already taken by an earlier nested bundle.`);
|
|
362
|
+
}
|
|
363
|
+
seen.add(name);
|
|
364
|
+
const abs = isAbsolute(source) ? source : resolve(input.projectDir, source);
|
|
365
|
+
if (!existsSync(abs)) {
|
|
366
|
+
throw new Error(`gjsify ship: \`${at}.source\` ${source} does not exist.`);
|
|
367
|
+
}
|
|
368
|
+
if (entitlements !== undefined && !Array.isArray(entitlements)) {
|
|
369
|
+
throw new Error(`gjsify ship: \`${at}.entitlements\` must be a list of strings.`);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
source: abs,
|
|
373
|
+
name,
|
|
374
|
+
entitlements: (entitlements ?? []).map((key, keyIndex) => {
|
|
375
|
+
if (typeof key !== 'string') {
|
|
376
|
+
throw new Error(`gjsify ship: \`${at}.entitlements[${keyIndex}]\` must be a string.`);
|
|
377
|
+
}
|
|
378
|
+
return key;
|
|
379
|
+
}),
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* `gjsify.ship.helperExecutables` → prebuilt executables staged into
|
|
385
|
+
* `Contents/Helpers/` beside the carried interpreter — e.g. a small Swift
|
|
386
|
+
* helper with no GI binding. Darwin-only, for the same reason as
|
|
387
|
+
* {@link resolveShipNestedBundles}.
|
|
388
|
+
*/
|
|
389
|
+
export function resolveShipHelperExecutables(input) {
|
|
390
|
+
const { declared } = input;
|
|
391
|
+
if (declared === undefined)
|
|
392
|
+
return [];
|
|
393
|
+
if (!Array.isArray(declared)) {
|
|
394
|
+
throw new Error('gjsify ship: `gjsify.ship.helperExecutables` must be a list of `{ "source", "name", ' +
|
|
395
|
+
'"entitlements"? }` objects.');
|
|
396
|
+
}
|
|
397
|
+
if (declared.length > 0 && input.layoutOs !== 'darwin') {
|
|
398
|
+
throw new Error(`gjsify ship: \`gjsify.ship.helperExecutables\` is set and this run assembles the ` +
|
|
399
|
+
`${input.layoutOs} layout, which has no \`Contents/Helpers\` or equivalent. Helper ` +
|
|
400
|
+
'executables are staged only for `gjsify ship darwin`.');
|
|
401
|
+
}
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
return declared.map((row, index) => {
|
|
404
|
+
const at = `gjsify.ship.helperExecutables[${index}]`;
|
|
405
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
406
|
+
throw new Error(`gjsify ship: \`${at}\` must be an object with \`source\` and \`name\`.`);
|
|
407
|
+
}
|
|
408
|
+
const { source, name, entitlements, ...unknown } = row;
|
|
409
|
+
const stray = Object.keys(unknown);
|
|
410
|
+
if (stray.length > 0) {
|
|
411
|
+
throw new Error(`gjsify ship: \`${at}\` has unknown key(s) ${stray.join(', ')}; known: source, name, entitlements.`);
|
|
412
|
+
}
|
|
413
|
+
if (typeof source !== 'string' || source === '') {
|
|
414
|
+
throw new Error(`gjsify ship: \`${at}.source\` must be the project-relative path of a prebuilt executable.`);
|
|
415
|
+
}
|
|
416
|
+
if (typeof name !== 'string') {
|
|
417
|
+
throw new Error(`gjsify ship: \`${at}.name\` must be a string — the leaf under \`Contents/Helpers/\`.`);
|
|
418
|
+
}
|
|
419
|
+
assertUsableBundleLeaf(name, at, 'Contents/Helpers');
|
|
420
|
+
if (seen.has(name)) {
|
|
421
|
+
throw new Error(`gjsify ship: \`${at}.name\` "${name}" is already taken by an earlier helper executable.`);
|
|
422
|
+
}
|
|
423
|
+
seen.add(name);
|
|
424
|
+
const abs = isAbsolute(source) ? source : resolve(input.projectDir, source);
|
|
425
|
+
if (!existsSync(abs)) {
|
|
426
|
+
throw new Error(`gjsify ship: \`${at}.source\` ${source} does not exist.`);
|
|
427
|
+
}
|
|
428
|
+
if (entitlements !== undefined && !Array.isArray(entitlements)) {
|
|
429
|
+
throw new Error(`gjsify ship: \`${at}.entitlements\` must be a list of strings.`);
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
source: abs,
|
|
433
|
+
name,
|
|
434
|
+
entitlements: (entitlements ?? []).map((key, keyIndex) => {
|
|
435
|
+
if (typeof key !== 'string') {
|
|
436
|
+
throw new Error(`gjsify ship: \`${at}.entitlements[${keyIndex}]\` must be a string.`);
|
|
437
|
+
}
|
|
438
|
+
return key;
|
|
439
|
+
}),
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
}
|
|
295
443
|
/**
|
|
296
444
|
* The ONE place a ship target's ENTRY is decided — {@link resolveShipApp}'s twin.
|
|
297
445
|
*
|
|
@@ -156,17 +156,25 @@ export declare function assertHostCanSign(signer: SignerDescriptor): void;
|
|
|
156
156
|
* host. Everything else about a bundle seal needs `codesign`, which this
|
|
157
157
|
* repository has on exactly one CI leg; this function needs nothing.
|
|
158
158
|
*
|
|
159
|
-
* `
|
|
160
|
-
* is unexpected — which is what the file did before ADR 0040
|
|
161
|
-
* right answer for `signtool`.
|
|
159
|
+
* `sealPrefixes` empty or absent means "this signer seals nothing", and then
|
|
160
|
+
* EVERY addition is unexpected — which is what the file did before ADR 0040
|
|
161
|
+
* and is still the right answer for `signtool`.
|
|
162
162
|
*/
|
|
163
163
|
export declare function partitionSignedFileSet(input: {
|
|
164
164
|
/** Paths found under the scratch directory after the tool ran. */
|
|
165
165
|
arrived: readonly string[];
|
|
166
166
|
/** Paths the payload had before it. */
|
|
167
167
|
planned: readonly string[];
|
|
168
|
-
/**
|
|
169
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Directory prefixes a seal is allowed to write into — one per bundle sealed
|
|
170
|
+
* this run. A plain array rather than a single optional string, because a
|
|
171
|
+
* darwin run with nested bundles (ADR 0024 amendment, "nested bundles and
|
|
172
|
+
* helper executables") seals MORE than one directory: the outer `.app` and
|
|
173
|
+
* every embedded `.appex`, each writing its own `_CodeSignature/`. `[]` and
|
|
174
|
+
* absent mean the same thing — "no seal this run" — so a caller with
|
|
175
|
+
* nothing to add may still pass `[]` rather than reaching for `undefined`.
|
|
176
|
+
*/
|
|
177
|
+
sealPrefixes?: readonly string[];
|
|
170
178
|
}): {
|
|
171
179
|
sealed: string[];
|
|
172
180
|
unexpected: string[];
|
|
@@ -195,6 +203,20 @@ export interface SignArgs {
|
|
|
195
203
|
}
|
|
196
204
|
/** `codesign --sign -`: the one identity value that names no identity at all. */
|
|
197
205
|
export declare const ADHOC_IDENTITY = "-";
|
|
206
|
+
/**
|
|
207
|
+
* One nested bundle or helper executable the darwin signer signs with its OWN
|
|
208
|
+
* entitlements, before the outer seal (ADR 0024 amendment, "nested bundles and
|
|
209
|
+
* helper executables"; the beifahrer ADR this answers: 0026 § 6).
|
|
210
|
+
*/
|
|
211
|
+
export interface SignedSubordinate {
|
|
212
|
+
/** Payload-relative path — a bundle DIRECTORY for a nested bundle, a FILE for a helper. */
|
|
213
|
+
path: string;
|
|
214
|
+
/**
|
|
215
|
+
* Entitlements this subordinate is signed with, REPLACING the outer signer's
|
|
216
|
+
* own set — never unioned with it. `[]` signs with none.
|
|
217
|
+
*/
|
|
218
|
+
entitlements: readonly string[];
|
|
219
|
+
}
|
|
198
220
|
export interface SignPayloadInput {
|
|
199
221
|
/** What `readStage` returned — the PRE-sign tree, already held against the manifest. */
|
|
200
222
|
payload: readonly PayloadEntry[];
|
|
@@ -218,6 +240,24 @@ export interface SignPayloadInput {
|
|
|
218
240
|
* read as a file the signer added.
|
|
219
241
|
*/
|
|
220
242
|
entitlementsPath: string;
|
|
243
|
+
/**
|
|
244
|
+
* Nested BUNDLES this run embeds — e.g. a Safari Web Extension `.appex` under
|
|
245
|
+
* `Contents/PlugIns/`. Signed INSIDE OUT: every Mach-O under each one's path,
|
|
246
|
+
* then that bundle's own seal, ALL of it before the outer bundle's images or
|
|
247
|
+
* seal — because the outer seal's `CodeResources` hashes every file under its
|
|
248
|
+
* tree, nested bundles included, so resigning one afterwards would invalidate
|
|
249
|
+
* a hash the outer seal already recorded. `[]` for a layout or project with
|
|
250
|
+
* none. `sealsBundle` must be `true` on {@link signer} for a non-empty list —
|
|
251
|
+
* `signtool` has no bundle concept to nest one inside.
|
|
252
|
+
*/
|
|
253
|
+
nestedBundles?: readonly SignedSubordinate[];
|
|
254
|
+
/**
|
|
255
|
+
* Helper EXECUTABLES this run embeds with their own entitlements — e.g. a
|
|
256
|
+
* small Swift binary with no GI binding. Signed before the outer images and
|
|
257
|
+
* seal, for the same reason as {@link nestedBundles}: a loose file's bytes
|
|
258
|
+
* are hashed into the outer seal too. `[]` for none.
|
|
259
|
+
*/
|
|
260
|
+
helperExecutables?: readonly SignedSubordinate[];
|
|
221
261
|
/** Print every invocation. */
|
|
222
262
|
verbose: boolean;
|
|
223
263
|
log: (line: string) => void;
|
|
@@ -341,18 +341,19 @@ export function assertHostCanSign(signer) {
|
|
|
341
341
|
* host. Everything else about a bundle seal needs `codesign`, which this
|
|
342
342
|
* repository has on exactly one CI leg; this function needs nothing.
|
|
343
343
|
*
|
|
344
|
-
* `
|
|
345
|
-
* is unexpected — which is what the file did before ADR 0040
|
|
346
|
-
* right answer for `signtool`.
|
|
344
|
+
* `sealPrefixes` empty or absent means "this signer seals nothing", and then
|
|
345
|
+
* EVERY addition is unexpected — which is what the file did before ADR 0040
|
|
346
|
+
* and is still the right answer for `signtool`.
|
|
347
347
|
*/
|
|
348
348
|
export function partitionSignedFileSet(input) {
|
|
349
349
|
const planned = new Set(input.planned);
|
|
350
350
|
const arrived = new Set(input.arrived);
|
|
351
351
|
const added = [...arrived].filter((path) => !planned.has(path)).sort();
|
|
352
|
-
const
|
|
352
|
+
const prefixes = input.sealPrefixes ?? [];
|
|
353
|
+
const underAnyPrefix = (path) => prefixes.some((prefix) => path.startsWith(prefix));
|
|
353
354
|
return {
|
|
354
|
-
sealed:
|
|
355
|
-
unexpected:
|
|
355
|
+
sealed: prefixes.length === 0 ? [] : added.filter(underAnyPrefix),
|
|
356
|
+
unexpected: prefixes.length === 0 ? added : added.filter((path) => !underAnyPrefix(path)),
|
|
356
357
|
removed: [...planned].filter((path) => !arrived.has(path)).sort(),
|
|
357
358
|
};
|
|
358
359
|
}
|
|
@@ -381,16 +382,30 @@ export const ADHOC_IDENTITY = '-';
|
|
|
381
382
|
*/
|
|
382
383
|
export async function signPayload(input) {
|
|
383
384
|
const { payload, signer, workDir } = input;
|
|
385
|
+
const nestedBundles = input.nestedBundles ?? [];
|
|
386
|
+
const helperExecutables = input.helperExecutables ?? [];
|
|
387
|
+
if ((nestedBundles.length > 0 || helperExecutables.length > 0) && !signer.sealsBundle) {
|
|
388
|
+
throw new Error(`gjsify ship: this payload declares a nested bundle or helper executable, and the ` +
|
|
389
|
+
`${signer.layoutOs} signer has no bundle to embed one in — \`signtool\` signs loose images, ` +
|
|
390
|
+
'never a bundle tree. Nested bundles and helper executables are a darwin-only feature.');
|
|
391
|
+
}
|
|
384
392
|
writePayload(workDir, payload, '');
|
|
385
393
|
const adhoc = input.identity === ADHOC_IDENTITY;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
const entitlementsCache = new Map();
|
|
395
|
+
/** Write (or reuse) the entitlements plist for one KEYED set of grants, `[]` meaning none. */
|
|
396
|
+
const entitlementsFileFor = (key, granted) => {
|
|
397
|
+
if (granted.length === 0)
|
|
398
|
+
return undefined;
|
|
399
|
+
const cached = entitlementsCache.get(key);
|
|
400
|
+
if (cached !== undefined)
|
|
401
|
+
return cached;
|
|
402
|
+
const path = key === '' ? input.entitlementsPath : `${input.entitlementsPath}.${key}.plist`;
|
|
403
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
404
|
+
writeFileSync(path, renderEntitlements(granted));
|
|
405
|
+
entitlementsCache.set(key, path);
|
|
406
|
+
return path;
|
|
407
|
+
};
|
|
408
|
+
const run = async (file, what, seal, entitlements) => {
|
|
394
409
|
const args = signer.args({
|
|
395
410
|
identity: input.identity,
|
|
396
411
|
file,
|
|
@@ -412,7 +427,38 @@ export async function signPayload(input) {
|
|
|
412
427
|
'signed installs and is refused at first launch, which is worse than not shipping.');
|
|
413
428
|
}
|
|
414
429
|
};
|
|
415
|
-
const
|
|
430
|
+
const outerEntitlements = entitlementsFileFor('', signer.entitlements(adhoc));
|
|
431
|
+
// Every path this run signs with its OWN entitlements rather than the outer
|
|
432
|
+
// signer's default — excluded from the generic loop below so neither set of
|
|
433
|
+
// files is signed twice with two different entitlement lists.
|
|
434
|
+
const subordinatePrefixes = nestedBundles.map((bundle) => `${bundle.path}/`);
|
|
435
|
+
const helperPaths = new Set(helperExecutables.map((helper) => helper.path));
|
|
436
|
+
const underSubordinate = (path) => subordinatePrefixes.some((prefix) => path.startsWith(prefix));
|
|
437
|
+
// INSIDE OUT, and before anything else: the outer seal's `CodeResources`
|
|
438
|
+
// hashes every file under its tree, nested bundles and helpers included, so
|
|
439
|
+
// each one has to be in its FINAL signed form before that hash is taken —
|
|
440
|
+
// resigning afterwards would invalidate a digest the outer seal already
|
|
441
|
+
// recorded. Order WITHIN this block does not matter; order relative to the
|
|
442
|
+
// generic loop and the outer seal below does.
|
|
443
|
+
const nestedSealPrefixes = [];
|
|
444
|
+
let nestedImagesSigned = 0;
|
|
445
|
+
for (const bundle of nestedBundles) {
|
|
446
|
+
const entitlements = entitlementsFileFor(bundle.path, bundle.entitlements);
|
|
447
|
+
const images = payload.filter((entry) => entry.path.startsWith(`${bundle.path}/`) && classifyBinary(entry.data) === signer.signs);
|
|
448
|
+
for (const entry of images) {
|
|
449
|
+
await run(join(workDir, entry.path.split('/').join(sep)), entry.path, false, entitlements);
|
|
450
|
+
}
|
|
451
|
+
nestedImagesSigned += images.length;
|
|
452
|
+
await run(join(workDir, bundle.path.split('/').join(sep)), `${bundle.path} (nested bundle seal)`, true, entitlements);
|
|
453
|
+
nestedSealPrefixes.push(signer.sealAddPrefix(bundle.path));
|
|
454
|
+
}
|
|
455
|
+
for (const helper of helperExecutables) {
|
|
456
|
+
const entitlements = entitlementsFileFor(helper.path, helper.entitlements);
|
|
457
|
+
await run(join(workDir, helper.path.split('/').join(sep)), helper.path, false, entitlements);
|
|
458
|
+
}
|
|
459
|
+
const signable = payload.filter((entry) => classifyBinary(entry.data) === signer.signs &&
|
|
460
|
+
!underSubordinate(entry.path) &&
|
|
461
|
+
!helperPaths.has(entry.path));
|
|
416
462
|
if (signable.length === 0 && !signer.sealsBundle) {
|
|
417
463
|
// NOT silent, and not an error either. A `--app gjs` payload really is
|
|
418
464
|
// JavaScript and a launcher, so there is nothing for the loader to
|
|
@@ -428,7 +474,7 @@ export async function signPayload(input) {
|
|
|
428
474
|
return [...payload];
|
|
429
475
|
}
|
|
430
476
|
for (const entry of signable) {
|
|
431
|
-
await run(join(workDir, entry.path.split('/').join(sep)), entry.path, false);
|
|
477
|
+
await run(join(workDir, entry.path.split('/').join(sep)), entry.path, false, outerEntitlements);
|
|
432
478
|
}
|
|
433
479
|
// THE IMAGES FIRST, THE SEAL LAST, and the order is not stylistic: a bundle
|
|
434
480
|
// seal hashes the files it seals, so sealing before the closure is signed
|
|
@@ -442,28 +488,32 @@ export async function signPayload(input) {
|
|
|
442
488
|
'stage would sign the payload of every other format beside it.');
|
|
443
489
|
}
|
|
444
490
|
sealPrefix = signer.sealAddPrefix(input.bundleRoot);
|
|
445
|
-
await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`, true);
|
|
491
|
+
await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`, true, outerEntitlements);
|
|
446
492
|
}
|
|
447
|
-
// THE FILE SET MAY ONLY GROW WHERE
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
493
|
+
// THE FILE SET MAY ONLY GROW WHERE A SEAL WRITES — the outer one, or one of
|
|
494
|
+
// the nested bundles signed above. Both tools rewrite an image in place, so
|
|
495
|
+
// any other new path means something else ran — and a file that appeared
|
|
496
|
+
// unnoticed would be packed with no mode from the plan, while one that
|
|
497
|
+
// vanished would be packed as absent.
|
|
498
|
+
const sealPrefixes = [...(sealPrefix === undefined ? [] : [sealPrefix]), ...nestedSealPrefixes];
|
|
451
499
|
const changes = partitionSignedFileSet({
|
|
452
500
|
arrived: listFilesRecursive(workDir),
|
|
453
501
|
planned: payload.map((entry) => entry.path),
|
|
454
|
-
|
|
502
|
+
sealPrefixes,
|
|
455
503
|
});
|
|
456
504
|
if (changes.unexpected.length > 0 || changes.removed.length > 0) {
|
|
457
505
|
throw new Error(`gjsify ship: ${signer.tool} changed the payload's file set — ` +
|
|
458
506
|
`${changes.unexpected.length} added (${changes.unexpected.slice(0, 3).join(', ') || '—'}), ` +
|
|
459
507
|
`${changes.removed.length} removed (${changes.removed.slice(0, 3).join(', ') || '—'}). ` +
|
|
460
508
|
'Signing is a mutation of the files that are already in the payload (ADR 0024 § A4), plus ' +
|
|
461
|
-
`exactly
|
|
509
|
+
`exactly each seal's own directory${sealPrefixes.length === 0 ? '' : ` (${sealPrefixes.map((prefix) => `\`${prefix}\``).join(', ')})`}.`);
|
|
462
510
|
}
|
|
463
511
|
const sealed = changes.sealed;
|
|
464
|
-
input.log(`${signer.tool} signed ${signable.length
|
|
512
|
+
input.log(`${signer.tool} signed ${signable.length + nestedImagesSigned + helperExecutables.length} of ` +
|
|
513
|
+
`${payload.length} payload file(s) as ` +
|
|
465
514
|
`${adhoc ? 'ad-hoc (`-`)' : `\`${input.identity}\``}` +
|
|
466
515
|
`${sealPrefix === undefined ? '' : ` and sealed ${input.bundleRoot} (${sealed.length} seal file(s))`}` +
|
|
516
|
+
`${nestedBundles.length === 0 ? '' : `, nested: ${nestedBundles.map((bundle) => bundle.path).join(', ')}`}` +
|
|
467
517
|
` — ${signer.gap}.`);
|
|
468
518
|
// Modes come from the PLAN, exactly as `readStage` insists: the signer
|
|
469
519
|
// rewrites bytes and this re-read must not turn a 0755 launcher into
|
|
@@ -141,6 +141,8 @@ export function toPackSettings(settings) {
|
|
|
141
141
|
minNodeVersion: settings.minNodeVersion,
|
|
142
142
|
flatpak: settings.flatpak,
|
|
143
143
|
executables: settings.executables.map(({ name, gui }) => ({ name, gui })),
|
|
144
|
+
nestedBundles: settings.nestedBundles.map(({ name, entitlements }) => ({ name, entitlements })),
|
|
145
|
+
helperExecutables: settings.helperExecutables.map(({ name, entitlements }) => ({ name, entitlements })),
|
|
144
146
|
};
|
|
145
147
|
}
|
|
146
148
|
/** Absolute path of a stage's manifest. */
|
|
@@ -512,6 +514,23 @@ function readPackSettings(data, at) {
|
|
|
512
514
|
}
|
|
513
515
|
return { name: expectString(row.name, field(`executables[${index}].name`)), gui: row.gui };
|
|
514
516
|
}),
|
|
517
|
+
// OPTIONAL on read, same reason as `executables`: a stage written before
|
|
518
|
+
// `gjsify.ship.nestedBundles`/`helperExecutables` existed carries neither,
|
|
519
|
+
// and `[]` is exactly right for it.
|
|
520
|
+
nestedBundles: expectArray(data.nestedBundles ?? [], field('nestedBundles')).map((entry, index) => {
|
|
521
|
+
const row = record(entry, field(`nestedBundles[${index}]`));
|
|
522
|
+
return {
|
|
523
|
+
name: expectString(row.name, field(`nestedBundles[${index}].name`)),
|
|
524
|
+
entitlements: expectArray(row.entitlements, field(`nestedBundles[${index}].entitlements`)).map((key, keyIndex) => expectString(key, field(`nestedBundles[${index}].entitlements[${keyIndex}]`))),
|
|
525
|
+
};
|
|
526
|
+
}),
|
|
527
|
+
helperExecutables: expectArray(data.helperExecutables ?? [], field('helperExecutables')).map((entry, index) => {
|
|
528
|
+
const row = record(entry, field(`helperExecutables[${index}]`));
|
|
529
|
+
return {
|
|
530
|
+
name: expectString(row.name, field(`helperExecutables[${index}].name`)),
|
|
531
|
+
entitlements: expectArray(row.entitlements, field(`helperExecutables[${index}].entitlements`)).map((key, keyIndex) => expectString(key, field(`helperExecutables[${index}].entitlements[${keyIndex}]`))),
|
|
532
|
+
};
|
|
533
|
+
}),
|
|
515
534
|
};
|
|
516
535
|
}
|
|
517
536
|
/**
|
|
@@ -414,6 +414,19 @@ export interface PackSettings {
|
|
|
414
414
|
* launcher, and the packing host needs the names for the Windows arch exemption.
|
|
415
415
|
*/
|
|
416
416
|
executables: ShipExecutableFacts[];
|
|
417
|
+
/**
|
|
418
|
+
* Further bundles the darwin container embeds (`gjsify.ship.nestedBundles`); `[]` for a
|
|
419
|
+
* package with none. Name and entitlements only — the packing host derives each one's
|
|
420
|
+
* stage-relative path from `layoutForOs('darwin').dirs(settings).plugins` and signs it
|
|
421
|
+
* there; the SOURCE directory stays assembling-host-only (it is staged, not re-read).
|
|
422
|
+
*/
|
|
423
|
+
nestedBundles: ShipNestedBundleFacts[];
|
|
424
|
+
/**
|
|
425
|
+
* Prebuilt helper executables staged into `Contents/Helpers/`
|
|
426
|
+
* (`gjsify.ship.helperExecutables`); `[]` for a package with none. Same shape as
|
|
427
|
+
* {@link PackSettings.nestedBundles}, one directory over (`dirs.helpers`).
|
|
428
|
+
*/
|
|
429
|
+
helperExecutables: ShipHelperExecutableFacts[];
|
|
417
430
|
}
|
|
418
431
|
/** What the packing host needs to know about one further executable. */
|
|
419
432
|
export interface ShipExecutableFacts {
|
|
@@ -427,6 +440,53 @@ export interface ShipExecutable extends ShipExecutableFacts {
|
|
|
427
440
|
/** Its entry inside {@link ShipSettings.bundleDir}, POSIX-separated. */
|
|
428
441
|
entry: string;
|
|
429
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* What the packing host needs to know about one nested bundle, to sign it.
|
|
445
|
+
*
|
|
446
|
+
* `name` rather than a stage-relative path, mirroring {@link ShipExecutableFacts}:
|
|
447
|
+
* the PATH is derived — `posix.join(layoutForOs(format.layoutOs).dirs(settings).plugins,
|
|
448
|
+
* name)` — from data every packing host already has (the layout, `PackSettings`),
|
|
449
|
+
* so this closure carries nothing a stage-relative string would duplicate.
|
|
450
|
+
*/
|
|
451
|
+
export interface ShipNestedBundleFacts {
|
|
452
|
+
/** Destination leaf under `Contents/PlugIns/`. */
|
|
453
|
+
name: string;
|
|
454
|
+
/**
|
|
455
|
+
* Entitlements this bundle is signed and sealed with, REPLACING the outer
|
|
456
|
+
* signer's own set — a sandboxed extension keeps the sandbox entitlements
|
|
457
|
+
* its own packager wrote and must not inherit the host app's grants.
|
|
458
|
+
*/
|
|
459
|
+
entitlements: readonly string[];
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* One nested bundle a darwin container embeds, as resolved from
|
|
463
|
+
* `gjsify.ship.nestedBundles` — e.g. a Safari Web Extension `.appex`.
|
|
464
|
+
*
|
|
465
|
+
* Darwin-only (ADR 0024 § A12-§ A17 amendment, "nested bundles and helper
|
|
466
|
+
* executables"; the beifahrer ADR this answers: 0026 § 6): `Contents/PlugIns`
|
|
467
|
+
* is Apple's own nested-code location and no other layout has an equivalent
|
|
468
|
+
* slot, so `resolveShipSettings` refuses a non-empty list for any other OS.
|
|
469
|
+
*/
|
|
470
|
+
export interface ShipNestedBundle extends ShipNestedBundleFacts {
|
|
471
|
+
/** Absolute path of the source directory — the built `.appex` tree. */
|
|
472
|
+
source: string;
|
|
473
|
+
}
|
|
474
|
+
/** What the packing host needs to know about one helper executable, to sign it. */
|
|
475
|
+
export interface ShipHelperExecutableFacts {
|
|
476
|
+
/** Destination leaf under `Contents/Helpers/`. */
|
|
477
|
+
name: string;
|
|
478
|
+
/** Entitlements this helper is signed with. */
|
|
479
|
+
entitlements: readonly string[];
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* One prebuilt helper executable staged into `Contents/Helpers/`, as resolved
|
|
483
|
+
* from `gjsify.ship.helperExecutables`. Darwin-only, for the same reason as
|
|
484
|
+
* {@link ShipNestedBundle}.
|
|
485
|
+
*/
|
|
486
|
+
export interface ShipHelperExecutable extends ShipHelperExecutableFacts {
|
|
487
|
+
/** Absolute path of the prebuilt executable. */
|
|
488
|
+
source: string;
|
|
489
|
+
}
|
|
430
490
|
/**
|
|
431
491
|
* What the generated Flatpak manifest says about everything that is not the payload.
|
|
432
492
|
*
|
|
@@ -511,6 +571,19 @@ export interface ShipSettings extends PackSettings {
|
|
|
511
571
|
rel: string;
|
|
512
572
|
abs: string;
|
|
513
573
|
}[];
|
|
574
|
+
/**
|
|
575
|
+
* Further bundles the darwin container embeds under `Contents/PlugIns/`
|
|
576
|
+
* (`gjsify.ship.nestedBundles`) — `[]` for a package with none. Ignored on
|
|
577
|
+
* every other layout; `resolveShipSettings` refuses a non-empty list there
|
|
578
|
+
* rather than silently dropping it.
|
|
579
|
+
*/
|
|
580
|
+
nestedBundles: ShipNestedBundle[];
|
|
581
|
+
/**
|
|
582
|
+
* Prebuilt helper executables staged into `Contents/Helpers/`
|
|
583
|
+
* (`gjsify.ship.helperExecutables`) — `[]` for a package with none.
|
|
584
|
+
* Darwin-only, like {@link ShipSettings.nestedBundles}.
|
|
585
|
+
*/
|
|
586
|
+
helperExecutables: ShipHelperExecutable[];
|
|
514
587
|
/**
|
|
515
588
|
* Absolute paths of the font faces the application ships (`gjsify.ship.fonts`).
|
|
516
589
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.21",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -142,22 +142,22 @@
|
|
|
142
142
|
"cli"
|
|
143
143
|
],
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@gjsify/blueprint": "^0.64.
|
|
146
|
-
"@gjsify/buffer": "^0.64.
|
|
147
|
-
"@gjsify/create-app": "^0.64.
|
|
148
|
-
"@gjsify/node-globals": "^0.64.
|
|
149
|
-
"@gjsify/node-polyfills": "^0.64.
|
|
150
|
-
"@gjsify/npm-registry": "^0.64.
|
|
151
|
-
"@gjsify/resolve-npm": "^0.64.
|
|
152
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.64.
|
|
153
|
-
"@gjsify/vite-plugin-blueprint": "^0.64.
|
|
154
|
-
"@gjsify/rolldown-plugin-pnp": "^0.64.
|
|
155
|
-
"@gjsify/semver": "^0.64.
|
|
156
|
-
"@gjsify/tar": "^0.64.
|
|
157
|
-
"@gjsify/tsc": "^0.64.
|
|
158
|
-
"@gjsify/utils": "^0.64.
|
|
159
|
-
"@gjsify/web-polyfills": "^0.64.
|
|
160
|
-
"@gjsify/workspace": "^0.64.
|
|
145
|
+
"@gjsify/blueprint": "^0.64.21",
|
|
146
|
+
"@gjsify/buffer": "^0.64.21",
|
|
147
|
+
"@gjsify/create-app": "^0.64.21",
|
|
148
|
+
"@gjsify/node-globals": "^0.64.21",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.64.21",
|
|
150
|
+
"@gjsify/npm-registry": "^0.64.21",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.64.21",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.64.21",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.21",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.64.21",
|
|
155
|
+
"@gjsify/semver": "^0.64.21",
|
|
156
|
+
"@gjsify/tar": "^0.64.21",
|
|
157
|
+
"@gjsify/tsc": "^0.64.21",
|
|
158
|
+
"@gjsify/utils": "^0.64.21",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.64.21",
|
|
160
|
+
"@gjsify/workspace": "^0.64.21",
|
|
161
161
|
"acorn": "^8.17.0",
|
|
162
162
|
"cosmiconfig": "^9.0.2",
|
|
163
163
|
"get-tsconfig": "^4.14.0",
|
|
@@ -166,13 +166,13 @@
|
|
|
166
166
|
"yargs": "^18.0.0"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
|
-
"@gjsify/unit": "^0.64.
|
|
169
|
+
"@gjsify/unit": "^0.64.21",
|
|
170
170
|
"@types/yargs": "^17.0.35",
|
|
171
171
|
"typescript": "^6.0.3"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
|
-
"@gjsify/oxfmt-native": "^0.64.
|
|
175
|
-
"@gjsify/rolldown-native": "^0.64.
|
|
174
|
+
"@gjsify/oxfmt-native": "^0.64.21",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.64.21"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
|
178
178
|
"@gjsify/oxfmt-native": {
|