@gjsify/cli 0.44.0 → 0.46.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.
Files changed (81) hide show
  1. package/dist/cli.gjs.mjs +202 -167
  2. package/lib/commands/gettext.d.ts +2 -1
  3. package/lib/commands/gettext.js +105 -47
  4. package/lib/commands/onboard.d.ts +7 -0
  5. package/lib/commands/onboard.js +349 -96
  6. package/lib/commands/ship.d.ts +3 -0
  7. package/lib/commands/ship.js +850 -58
  8. package/lib/commands/trust.d.ts +6 -0
  9. package/lib/commands/trust.js +1 -1
  10. package/lib/commands/upgrade.d.ts +23 -0
  11. package/lib/commands/upgrade.js +95 -12
  12. package/lib/templates/install.mjs.tmpl +2 -2
  13. package/lib/types/config-data.d.ts +127 -1
  14. package/lib/utils/gi-search-path.js +11 -3
  15. package/lib/utils/msgfmt-merge.d.ts +35 -0
  16. package/lib/utils/msgfmt-merge.js +102 -0
  17. package/lib/utils/npm-otp.d.ts +27 -2
  18. package/lib/utils/npm-otp.js +82 -7
  19. package/lib/utils/onboard-discovery.d.ts +68 -0
  20. package/lib/utils/onboard-discovery.js +178 -0
  21. package/lib/utils/onboard-probe.d.ts +118 -1
  22. package/lib/utils/onboard-probe.js +153 -4
  23. package/lib/utils/prompt-output.d.ts +12 -0
  24. package/lib/utils/prompt-output.js +54 -0
  25. package/lib/utils/prompt.js +22 -2
  26. package/lib/utils/publishable-packages.d.ts +1 -1
  27. package/lib/utils/publishable-packages.js +1 -0
  28. package/lib/utils/resolve-npm-package.d.ts +56 -0
  29. package/lib/utils/resolve-npm-package.js +69 -0
  30. package/lib/utils/ship/app-runtime.d.ts +214 -0
  31. package/lib/utils/ship/app-runtime.js +429 -0
  32. package/lib/utils/ship/depends.d.ts +50 -0
  33. package/lib/utils/ship/depends.js +117 -3
  34. package/lib/utils/ship/discover.js +103 -1
  35. package/lib/utils/ship/dmg.d.ts +71 -0
  36. package/lib/utils/ship/dmg.js +181 -0
  37. package/lib/utils/ship/flatpak.js +1 -1
  38. package/lib/utils/ship/formats.d.ts +108 -7
  39. package/lib/utils/ship/formats.js +778 -15
  40. package/lib/utils/ship/launcher.d.ts +13 -3
  41. package/lib/utils/ship/launcher.js +419 -17
  42. package/lib/utils/ship/layout.d.ts +330 -0
  43. package/lib/utils/ship/layout.js +504 -0
  44. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  45. package/lib/utils/ship/localize-metadata.js +126 -0
  46. package/lib/utils/ship/mime.js +1 -9
  47. package/lib/utils/ship/msi.d.ts +126 -0
  48. package/lib/utils/ship/msi.js +434 -0
  49. package/lib/utils/ship/node-runtime.d.ts +71 -0
  50. package/lib/utils/ship/node-runtime.js +122 -0
  51. package/lib/utils/ship/payload.d.ts +156 -5
  52. package/lib/utils/ship/payload.js +599 -14
  53. package/lib/utils/ship/pe-launcher.d.ts +29 -0
  54. package/lib/utils/ship/pe-launcher.js +663 -0
  55. package/lib/utils/ship/plan.d.ts +21 -0
  56. package/lib/utils/ship/plan.js +55 -7
  57. package/lib/utils/ship/plist.d.ts +98 -0
  58. package/lib/utils/ship/plist.js +256 -0
  59. package/lib/utils/ship/rpm.js +16 -3
  60. package/lib/utils/ship/schemas.d.ts +61 -0
  61. package/lib/utils/ship/schemas.js +118 -0
  62. package/lib/utils/ship/scripts.js +7 -3
  63. package/lib/utils/ship/settings.d.ts +91 -2
  64. package/lib/utils/ship/settings.js +93 -1
  65. package/lib/utils/ship/share-dirs.d.ts +53 -0
  66. package/lib/utils/ship/share-dirs.js +76 -0
  67. package/lib/utils/ship/signing.d.ts +311 -0
  68. package/lib/utils/ship/signing.js +506 -0
  69. package/lib/utils/ship/stage-manifest.d.ts +40 -12
  70. package/lib/utils/ship/stage-manifest.js +99 -13
  71. package/lib/utils/ship/stage-writer.d.ts +48 -0
  72. package/lib/utils/ship/stage-writer.js +91 -2
  73. package/lib/utils/ship/types.d.ts +191 -14
  74. package/lib/utils/ship/xml.d.ts +7 -0
  75. package/lib/utils/ship/xml.js +25 -0
  76. package/lib/utils/ship/zip.d.ts +69 -0
  77. package/lib/utils/ship/zip.js +245 -0
  78. package/lib/utils/system-gi.d.ts +20 -5
  79. package/lib/utils/system-gi.js +36 -13
  80. package/lib/utils/trust-registry.js +5 -0
  81. package/package.json +19 -19
