@gjsify/cli 0.64.15 → 0.64.16
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 +138 -138
- package/lib/actions/build.d.ts +10 -0
- package/lib/actions/build.js +35 -1
- package/lib/commands/ship.js +28 -1
- package/lib/types/config-data.d.ts +25 -0
- package/lib/utils/ship/dmg.d.ts +23 -0
- package/lib/utils/ship/dmg.js +19 -0
- package/lib/utils/ship/layout.d.ts +8 -0
- package/lib/utils/ship/layout.js +30 -1
- package/lib/utils/ship/msi.js +15 -0
- package/lib/utils/ship/plan.d.ts +2 -0
- package/lib/utils/ship/plan.js +8 -0
- package/lib/utils/ship/settings.d.ts +17 -1
- package/lib/utils/ship/settings.js +62 -1
- package/lib/utils/ship/stage-manifest.js +13 -0
- package/lib/utils/ship/types.d.ts +20 -0
- package/package.json +20 -20
package/lib/actions/build.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ConfigData } from '../types/index.js';
|
|
|
2
2
|
import type { App } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import type { RolldownOutput } from 'rolldown';
|
|
4
4
|
import { type RunBundleOptions } from '../bundler-pick.js';
|
|
5
|
+
import { WorkerEntries } from '@gjsify/rolldown-plugin-gjsify';
|
|
5
6
|
export declare class BuildAction {
|
|
6
7
|
readonly configData: ConfigData;
|
|
7
8
|
constructor(configData?: ConfigData);
|
|
@@ -229,7 +230,16 @@ export declare class BuildAction {
|
|
|
229
230
|
preserveDefaultExport?: boolean;
|
|
230
231
|
toolchainAnchor?: string;
|
|
231
232
|
onWatchFiles?: RunBundleOptions['onWatchFiles'];
|
|
233
|
+
/** Set by the top-level build for its nested worker builds, so names and "already built" are shared. */
|
|
234
|
+
workers?: WorkerEntries;
|
|
232
235
|
}): Promise<RolldownOutput[]>;
|
|
236
|
+
/**
|
|
237
|
+
* Build every worker script the main bundle (or an earlier worker) names by
|
|
238
|
+
* `new Worker(new URL('./x', import.meta.url))`, each as its own app build beside the
|
|
239
|
+
* output, under the main build's config. Rounds, not recursion: a nested build registers
|
|
240
|
+
* what ITS workers need, and the shared registry hands each source out once.
|
|
241
|
+
*/
|
|
242
|
+
private buildWorkers;
|
|
233
243
|
/**
|
|
234
244
|
* Drive `rolldown.watch(...)`: rebuild on source change, apply the
|
|
235
245
|
* post-bundle shebang hook on each successful build, surface errors
|
package/lib/actions/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
|
-
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, scanGiNamespaces, planGiSelection, } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
|
+
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, scanGiNamespaces, planGiSelection, WorkerEntries, } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
@@ -715,6 +715,15 @@ export class BuildAction {
|
|
|
715
715
|
...(opts.preserveDefaultExport ? { preserveDefaultExport: true } : {}),
|
|
716
716
|
...(opts.toolchainAnchor !== undefined ? { toolchainAnchor: opts.toolchainAnchor } : {}),
|
|
717
717
|
};
|
|
718
|
+
// `new Worker(new URL('./x', import.meta.url))` targets are built beside the bundle after it
|
|
719
|
+
// (see `buildWorkers`). Needs a concrete output location and a one-file app target.
|
|
720
|
+
const ownsWorkers = opts.workers === undefined;
|
|
721
|
+
const workers = opts.workers ??
|
|
722
|
+
(app !== 'nativescript' && !opts.watch && (outfile || outdir)
|
|
723
|
+
? new WorkerEntries(outfile ? extname(outfile) || '.js' : '.js')
|
|
724
|
+
: undefined);
|
|
725
|
+
if (workers)
|
|
726
|
+
pluginOpts.workers = workers;
|
|
718
727
|
const { autoMode, extras } = this.parseGlobalsValue(globals);
|
|
719
728
|
const pnp = await buildPnpPlugin();
|
|
720
729
|
const pnpPlugins = pnp ? [pnp] : [];
|
|
@@ -916,8 +925,33 @@ export class BuildAction {
|
|
|
916
925
|
if (app === 'browser' && htmlEntry && outfile) {
|
|
917
926
|
await this.applyBrowserHtml(htmlEntry, outfile, verbose);
|
|
918
927
|
}
|
|
928
|
+
if (workers && ownsWorkers) {
|
|
929
|
+
await this.buildWorkers(workers, app, dirname(resolve(outfile ?? join(outdir, 'x'))), opts);
|
|
930
|
+
}
|
|
919
931
|
return [writeResult];
|
|
920
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Build every worker script the main bundle (or an earlier worker) names by
|
|
935
|
+
* `new Worker(new URL('./x', import.meta.url))`, each as its own app build beside the
|
|
936
|
+
* output, under the main build's config. Rounds, not recursion: a nested build registers
|
|
937
|
+
* what ITS workers need, and the shared registry hands each source out once.
|
|
938
|
+
*/
|
|
939
|
+
async buildWorkers(workers, app, outDir, opts) {
|
|
940
|
+
const { esbuild: _legacy, ...rest } = this.configData;
|
|
941
|
+
const bundler = normalizeBundlerOptions(this.configData);
|
|
942
|
+
for (let round = workers.takePending(); round.length > 0; round = workers.takePending()) {
|
|
943
|
+
for (const { source, fileName } of round) {
|
|
944
|
+
if (this.configData.verbose)
|
|
945
|
+
console.debug(`[gjsify] bundling worker ${source} → ${fileName}`);
|
|
946
|
+
const { dir: _dir, ...output } = bundler.output ?? {};
|
|
947
|
+
await new BuildAction({
|
|
948
|
+
...rest,
|
|
949
|
+
shebang: false,
|
|
950
|
+
bundler: { ...bundler, input: source, output: { ...output, file: join(outDir, fileName) } },
|
|
951
|
+
}).buildApp(app, { ...opts, workers });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
921
955
|
/**
|
|
922
956
|
* Drive `rolldown.watch(...)`: rebuild on source change, apply the
|
|
923
957
|
* post-bundle shebang hook on each successful build, surface errors
|
package/lib/commands/ship.js
CHANGED
|
@@ -39,7 +39,7 @@ import { resolveNodeModulesClosure, stageNodeModules } from '../utils/ship/node-
|
|
|
39
39
|
import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
|
|
40
40
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
41
41
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
42
|
-
import { addApplicationsLink, buildDmgImage, dmgVolumeDir, dmgVolumeName, signDmgImage } from '../utils/ship/dmg.js';
|
|
42
|
+
import { addApplicationsLink, buildDmgImage, dmgVolumeDir, dmgVolumeName, signDmgImage, stapleBundleBeforeImage, } from '../utils/ship/dmg.js';
|
|
43
43
|
import { assertNoDroppedFlatpakModules } from '../utils/ship/flatpak-config.js';
|
|
44
44
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
45
45
|
import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
|
|
@@ -422,6 +422,10 @@ async function assemble(args) {
|
|
|
422
422
|
const stageInputs = {
|
|
423
423
|
bundleFiles: discovered.bundleFiles,
|
|
424
424
|
launcher: renderLauncher(settings, basename(settings.bundlePath), layout, runtime.launcher),
|
|
425
|
+
executableLaunchers: Object.fromEntries(settings.executables.map((executable) => [
|
|
426
|
+
executable.name,
|
|
427
|
+
renderLauncher(settings, executable.entry, layout, runtime.launcher),
|
|
428
|
+
])),
|
|
425
429
|
metainfo: translated.metainfo,
|
|
426
430
|
desktopEntry: translated.desktopEntry,
|
|
427
431
|
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
@@ -1262,6 +1266,29 @@ async function packOne(input) {
|
|
|
1262
1266
|
// mounts showing `Contents/` — a window with no application in it.
|
|
1263
1267
|
const volumeDir = dmgVolumeDir(outRoot);
|
|
1264
1268
|
writePayload(volumeDir, payload, '');
|
|
1269
|
+
// THE BUNDLE'S OWN TICKET, before the image exists (#162): a `.app` dragged
|
|
1270
|
+
// out of a volume carries only what was stapled to IT. Nothing signs after
|
|
1271
|
+
// this point except the image, which is a different object.
|
|
1272
|
+
if (input.notarize.kind === 'notarize') {
|
|
1273
|
+
const notarizePlan = input.notarize;
|
|
1274
|
+
const bundleDir = join(volumeDir, `${settings.name}.app`);
|
|
1275
|
+
const archive = join(dirname(volumeDir), 'bundle-notarize.zip');
|
|
1276
|
+
await stapleBundleBeforeImage({
|
|
1277
|
+
writeArchive: () => writeFileSync(archive, buildZip(zipEntriesFromPayload(payload), mtime)),
|
|
1278
|
+
notarize: () => notarizeArtifact({
|
|
1279
|
+
plan: notarizePlan,
|
|
1280
|
+
artifact: archive,
|
|
1281
|
+
verbose: input.verbose,
|
|
1282
|
+
log: (line) => console.log(`${LOG} ${line}`),
|
|
1283
|
+
}),
|
|
1284
|
+
staple: () => stapleArtifact({
|
|
1285
|
+
notary: notarizePlan.notary,
|
|
1286
|
+
artifact: bundleDir,
|
|
1287
|
+
verbose: input.verbose,
|
|
1288
|
+
log: (line) => console.log(`${LOG} ${line}`),
|
|
1289
|
+
}),
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1265
1292
|
addApplicationsLink(volumeDir);
|
|
1266
1293
|
await buildDmgImage({
|
|
1267
1294
|
volumeName: dmgVolumeName(settings),
|
|
@@ -702,6 +702,15 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
702
702
|
extraFiles?: Record<string, string>;
|
|
703
703
|
/** Arguments the launcher appends before the user's own. */
|
|
704
704
|
execArgs?: string[];
|
|
705
|
+
/**
|
|
706
|
+
* Further executables of the SAME package, beside the primary one `gjsify.main` names.
|
|
707
|
+
*
|
|
708
|
+
* Each is one more launcher over the one shared bundle directory: the primary stays
|
|
709
|
+
* `bin/<binaryName>` (and `CFBundleExecutable` on macOS), these are `bin/<name>`.
|
|
710
|
+
* Absent means one executable, which is what every package shipped before this key.
|
|
711
|
+
* See `docs/ship-formats.md` for what each format does with them.
|
|
712
|
+
*/
|
|
713
|
+
executables?: ShipExecutableOptions[];
|
|
705
714
|
/**
|
|
706
715
|
* How `--target flatpak` builds the app: the runtime it links against and
|
|
707
716
|
* what the finished app is allowed to do.
|
|
@@ -719,6 +728,22 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
719
728
|
*/
|
|
720
729
|
sign?: ShipSignOptions;
|
|
721
730
|
}
|
|
731
|
+
/** One entry of `gjsify.ship.executables`. */
|
|
732
|
+
export interface ShipExecutableOptions {
|
|
733
|
+
/**
|
|
734
|
+
* The command name — `bin/<name>`, `Contents/MacOS/<name>`, `<name>.cmd`. Lower-case
|
|
735
|
+
* `[a-z0-9][a-z0-9._+-]*`, unique, and not the package's own `binaryName`.
|
|
736
|
+
*/
|
|
737
|
+
name: string;
|
|
738
|
+
/** The built entry, project-relative, inside the directory `gjsify.main` lives in. */
|
|
739
|
+
entry: string;
|
|
740
|
+
/**
|
|
741
|
+
* Windows only: also stage a GUI-subsystem `<name>.exe` beside the `<name>.cmd`, so a
|
|
742
|
+
* double-click or shortcut opens no console window. Default `false` — a command-line
|
|
743
|
+
* tool wants the console.
|
|
744
|
+
*/
|
|
745
|
+
gui?: boolean;
|
|
746
|
+
}
|
|
722
747
|
/**
|
|
723
748
|
* `gjsify.ship.app` — the shipped runtime per OS, overriding `gjsify.app`.
|
|
724
749
|
*
|
package/lib/utils/ship/dmg.d.ts
CHANGED
|
@@ -117,3 +117,26 @@ export declare function signDmgImage(input: DmgSignInput & {
|
|
|
117
117
|
verbose: boolean;
|
|
118
118
|
workDir: string;
|
|
119
119
|
}): Promise<void>;
|
|
120
|
+
export interface BundleTicketSteps {
|
|
121
|
+
/** Write the archive `notarytool` submits (it takes no directory). */
|
|
122
|
+
writeArchive: () => void;
|
|
123
|
+
/** Submit the archive and wait for the verdict. */
|
|
124
|
+
notarize: () => Promise<void>;
|
|
125
|
+
/** Attach the ticket to the bundle ON DISK, the one the image is built from. */
|
|
126
|
+
staple: () => Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Notarise and staple the `.app` BEFORE the image is built from it (#162).
|
|
130
|
+
*
|
|
131
|
+
* Notarising only the `.dmg` clears the image and, online, the app inside it —
|
|
132
|
+
* but the ticket is stapled to the container, so an `.app` dragged out of the
|
|
133
|
+
* volume carries none and asks Gatekeeper over the network on every first launch.
|
|
134
|
+
* Apple's rule for the order (`stapler(1)`): staple after signing and notarising,
|
|
135
|
+
* and never `codesign` afterwards — stapling does not invalidate the signature,
|
|
136
|
+
* re-signing does invalidate the ticket. So the sequence is fixed:
|
|
137
|
+
* sign bundle → notarise bundle → staple bundle → build image → sign image →
|
|
138
|
+
* notarise image → staple image. This function owns the middle three and is a
|
|
139
|
+
* sequence of injected steps so the ORDER is unit-testable off macOS, where none
|
|
140
|
+
* of the tools run.
|
|
141
|
+
*/
|
|
142
|
+
export declare function stapleBundleBeforeImage(steps: BundleTicketSteps): Promise<void>;
|
package/lib/utils/ship/dmg.js
CHANGED
|
@@ -243,3 +243,22 @@ export async function signDmgImage(input) {
|
|
|
243
243
|
throw new Error(`gjsify ship: ${DMG_SIGN_TOOL} failed on the image with ${describeExit(result)}.`);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Notarise and staple the `.app` BEFORE the image is built from it (#162).
|
|
248
|
+
*
|
|
249
|
+
* Notarising only the `.dmg` clears the image and, online, the app inside it —
|
|
250
|
+
* but the ticket is stapled to the container, so an `.app` dragged out of the
|
|
251
|
+
* volume carries none and asks Gatekeeper over the network on every first launch.
|
|
252
|
+
* Apple's rule for the order (`stapler(1)`): staple after signing and notarising,
|
|
253
|
+
* and never `codesign` afterwards — stapling does not invalidate the signature,
|
|
254
|
+
* re-signing does invalidate the ticket. So the sequence is fixed:
|
|
255
|
+
* sign bundle → notarise bundle → staple bundle → build image → sign image →
|
|
256
|
+
* notarise image → staple image. This function owns the middle three and is a
|
|
257
|
+
* sequence of injected steps so the ORDER is unit-testable off macOS, where none
|
|
258
|
+
* of the tools run.
|
|
259
|
+
*/
|
|
260
|
+
export async function stapleBundleBeforeImage(steps) {
|
|
261
|
+
steps.writeArchive();
|
|
262
|
+
await steps.notarize();
|
|
263
|
+
await steps.staple();
|
|
264
|
+
}
|
|
@@ -18,6 +18,14 @@ export interface LayoutIdentity {
|
|
|
18
18
|
binaryName: string;
|
|
19
19
|
/** Human-readable display name — the `<App>.app` directory a user sees in Finder. */
|
|
20
20
|
name: string;
|
|
21
|
+
/**
|
|
22
|
+
* The package's further executables (`gjsify.ship.executables`), absent or empty for one.
|
|
23
|
+
* `bin/<name>` is each one's planned launcher; the layout decides where it lands.
|
|
24
|
+
*/
|
|
25
|
+
executables?: readonly {
|
|
26
|
+
name: string;
|
|
27
|
+
gui: boolean;
|
|
28
|
+
}[];
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
23
31
|
* What a layout needs to render the metadata it OWNS.
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -509,6 +509,20 @@ export const LAYOUTS = {
|
|
|
509
509
|
}),
|
|
510
510
|
},
|
|
511
511
|
},
|
|
512
|
+
// A further executable that asked for a GUI subsystem gets the same stub
|
|
513
|
+
// under its own name, so its `.exe` finds its own `.cmd` and logs to its
|
|
514
|
+
// own leaf. The application's icon, since it is one application.
|
|
515
|
+
...guiExecutables(input).map((executable) => ({
|
|
516
|
+
path: windowsGuiLauncherPath({ ...input, binaryName: executable.name }),
|
|
517
|
+
mode: 0o755,
|
|
518
|
+
source: {
|
|
519
|
+
kind: 'bytes',
|
|
520
|
+
data: buildGuiLauncher({
|
|
521
|
+
logLeaf: windowsLaunchLogLeaf({ ...input, binaryName: executable.name }),
|
|
522
|
+
...(icon === undefined ? {} : { icon: iconResources(icon) }),
|
|
523
|
+
}),
|
|
524
|
+
},
|
|
525
|
+
})),
|
|
512
526
|
];
|
|
513
527
|
},
|
|
514
528
|
// TWO now. `wingtk/gvsbuild` still hardcodes `self.platform = "x64"` and
|
|
@@ -570,7 +584,16 @@ export function windowsGuiLauncherPath(identity) {
|
|
|
570
584
|
* for an OS with no such file.
|
|
571
585
|
*/
|
|
572
586
|
export function payloadArchExemptions(os, identity) {
|
|
573
|
-
|
|
587
|
+
if (os !== 'win32')
|
|
588
|
+
return undefined;
|
|
589
|
+
return new Set([
|
|
590
|
+
windowsGuiLauncherPath(identity),
|
|
591
|
+
...guiExecutables(identity).map((executable) => windowsGuiLauncherPath({ ...identity, binaryName: executable.name })),
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
/** The further executables that want a GUI-subsystem stub. */
|
|
595
|
+
function guiExecutables(identity) {
|
|
596
|
+
return (identity.executables ?? []).filter((executable) => executable.gui);
|
|
574
597
|
}
|
|
575
598
|
/**
|
|
576
599
|
* What the windows GUI launcher calls the file it writes under `%TEMP%`.
|
|
@@ -676,6 +699,12 @@ export function place(layout, identity, rel) {
|
|
|
676
699
|
const dirs = layout.dirs(identity);
|
|
677
700
|
if (rel === `bin/${identity.binaryName}`)
|
|
678
701
|
return launcherPath(layout, identity);
|
|
702
|
+
// A further executable's launcher is placed exactly like the primary one — in the
|
|
703
|
+
// launcher directory, with the layout's extension — and not by the generic `bin/*` rule
|
|
704
|
+
// below, which has no extension: a windows `<name>` without `.cmd` is not runnable.
|
|
705
|
+
const further = identity.executables?.find((executable) => rel === `bin/${executable.name}`);
|
|
706
|
+
if (further !== undefined)
|
|
707
|
+
return launcherPath(layout, { ...identity, binaryName: further.name });
|
|
679
708
|
const libPrefix = `lib/${identity.binaryName}/`;
|
|
680
709
|
const giPrefix = `${libPrefix}gi/`;
|
|
681
710
|
if (rel.startsWith(giPrefix))
|
package/lib/utils/ship/msi.js
CHANGED
|
@@ -396,6 +396,12 @@ export function renderWxs(input) {
|
|
|
396
396
|
// behind empty by an uninstall.
|
|
397
397
|
` <Directory Id="INSTALLDIR" Name="${xmlEscape(programDirName)}">`,
|
|
398
398
|
...lines,
|
|
399
|
+
` <Component Id="AddToPathComponent" Guid="${msiComponentGuid(settings, ':path')}" ` +
|
|
400
|
+
`Win64="${win64 ? 'yes' : 'no'}">`,
|
|
401
|
+
' <CreateFolder />',
|
|
402
|
+
' <Environment Id="GjsifyPath" Name="PATH" Action="set" Part="last" System="yes" ' +
|
|
403
|
+
'Permanent="no" Value="[INSTALLDIR]" />',
|
|
404
|
+
' </Component>',
|
|
399
405
|
' </Directory>',
|
|
400
406
|
' </Directory>',
|
|
401
407
|
// Declared and left empty: the shortcut reaches it by `Directory=`, so this
|
|
@@ -406,6 +412,15 @@ export function renderWxs(input) {
|
|
|
406
412
|
` <Feature Id="Main" Title="${xmlEscape(settings.name)}" Level="1">`,
|
|
407
413
|
...componentIds.map((id) => ` <ComponentRef Id="${id}" />`),
|
|
408
414
|
' </Feature>',
|
|
415
|
+
// OPTIONAL and OFF by default (Level 2 is above the default install level of 1):
|
|
416
|
+
// appending the install directory to the MACHINE `PATH` changes every
|
|
417
|
+
// program on the box, so a user asks for it. Silent opt-in:
|
|
418
|
+
// `msiexec /i app.msi ADDTOPATH=1` or `ADDLOCAL=Main,AddToPath`. The
|
|
419
|
+
// `Environment` row is removed again on uninstall (`Permanent="no"`).
|
|
420
|
+
` <Feature Id="AddToPath" Title="Add ${xmlEscape(settings.name)} to PATH" Level="2">`,
|
|
421
|
+
' <ComponentRef Id="AddToPathComponent" />',
|
|
422
|
+
' <Condition Level="1">ADDTOPATH=1</Condition>',
|
|
423
|
+
' </Feature>',
|
|
409
424
|
' </Product>',
|
|
410
425
|
'</Wix>',
|
|
411
426
|
'',
|
package/lib/utils/ship/plan.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface StageInputs {
|
|
|
5
5
|
bundleFiles: readonly string[];
|
|
6
6
|
/** The rendered launcher script. */
|
|
7
7
|
launcher: string;
|
|
8
|
+
/** One rendered launcher per `settings.executables` entry, by name. Absent means none. */
|
|
9
|
+
executableLaunchers?: Readonly<Record<string, string>>;
|
|
8
10
|
/** The rendered AppStream MetaInfo XML. */
|
|
9
11
|
metainfo: string;
|
|
10
12
|
/** The rendered desktop entry — required for `kind: 'app'`, unused for `'cli'`. */
|
package/lib/utils/ship/plan.js
CHANGED
|
@@ -27,6 +27,14 @@ export function planStage(settings, inputs) {
|
|
|
27
27
|
const files = [];
|
|
28
28
|
const libDir = `lib/${settings.binaryName}`;
|
|
29
29
|
files.push({ path: `bin/${settings.binaryName}`, mode: 0o755, source: { kind: 'text', text: inputs.launcher } });
|
|
30
|
+
for (const executable of settings.executables) {
|
|
31
|
+
const text = inputs.executableLaunchers?.[executable.name];
|
|
32
|
+
if (text === undefined || !inputs.bundleFiles.includes(executable.entry)) {
|
|
33
|
+
throw new Error(`gjsify ship: \`gjsify.ship.executables\` "${executable.name}" runs ${executable.entry}, which is ` +
|
|
34
|
+
`not in ${settings.bundleDir} — build it before shipping, or the launcher would point at nothing.`);
|
|
35
|
+
}
|
|
36
|
+
files.push({ path: `bin/${executable.name}`, mode: 0o755, source: { kind: 'text', text } });
|
|
37
|
+
}
|
|
30
38
|
for (const rel of inputs.bundleFiles) {
|
|
31
39
|
files.push({
|
|
32
40
|
path: posix.join(libDir, rel),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AppMetadata, ConfigDataFlatpak, ConfigDataShip, DescriptionBlock, ShipAppOptions } from '../../types/config-data.js';
|
|
2
|
-
import type { HostOs, ShipSettings } from './types.js';
|
|
2
|
+
import type { HostOs, ShipExecutable, ShipSettings } from './types.js';
|
|
3
3
|
/** The subset of `package.json` the resolver reads. */
|
|
4
4
|
export interface ShipPackageManifest {
|
|
5
5
|
name?: string;
|
|
@@ -161,6 +161,22 @@ export interface ResolvedSettings {
|
|
|
161
161
|
*/
|
|
162
162
|
export declare function resolveShipKind(ship: ConfigDataShip, flatpak: ConfigDataFlatpak): 'app' | 'cli';
|
|
163
163
|
export declare function resolveShipSettings(input: SettingsInput): ResolvedSettings;
|
|
164
|
+
/**
|
|
165
|
+
* `gjsify.ship.executables` → the further launchers of one package.
|
|
166
|
+
*
|
|
167
|
+
* REFUSED rather than normalised, for each rule: a name collision between two entries
|
|
168
|
+
* is one launcher silently replacing the other (`placeStage` would catch the Linux
|
|
169
|
+
* case and miss a Windows `Foo.cmd`/`foo.cmd` pair on a case-insensitive disk), an
|
|
170
|
+
* entry outside the bundle directory is a file the stage does not carry, and a name
|
|
171
|
+
* with a path separator or a space escapes `bin/`. All three install cleanly and
|
|
172
|
+
* start nothing, which is the class this command is built against.
|
|
173
|
+
*/
|
|
174
|
+
export declare function resolveShipExecutables(input: {
|
|
175
|
+
declared: unknown;
|
|
176
|
+
binaryName: string;
|
|
177
|
+
projectDir: string;
|
|
178
|
+
bundleDir: string;
|
|
179
|
+
}): ShipExecutable[];
|
|
164
180
|
/** The entry one ship target installs, which key named it, and what it must NOT carry. */
|
|
165
181
|
export interface ResolvedShipBundle {
|
|
166
182
|
/** The declared path as written — project-relative or absolute. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// only) > derived from package.json. Never a yargs `default:`, which is
|
|
11
11
|
// indistinguishable from a value the user typed and would clobber the config.
|
|
12
12
|
import { validateMimeTypes } from './mime.js';
|
|
13
|
-
import { basename } from 'node:path';
|
|
13
|
+
import { basename, isAbsolute, posix, relative, resolve, sep } 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';
|
|
@@ -221,6 +221,12 @@ export function resolveShipSettings(input) {
|
|
|
221
221
|
fontFiles: discovered.fontFiles,
|
|
222
222
|
extraFiles: ship.extraFiles ?? {},
|
|
223
223
|
execArgs: ship.execArgs ?? [],
|
|
224
|
+
executables: resolveShipExecutables({
|
|
225
|
+
declared: ship.executables,
|
|
226
|
+
binaryName,
|
|
227
|
+
projectDir: input.projectDir,
|
|
228
|
+
bundleDir: discovered.bundleDir,
|
|
229
|
+
}),
|
|
224
230
|
outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
|
|
225
231
|
arch: input.cli.arch ?? process.arch,
|
|
226
232
|
app,
|
|
@@ -231,6 +237,61 @@ export function resolveShipSettings(input) {
|
|
|
231
237
|
};
|
|
232
238
|
return { settings, metadata, warnings };
|
|
233
239
|
}
|
|
240
|
+
/** A command name is a file name on every layout and a command a person types. */
|
|
241
|
+
const EXECUTABLE_NAME = /^[a-z0-9][a-z0-9._+-]*$/;
|
|
242
|
+
/**
|
|
243
|
+
* `gjsify.ship.executables` → the further launchers of one package.
|
|
244
|
+
*
|
|
245
|
+
* REFUSED rather than normalised, for each rule: a name collision between two entries
|
|
246
|
+
* is one launcher silently replacing the other (`placeStage` would catch the Linux
|
|
247
|
+
* case and miss a Windows `Foo.cmd`/`foo.cmd` pair on a case-insensitive disk), an
|
|
248
|
+
* entry outside the bundle directory is a file the stage does not carry, and a name
|
|
249
|
+
* with a path separator or a space escapes `bin/`. All three install cleanly and
|
|
250
|
+
* start nothing, which is the class this command is built against.
|
|
251
|
+
*/
|
|
252
|
+
export function resolveShipExecutables(input) {
|
|
253
|
+
const { declared } = input;
|
|
254
|
+
if (declared === undefined)
|
|
255
|
+
return [];
|
|
256
|
+
if (!Array.isArray(declared)) {
|
|
257
|
+
throw new Error('gjsify ship: `gjsify.ship.executables` must be a list of `{ "name", "entry", "gui"? }` objects.');
|
|
258
|
+
}
|
|
259
|
+
const seen = new Set([input.binaryName]);
|
|
260
|
+
return declared.map((row, index) => {
|
|
261
|
+
const at = `gjsify.ship.executables[${index}]`;
|
|
262
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
263
|
+
throw new Error(`gjsify ship: \`${at}\` must be an object with \`name\` and \`entry\`.`);
|
|
264
|
+
}
|
|
265
|
+
const { name, entry, gui, ...unknown } = row;
|
|
266
|
+
const stray = Object.keys(unknown);
|
|
267
|
+
if (stray.length > 0) {
|
|
268
|
+
throw new Error(`gjsify ship: \`${at}\` has unknown key(s) ${stray.join(', ')}; known: name, entry, gui.`);
|
|
269
|
+
}
|
|
270
|
+
if (typeof name !== 'string' || !EXECUTABLE_NAME.test(name)) {
|
|
271
|
+
throw new Error(`gjsify ship: \`${at}.name\` must match ${EXECUTABLE_NAME} — it becomes \`bin/<name>\`, ` +
|
|
272
|
+
`\`Contents/MacOS/<name>\` and \`<name>.cmd\`, so it is lower-case with no path separator or space.`);
|
|
273
|
+
}
|
|
274
|
+
if (seen.has(name)) {
|
|
275
|
+
throw new Error(`gjsify ship: \`${at}.name\` "${name}" is already taken — by the package's own \`binaryName\` ` +
|
|
276
|
+
'or by an earlier entry. Two launchers cannot share one file name.');
|
|
277
|
+
}
|
|
278
|
+
seen.add(name);
|
|
279
|
+
if (typeof entry !== 'string' || entry === '') {
|
|
280
|
+
throw new Error(`gjsify ship: \`${at}.entry\` must be the project-relative path of a built bundle.`);
|
|
281
|
+
}
|
|
282
|
+
if (gui !== undefined && typeof gui !== 'boolean') {
|
|
283
|
+
throw new Error(`gjsify ship: \`${at}.gui\` must be a boolean.`);
|
|
284
|
+
}
|
|
285
|
+
const abs = isAbsolute(entry) ? entry : resolve(input.projectDir, entry);
|
|
286
|
+
const rel = relative(input.bundleDir, abs);
|
|
287
|
+
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
288
|
+
throw new Error(`gjsify ship: \`${at}.entry\` ${entry} is outside ${input.bundleDir}, the directory \`gjsify.main\` ` +
|
|
289
|
+
'lives in — only that directory is staged, so the launcher would point at a file the package ' +
|
|
290
|
+
'does not carry. Build the entries into one directory.');
|
|
291
|
+
}
|
|
292
|
+
return { name, entry: rel.split(sep).join(posix.sep), gui: gui === true };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
234
295
|
/**
|
|
235
296
|
* The ONE place a ship target's ENTRY is decided — {@link resolveShipApp}'s twin.
|
|
236
297
|
*
|
|
@@ -140,6 +140,7 @@ export function toPackSettings(settings) {
|
|
|
140
140
|
minGjsVersion: settings.minGjsVersion,
|
|
141
141
|
minNodeVersion: settings.minNodeVersion,
|
|
142
142
|
flatpak: settings.flatpak,
|
|
143
|
+
executables: settings.executables.map(({ name, gui }) => ({ name, gui })),
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
/** Absolute path of a stage's manifest. */
|
|
@@ -499,6 +500,18 @@ function readPackSettings(data, at) {
|
|
|
499
500
|
minGjsVersion: expectString(data.minGjsVersion, field('minGjsVersion')),
|
|
500
501
|
minNodeVersion: expectString(data.minNodeVersion, field('minNodeVersion')),
|
|
501
502
|
flatpak: readFlatpakSettings(record(data.flatpak, field('flatpak')), field('flatpak')),
|
|
503
|
+
// OPTIONAL on read and NOT a schema bump: a stage written before
|
|
504
|
+
// `gjsify.ship.executables` is a one-executable stage, `[]` is exactly
|
|
505
|
+
// right for it, and an older reader meeting a newer stage refuses the
|
|
506
|
+
// extra `.exe` stubs by name in its architecture check rather than
|
|
507
|
+
// dropping them silently.
|
|
508
|
+
executables: expectArray(data.executables ?? [], field('executables')).map((entry, index) => {
|
|
509
|
+
const row = record(entry, field(`executables[${index}]`));
|
|
510
|
+
if (typeof row.gui !== 'boolean') {
|
|
511
|
+
throw new Error(`gjsify ship: ${field(`executables[${index}].gui`)} must be a boolean.`);
|
|
512
|
+
}
|
|
513
|
+
return { name: expectString(row.name, field(`executables[${index}].name`)), gui: row.gui };
|
|
514
|
+
}),
|
|
502
515
|
};
|
|
503
516
|
}
|
|
504
517
|
/**
|
|
@@ -408,6 +408,24 @@ export interface PackSettings {
|
|
|
408
408
|
minNodeVersion: string;
|
|
409
409
|
/** The Flatpak manifest's non-payload half, fully defaulted at stage time. */
|
|
410
410
|
flatpak: ShipFlatpakSettings;
|
|
411
|
+
/**
|
|
412
|
+
* The package's FURTHER executables (`gjsify.ship.executables`); `[]` for a single-executable
|
|
413
|
+
* package. Name and subsystem only: the entry each one runs is already inside its staged
|
|
414
|
+
* launcher, and the packing host needs the names for the Windows arch exemption.
|
|
415
|
+
*/
|
|
416
|
+
executables: ShipExecutableFacts[];
|
|
417
|
+
}
|
|
418
|
+
/** What the packing host needs to know about one further executable. */
|
|
419
|
+
export interface ShipExecutableFacts {
|
|
420
|
+
/** The command name — `bin/<name>`, `Contents/MacOS/<name>`, `<name>.cmd`. */
|
|
421
|
+
name: string;
|
|
422
|
+
/** Whether the windows layout stages a GUI-subsystem `<name>.exe` for it. */
|
|
423
|
+
gui: boolean;
|
|
424
|
+
}
|
|
425
|
+
/** One further executable, as resolved from `gjsify.ship.executables`. */
|
|
426
|
+
export interface ShipExecutable extends ShipExecutableFacts {
|
|
427
|
+
/** Its entry inside {@link ShipSettings.bundleDir}, POSIX-separated. */
|
|
428
|
+
entry: string;
|
|
411
429
|
}
|
|
412
430
|
/**
|
|
413
431
|
* What the generated Flatpak manifest says about everything that is not the payload.
|
|
@@ -507,6 +525,8 @@ export interface ShipSettings extends PackSettings {
|
|
|
507
525
|
extraFiles: Record<string, string>;
|
|
508
526
|
/** Arguments appended to the launcher's `exec` line. */
|
|
509
527
|
execArgs: string[];
|
|
528
|
+
/** The further executables with their entries; narrows {@link PackSettings.executables}. */
|
|
529
|
+
executables: ShipExecutable[];
|
|
510
530
|
/** Output root; `stage/`, `overlay/` and `out/` hang under it. */
|
|
511
531
|
outDir: string;
|
|
512
532
|
/** Target architecture in `process.arch` spelling. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.16",
|
|
4
4
|
"description": "CLI for Gjsify",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -142,22 +142,22 @@
|
|
|
142
142
|
"cli"
|
|
143
143
|
],
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@gjsify/blueprint": "^0.64.
|
|
146
|
-
"@gjsify/buffer": "^0.64.
|
|
147
|
-
"@gjsify/create-app": "^0.64.
|
|
148
|
-
"@gjsify/node-globals": "^0.64.
|
|
149
|
-
"@gjsify/node-polyfills": "^0.64.
|
|
150
|
-
"@gjsify/npm-registry": "^0.64.
|
|
151
|
-
"@gjsify/resolve-npm": "^0.64.
|
|
152
|
-
"@gjsify/rolldown-plugin-gjsify": "^0.64.
|
|
153
|
-
"@gjsify/vite-plugin-blueprint": "^0.64.
|
|
154
|
-
"@gjsify/rolldown-plugin-pnp": "^0.64.
|
|
155
|
-
"@gjsify/semver": "^0.64.
|
|
156
|
-
"@gjsify/tar": "^0.64.
|
|
157
|
-
"@gjsify/tsc": "^0.64.
|
|
158
|
-
"@gjsify/utils": "^0.64.
|
|
159
|
-
"@gjsify/web-polyfills": "^0.64.
|
|
160
|
-
"@gjsify/workspace": "^0.64.
|
|
145
|
+
"@gjsify/blueprint": "^0.64.16",
|
|
146
|
+
"@gjsify/buffer": "^0.64.16",
|
|
147
|
+
"@gjsify/create-app": "^0.64.16",
|
|
148
|
+
"@gjsify/node-globals": "^0.64.16",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.64.16",
|
|
150
|
+
"@gjsify/npm-registry": "^0.64.16",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.64.16",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.64.16",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.16",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.64.16",
|
|
155
|
+
"@gjsify/semver": "^0.64.16",
|
|
156
|
+
"@gjsify/tar": "^0.64.16",
|
|
157
|
+
"@gjsify/tsc": "^0.64.16",
|
|
158
|
+
"@gjsify/utils": "^0.64.16",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.64.16",
|
|
160
|
+
"@gjsify/workspace": "^0.64.16",
|
|
161
161
|
"acorn": "^8.17.0",
|
|
162
162
|
"cosmiconfig": "^9.0.2",
|
|
163
163
|
"get-tsconfig": "^4.14.0",
|
|
@@ -166,13 +166,13 @@
|
|
|
166
166
|
"yargs": "^18.0.0"
|
|
167
167
|
},
|
|
168
168
|
"devDependencies": {
|
|
169
|
-
"@gjsify/unit": "^0.64.
|
|
169
|
+
"@gjsify/unit": "^0.64.16",
|
|
170
170
|
"@types/yargs": "^17.0.35",
|
|
171
171
|
"typescript": "^6.0.3"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
|
-
"@gjsify/oxfmt-native": "^0.64.
|
|
175
|
-
"@gjsify/rolldown-native": "^0.64.
|
|
174
|
+
"@gjsify/oxfmt-native": "^0.64.16",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.64.16"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
|
178
178
|
"@gjsify/oxfmt-native": {
|