@gjsify/cli 0.64.9 → 0.64.11
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 +59 -59
- package/lib/commands/devtools.js +1 -1
- package/lib/commands/dlx.d.ts +7 -0
- package/lib/commands/dlx.js +6 -1
- package/lib/commands/publish.js +4 -3
- package/lib/commands/ship.js +45 -5
- package/lib/types/config-data.d.ts +25 -0
- package/lib/utils/dlx-cache.d.ts +6 -0
- package/lib/utils/dlx-cache.js +1 -1
- package/lib/utils/install-backend-native.js +3 -2
- package/lib/utils/install-backend.d.ts +10 -0
- package/lib/utils/install-backend.js +3 -1
- package/lib/utils/publish-readback.d.ts +2 -0
- package/lib/utils/publish-readback.js +20 -0
- package/lib/utils/ship/gtk-runtime-acquire.js +6 -0
- package/lib/utils/ship/layout.d.ts +10 -0
- package/lib/utils/ship/layout.js +12 -0
- package/lib/utils/ship/node-modules.d.ts +39 -0
- package/lib/utils/ship/node-modules.js +192 -0
- package/package.json +20 -20
package/lib/commands/devtools.js
CHANGED
|
@@ -62,7 +62,7 @@ system.exit(await runDevtoolsCli(system.programArgs));
|
|
|
62
62
|
* `instances` — so anything a rig does by hand over `gdbus` has a scriptable spelling.
|
|
63
63
|
*/
|
|
64
64
|
export const devtoolsCommand = {
|
|
65
|
-
command: 'devtools',
|
|
65
|
+
command: 'devtools <operation> [operands..]',
|
|
66
66
|
description: 'Drive a running devtools-enabled app from a script: capture a screenshot to a file, find a widget, ' +
|
|
67
67
|
'activate it, send a key. Peer of `gjsify debug`, which speaks MCP for an AI agent.',
|
|
68
68
|
builder: (yargs) => yargs
|
package/lib/commands/dlx.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
2
|
import { type ParsedSpec } from '../utils/parse-spec.js';
|
|
3
|
+
import { type PlatformTarget } from '../utils/platform-check.js';
|
|
3
4
|
interface DlxOptions {
|
|
4
5
|
spec: string;
|
|
5
6
|
binOrArg?: string;
|
|
@@ -31,6 +32,12 @@ export interface EnsureOpts {
|
|
|
31
32
|
* Part of the cache key, so the gjs and node trees never share an entry.
|
|
32
33
|
*/
|
|
33
34
|
extraSpecs?: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Install the tree for this platform rather than the host's: a package that is
|
|
37
|
+
* copied into another machine's artifact (`gjsify ship` fetching a darwin GTK
|
|
38
|
+
* closure on Linux). Part of the cache key.
|
|
39
|
+
*/
|
|
40
|
+
platform?: PlatformTarget;
|
|
34
41
|
}
|
|
35
42
|
/**
|
|
36
43
|
* Resolve (installing + caching when remote) the on-disk directory of a package
|
package/lib/commands/dlx.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveGjsEntry } from '../utils/resolve-gjs-entry.js';
|
|
|
16
16
|
import { doubleDashArgs } from '../utils/double-dash-args.js';
|
|
17
17
|
import { cacheDirFor, createCacheKey, getValidCachedPkg, makePrepareDir, resolveInstalledPkgDir, symlinkSwap, } from '../utils/dlx-cache.js';
|
|
18
18
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
19
|
+
import { describePlatformTarget } from '../utils/platform-check.js';
|
|
19
20
|
export const dlxCommand = {
|
|
20
21
|
command: 'dlx <spec> [binOrArg] [extraArgs..]',
|
|
21
22
|
description: 'Run the GJS bundle of an npm-published package without installing it locally.',
|
|
@@ -118,7 +119,10 @@ export async function ensurePkgDir(parsed, opts) {
|
|
|
118
119
|
return { pkgDir: parsed.path, cachedPkgName: null };
|
|
119
120
|
}
|
|
120
121
|
const specs = [parsed.spec, ...(opts.extraSpecs ?? [])];
|
|
121
|
-
const cacheKey = createCacheKey({
|
|
122
|
+
const cacheKey = createCacheKey({
|
|
123
|
+
packages: specs,
|
|
124
|
+
...(opts.platform ? { platform: describePlatformTarget(opts.platform) } : {}),
|
|
125
|
+
});
|
|
122
126
|
const cacheDir = cacheDirFor(cacheKey);
|
|
123
127
|
const cached = opts.cacheMaxAge > 0 ? getValidCachedPkg(cacheDir, opts.cacheMaxAge) : undefined;
|
|
124
128
|
if (cached) {
|
|
@@ -145,6 +149,7 @@ export async function ensurePkgDir(parsed, opts) {
|
|
|
145
149
|
lockfile: true,
|
|
146
150
|
frozen: opts.frozen,
|
|
147
151
|
progress,
|
|
152
|
+
...(opts.platform ? { platform: opts.platform } : {}),
|
|
148
153
|
});
|
|
149
154
|
const liveTarget = symlinkSwap(cacheDir, prepareDir);
|
|
150
155
|
return {
|
package/lib/commands/publish.js
CHANGED
|
@@ -48,7 +48,7 @@ import { buildPublishHeaders, escapePackageName } from '../utils/publish-headers
|
|
|
48
48
|
import { packWorkspace } from './pack.js';
|
|
49
49
|
import { getNpmTrustedToken, hasGithubOidcEnv, OidcExchangeError, OidcUnavailableError } from '../utils/npm-oidc.js';
|
|
50
50
|
import { diagnose404, is404DiagnosticCandidate } from '../utils/publish-diagnose.js';
|
|
51
|
-
import { DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, probePackageName, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
51
|
+
import { budgetForPayload, DEFAULT_VERIFY_BUDGET_MS, formatUnconfirmedPublish, probePackageName, verifyPublishedVersion, } from '../utils/publish-readback.js';
|
|
52
52
|
import { collectWorkspacePins, REQUIRED_BLOCK } from '../utils/publish-pins.js';
|
|
53
53
|
import { appendLedger, DEFAULT_HOLD_WAIT_MS, findUnavailablePins, holdsPublish, markHeldPublished, pendingEntries, readLedger, recordUnserved, renderLedgerSummary, settleUnserved, summariseReasons, } from '../utils/publish-hold.js';
|
|
54
54
|
import { describeClass, statusTokenFromEnv } from '../utils/publish-status.js';
|
|
@@ -584,9 +584,10 @@ export async function publishWorkspace(input) {
|
|
|
584
584
|
* `publish-unconfirmed`. Same origin, same credential, one request later.
|
|
585
585
|
*/
|
|
586
586
|
const readBack = async () => {
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
587
|
+
const configuredMs = input.verifyBudgetMs ?? DEFAULT_VERIFY_BUDGET_MS;
|
|
588
|
+
if (configuredMs <= 0)
|
|
589
589
|
return undefined;
|
|
590
|
+
const budgetMs = budgetForPayload(configuredMs, bodyStr.length);
|
|
590
591
|
return verifyPublishedVersion({
|
|
591
592
|
registry: registryClean,
|
|
592
593
|
name: packed.name,
|
package/lib/commands/ship.js
CHANGED
|
@@ -35,6 +35,7 @@ import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
|
35
35
|
import { buildDeb } from '../utils/ship/deb.js';
|
|
36
36
|
import { deriveDepends, warnAboutGjsFloor, warnAboutNodeFloor } from '../utils/ship/depends.js';
|
|
37
37
|
import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.js';
|
|
38
|
+
import { resolveNodeModulesClosure, stageNodeModules } from '../utils/ship/node-modules.js';
|
|
38
39
|
import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
|
|
39
40
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
40
41
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
@@ -44,7 +45,7 @@ import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
|
44
45
|
import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
|
|
45
46
|
import { localizeMetadata } from '../utils/ship/localize-metadata.js';
|
|
46
47
|
import { assertHostCanFinish, assertKindCanPack, assertToolsInstalled, canCarryTicket, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
|
|
47
|
-
import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout,
|
|
48
|
+
import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout, payloadArchExemptions, LAYOUT_NAMES, } from '../utils/ship/layout.js';
|
|
48
49
|
import { isNodeRuntimeTarget, resolveNodeRuntime } from '../utils/ship/node-runtime.js';
|
|
49
50
|
import { isUnder, SHARE } from '../utils/ship/share-dirs.js';
|
|
50
51
|
import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/schemas.js';
|
|
@@ -416,6 +417,7 @@ async function assemble(args) {
|
|
|
416
417
|
// carrying a second copy inside the payload would be a private interpreter no
|
|
417
418
|
// distribution update ever patches.
|
|
418
419
|
const runtime = await resolveCarriedRuntime(layout, settings, projectDir);
|
|
420
|
+
carryNodeModules(runtime, layout, settings, projectDir, ship.nodeModules, ship.nodeModulesExclude);
|
|
419
421
|
const stageInputs = {
|
|
420
422
|
bundleFiles: discovered.bundleFiles,
|
|
421
423
|
launcher: renderLauncher(settings, basename(settings.bundlePath), layout, runtime.launcher),
|
|
@@ -510,7 +512,7 @@ async function assemble(args) {
|
|
|
510
512
|
// stub unconditionally (it used to skip arm64 entirely), so an arm64 stage
|
|
511
513
|
// legitimately carries one x64 PE beside its arm64 payload.
|
|
512
514
|
assertPayloadMatchesArch(readStage([stageDir], staged), settings.arch, {
|
|
513
|
-
exempt: layout
|
|
515
|
+
exempt: payloadArchExemptions(layout.os, settings),
|
|
514
516
|
});
|
|
515
517
|
// What the map carried into a layout that has no install step for it.
|
|
516
518
|
//
|
|
@@ -784,12 +786,19 @@ async function resolveCarriedRuntime(layout, settings, projectDir) {
|
|
|
784
786
|
return empty;
|
|
785
787
|
// The closure is fetched, not installed: a project that lists it puts a second
|
|
786
788
|
// GLib next to Homebrew's in its dev tree (see `gtk-runtime-acquire.ts`).
|
|
789
|
+
// Fetched FOR THE TARGET: on a Linux host the package's own `os`/`cpu` gate
|
|
790
|
+
// refused it as EBADPLATFORM, and every macOS and Windows artifact assembled
|
|
791
|
+
// on Linux shipped without GTK, starting only where Homebrew happened to have it.
|
|
787
792
|
const acquired = await acquireGtkRuntime({
|
|
788
793
|
target,
|
|
789
794
|
version: cliVersion(),
|
|
790
795
|
discovery: { cwd: projectDir },
|
|
791
|
-
ensure: async (spec) => (await ensurePkgDir(parseSpec(spec), {
|
|
792
|
-
|
|
796
|
+
ensure: async (spec) => (await ensurePkgDir(parseSpec(spec), {
|
|
797
|
+
verbose: false,
|
|
798
|
+
cacheMaxAge: DLX_CACHE_MAX_AGE_MIN,
|
|
799
|
+
frozen: false,
|
|
800
|
+
platform: { os: layout.os, cpu: settings.arch },
|
|
801
|
+
})).pkgDir,
|
|
793
802
|
log: (message) => console.log(`${LOG} ${message}`),
|
|
794
803
|
});
|
|
795
804
|
return stageAppRuntime({
|
|
@@ -802,6 +811,35 @@ async function resolveCarriedRuntime(layout, settings, projectDir) {
|
|
|
802
811
|
...(acquired.kind === 'failed' ? { gtkFetchFailure: acquired.reason } : {}),
|
|
803
812
|
});
|
|
804
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* `gjsify.ship.nodeModules`: the npm packages an `--app node` bundle imports and cannot contain,
|
|
816
|
+
* staged beside the bundle with what they need at runtime (`utils/ship/node-modules.ts`).
|
|
817
|
+
*
|
|
818
|
+
* Only for a node payload: a GJS bundle inlines or rewrites them, so one declaration can serve a
|
|
819
|
+
* project that ships GJS on Linux and Node on macOS and Windows. Mutates `runtime` because the
|
|
820
|
+
* files take the same route as the carried interpreter — already stage-relative, past the planner.
|
|
821
|
+
*/
|
|
822
|
+
function carryNodeModules(runtime, layout, settings, projectDir, declared, exclude) {
|
|
823
|
+
if (declared === undefined || declared.length === 0 || settings.app !== 'node')
|
|
824
|
+
return;
|
|
825
|
+
const closure = resolveNodeModulesClosure(declared, projectDir, {
|
|
826
|
+
target: { os: layout.os, cpu: settings.arch },
|
|
827
|
+
exclude,
|
|
828
|
+
});
|
|
829
|
+
runtime.files.push(...stageNodeModules(closure, layout.dirs(settings).bundle));
|
|
830
|
+
runtime.found.push(`node_modules: ${closure.packages.length} package(s) for ${declared.join(', ')}`);
|
|
831
|
+
if (closure.excluded.length > 0) {
|
|
832
|
+
console.log(`${LOG} node_modules: left out by gjsify.ship.nodeModulesExclude: ${closure.excluded.join(', ')}`);
|
|
833
|
+
}
|
|
834
|
+
if (closure.skippedForTarget.length > 0) {
|
|
835
|
+
console.log(`${LOG} node_modules: left out, installed but built for another platform than ${layout.os}-${settings.arch}: ` +
|
|
836
|
+
closure.skippedForTarget.join(', '));
|
|
837
|
+
}
|
|
838
|
+
if (closure.skippedOptional.length > 0) {
|
|
839
|
+
console.log(`${LOG} node_modules: ${closure.skippedOptional.length} optional dependenc(ies) not installed, ` +
|
|
840
|
+
`so not carried (other platforms' builds): ${closure.skippedOptional.join(', ')}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
805
843
|
/** How long a fetched runtime stays in the dlx cache — the same week `gjsify showcase` uses. */
|
|
806
844
|
const DLX_CACHE_MAX_AGE_MIN = 60 * 24 * 7;
|
|
807
845
|
/**
|
|
@@ -979,7 +1017,9 @@ async function packOne(input) {
|
|
|
979
1017
|
// asserting afterwards would compare against a label that no longer names an
|
|
980
1018
|
// architecture, and the mismatch that matters is exactly the one on a payload
|
|
981
1019
|
// that IS architecture-specific.
|
|
982
|
-
assertPayloadMatchesArch(assembled, input.arch
|
|
1020
|
+
assertPayloadMatchesArch(assembled, input.arch, {
|
|
1021
|
+
exempt: payloadArchExemptions(format.layoutOs, settings),
|
|
1022
|
+
});
|
|
983
1023
|
// The second half of the same idea, on the other label this artifact carries:
|
|
984
1024
|
// something declares which interpreter runs the app, and until now nothing
|
|
985
1025
|
// compared it to the launcher that will. They CAN disagree — a stage assembled
|
|
@@ -606,6 +606,31 @@ export interface ConfigDataShip extends AppMetadata {
|
|
|
606
606
|
* bundle seal and `dlopen` both refuse. A target with no key stages nothing.
|
|
607
607
|
*/
|
|
608
608
|
bundledTypelibs?: string[] | ShipBundledTypelibsOptions;
|
|
609
|
+
/**
|
|
610
|
+
* npm packages the `--app node` bundle imports but cannot contain, by NAME — e.g.
|
|
611
|
+
* `["node-llama-cpp"]`.
|
|
612
|
+
*
|
|
613
|
+
* An `--app node` build keeps every Node-API addon package external (an addon finds its
|
|
614
|
+
* `.node` relative to its own files), so the bundle `import`s it by name at runtime. A `.app`
|
|
615
|
+
* or program directory has no `npm install`; without this entry the artifact fails with
|
|
616
|
+
* `ERR_MODULE_NOT_FOUND` before the program starts. Each name is staged into the bundle's
|
|
617
|
+
* `node_modules/` with its `dependencies` and `optionalDependencies`, resolved from the
|
|
618
|
+
* project the way Node would, at the paths they are installed at (two versions of one package
|
|
619
|
+
* stay nested). An optional dependency that is not installed is skipped, which is how a
|
|
620
|
+
* per-platform addon package is chosen: `gjsify install --os <os> --cpu <cpu>` before `ship`.
|
|
621
|
+
* A required one that is missing is an error.
|
|
622
|
+
*
|
|
623
|
+
* Ignored for a GJS payload, so one declaration serves a project that ships GJS on Linux and
|
|
624
|
+
* Node on macOS and Windows. `@gjsify/node-gi` is not listed here; `ship` carries it itself.
|
|
625
|
+
*/
|
|
626
|
+
nodeModules?: string[];
|
|
627
|
+
/**
|
|
628
|
+
* Packages {@link ConfigDataShip.nodeModules} leaves out wherever they turn up in the closure —
|
|
629
|
+
* for an optional dependency the project does not want to ship, e.g. the CUDA and Vulkan
|
|
630
|
+
* builds of an inference addon (`@node-llama-cpp/win-x64-cuda`, hundreds of MB) when the
|
|
631
|
+
* program only runs on the CPU. Names a declared package itself are not affected.
|
|
632
|
+
*/
|
|
633
|
+
nodeModulesExclude?: string[];
|
|
609
634
|
/**
|
|
610
635
|
* Additions for ONE package format — `deb` or `rpm` — on top of everything above.
|
|
611
636
|
*
|
package/lib/utils/dlx-cache.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
interface CacheKeyOpts {
|
|
2
2
|
packages: string[];
|
|
3
3
|
registries?: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* The platform the tree was installed FOR (`darwin-arm64`), when not the host's.
|
|
6
|
+
* The same specs filter to a different tree per platform. Absent, the key is
|
|
7
|
+
* the one it always was, so existing cache entries stay valid.
|
|
8
|
+
*/
|
|
9
|
+
platform?: string;
|
|
4
10
|
}
|
|
5
11
|
/** Stable, sorted JSON hash of inputs. */
|
|
6
12
|
export declare function createCacheKey(opts: CacheKeyOpts): string;
|
package/lib/utils/dlx-cache.js
CHANGED
|
@@ -19,7 +19,7 @@ function lexCompare(a, b) {
|
|
|
19
19
|
export function createCacheKey(opts) {
|
|
20
20
|
const sortedPkgs = [...opts.packages].sort(lexCompare);
|
|
21
21
|
const sortedRegs = Object.entries(opts.registries ?? {}).sort(([a], [b]) => lexCompare(a, b));
|
|
22
|
-
const payload = JSON.stringify([sortedPkgs, sortedRegs]);
|
|
22
|
+
const payload = JSON.stringify(opts.platform === undefined ? [sortedPkgs, sortedRegs] : [sortedPkgs, sortedRegs, opts.platform]);
|
|
23
23
|
return createHash('sha256').update(payload).digest('hex');
|
|
24
24
|
}
|
|
25
25
|
/** $XDG_CACHE_HOME/gjsify/dlx — created if missing. */
|
|
@@ -143,8 +143,9 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
143
143
|
// The triple this install materialises FOR. Normally the running host;
|
|
144
144
|
// `--os/--cpu/--libc` (npm's config keys, read from the environment by
|
|
145
145
|
// `resolveHostPlatform`) override it, which is what makes the darwin/win32/musl
|
|
146
|
-
// branches reviewable from one machine.
|
|
147
|
-
|
|
146
|
+
// branches reviewable from one machine. A caller that installs for another
|
|
147
|
+
// machine's artifact (`opts.platform`) wins over both.
|
|
148
|
+
const target = opts.platform ?? resolveHostPlatform();
|
|
148
149
|
const force = readPlatformForce(process.env);
|
|
149
150
|
log('install: platform target %s%s', describePlatformTarget(target), force ? ' (--force: checks bypassed)' : '');
|
|
150
151
|
let nodes;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProgressReporter } from './install-progress.js';
|
|
2
|
+
import type { PlatformTarget } from './platform-check.js';
|
|
2
3
|
export type { ProgressEvent, ProgressPhase, ProgressReporter } from './install-progress.js';
|
|
3
4
|
export { makeProgressReporter } from './install-progress.js';
|
|
4
5
|
export interface InstallOptions {
|
|
@@ -79,6 +80,15 @@ export interface InstallOptions {
|
|
|
79
80
|
* and the warning falls back to bare ranges.
|
|
80
81
|
*/
|
|
81
82
|
specOrigins?: Map<string, string[]>;
|
|
83
|
+
/**
|
|
84
|
+
* The `os`/`cpu`/`libc` triple to install FOR, instead of the host's (and of
|
|
85
|
+
* `npm_config_os`/`_cpu`/`_libc`). For a tree that is copied into an artifact
|
|
86
|
+
* for another machine and never run here — `gjsify ship` assembling a darwin or
|
|
87
|
+
* win32 bundle on Linux fetches that target's GTK closure, whose `os`/`cpu`
|
|
88
|
+
* gate would otherwise refuse it as EBADPLATFORM (ADR 0024 § A1). The npm
|
|
89
|
+
* backend passes it on as `--os`/`--cpu`/`--libc`.
|
|
90
|
+
*/
|
|
91
|
+
platform?: PlatformTarget;
|
|
82
92
|
}
|
|
83
93
|
export interface InstallResult {
|
|
84
94
|
/** Requested top-level packages with the version each resolved to. Empty for
|
|
@@ -24,7 +24,7 @@ export async function installPackages(opts) {
|
|
|
24
24
|
const installed = await installPackagesNative(opts);
|
|
25
25
|
return { installed };
|
|
26
26
|
}
|
|
27
|
-
async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
27
|
+
async function installViaNpm({ prefix, specs, verbose, registry, platform }) {
|
|
28
28
|
if (specs.length === 0) {
|
|
29
29
|
throw new Error('installPackages: empty specs list');
|
|
30
30
|
}
|
|
@@ -42,6 +42,8 @@ async function installViaNpm({ prefix, specs, verbose, registry }) {
|
|
|
42
42
|
'--prefix',
|
|
43
43
|
prefix,
|
|
44
44
|
...(verbose ? ['--loglevel', 'verbose'] : ['--loglevel', 'warn']),
|
|
45
|
+
...(platform ? ['--os', platform.os, '--cpu', platform.cpu] : []),
|
|
46
|
+
...(platform?.libc ? ['--libc', platform.libc] : []),
|
|
45
47
|
...specs,
|
|
46
48
|
];
|
|
47
49
|
// Through `spawnToCompletion` because a bare `spawn('npm', …)` is ENOENT on
|
|
@@ -146,6 +146,8 @@ export declare function probeTimeoutFor(configuredMs: number, remainingMs: numbe
|
|
|
146
146
|
* state is what the `recorded-not-served` verdict names.
|
|
147
147
|
*/
|
|
148
148
|
export declare const DEFAULT_VERIFY_BUDGET_MS = 600000;
|
|
149
|
+
/** The read-back budget for a payload: `configuredMs`, longer if it is the default and the upload is large. */
|
|
150
|
+
export declare function budgetForPayload(configuredMs: number, payloadBytes: number): number;
|
|
149
151
|
/**
|
|
150
152
|
* Poll the registry until `name@version` resolves, or the budget runs out, then
|
|
151
153
|
* say WHICH of the three states it found (see {@link ReadbackVerdict}).
|
|
@@ -138,6 +138,26 @@ export function probeTimeoutFor(configuredMs, remainingMs) {
|
|
|
138
138
|
* state is what the `recorded-not-served` verdict names.
|
|
139
139
|
*/
|
|
140
140
|
export const DEFAULT_VERIFY_BUDGET_MS = 600_000;
|
|
141
|
+
/**
|
|
142
|
+
* Payload size above which the DEFAULT window starts to grow, and the growth per MiB.
|
|
143
|
+
*
|
|
144
|
+
* v0.64.9, run 40145205912: `@gjsify/gtk-runtime-win32-arm64` PUT 69.7 MB, got a 202,
|
|
145
|
+
* and the read-back gave up after 600 s with `not-published`; the registry's `time`
|
|
146
|
+
* map recorded the version 20 s later. The lag scales with what npm has to ingest,
|
|
147
|
+
* and the 600 s ceiling was measured on packages of a few MB. So the window grows by
|
|
148
|
+
* 10 s per MiB past 32 MiB (that package: ~945 s). Only the PATIENT default grows —
|
|
149
|
+
* an explicit short budget (the sweep's `--verify-timeout 5`) is a caller's decision.
|
|
150
|
+
* An upload npm accepted but never recorded still ends in `not-published`.
|
|
151
|
+
*/
|
|
152
|
+
const LARGE_PAYLOAD_BYTES = 32 * 1024 * 1024;
|
|
153
|
+
const EXTRA_BUDGET_PER_MIB_MS = 10_000;
|
|
154
|
+
/** The read-back budget for a payload: `configuredMs`, longer if it is the default and the upload is large. */
|
|
155
|
+
export function budgetForPayload(configuredMs, payloadBytes) {
|
|
156
|
+
if (configuredMs < DEFAULT_VERIFY_BUDGET_MS || payloadBytes <= LARGE_PAYLOAD_BYTES)
|
|
157
|
+
return configuredMs;
|
|
158
|
+
const extraMiB = (payloadBytes - LARGE_PAYLOAD_BYTES) / (1024 * 1024);
|
|
159
|
+
return configuredMs + Math.ceil(extraMiB) * EXTRA_BUDGET_PER_MIB_MS;
|
|
160
|
+
}
|
|
141
161
|
/**
|
|
142
162
|
* Make one probe's URL uncacheable, by giving it a key no cache holds.
|
|
143
163
|
*
|
|
@@ -40,6 +40,12 @@ export async function acquireGtkRuntime(input) {
|
|
|
40
40
|
return { kind: 'present' };
|
|
41
41
|
const name = gtkRuntimePackageName(input.target);
|
|
42
42
|
const spec = `${name}@${input.version}`;
|
|
43
|
+
// An offline build, or a suite that must not download 100 MB per run, stages what is
|
|
44
|
+
// reachable and nothing more. The e2e suites that assert "a project without the packages
|
|
45
|
+
// stages no runtime" held only while the fetch failed on Linux for every foreign target.
|
|
46
|
+
if ((discovery.env ?? process.env)['GJSIFY_GTK_RUNTIME_FETCH'] === '0') {
|
|
47
|
+
return { kind: 'failed', reason: `GJSIFY_GTK_RUNTIME_FETCH=0 turned off the fetch of ${spec}` };
|
|
48
|
+
}
|
|
43
49
|
input.log?.(`${name} is not installed here, and is not meant to be — fetching ${spec} into the cache ` +
|
|
44
50
|
'(about 100 MB, once)');
|
|
45
51
|
let dir;
|
|
@@ -330,6 +330,16 @@ export declare function launcherPath(layout: Layout, identity: LayoutIdentity):
|
|
|
330
330
|
* is not there, at exit 0, with a window that never appears.
|
|
331
331
|
*/
|
|
332
332
|
export declare function windowsGuiLauncherPath(identity: LayoutIdentity): string;
|
|
333
|
+
/**
|
|
334
|
+
* The payload paths exempt from the architecture check for one OS (`assertPayloadMatchesArch`).
|
|
335
|
+
*
|
|
336
|
+
* On windows that is the GUI-launcher stub, x86-64 machine code on every windows arch by design.
|
|
337
|
+
* ONE function for the two places that read the stage back — the assemble phase and the pack
|
|
338
|
+
* phase — because the pack phase once had its own call without the exemption: an arm64 stage
|
|
339
|
+
* assembled fine and every package refused it with "stamped.exe is built for x64". `undefined`
|
|
340
|
+
* for an OS with no such file.
|
|
341
|
+
*/
|
|
342
|
+
export declare function payloadArchExemptions(os: HostOs, identity: LayoutIdentity): ReadonlySet<string> | undefined;
|
|
333
343
|
/**
|
|
334
344
|
* What the windows GUI launcher calls the file it writes under `%TEMP%`.
|
|
335
345
|
*
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -552,6 +552,18 @@ export function windowsGuiLauncherPath(identity) {
|
|
|
552
552
|
}
|
|
553
553
|
return posix.join(layout.dirs(identity).launcher, `${identity.binaryName}.exe`);
|
|
554
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* The payload paths exempt from the architecture check for one OS (`assertPayloadMatchesArch`).
|
|
557
|
+
*
|
|
558
|
+
* On windows that is the GUI-launcher stub, x86-64 machine code on every windows arch by design.
|
|
559
|
+
* ONE function for the two places that read the stage back — the assemble phase and the pack
|
|
560
|
+
* phase — because the pack phase once had its own call without the exemption: an arm64 stage
|
|
561
|
+
* assembled fine and every package refused it with "stamped.exe is built for x64". `undefined`
|
|
562
|
+
* for an OS with no such file.
|
|
563
|
+
*/
|
|
564
|
+
export function payloadArchExemptions(os, identity) {
|
|
565
|
+
return os === 'win32' ? new Set([windowsGuiLauncherPath(identity)]) : undefined;
|
|
566
|
+
}
|
|
555
567
|
/**
|
|
556
568
|
* What the windows GUI launcher calls the file it writes under `%TEMP%`.
|
|
557
569
|
*
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { StagedFile } from './types.js';
|
|
2
|
+
/** One package of the closure, ready to stage. */
|
|
3
|
+
export interface NodeModulePackage {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
/** Where the files are read from — the installed directory. */
|
|
7
|
+
dir: string;
|
|
8
|
+
/** POSIX path below the staged `node_modules/`: `yargs`, `a/node_modules/b`, `@scope/pkg`. */
|
|
9
|
+
dest: string;
|
|
10
|
+
}
|
|
11
|
+
/** The platform the artifact is for, spelled like npm's `os` / `cpu` fields (`darwin`, `win32`; `arm64`, `x64`). */
|
|
12
|
+
export interface NodeModulesTarget {
|
|
13
|
+
os: string;
|
|
14
|
+
cpu: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NodeModulesOptions {
|
|
17
|
+
/** Platform filter, see {@link NodeModulesTarget}. Without one nothing is filtered by `os` / `cpu`. */
|
|
18
|
+
target?: NodeModulesTarget;
|
|
19
|
+
/** Package names left out wherever they appear below a declared one — `gjsify.ship.nodeModulesExclude`. */
|
|
20
|
+
exclude?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export interface NodeModulesClosure {
|
|
23
|
+
packages: NodeModulePackage[];
|
|
24
|
+
/** Optional dependencies that are not installed — expected for other platforms, listed for the log. */
|
|
25
|
+
skippedOptional: string[];
|
|
26
|
+
/** Dependencies installed here but built for another platform, named by their `os` / `cpu` fields. */
|
|
27
|
+
skippedForTarget: string[];
|
|
28
|
+
/** Dependencies the project asked to leave out, and that were reached. */
|
|
29
|
+
excluded: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve `names` and everything they need at runtime, from `cwd`.
|
|
33
|
+
*
|
|
34
|
+
* Throws on a name that is not installed, a required dependency that is not, and two different
|
|
35
|
+
* installed copies that would land on one staged path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveNodeModulesClosure(names: readonly string[], cwd: string, options?: NodeModulesOptions): NodeModulesClosure;
|
|
38
|
+
/** The closure as stage-relative files, under `<bundleDir>/node_modules/`. */
|
|
39
|
+
export declare function stageNodeModules(closure: NodeModulesClosure, bundleDir: string): StagedFile[];
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// npm packages an `--app node` bundle imports but cannot contain: found, then staged as a TREE.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. `rolldown-plugin-gjsify`'s `nodeNativeExternalPlugin` keeps every Node-API
|
|
5
|
+
// addon package EXTERNAL in an `--app node` bundle, because an addon finds its `.node` relative
|
|
6
|
+
// to its own files and a bundle moves them ("an `--app node` bundle of a native dependency needs
|
|
7
|
+
// `node_modules` at runtime, like any `npm install`-ed program"). A shipped `.app` or program
|
|
8
|
+
// directory has no `npm install`. Until this module the only package `ship` carried was
|
|
9
|
+
// `@gjsify/node-gi`, by name — so a project with one more native dependency produced an artifact
|
|
10
|
+
// that fails at its first `import` on every machine but the author's. Measured on a real consumer
|
|
11
|
+
// (Stamped, `node-llama-cpp`): the released macOS `.app` exited with
|
|
12
|
+
// `ERR_MODULE_NOT_FOUND: Cannot find package 'node-llama-cpp'` before opening a window, and no
|
|
13
|
+
// CI step noticed because nothing ever started the artifact.
|
|
14
|
+
//
|
|
15
|
+
// THE DECLARATION. `gjsify.ship.nodeModules` lists package NAMES, nothing else. The names are the
|
|
16
|
+
// entry points the bundle imports; this module adds everything they need at runtime.
|
|
17
|
+
//
|
|
18
|
+
// THE CLOSURE is node's own resolution rule applied to the installed tree: for each package,
|
|
19
|
+
// `dependencies` and `optionalDependencies`, each found by walking `node_modules` upward from the
|
|
20
|
+
// package that asks for it. A missing optional dependency is skipped silently — that is the
|
|
21
|
+
// per-platform filter (`@node-llama-cpp/mac-arm64-metal` is optional; `gjsify install --os
|
|
22
|
+
// darwin --cpu arm64` is what puts the right one on disk; one that IS on disk but whose `os` /
|
|
23
|
+
// `cpu` fields exclude the artifact's target is left out too, since installs accumulate) — and a missing required one is an
|
|
24
|
+
// error naming the chain, because a package without its dependency is the very artifact this
|
|
25
|
+
// exists to prevent.
|
|
26
|
+
//
|
|
27
|
+
// THE LAYOUT IS THE INSTALLED ONE. Each package lands at the path it was FOUND at, relative to
|
|
28
|
+
// the first `node_modules` in that path: `node_modules/a/node_modules/b` stays nested, which is
|
|
29
|
+
// how two versions of `yargs` coexist. Flattening would be a guess about which one wins.
|
|
30
|
+
//
|
|
31
|
+
// NOT INCLUDED: `@gjsify/node-gi`, which `app-runtime.ts` stages as a trimmed tree of its own, and
|
|
32
|
+
// any nested `node_modules` that is not reached through a declared dependency (an npm install
|
|
33
|
+
// leaves dev-only trees there).
|
|
34
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
35
|
+
import { dirname, join, posix, relative, sep } from 'node:path';
|
|
36
|
+
import { isExecutableAsset } from './plan.js';
|
|
37
|
+
/** The package this module leaves to `app-runtime.ts`. */
|
|
38
|
+
const LEFT_TO_APP_RUNTIME = '@gjsify/node-gi';
|
|
39
|
+
function readManifest(dir) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** `<from>/node_modules/<name>`, then each ancestor's — node's lookup, on the file system alone. */
|
|
48
|
+
function findInstalled(name, from) {
|
|
49
|
+
let dir = from;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const candidate = join(dir, 'node_modules', ...name.split('/'));
|
|
52
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
53
|
+
return candidate;
|
|
54
|
+
const parent = dirname(dir);
|
|
55
|
+
if (parent === dir)
|
|
56
|
+
return null;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** npm's rule for `os` / `cpu`: a `!x` entry blocks `x`; otherwise, if any plain entry exists, `value` must be one. */
|
|
61
|
+
function allows(field, value) {
|
|
62
|
+
const list = typeof field === 'string' ? [field] : (field ?? []);
|
|
63
|
+
if (list.includes(`!${value}`))
|
|
64
|
+
return false;
|
|
65
|
+
const positive = list.filter((entry) => !entry.startsWith('!'));
|
|
66
|
+
return positive.length === 0 || positive.includes(value);
|
|
67
|
+
}
|
|
68
|
+
/** The path below the first `node_modules` segment, POSIX-separated, or `null` when there is none. */
|
|
69
|
+
function destinationOf(found) {
|
|
70
|
+
const parts = found.split(sep);
|
|
71
|
+
const at = parts.indexOf('node_modules');
|
|
72
|
+
return at === -1 || at === parts.length - 1 ? null : parts.slice(at + 1).join('/');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve `names` and everything they need at runtime, from `cwd`.
|
|
76
|
+
*
|
|
77
|
+
* Throws on a name that is not installed, a required dependency that is not, and two different
|
|
78
|
+
* installed copies that would land on one staged path.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveNodeModulesClosure(names, cwd, options = {}) {
|
|
81
|
+
const { target, exclude = [] } = options;
|
|
82
|
+
const packages = new Map();
|
|
83
|
+
const skippedOptional = [];
|
|
84
|
+
const skippedForTarget = [];
|
|
85
|
+
const excluded = [];
|
|
86
|
+
const visit = (name, from, via, optional) => {
|
|
87
|
+
if (name === LEFT_TO_APP_RUNTIME)
|
|
88
|
+
return;
|
|
89
|
+
if (via.length > 0 && exclude.includes(name)) {
|
|
90
|
+
if (!excluded.includes(name))
|
|
91
|
+
excluded.push(name);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const found = findInstalled(name, from);
|
|
95
|
+
if (found === null) {
|
|
96
|
+
if (optional) {
|
|
97
|
+
if (!skippedOptional.includes(name))
|
|
98
|
+
skippedOptional.push(name);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`gjsify ship: \`${name}\` is not installed${via.length > 0 ? ` (needed by ${via.join(' > ')})` : ''}. ` +
|
|
102
|
+
(via.length === 0
|
|
103
|
+
? '`gjsify.ship.nodeModules` names it, so the artifact must carry it — install it, or remove the entry.'
|
|
104
|
+
: 'A package without its required dependency is an artifact that fails at its first import.'));
|
|
105
|
+
}
|
|
106
|
+
const dest = destinationOf(found);
|
|
107
|
+
if (dest === null) {
|
|
108
|
+
throw new Error(`gjsify ship: ${found} is not inside a node_modules directory, so it has no place to be staged.`);
|
|
109
|
+
}
|
|
110
|
+
const dir = realpathSync(found);
|
|
111
|
+
const manifest = readManifest(dir);
|
|
112
|
+
if (manifest === null)
|
|
113
|
+
throw new Error(`gjsify ship: ${join(dir, 'package.json')} is unreadable.`);
|
|
114
|
+
// A dependency built for another platform may well be on disk: `gjsify install --os` adds to a
|
|
115
|
+
// tree, it does not clear it. Carrying it would put a foreign binary in the artifact.
|
|
116
|
+
if (via.length > 0 &&
|
|
117
|
+
target !== undefined &&
|
|
118
|
+
!(allows(manifest.os, target.os) && allows(manifest.cpu, target.cpu))) {
|
|
119
|
+
if (!skippedForTarget.includes(name))
|
|
120
|
+
skippedForTarget.push(name);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const existing = packages.get(dest);
|
|
124
|
+
if (existing !== undefined) {
|
|
125
|
+
if (existing.dir !== dir) {
|
|
126
|
+
throw new Error(`gjsify ship: two installed copies of \`${name}\` would both be staged at node_modules/${dest} ` +
|
|
127
|
+
`(${existing.dir} and ${dir}).`);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
packages.set(dest, { name, version: manifest.version ?? '0.0.0', dir, dest });
|
|
132
|
+
const chain = [...via, name];
|
|
133
|
+
for (const dep of Object.keys(manifest.dependencies ?? {}))
|
|
134
|
+
visit(dep, found, chain, false);
|
|
135
|
+
for (const dep of Object.keys(manifest.optionalDependencies ?? {}))
|
|
136
|
+
visit(dep, found, chain, true);
|
|
137
|
+
};
|
|
138
|
+
for (const name of names)
|
|
139
|
+
visit(name, cwd, [], false);
|
|
140
|
+
return {
|
|
141
|
+
packages: [...packages.values()].sort((a, b) => (a.dest < b.dest ? -1 : 1)),
|
|
142
|
+
skippedOptional: skippedOptional.sort(),
|
|
143
|
+
skippedForTarget: skippedForTarget.sort(),
|
|
144
|
+
excluded: excluded.sort(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Every file of one package, package-relative, without the `node_modules` directories inside it —
|
|
149
|
+
* those are separate packages of the closure, or not wanted at all.
|
|
150
|
+
*/
|
|
151
|
+
function listPackageFiles(root) {
|
|
152
|
+
const out = [];
|
|
153
|
+
const walk = (current) => {
|
|
154
|
+
for (const entry of readdirSync(current, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
|
|
155
|
+
const full = join(current, entry.name);
|
|
156
|
+
if (entry.isDirectory()) {
|
|
157
|
+
if (entry.name !== 'node_modules')
|
|
158
|
+
walk(full);
|
|
159
|
+
}
|
|
160
|
+
else if (entry.isSymbolicLink()) {
|
|
161
|
+
throw new Error(`gjsify ship: ${full} is a symlink. The payload has to be self-contained — a package that ` +
|
|
162
|
+
'links inside itself needs the link replaced by the file.');
|
|
163
|
+
}
|
|
164
|
+
else if (entry.isFile()) {
|
|
165
|
+
out.push(posix.join(...relative(root, full).split(sep)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
walk(root);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/** 0755 for what the source marks executable or is a shared library, 0644 for the rest. */
|
|
173
|
+
function modeOf(rel, abs) {
|
|
174
|
+
if (isExecutableAsset(rel))
|
|
175
|
+
return 0o755;
|
|
176
|
+
return (statSync(abs).mode & 0o111) !== 0 ? 0o755 : 0o644;
|
|
177
|
+
}
|
|
178
|
+
/** The closure as stage-relative files, under `<bundleDir>/node_modules/`. */
|
|
179
|
+
export function stageNodeModules(closure, bundleDir) {
|
|
180
|
+
const files = [];
|
|
181
|
+
for (const pkg of closure.packages) {
|
|
182
|
+
for (const rel of listPackageFiles(pkg.dir)) {
|
|
183
|
+
const abs = join(pkg.dir, ...rel.split('/'));
|
|
184
|
+
files.push({
|
|
185
|
+
path: posix.join(bundleDir, 'node_modules', pkg.dest, rel),
|
|
186
|
+
mode: modeOf(rel, abs),
|
|
187
|
+
source: { kind: 'file', path: abs },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return files;
|
|
192
|
+
}
|