@gjsify/cli 0.45.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.
- package/dist/cli.gjs.mjs +142 -134
- 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/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 +165 -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
package/lib/utils/ship/plan.js
CHANGED
|
@@ -93,11 +93,46 @@ export function planStage(settings, inputs) {
|
|
|
93
93
|
source: { kind: 'file', path: locale.abs },
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
+
for (const font of planFonts(settings))
|
|
97
|
+
files.push(font);
|
|
96
98
|
for (const [dest, source] of Object.entries(settings.extraFiles)) {
|
|
97
99
|
files.push({ path: assertInsidePrefix(dest), mode: 0o644, source: { kind: 'file', path: source } });
|
|
98
100
|
}
|
|
99
101
|
return normalise(files);
|
|
100
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
|
+
}
|
|
101
136
|
/**
|
|
102
137
|
* The per-format additions. Only the licence differs today, and it differs in
|
|
103
138
|
* BOTH place and shape: Debian policy wants a machine-readable `copyright` in
|
|
@@ -38,8 +38,32 @@ export declare const BUNDLE_PKGINFO = "Contents/PkgInfo";
|
|
|
38
38
|
* which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
|
|
39
39
|
* entering the CI image; until one does, the hicolor PNG/SVG the payload already
|
|
40
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.
|
|
41
65
|
*/
|
|
42
|
-
export declare function renderInfoPlist(input: LayoutMetadataInput): string;
|
|
66
|
+
export declare function renderInfoPlist(input: LayoutMetadataInput, fontsPath?: string): string;
|
|
43
67
|
/**
|
|
44
68
|
* Render `Contents/PkgInfo`: the package type and the signature, eight bytes, no
|
|
45
69
|
* newline.
|
|
@@ -56,3 +80,19 @@ export declare function renderInfoPlist(input: LayoutMetadataInput): string;
|
|
|
56
80
|
* the same reason this function takes none — both values are constants here.
|
|
57
81
|
*/
|
|
58
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;
|
package/lib/utils/ship/plist.js
CHANGED
|
@@ -147,8 +147,32 @@ function arrayEntry(key, values) {
|
|
|
147
147
|
* which `flatpak.spec.ts` reds. The unblocker is an independent Linux ICNS reader
|
|
148
148
|
* entering the CI image; until one does, the hicolor PNG/SVG the payload already
|
|
149
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.
|
|
150
174
|
*/
|
|
151
|
-
export function renderInfoPlist(input) {
|
|
175
|
+
export function renderInfoPlist(input, fontsPath) {
|
|
152
176
|
const name = assertPlistText(input.name, 'CFBundleName', 'gjsify.ship.name');
|
|
153
177
|
const executable = assertPlistText(input.binaryName, 'CFBundleExecutable', 'gjsify.ship.binaryName');
|
|
154
178
|
const appId = assertPlistText(input.appId, 'CFBundleIdentifier', 'gjsify.ship.appId');
|
|
@@ -162,6 +186,12 @@ export function renderInfoPlist(input) {
|
|
|
162
186
|
// → ShortVersionString → SupportedPlatforms → Version, and `v8/gni`'s
|
|
163
187
|
// CFBundle keys sort the same way. Matching them keeps a diff against a
|
|
164
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)]),
|
|
165
195
|
stringEntry('CFBundleDevelopmentRegion', 'en'),
|
|
166
196
|
stringEntry('CFBundleDisplayName', name),
|
|
167
197
|
stringEntry('CFBundleExecutable', executable),
|
|
@@ -196,3 +226,31 @@ export function renderInfoPlist(input) {
|
|
|
196
226
|
export function renderPkgInfo() {
|
|
197
227
|
return `${PACKAGE_TYPE}${SIGNATURE}`;
|
|
198
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
|
+
}
|
package/lib/utils/ship/rpm.js
CHANGED
|
@@ -139,6 +139,13 @@ const SYSTEM_OWNED_DIRECTORIES = new Set([
|
|
|
139
139
|
// directory the whole system's schemas live in. Verified with `rpm -qf`.
|
|
140
140
|
'/usr/share/glib-2.0',
|
|
141
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}`,
|
|
142
149
|
'/etc',
|
|
143
150
|
'/opt',
|
|
144
151
|
'/var',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppMetadata, ConfigDataFlatpak, ConfigDataShip, DescriptionBlock } from '../../types/config-data.js';
|
|
1
|
+
import type { AppMetadata, ConfigDataFlatpak, ConfigDataShip, DescriptionBlock, ShipAppOptions } from '../../types/config-data.js';
|
|
2
2
|
import type { HostOs, ShipSettings } from './types.js';
|
|
3
3
|
/** The subset of `package.json` the resolver reads. */
|
|
4
4
|
export interface ShipPackageManifest {
|
|
@@ -34,6 +34,8 @@ export interface DiscoveredPayload {
|
|
|
34
34
|
rel: string;
|
|
35
35
|
abs: string;
|
|
36
36
|
}[];
|
|
37
|
+
/** Absolute paths of the font faces the application ships itself. */
|
|
38
|
+
fontFiles: string[];
|
|
37
39
|
licenseFile?: string;
|
|
38
40
|
}
|
|
39
41
|
export interface SettingsInput {
|
|
@@ -59,13 +61,82 @@ export interface SettingsInput {
|
|
|
59
61
|
};
|
|
60
62
|
discovered: DiscoveredPayload;
|
|
61
63
|
/**
|
|
62
|
-
*
|
|
64
|
+
* THIS TARGET's runtime, as {@link resolveShipApp} resolved it — never the
|
|
65
|
+
* raw `gjsify.app`.
|
|
63
66
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
67
|
+
* The TYPE is the enforcement: a caller handing the project field straight
|
|
68
|
+
* through no longer compiles. That is exactly the regression this field used
|
|
69
|
+
* to carry in silence, since every generator downstream reads `settings.app`.
|
|
66
70
|
*/
|
|
67
|
-
app
|
|
71
|
+
app: 'gjs' | 'node';
|
|
68
72
|
}
|
|
73
|
+
/** The runtime one ship target runs, and which key decided it. */
|
|
74
|
+
export interface ResolvedShipApp {
|
|
75
|
+
/** What the launcher execs, what the package depends on, and which formats can wrap it. */
|
|
76
|
+
app: 'gjs' | 'node';
|
|
77
|
+
/** The config key the answer came from — named in the refusal and in the notice. */
|
|
78
|
+
key: string;
|
|
79
|
+
/** Whether a per-target key overrode the project default. */
|
|
80
|
+
overridden: boolean;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The ONE place a ship target's runtime is decided: `gjsify.app` as the DEFAULT,
|
|
84
|
+
* `gjsify.ship.app.<os>` as this target's override.
|
|
85
|
+
*
|
|
86
|
+
* WHY IT IS PER TARGET, measured on a GTK4 application rather than derived from
|
|
87
|
+
* the schema: with one field, `gjsify ship darwin` refused its formats until
|
|
88
|
+
* `gjsify.app` was `"node"` — and setting it turned the LINUX `.deb`'s
|
|
89
|
+
* `Depends: gjs (>= 1.86)` into `Depends: nodejs (>= 24)`, which apt refuses on
|
|
90
|
+
* trixie, Ubuntu 24.04 and Ubuntu 26.04. A macOS decision made the Linux package
|
|
91
|
+
* uninstallable, because every generator read the PROJECT field instead of the
|
|
92
|
+
* resolved runtime of its OWN target (#1486, ADR 0024 § A22). Nothing downstream
|
|
93
|
+
* reads either key; they all read `settings.app`, which is this result.
|
|
94
|
+
*
|
|
95
|
+
* NOT PER LAYOUT BY DEFAULT, and the first cut of the layout axis got this
|
|
96
|
+
* exactly backwards. Reading ADR § 4's runtime table as a per-layout
|
|
97
|
+
* REQUIREMENT refused `gjsify.app: "gjs"` for the macOS layout — the entire
|
|
98
|
+
* audience of that command — while a project with no `gjsify.app` key sailed
|
|
99
|
+
* through and staged a launcher naming `node` in front of a GJS bundle. § 4
|
|
100
|
+
* derives the runtime a shipped artifact CARRIES, which is
|
|
101
|
+
* `Layout.shippedRuntime`; what an artifact runs is what its author says, which
|
|
102
|
+
* is this. So an absent override means the project field and never the layout's
|
|
103
|
+
* derived answer.
|
|
104
|
+
*
|
|
105
|
+
* MUST NOT be fed `Config.forBuild`'s resolution, whose fallback is the HOST
|
|
106
|
+
* runtime. That fallback is right for a build (`gjsify build` under Node should
|
|
107
|
+
* produce a Node bundle by default) and catastrophic for a package: running this
|
|
108
|
+
* command under Node would silently turn every undeclared GJS project into one
|
|
109
|
+
* whose launcher execs `node`. An undeclared target is the common case for a GJS
|
|
110
|
+
* app, and it means GJS.
|
|
111
|
+
*
|
|
112
|
+
* TWO REFUSALS, and each is here because the alternative to refusing is SILENT.
|
|
113
|
+
* A `browser` or `nativescript` value has no artifact to be — no process to
|
|
114
|
+
* start, an APK/IPA through another pipeline (§ 4) — and would otherwise leave
|
|
115
|
+
* here as a `.deb` a stranger installs and cannot run, with no gate between here
|
|
116
|
+
* and their machine; `Layout.runtimeGap` only WARNS because what it describes is
|
|
117
|
+
* a staged tree rather than an artifact, and the per-FORMAT half of the question
|
|
118
|
+
* is `assertFormatCanRunInterpreter`, which asks what a container can EXECUTE.
|
|
119
|
+
* A key outside {@link SHIP_APP_OSES} — `windows`, the `gjsify ship <os>`
|
|
120
|
+
* POSITIONAL's spelling — would resolve to nothing and leave that target on the
|
|
121
|
+
* project-wide answer with every gate green, which is why the author who typed
|
|
122
|
+
* it reads the result as gjsify not supporting the split at all. Both refusals
|
|
123
|
+
* name the KEY that carried the value: with two keys, "this project declares" no
|
|
124
|
+
* longer locates it.
|
|
125
|
+
*
|
|
126
|
+
* `gjsify ship --from-stage` reads neither key and needs neither: the stage
|
|
127
|
+
* manifest records the RESOLVED value, so the packing host reads an answer
|
|
128
|
+
* rather than a default, and `assertLauncherMatchesInterpreter` still compares
|
|
129
|
+
* the staged `bin/<name>`'s own `exec` line against the dependency about to be
|
|
130
|
+
* written.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveShipApp(input: {
|
|
133
|
+
/** `gjsify.app` as DECLARED — `undefined` when the project declares nothing. */
|
|
134
|
+
project?: string;
|
|
135
|
+
/** `gjsify.ship.app`, the per-layout override table. */
|
|
136
|
+
perTarget?: ShipAppOptions;
|
|
137
|
+
/** The layout this run assembles. */
|
|
138
|
+
layoutOs: HostOs;
|
|
139
|
+
}): ResolvedShipApp;
|
|
69
140
|
export interface ResolvedSettings {
|
|
70
141
|
settings: ShipSettings;
|
|
71
142
|
/** The merged metadata the renderers read. */
|
|
@@ -14,6 +14,90 @@ import { basename } from 'node:path';
|
|
|
14
14
|
import { DEFAULT_GJS_FLOOR, DEFAULT_NODE_FLOOR } from './depends.js';
|
|
15
15
|
import { resolveShipFlatpakSettings } from './flatpak-config.js';
|
|
16
16
|
import { assertRelease, normaliseVersion } from './version.js';
|
|
17
|
+
/**
|
|
18
|
+
* The OS keys `gjsify.ship.app` may carry — {@link HostOs}, the
|
|
19
|
+
* `process.platform` spelling.
|
|
20
|
+
*
|
|
21
|
+
* `satisfies Record<HostOs, true>` rather than a bare list: a fourth layout
|
|
22
|
+
* cannot arrive without this object failing to compile, so the accepted set
|
|
23
|
+
* stays total without a second place to remember to update.
|
|
24
|
+
*/
|
|
25
|
+
const SHIP_APP_OSES = { linux: true, darwin: true, win32: true };
|
|
26
|
+
/**
|
|
27
|
+
* The ONE place a ship target's runtime is decided: `gjsify.app` as the DEFAULT,
|
|
28
|
+
* `gjsify.ship.app.<os>` as this target's override.
|
|
29
|
+
*
|
|
30
|
+
* WHY IT IS PER TARGET, measured on a GTK4 application rather than derived from
|
|
31
|
+
* the schema: with one field, `gjsify ship darwin` refused its formats until
|
|
32
|
+
* `gjsify.app` was `"node"` — and setting it turned the LINUX `.deb`'s
|
|
33
|
+
* `Depends: gjs (>= 1.86)` into `Depends: nodejs (>= 24)`, which apt refuses on
|
|
34
|
+
* trixie, Ubuntu 24.04 and Ubuntu 26.04. A macOS decision made the Linux package
|
|
35
|
+
* uninstallable, because every generator read the PROJECT field instead of the
|
|
36
|
+
* resolved runtime of its OWN target (#1486, ADR 0024 § A22). Nothing downstream
|
|
37
|
+
* reads either key; they all read `settings.app`, which is this result.
|
|
38
|
+
*
|
|
39
|
+
* NOT PER LAYOUT BY DEFAULT, and the first cut of the layout axis got this
|
|
40
|
+
* exactly backwards. Reading ADR § 4's runtime table as a per-layout
|
|
41
|
+
* REQUIREMENT refused `gjsify.app: "gjs"` for the macOS layout — the entire
|
|
42
|
+
* audience of that command — while a project with no `gjsify.app` key sailed
|
|
43
|
+
* through and staged a launcher naming `node` in front of a GJS bundle. § 4
|
|
44
|
+
* derives the runtime a shipped artifact CARRIES, which is
|
|
45
|
+
* `Layout.shippedRuntime`; what an artifact runs is what its author says, which
|
|
46
|
+
* is this. So an absent override means the project field and never the layout's
|
|
47
|
+
* derived answer.
|
|
48
|
+
*
|
|
49
|
+
* MUST NOT be fed `Config.forBuild`'s resolution, whose fallback is the HOST
|
|
50
|
+
* runtime. That fallback is right for a build (`gjsify build` under Node should
|
|
51
|
+
* produce a Node bundle by default) and catastrophic for a package: running this
|
|
52
|
+
* command under Node would silently turn every undeclared GJS project into one
|
|
53
|
+
* whose launcher execs `node`. An undeclared target is the common case for a GJS
|
|
54
|
+
* app, and it means GJS.
|
|
55
|
+
*
|
|
56
|
+
* TWO REFUSALS, and each is here because the alternative to refusing is SILENT.
|
|
57
|
+
* A `browser` or `nativescript` value has no artifact to be — no process to
|
|
58
|
+
* start, an APK/IPA through another pipeline (§ 4) — and would otherwise leave
|
|
59
|
+
* here as a `.deb` a stranger installs and cannot run, with no gate between here
|
|
60
|
+
* and their machine; `Layout.runtimeGap` only WARNS because what it describes is
|
|
61
|
+
* a staged tree rather than an artifact, and the per-FORMAT half of the question
|
|
62
|
+
* is `assertFormatCanRunInterpreter`, which asks what a container can EXECUTE.
|
|
63
|
+
* A key outside {@link SHIP_APP_OSES} — `windows`, the `gjsify ship <os>`
|
|
64
|
+
* POSITIONAL's spelling — would resolve to nothing and leave that target on the
|
|
65
|
+
* project-wide answer with every gate green, which is why the author who typed
|
|
66
|
+
* it reads the result as gjsify not supporting the split at all. Both refusals
|
|
67
|
+
* name the KEY that carried the value: with two keys, "this project declares" no
|
|
68
|
+
* longer locates it.
|
|
69
|
+
*
|
|
70
|
+
* `gjsify ship --from-stage` reads neither key and needs neither: the stage
|
|
71
|
+
* manifest records the RESOLVED value, so the packing host reads an answer
|
|
72
|
+
* rather than a default, and `assertLauncherMatchesInterpreter` still compares
|
|
73
|
+
* the staged `bin/<name>`'s own `exec` line against the dependency about to be
|
|
74
|
+
* written.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveShipApp(input) {
|
|
77
|
+
const perTarget = input.perTarget ?? {};
|
|
78
|
+
// The WHOLE table, not only this run's key: a mis-keyed override is silent by
|
|
79
|
+
// construction, so `gjsify ship linux` is the run that has to say `windows` is
|
|
80
|
+
// not a key. Checking only the resolving key would leave the typo to be found
|
|
81
|
+
// by the `gjsify ship windows` that already reads as unsupported.
|
|
82
|
+
for (const os of Object.keys(perTarget)) {
|
|
83
|
+
if (Object.hasOwn(SHIP_APP_OSES, os))
|
|
84
|
+
continue;
|
|
85
|
+
throw new Error(`gjsify ship: \`gjsify.ship.app.${os}\` is not an OS this command assembles for. Known: ` +
|
|
86
|
+
`${Object.keys(SHIP_APP_OSES).join(', ')} — the \`process.platform\` spelling, not the ` +
|
|
87
|
+
"`gjsify ship <os>` positional's. A runtime under a key nothing reads leaves that target on " +
|
|
88
|
+
'`gjsify.app` with nothing to say so.');
|
|
89
|
+
}
|
|
90
|
+
const override = perTarget[input.layoutOs];
|
|
91
|
+
const overridden = override !== undefined;
|
|
92
|
+
const key = overridden ? `gjsify.ship.app.${input.layoutOs}` : 'gjsify.app';
|
|
93
|
+
const declared = overridden ? override : input.project;
|
|
94
|
+
if (declared !== undefined && declared !== 'gjs' && declared !== 'node') {
|
|
95
|
+
throw new Error(`gjsify ship: \`${key}\` is "${declared}", and only \`gjs\` and \`node\` can be ` +
|
|
96
|
+
'packaged. A browser bundle has no process to launch, and a NativeScript app ships as an APK/IPA ' +
|
|
97
|
+
'through a different pipeline. ADR 0024 § 4 has the runtime-per-OS table.');
|
|
98
|
+
}
|
|
99
|
+
return { app: declared === 'node' ? 'node' : 'gjs', key, overridden };
|
|
100
|
+
}
|
|
17
101
|
/** Freedesktop main category → deb `Section:` / rpm `Group:`. First match wins. */
|
|
18
102
|
const CATEGORY_SECTIONS = [
|
|
19
103
|
['Development', { section: 'devel', group: 'Development/Tools' }],
|
|
@@ -49,14 +133,10 @@ export function resolveShipSettings(input) {
|
|
|
49
133
|
const appId = ship.appId ?? flatpak.appId ?? reverseDnsOrThrow(pkg.name, binaryName);
|
|
50
134
|
const name = metadata.name ?? titleCase(binaryName);
|
|
51
135
|
const kind = metadata.kind ?? 'app';
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
// command under Node would silently turn every undeclared GJS project into
|
|
57
|
-
// one whose launcher execs `node`. An undeclared target is the common case
|
|
58
|
-
// for a GJS app, and it means GJS.
|
|
59
|
-
const app = input.app === 'node' ? 'node' : 'gjs';
|
|
136
|
+
// NOT resolved here: `resolveShipApp` already did, before the format list was
|
|
137
|
+
// decided from it. A second defaulting rule in this function would be the
|
|
138
|
+
// second path those two answers could come apart on.
|
|
139
|
+
const app = input.app;
|
|
60
140
|
const rawVersion = ship.version ?? pkg.version;
|
|
61
141
|
if (!rawVersion) {
|
|
62
142
|
throw new Error('gjsify ship: no version — set `version` in package.json or `gjsify.ship.version`.');
|
|
@@ -110,6 +190,7 @@ export function resolveShipSettings(input) {
|
|
|
110
190
|
schemaFiles: discovered.schemaFiles,
|
|
111
191
|
typelibFiles: discovered.typelibFiles,
|
|
112
192
|
localeFiles: discovered.localeFiles,
|
|
193
|
+
fontFiles: discovered.fontFiles,
|
|
113
194
|
extraFiles: ship.extraFiles ?? {},
|
|
114
195
|
execArgs: ship.execArgs ?? [],
|
|
115
196
|
outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
|
|
@@ -18,6 +18,36 @@ export declare const SHARE: {
|
|
|
18
18
|
readonly metainfo: "share/metainfo";
|
|
19
19
|
/** Compiled gettext catalogues, read directly — no install step. */
|
|
20
20
|
readonly locale: "share/locale";
|
|
21
|
+
/**
|
|
22
|
+
* Font faces the application ships — ONE directory, and three different things
|
|
23
|
+
* reach it (ADR 0038). No install step on any of them.
|
|
24
|
+
*
|
|
25
|
+
* On LINUX fontconfig scans it, by two rules that are both the stock
|
|
26
|
+
* `fonts.conf`'s rather than ours: `<dir>/usr/share/fonts</dir>` unconditionally,
|
|
27
|
+
* which covers a `.deb`/`.rpm`, and `<dir prefix="xdg">fonts</dir>`, which
|
|
28
|
+
* fontconfig expands over `XDG_DATA_DIRS` as well as `XDG_DATA_HOME` — so the
|
|
29
|
+
* variable the launcher already exports covers `/app` in a Flatpak and any other
|
|
30
|
+
* prefix. `fonts-conf(5)` documents only `XDG_DATA_HOME` there; the expansion was
|
|
31
|
+
* measured in EIGHT independently built fontconfigs (2.14.1 through 2.18.3), in
|
|
32
|
+
* every list position, recursively, with a cold cache and a negative control.
|
|
33
|
+
* Behaviour, not documentation — which is why it is written here and in ADR 0038
|
|
34
|
+
* § *What was measured* rather than re-derived from the man page.
|
|
35
|
+
*
|
|
36
|
+
* On MACOS none of that applies and the directory is the same: Pango there is
|
|
37
|
+
* CoreText-backed and GTK is not built against fontconfig, so `Info.plist`'s
|
|
38
|
+
* `ATSApplicationFontsPath` points at this directory instead (`layout.ts`).
|
|
39
|
+
*
|
|
40
|
+
* On WINDOWS nothing reaches it by configuration at all — measured there, not
|
|
41
|
+
* inferred: the default font map does not read fontconfig even when the staged
|
|
42
|
+
* directory is the ONLY configuration it is given, while `add_font_file` on that
|
|
43
|
+
* same map does register the family. So the launcher's `GJSIFY_FONT_DIR` hands
|
|
44
|
+
* the directory to the app to register. `Layout.fontGap` carries the numbers and
|
|
45
|
+
* the three candidates that look like they would work and do not.
|
|
46
|
+
*
|
|
47
|
+
* That the PATH is one while the mechanisms are three is the point: `planStage`
|
|
48
|
+
* keeps emitting one payload, and every per-OS answer is a layout decision.
|
|
49
|
+
*/
|
|
50
|
+
readonly fonts: "share/fonts";
|
|
21
51
|
};
|
|
22
52
|
/** Is `path` inside `dir`? Directory-boundary aware, so `share/mimetypes` is not inside `share/mime`. */
|
|
23
53
|
export declare function isUnder(path: string, dir: string): boolean;
|
|
@@ -39,6 +39,36 @@ export const SHARE = {
|
|
|
39
39
|
metainfo: 'share/metainfo',
|
|
40
40
|
/** Compiled gettext catalogues, read directly — no install step. */
|
|
41
41
|
locale: 'share/locale',
|
|
42
|
+
/**
|
|
43
|
+
* Font faces the application ships — ONE directory, and three different things
|
|
44
|
+
* reach it (ADR 0038). No install step on any of them.
|
|
45
|
+
*
|
|
46
|
+
* On LINUX fontconfig scans it, by two rules that are both the stock
|
|
47
|
+
* `fonts.conf`'s rather than ours: `<dir>/usr/share/fonts</dir>` unconditionally,
|
|
48
|
+
* which covers a `.deb`/`.rpm`, and `<dir prefix="xdg">fonts</dir>`, which
|
|
49
|
+
* fontconfig expands over `XDG_DATA_DIRS` as well as `XDG_DATA_HOME` — so the
|
|
50
|
+
* variable the launcher already exports covers `/app` in a Flatpak and any other
|
|
51
|
+
* prefix. `fonts-conf(5)` documents only `XDG_DATA_HOME` there; the expansion was
|
|
52
|
+
* measured in EIGHT independently built fontconfigs (2.14.1 through 2.18.3), in
|
|
53
|
+
* every list position, recursively, with a cold cache and a negative control.
|
|
54
|
+
* Behaviour, not documentation — which is why it is written here and in ADR 0038
|
|
55
|
+
* § *What was measured* rather than re-derived from the man page.
|
|
56
|
+
*
|
|
57
|
+
* On MACOS none of that applies and the directory is the same: Pango there is
|
|
58
|
+
* CoreText-backed and GTK is not built against fontconfig, so `Info.plist`'s
|
|
59
|
+
* `ATSApplicationFontsPath` points at this directory instead (`layout.ts`).
|
|
60
|
+
*
|
|
61
|
+
* On WINDOWS nothing reaches it by configuration at all — measured there, not
|
|
62
|
+
* inferred: the default font map does not read fontconfig even when the staged
|
|
63
|
+
* directory is the ONLY configuration it is given, while `add_font_file` on that
|
|
64
|
+
* same map does register the family. So the launcher's `GJSIFY_FONT_DIR` hands
|
|
65
|
+
* the directory to the app to register. `Layout.fontGap` carries the numbers and
|
|
66
|
+
* the three candidates that look like they would work and do not.
|
|
67
|
+
*
|
|
68
|
+
* That the PATH is one while the mechanisms are three is the point: `planStage`
|
|
69
|
+
* keeps emitting one payload, and every per-OS answer is a layout decision.
|
|
70
|
+
*/
|
|
71
|
+
fonts: 'share/fonts',
|
|
42
72
|
};
|
|
43
73
|
/** Is `path` inside `dir`? Directory-boundary aware, so `share/mimetypes` is not inside `share/mime`. */
|
|
44
74
|
export function isUnder(path, dir) {
|
|
@@ -28,7 +28,34 @@ export interface SignerDescriptor {
|
|
|
28
28
|
*/
|
|
29
29
|
signs: 'macho' | 'pe';
|
|
30
30
|
/** argv after {@link tool}, for ONE file. */
|
|
31
|
-
args: (
|
|
31
|
+
args: (input: SignArgs) => readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* The entitlements this signer asks for, or `[]` where the OS has none.
|
|
34
|
+
*
|
|
35
|
+
* `[]` is an ANSWER and not an omission — the same rule `Layout.metadata`
|
|
36
|
+
* states. Windows has no equivalent concept at all, and a signer that granted
|
|
37
|
+
* nothing on macOS would be a different decision from one that has nothing to
|
|
38
|
+
* grant.
|
|
39
|
+
*/
|
|
40
|
+
entitlements: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Does this signer seal the ARTIFACT ROOT after signing the images inside it?
|
|
43
|
+
*
|
|
44
|
+
* Only true where the OS has a bundle format, which is darwin alone. The root
|
|
45
|
+
* itself is not on the descriptor because it belongs to the LAYOUT
|
|
46
|
+
* (`Layout.root`), and a second copy here is where the two would come apart.
|
|
47
|
+
*/
|
|
48
|
+
sealsBundle: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Payload paths a seal is allowed to ADD, given the artifact root.
|
|
51
|
+
*
|
|
52
|
+
* A PREFIX rather than a free hand: `codesign` on a bundle writes
|
|
53
|
+
* `Contents/_CodeSignature/{CodeResources,CodeDirectory,CodeRequirements,
|
|
54
|
+
* CodeRequirements-1,CodeSignature}` and nothing else, so anything appearing
|
|
55
|
+
* outside that directory is something else having run — which is exactly the
|
|
56
|
+
* class {@link signPayload}'s file-set check exists to catch.
|
|
57
|
+
*/
|
|
58
|
+
sealAddPrefix: (bundleRoot: string) => string;
|
|
32
59
|
/** The config key a project spells its default identity under. */
|
|
33
60
|
configKey: string;
|
|
34
61
|
/** What a signature from this signer does NOT yet cover — printed once per run. */
|
|
@@ -91,6 +118,42 @@ export declare function resolveSignPlan(input: {
|
|
|
91
118
|
* knowable before anything is packed.
|
|
92
119
|
*/
|
|
93
120
|
export declare function assertHostCanSign(signer: SignerDescriptor): void;
|
|
121
|
+
/**
|
|
122
|
+
* What a signing run did to the payload's FILE SET, split three ways.
|
|
123
|
+
*
|
|
124
|
+
* PURE, and extracted from {@link signPayload} for one reason: it is the whole of
|
|
125
|
+
* the seal decision and the only half of it that can be checked without a macOS
|
|
126
|
+
* host. Everything else about a bundle seal needs `codesign`, which this
|
|
127
|
+
* repository has on exactly one CI leg; this function needs nothing.
|
|
128
|
+
*
|
|
129
|
+
* `sealPrefix` absent means "this signer seals nothing", and then EVERY addition
|
|
130
|
+
* is unexpected — which is what the file did before ADR 0040 and is still the
|
|
131
|
+
* right answer for `signtool`.
|
|
132
|
+
*/
|
|
133
|
+
export declare function partitionSignedFileSet(input: {
|
|
134
|
+
/** Paths found under the scratch directory after the tool ran. */
|
|
135
|
+
arrived: readonly string[];
|
|
136
|
+
/** Paths the payload had before it. */
|
|
137
|
+
planned: readonly string[];
|
|
138
|
+
/** Directory prefix the seal is allowed to write into, when there is one. */
|
|
139
|
+
sealPrefix?: string;
|
|
140
|
+
}): {
|
|
141
|
+
sealed: string[];
|
|
142
|
+
unexpected: string[];
|
|
143
|
+
removed: string[];
|
|
144
|
+
};
|
|
145
|
+
/** What one invocation of a signing tool is given. */
|
|
146
|
+
export interface SignArgs {
|
|
147
|
+
identity: string;
|
|
148
|
+
/** Absolute path of the file — or, for a bundle seal, of the bundle DIRECTORY. */
|
|
149
|
+
file: string;
|
|
150
|
+
/** Absolute path of the entitlements plist this run wrote, or absent when the signer grants none. */
|
|
151
|
+
entitlements?: string;
|
|
152
|
+
/** Is {@link identity} the reserved ad-hoc `-`? */
|
|
153
|
+
adhoc: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** `codesign --sign -`: the one identity value that names no identity at all. */
|
|
156
|
+
export declare const ADHOC_IDENTITY = "-";
|
|
94
157
|
export interface SignPayloadInput {
|
|
95
158
|
/** What `readStage` returned — the PRE-sign tree, already held against the manifest. */
|
|
96
159
|
payload: readonly PayloadEntry[];
|
|
@@ -98,6 +161,22 @@ export interface SignPayloadInput {
|
|
|
98
161
|
signer: SignerDescriptor;
|
|
99
162
|
/** A scratch directory this function owns and WIPES. Never the arriving stage. */
|
|
100
163
|
workDir: string;
|
|
164
|
+
/**
|
|
165
|
+
* `Layout.root` — the payload-relative directory that IS the artifact, or `''`.
|
|
166
|
+
*
|
|
167
|
+
* Read only by a signer that seals: it is what `codesign` is pointed at, and
|
|
168
|
+
* it comes from the layout rather than from this table because the layout is
|
|
169
|
+
* what decided the `<App>.app` name in the first place.
|
|
170
|
+
*/
|
|
171
|
+
bundleRoot: string;
|
|
172
|
+
/**
|
|
173
|
+
* Where to write the entitlements plist, OUTSIDE {@link workDir}.
|
|
174
|
+
*
|
|
175
|
+
* Outside is not a preference: the file-set check below lists `workDir` and
|
|
176
|
+
* compares it with the payload, so an entitlements file written inside would
|
|
177
|
+
* read as a file the signer added.
|
|
178
|
+
*/
|
|
179
|
+
entitlementsPath: string;
|
|
101
180
|
/** Print every invocation. */
|
|
102
181
|
verbose: boolean;
|
|
103
182
|
log: (line: string) => void;
|
|
@@ -112,11 +191,16 @@ export interface SignPayloadInput {
|
|
|
112
191
|
* same tree fail `readStage`'s size comparison (measured — see the header), and
|
|
113
192
|
* `writeStage`'s wipe would delete the payload it was about to pack.
|
|
114
193
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
194
|
+
* TWO STEPS ON DARWIN, ONE EVERYWHERE ELSE: every Mach-O in the closure, then the
|
|
195
|
+
* bundle itself. The second is what `codesign --verify <App>.app` reads, and it
|
|
196
|
+
* writes five regular files into `Contents/_CodeSignature/` that the returned
|
|
197
|
+
* payload carries — so the seal reaches the `.zip` and the `.dmg` alike, which is
|
|
198
|
+
* the correction ADR 0040 makes to this file's previous claim that it could not.
|
|
199
|
+
*
|
|
200
|
+
* THE FILE SET MAY ONLY GROW WHERE THE SEAL WRITES, and that is asserted rather
|
|
201
|
+
* than assumed. Both tools rewrite an image in place, so a payload that gained a
|
|
202
|
+
* path outside `sealAddPrefix` — or lost one anywhere — means something else ran,
|
|
203
|
+
* and its files would be packed with no mode from the plan.
|
|
120
204
|
*/
|
|
121
205
|
export declare function signPayload(input: SignPayloadInput): Promise<PayloadEntry[]>;
|
|
122
206
|
/**
|
|
@@ -193,3 +277,35 @@ export declare function notarizeArtifact(input: {
|
|
|
193
277
|
verbose: boolean;
|
|
194
278
|
log: (line: string) => void;
|
|
195
279
|
}): Promise<void>;
|
|
280
|
+
/**
|
|
281
|
+
* Attach the notarisation ticket to an artifact that can hold one.
|
|
282
|
+
*
|
|
283
|
+
* WHY IT IS A SEPARATE CALL AND NOT THE TAIL OF {@link notarizeArtifact}: not
|
|
284
|
+
* every artifact the notary service ACCEPTS can carry a ticket, and the pair that
|
|
285
|
+
* comes apart is exactly the one this command produces. Apple:
|
|
286
|
+
* *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
287
|
+
* Instead, run stapler against each item that you added to the archive. Then
|
|
288
|
+
* create a new ZIP file containing the stapled items for distribution."*
|
|
289
|
+
* `stapler(1)` gives the accepting side of the same list: *"stapler works only
|
|
290
|
+
* with UDIF disk images, signed \"flat\" installer packages, and certain
|
|
291
|
+
* code-signed executable bundles such as \".app\"."* So the `.dmg` is stapled
|
|
292
|
+
* here and the `.zip` is told, in the artifact log, what a person has to do
|
|
293
|
+
* instead — which is a fact about the CONTAINER and therefore lives on the format
|
|
294
|
+
* (`canCarryTicket`), not in a branch here.
|
|
295
|
+
*
|
|
296
|
+
* ⚠️ UNVERIFIED END TO END, for the same reason `notarizeArtifact` is: stapling
|
|
297
|
+
* needs a real notarisation, which needs an Apple account (ADR 0024 § A15, § A17).
|
|
298
|
+
* The argv is unit-tested and the invocation has never run in this repository.
|
|
299
|
+
*
|
|
300
|
+
* `stapler(1)` also states the constraint that decides the ORDER: *"Stapling does
|
|
301
|
+
* not invalidate the code signature and must be run after an executable or archive
|
|
302
|
+
* has been code-signed and notarized … Code-signing a supported file format
|
|
303
|
+
* invalidates any stapled tickets"* — so nothing may re-sign after this, and
|
|
304
|
+
* nothing in `packOne` does.
|
|
305
|
+
*/
|
|
306
|
+
export declare function stapleArtifact(input: {
|
|
307
|
+
notary: NotaryDescriptor;
|
|
308
|
+
artifact: string;
|
|
309
|
+
verbose: boolean;
|
|
310
|
+
log: (line: string) => void;
|
|
311
|
+
}): Promise<void>;
|