@@ -18,6 +18,13 @@ export interface StageInputs {
18
18
  * The result is deduplicated by path (last write wins, so `extraFiles` can
19
19
  * override anything the defaults staged) and sorted, which makes the tree —
20
20
  * and therefore every artifact built from it — deterministic.
21
+ *
22
+ * ONE plan, in the Linux/XDG shape, for every OS. Where those paths end up is
23
+ * `utils/ship/layout.ts`'s `placeStage`, applied afterwards by the caller, and
24
+ * keeping the two steps apart is what makes ADR 0024 § 2's "one payload, a
25
+ * handful of layouts" checkable: `tests/e2e/ship-layout` asserts the three
26
+ * staged trees agree modulo that map. A planner that took the layout would have
27
+ * three code paths to keep in agreement instead.
21
28
  */
22
29
  export declare function planStage(settings: ShipSettings, inputs: StageInputs): StagedFile[];
23
30
  /**
@@ -61,3 +68,17 @@ export declare function assertOverlayIsLicensed(formatId: FormatId, binaryName:
61
68
  * filename.
62
69
  */
63
70
  export declare function iconSizeDir(iconPath: string): string;
71
+ /**
72
+ * A native module inside the payload has to keep its executable bit.
73
+ *
74
+ * By NAME here, unlike `isArchIndependent`, which reads the file's magic: the
75
+ * planner is pure and has no bytes to look at. The two answer different
76
+ * questions — "should this be 0755" versus "is this package portable" — and
77
+ * getting this one wrong costs a mode bit, not an unusable package.
78
+ *
79
+ * EXPORTED for `utils/ship/app-runtime.ts`, which stages a relocated GTK closure
80
+ * that never passes through this plan and needs the same answer for the same
81
+ * files. A second copy of the pattern there would be a second answer to "is this
82
+ * a shared library", and the drifted one is the one that stages a `.dylib` 0644.
83
+ */
84
+ export declare function isExecutableAsset(rel: string): boolean;
@@ -3,6 +3,7 @@
3
3
  // as `utils/copy-targets.ts`, and for the same reason: a stager that discovers
4
4
  // its own inputs can only be tested by building a real project.
5
5
  import { renderMimePackage } from './mime.js';
6
+ import { SHARE } from './share-dirs.js';
6
7
  import { basename, extname, posix } from 'node:path';
7
8
  /**
8
9
  * Build the prefix-relative payload.
@@ -10,6 +11,13 @@ import { basename, extname, posix } from 'node:path';
10
11
  * The result is deduplicated by path (last write wins, so `extraFiles` can
11
12
  * override anything the defaults staged) and sorted, which makes the tree —
12
13
  * and therefore every artifact built from it — deterministic.
14
+ *
15
+ * ONE plan, in the Linux/XDG shape, for every OS. Where those paths end up is
16
+ * `utils/ship/layout.ts`'s `placeStage`, applied afterwards by the caller, and
17
+ * keeping the two steps apart is what makes ADR 0024 § 2's "one payload, a
18
+ * handful of layouts" checkable: `tests/e2e/ship-layout` asserts the three
19
+ * staged trees agree modulo that map. A planner that took the layout would have
20
+ * three code paths to keep in agreement instead.
13
21
  */
14
22
  export function planStage(settings, inputs) {
15
23
  const files = [];
@@ -23,7 +31,7 @@ export function planStage(settings, inputs) {
23
31
  });
24
32
  }
25
33
  files.push({
26
- path: `share/metainfo/${settings.appId}.metainfo.xml`,
34
+ path: `${SHARE.metainfo}/${settings.appId}.metainfo.xml`,
27
35
  mode: 0o644,
28
36
  source: { kind: 'text', text: inputs.metainfo },
29
37
  });
@@ -32,7 +40,7 @@ export function planStage(settings, inputs) {
32
40
  throw new Error('gjsify ship: internal error — a desktop entry is required for `kind: "app"`.');
33
41
  }
34
42
  files.push({
35
- path: `share/applications/${settings.appId}.desktop`,
43
+ path: `${SHARE.applications}/${settings.appId}.desktop`,
36
44
  mode: 0o644,
37
45
  source: { kind: 'text', text: inputs.desktopEntry },
38
46
  });
@@ -50,7 +58,7 @@ export function planStage(settings, inputs) {
50
58
  'Every installed schema shares one system directory, so the app id is what keeps it unique.');
51
59
  }
52
60
  files.push({
53
- path: `share/glib-2.0/schemas/${name}`,
61
+ path: `${SHARE.schemas}/${name}`,
54
62
  mode: 0o644,
55
63
  source: { kind: 'file', path: schema },
56
64
  });
