@gjsify/cli 0.64.13-next.283 → 0.64.14
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 +224 -221
- package/lib/cli-app.js +2 -1
- package/lib/commands/index.d.ts +1 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/release-notes.d.ts +7 -0
- package/lib/commands/release-notes.js +42 -0
- package/lib/utils/app-metadata.d.ts +3 -1
- package/lib/utils/app-metadata.js +1 -1
- package/lib/utils/check-system-deps.js +1 -0
- package/lib/utils/release-notes.d.ts +19 -0
- package/lib/utils/release-notes.js +49 -0
- package/lib/utils/ship/layout.d.ts +18 -0
- package/lib/utils/ship/layout.js +30 -1
- package/lib/utils/ship/signing.d.ts +36 -0
- package/lib/utils/ship/signing.js +77 -16
- package/package.json +21 -22
package/lib/cli-app.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import yargs from 'yargs';
|
|
19
19
|
import { classifyCliFailure } from './cli-fail.js';
|
|
20
20
|
import { cliVersion } from './utils/publish-headers.js';
|
|
21
|
-
import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
|
|
21
|
+
import { buildCommand as build, clearCommand as clear, copyCommand as copy, envCommand as env, execCommand as exec, testCommand as test, runCommand as run, infoCommand as info, systemCheckCommand as systemCheck, checkCommand as check, showcaseCommand as showcase, createCommand as create, gresourceCommand as gresource, gettextCommand as gettext, gsettingsCommand as gsettings, flatpakCommand as flatpak, shipCommand as ship, dlxCommand as dlx, installCommand as install, linkCommand as link, unlinkCommand as unlink, foreachCommand as foreach, workspaceCommand as workspace, packCommand as pack, publishCommand as publish, whoamiCommand as whoami, releaseNotesCommand as releaseNotes, loginCommand as login, logoutCommand as logout, trustCommand as trust, onboardCommand as onboard, selfUpdateCommand as selfUpdate, generateInstallerCommand as generateInstaller, pruneCommand as prune, uninstallCommand as uninstall, formatCommand as format, lintCommand as lint, fixCommand as fix, upgradeCommand as upgrade, barrelsCommand as barrels, tscCommand as tsc, affectedCommand as affected, storybookCommand as storybook, siteCommand as site, devCommand as dev, debugCommand as debug, devtoolsCommand as devtools, browseCommand as browse, webextCommand as webext, blueprintCommand as blueprint, } from './commands/index.js';
|
|
22
22
|
import { APP_NAME } from './constants.js';
|
|
23
23
|
import { isBun, isDeno, isNode, gjsSystemVersion } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
24
24
|
import { ensureGjsifyShimOnPath } from './utils/gjsify-shim.js';
|
|
@@ -117,6 +117,7 @@ export async function runCli(argv) {
|
|
|
117
117
|
.command(pack.command, pack.description, pack.builder, pack.handler)
|
|
118
118
|
.command(publish.command, publish.description, publish.builder, publish.handler)
|
|
119
119
|
.command(whoami.command, whoami.description, whoami.builder, whoami.handler)
|
|
120
|
+
.command(releaseNotes.command, releaseNotes.description, releaseNotes.builder, releaseNotes.handler)
|
|
120
121
|
.command(login.command, login.description, login.builder, login.handler)
|
|
121
122
|
.command(logout.command, logout.description, logout.builder, logout.handler)
|
|
122
123
|
.command(trust.command, trust.description, trust.builder, trust.handler)
|
package/lib/commands/index.d.ts
CHANGED
package/lib/commands/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// `gjsify release-notes [release] [--compare]`
|
|
2
|
+
//
|
|
3
|
+
// Prints one `releases` entry as GitHub Markdown, so a release workflow can pass
|
|
4
|
+
// it to `gh release create --notes-file -` instead of an empty body. Exits 1 when
|
|
5
|
+
// the version has no entry or no description: a release job must fail loudly
|
|
6
|
+
// rather than publish nothing.
|
|
7
|
+
import { Config } from '../config.js';
|
|
8
|
+
import { readPackageJson } from '../utils/pkg-json.js';
|
|
9
|
+
import { renderReleaseNotes, stripVersionPrefix } from '../utils/release-notes.js';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
export const releaseNotesCommand = {
|
|
12
|
+
command: 'release-notes [release]',
|
|
13
|
+
description: 'Print the `releases` entry for a version as GitHub Markdown. Fails when there is no entry or no description.',
|
|
14
|
+
builder: (yargs) => yargs
|
|
15
|
+
.positional('release', {
|
|
16
|
+
description: 'Version or tag (a leading `v` is accepted). Default: the version in package.json.',
|
|
17
|
+
type: 'string',
|
|
18
|
+
})
|
|
19
|
+
.option('compare', {
|
|
20
|
+
description: 'Append a `**Full changelog:**` compare link to the previous entry (needs `vcsBrowserUrl`).',
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
default: false,
|
|
23
|
+
}),
|
|
24
|
+
handler: async (args) => {
|
|
25
|
+
const projectDir = process.cwd();
|
|
26
|
+
try {
|
|
27
|
+
const config = await new Config().forCommand(projectDir);
|
|
28
|
+
const ship = config.ship ?? {};
|
|
29
|
+
const flatpak = config.flatpak ?? {};
|
|
30
|
+
const version = args.release ?? readPackageJson(join(projectDir, 'package.json'))?.version;
|
|
31
|
+
if (!version)
|
|
32
|
+
throw new Error('no version given and package.json has none');
|
|
33
|
+
const releases = ship.releases ?? flatpak.releases;
|
|
34
|
+
const repoUrl = args.compare === true ? (ship.vcsBrowserUrl ?? flatpak.vcsBrowserUrl) : undefined;
|
|
35
|
+
process.stdout.write(renderReleaseNotes({ releases, version: stripVersionPrefix(version), repoUrl }));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
process.stderr.write(`gjsify release-notes: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
39
|
+
return process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppMetadata } from '../types/config-data.js';
|
|
1
|
+
import type { AppMetadata, DescriptionBlock } from '../types/config-data.js';
|
|
2
2
|
/** Everything a renderer needs that is not part of {@link AppMetadata} itself. */
|
|
3
3
|
export interface AppMetadataInputs {
|
|
4
4
|
/** Reverse-DNS application id — the MetaInfo `<id>` and the icon/desktop basename. */
|
|
@@ -50,4 +50,6 @@ export declare function renderMetainfoCli(inputs: AppMetadataInputs): string;
|
|
|
50
50
|
* wins over file types: a URL handler must take `%u`.
|
|
51
51
|
*/
|
|
52
52
|
export declare function renderDesktopEntry(inputs: AppMetadataInputs): string;
|
|
53
|
+
/** Auto-convert blank-line-split string into paragraph blocks. */
|
|
54
|
+
export declare function stringToBlocks(s: string): DescriptionBlock[];
|
|
53
55
|
export declare function escapeXml(value: string): string;
|
|
@@ -332,7 +332,7 @@ function renderDescriptionBlocks(description, indent) {
|
|
|
332
332
|
return out;
|
|
333
333
|
}
|
|
334
334
|
/** Auto-convert blank-line-split string into paragraph blocks. */
|
|
335
|
-
function stringToBlocks(s) {
|
|
335
|
+
export function stringToBlocks(s) {
|
|
336
336
|
return s
|
|
337
337
|
.trim()
|
|
338
338
|
.split(/\n\n+/)
|
|
@@ -281,6 +281,7 @@ export const PACKAGE_DEPS = {
|
|
|
281
281
|
'@gjsify/gamepad': ['manette'],
|
|
282
282
|
'@gjsify/speech-synthesis': ['spiel'],
|
|
283
283
|
'@gjsify/webaudio': ['gstreamer', 'gst-app'],
|
|
284
|
+
'@gjsify/media-capture': ['gstreamer', 'gst-app'],
|
|
284
285
|
'@gjsify/iframe': ['webkitgtk'],
|
|
285
286
|
'@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
|
|
286
287
|
'@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DescriptionBlock } from '../types/config-data.js';
|
|
2
|
+
export interface ReleaseEntry {
|
|
3
|
+
version: string;
|
|
4
|
+
date: string;
|
|
5
|
+
description?: string | DescriptionBlock[];
|
|
6
|
+
}
|
|
7
|
+
/** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
|
|
8
|
+
export declare function descriptionToMarkdown(description: string | DescriptionBlock[]): string;
|
|
9
|
+
/** `v0.1.7` and `0.1.7` name the same release. */
|
|
10
|
+
export declare function stripVersionPrefix(version: string): string;
|
|
11
|
+
export interface ReleaseNotesInput {
|
|
12
|
+
releases: readonly ReleaseEntry[] | undefined;
|
|
13
|
+
/** Requested version, with or without a leading `v`. */
|
|
14
|
+
version: string;
|
|
15
|
+
/** Append the compare link when set and a previous release exists. */
|
|
16
|
+
repoUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
19
|
+
export declare function renderReleaseNotes({ releases, version, repoUrl }: ReleaseNotesInput): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// GitHub-Markdown rendering of one `releases` entry, for `gjsify release-notes`.
|
|
2
|
+
//
|
|
3
|
+
// The AppStream `<release>` blocks and a GitHub release body describe the same
|
|
4
|
+
// release, so both read the one `releases` list in the config. The string →
|
|
5
|
+
// block normalisation is `stringToBlocks` from app-metadata.ts, not a second copy.
|
|
6
|
+
import { stringToBlocks } from './app-metadata.js';
|
|
7
|
+
/** Paragraphs → paragraphs, lists → `- ` items. Translator hints are dropped. */
|
|
8
|
+
export function descriptionToMarkdown(description) {
|
|
9
|
+
const blocks = typeof description === 'string' ? stringToBlocks(description) : description;
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const block of blocks) {
|
|
12
|
+
if ('p' in block) {
|
|
13
|
+
const text = block.p.trim().replace(/\s+/g, ' ');
|
|
14
|
+
if (text)
|
|
15
|
+
out.push(text);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const items = block.ul.map((item) => (typeof item === 'string' ? item : item.item).trim()).filter(Boolean);
|
|
19
|
+
if (items.length)
|
|
20
|
+
out.push(items.map((item) => `- ${item}`).join('\n'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out.join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
/** `v0.1.7` and `0.1.7` name the same release. */
|
|
26
|
+
export function stripVersionPrefix(version) {
|
|
27
|
+
return version.trim().replace(/^v/i, '');
|
|
28
|
+
}
|
|
29
|
+
/** Throws an `Error` whose message is meant to be printed as-is. */
|
|
30
|
+
export function renderReleaseNotes({ releases, version, repoUrl }) {
|
|
31
|
+
const wanted = stripVersionPrefix(version);
|
|
32
|
+
const list = releases ?? [];
|
|
33
|
+
const index = list.findIndex((r) => stripVersionPrefix(r.version) === wanted);
|
|
34
|
+
if (index < 0) {
|
|
35
|
+
throw new Error(`no \`releases\` entry for version ${wanted} in the gjsify config` +
|
|
36
|
+
(list.length ? ` (found: ${list.map((r) => r.version).join(', ')})` : ' (no `releases` configured)'));
|
|
37
|
+
}
|
|
38
|
+
const entry = list[index];
|
|
39
|
+
const body = entry.description === undefined ? '' : descriptionToMarkdown(entry.description);
|
|
40
|
+
if (!body)
|
|
41
|
+
throw new Error(`the \`releases\` entry for version ${wanted} has no description`);
|
|
42
|
+
// Most recent first, so the previous release is the next entry.
|
|
43
|
+
const previous = list[index + 1];
|
|
44
|
+
if (!repoUrl || !previous)
|
|
45
|
+
return `${body}\n`;
|
|
46
|
+
const base = repoUrl.replace(/\/+$/, '');
|
|
47
|
+
const compare = `${base}/compare/v${stripVersionPrefix(previous.version)}...v${wanted}`;
|
|
48
|
+
return `${body}\n\n**Full changelog:** ${compare}\n`;
|
|
49
|
+
}
|
|
@@ -170,6 +170,13 @@ export interface Layout {
|
|
|
170
170
|
* first line is `import Gtk from 'gi://Gtk?version=4.0'`.
|
|
171
171
|
*/
|
|
172
172
|
shippedRuntime: 'gjs' | 'node';
|
|
173
|
+
/**
|
|
174
|
+
* Set when the artifact's container stores names in a DECOMPOSED form and a
|
|
175
|
+
* seal over those names is byte-exact: every path BELOW the layout root is
|
|
176
|
+
* then staged in that form, so the bytes the seal was made over are the
|
|
177
|
+
* bytes the container hands back. See {@link nameFormBelowRoot}.
|
|
178
|
+
*/
|
|
179
|
+
nameForm?: 'NFD';
|
|
173
180
|
/**
|
|
174
181
|
* Why the staged launcher cannot name {@link shippedRuntime} yet, or absent
|
|
175
182
|
* when it already does.
|
|
@@ -402,6 +409,17 @@ export declare function assertLayoutSupportsArch(layout: Layout, arch: string):
|
|
|
402
409
|
* `lib/<name>/`, because on macOS those two go to different directories.
|
|
403
410
|
*/
|
|
404
411
|
export declare function place(layout: Layout, identity: LayoutIdentity, rel: string): string;
|
|
412
|
+
/**
|
|
413
|
+
* A staged path with every segment BELOW the layout root put in the layout's
|
|
414
|
+
* {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
|
|
415
|
+
*
|
|
416
|
+
* The root is exempt for two reasons: no seal covers its own name, and the
|
|
417
|
+
* packers look the directory up by `${name}.app` on a Linux host whose
|
|
418
|
+
* filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
|
|
419
|
+
* path is returned unchanged, so every bundle that already verified keeps
|
|
420
|
+
* its exact bytes.
|
|
421
|
+
*/
|
|
422
|
+
export declare function nameFormBelowRoot(layout: Layout, path: string): string;
|
|
405
423
|
/**
|
|
406
424
|
* Apply a layout to a whole planned payload, and add what the layout itself owns.
|
|
407
425
|
*
|
package/lib/utils/ship/layout.js
CHANGED
|
@@ -268,6 +268,14 @@ export const LAYOUTS = {
|
|
|
268
268
|
'with `PangoCairo.FontMap.get_default().list_families()`, and `PANGOCAIRO_BACKEND=bogus` to ' +
|
|
269
269
|
'print which backends it was built with.',
|
|
270
270
|
launcherExt: '',
|
|
271
|
+
// `codesign` seals the bytes of every name inside the bundle, and the `.dmg` is an
|
|
272
|
+
// HFS+ volume (`dmg.ts`), which stores names decomposed. Measured on macOS 27
|
|
273
|
+
// with an app whose display name holds "ä": the bundle sealed with the precomposed
|
|
274
|
+
// `Contents/Helpers/<name>` verifies out of the zip and fails inside the mounted
|
|
275
|
+
// image ("file added / file missing" on that one path), because HFS+ rewrote the
|
|
276
|
+
// name to a + U+0308. APFS keeps the bytes it is given, so staging NFD makes
|
|
277
|
+
// zip and image agree.
|
|
278
|
+
nameForm: 'NFD',
|
|
271
279
|
root: appBundleDir,
|
|
272
280
|
// Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
|
|
273
281
|
// everything that is not, and `Contents/Frameworks` the dylibs — which is
|
|
@@ -680,6 +688,26 @@ export function place(layout, identity, rel) {
|
|
|
680
688
|
return posix.join(dirs.data, rel.slice('share/'.length));
|
|
681
689
|
return posix.join(dirs.other, rel);
|
|
682
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* A staged path with every segment BELOW the layout root put in the layout's
|
|
693
|
+
* {@link Layout.nameForm}; the first segment (`<App>.app`) is left as given.
|
|
694
|
+
*
|
|
695
|
+
* The root is exempt for two reasons: no seal covers its own name, and the
|
|
696
|
+
* packers look the directory up by `${name}.app` on a Linux host whose
|
|
697
|
+
* filesystem compares bytes. Only ever changes non-ASCII names — an ASCII
|
|
698
|
+
* path is returned unchanged, so every bundle that already verified keeps
|
|
699
|
+
* its exact bytes.
|
|
700
|
+
*/
|
|
701
|
+
export function nameFormBelowRoot(layout, path) {
|
|
702
|
+
if (layout.nameForm === undefined)
|
|
703
|
+
return path;
|
|
704
|
+
const form = layout.nameForm;
|
|
705
|
+
// Staged paths are stage-relative and built with `posix.join` on every host.
|
|
706
|
+
const [root, ...below] = path.split(posix.sep);
|
|
707
|
+
if (below.length === 0)
|
|
708
|
+
return path;
|
|
709
|
+
return [root, ...below.map((segment) => segment.normalize(form))].join(posix.sep);
|
|
710
|
+
}
|
|
683
711
|
/**
|
|
684
712
|
* Apply a layout to a whole planned payload, and add what the layout itself owns.
|
|
685
713
|
*
|
|
@@ -712,7 +740,8 @@ export function place(layout, identity, rel) {
|
|
|
712
740
|
export function placeStage(layout, identity, files, carried = []) {
|
|
713
741
|
const byPlaced = new Map();
|
|
714
742
|
const out = [];
|
|
715
|
-
const claim = (
|
|
743
|
+
const claim = (rawPath, from, file) => {
|
|
744
|
+
const path = nameFormBelowRoot(layout, rawPath);
|
|
716
745
|
const previous = byPlaced.get(path);
|
|
717
746
|
if (previous !== undefined) {
|
|
718
747
|
throw new Error(`gjsify ship: in the ${layout.name} layout both ${previous} and ${from} install as ` +
|
|
@@ -66,6 +66,31 @@ export interface SignerDescriptor {
|
|
|
66
66
|
/** What a signature from this signer does NOT yet cover — printed once per run. */
|
|
67
67
|
gap: string;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* An explicit `--identifier` for an image whose FILE NAME is not ASCII, or
|
|
71
|
+
* `undefined` when codesign's own default is already right.
|
|
72
|
+
*
|
|
73
|
+
* WHY THIS EXISTS, measured on macOS 2026-10-30 (steuererklaerung CI run
|
|
74
|
+
* 41050980433, plus mini bundles beside it): the darwin layout puts the
|
|
75
|
+
* interpreter at `Contents/Helpers/<display name>`, so an app called
|
|
76
|
+
* "Steuererklärung" carries a non-ASCII leaf. codesign derives an image's code
|
|
77
|
+
* identifier from that file name; with a NAMED identity the designated
|
|
78
|
+
* requirement becomes `identifier "<derived>" and certificate leaf …`, and
|
|
79
|
+
* `codesign --verify --strict <App>.app` then fails with *"the sealed resource
|
|
80
|
+
* directory is invalid"* for the NFD-derived identifier (`…61cc88…`); why
|
|
81
|
+
* codesign rejects it is not established, only that it does. Ad-hoc (a cdhash
|
|
82
|
+
* requirement, no identifier at all) and ASCII names are unaffected; the same
|
|
83
|
+
* bundle with `--identifier eu.test.x.helper` verifies, and an NFC identifier
|
|
84
|
+
* still does not, so the value has to be plain ASCII rather than merely
|
|
85
|
+
* normalised.
|
|
86
|
+
*
|
|
87
|
+
* Hence: an ASCII leaf keeps codesign's default (this returns `undefined` and
|
|
88
|
+
* the flag is not sent, so every name that already worked is unchanged), and
|
|
89
|
+
* only a non-ASCII leaf gets a stable ASCII identifier. NFD first, so a
|
|
90
|
+
* precomposed "ä" and a decomposed "a"+U+0308 fold to the same value instead of
|
|
91
|
+
* to two identifiers that differ by bytes nobody can see.
|
|
92
|
+
*/
|
|
93
|
+
export declare function asciiCodesignIdentifier(fileName: string): string | undefined;
|
|
69
94
|
/**
|
|
70
95
|
* The signers, keyed by the LAYOUT they sign — never by the host they run on.
|
|
71
96
|
*
|
|
@@ -156,6 +181,17 @@ export interface SignArgs {
|
|
|
156
181
|
entitlements?: string;
|
|
157
182
|
/** Is {@link identity} the reserved ad-hoc `-`? */
|
|
158
183
|
adhoc: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Is {@link file} the bundle DIRECTORY being sealed, rather than one image
|
|
186
|
+
* inside it?
|
|
187
|
+
*
|
|
188
|
+
* A signer needs the distinction because the two invocations are not the
|
|
189
|
+
* same operation on the same kind of object: a bundle takes its identifier
|
|
190
|
+
* from `Info.plist` and must never be given one, while an image's identifier
|
|
191
|
+
* is derived from its own file name — which is where a non-ASCII helper name
|
|
192
|
+
* becomes a problem ({@link asciiCodesignIdentifier}).
|
|
193
|
+
*/
|
|
194
|
+
seal: boolean;
|
|
159
195
|
}
|
|
160
196
|
/** `codesign --sign -`: the one identity value that names no identity at all. */
|
|
161
197
|
export declare const ADHOC_IDENTITY = "-";
|
|
@@ -74,13 +74,57 @@
|
|
|
74
74
|
// ad-hoc answer, and § A4's re-sign of every image stays the design of record for
|
|
75
75
|
// the Developer ID that notarisation will actually require.
|
|
76
76
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
77
|
-
import { dirname, join, sep } from 'node:path';
|
|
77
|
+
import { basename, dirname, join, sep } from 'node:path';
|
|
78
78
|
import { isOnPath } from '../check-system-deps.js';
|
|
79
79
|
import { describeExit, spawnToCompletion } from '../spawn.js';
|
|
80
80
|
import { listFilesRecursive } from './discover.js';
|
|
81
81
|
import { classifyBinary } from './payload.js';
|
|
82
82
|
import { renderEntitlements } from './plist.js';
|
|
83
83
|
import { writePayload } from './stage-writer.js';
|
|
84
|
+
/**
|
|
85
|
+
* An explicit `--identifier` for an image whose FILE NAME is not ASCII, or
|
|
86
|
+
* `undefined` when codesign's own default is already right.
|
|
87
|
+
*
|
|
88
|
+
* WHY THIS EXISTS, measured on macOS 2026-10-30 (steuererklaerung CI run
|
|
89
|
+
* 41050980433, plus mini bundles beside it): the darwin layout puts the
|
|
90
|
+
* interpreter at `Contents/Helpers/<display name>`, so an app called
|
|
91
|
+
* "Steuererklärung" carries a non-ASCII leaf. codesign derives an image's code
|
|
92
|
+
* identifier from that file name; with a NAMED identity the designated
|
|
93
|
+
* requirement becomes `identifier "<derived>" and certificate leaf …`, and
|
|
94
|
+
* `codesign --verify --strict <App>.app` then fails with *"the sealed resource
|
|
95
|
+
* directory is invalid"* for the NFD-derived identifier (`…61cc88…`); why
|
|
96
|
+
* codesign rejects it is not established, only that it does. Ad-hoc (a cdhash
|
|
97
|
+
* requirement, no identifier at all) and ASCII names are unaffected; the same
|
|
98
|
+
* bundle with `--identifier eu.test.x.helper` verifies, and an NFC identifier
|
|
99
|
+
* still does not, so the value has to be plain ASCII rather than merely
|
|
100
|
+
* normalised.
|
|
101
|
+
*
|
|
102
|
+
* Hence: an ASCII leaf keeps codesign's default (this returns `undefined` and
|
|
103
|
+
* the flag is not sent, so every name that already worked is unchanged), and
|
|
104
|
+
* only a non-ASCII leaf gets a stable ASCII identifier. NFD first, so a
|
|
105
|
+
* precomposed "ä" and a decomposed "a"+U+0308 fold to the same value instead of
|
|
106
|
+
* to two identifiers that differ by bytes nobody can see.
|
|
107
|
+
*/
|
|
108
|
+
export function asciiCodesignIdentifier(fileName) {
|
|
109
|
+
// `fileName` may be a path, and codesign derives the identifier from the
|
|
110
|
+
// LEAF — so both the ASCII test and the fallback bytes are about the leaf.
|
|
111
|
+
const leaf = basename(fileName);
|
|
112
|
+
if ([...leaf].every((char) => (char.codePointAt(0) ?? 0) < 0x80))
|
|
113
|
+
return undefined;
|
|
114
|
+
const slug = leaf
|
|
115
|
+
.normalize('NFD')
|
|
116
|
+
.replace(/\p{M}+/gu, '')
|
|
117
|
+
.replace(/[^A-Za-z0-9.-]+/g, '-')
|
|
118
|
+
.replace(/^-+|-+$/g, '');
|
|
119
|
+
if (/[A-Za-z0-9]/.test(slug))
|
|
120
|
+
return slug;
|
|
121
|
+
// Nothing Latin survives (a name that is entirely CJK or emoji): a readable
|
|
122
|
+
// slug would be the empty string, which is not an identifier, so the UTF-8
|
|
123
|
+
// bytes are spelled out — ASCII, unique, and reproducible from the name it
|
|
124
|
+
// came from.
|
|
125
|
+
const bytes = new TextEncoder().encode(leaf);
|
|
126
|
+
return `x-${[...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
127
|
+
}
|
|
84
128
|
/**
|
|
85
129
|
* The signers, keyed by the LAYOUT they sign — never by the host they run on.
|
|
86
130
|
*
|
|
@@ -116,16 +160,25 @@ export const SIGNERS = {
|
|
|
116
160
|
// the timestamp authority service cannot be contacted … the signing
|
|
117
161
|
// operation will fail", which is not a failure worth inventing for a
|
|
118
162
|
// signature that cannot carry the result.
|
|
119
|
-
args: ({ identity, file, entitlements, adhoc }) =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
163
|
+
args: ({ identity, file, entitlements, adhoc, seal }) => {
|
|
164
|
+
// A non-ASCII leaf gets an explicit ASCII identifier — see
|
|
165
|
+
// {@link asciiCodesignIdentifier}. NEVER for the seal: the bundle
|
|
166
|
+
// takes its identifier from `CFBundleIdentifier` in `Info.plist`,
|
|
167
|
+
// and overriding that would make the seal disagree with the bundle
|
|
168
|
+
// the system reads.
|
|
169
|
+
const derived = seal ? undefined : asciiCodesignIdentifier(file);
|
|
170
|
+
return [
|
|
171
|
+
'--force',
|
|
172
|
+
'--sign',
|
|
173
|
+
identity,
|
|
174
|
+
'--options',
|
|
175
|
+
'runtime',
|
|
176
|
+
...(entitlements === undefined ? [] : ['--entitlements', entitlements]),
|
|
177
|
+
...(adhoc ? [] : ['--timestamp']),
|
|
178
|
+
...(derived === undefined ? [] : ['--identifier', derived]),
|
|
179
|
+
file,
|
|
180
|
+
];
|
|
181
|
+
},
|
|
129
182
|
// FIVE OF THE REFERENCE'S SIX for an ad-hoc identity, FOUR for a real one,
|
|
130
183
|
// and the difference is the one § A16 left open — now MEASURED, on
|
|
131
184
|
// darwin-arm64 with the published `@gjsify/gtk-runtime-darwin-arm64` staged
|
|
@@ -182,7 +235,9 @@ export const SIGNERS = {
|
|
|
182
235
|
gap: 'the images are signed and the bundle is sealed, and the seal survives both the `.zip` and the ' +
|
|
183
236
|
'`.dmg` round trip; § A16 stands measured in ONE direction only — library validation refuses an ' +
|
|
184
237
|
'ad-hoc-signed dylib, so the ad-hoc identity grants `disable-library-validation` while a named ' +
|
|
185
|
-
'one does not
|
|
238
|
+
'one does not; a named Developer ID was signed and notarised on 2026-10-30 for an ASCII-named ' +
|
|
239
|
+
'app, and a non-ASCII image name verifies only with the explicit ASCII `--identifier` this ' +
|
|
240
|
+
'signer now passes',
|
|
186
241
|
},
|
|
187
242
|
win32: {
|
|
188
243
|
layoutOs: 'win32',
|
|
@@ -335,8 +390,14 @@ export async function signPayload(input) {
|
|
|
335
390
|
writeFileSync(input.entitlementsPath, renderEntitlements(granted));
|
|
336
391
|
entitlements = input.entitlementsPath;
|
|
337
392
|
}
|
|
338
|
-
const run = async (file, what) => {
|
|
339
|
-
const args = signer.args({
|
|
393
|
+
const run = async (file, what, seal) => {
|
|
394
|
+
const args = signer.args({
|
|
395
|
+
identity: input.identity,
|
|
396
|
+
file,
|
|
397
|
+
adhoc,
|
|
398
|
+
seal,
|
|
399
|
+
...(entitlements ? { entitlements } : {}),
|
|
400
|
+
});
|
|
340
401
|
if (input.verbose)
|
|
341
402
|
input.log(`${signer.tool} ${args.join(' ')}`);
|
|
342
403
|
const result = await spawnToCompletion(signer.tool, args, {
|
|
@@ -367,7 +428,7 @@ export async function signPayload(input) {
|
|
|
367
428
|
return [...payload];
|
|
368
429
|
}
|
|
369
430
|
for (const entry of signable) {
|
|
370
|
-
await run(join(workDir, entry.path.split('/').join(sep)), entry.path);
|
|
431
|
+
await run(join(workDir, entry.path.split('/').join(sep)), entry.path, false);
|
|
371
432
|
}
|
|
372
433
|
// THE IMAGES FIRST, THE SEAL LAST, and the order is not stylistic: a bundle
|
|
373
434
|
// seal hashes the files it seals, so sealing before the closure is signed
|
|
@@ -381,7 +442,7 @@ export async function signPayload(input) {
|
|
|
381
442
|
'stage would sign the payload of every other format beside it.');
|
|
382
443
|
}
|
|
383
444
|
sealPrefix = signer.sealAddPrefix(input.bundleRoot);
|
|
384
|
-
await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)
|
|
445
|
+
await run(join(workDir, input.bundleRoot.split('/').join(sep)), `${input.bundleRoot} (bundle seal)`, true);
|
|
385
446
|
}
|
|
386
447
|
// THE FILE SET MAY ONLY GROW WHERE THE SEAL WRITES. Both tools rewrite an
|
|
387
448
|
// image in place, so any other new path means something else ran — and a file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/cli",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.14",
|
|
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.14",
|
|
146
|
+
"@gjsify/buffer": "^0.64.14",
|
|
147
|
+
"@gjsify/create-app": "^0.64.14",
|
|
148
|
+
"@gjsify/node-globals": "^0.64.14",
|
|
149
|
+
"@gjsify/node-polyfills": "^0.64.14",
|
|
150
|
+
"@gjsify/npm-registry": "^0.64.14",
|
|
151
|
+
"@gjsify/resolve-npm": "^0.64.14",
|
|
152
|
+
"@gjsify/rolldown-plugin-gjsify": "^0.64.14",
|
|
153
|
+
"@gjsify/vite-plugin-blueprint": "^0.64.14",
|
|
154
|
+
"@gjsify/rolldown-plugin-pnp": "^0.64.14",
|
|
155
|
+
"@gjsify/semver": "^0.64.14",
|
|
156
|
+
"@gjsify/tar": "^0.64.14",
|
|
157
|
+
"@gjsify/tsc": "^0.64.14",
|
|
158
|
+
"@gjsify/utils": "^0.64.14",
|
|
159
|
+
"@gjsify/web-polyfills": "^0.64.14",
|
|
160
|
+
"@gjsify/workspace": "^0.64.14",
|
|
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.14",
|
|
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.14",
|
|
175
|
+
"@gjsify/rolldown-native": "^0.64.14"
|
|
176
176
|
},
|
|
177
177
|
"peerDependenciesMeta": {
|
|
178
178
|
"@gjsify/oxfmt-native": {
|
|
@@ -191,6 +191,5 @@
|
|
|
191
191
|
"bugs": {
|
|
192
192
|
"url": "https://github.com/gjsify/gjsify/issues"
|
|
193
193
|
},
|
|
194
|
-
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
|
|
195
|
-
"gitHead": "05fae97125a374e84bf57c2d2d086fb53523f205"
|
|
194
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/infra/cli#readme"
|
|
196
195
|
}
|