@gjsify/cli 0.64.23 → 0.64.24

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.
@@ -430,6 +430,7 @@ async function assemble(args) {
430
430
  identity: settings,
431
431
  nestedBundles: settings.nestedBundles,
432
432
  helperExecutables: settings.helperExecutables,
433
+ mainExecutable: settings.mainExecutableSource,
433
434
  });
434
435
  const stageInputs = {
435
436
  bundleFiles: discovered.bundleFiles,
@@ -776,6 +776,20 @@ export interface ConfigDataShip extends AppMetadata {
776
776
  * for the same reason as {@link ConfigDataShip.nestedBundles}.
777
777
  */
778
778
  helperExecutables?: ShipHelperExecutableOptions[];
779
+ /**
780
+ * An app-provided prebuilt Mach-O staged as the bundle's MAIN executable
781
+ * (`Contents/MacOS/<binaryName>`, the `CFBundleExecutable`); the generated
782
+ * `/bin/sh` launcher moves to `Contents/Resources/<binaryName>-launcher`. Darwin
783
+ * only — ignored with a stderr notice on other layouts (shape still validated).
784
+ * The Mach-O must `execv("<dir of its own executable>/../Resources/<binaryName>-launcher", argv)`
785
+ * for every argv it does not handle itself (ADR 0024, main-executable amendment).
786
+ */
787
+ mainExecutable?: ShipMainExecutableOptions;
788
+ }
789
+ /** `gjsify.ship.mainExecutable`. */
790
+ export interface ShipMainExecutableOptions {
791
+ /** Package-relative path of the prebuilt Mach-O (thin or universal). */
792
+ source: string;
779
793
  }
780
794
  /** One entry of `gjsify.ship.nestedBundles`. */
781
795
  export interface ShipNestedBundleOptions {
@@ -26,6 +26,12 @@ export interface LayoutIdentity {
26
26
  name: string;
27
27
  gui: boolean;
28
28
  }[];
29
+ /**
30
+ * The darwin `.app` carries an app-provided Mach-O at `Contents/MacOS/<binaryName>`
31
+ * (`gjsify.ship.mainExecutable`), so the generated launcher moves to
32
+ * `Contents/Resources/<binaryName>-launcher`. Ignored by every other layout.
33
+ */
34
+ hasMainExecutable?: boolean;
29
35
  }
30
36
  /**
31
37
  * What a layout needs to render the metadata it OWNS.
@@ -346,6 +352,8 @@ export declare const LAYOUTS: Record<LayoutName, Layout>;
346
352
  export declare const LAYOUT_NAMES: LayoutName[];
347
353
  /** Stage-relative path of the launcher this layout puts in front of the app. */
348
354
  export declare function launcherPath(layout: Layout, identity: LayoutIdentity): string;
355
+ /** Stage-relative path of the app-provided Mach-O main executable, the `CFBundleExecutable`. */
356
+ export declare function mainExecutablePath(layout: Layout, identity: LayoutIdentity): string;
349
357
  /**
350
358
  * Stage-relative path of the windows layout's GUI launcher.
351
359
  *
@@ -563,6 +563,17 @@ export const LAYOUT_NAMES = Object.keys(LAYOUTS);
563
563
  const BY_OS = new Map(LAYOUT_NAMES.map((name) => [LAYOUTS[name].os, LAYOUTS[name]]));
564
564
  /** Stage-relative path of the launcher this layout puts in front of the app. */
565
565
  export function launcherPath(layout, identity) {
566
+ const dirs = layout.dirs(identity);
567
+ // Beside an app-provided Mach-O main the sh launcher is a RESOURCE, not nested code: a script in
568
+ // `Contents/MacOS` that is not the main executable keeps its signature only in an xattr, which our
569
+ // packers drop, while `Contents/Resources` is sealed by hash and needs no signature of its own.
570
+ if (layout.name === 'darwin' && identity.hasMainExecutable === true) {
571
+ return posix.join(dirs.other, `${identity.binaryName}-launcher${layout.launcherExt}`);
572
+ }
573
+ return posix.join(dirs.launcher, `${identity.binaryName}${layout.launcherExt}`);
574
+ }
575
+ /** Stage-relative path of the app-provided Mach-O main executable, the `CFBundleExecutable`. */
576
+ export function mainExecutablePath(layout, identity) {
566
577
  return posix.join(layout.dirs(identity).launcher, `${identity.binaryName}${layout.launcherExt}`);
567
578
  }
568
579
  /**
@@ -715,7 +726,7 @@ export function place(layout, identity, rel) {
715
726
  // below, which has no extension: a windows `<name>` without `.cmd` is not runnable.
716
727
  const further = identity.executables?.find((executable) => rel === `bin/${executable.name}`);
717
728
  if (further !== undefined)
718
- return launcherPath(layout, { ...identity, binaryName: further.name });
729
+ return launcherPath(layout, { ...identity, binaryName: further.name, hasMainExecutable: false });
719
730
  const libPrefix = `lib/${identity.binaryName}/`;
720
731
  const giPrefix = `${libPrefix}gi/`;
721
732
  if (rel.startsWith(giPrefix))
@@ -1,4 +1,4 @@
1
- import type { Layout, LayoutIdentity } from './layout.js';
1
+ import { type Layout, type LayoutIdentity } from './layout.js';
2
2
  import type { PayloadEntry } from './payload.js';
3
3
  import type { RecordedNestedBundle } from './release-record.js';
4
4
  import type { SignedSubordinate } from './signing.js';
@@ -22,6 +22,8 @@ export declare function stageSubordinates(input: {
22
22
  identity: LayoutIdentity;
23
23
  nestedBundles: readonly ShipNestedBundle[];
24
24
  helperExecutables: readonly ShipHelperExecutable[];
25
+ /** Absolute path of the app's own Mach-O main (`gjsify.ship.mainExecutable`), darwin only. */
26
+ mainExecutable?: string;
25
27
  }): StagedSubordinates;
