@nativedesktop/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/config.test.ts +54 -0
- package/src/config.ts +87 -0
- package/src/index.ts +25 -4
- package/src/package/cef.test.ts +332 -0
- package/src/package/cef.ts +454 -0
- package/src/package/doctor.test.ts +84 -0
- package/src/package/doctor.ts +91 -3
- package/src/package/identity.test.ts +14 -0
- package/src/package/linux.ts +17 -1
- package/src/package/mac.ts +61 -3
- package/src/package/payload.test.ts +32 -1
- package/src/package/payload.ts +19 -3
- package/src/package/templates.ts +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nativedesktop/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The nd CLI: dev-runs, builds, and packages NativeDesktop apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,6 @@
|
|
|
25
25
|
"./config": "./src/config.ts"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@nativedesktop/host": "0.
|
|
28
|
+
"@nativedesktop/host": "0.3.0"
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Webview engine and scheme resolution: the decisions `nd dev`, `nd package`
|
|
2
|
+
// and `nd doctor` all make from the same config, and the values `nd dev` hands
|
|
3
|
+
// the host as ND_WEBVIEW_ENGINE and ND_CEF_SCHEMES.
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
|
+
import { engineTargetFor, type NativeDesktopConfig, resolveCefSchemes, resolveWebViewEngine } from "./config.ts";
|
|
6
|
+
|
|
7
|
+
const chromiumOnMac: NativeDesktopConfig = { webview: { engine: { mac: "chromium" } } };
|
|
8
|
+
|
|
9
|
+
describe("engineTargetFor", () => {
|
|
10
|
+
test("maps the packaging platforms, and nothing else yet", () => {
|
|
11
|
+
expect(engineTargetFor("darwin")).toBe("mac");
|
|
12
|
+
expect(engineTargetFor("linux")).toBe("linux");
|
|
13
|
+
expect(engineTargetFor("win32")).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("resolveWebViewEngine", () => {
|
|
18
|
+
test("defaults to system, per platform", () => {
|
|
19
|
+
expect(resolveWebViewEngine({}, "mac", {})).toBe("system");
|
|
20
|
+
expect(resolveWebViewEngine(chromiumOnMac, "mac", {})).toBe("chromium");
|
|
21
|
+
expect(resolveWebViewEngine(chromiumOnMac, "linux", {})).toBe("system");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("ND_WEBVIEW_ENGINE overrides the config in both directions", () => {
|
|
25
|
+
expect(resolveWebViewEngine({}, "linux", { ND_WEBVIEW_ENGINE: "chromium" })).toBe("chromium");
|
|
26
|
+
expect(resolveWebViewEngine(chromiumOnMac, "mac", { ND_WEBVIEW_ENGINE: "system" })).toBe("system");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("an unknown engine names where it came from", () => {
|
|
30
|
+
expect(() => resolveWebViewEngine({}, "mac", { ND_WEBVIEW_ENGINE: "webkit2" })).toThrow("ND_WEBVIEW_ENGINE");
|
|
31
|
+
const bad = { webview: { engine: { linux: "blink" } } } as unknown as NativeDesktopConfig;
|
|
32
|
+
expect(() => resolveWebViewEngine(bad, "linux", {})).toThrow("webview.engine.linux");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("resolveCefSchemes", () => {
|
|
37
|
+
const declared: NativeDesktopConfig = { webview: { cef: { schemes: ["nbext", "nbext"] } } };
|
|
38
|
+
|
|
39
|
+
test("defaults to none, and deduplicates what the config declares", () => {
|
|
40
|
+
expect(resolveCefSchemes({}, {})).toEqual([]);
|
|
41
|
+
expect(resolveCefSchemes(declared, {})).toEqual(["nbext"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("ND_CEF_SCHEMES overrides the config, comma separated", () => {
|
|
45
|
+
expect(resolveCefSchemes(declared, { ND_CEF_SCHEMES: "one, two" })).toEqual(["one", "two"]);
|
|
46
|
+
expect(resolveCefSchemes(declared, { ND_CEF_SCHEMES: "" })).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a name that is not a scheme names where it came from", () => {
|
|
50
|
+
expect(() => resolveCefSchemes({}, { ND_CEF_SCHEMES: "NBExt" })).toThrow("ND_CEF_SCHEMES");
|
|
51
|
+
const bad = { webview: { cef: { schemes: ["nb ext"] } } };
|
|
52
|
+
expect(() => resolveCefSchemes(bad, {})).toThrow("webview.cef.schemes");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/config.ts
CHANGED
|
@@ -142,10 +142,97 @@ export interface PackageConfig {
|
|
|
142
142
|
updates?: UpdatesConfig;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/** `<webview>` engine: the platform's own WebKit, or Chromium via CEF. */
|
|
146
|
+
export type WebViewEngine = "system" | "chromium";
|
|
147
|
+
|
|
148
|
+
export interface CefConfig {
|
|
149
|
+
/** Pinned CEF release, matched against cef-builds.spotifycdn.com/index.json.
|
|
150
|
+
* Default: DEFAULT_CEF_VERSION from @nativedesktop/host/cef. */
|
|
151
|
+
version?: string;
|
|
152
|
+
/** Locale .pak files staged on Linux. Default ["en-US"]. */
|
|
153
|
+
locales?: string[];
|
|
154
|
+
/** Custom schemes declared at launch, e.g. ["nbext"]. A scheme is only
|
|
155
|
+
* standard, secure and CORS-enabled if every process was told about it before
|
|
156
|
+
* cef_initialize, which is long before app code runs, so the list has to come
|
|
157
|
+
* from config rather than a runtime call. */
|
|
158
|
+
schemes?: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface WebViewConfig {
|
|
162
|
+
/** Per-platform engine. Absent means "system", which ships zero Chromium bytes. */
|
|
163
|
+
engine?: { mac?: WebViewEngine; linux?: WebViewEngine };
|
|
164
|
+
cef?: CefConfig;
|
|
165
|
+
}
|
|
166
|
+
|
|
145
167
|
export interface NativeDesktopConfig {
|
|
146
168
|
native?: { plugins?: NativePluginConfig[] };
|
|
147
169
|
app?: AppIdentity;
|
|
148
170
|
package?: PackageConfig;
|
|
171
|
+
webview?: WebViewConfig;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Packaging targets, which is also how `webview.engine` is keyed. */
|
|
175
|
+
export type EngineTarget = "mac" | "linux";
|
|
176
|
+
|
|
177
|
+
const ENGINES: readonly WebViewEngine[] = ["system", "chromium"];
|
|
178
|
+
|
|
179
|
+
/** The target a platform packages for, or undefined on an unsupported one. */
|
|
180
|
+
export function engineTargetFor(platform: string = process.platform): EngineTarget | undefined {
|
|
181
|
+
if (platform === "darwin") return "mac";
|
|
182
|
+
if (platform === "linux") return "linux";
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function expectEngine(value: string, source: string): WebViewEngine {
|
|
187
|
+
if (!ENGINES.includes(value as WebViewEngine)) {
|
|
188
|
+
throw new Error(`nd: ${source} must be "system" or "chromium" (got "${value}")`);
|
|
189
|
+
}
|
|
190
|
+
return value as WebViewEngine;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The engine a target actually gets. ND_WEBVIEW_ENGINE wins outright (the dev
|
|
195
|
+
* override the spec documents, and the handshake `nd dev` re-exports to the
|
|
196
|
+
* host), then the per-platform config, then "system".
|
|
197
|
+
*/
|
|
198
|
+
export function resolveWebViewEngine(
|
|
199
|
+
config: NativeDesktopConfig,
|
|
200
|
+
target: EngineTarget,
|
|
201
|
+
env: Record<string, string | undefined> = process.env,
|
|
202
|
+
): WebViewEngine {
|
|
203
|
+
const override = env.ND_WEBVIEW_ENGINE;
|
|
204
|
+
if (override) return expectEngine(override, "ND_WEBVIEW_ENGINE");
|
|
205
|
+
const declared = config.webview?.engine?.[target];
|
|
206
|
+
return declared ? expectEngine(declared, `webview.engine.${target}`) : "system";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** URL scheme grammar, minus the uppercase half: CEF lowercases what it
|
|
210
|
+
* registers, so a declared "NBExt" would never match the URLs it serves. 63 is
|
|
211
|
+
* the longest name the engine's fixed scheme buffer takes. */
|
|
212
|
+
const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*$/;
|
|
213
|
+
|
|
214
|
+
function expectScheme(value: string, source: string): string {
|
|
215
|
+
if (!SCHEME_PATTERN.test(value) || value.length > 63) {
|
|
216
|
+
throw new Error(`nd: ${source} must be a lowercase scheme name of at most 63 characters (got "${value}")`);
|
|
217
|
+
}
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The schemes a launch declares, in config order and deduplicated.
|
|
223
|
+
* ND_CEF_SCHEMES wins outright, the same way ND_WEBVIEW_ENGINE does, and takes
|
|
224
|
+
* the comma-separated form the host reads.
|
|
225
|
+
*/
|
|
226
|
+
export function resolveCefSchemes(
|
|
227
|
+
config: NativeDesktopConfig,
|
|
228
|
+
env: Record<string, string | undefined> = process.env,
|
|
229
|
+
): string[] {
|
|
230
|
+
const override = env.ND_CEF_SCHEMES;
|
|
231
|
+
const declared = override !== undefined
|
|
232
|
+
? override.split(",").map((name) => name.trim()).filter((name) => name.length)
|
|
233
|
+
.map((name) => expectScheme(name, "ND_CEF_SCHEMES"))
|
|
234
|
+
: (config.webview?.cef?.schemes ?? []).map((name) => expectScheme(name, "webview.cef.schemes"));
|
|
235
|
+
return [...new Set(declared)];
|
|
149
236
|
}
|
|
150
237
|
|
|
151
238
|
export function defineConfig(config: NativeDesktopConfig): NativeDesktopConfig { return config; }
|
package/src/index.ts
CHANGED
|
@@ -7,21 +7,42 @@
|
|
|
7
7
|
// `nd package` == assemble + sign the platform bundle (packages/nd/src/package/)
|
|
8
8
|
// `nd doctor` == packaging/toolchain readiness checks
|
|
9
9
|
import { type Backend, resolveHostBinary } from "@nativedesktop/host";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildNativePlugins,
|
|
12
|
+
engineTargetFor,
|
|
13
|
+
loadConfig,
|
|
14
|
+
type NativeDesktopConfig,
|
|
15
|
+
resolveCefSchemes,
|
|
16
|
+
resolveWebViewEngine,
|
|
17
|
+
} from "./config.ts";
|
|
11
18
|
import { packageApp, type PackageOptions } from "./package/index.ts";
|
|
12
19
|
|
|
13
20
|
const DEFAULT_ENTRY = "src/main.tsx";
|
|
14
21
|
|
|
15
|
-
async function nativeEnv(): Promise<Record<string, string>> {
|
|
16
|
-
const paths = await buildNativePlugins(
|
|
22
|
+
async function nativeEnv(config: NativeDesktopConfig): Promise<Record<string, string>> {
|
|
23
|
+
const paths = await buildNativePlugins(config);
|
|
17
24
|
return paths.length ? { ND_PLUGINS: "1", ND_PLUGIN_PATHS: paths.join(":") } : {};
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
/** The host reads the engine and its launch-declared schemes off its
|
|
28
|
+
* environment, so both config decisions have to be made here and handed down.
|
|
29
|
+
* Windows has no engine target yet. */
|
|
30
|
+
function engineEnv(config: NativeDesktopConfig): Record<string, string> {
|
|
31
|
+
const target = engineTargetFor();
|
|
32
|
+
if (!target) return {};
|
|
33
|
+
const schemes = resolveCefSchemes(config);
|
|
34
|
+
return {
|
|
35
|
+
ND_WEBVIEW_ENGINE: resolveWebViewEngine(config, target),
|
|
36
|
+
...(schemes.length ? { ND_CEF_SCHEMES: schemes.join(",") } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
async function runDev(entry: string, backend?: Backend): Promise<number> {
|
|
21
41
|
const hostBinary = await resolveHostBinary({ backend });
|
|
42
|
+
const config = await loadConfig();
|
|
22
43
|
const proc = Bun.spawn([hostBinary], {
|
|
23
44
|
cwd: process.cwd(),
|
|
24
|
-
env: { ...process.env, ...(await nativeEnv()), ND_DEV: "1", ND_SCRIPT: entry },
|
|
45
|
+
env: { ...process.env, ...(await nativeEnv(config)), ...engineEnv(config), ND_DEV: "1", ND_SCRIPT: entry },
|
|
25
46
|
stdin: "inherit",
|
|
26
47
|
stdout: "inherit",
|
|
27
48
|
stderr: "inherit",
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// CEF acquisition and staging. The mac leg runs against the real cached dist
|
|
2
|
+
// when the machine has one (no download from a test) and against a synthetic
|
|
3
|
+
// framework otherwise; the linux dist is never fetched here, so its leg builds
|
|
4
|
+
// the dist tree it expects and asserts the plan over that.
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { cefDistDir, isCefRoot } from "@nativedesktop/host/cef";
|
|
10
|
+
import {
|
|
11
|
+
applyCefMacPlan,
|
|
12
|
+
cefBundleAudit,
|
|
13
|
+
type CefIndex,
|
|
14
|
+
cefMacSignTargets,
|
|
15
|
+
cefVersionFor,
|
|
16
|
+
CEF_HELPER_BINARY_NAME,
|
|
17
|
+
CEF_HELPER_SUFFIXES,
|
|
18
|
+
ensureCefDist,
|
|
19
|
+
fetchCefIndex,
|
|
20
|
+
planCefLinux,
|
|
21
|
+
planCefMac,
|
|
22
|
+
resolveCefHelperBinary,
|
|
23
|
+
selectCefBuild,
|
|
24
|
+
} from "./cef.ts";
|
|
25
|
+
|
|
26
|
+
const FRAMEWORK = "Chromium Embedded Framework.framework";
|
|
27
|
+
const PINNED = cefVersionFor(undefined);
|
|
28
|
+
|
|
29
|
+
function tempDir(): string {
|
|
30
|
+
return mkdtempSync(join(tmpdir(), "nd-cef-pkg-"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function macPlanInput(distRoot: string, contents: string, helperBinary: string) {
|
|
34
|
+
return {
|
|
35
|
+
distRoot,
|
|
36
|
+
contents,
|
|
37
|
+
appName: "Demo",
|
|
38
|
+
appId: "com.example.demo",
|
|
39
|
+
version: "1.2.3",
|
|
40
|
+
minimumSystemVersion: "26.0",
|
|
41
|
+
helperBinary,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A CEF mac dist in the versioned framework layout, symlinks and all. The
|
|
46
|
+
* shipped 151.3.23 dist is flat, but codesign only accepts a versioned bundle
|
|
47
|
+
* when the links survive the copy, so the fixture keeps them. */
|
|
48
|
+
function synthesizeMacDist(root: string): string {
|
|
49
|
+
const framework = join(root, "Release", FRAMEWORK);
|
|
50
|
+
const versionA = join(framework, "Versions", "A");
|
|
51
|
+
mkdirSync(join(versionA, "Libraries"), { recursive: true });
|
|
52
|
+
mkdirSync(join(versionA, "Resources"), { recursive: true });
|
|
53
|
+
writeFileSync(join(versionA, "Chromium Embedded Framework"), "mach-o");
|
|
54
|
+
writeFileSync(join(versionA, "Libraries", "libEGL.dylib"), "mach-o");
|
|
55
|
+
writeFileSync(join(versionA, "Libraries", "libGLESv2.dylib"), "mach-o");
|
|
56
|
+
writeFileSync(join(versionA, "Resources", "Info.plist"), "<plist/>");
|
|
57
|
+
symlinkSync("A", join(framework, "Versions", "Current"));
|
|
58
|
+
symlinkSync("Versions/Current/Chromium Embedded Framework", join(framework, "Chromium Embedded Framework"));
|
|
59
|
+
symlinkSync("Versions/Current/Libraries", join(framework, "Libraries"));
|
|
60
|
+
symlinkSync("Versions/Current/Resources", join(framework, "Resources"));
|
|
61
|
+
return root;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The linux dist's own file set, from cmake/cef_variables.cmake. Empty files:
|
|
65
|
+
* the plan is about layout, and downloading 269 MB in a unit test is not. */
|
|
66
|
+
function synthesizeLinuxDist(root: string, locales: string[]): string {
|
|
67
|
+
const release = join(root, "Release");
|
|
68
|
+
const resources = join(root, "Resources");
|
|
69
|
+
mkdirSync(release, { recursive: true });
|
|
70
|
+
mkdirSync(join(resources, "locales"), { recursive: true });
|
|
71
|
+
for (const name of [
|
|
72
|
+
"libcef.so",
|
|
73
|
+
"libEGL.so",
|
|
74
|
+
"libGLESv2.so",
|
|
75
|
+
"libvk_swiftshader.so",
|
|
76
|
+
"libvulkan.so.1",
|
|
77
|
+
"vk_swiftshader_icd.json",
|
|
78
|
+
"v8_context_snapshot.bin",
|
|
79
|
+
"chrome-sandbox",
|
|
80
|
+
]) writeFileSync(join(release, name), "");
|
|
81
|
+
for (const name of ["icudtl.dat", "resources.pak", "chrome_100_percent.pak", "chrome_200_percent.pak"]) {
|
|
82
|
+
writeFileSync(join(resources, name), "");
|
|
83
|
+
}
|
|
84
|
+
for (const locale of locales) writeFileSync(join(resources, "locales", `${locale}.pak`), "");
|
|
85
|
+
return root;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const INDEX: CefIndex = {
|
|
89
|
+
macosarm64: {
|
|
90
|
+
versions: [
|
|
91
|
+
{
|
|
92
|
+
cef_version: "152.0.1+gaaaaaaa+chromium-152.0.0.1",
|
|
93
|
+
channel: "stable",
|
|
94
|
+
files: [{ type: "minimal", name: "cef_binary_152.0.1_macosarm64_minimal.tar.bz2", sha1: "newer", size: 1 }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
cef_version: "151.3.23+gd211df0+chromium-151.0.7922.170",
|
|
98
|
+
channel: "stable",
|
|
99
|
+
files: [
|
|
100
|
+
{ type: "standard", name: "cef_binary_151.3.23+gd211df0+chromium-151.0.7922.170_macosarm64.tar.bz2", sha1: "std", size: 297930532 },
|
|
101
|
+
{ type: "minimal", name: "cef_binary_151.3.23+gd211df0+chromium-151.0.7922.170_macosarm64_minimal.tar.bz2", sha1: "94b409659401b34bf8616f9592ba5daeb9ee82d4", size: 130993494 },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
cef_version: "151.3.2+gc0ffee0+chromium-151.0.7922.100",
|
|
106
|
+
channel: "stable",
|
|
107
|
+
files: [{ type: "minimal", name: "cef_binary_151.3.2_macosarm64_minimal.tar.bz2", sha1: "older", size: 1 }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
describe("selectCefBuild", () => {
|
|
114
|
+
test("picks the minimal dist for the pinned release and percent-encodes the URL", () => {
|
|
115
|
+
const build = selectCefBuild(INDEX, "macosarm64", "151.3.23");
|
|
116
|
+
expect(build.cefVersion).toBe("151.3.23+gd211df0+chromium-151.0.7922.170");
|
|
117
|
+
expect(build.sha1).toBe("94b409659401b34bf8616f9592ba5daeb9ee82d4");
|
|
118
|
+
expect(build.size).toBe(130993494);
|
|
119
|
+
expect(build.url).toBe(
|
|
120
|
+
"https://cef-builds.spotifycdn.com/cef_binary_151.3.23%2Bgd211df0%2Bchromium-151.0.7922.170_macosarm64_minimal.tar.bz2",
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("the pin matches on the + boundary, so 151.3.2 is not 151.3.23", () => {
|
|
125
|
+
expect(selectCefBuild(INDEX, "macosarm64", "151.3.2").cefVersion).toBe("151.3.2+gc0ffee0+chromium-151.0.7922.100");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("names the newest build when the pin is not in the index", () => {
|
|
129
|
+
expect(() => selectCefBuild(INDEX, "macosarm64", "9.9.9")).toThrow("152.0.1");
|
|
130
|
+
expect(() => selectCefBuild(INDEX, "linux64", "151.3.23")).toThrow("no builds");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("fetchCefIndex", () => {
|
|
135
|
+
test("ND_CEF_INDEX reads a local copy instead of the network", async () => {
|
|
136
|
+
const path = join(tempDir(), "index.json");
|
|
137
|
+
writeFileSync(path, JSON.stringify(INDEX));
|
|
138
|
+
expect(await fetchCefIndex({ ND_CEF_INDEX: path })).toEqual(INDEX);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("ensureCefDist", () => {
|
|
143
|
+
test("an already-extracted dist is returned without a download", async () => {
|
|
144
|
+
const dist = cefDistDir(PINNED, "macosarm64");
|
|
145
|
+
if (!isCefRoot(join(dist, "Release"), "macosarm64")) {
|
|
146
|
+
console.error(`ND_TEST_SKIP no cached CEF ${PINNED} macosarm64 dist at ${dist}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
expect(await ensureCefDist({ cefPlatform: "macosarm64", version: PINNED, offline: true })).toBe(dist);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("offline with nothing cached names the path it wanted", async () => {
|
|
153
|
+
const env = { ...process.env, XDG_CACHE_HOME: tempDir() };
|
|
154
|
+
expect(ensureCefDist({ cefPlatform: "linux64", version: PINNED, env, offline: true }))
|
|
155
|
+
.rejects.toThrow("downloads are disabled");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("planCefMac", () => {
|
|
160
|
+
test("stages the framework and the five helper bundles CEF requires", () => {
|
|
161
|
+
const root = tempDir();
|
|
162
|
+
const dist = synthesizeMacDist(join(root, "dist"));
|
|
163
|
+
const contents = join(root, "Demo.app", "Contents");
|
|
164
|
+
const plan = planCefMac(macPlanInput(dist, contents, join(root, CEF_HELPER_BINARY_NAME)));
|
|
165
|
+
|
|
166
|
+
expect(plan.framework.to).toBe(join(contents, "Frameworks", FRAMEWORK));
|
|
167
|
+
expect(plan.frameworkLibraries.map((p) => p.replace(`${plan.framework.to}/`, "")))
|
|
168
|
+
.toEqual(["Libraries/libEGL.dylib", "Libraries/libGLESv2.dylib"]);
|
|
169
|
+
expect(plan.helpers.map((h) => h.suffix)).toEqual([...CEF_HELPER_SUFFIXES]);
|
|
170
|
+
expect(plan.helpers.map((h) => h.bundleId)).toEqual([
|
|
171
|
+
"com.example.demo.helper",
|
|
172
|
+
"com.example.demo.helper.alerts",
|
|
173
|
+
"com.example.demo.helper.gpu",
|
|
174
|
+
"com.example.demo.helper.plugin",
|
|
175
|
+
"com.example.demo.helper.renderer",
|
|
176
|
+
]);
|
|
177
|
+
expect(plan.helpers.map((h) => h.jit)).toEqual([false, false, true, false, true]);
|
|
178
|
+
expect(plan.helpers[4]!.executablePath)
|
|
179
|
+
.toBe(join(contents, "Frameworks", "Demo Helper (Renderer).app", "Contents", "MacOS", "Demo Helper (Renderer)"));
|
|
180
|
+
expect(plan.helpers[0]!.plist).toContain("<key>CFBundleIdentifier</key><string>com.example.demo.helper</string>");
|
|
181
|
+
expect(plan.helpers[0]!.plist).toContain("<key>LSUIElement</key><string>1</string>");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("rejects a dist with no framework in it", () => {
|
|
185
|
+
const root = tempDir();
|
|
186
|
+
mkdirSync(join(root, "dist", "Release"), { recursive: true });
|
|
187
|
+
expect(() => planCefMac(macPlanInput(join(root, "dist"), join(root, "x", "Contents"), "/nope")))
|
|
188
|
+
.toThrow("has no Chromium Embedded Framework.framework");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("the real cached dist plans the same way", () => {
|
|
192
|
+
const dist = cefDistDir(PINNED, "macosarm64");
|
|
193
|
+
if (!isCefRoot(join(dist, "Release"), "macosarm64")) {
|
|
194
|
+
console.error(`ND_TEST_SKIP no cached CEF ${PINNED} macosarm64 dist at ${dist}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const root = tempDir();
|
|
198
|
+
const plan = planCefMac(macPlanInput(dist, join(root, "Demo.app", "Contents"), join(root, CEF_HELPER_BINARY_NAME)));
|
|
199
|
+
expect(plan.framework.from).toBe(join(dist, "Release", FRAMEWORK));
|
|
200
|
+
expect(plan.frameworkLibraries.length).toBeGreaterThan(0);
|
|
201
|
+
expect(plan.helpers).toHaveLength(5);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("cefMacSignTargets", () => {
|
|
206
|
+
test("signs inside-out: framework Mach-O, the framework, then each helper", () => {
|
|
207
|
+
const root = tempDir();
|
|
208
|
+
const dist = synthesizeMacDist(join(root, "dist"));
|
|
209
|
+
const contents = join(root, "Demo.app", "Contents");
|
|
210
|
+
const plan = planCefMac(macPlanInput(dist, contents, join(root, CEF_HELPER_BINARY_NAME)));
|
|
211
|
+
const targets = cefMacSignTargets(plan).map((t) => ({ path: t.path.replace(`${contents}/Frameworks/`, ""), jit: t.jit }));
|
|
212
|
+
|
|
213
|
+
expect(targets.slice(0, 3)).toEqual([
|
|
214
|
+
{ path: `${FRAMEWORK}/Libraries/libEGL.dylib`, jit: false },
|
|
215
|
+
{ path: `${FRAMEWORK}/Libraries/libGLESv2.dylib`, jit: false },
|
|
216
|
+
{ path: FRAMEWORK, jit: false },
|
|
217
|
+
]);
|
|
218
|
+
// Each helper's executable precedes its bundle, and only renderer/GPU ask
|
|
219
|
+
// for the JIT entitlements.
|
|
220
|
+
expect(targets.slice(3)).toEqual([
|
|
221
|
+
{ path: "Demo Helper.app/Contents/MacOS/Demo Helper", jit: false },
|
|
222
|
+
{ path: "Demo Helper.app", jit: false },
|
|
223
|
+
{ path: "Demo Helper (Alerts).app/Contents/MacOS/Demo Helper (Alerts)", jit: false },
|
|
224
|
+
{ path: "Demo Helper (Alerts).app", jit: false },
|
|
225
|
+
{ path: "Demo Helper (GPU).app/Contents/MacOS/Demo Helper (GPU)", jit: true },
|
|
226
|
+
{ path: "Demo Helper (GPU).app", jit: true },
|
|
227
|
+
{ path: "Demo Helper (Plugin).app/Contents/MacOS/Demo Helper (Plugin)", jit: false },
|
|
228
|
+
{ path: "Demo Helper (Plugin).app", jit: false },
|
|
229
|
+
{ path: "Demo Helper (Renderer).app/Contents/MacOS/Demo Helper (Renderer)", jit: true },
|
|
230
|
+
{ path: "Demo Helper (Renderer).app", jit: true },
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("applyCefMacPlan", () => {
|
|
236
|
+
test("keeps the framework's symlinks as symlinks and writes the helper bundles", () => {
|
|
237
|
+
const root = tempDir();
|
|
238
|
+
const dist = synthesizeMacDist(join(root, "dist"));
|
|
239
|
+
const contents = join(root, "Demo.app", "Contents");
|
|
240
|
+
const helper = join(root, CEF_HELPER_BINARY_NAME);
|
|
241
|
+
writeFileSync(helper, "helper");
|
|
242
|
+
const plan = planCefMac(macPlanInput(dist, contents, helper));
|
|
243
|
+
applyCefMacPlan(plan);
|
|
244
|
+
|
|
245
|
+
expect(lstatSync(join(plan.framework.to, "Chromium Embedded Framework")).isSymbolicLink()).toBe(true);
|
|
246
|
+
expect(lstatSync(join(plan.framework.to, "Versions", "Current")).isSymbolicLink()).toBe(true);
|
|
247
|
+
expect(lstatSync(join(plan.framework.to, "Versions", "A", "Chromium Embedded Framework")).isFile()).toBe(true);
|
|
248
|
+
for (const h of plan.helpers) {
|
|
249
|
+
expect(readFileSync(h.executablePath, "utf8")).toBe("helper");
|
|
250
|
+
expect(existsSync(join(h.appPath, "Contents", "Info.plist"))).toBe(true);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("names the missing helper binary instead of producing a broken bundle", () => {
|
|
255
|
+
const root = tempDir();
|
|
256
|
+
const dist = synthesizeMacDist(join(root, "dist"));
|
|
257
|
+
const plan = planCefMac(macPlanInput(dist, join(root, "Demo.app", "Contents"), join(root, CEF_HELPER_BINARY_NAME)));
|
|
258
|
+
expect(() => applyCefMacPlan(plan)).toThrow(CEF_HELPER_BINARY_NAME);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("resolveCefHelperBinary", () => {
|
|
263
|
+
test("ND_CEF_HELPER wins, else the binary sits beside the host", () => {
|
|
264
|
+
expect(resolveCefHelperBinary("/b/nd-shell", { ND_CEF_HELPER: "/x/helper" })).toBe("/x/helper");
|
|
265
|
+
expect(resolveCefHelperBinary("/b/nd-shell", {})).toBe(`/b/${CEF_HELPER_BINARY_NAME}`);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("planCefLinux", () => {
|
|
270
|
+
test("stages CEF under lib/cef, strips only libcef.so, and defaults to en-US", () => {
|
|
271
|
+
const root = tempDir();
|
|
272
|
+
const dist = synthesizeLinuxDist(join(root, "dist"), ["en-US", "de", "fr"]);
|
|
273
|
+
const plan = planCefLinux({ distRoot: dist, appDir: join(root, "AppDir") });
|
|
274
|
+
const staged = plan.files.map((f) => f.to.replace(`${plan.root}/`, ""));
|
|
275
|
+
|
|
276
|
+
expect(plan.root).toBe(join(root, "AppDir", "lib", "cef"));
|
|
277
|
+
expect(plan.missing).toEqual([]);
|
|
278
|
+
expect(staged).toEqual([
|
|
279
|
+
"libcef.so",
|
|
280
|
+
"libEGL.so",
|
|
281
|
+
"libGLESv2.so",
|
|
282
|
+
"libvk_swiftshader.so",
|
|
283
|
+
"libvulkan.so.1",
|
|
284
|
+
"vk_swiftshader_icd.json",
|
|
285
|
+
"v8_context_snapshot.bin",
|
|
286
|
+
"chrome-sandbox",
|
|
287
|
+
"icudtl.dat",
|
|
288
|
+
"resources.pak",
|
|
289
|
+
"chrome_100_percent.pak",
|
|
290
|
+
"chrome_200_percent.pak",
|
|
291
|
+
]);
|
|
292
|
+
expect(plan.files.filter((f) => f.strip).map((f) => f.to.replace(`${plan.root}/`, ""))).toEqual(["libcef.so"]);
|
|
293
|
+
expect(plan.locales.map((f) => f.to.replace(`${plan.root}/`, ""))).toEqual(["locales/en-US.pak"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("the config's locale list is the whole locales/ directory", () => {
|
|
297
|
+
const root = tempDir();
|
|
298
|
+
const dist = synthesizeLinuxDist(join(root, "dist"), ["en-US", "de", "nl"]);
|
|
299
|
+
const plan = planCefLinux({ distRoot: dist, appDir: join(root, "AppDir"), locales: ["de", "nl"] });
|
|
300
|
+
expect(plan.locales.map((f) => f.to.replace(`${plan.root}/`, ""))).toEqual(["locales/de.pak", "locales/nl.pak"]);
|
|
301
|
+
expect(plan.missing).toEqual([]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("a locale the dist does not carry is reported, not silently dropped", () => {
|
|
305
|
+
const root = tempDir();
|
|
306
|
+
const dist = synthesizeLinuxDist(join(root, "dist"), ["en-US"]);
|
|
307
|
+
const plan = planCefLinux({ distRoot: dist, appDir: join(root, "AppDir"), locales: ["en-US", "kl"] });
|
|
308
|
+
expect(plan.missing).toEqual(["Resources/locales/kl.pak"]);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe("cefBundleAudit", () => {
|
|
313
|
+
test("an engine=system bundle reports nothing", () => {
|
|
314
|
+
const root = tempDir();
|
|
315
|
+
mkdirSync(join(root, "Demo.app", "Contents", "Frameworks"), { recursive: true });
|
|
316
|
+
writeFileSync(join(root, "Demo.app", "Contents", "MacOS"), "");
|
|
317
|
+
expect(cefBundleAudit(join(root, "Demo.app"))).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("a staged framework and a staged libcef.so are both found", () => {
|
|
321
|
+
const root = tempDir();
|
|
322
|
+
const frameworks = join(root, "Demo.app", "Contents", "Frameworks");
|
|
323
|
+
mkdirSync(join(frameworks, FRAMEWORK), { recursive: true });
|
|
324
|
+
expect(cefBundleAudit(join(root, "Demo.app"))).toEqual([`Contents/Frameworks/${FRAMEWORK}`]);
|
|
325
|
+
|
|
326
|
+
const appdir = join(root, "AppDir", "lib", "cef");
|
|
327
|
+
mkdirSync(appdir, { recursive: true });
|
|
328
|
+
writeFileSync(join(appdir, "libcef.so"), "");
|
|
329
|
+
writeFileSync(join(appdir, "icudtl.dat"), "");
|
|
330
|
+
expect(cefBundleAudit(join(root, "AppDir"))).toEqual(["lib/cef/icudtl.dat", "lib/cef/libcef.so"]);
|
|
331
|
+
});
|
|
332
|
+
});
|