@gjsify/cli 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.gjs.mjs +144 -135
- package/lib/commands/onboard.js +11 -0
- package/lib/commands/publish.d.ts +41 -0
- package/lib/commands/publish.js +222 -2
- package/lib/commands/ship.js +89 -76
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +64 -0
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/publish-readback.d.ts +132 -0
- package/lib/utils/publish-readback.js +273 -0
- package/lib/utils/ship/discover.js +85 -0
- package/lib/utils/ship/formats.d.ts +23 -0
- package/lib/utils/ship/formats.js +44 -0
- package/lib/utils/ship/launcher.js +57 -0
- package/lib/utils/ship/layout.d.ts +64 -2
- package/lib/utils/ship/layout.js +169 -12
- package/lib/utils/ship/msi.js +24 -7
- package/lib/utils/ship/payload.js +22 -3
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.js +35 -0
- package/lib/utils/ship/plist.d.ts +41 -1
- package/lib/utils/ship/plist.js +59 -1
- package/lib/utils/ship/rpm.js +7 -0
- package/lib/utils/ship/settings.d.ts +76 -5
- package/lib/utils/ship/settings.js +89 -8
- package/lib/utils/ship/share-dirs.d.ts +30 -0
- package/lib/utils/ship/share-dirs.js +30 -0
- package/lib/utils/ship/signing.d.ts +122 -6
- package/lib/utils/ship/signing.js +256 -67
- package/lib/utils/ship/stage-writer.js +20 -1
- package/lib/utils/ship/types.d.ts +34 -2
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/package.json +19 -19
package/lib/commands/ship.js
CHANGED
|
@@ -35,19 +35,20 @@ import { discoverPayload } from '../utils/ship/discover.js';
|
|
|
35
35
|
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
36
36
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
37
37
|
import { localizeMetadata } from '../utils/ship/localize-metadata.js';
|
|
38
|
-
import { assertHostCanFinish, assertToolsInstalled, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
|
|
38
|
+
import { assertHostCanFinish, assertToolsInstalled, canCarryTicket, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
|
|
39
39
|
import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout, LAYOUT_NAMES, } from '../utils/ship/layout.js';
|
|
40
40
|
import { isNodeRuntimeTarget, resolveNodeRuntime } from '../utils/ship/node-runtime.js';
|
|
41
|
+
import { isUnder, SHARE } from '../utils/ship/share-dirs.js';
|
|
41
42
|
import { compileSchemasForStage } from '../utils/ship/schemas.js';
|
|
42
43
|
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
43
44
|
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
44
45
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
45
46
|
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
46
47
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
47
|
-
import { assertHostCanSign, notarizeArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
48
|
+
import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
48
49
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
49
50
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
50
|
-
import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
|
|
51
|
+
import { declaredBundlePath, resolveShipApp, resolveShipSettings, } from '../utils/ship/settings.js';
|
|
51
52
|
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
|
|
52
53
|
import { directorySize, readStage, writePayload, writeStage } from '../utils/ship/stage-writer.js';
|
|
53
54
|
const LOG = '[gjsify ship]';
|
|
@@ -177,13 +178,21 @@ async function assemble(args) {
|
|
|
177
178
|
const configData = await config.forCommand(projectDir);
|
|
178
179
|
const ship = configData.ship ?? {};
|
|
179
180
|
const flatpak = configData.flatpak ?? {};
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
const
|
|
181
|
+
// THE ONE RESOLUTION, and it runs HERE because the format list is decided
|
|
182
|
+
// before anything is built while `resolveShipSettings` runs after. Its result
|
|
183
|
+
// is threaded into the settings rather than re-derived there: two defaulting
|
|
184
|
+
// rules for one question is how the format list and the emitted `Depends:`
|
|
185
|
+
// came to answer for different targets (#1486). Why the runtime is per target
|
|
186
|
+
// at all, and why an absent override means the project field: `resolveShipApp`.
|
|
187
|
+
const resolvedApp = resolveShipApp({ project: configData.app, perTarget: ship.app, layoutOs: layout.os });
|
|
188
|
+
const interpreter = resolvedApp.app;
|
|
189
|
+
// SAID, not swallowed, like every other narrowing in this function. An
|
|
190
|
+
// override that changed the launcher, the dependency and the format list
|
|
191
|
+
// without a line of output is one nobody can tell from the project default.
|
|
192
|
+
if (resolvedApp.overridden) {
|
|
193
|
+
console.log(`${LOG} runtime for ${layout.name}: ${interpreter} — \`${resolvedApp.key}\` overrides ` +
|
|
194
|
+
`\`gjsify.app${configData.app === undefined ? '' : `: "${configData.app}"`}\`.`);
|
|
195
|
+
}
|
|
187
196
|
const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
|
|
188
197
|
// Resolved before anything is built or written: a typo'd `--target`
|
|
189
198
|
// should not cost a build and leave a half-written `ship/stage/`.
|
|
@@ -233,8 +242,11 @@ async function assemble(args) {
|
|
|
233
242
|
// and the two need different next steps.
|
|
234
243
|
if (unusable.length > 0) {
|
|
235
244
|
console.log(`${LOG} ${unusable.map((format) => format.id).join(', ')} wrap the ${layout.name} layout but ` +
|
|
236
|
-
`cannot run
|
|
237
|
-
'
|
|
245
|
+
`cannot run \`${interpreter}\`, which \`${resolvedApp.key}\` resolves to — ` +
|
|
246
|
+
`${unusable[0]?.interpreterGap ?? ''}. Set \`gjsify.ship.app.${layout.os}\` to ` +
|
|
247
|
+
`"${unusable[0]?.interpreters.join('" / "')}" to ship this OS on that runtime; it leaves every ` +
|
|
248
|
+
'other target on `gjsify.app`. Assembling the layout only; pass --target to be told the same ' +
|
|
249
|
+
'thing as an error.');
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
252
|
if (!args.stage)
|
|
@@ -282,7 +294,11 @@ async function assemble(args) {
|
|
|
282
294
|
flatpak,
|
|
283
295
|
cli: { outDir: args.out, arch: args.arch, layoutOs: layout.os },
|
|
284
296
|
discovered,
|
|
285
|
-
|
|
297
|
+
// The RESOLVED runtime, never `configData.app`. Everything downstream —
|
|
298
|
+
// the launcher, `deriveDepends`, the carried runtime, the stage manifest —
|
|
299
|
+
// reads `settings.app`, so this is the one hand-off that decides whether
|
|
300
|
+
// those generators speak about this target or about the project.
|
|
301
|
+
app: interpreter,
|
|
286
302
|
});
|
|
287
303
|
for (const warning of warnings)
|
|
288
304
|
console.warn(`${LOG} ${warning}`);
|
|
@@ -420,6 +436,17 @@ async function assemble(args) {
|
|
|
420
436
|
if (runtime.launcher.interpreter === undefined && layout.runtimeGap !== undefined) {
|
|
421
437
|
console.warn(`${LOG} ${layout.runtimeGap}`);
|
|
422
438
|
}
|
|
439
|
+
// The same shape one axis over (ADR 0038). `share/fonts` is NOT in the list
|
|
440
|
+
// above — it needs no install step on any layout, so `linuxInstallDependent`
|
|
441
|
+
// classifies it as portable — but "the directory is reached" and "this OS's
|
|
442
|
+
// Pango reads that directory at all" are different questions, and only the
|
|
443
|
+
// first is settled by the payload. CONDITIONAL on the payload actually
|
|
444
|
+
// carrying a face, like `runtimeGap` is on the missing interpreter: telling a
|
|
445
|
+
// project that ships no font which fontconfig its Mac would consult is noise,
|
|
446
|
+
// and noise is what trains people to stop reading these lines.
|
|
447
|
+
if (layout.fontGap !== undefined && planned.some((file) => isUnder(file.path, SHARE.fonts))) {
|
|
448
|
+
console.warn(`${LOG} ${layout.fontGap}`);
|
|
449
|
+
}
|
|
423
450
|
// THE OTHER AXIS, warned about HERE and refused in `packOne`, and the split
|
|
424
451
|
// is the same one `runtimeGap` and `assertFormatCanRunInterpreter` already
|
|
425
452
|
// draw. A stage is a build intermediate, and assembling one for an
|
|
@@ -517,7 +544,11 @@ async function assemble(args) {
|
|
|
517
544
|
// none; or the configured `targets` all wrap another one, which the branch
|
|
518
545
|
// above has already SAID in its own words rather than repeating here.
|
|
519
546
|
const none = unusable.length > 0
|
|
520
|
-
?
|
|
547
|
+
? // The key that fixes it, which used to be `gjsify.app` and is now
|
|
548
|
+
// this target's own: naming the project field here is how a macOS
|
|
549
|
+
// stage came to move the Linux `Depends:` (#1486).
|
|
550
|
+
`(none — ${unusable.map((format) => format.id).join(' and ')} need ` +
|
|
551
|
+
`\`gjsify.ship.app.${layout.os}: "node"\`)`
|
|
521
552
|
: formatIdsFor(layout.os).length === 0
|
|
522
553
|
? `(none — no format wraps the ${layout.name} layout)`
|
|
523
554
|
: '(none asked for)';
|
|
@@ -842,6 +873,13 @@ async function packOne(input) {
|
|
|
842
873
|
identity: input.sign.identity,
|
|
843
874
|
signer: input.sign.signer,
|
|
844
875
|
workDir: join(outRoot, 'signed', format.id),
|
|
876
|
+
// What the darwin signer SEALS, from the layout that named it —
|
|
877
|
+
// `<App>.app` there, `''` on the two layouts with no bundle.
|
|
878
|
+
bundleRoot: layout.root(settings),
|
|
879
|
+
// OUTSIDE `signed/<id>/`: that directory is listed and compared
|
|
880
|
+
// with the payload, so an entitlements plist written into it would
|
|
881
|
+
// read as a file `codesign` added.
|
|
882
|
+
entitlementsPath: join(outRoot, 'signed', `${format.id}.entitlements.plist`),
|
|
845
883
|
verbose: input.verbose,
|
|
846
884
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
847
885
|
})
|
|
@@ -1030,6 +1068,27 @@ async function packOne(input) {
|
|
|
1030
1068
|
verbose: input.verbose,
|
|
1031
1069
|
log: (line) => console.log(`${LOG} ${line}`),
|
|
1032
1070
|
});
|
|
1071
|
+
// AND THEN THE TICKET, on the artifacts that can hold one. Submitting
|
|
1072
|
+
// and stapling are two questions with two answers, and the `.zip` is
|
|
1073
|
+
// where they come apart: the notary service accepts it and `stapler`
|
|
1074
|
+
// refuses it. Without the ticket the artifact still installs — every
|
|
1075
|
+
// launch asks Gatekeeper online instead, and fails closed on a machine
|
|
1076
|
+
// with no network, which is the failure a user reads as "the app is
|
|
1077
|
+
// damaged".
|
|
1078
|
+
if (canCarryTicket(format.id)) {
|
|
1079
|
+
await stapleArtifact({
|
|
1080
|
+
notary: input.notarize.notary,
|
|
1081
|
+
artifact: target,
|
|
1082
|
+
verbose: input.verbose,
|
|
1083
|
+
log: (line) => console.log(`${LOG} ${line}`),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
console.warn(`${LOG} ${format.id} is notarised and NOT stapled: \`stapler\` takes a UDIF image, a ` +
|
|
1088
|
+
"signed flat package or a code-signed bundle, and an archive is none of them. Apple's " +
|
|
1089
|
+
'remedy is to staple each item that went INTO the archive and re-create it — for this ' +
|
|
1090
|
+
'artifact that is the `.app`, which `--target macos-app` writes beside it.');
|
|
1091
|
+
}
|
|
1033
1092
|
}
|
|
1034
1093
|
}
|
|
1035
1094
|
return { format: format.id, path: target, size };
|
|
@@ -1078,8 +1137,10 @@ function assertPackable(formats, layout, chosenBy, unusable = []) {
|
|
|
1078
1137
|
if (unusable.length > 0) {
|
|
1079
1138
|
throw new Error(`gjsify ship: ${unusable.map((format) => format.id).join(' and ')} wrap the ${layout.name} ` +
|
|
1080
1139
|
`layout, and neither can run this project — ${unusable[0]?.interpreterGap ?? ''}.\n` +
|
|
1081
|
-
` \`gjsify
|
|
1082
|
-
'
|
|
1140
|
+
` Set \`gjsify.ship.app.${layout.os}\` to ` +
|
|
1141
|
+
`"${unusable[0]?.interpreters.join('" / "')}" — it overrides \`gjsify.app\` for THIS target and\n` +
|
|
1142
|
+
' leaves every other one alone, which is what lets one project be GJS on Linux and Node here.\n' +
|
|
1143
|
+
` \`gjsify ship ${layout.name} --stage\` assembles the tree either way.`);
|
|
1083
1144
|
}
|
|
1084
1145
|
// A THIRD emptiness, and the one the last throw in this function used to
|
|
1085
1146
|
// mis-diagnose. A layout that HAS formats still reaches here with none
|
|
@@ -1136,66 +1197,13 @@ function assertPackable(formats, layout, chosenBy, unusable = []) {
|
|
|
1136
1197
|
' Every layout this gjsify knows has one (ADR 0024 stages 2-5), so this is a layout added ' +
|
|
1137
1198
|
'without a `FORMATS` row.');
|
|
1138
1199
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Refuse a build target this command cannot package correctly.
|
|
1141
|
-
*
|
|
1142
|
-
* `gjs` and `node` are packageable: `renderLauncher` execs the one
|
|
1143
|
-
* `settings.app` names and `deriveDepends` declares the same one — `gjs >= …`,
|
|
1144
|
-
* or `nodejs (>= 24)` / `nodejs(engine) >= 24`. Until #1354's M0 this said "only
|
|
1145
|
-
* `gjs` can be packaged today", which was true: the launcher execed gjs
|
|
1146
|
-
* unconditionally, so a `--app node` package would have installed and died at
|
|
1147
|
-
* startup.
|
|
1148
|
-
*
|
|
1149
|
-
* `browser` and `nativescript` stay refused, and not for want of a launcher
|
|
1150
|
-
* line: a browser bundle has no process to start, and NativeScript ships as an
|
|
1151
|
-
* APK/IPA through a different pipeline entirely (ADR 0024 § 4).
|
|
1152
|
-
*
|
|
1153
|
-
* DELIBERATELY NOT PER LAYOUT, and the first cut of the layout axis got this
|
|
1154
|
-
* exactly backwards. Reading ADR § 4's runtime table as a per-layout `app`
|
|
1155
|
-
* requirement refused `gjsify.app: "gjs"` for the macOS layout — i.e. refused the
|
|
1156
|
-
* entire audience of this command from assembling a `.app` — while a project with
|
|
1157
|
-
* no `gjsify.app` key sailed through and staged a launcher naming `node` in front
|
|
1158
|
-
* of a GJS bundle. Both halves came from the same mistake: § 4 derives the runtime
|
|
1159
|
-
* a SHIPPED ARTIFACT carries, which is `Layout.shippedRuntime` and is printed as
|
|
1160
|
-
* `Layout.runtimeGap`. What the launcher execs is what the PAYLOAD was built for,
|
|
1161
|
-
* and that is `settings.app` on every layout.
|
|
1162
|
-
*
|
|
1163
|
-
* An undeclared target is the common case for a GJS app and is allowed. It must
|
|
1164
|
-
* NOT be resolved through `Config.forBuild`, whose fallback is the HOST runtime
|
|
1165
|
-
* — that would refuse, or worse silently re-target, a perfectly good GJS project
|
|
1166
|
-
* merely for running this command under Node.
|
|
1167
|
-
*
|
|
1168
|
-
* WHY THIS REFUSES AND `Layout.runtimeGap` ONLY WARNS, since the two describe
|
|
1169
|
-
* neighbouring predicaments and get opposite treatment. A target this command
|
|
1170
|
-
* cannot package would leave here as an ARTIFACT — a `.deb` a stranger installs,
|
|
1171
|
-
* which then fails at startup, with no gate between here and their machine. The
|
|
1172
|
-
* `runtimeGap` warns about something that is not an artifact: it is printed over a
|
|
1173
|
-
* staged tree, and only when that tree carries no interpreter of its own.
|
|
1174
|
-
*
|
|
1175
|
-
* This paragraph used to end "the day a Windows format exists, that warning has to
|
|
1176
|
-
* become this refusal". #1354 M3 is that day, and the refusal it became is
|
|
1177
|
-
* {@link assertFormatCanRunInterpreter} — per FORMAT rather than per layout,
|
|
1178
|
-
* because what a runtime can execute is a property of the container and not of the
|
|
1179
|
-
* tree it wraps. This function stayed exactly as wide as it was.
|
|
1180
|
-
*
|
|
1181
|
-
* Phase one only. Phase two does not re-read `gjsify.app` — it has no project —
|
|
1182
|
-
* but it is not unchecked either: `assertLauncherMatchesInterpreter` compares
|
|
1183
|
-
* the staged `bin/<name>`'s own `exec` line against the dependency about to be
|
|
1184
|
-
* written.
|
|
1185
|
-
*/
|
|
1186
|
-
function assertShippableTarget(app) {
|
|
1187
|
-
if (app === undefined || app === 'gjs' || app === 'node')
|
|
1188
|
-
return;
|
|
1189
|
-
throw new Error(`gjsify ship: this project declares \`gjsify.app: "${app}"\`, and only \`gjs\` and \`node\` can be ` +
|
|
1190
|
-
'packaged. A browser bundle has no process to launch, and a NativeScript app ships as an APK/IPA ' +
|
|
1191
|
-
'through a different pipeline. ADR 0024 § 4 has the runtime-per-OS table.');
|
|
1192
|
-
}
|
|
1193
1200
|
/**
|
|
1194
1201
|
* Refuse a format whose runtime cannot provide the interpreter the launcher execs.
|
|
1195
1202
|
*
|
|
1196
1203
|
* THE HOLE THIS CLOSES, and it was opened by the commit that made `--app node`
|
|
1197
|
-
* packageable.
|
|
1198
|
-
*
|
|
1204
|
+
* packageable. The shippable-target refusal — `resolveShipApp` in
|
|
1205
|
+
* `utils/ship/settings.ts`, a free function in this file until #1486 — used to
|
|
1206
|
+
* refuse `app: 'node'` outright, so no format ever saw one. Lifting that made deb and rpm correct and left Flatpak
|
|
1199
1207
|
* silently wrong: measured at exit 0, a `--target flatpak --stage` with
|
|
1200
1208
|
* `app: 'node'` emitted a manifest with `runtime: org.gnome.Platform`,
|
|
1201
1209
|
* `runtime-version: 50`, no `sdk-extensions` and no `append-path`, beside a
|
|
@@ -1213,14 +1221,19 @@ function assertShippableTarget(app) {
|
|
|
1213
1221
|
function assertFormatCanRunInterpreter(format, app) {
|
|
1214
1222
|
if (format.interpreters.includes(app))
|
|
1215
1223
|
return;
|
|
1216
|
-
throw new Error(
|
|
1217
|
-
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
// The TARGET's runtime, not "this project is": since #1486 the two can
|
|
1226
|
+
// differ, and a message naming `gjsify.app` for a per-target answer sends
|
|
1227
|
+
// the reader to the key whose edit moves every OTHER target's `Depends:`.
|
|
1228
|
+
`gjsify ship: this ${format.layoutOs} target runs \`${app}\`, and the ${format.id} runtime cannot ` +
|
|
1229
|
+
`run it — it provides ${format.interpreters.join(', ')}.\n` +
|
|
1218
1230
|
// The ROW's own sentence, not one written here. Hardcoded, this
|
|
1219
1231
|
// paragraph told the first `.app` author about
|
|
1220
1232
|
// `org.freedesktop.Sdk.Extension.node2x` and the GNOME runtime.
|
|
1221
1233
|
` ${format.interpreterGap}.\n` +
|
|
1222
|
-
' Build the formats that wrap this layout and can run it, or
|
|
1223
|
-
`\`gjsify.app: "${format.interpreters.join('" / "')}"
|
|
1234
|
+
' Build the formats that wrap this layout and can run it, or set ' +
|
|
1235
|
+
`\`gjsify.ship.app.${format.layoutOs}: "${format.interpreters.join('" / "')}"\` ` +
|
|
1236
|
+
'(which overrides `gjsify.app` for this target alone).');
|
|
1224
1237
|
}
|
|
1225
1238
|
/**
|
|
1226
1239
|
* Run the project's own `build` script, as a CHILD process.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type DependencyGroup } from '../utils/dep-aggregation.js';
|
|
1
2
|
import type { Command } from '../types/index.js';
|
|
2
3
|
interface UpgradeOptions {
|
|
3
4
|
latest?: boolean;
|
|
@@ -8,10 +9,32 @@ interface UpgradeOptions {
|
|
|
8
9
|
excludeWorkspace?: string;
|
|
9
10
|
align?: boolean;
|
|
10
11
|
check?: boolean;
|
|
12
|
+
exact?: boolean;
|
|
11
13
|
dryRun?: boolean;
|
|
12
14
|
cwd?: string;
|
|
13
15
|
verbose?: boolean;
|
|
14
16
|
yes?: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare const upgradeCommand: Command<unknown, UpgradeOptions>;
|
|
19
|
+
/**
|
|
20
|
+
* `--exact` half of the CI gate: does every matched dep carry a bare version?
|
|
21
|
+
*
|
|
22
|
+
* Consistency and exactness are different questions, and the first cannot answer the
|
|
23
|
+
* second: a repository where every manifest agrees on `^4.1.0` is perfectly consistent
|
|
24
|
+
* and still lets a minor release move a subpath under a lockfile-less install. That is
|
|
25
|
+
* the hazard ADR 0029 § Risks 1 names for `@girs/*`, whose `./vocabulary` export is what
|
|
26
|
+
* `@gjsify/gtk-host` consumes — so the pin has to be the whole version, and something
|
|
27
|
+
* has to say so on every PR.
|
|
28
|
+
*
|
|
29
|
+
* Returns the number of offending declarations so the caller can exit once for both arms.
|
|
30
|
+
*/
|
|
31
|
+
export declare function reportInexactRanges(groups: readonly DependencyGroup[]): number;
|
|
32
|
+
/**
|
|
33
|
+
* The range a dependency will be WRITTEN at.
|
|
34
|
+
*
|
|
35
|
+
* Preview and write share this deliberately. They were two expressions for one decision
|
|
36
|
+
* for exactly one revision, and `--dry-run --exact` announced `^4.3.0` while the writer
|
|
37
|
+
* would have produced `4.3.0` — a tool describing a change it was not about to make.
|
|
38
|
+
*/
|
|
39
|
+
export declare function targetRange(prefix: string, version: string, exact: boolean): string;
|
|
17
40
|
export {};
|
package/lib/commands/upgrade.js
CHANGED
|
@@ -79,6 +79,11 @@ export const upgradeCommand = {
|
|
|
79
79
|
description: 'CI gate: exit non-zero when any dep is declared inconsistently across workspaces. Implies offline (no registry calls). Pairs with `--align` as the fix.',
|
|
80
80
|
type: 'boolean',
|
|
81
81
|
default: false,
|
|
82
|
+
})
|
|
83
|
+
.option('exact', {
|
|
84
|
+
description: 'Pin without a range operator. When writing, emits `1.2.3` instead of `^1.2.3`; with `--check`, fails on any matched dep that carries one. Pair with `--filter` — a repository-wide exactness check fails on every ordinary caret dep by design.',
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: false,
|
|
82
87
|
})
|
|
83
88
|
.option('dry-run', {
|
|
84
89
|
description: 'Print the upgrade plan without writing package.json files.',
|
|
@@ -118,7 +123,7 @@ export const upgradeCommand = {
|
|
|
118
123
|
const groups = groupByDependency(decls);
|
|
119
124
|
// --check: exit non-zero if any inconsistency.
|
|
120
125
|
if (args.check) {
|
|
121
|
-
return runCheckMode(groups);
|
|
126
|
+
return runCheckMode(groups, args.exact);
|
|
122
127
|
}
|
|
123
128
|
// --align: offline consistency-only, no registry calls.
|
|
124
129
|
if (args.align) {
|
|
@@ -163,10 +168,10 @@ export const upgradeCommand = {
|
|
|
163
168
|
if (args.dryRun) {
|
|
164
169
|
const fileCount = uniqueLocations(selected).length;
|
|
165
170
|
console.log(`[gjsify upgrade] --dry-run: would update ${selected.length} deps across ${fileCount} package.json file(s).`);
|
|
166
|
-
printChangePlan(selected);
|
|
171
|
+
printChangePlan(selected, args.exact);
|
|
167
172
|
return;
|
|
168
173
|
}
|
|
169
|
-
const files = applyToFiles(selected);
|
|
174
|
+
const files = applyToFiles(selected, args.exact);
|
|
170
175
|
console.log(`✏️ updated ${selected.length} dep(s) across ${files} package.json file(s). Run \`gjsify install\` to apply.`);
|
|
171
176
|
},
|
|
172
177
|
};
|
|
@@ -281,10 +286,53 @@ function splitRange(range) {
|
|
|
281
286
|
return { prefix, version: parsed?.version ?? null };
|
|
282
287
|
}
|
|
283
288
|
// ─── Modes: --check / --align ──────────────────────────────────────────
|
|
284
|
-
|
|
289
|
+
/**
|
|
290
|
+
* `--exact` half of the CI gate: does every matched dep carry a bare version?
|
|
291
|
+
*
|
|
292
|
+
* Consistency and exactness are different questions, and the first cannot answer the
|
|
293
|
+
* second: a repository where every manifest agrees on `^4.1.0` is perfectly consistent
|
|
294
|
+
* and still lets a minor release move a subpath under a lockfile-less install. That is
|
|
295
|
+
* the hazard ADR 0029 § Risks 1 names for `@girs/*`, whose `./vocabulary` export is what
|
|
296
|
+
* `@gjsify/gtk-host` consumes — so the pin has to be the whole version, and something
|
|
297
|
+
* has to say so on every PR.
|
|
298
|
+
*
|
|
299
|
+
* Returns the number of offending declarations so the caller can exit once for both arms.
|
|
300
|
+
*/
|
|
301
|
+
export function reportInexactRanges(groups) {
|
|
302
|
+
const offenders = groups
|
|
303
|
+
.map((g) => ({ name: g.name, loose: g.occurrences.filter((o) => o.prefix !== '') }))
|
|
304
|
+
.filter((entry) => entry.loose.length > 0);
|
|
305
|
+
if (offenders.length === 0)
|
|
306
|
+
return 0;
|
|
307
|
+
const total = offenders.reduce((n, entry) => n + entry.loose.length, 0);
|
|
308
|
+
console.error(`gjsify upgrade --check --exact: FAIL. ${total} declaration(s) across ${offenders.length} dep(s) carry a range operator:\n`);
|
|
309
|
+
for (const entry of offenders) {
|
|
310
|
+
const byRange = new Map();
|
|
311
|
+
for (const occ of entry.loose) {
|
|
312
|
+
const list = byRange.get(occ.currentRange) ?? [];
|
|
313
|
+
list.push(occ.workspace);
|
|
314
|
+
byRange.set(occ.currentRange, list);
|
|
315
|
+
}
|
|
316
|
+
console.error(` ${ANSI.bold}${entry.name}${ANSI.reset}`);
|
|
317
|
+
for (const [range, holders] of byRange.entries()) {
|
|
318
|
+
const shown = holders.length > 6 ? `${holders.slice(0, 6).join(', ')} … +${holders.length - 6}` : holders.join(', ');
|
|
319
|
+
console.error(` ${range.padEnd(16)} — ${shown}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return total;
|
|
323
|
+
}
|
|
324
|
+
function runCheckMode(groups, exact = false) {
|
|
325
|
+
const inexact = exact ? reportInexactRanges(groups) : 0;
|
|
285
326
|
const inconsistencies = findInconsistencies(groups);
|
|
327
|
+
if (inconsistencies.length === 0 && inexact > 0) {
|
|
328
|
+
console.error(`\nFix: run \`gjsify upgrade --align --exact\` (offline; drops the operator and keeps the version).`);
|
|
329
|
+
// `return`, not a bare call: under GJS the process does not halt here, and the
|
|
330
|
+
// consistency report below would run on top of the exactness one.
|
|
331
|
+
return process.exit(1);
|
|
332
|
+
}
|
|
286
333
|
if (inconsistencies.length === 0) {
|
|
287
|
-
console.log(`gjsify upgrade --check: OK. ${groups.length} dep(s) consistently declared across workspaces
|
|
334
|
+
console.log(`gjsify upgrade --check: OK. ${groups.length} dep(s) consistently declared across workspaces` +
|
|
335
|
+
(exact ? `, every declaration pinned exactly.` : `.`));
|
|
288
336
|
return;
|
|
289
337
|
}
|
|
290
338
|
console.error(`gjsify upgrade --check: FAIL. ${inconsistencies.length} dep(s) declared at inconsistent ranges:\n`);
|
|
@@ -334,13 +382,13 @@ function runAlignMode(groups, args) {
|
|
|
334
382
|
console.log(`gjsify upgrade --align: aligning ${updates.length} inconsistent dep(s) to their highest declared version:\n`);
|
|
335
383
|
for (const u of updates) {
|
|
336
384
|
const newPrefix = u.occurrences[0]?.prefix ?? '^';
|
|
337
|
-
console.log(` ${u.name.padEnd(28)} ranges: ${[...u.declaredRanges].join(', ')} → ${newPrefix
|
|
385
|
+
console.log(` ${u.name.padEnd(28)} ranges: ${[...u.declaredRanges].join(', ')} → ${targetRange(newPrefix, u.latestVersion, args.exact ?? false)}`);
|
|
338
386
|
}
|
|
339
387
|
if (args.dryRun) {
|
|
340
388
|
console.log('\n[gjsify upgrade --align] --dry-run: no files changed.');
|
|
341
389
|
return;
|
|
342
390
|
}
|
|
343
|
-
const files = applyToFiles(updates);
|
|
391
|
+
const files = applyToFiles(updates, args.exact);
|
|
344
392
|
console.log(`\n✏️ updated ${updates.length} dep(s) across ${files} package.json file(s).`);
|
|
345
393
|
}
|
|
346
394
|
// ─── Registry resolution (group-aware) ─────────────────────────────────
|
|
@@ -491,10 +539,10 @@ function renderDeclaredCell(c) {
|
|
|
491
539
|
return joined;
|
|
492
540
|
return joined.slice(0, 29) + '…';
|
|
493
541
|
}
|
|
494
|
-
function printChangePlan(selected) {
|
|
542
|
+
function printChangePlan(selected, exact = false) {
|
|
495
543
|
for (const u of selected) {
|
|
496
544
|
for (const occ of u.occurrences) {
|
|
497
|
-
console.log(` ${occ.workspace.padEnd(38)} ${u.name.padEnd(28)} ${occ.currentRange.padEnd(12)} → ${occ.prefix
|
|
545
|
+
console.log(` ${occ.workspace.padEnd(38)} ${u.name.padEnd(28)} ${occ.currentRange.padEnd(12)} → ${targetRange(occ.prefix, u.latestVersion, exact)}`);
|
|
498
546
|
}
|
|
499
547
|
}
|
|
500
548
|
}
|
|
@@ -543,7 +591,17 @@ async function promptSelection(candidates) {
|
|
|
543
591
|
}
|
|
544
592
|
}
|
|
545
593
|
// ─── Write-back across all affected workspaces ─────────────────────────
|
|
546
|
-
|
|
594
|
+
/**
|
|
595
|
+
* The range a dependency will be WRITTEN at.
|
|
596
|
+
*
|
|
597
|
+
* Preview and write share this deliberately. They were two expressions for one decision
|
|
598
|
+
* for exactly one revision, and `--dry-run --exact` announced `^4.3.0` while the writer
|
|
599
|
+
* would have produced `4.3.0` — a tool describing a change it was not about to make.
|
|
600
|
+
*/
|
|
601
|
+
export function targetRange(prefix, version, exact) {
|
|
602
|
+
return exact ? version : `${prefix}${version}`;
|
|
603
|
+
}
|
|
604
|
+
function applyToFiles(updates, exact = false) {
|
|
547
605
|
// Group by workspace.location so each package.json is read + written once.
|
|
548
606
|
const byLocation = new Map();
|
|
549
607
|
for (const u of updates) {
|
|
@@ -559,25 +617,50 @@ function applyToFiles(updates) {
|
|
|
559
617
|
const raw = readFileSync(pkgJsonPath, 'utf-8');
|
|
560
618
|
const pkg = JSON.parse(raw);
|
|
561
619
|
let changed = false;
|
|
620
|
+
const changedNames = [];
|
|
562
621
|
for (const g of groups) {
|
|
563
622
|
const occ = g.occurrences[0];
|
|
564
623
|
const map = pkg[occ.field];
|
|
565
624
|
if (!map)
|
|
566
625
|
continue;
|
|
567
|
-
const next =
|
|
626
|
+
const next = targetRange(occ.prefix, g.latestVersion, exact);
|
|
568
627
|
if (map[g.name] !== next) {
|
|
569
628
|
map[g.name] = next;
|
|
570
629
|
changed = true;
|
|
630
|
+
changedNames.push(g.name);
|
|
571
631
|
}
|
|
572
632
|
}
|
|
573
633
|
if (changed) {
|
|
574
634
|
const indent = detectIndent(raw);
|
|
575
|
-
|
|
635
|
+
const rewritten = JSON.stringify(pkg, null, indent) + (raw.endsWith('\n') ? '\n' : '');
|
|
636
|
+
writeFileSync(pkgJsonPath, keepUntouchedLines(raw, rewritten, changedNames), 'utf-8');
|
|
576
637
|
touched++;
|
|
577
638
|
}
|
|
578
639
|
}
|
|
579
640
|
return touched;
|
|
580
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Take the re-serialised file, but keep every line that is not about a changed dep.
|
|
644
|
+
*
|
|
645
|
+
* A `JSON.parse` → `JSON.stringify` round trip rewrites the WHOLE file, and re-encoding
|
|
646
|
+
* is not identity: measured on this repository, pinning `@girs/*` decoded `\u2014` into
|
|
647
|
+
* an em dash inside three `description` fields nobody asked to touch, so a dependency
|
|
648
|
+
* bump arrived as a diff over unrelated prose. Semantically equal, and still noise a
|
|
649
|
+
* reviewer has to read past.
|
|
650
|
+
*
|
|
651
|
+
* Line-for-line only. Adding or removing a key changes the line count, and then the
|
|
652
|
+
* mapping is not sound — so that case falls back to the plain re-serialisation rather
|
|
653
|
+
* than guessing an alignment.
|
|
654
|
+
*/
|
|
655
|
+
function keepUntouchedLines(raw, rewritten, changedNames) {
|
|
656
|
+
const before = raw.split('\n');
|
|
657
|
+
const after = rewritten.split('\n');
|
|
658
|
+
if (before.length !== after.length)
|
|
659
|
+
return rewritten;
|
|
660
|
+
return after
|
|
661
|
+
.map((line, i) => (changedNames.some((name) => line.includes(`"${name}"`)) ? line : before[i]))
|
|
662
|
+
.join('\n');
|
|
663
|
+
}
|
|
581
664
|
function uniqueLocations(updates) {
|
|
582
665
|
const set = new Set();
|
|
583
666
|
for (const u of updates) {
|
|
@@ -430,6 +430,25 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
430
430
|
release?: string;
|
|
431
431
|
/** `Maintainer:` / `Packager:` as `Name <email>`. Default: `package.json#author`. */
|
|
432
432
|
maintainer?: string;
|
|
433
|
+
/**
|
|
434
|
+
* The runtime a shipped artifact runs, PER TARGET — `gjsify.app` is the
|
|
435
|
+
* default and a key here overrides it for one layout.
|
|
436
|
+
*
|
|
437
|
+
* KEYED IN THE `process.platform` SPELLING, like {@link ShipSignOptions} and
|
|
438
|
+
* for the same reason: the value that resolves is chosen by the LAYOUT being
|
|
439
|
+
* assembled — `FormatDescriptor.layoutOs` and the stage manifest's `target.os`
|
|
440
|
+
* — never by the host doing the assembling. `macos` and `windows` are the
|
|
441
|
+
* `gjsify ship <os>` POSITIONAL's spelling, and both `gjsify ship` and
|
|
442
|
+
* `manifest-conformance`'s `ship` rule refuse them BY NAME, because a runtime
|
|
443
|
+
* under a key nothing reads is a target that silently keeps the project-wide
|
|
444
|
+
* answer.
|
|
445
|
+
*
|
|
446
|
+
* `resolveShipApp` (`utils/ship/settings.ts`) is the only reader of this key
|
|
447
|
+
* or of `gjsify.app`, and holds the rest: what one field could not answer,
|
|
448
|
+
* the measurement behind it, and why an absent key means `gjsify.app` and
|
|
449
|
+
* never `Layout.shippedRuntime`.
|
|
450
|
+
*/
|
|
451
|
+
app?: ShipAppOptions;
|
|
433
452
|
/**
|
|
434
453
|
* Formats to build when `--target` is not given. Default `['deb', 'rpm']`.
|
|
435
454
|
*
|
|
@@ -526,6 +545,30 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
526
545
|
* produces a package that installs its translations and shows none of them.
|
|
527
546
|
*/
|
|
528
547
|
localeDir?: string;
|
|
548
|
+
/**
|
|
549
|
+
* A font file, or a directory of them, the application SHIPS (ADR 0038).
|
|
550
|
+
* Default: `data/fonts` when it exists.
|
|
551
|
+
*
|
|
552
|
+
* Staged into `share/fonts/<appId>/`, which is where fontconfig looks: under a
|
|
553
|
+
* `/usr` prefix through the stock `fonts.conf`'s unconditional
|
|
554
|
+
* `<dir>/usr/share/fonts</dir>`, and under every other prefix through
|
|
555
|
+
* `<dir prefix="xdg">fonts</dir>` plus the `XDG_DATA_DIRS` the launcher already
|
|
556
|
+
* exports. The app names the FAMILY in its styles and needs no install-layout
|
|
557
|
+
* knowledge, exactly as it needs none for icons or schemas.
|
|
558
|
+
*
|
|
559
|
+
* This is what a branded application needs: on a GNOME host the family is
|
|
560
|
+
* usually installed anyway, and on macOS, on Windows and on any Linux that is
|
|
561
|
+
* not GNOME it is not — so without shipping it Pango silently substitutes a
|
|
562
|
+
* fallback and the app merely looks wrong, at exit 0.
|
|
563
|
+
*
|
|
564
|
+
* `.woff` / `.woff2` / `.eot` are REFUSED. Whether FreeType opens a web-font
|
|
565
|
+
* wrapper is a BUILD option of the FreeType the app runs against (brotli for
|
|
566
|
+
* woff2, zlib for woff), so it is a property of the host's or the bundle's
|
|
567
|
+
* closure rather than of the file — and a font that resolves on the developer's
|
|
568
|
+
* box and not in the shipped bundle is the exact silent substitution this key
|
|
569
|
+
* exists to prevent. Ship the desktop face.
|
|
570
|
+
*/
|
|
571
|
+
fonts?: string;
|
|
529
572
|
/** Extra payload entries: prefix-relative destination → project-relative source. */
|
|
530
573
|
extraFiles?: Record<string, string>;
|
|
531
574
|
/** Arguments the launcher appends before the user's own. */
|
|
@@ -547,6 +590,27 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
547
590
|
*/
|
|
548
591
|
sign?: ShipSignOptions;
|
|
549
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* `gjsify.ship.app` — the shipped runtime per OS, overriding `gjsify.app`.
|
|
595
|
+
*
|
|
596
|
+
* All three keys are present, which is where this differs from
|
|
597
|
+
* {@link ShipSignOptions}: `linux` is absent there because a `.deb` carries no
|
|
598
|
+
* per-file signature, while a Linux artifact very much has a runtime — it is the
|
|
599
|
+
* one that emits `Depends: gjs (>= 1.86)` or `Depends: nodejs (>= 24)`, and the
|
|
600
|
+
* whole point of this table is that stating darwin's answer must not move it.
|
|
601
|
+
*
|
|
602
|
+
* Only `gjs` and `node` are shippable. `browser` has no process to launch and
|
|
603
|
+
* `nativescript` ships as an APK/IPA through another pipeline (ADR 0024 § 4), so
|
|
604
|
+
* either one is refused here exactly as it is on `gjsify.app`.
|
|
605
|
+
*/
|
|
606
|
+
export interface ShipAppOptions {
|
|
607
|
+
/** What a `.deb`/`.rpm`/Flatpak depends on and what its launcher execs. */
|
|
608
|
+
linux?: 'gjs' | 'node';
|
|
609
|
+
/** What a `<App>.app` carries. `node` is the only value the macOS formats accept. */
|
|
610
|
+
darwin?: 'gjs' | 'node';
|
|
611
|
+
/** What a Windows program directory carries. `node` is the only value its formats accept. */
|
|
612
|
+
win32?: 'gjs' | 'node';
|
|
613
|
+
}
|
|
550
614
|
/**
|
|
551
615
|
* `gjsify.ship.sign` — an IDENTITY per OS, and never a certificate.
|
|
552
616
|
*
|
|
@@ -36,9 +36,17 @@ import { detectNativePackages } from './detect-native-packages.js';
|
|
|
36
36
|
*
|
|
37
37
|
* `globalThis.imports?.gi` is both the sanctioned GJS probe spelling
|
|
38
38
|
* (`.oxlintrc.json`) and the correct CONDITION, so no `isGjs()` beside it: the
|
|
39
|
-
* question is "is there a GIRepository to prepend to", and a Node
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* question is "is there a GIRepository to prepend to", and a plain Node process has
|
|
40
|
+
* none — nothing here to write to, which is the required no-op.
|
|
41
|
+
*
|
|
42
|
+
* NOT because a Node host wants no prebuild. That justification stood here and is
|
|
43
|
+
* false exactly where it matters: on darwin `@gjsify/webkit-native`'s prebuild is
|
|
44
|
+
* the ONLY WebKit any host has (ADR 0022), and ADR 0024 § 4 puts macOS and Windows
|
|
45
|
+
* applications on Node + node-gi. What is true is narrower — the CLI on Node uses
|
|
46
|
+
* the npm `rolldown` crate — and generalising it to "a Node host never wants a
|
|
47
|
+
* prebuild" is what kept every OTHER Node prebuild env-bound until
|
|
48
|
+
* `@gjsify/node-gi`'s `native-prebuilds.js` (ADR 0021 § The Node host). The Node
|
|
49
|
+
* host is covered THERE, in the package that owns a GIRepository, not here.
|
|
42
50
|
*
|
|
43
51
|
* `dup_default()` is GLib >= 2.86, probed as a capability rather than caught:
|
|
44
52
|
* an older GLib simply keeps today's behaviour (the launcher's environment, or
|