26
28
  /**
27
29
  * What `signPayload` needs to sign the subordinates `stageSubordinates` staged —
@@ -19,6 +19,7 @@
19
19
  import { join, posix } from 'node:path';
20
20
  import { listFilesRecursive } from './discover.js';
21
21
  import { stagedMode } from './app-runtime.js';
22
+ import { mainExecutablePath } from './layout.js';
22
23
  /**
23
24
  * Stage every `gjsify.ship.nestedBundles` entry and `gjsify.ship.helperExecutables`
24
25
  * entry into the layout's `plugins`/`helpers` slots.
@@ -61,6 +62,17 @@ export function stageSubordinates(input) {
61
62
  files.push({ path, mode: 0o755, source: { kind: 'file', path: helper.source } });
62
63
  helperExecutables.push({ path, entitlements: helper.entitlements });
63
64
  }
65
+ if (input.mainExecutable !== undefined) {
66
+ if (input.layout.name !== 'darwin') {
67
+ throw new Error(`gjsify ship: \`gjsify.ship.mainExecutable\` is darwin-only, but the ${input.layout.name} layout was asked ` +
68
+ 'to stage it — report this as a gjsify bug.');
69
+ }
70
+ files.push({
71
+ path: mainExecutablePath(input.layout, input.identity),
72
+ mode: 0o755,
73
+ source: { kind: 'file', path: input.mainExecutable },
74
+ });
75
+ }
64
76
  return { files, nestedBundles, helperExecutables };
65
77
  }
66
78
  /**
@@ -208,6 +208,19 @@ export declare function resolveShipHelperExecutables(input: {
208
208
  projectDir: string;
209
209
  layoutOs: HostOs;
210
210
  }): ShipHelperExecutable[];
211
+ /**
212
+ * `gjsify.ship.mainExecutable` → the app's own prebuilt Mach-O, staged as the
213
+ * `.app`'s `CFBundleExecutable`. Needed because macOS 27's
214
+ * `SFSafariExtensionManager` resolves the host app record only for the process
215
+ * whose executable IS `CFBundleExecutable` (ADR 0024 amendment). Same split as
216
+ * {@link resolveShipHelperExecutables}: shape on every host, existence and the
217
+ * Mach-O check on darwin only; ignored (`undefined`) elsewhere.
218
+ */
219
+ export declare function resolveShipMainExecutable(input: {
220
+ declared: unknown;
221
+ projectDir: string;
222
+ layoutOs: HostOs;
223
+ }): string | undefined;
211
224
  /** The entry one ship target installs, which key named it, and what it must NOT carry. */