@@ -71,7 +79,7 @@ export function planStage(settings, inputs) {
71
79
  // reason it is for the GSettings schema: a generic name collides with another package's file.
72
80
  if (settings.mimeTypes.length > 0) {
73
81
  files.push({
74
- path: `share/mime/packages/${settings.appId}.xml`,
82
+ path: `${SHARE.mime}/${settings.appId}.xml`,
75
83
  mode: 0o644,
76
84
  source: { kind: 'text', text: renderMimePackage(settings.mimeTypes) },
77
85
  });
@@ -80,16 +88,51 @@ export function planStage(settings, inputs) {
80
88
  // is where `bindtextdomain` looks, and nowhere else.
81
89
  for (const locale of settings.localeFiles) {
82
90
  files.push({
83
- path: posix.join('share/locale', locale.rel),
91
+ path: posix.join(SHARE.locale, locale.rel),
84
92
  mode: 0o644,
85
93
  source: { kind: 'file', path: locale.abs },
86
94
  });
87
95
  }
96
+ for (const font of planFonts(settings))
97
+ files.push(font);
88
98
  for (const [dest, source] of Object.entries(settings.extraFiles)) {
89
99
  files.push({ path: assertInsidePrefix(dest), mode: 0o644, source: { kind: 'file', path: source } });
90
100
  }
91
101
  return normalise(files);
92
102
  }
103
+ /**
104
+ * Font faces, under a directory named after the app id (ADR 0038).
105
+ *
106
+ * `share/fonts` is shared with every other package on a `/usr` prefix, exactly as
107
+ * `share/glib-2.0/schemas` and `share/mime/packages` are — so the app id is what
108
+ * keeps a face called `Regular.ttf` from being one of two files claiming the same
109
+ * path, with install order deciding the winner. Unlike those two the collision is
110
+ * not refused by NAME: a font's filename is the foundry's and there is no
111
+ * convention to hold it to, so the directory carries the id and the basenames are
112
+ * left alone. fontconfig scans recursively, so nesting them costs nothing —
113
+ * measured across fontconfig 2.14.1 → 2.18.3.
114
+ *
115
+ * A basename appearing twice IS refused, for the reason `planIcons` refuses one:
116
+ * two source files installing as one path means one of them is silently not
117
+ * shipped, and a missing face is a substituted typeface rather than an error.
118
+ */
119
+ function planFonts(settings) {
120
+ const out = [];
121
+ const seen = new Map();
122
+ for (const font of settings.fontFiles) {
123
+ const path = `${SHARE.fonts}/${settings.appId}/${basename(font)}`;
124
+ const previous = seen.get(path);
125
+ if (previous !== undefined) {
126
+ throw new Error(`gjsify ship: ${font} and ${previous} both install as ${path}. ` +
127
+ 'Two faces cannot share one filename — one would silently not ship, and a missing face ' +
128
+ 'is a substituted typeface rather than an error. Rename one, or point ' +
129
+ '`gjsify.ship.fonts` at a directory holding only the faces you mean to ship.');
130
+ }
131
+ seen.set(path, font);
132
+ out.push({ path, mode: 0o644, source: { kind: 'file', path: font } });
133
+ }
134
+ return out;
135
+ }
93
136
  /**
94
137
  * The per-format additions. Only the licence differs today, and it differs in
95
138
  * BOTH place and shape: Debian policy wants a machine-readable `copyright` in
@@ -178,7 +221,7 @@ function planIcons(settings) {
178
221
  for (const icon of settings.iconFiles) {
179
222
  const ext = extname(icon);
180
223
  const dir = iconSizeDir(icon);
181
- const path = `share/icons/hicolor/${dir}/apps/${settings.appId}${ext}`;
224
+ const path = `${SHARE.icons}/${dir}/apps/${settings.appId}${ext}`;
182
225
  const previous = seen.get(path);
183
226
  if (previous !== undefined) {
184
227
  throw new Error(`gjsify ship: ${icon} and ${previous} both install as ${path}. ` +
@@ -226,8 +269,13 @@ function assertInsidePrefix(dest) {
226
269
  * planner is pure and has no bytes to look at. The two answer different
227
270
  * questions — "should this be 0755" versus "is this package portable" — and
228
271
  * getting this one wrong costs a mode bit, not an unusable package.
272
+ *
273
+ * EXPORTED for `utils/ship/app-runtime.ts`, which stages a relocated GTK closure
274
+ * that never passes through this plan and needs the same answer for the same
275
+ * files. A second copy of the pattern there would be a second answer to "is this
276
+ * a shared library", and the drifted one is the one that stages a `.dylib` 0644.
229
277
  */
230
- function isExecutableAsset(rel) {
278
+ export function isExecutableAsset(rel) {
231
279
  return /\.(so|node|dylib)(\.\d+)*$/.test(rel);
232
280
  }
233
281
  /** Last write wins, then sort — deterministic output for a deterministic artifact. */
@@ -0,0 +1,98 @@
1
+ import type { LayoutMetadataInput } from './layout.js';
2
+ /** Where the two files live inside a bundle. Apple's names, not ours. */
3
+ export declare const BUNDLE_INFO_PLIST = "Contents/Info.plist";
4
+ export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
5
+ /**
6
+ * Render `Contents/Info.plist`.
7
+ *
8
+ * The eleven keys, each with the file in `refs/` it was read off (all paths
9
+ * relative to the repository root, all read-only submodules):
10
+ *
11
+ * | key | value | citation |
12
+ * |---|---|---|
13
+ * | `CFBundleExecutable` | `binaryName` | `refs/node/test/fixtures/macos-app-sandbox/Info.plist:5` |
14
+ * | `CFBundleIdentifier` | `appId` | same file, line 7 |
15
+ * | `CFBundleInfoDictionaryVersion` | `6.0` | same file, line 9 |
16
+ * | `CFBundleName` | `name` | same file, line 11 |
17
+ * | `CFBundlePackageType` | `APPL` | same file, line 13 |
18
+ * | `CFBundleShortVersionString` | `version` | same file, line 15 |
19
+ * | `CFBundleSupportedPlatforms` | `["MacOSX"]` | same file, lines 17-20 |
20
+ * | `CFBundleVersion` | `version-release` | same file, line 21 |
21
+ * | `CFBundleDevelopmentRegion` | `en` | `refs/node/deps/v8/gni/Info.plist:5` |
22
+ * | `CFBundleDisplayName` | `name` | same file, line 7 |
23
+ * | `CFBundleSignature` | `????` | same file, line 21 |
24
+ *
25
+ * `CFBundleVersion` carries the RELEASE and `CFBundleShortVersionString` does
26
+ * not, which is the one place the two differ. Apple's rule is that the short
27
+ * string is the marketing version a user sees and `CFBundleVersion` is the build
28
+ * — and this command already has that exact pair: `version` is upstream's,
29
+ * `release` is the packaging revision within it (the deb revision, the rpm
30
+ * release). Emitting `version` twice would throw the distinction away on the one
31
+ * OS that has a field for it.
32
+ *
33
+ * KEYS DELIBERATELY ABSENT are listed in this module's header. `CFBundleIconFile`
34
+ * is the one a reader will reach for first, and it is absent for a reason that
35
+ * outlives the key: M2a ships NO icon. `png2icns`, `icnsutil` and `iconutil` are
36
+ * all absent from this workstation and from the CI image, so an `.icns` written
37
+ * here could only be read back by a reader written here — `selfReading: true`,
38
+ * which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
39
+ * entering the CI image; until one does, the hicolor PNG/SVG the payload already
40
+ * carries stays the only icon, unread on macOS.
41
+ *
42
+ * A TWELFTH KEY, `ATSApplicationFontsPath`, is emitted when the bundle carries
43
+ * faces, and it is the first one here NOT cited to `refs/` — so the exception is
44
+ * stated rather than left to be noticed (ADR 0038). The rule this module opens
45
+ * with exists against DECORATION: the five absent keys are cosmetic, nothing a
46
+ * reader here can open contains them, and emitting one would be a guess with no
47
+ * observable behind it. This key is a MECHANISM, and its citation is Apple's own
48
+ * *Information Property List Key Reference*, which states the scope in the terms
49
+ * this command needs: *"If present, macOS activates the fonts at the specified
50
+ * path for use by the bundled app. The fonts are activated only for the bundled
51
+ * app and not for the system as a whole."* The value is a path relative to
52
+ * `Contents/Resources`.
53
+ *
54
+ * It is also the only route there is. Pango on macOS is CoreText-backed, GTK is
55
+ * not built against fontconfig on that platform, and
56
+ * `pango_font_map_add_font_file()` answers `G_IO_ERROR_NOT_SUPPORTED` on the
57
+ * CoreText map — so the alternatives are this key or a `CTFontManager` call made
58
+ * from inside somebody else's application, which is not a packaging command's
59
+ * business.
60
+ *
61
+ * WHAT THE ORACLE CAN AND CANNOT SEE: `plistlib` reads the key back like any
62
+ * other, so the FILE is checked. That macOS activates the directory, and that
63
+ * Pango's CoreText map then holds the family, is unverified here — which is what
64
+ * `Layout.fontGap` says out loud rather than letting a green stage imply.
65
+ */
66
+ export declare function renderInfoPlist(input: LayoutMetadataInput, fontsPath?: string): string;
67
+ /**
68
+ * Render `Contents/PkgInfo`: the package type and the signature, eight bytes, no
69
+ * newline.
70
+ *
71
+ * `refs/node/tools/gyp/pylib/gyp/mac_tool.py:236-245` is the whole
72
+ * specification — "eight characters, representing the bundle type and bundle
73
+ * signature, each four characters" — and its
74
+ * `fp.write(f"{package_type}{signature_code}")` writes no terminator. A trailing
75
+ * newline would make the file nine bytes, which is the kind of difference
76
+ * nothing complains about and every byte comparison notices.
77
+ *
78
+ * Redundant with `Info.plist` by design: it is the pre-plist Carbon record that
79
+ * LaunchServices still reads, and gyp writes both from one source for exactly
80
+ * the same reason this function takes none — both values are constants here.
81
+ */
82
+ export declare function renderPkgInfo(): string;
83
+ /**
84
+ * Render the entitlements plist `codesign --entitlements` takes.
85
+ *
86
+ * A SEPARATE FILE FROM `Info.plist` and a separate function, because it is a
87
+ * different document with a different reader: `Info.plist` describes the bundle
88
+ * to LaunchServices and ships inside it, while this one is an INPUT to the
89
+ * signature — `codesign` embeds the claims and the file itself is never staged.
90
+ *
91
+ * Every value is `<true/>`. An entitlement of this class is a boolean capability,
92
+ * and a `<false/>` entry is not "off", it is the same as absent — so a renderer
93
+ * taking values would offer a spelling with no meaning.
94
+ *
95
+ * WHICH KEYS, and why the set is not the reference's, is
96
+ * `utils/ship/signing.ts`'s decision; this function renders what it is given.
97
+ */
98
+ export declare function renderEntitlements(keys: readonly string[]): string;
@@ -0,0 +1,256 @@
1
+ // `Contents/Info.plist` and `Contents/PkgInfo` — what makes a `*.app` directory
2
+ // an application BUNDLE rather than a directory whose name ends in `.app`.
3
+ //
4
+ // M1 staged `<Name>.app/Contents/{MacOS,Resources}` with no `Info.plist` at all.
5
+ // LaunchServices reads that file to learn which file under `Contents/MacOS` to
6
+ // exec and what identity to register the bundle under, so without it the tree is
7
+ // a directory the Finder shows as a folder — the hole #1354 M2a closes.
8
+ //
9
+ // XML, never the binary variant, and that is an ORACLE decision rather than a
10
+ // taste one. `plistlib` reads both, but a binary plist this tree wrote and then
11
+ // read back with a reader this tree also wrote is `selfReading: true` — ADR 0024
12
+ // § A3 calls that legal to declare and illegal to release. CPython's `plistlib`
13
+ // is a different implementation family and is already precedent here
14
+ // (`.github/ship-oracle/verify-modes.py`), and it parses the XML form.
15
+ //
16
+ // EVERY KEY BELOW IS CITED to a file in `refs/` that a real macOS toolchain
17
+ // produced or consumes. Keys that are merely plausible — `CFBundleIconFile`,
18
+ // `LSMinimumSystemVersion`, `NSHighResolutionCapable`, `LSApplicationCategoryType`,
19
+ // `NSHumanReadableCopyright` — are not emitted, because nothing a reader here can
20
+ // open contains them.
21
+ //
22
+ // AND THE SCOPE OF THAT MEASUREMENT IS PART OF IT, because `grep -r … refs/` is a
23
+ // command that answers cheerfully after reading almost nothing: 89 of the 95
24
+ // submodules `.gitmodules` declares are not checked out in a working tree, and an
25
+ // empty directory produces zero hits with no error. Measured on a checkout with
26
+ // SIX of them present (`cambalache`, `gtkx`, `libadwaita`, `metro`, `node`,
27
+ // `peachy`): 0 files for each of the five keys above, against 3 files for
28
+ // `CFBundleSignature` as the CONTROL — a string known to be there, which is what
29
+ // makes the zeros mean anything. So the claim is "absent from the six pools a
30
+ // reader can open", not "absent from the pool", and a key that turns up in a
31
+ // seventh is a citation waiting to be written, not a contradiction.
32
+ /** Where the two files live inside a bundle. Apple's names, not ours. */
33
+ export const BUNDLE_INFO_PLIST = 'Contents/Info.plist';
34
+ export const BUNDLE_PKGINFO = 'Contents/PkgInfo';
35
+ /**
36
+ * The four-character bundle type of an application, and the signature that says
37
+ * "none".
38
+ *
39
+ * `refs/node/tools/gyp/pylib/gyp/mac_tool.py:232` reads `CFBundlePackageType` and
40
+ * writes a `PkgInfo` only when it is `APPL`; line 239 defaults
41
+ * `CFBundleSignature` to `????` and line 240 resets a signature of the wrong
42
+ * length to the same four question marks. `refs/node/deps/v8/gni/Info.plist:22`
43
+ * spells `????` out.
44
+ */
45
+ const PACKAGE_TYPE = 'APPL';
46
+ const SIGNATURE = '????';
47
+ /**
48
+ * The one XML escape set a plist needs.
49
+ *
50
+ * `&` first, or the ampersands introduced by the later replacements get escaped
51
+ * twice and `CFBundleName` comes out as `Ship &amp;amp; Co`. `<` and `>` are the
52
+ * element delimiters; `"` and `'` are not special in element CONTENT, which is
53
+ * the only place this function's output ever lands, so they are left alone
54
+ * rather than turned into entities a human reading the file has to decode.
55
+ */
56
+ function xmlText(value) {
57
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
58
+ }
59
+ /**
60
+ * Refuse a value a plist cannot carry, naming the field.
61
+ *
62
+ * XML 1.0 has no escape for most C0 control characters — they are not
63
+ * representable in a document at all, not merely inconvenient — so a name
64
+ * carrying one produces a file `plistlib` and `CFPropertyList` both reject. The
65
+ * refusal is here rather than at parse time on a Mac three milestones away.
66
+ *
67
+ * C0 MINUS TAB/LF/CR, AND NOTHING ELSE — measured against the oracle rather than
68
+ * reasoned from the spec, because the first cut of this guard also refused DEL
69
+ * and the C1 range on the stated grounds that XML forbids them, and XML 1.0 does
70
+ * not. Probed through `plistlib` on CPython 3.14, one code point per document:
71
+ *
72
+ * U+0001, U+0008, U+000B ExpatError: not well-formed (invalid token)
73
+ * U+0009 (TAB) accepted
74
+ * U+007F (DEL) accepted
75
+ * U+0085, U+009F (C1) accepted
76
+ *
77
+ * So refusing those four would reject a name every reader in the chain reads
78
+ * back correctly — a guard that turns working packages into failures, which buys
79
+ * nothing over the defect it prevents. The rule that survives is the measured
80
+ * one; the reason that did not is recorded here rather than quietly dropped,
81
+ * because a rule carrying an invented reason is one that gets "simplified" back
82
+ * into the bug.
83
+ */
84
+ function assertPlistText(value, field, configKey) {
85
+ // A SCAN rather than a character class, and not only to keep `no-control-regex`
86
+ // quiet: the code point is what the message needs, and lifting it back out of
87
+ // a match is a second step that can disagree with the first.
88
+ let point = -1;
89
+ for (const char of value) {
90
+ const code = char.codePointAt(0) ?? 0;
91
+ // Tab, LF and CR are the three C0 code points XML 1.0 does carry.
92
+ if (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) {
93
+ point = code;
94
+ break;
95
+ }
96
+ }
97
+ if (point !== -1) {
98
+ throw new Error(`gjsify ship: ${field} would carry the control character ` +
99
+ `U+${point.toString(16).padStart(4, '0').toUpperCase()}, and XML has no escape for it — the ` +
100
+ 'Info.plist would be unparseable by every reader macOS has. Set ' +
101
+ `\`${configKey}\` to a value without it.`);
102
+ }
103
+ return value;
104
+ }
105
+ /** One `<key>…</key><string>…</string>` pair, indented to sit inside the top-level `<dict>`. */
106
+ function stringEntry(key, value) {
107
+ return `\t<key>${key}</key>\n\t<string>${xmlText(value)}</string>`;
108
+ }
109
+ /** `CFBundleSupportedPlatforms` is the one array here — see the key table below. */
110
+ function arrayEntry(key, values) {
111
+ const items = values.map((value) => `\t\t<string>${xmlText(value)}</string>`).join('\n');
112
+ return `\t<key>${key}</key>\n\t<array>\n${items}\n\t</array>`;
113
+ }
114
+ /**
115
+ * Render `Contents/Info.plist`.
116
+ *
117
+ * The eleven keys, each with the file in `refs/` it was read off (all paths
118
+ * relative to the repository root, all read-only submodules):
119
+ *
120
+ * | key | value | citation |
121
+ * |---|---|---|
122
+ * | `CFBundleExecutable` | `binaryName` | `refs/node/test/fixtures/macos-app-sandbox/Info.plist:5` |
123
+ * | `CFBundleIdentifier` | `appId` | same file, line 7 |
124
+ * | `CFBundleInfoDictionaryVersion` | `6.0` | same file, line 9 |
125
+ * | `CFBundleName` | `name` | same file, line 11 |
126
+ * | `CFBundlePackageType` | `APPL` | same file, line 13 |
127
+ * | `CFBundleShortVersionString` | `version` | same file, line 15 |
128
+ * | `CFBundleSupportedPlatforms` | `["MacOSX"]` | same file, lines 17-20 |
129
+ * | `CFBundleVersion` | `version-release` | same file, line 21 |
130
+ * | `CFBundleDevelopmentRegion` | `en` | `refs/node/deps/v8/gni/Info.plist:5` |
131
+ * | `CFBundleDisplayName` | `name` | same file, line 7 |
132
+ * | `CFBundleSignature` | `????` | same file, line 21 |
133
+ *
134
+ * `CFBundleVersion` carries the RELEASE and `CFBundleShortVersionString` does
135
+ * not, which is the one place the two differ. Apple's rule is that the short
136
+ * string is the marketing version a user sees and `CFBundleVersion` is the build
137
+ * — and this command already has that exact pair: `version` is upstream's,
138
+ * `release` is the packaging revision within it (the deb revision, the rpm
139
+ * release). Emitting `version` twice would throw the distinction away on the one
140
+ * OS that has a field for it.
141
+ *
142
+ * KEYS DELIBERATELY ABSENT are listed in this module's header. `CFBundleIconFile`
143
+ * is the one a reader will reach for first, and it is absent for a reason that
144
+ * outlives the key: M2a ships NO icon. `png2icns`, `icnsutil` and `iconutil` are
145
+ * all absent from this workstation and from the CI image, so an `.icns` written
146
+ * here could only be read back by a reader written here — `selfReading: true`,
147
+ * which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
148
+ * entering the CI image; until one does, the hicolor PNG/SVG the payload already
149
+ * carries stays the only icon, unread on macOS.
150
+ *
151
+ * A TWELFTH KEY, `ATSApplicationFontsPath`, is emitted when the bundle carries
152
+ * faces, and it is the first one here NOT cited to `refs/` — so the exception is
153
+ * stated rather than left to be noticed (ADR 0038). The rule this module opens
154
+ * with exists against DECORATION: the five absent keys are cosmetic, nothing a
155
+ * reader here can open contains them, and emitting one would be a guess with no
156
+ * observable behind it. This key is a MECHANISM, and its citation is Apple's own
157
+ * *Information Property List Key Reference*, which states the scope in the terms
158
+ * this command needs: *"If present, macOS activates the fonts at the specified
159
+ * path for use by the bundled app. The fonts are activated only for the bundled
160
+ * app and not for the system as a whole."* The value is a path relative to
161
+ * `Contents/Resources`.
162
+ *
163
+ * It is also the only route there is. Pango on macOS is CoreText-backed, GTK is
164
+ * not built against fontconfig on that platform, and
165
+ * `pango_font_map_add_font_file()` answers `G_IO_ERROR_NOT_SUPPORTED` on the
166
+ * CoreText map — so the alternatives are this key or a `CTFontManager` call made
167
+ * from inside somebody else's application, which is not a packaging command's
168
+ * business.
169
+ *
170
+ * WHAT THE ORACLE CAN AND CANNOT SEE: `plistlib` reads the key back like any
171
+ * other, so the FILE is checked. That macOS activates the directory, and that
172
+ * Pango's CoreText map then holds the family, is unverified here — which is what
173
+ * `Layout.fontGap` says out loud rather than letting a green stage imply.
174
+ */
175
+ export function renderInfoPlist(input, fontsPath) {
176
+ const name = assertPlistText(input.name, 'CFBundleName', 'gjsify.ship.name');
177
+ const executable = assertPlistText(input.binaryName, 'CFBundleExecutable', 'gjsify.ship.binaryName');
178
+ const appId = assertPlistText(input.appId, 'CFBundleIdentifier', 'gjsify.ship.appId');
179
+ return [
180
+ '<?xml version="1.0" encoding="UTF-8"?>',
181
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
182
+ '<plist version="1.0">',
183
+ '<dict>',
184
+ // Alphabetical, because both cited files are: `macos-app-sandbox/Info.plist`
185
+ // runs Executable → Identifier → InfoDictionaryVersion → Name → PackageType
186
+ // → ShortVersionString → SupportedPlatforms → Version, and `v8/gni`'s
187
+ // CFBundle keys sort the same way. Matching them keeps a diff against a
188
+ // real bundle readable; no claim is made about what Xcode emits.
189
+ //
190
+ // `ATSApplicationFontsPath` sorts before every `CFBundle*` key, which is
191
+ // where alphabetical order puts it and also where a reader looks for it.
192
+ // Absent, not empty, when the bundle carries no face: an empty string is a
193
+ // path, and macOS would activate the Resources directory itself.
194
+ ...(fontsPath === undefined ? [] : [stringEntry('ATSApplicationFontsPath', fontsPath)]),
195
+ stringEntry('CFBundleDevelopmentRegion', 'en'),
196
+ stringEntry('CFBundleDisplayName', name),
197
+ stringEntry('CFBundleExecutable', executable),
198
+ stringEntry('CFBundleIdentifier', appId),
199
+ stringEntry('CFBundleInfoDictionaryVersion', '6.0'),
200
+ stringEntry('CFBundleName', name),
201
+ stringEntry('CFBundlePackageType', PACKAGE_TYPE),
202
+ stringEntry('CFBundleShortVersionString', input.version),
203
+ stringEntry('CFBundleSignature', SIGNATURE),
204
+ arrayEntry('CFBundleSupportedPlatforms', ['MacOSX']),
205
+ stringEntry('CFBundleVersion', `${input.version}-${input.release}`),
206
+ '</dict>',
207
+ '</plist>',
208
+ '',
209
+ ].join('\n');
210
+ }
211
+ /**
212
+ * Render `Contents/PkgInfo`: the package type and the signature, eight bytes, no
213
+ * newline.
214
+ *
215
+ * `refs/node/tools/gyp/pylib/gyp/mac_tool.py:236-245` is the whole
216
+ * specification — "eight characters, representing the bundle type and bundle
217
+ * signature, each four characters" — and its
218
+ * `fp.write(f"{package_type}{signature_code}")` writes no terminator. A trailing
219
+ * newline would make the file nine bytes, which is the kind of difference
220
+ * nothing complains about and every byte comparison notices.
221
+ *
222
+ * Redundant with `Info.plist` by design: it is the pre-plist Carbon record that
223
+ * LaunchServices still reads, and gyp writes both from one source for exactly
224
+ * the same reason this function takes none — both values are constants here.
225
+ */
226
+ export function renderPkgInfo() {
227
+ return `${PACKAGE_TYPE}${SIGNATURE}`;
228
+ }
229
+ /**
230
+ * Render the entitlements plist `codesign --entitlements` takes.
231
+ *
232
+ * A SEPARATE FILE FROM `Info.plist` and a separate function, because it is a
233
+ * different document with a different reader: `Info.plist` describes the bundle
234
+ * to LaunchServices and ships inside it, while this one is an INPUT to the
235
+ * signature — `codesign` embeds the claims and the file itself is never staged.
236
+ *
237
+ * Every value is `<true/>`. An entitlement of this class is a boolean capability,
238
+ * and a `<false/>` entry is not "off", it is the same as absent — so a renderer
239
+ * taking values would offer a spelling with no meaning.
240
+ *
241
+ * WHICH KEYS, and why the set is not the reference's, is
242
+ * `utils/ship/signing.ts`'s decision; this function renders what it is given.
243
+ */
244
+ export function renderEntitlements(keys) {
245
+ const entries = keys.map((key) => `\t<key>${xmlText(key)}</key>\n\t<true/>`).join('\n');
246
+ return [
247
+ '<?xml version="1.0" encoding="UTF-8"?>',
248
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
249
+ '<plist version="1.0">',
250
+ '<dict>',
251
+ entries,
252
+ '</dict>',
253
+ '</plist>',
254
+ '',
255
+ ].join('\n');
256
+ }
@@ -19,6 +19,7 @@ import { readPayloadFacts, readShebangInterpreters } from './payload.js';
19
19
  import { buildRpmHeader, buildRpmLead, padToEight, RpmType, RPM_TAG_HEADERIMMUTABLE, RPM_TAG_HEADERSIGNATURES, } from './rpm-header.js';
20
20
  import { gzipDeterministic } from './gzip.js';
21
21
  import { renderRpmScriptlets } from './scripts.js';
22
+ import { SHARE } from './share-dirs.js';
22
23
  // Header tags. Named rather than inlined because a wrong NUMBER produces a
23
24
  // package that parses and is quietly wrong, which is the hardest kind to spot.
24
25
  const TAG = {
@@ -115,6 +116,11 @@ const RPMLIB_REQUIRES = [
115
116
  * rpmlint noise, not a broken install. The entries that matter are the ones a
116
117
  * package would otherwise claim on every install.
117
118
  */
119
+ // The three that name a directory `plan.ts` actually stages into are DERIVED, so
120
+ // they cannot drift from it. The rest stay literal on purpose: they are PARENTS
121
+ // (`/usr/share/icons`, `/usr/share/glib-2.0`) or directories this packer never
122
+ // writes (`/etc`, `/opt`, `/var`, `/usr/share/man`), which is a different question
123
+ // from "where does the payload install" and would be wrong to fold into one list.
118
124
  const SYSTEM_OWNED_DIRECTORIES = new Set([
119
125
  '/usr',
120
126
  '/usr/bin',
@@ -123,16 +129,23 @@ const SYSTEM_OWNED_DIRECTORIES = new Set([
123
129
  '/usr/libexec',
124
130
  '/usr/sbin',
125
131
  '/usr/share',
126
- '/usr/share/applications',
132
+ `/usr/${SHARE.applications}`,
127
133
  '/usr/share/doc',
128
134
  '/usr/share/icons',
129
135
  '/usr/share/licenses',
130
136
  '/usr/share/man',
131
- '/usr/share/metainfo',
137
+ `/usr/${SHARE.metainfo}`,
132
138
  // Owned by glib2 — an app that installs a schema must not claim the
133
139
  // directory the whole system's schemas live in. Verified with `rpm -qf`.
134
140
  '/usr/share/glib-2.0',
135
- '/usr/share/glib-2.0/schemas',
141
+ `/usr/${SHARE.schemas}`,
142
+ // Owned by `fonts-filesystem` — measured, `rpm -qf /usr/share/fonts` →
143
+ // `fonts-filesystem-5.0.0-2.fc44`. DERIVED although it is a PARENT rather than a
144
+ // directory the planner stages into: `plan.ts` writes `share/fonts/<appId>`,
145
+ // which this package legitimately owns, and the parent happens to BE
146
+ // `SHARE.fonts` exactly — so deriving it costs nothing and cannot drift the way
147
+ // the two literal parents above can.
148
+ `/usr/${SHARE.fonts}`,
136
149
  '/etc',
137
150
  '/opt',
138
151
  '/var',
@@ -0,0 +1,61 @@
1
+ import type { StagedFile } from './types.js';
2
+ /** The one spelling of the tool, read by this module AND by the format rows. */
3
+ export declare const SCHEMA_COMPILER = "glib-compile-schemas";
4
+ /**
5
+ * How to install it, in the two distributions' words.
6
+ *
7
+ * The same two package names `depends.ts`'s `SCHEMA_COMPILER_PACKAGE` emits into
8
+ * a `Depends:`/`Requires:`, so the advice a user gets here and the dependency a
9
+ * `.deb` declares cannot name different packages.
10
+ */
11
+ export declare const SCHEMA_COMPILER_HINT = "Fedora: `sudo dnf install glib2`, Debian/Ubuntu: `sudo apt install libglib2.0-bin`";
12
+ /** The compiled cache's prefix-relative path — one definition, several readers. */
13
+ export declare const SCHEMA_CACHE: string;
14
+ export interface CompileSchemasInput {
15
+ /** Absolute paths of the project's `*.gschema.xml` files. */
16
+ schemaFiles: readonly string[];
17
+ /** A scratch directory this function owns; wiped and rewritten on every run. */
18
+ workDir: string;
19
+ }
20
+ /**
21
+ * Compile the payload's schemas and return the cache as ONE prefix-relative
22
+ * planned file, or nothing when the payload has no schema.
23
+ *
24
+ * PREFIX-RELATIVE, so `place()` maps it into `Contents/Resources/share/…` or a
25
+ * Windows `share\` with no rule of its own. That is the whole reason this returns
26
+ * a `StagedFile` rather than writing into the stage directly: the layout map stays
27
+ * the single place that knows where `share/` goes, and the e2e's hand-written map
28
+ * needs no new entry to cover it.
29
+ *
30
+ * `--strict` IS LOAD-BEARING and was measured, not assumed. Against a schema whose
31
+ * `path` attribute lacks its leading slash, on glib 2.88.3 — the observable is the
32
+ * exit code and the file, not the wording, because the diagnostic is translated
33
+ * and was read here in German:
34
+ *
35
+ * glib-compile-schemas in --targetdir out → exit 0, warns that the
36
+ * whole file was ignored,
37
+ * gschemas.compiled WRITTEN
38
+ * glib-compile-schemas in --targetdir out --strict → exit 1, nothing written
39
+ *
40
+ * Without the flag a broken schema is skipped at exit 0 and a cache is produced
41
+ * that simply does not contain it — so the stage looks compiled, the warning goes
42
+ * away, and `g_settings_new()` still aborts on the schema that was dropped. That
43
+ * is a green that checked nothing, produced by the step meant to close the hole.
44
+ *
45
+ * A DIRECTORY, because that is the only input the tool takes: the schemas are
46
+ * copied into `workDir` under their own basenames first. `plan.ts` has already
47
+ * refused any basename not prefixed with the app id, so two schemas cannot
48
+ * collide here.
49
+ *
50
+ * ENDIANNESS, stated rather than assumed away. The output is GVDB, and the file
51
+ * this tree produces is HOST-endian — measured on x86-64 with glib 2.88.3: in a
52
+ * 256-byte `gschemas.compiled` compiled from one schema with one key, the eight
53
+ * bytes at offset 16 read `18 00 00 00 58 00 00 00`, i.e. the pointer pair 24 and
54
+ * 88 stored little-endian. Whether GLib's reader byte-swaps a foreign-endian
55
+ * cache is UNVERIFIED here — nothing in this tree tested it and nothing below
56
+ * assumes an answer. It does not bite today: both darwin targets (x64, arm64) and
57
+ * every runner in this project's CI are little-endian, so no cache yet crosses
58
+ * that boundary. A big-endian assembling host is the case to measure before
59
+ * anyone claims otherwise.
60
+ */
61
+ export declare function compileSchemasForStage(input: CompileSchemasInput): Promise<StagedFile[]>;