212
225
  export interface ResolvedShipBundle {
213
226
  /** The declared path as written — project-relative or absolute. */
@@ -10,8 +10,9 @@
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
+ import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
14
14
  import { basename, isAbsolute, posix, relative, resolve, sep } from 'node:path';
15
+ import { classifyBinary } from './payload.js';
15
16
  import { DEFAULT_GJS_FLOOR, DEFAULT_NODE_FLOOR } from './depends.js';
16
17
  import { resolveShipFlatpakSettings } from './flatpak-config.js';
17
18
  import { assertRelease, normaliseVersion } from './version.js';
@@ -207,10 +208,22 @@ export function resolveShipSettings(input) {
207
208
  projectDir: input.projectDir,
208
209
  layoutOs: input.cli.layoutOs,
209
210
  });
210
- if (input.cli.layoutOs !== 'darwin' &&
211
- ((Array.isArray(ship.nestedBundles) && ship.nestedBundles.length > 0) ||
212
- (Array.isArray(ship.helperExecutables) && ship.helperExecutables.length > 0))) {
213
- warnings.push(`gjsify ship: nestedBundles/helperExecutables apply to darwin only — not staged for ${input.cli.layoutOs}`);
211
+ const mainExecutableResolved = resolveShipMainExecutable({
212
+ declared: ship.mainExecutable,
213
+ projectDir: input.projectDir,
214
+ layoutOs: input.cli.layoutOs,
215
+ });
216
+ if (input.cli.layoutOs !== 'darwin') {
217
+ const ignored = [
218
+ ...(Array.isArray(ship.nestedBundles) && ship.nestedBundles.length > 0 ? ['nestedBundles'] : []),
219
+ ...(Array.isArray(ship.helperExecutables) && ship.helperExecutables.length > 0
220
+ ? ['helperExecutables']
221
+ : []),
222
+ ...(ship.mainExecutable !== undefined ? ['mainExecutable'] : []),
223
+ ];
224
+ if (ignored.length > 0) {
225
+ warnings.push(`gjsify ship: ${ignored.join('/')} apply to darwin only — not staged for ${input.cli.layoutOs}`);
226
+ }
214
227
  }
215
228
  const settings = {
216
229
  projectDir: input.projectDir,
@@ -250,6 +263,9 @@ export function resolveShipSettings(input) {
250
263
  }),
251
264
  nestedBundles: nestedBundlesResolved,
252
265
  helperExecutables: helperExecutablesResolved,
266
+ ...(mainExecutableResolved === undefined
267
+ ? {}
268
+ : { mainExecutableSource: mainExecutableResolved, hasMainExecutable: true }),
253
269
  outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
254
270
  arch: input.cli.arch ?? process.arch,
255
271
  app,
@@ -455,6 +471,50 @@ export function resolveShipHelperExecutables(input) {
455
471
  });
456
472
  return input.layoutOs === 'darwin' ? resolved : [];
457
473
  }
474
+ /**
475
+ * `gjsify.ship.mainExecutable` → the app's own prebuilt Mach-O, staged as the
476
+ * `.app`'s `CFBundleExecutable`. Needed because macOS 27's
477
+ * `SFSafariExtensionManager` resolves the host app record only for the process
478
+ * whose executable IS `CFBundleExecutable` (ADR 0024 amendment). Same split as
479
+ * {@link resolveShipHelperExecutables}: shape on every host, existence and the
480
+ * Mach-O check on darwin only; ignored (`undefined`) elsewhere.
481
+ */
482
+ export function resolveShipMainExecutable(input) {
483
+ const { declared } = input;
484
+ if (declared === undefined)
485
+ return undefined;
486
+ if (typeof declared !== 'object' || declared === null || Array.isArray(declared)) {
487
+ throw new Error('gjsify ship: `gjsify.ship.mainExecutable` must be an object `{ "source" }`.');
488
+ }
489
+ const { source, ...unknown } = declared;
490
+ const stray = Object.keys(unknown);
491
+ if (stray.length > 0) {
492
+ throw new Error(`gjsify ship: \`gjsify.ship.mainExecutable\` has unknown key(s) ${stray.join(', ')}; known: source.`);
493
+ }
494
+ if (typeof source !== 'string' || source === '') {
495
+ throw new Error('gjsify ship: `gjsify.ship.mainExecutable.source` must be the project-relative path of a prebuilt Mach-O executable.');
496
+ }
497
+ if (input.layoutOs !== 'darwin')
498
+ return undefined;
499
+ const abs = isAbsolute(source) ? source : resolve(input.projectDir, source);
500
+ if (!existsSync(abs) || !statSync(abs).isFile()) {
501
+ throw new Error(`gjsify ship: \`gjsify.ship.mainExecutable.source\` ${source} does not exist or is not a file.`);
502
+ }
503
+ const head = Buffer.alloc(4);
504
+ const fd = openSync(abs, 'r');
505
+ try {
506
+ readSync(fd, head, 0, 4, 0);
507
+ }
508
+ finally {
509
+ closeSync(fd);
510
+ }
511
+ if (classifyBinary(head) !== 'macho') {
512
+ throw new Error(`gjsify ship: \`gjsify.ship.mainExecutable.source\` ${source} is not a Mach-O binary (thin or universal). ` +
513
+ 'macOS resolves the host app record only for a native main executable, and the Mac App Store refuses a script; ' +
514
+ 'the generated launcher is staged as `Contents/Resources/<binaryName>-launcher`.');
515
+ }
516
+ return abs;
517
+ }
458
518
  /**
459
519
  * The ONE place a ship target's ENTRY is decided — {@link resolveShipApp}'s twin.
460
520
  *
@@ -143,6 +143,8 @@ export function toPackSettings(settings) {
143
143
  executables: settings.executables.map(({ name, gui }) => ({ name, gui })),
144
144
  nestedBundles: settings.nestedBundles.map(({ name, entitlements }) => ({ name, entitlements })),
145
145
  helperExecutables: settings.helperExecutables.map(({ name, entitlements }) => ({ name, entitlements })),
146
+ // Only when true: a stage without the setting stays byte-identical.
147
+ ...(settings.hasMainExecutable === true ? { hasMainExecutable: true } : {}),
146
148
  };
147
149
  }
148
150
  /** Absolute path of a stage's manifest. */
@@ -531,6 +533,15 @@ function readPackSettings(data, at) {
531
533
  entitlements: expectArray(row.entitlements, field(`helperExecutables[${index}].entitlements`)).map((key, keyIndex) => expectString(key, field(`helperExecutables[${index}].entitlements[${keyIndex}]`))),
532
534
  };
533
535
  }),
536
+ // OPTIONAL on read: absent in every stage written before `gjsify.ship.mainExecutable`.
537
+ ...(data.hasMainExecutable === undefined
538
+ ? {}
539
+ : (() => {
540
+ if (typeof data.hasMainExecutable !== 'boolean') {
541
+ throw new Error(`gjsify ship: ${field('hasMainExecutable')} must be a boolean.`);
542
+ }
543
+ return data.hasMainExecutable ? { hasMainExecutable: true } : {};
544
+ })()),
534
545
  };
535
546
  }
536
547
  /**
@@ -427,6 +427,12 @@ export interface PackSettings {
427
427
  * {@link PackSettings.nestedBundles}, one directory over (`dirs.helpers`).
428
428
  */
429
429
  helperExecutables: ShipHelperExecutableFacts[];
430
+ /**
431
+ * `true` when the darwin `.app` carries an app-provided Mach-O as its main executable
432
+ * (`gjsify.ship.mainExecutable`) and the generated launcher lives at
433
+ * `Contents/Resources/<binaryName>-launcher`. Absent = `false`.
434
+ */
435
+ hasMainExecutable?: boolean;
430
436
  }
431
437
  /** What the packing host needs to know about one further executable. */
432
438
  export interface ShipExecutableFacts {
@@ -584,6 +590,13 @@ export interface ShipSettings extends PackSettings {
584
590
  * Darwin-only, like {@link ShipSettings.nestedBundles}.
585
591
  */
586
592
  helperExecutables: ShipHelperExecutable[];
593
+ /**
594
+ * Absolute path of the app's own prebuilt Mach-O main executable
595
+ * (`gjsify.ship.mainExecutable`), darwin only; absent when none.
596
+ */
597
+ mainExecutableSource?: string;
598
+ /** `true` iff {@link mainExecutableSource} is set. */
599
+ hasMainExecutable?: boolean;
587
600
  /**
588
601
  * Absolute paths of the font faces the application ships (`gjsify.ship.fonts`).
589
602
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.64.23",
3
+ "version": "0.64.24",
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.23",
146
- "@gjsify/buffer": "^0.64.23",
147
- "@gjsify/create-app": "^0.64.23",
148
- "@gjsify/node-globals": "^0.64.23",
149
- "@gjsify/node-polyfills": "^0.64.23",
150
- "@gjsify/npm-registry": "^0.64.23",
151
- "@gjsify/resolve-npm": "^0.64.23",
152
- "@gjsify/rolldown-plugin-gjsify": "^0.64.23",
153
- "@gjsify/vite-plugin-blueprint": "^0.64.23",
154
- "@gjsify/rolldown-plugin-pnp": "^0.64.23",
155
- "@gjsify/semver": "^0.64.23",
156
- "@gjsify/tar": "^0.64.23",
157
- "@gjsify/tsc": "^0.64.23",
158
- "@gjsify/utils": "^0.64.23",
159
- "@gjsify/web-polyfills": "^0.64.23",
160
- "@gjsify/workspace": "^0.64.23",
145
+ "@gjsify/blueprint": "^0.64.24",
146
+ "@gjsify/buffer": "^0.64.24",
147
+ "@gjsify/create-app": "^0.64.24",
148
+ "@gjsify/node-globals": "^0.64.24",
149
+ "@gjsify/node-polyfills": "^0.64.24",
150
+ "@gjsify/npm-registry": "^0.64.24",
151
+ "@gjsify/resolve-npm": "^0.64.24",
152
+ "@gjsify/rolldown-plugin-gjsify": "^0.64.24",
153
+ "@gjsify/vite-plugin-blueprint": "^0.64.24",
154
+ "@gjsify/rolldown-plugin-pnp": "^0.64.24",
155
+ "@gjsify/semver": "^0.64.24",
156
+ "@gjsify/tar": "^0.64.24",
157
+ "@gjsify/tsc": "^0.64.24",
158
+ "@gjsify/utils": "^0.64.24",
159
+ "@gjsify/web-polyfills": "^0.64.24",
160
+ "@gjsify/workspace": "^0.64.24",
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.23",
169
+ "@gjsify/unit": "^0.64.24",
170
170
  "@types/yargs": "^17.0.35",
171
171
  "typescript": "^6.0.3"
172
172
  },
173
173
  "peerDependencies": {
174
- "@gjsify/oxfmt-native": "^0.64.23",
175
- "@gjsify/rolldown-native": "^0.64.23"
174
+ "@gjsify/oxfmt-native": "^0.64.24",
175
+ "@gjsify/rolldown-native": "^0.64.24"
176
176
  },
177
177
  "peerDependenciesMeta": {
178
178
  "@gjsify/oxfmt-native": {