akanjs 3.0.0-beta.30 → 3.0.0-beta.32
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/client/translator.ts +26 -0
- package/index.ts +29 -4
- package/package.json +1 -1
- package/server/rscWorker.tsx +12 -1
- package/server/rscWorkerHost.ts +20 -1
- package/server/webRouter.ts +4 -1
- package/types/client/translator.d.ts +1 -0
- package/types/index.d.ts +28 -4
- package/types/server/rscWorkerHost.d.ts +1 -0
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +20 -4
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +7 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/addons.d.ts +19 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +95 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +21 -1
- package/types/vendor/@akanjs/native/packages/cli/src/lib/signing.d.ts +2 -2
- package/types/vendor/@akanjs/native/packages/cli/src/lib/toolchains.d.ts +9 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.d.ts +30 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop.d.ts +8 -2
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +2 -0
- package/ui/System/Client.tsx +9 -16
- package/ui/System/SSR.tsx +1 -1
- package/vendor/@akanjs/native/packages/cli/src/api.ts +53 -7
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +38 -4
- package/vendor/@akanjs/native/packages/cli/src/config.ts +7 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +217 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +1 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +357 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +34 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +1 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/signing.ts +4 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +24 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.ts +155 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +30 -13
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +84 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +4 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +115 -43
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +29 -2
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +12 -1
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
|
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { delimiter, join } from "node:path";
|
|
7
|
+
import { addonReport } from "../lib/addons.ts";
|
|
7
8
|
import type { ParsedArgs } from "../lib/args.ts";
|
|
8
9
|
import { makeNativeBoot } from "../lib/boot.ts";
|
|
9
10
|
import { exec, execOrThrow } from "../lib/exec.ts";
|
|
@@ -13,7 +14,7 @@ import { desktopModuleProblems, pluginFile } from "../lib/native-plugins.ts";
|
|
|
13
14
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
14
15
|
import { PACKAGE_ROOT } from "../lib/root.ts";
|
|
15
16
|
import { akanNativeHome } from "../lib/toolchains.ts";
|
|
16
|
-
import { updatesResource } from "../lib/updates.ts";
|
|
17
|
+
import { type DesktopArch, hostArch, updatesResource } from "../lib/updates.ts";
|
|
17
18
|
|
|
18
19
|
export const NATIVE_DIR = join(PACKAGE_ROOT, "native", "desktop");
|
|
19
20
|
const DESKTOP_PKG = join(PACKAGE_ROOT, "packages", "desktop", "src");
|
|
@@ -70,12 +71,12 @@ const TRIPLE_OS: Record<DesktopOs, string> = {
|
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* The target the library must be built for: the
|
|
74
|
-
*
|
|
75
|
-
*
|
|
74
|
+
* The target the library must be built for: the app's CPU, which can differ from rustc's host (an x64 app built on
|
|
75
|
+
* an ARM64 Mac, or x64 Bun on an ARM64 PC under emulation). Null when it is rustc's host, so the usual
|
|
76
|
+
* target/<profile> folder is used.
|
|
76
77
|
*/
|
|
77
|
-
async function libraryTarget(os: DesktopOs): Promise<string | null> {
|
|
78
|
-
const wanted = `${ARCH[
|
|
78
|
+
async function libraryTarget(os: DesktopOs, arch: DesktopArch): Promise<string | null> {
|
|
79
|
+
const wanted = `${ARCH[arch] ?? arch}-${TRIPLE_OS[os]}`;
|
|
79
80
|
const verbose = await exec(["rustc", "-vV"], { cwd: NATIVE_DIR, env: cargoEnv(), echo: false });
|
|
80
81
|
const host = /^host: (\S+)/m.exec(verbose.stdout)?.[1];
|
|
81
82
|
return host && host !== wanted ? wanted : null;
|
|
@@ -93,16 +94,14 @@ export function cargoTargetDir(): string {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
/** Builds (incrementally) the Rust cdylib and returns its path. */
|
|
96
|
-
export async function buildNativeLibrary(os: DesktopOs, release: boolean): Promise<string> {
|
|
97
|
-
log.step(
|
|
98
|
-
const target = await libraryTarget(os);
|
|
97
|
+
export async function buildNativeLibrary(os: DesktopOs, release: boolean, arch = hostArch()): Promise<string> {
|
|
98
|
+
log.step(`native: cargo build (TAO + WRY)${arch === hostArch() ? "" : ` for ${arch}`}`);
|
|
99
|
+
const target = await libraryTarget(os, arch);
|
|
99
100
|
|
|
100
101
|
const args = ["cargo", "build", "--locked", "--manifest-path", join(NATIVE_DIR, "Cargo.toml"), "--quiet"];
|
|
101
102
|
if (release) args.push("--release");
|
|
102
103
|
if (target) {
|
|
103
|
-
log.info(
|
|
104
|
-
dim(`for ${target} (Bun is ${process.arch}); if the standard library is missing: rustup target add ${target}`),
|
|
105
|
-
);
|
|
104
|
+
log.info(dim(`for ${target}; if its standard library is missing: rustup target add ${target}`));
|
|
106
105
|
args.push("--target", target);
|
|
107
106
|
}
|
|
108
107
|
await execOrThrow(args, {
|
|
@@ -153,8 +152,19 @@ startMain(new URL("./host-entry.ts", import.meta.url).href);
|
|
|
153
152
|
*/
|
|
154
153
|
export const COMPILE_FLAGS = ["--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig"];
|
|
155
154
|
|
|
156
|
-
/**
|
|
155
|
+
/** The CPU a desktop build makes code for. */
|
|
156
|
+
export function targetArch(ctx: Pick<BuildContext, "arch">): DesktopArch {
|
|
157
|
+
return ctx.arch ?? hostArch();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const BUN_OS: Record<DesktopOs, string> = { macos: "darwin", windows: "windows", linux: "linux" };
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* bun build --compile of the main thread entry and the plugin host Worker into `outfile`, for the build's CPU: another
|
|
164
|
+
* CPU's Bun is a cross-compilation target Bun downloads once.
|
|
165
|
+
*/
|
|
157
166
|
export async function compileExecutable(ctx: BuildContext, outfile: string, extra: string[] = []): Promise<void> {
|
|
167
|
+
const arch = targetArch(ctx);
|
|
158
168
|
const problems = (await Promise.all(ctx.project.plugins.map(desktopModuleProblems))).flat();
|
|
159
169
|
if (problems.length)
|
|
160
170
|
throw new CliError(`desktop plugin modules do not match their manifests:\n - ${problems.join("\n - ")}`);
|
|
@@ -171,6 +181,7 @@ export async function compileExecutable(ctx: BuildContext, outfile: string, extr
|
|
|
171
181
|
"--outfile",
|
|
172
182
|
outfile,
|
|
173
183
|
...(ctx.dev ? [] : ["--minify"]),
|
|
184
|
+
...(arch === hostArch() ? [] : [`--target=bun-${BUN_OS[ctx.platform as DesktopOs]}-${arch}`]),
|
|
174
185
|
...extra,
|
|
175
186
|
],
|
|
176
187
|
{
|
|
@@ -214,6 +225,12 @@ export function writeDesktopResources(
|
|
|
214
225
|
log.warn(
|
|
215
226
|
"desktop.server without the single-instance plugin: every launch starts another server on the same data",
|
|
216
227
|
);
|
|
228
|
+
const addons = addonReport(server.dir, os, targetArch(ctx));
|
|
229
|
+
for (const warning of addons.warnings) log.warn(warning);
|
|
230
|
+
if (addons.problems.length)
|
|
231
|
+
throw new CliError(
|
|
232
|
+
`the server's native addons would not load on a user's computer:\n - ${addons.problems.join("\n - ")}`,
|
|
233
|
+
);
|
|
217
234
|
cpSync(server.dir, join(resources, "server"), { recursive: true, verbatimSymlinks: true });
|
|
218
235
|
writeFileSync(join(resources, "server.json"), JSON.stringify({ entry: server.entry, env: server.env }));
|
|
219
236
|
writeFileSync(join(resources, "server.bunfig.toml"), "");
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { chmodSync, cpSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execOrThrow } from "../lib/exec.ts";
|
|
6
|
+
import { iconArt } from "../lib/icons.ts";
|
|
7
|
+
import { resize } from "../lib/image.ts";
|
|
8
|
+
import { log, ToolchainError } from "../lib/log.ts";
|
|
9
|
+
import { encodePng } from "../lib/png.ts";
|
|
10
|
+
import type { BuildContext } from "../lib/prepare.ts";
|
|
11
|
+
import { ensureInstalled, TOOLCHAIN } from "../lib/toolchains.ts";
|
|
12
|
+
import { targetArch } from "./desktop.ts";
|
|
13
|
+
|
|
14
|
+
/** The launcher entry (freedesktop Desktop Entry): what a menu shows and which link schemes open the app. */
|
|
15
|
+
export function desktopEntry(input: { name: string; fileName: string; schemes: string[] }): string {
|
|
16
|
+
const escape = (value: string) => value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
|
|
17
|
+
return [
|
|
18
|
+
"[Desktop Entry]",
|
|
19
|
+
"Type=Application",
|
|
20
|
+
`Name=${escape(input.name)}`,
|
|
21
|
+
`Exec=${input.fileName} %u`,
|
|
22
|
+
`Icon=${input.fileName}`,
|
|
23
|
+
"Terminal=false",
|
|
24
|
+
"Categories=Utility;",
|
|
25
|
+
...(input.schemes.length ? [`MimeType=${input.schemes.map((s) => `x-scheme-handler/${s};`).join("")}`] : []),
|
|
26
|
+
"",
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** AppRun: the runtime starts it from the mounted image; it starts the app from beside itself. */
|
|
31
|
+
export function appRun(exe: string): string {
|
|
32
|
+
return `#!/bin/sh
|
|
33
|
+
HERE="$(dirname "$(readlink -f "$0")")"
|
|
34
|
+
exec "$HERE/${exe}" "$@"
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Writes the AppImage beside the app folder and answers its path. */
|
|
39
|
+
export async function buildAppImage(ctx: BuildContext, folder: string): Promise<string> {
|
|
40
|
+
const mksquashfs = Bun.which("mksquashfs");
|
|
41
|
+
if (!mksquashfs)
|
|
42
|
+
throw new ToolchainError("mksquashfs packs the AppImage: apt install squashfs-tools (dnf install squashfs-tools)");
|
|
43
|
+
const { config } = ctx.project;
|
|
44
|
+
const arch = targetArch(ctx);
|
|
45
|
+
const spec = TOOLCHAIN.appimageRuntime.downloads[arch];
|
|
46
|
+
if (!spec) throw new ToolchainError(`no pinned AppImage runtime for ${arch}`);
|
|
47
|
+
const runtime = join(
|
|
48
|
+
await ensureInstalled("appimage-runtime", `${TOOLCHAIN.appimageRuntime.version}-${arch}`, spec),
|
|
49
|
+
spec.root,
|
|
50
|
+
);
|
|
51
|
+
if (config.updates)
|
|
52
|
+
log.warn(
|
|
53
|
+
"an AppImage runs from a read-only image, so the updates plugin cannot replace it: publish a new AppImage for each release",
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const appDir = join(ctx.outDir, "gen", "AppDir");
|
|
57
|
+
rmSync(appDir, { recursive: true, force: true });
|
|
58
|
+
cpSync(folder, appDir, { recursive: true, verbatimSymlinks: true });
|
|
59
|
+
writeFileSync(join(appDir, "AppRun"), appRun(config.app.fileName));
|
|
60
|
+
chmodSync(join(appDir, "AppRun"), 0o755);
|
|
61
|
+
writeFileSync(
|
|
62
|
+
join(appDir, `${config.app.fileName}.desktop`),
|
|
63
|
+
desktopEntry({ name: config.app.name, fileName: config.app.fileName, schemes: config.deepLinks.schemes }),
|
|
64
|
+
);
|
|
65
|
+
const art = iconArt(config);
|
|
66
|
+
if (art) {
|
|
67
|
+
writeFileSync(join(appDir, `${config.app.fileName}.png`), encodePng(resize(art.master, 256, 256)));
|
|
68
|
+
symlinkSync(`${config.app.fileName}.png`, join(appDir, ".DirIcon"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
log.step("installer: AppImage");
|
|
72
|
+
const squashfs = join(ctx.outDir, "gen", "app.squashfs");
|
|
73
|
+
rmSync(squashfs, { force: true });
|
|
74
|
+
await execOrThrow(
|
|
75
|
+
[mksquashfs, appDir, squashfs, "-root-owned", "-noappend", "-comp", "zstd", "-no-progress", "-quiet"],
|
|
76
|
+
{ echo: false },
|
|
77
|
+
);
|
|
78
|
+
const outFile = join(ctx.outDir, `${config.app.fileName}-${config.app.version}-${arch}.AppImage`);
|
|
79
|
+
writeFileSync(outFile, Buffer.concat([readFileSync(runtime), readFileSync(squashfs)]));
|
|
80
|
+
chmodSync(outFile, 0o755);
|
|
81
|
+
rmSync(squashfs, { force: true });
|
|
82
|
+
log.info(`installer ${outFile} (${Math.round(readFileSync(outFile).length / 1024 / 1024)} MB)`);
|
|
83
|
+
return outFile;
|
|
84
|
+
}
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
requireHost,
|
|
17
17
|
resourcesOf,
|
|
18
18
|
runExecutable,
|
|
19
|
+
targetArch,
|
|
19
20
|
writeDesktopResources,
|
|
20
21
|
} from "./desktop.ts";
|
|
22
|
+
import { buildAppImage } from "./linux-appimage.ts";
|
|
21
23
|
|
|
22
24
|
const exePath = (ctx: BuildContext, dir: string) => join(dir, ctx.project.config.app.fileName);
|
|
23
25
|
|
|
@@ -25,7 +27,7 @@ export async function buildLinux(ctx: BuildContext): Promise<string> {
|
|
|
25
27
|
requireHost("linux");
|
|
26
28
|
const { outDir } = ctx;
|
|
27
29
|
const { config } = ctx.project;
|
|
28
|
-
const lib = await buildNativeLibrary("linux", !ctx.dev);
|
|
30
|
+
const lib = await buildNativeLibrary("linux", !ctx.dev, targetArch(ctx));
|
|
29
31
|
|
|
30
32
|
const dir = join(outDir, config.app.fileName);
|
|
31
33
|
rmSync(outDir, { recursive: true, force: true });
|
|
@@ -38,6 +40,7 @@ export async function buildLinux(ctx: BuildContext): Promise<string> {
|
|
|
38
40
|
writeDesktopResources(ctx, resources, "linux");
|
|
39
41
|
const art = iconArt(config);
|
|
40
42
|
if (art) writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
|
|
43
|
+
if (ctx.linux?.appImage) ctx.artifacts.push({ kind: "installer", path: await buildAppImage(ctx, dir) });
|
|
41
44
|
const size = await folderSize(dir);
|
|
42
45
|
if (size) log.info(dim(`size ${size}`));
|
|
43
46
|
return dir;
|
|
@@ -12,16 +12,30 @@ import {
|
|
|
12
12
|
rmSync,
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
16
|
import type { ParsedArgs } from "../lib/args.ts";
|
|
17
17
|
import { execOrThrow } from "../lib/exec.ts";
|
|
18
18
|
import { icns, iconArt, macosIconImage } from "../lib/icons.ts";
|
|
19
19
|
import type { Launched, LaunchOptions } from "../lib/launch.ts";
|
|
20
20
|
import { dim, log } from "../lib/log.ts";
|
|
21
|
+
import {
|
|
22
|
+
appEntitlements,
|
|
23
|
+
assessGatekeeper,
|
|
24
|
+
buildDmg,
|
|
25
|
+
codesign,
|
|
26
|
+
notarize,
|
|
27
|
+
type ResolvedMacosIdentity,
|
|
28
|
+
staple,
|
|
29
|
+
withMacosIdentity,
|
|
30
|
+
writeEntitlements,
|
|
31
|
+
zipForNotary,
|
|
32
|
+
} from "../lib/macossigning.ts";
|
|
21
33
|
import { permissionPlist } from "../lib/permissions.ts";
|
|
22
34
|
import { type PlistValue, toPlist } from "../lib/plist.ts";
|
|
23
35
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
36
|
+
import { SigningError } from "../lib/prepare.ts";
|
|
24
37
|
import { devIdentity } from "../lib/signing.ts";
|
|
38
|
+
import { hostArch } from "../lib/updates.ts";
|
|
25
39
|
import {
|
|
26
40
|
buildNativeLibrary,
|
|
27
41
|
compileExecutable,
|
|
@@ -30,6 +44,7 @@ import {
|
|
|
30
44
|
NATIVE_LIB,
|
|
31
45
|
requireHost,
|
|
32
46
|
runExecutable,
|
|
47
|
+
targetArch,
|
|
33
48
|
writeDesktopResources,
|
|
34
49
|
} from "./desktop.ts";
|
|
35
50
|
|
|
@@ -39,7 +54,7 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
|
|
|
39
54
|
requireHost("macos");
|
|
40
55
|
const { project, outDir } = ctx;
|
|
41
56
|
const { config } = project;
|
|
42
|
-
const lib = await buildNativeLibrary("macos", !ctx.dev);
|
|
57
|
+
const lib = await buildNativeLibrary("macos", !ctx.dev, targetArch(ctx));
|
|
43
58
|
|
|
44
59
|
const appPath = join(outDir, `${config.app.name}.app`);
|
|
45
60
|
const contents = join(appPath, "Contents");
|
|
@@ -51,53 +66,110 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
|
|
|
51
66
|
cpSync(lib, join(contents, "Frameworks", NATIVE_LIB.macos));
|
|
52
67
|
|
|
53
68
|
const resources = join(contents, "Resources");
|
|
54
|
-
|
|
55
|
-
const identity = await devIdentity();
|
|
56
|
-
writeDesktopResources(ctx, resources, "macos", { signing: identity ? "identity" : "adhoc" });
|
|
57
|
-
|
|
58
|
-
const art = iconArt(config);
|
|
59
|
-
if (art) writeFileSync(join(resources, "AppIcon.icns"), icns(macosIconImage(art)));
|
|
60
|
-
|
|
61
69
|
const usage: Record<string, PlistValue> = {};
|
|
62
70
|
for (const plugin of project.plugins) Object.assign(usage, plugin.manifest.macos?.infoPlist ?? {});
|
|
63
71
|
Object.assign(usage, permissionPlist(config.permissions, "macos"));
|
|
64
72
|
for (const key of Object.keys(usage)) if (config.usageDescriptions[key]) usage[key] = config.usageDescriptions[key]!;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
73
|
+
|
|
74
|
+
return withMacosIdentity(
|
|
75
|
+
ctx.macos?.signing,
|
|
76
|
+
async (identity) => {
|
|
77
|
+
if (ctx.macos?.notarize && identity.kind !== "distribution")
|
|
78
|
+
throw new SigningError(
|
|
79
|
+
`notarization takes a Developer ID Application identity, and the build is signed with "${identity.name}"`,
|
|
80
|
+
);
|
|
81
|
+
if (ctx.macos?.notarize && ctx.dev) throw new SigningError("a debug build is never notarized: build a release");
|
|
82
|
+
writeDesktopResources(ctx, resources, "macos", { signing: shellSigning(identity) });
|
|
83
|
+
|
|
84
|
+
const art = iconArt(config);
|
|
85
|
+
if (art) writeFileSync(join(resources, "AppIcon.icns"), icns(macosIconImage(art)));
|
|
86
|
+
writeFileSync(
|
|
87
|
+
join(contents, "Info.plist"),
|
|
88
|
+
toPlist({
|
|
89
|
+
CFBundleDevelopmentRegion: "en",
|
|
90
|
+
CFBundleExecutable: exe,
|
|
91
|
+
CFBundleIdentifier: config.app.id,
|
|
92
|
+
CFBundleName: config.app.name,
|
|
93
|
+
CFBundleDisplayName: config.app.name,
|
|
94
|
+
CFBundlePackageType: "APPL",
|
|
95
|
+
CFBundleShortVersionString: config.app.version,
|
|
96
|
+
CFBundleVersion: String(config.app.build),
|
|
97
|
+
CFBundleInfoDictionaryVersion: "6.0",
|
|
98
|
+
LSMinimumSystemVersion: MACOS_MIN,
|
|
99
|
+
NSHighResolutionCapable: true,
|
|
100
|
+
NSSupportsAutomaticGraphicsSwitching: true,
|
|
101
|
+
...(art ? { CFBundleIconFile: "AppIcon" } : {}),
|
|
102
|
+
...(config.deepLinks.schemes.length
|
|
103
|
+
? { CFBundleURLTypes: [{ CFBundleURLName: config.app.id, CFBundleURLSchemes: config.deepLinks.schemes }] }
|
|
104
|
+
: {}),
|
|
105
|
+
...usage,
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
log.step(`sign: ${identity.name}`);
|
|
110
|
+
|
|
111
|
+
await execOrThrow(["xattr", "-cr", appPath], { echo: false });
|
|
112
|
+
|
|
113
|
+
const team = identity.kind === "development" || identity.kind === "distribution";
|
|
114
|
+
for (const addon of carriedNativeCode(resources)) await codesign(identity, addon, { hardened: team });
|
|
115
|
+
await codesign(identity, join(contents, "Frameworks", NATIVE_LIB.macos), { hardened: team });
|
|
116
|
+
const entitlements = team
|
|
117
|
+
? writeEntitlements(
|
|
118
|
+
join(outDir, "gen"),
|
|
119
|
+
appEntitlements(usage, (config.native?.macos?.entitlements ?? {}) as Record<string, PlistValue>),
|
|
120
|
+
)
|
|
121
|
+
: undefined;
|
|
122
|
+
await codesign(identity, appPath, { hardened: team, ...(entitlements ? { entitlements } : {}) });
|
|
123
|
+
await execOrThrow(["codesign", "--verify", "--deep", "--strict", appPath], { echo: false });
|
|
124
|
+
ctx.signedAs =
|
|
125
|
+
identity.kind === "distribution" ? "distribution" : identity.kind === "development" ? "development" : "adhoc";
|
|
126
|
+
if (identity.kind !== "distribution" && !ctx.dev)
|
|
127
|
+
log.warn(
|
|
128
|
+
"not signed with a Developer ID: Gatekeeper blocks this app once it is downloaded to another Mac (set AKAN_NATIVE_MACOS_IDENTITY or AKAN_NATIVE_MACOS_CERTIFICATE)",
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const notarization = ctx.macos?.notarize;
|
|
132
|
+
if (notarization) {
|
|
133
|
+
const zip = join(outDir, "gen", `${config.app.fileName}-notarize.zip`);
|
|
134
|
+
await zipForNotary(appPath, zip);
|
|
135
|
+
await notarize(zip, notarization);
|
|
136
|
+
rmSync(zip, { force: true });
|
|
137
|
+
await staple(appPath);
|
|
138
|
+
await assessGatekeeper(appPath, "execute");
|
|
139
|
+
} else if (identity.kind === "distribution" && !ctx.dev)
|
|
140
|
+
log.warn(
|
|
141
|
+
"signed with a Developer ID but not notarized: Gatekeeper still blocks a downloaded copy (set AKAN_NATIVE_MACOS_NOTARY_KEY, _KEY_ID and _ISSUER)",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (ctx.macos?.dmg) {
|
|
145
|
+
const dmg = join(outDir, `${config.app.fileName}-${config.app.version}-${ctx.arch ?? hostArch()}.dmg`);
|
|
146
|
+
log.step(`dmg: ${basename(dmg)}`);
|
|
147
|
+
await buildDmg(appPath, dmg, config.app.name, join(outDir, "gen"));
|
|
148
|
+
|
|
149
|
+
if (identity.sign !== "-") await codesign(identity, dmg, { hardened: false, timestamp: team });
|
|
150
|
+
if (notarization) {
|
|
151
|
+
await notarize(dmg, notarization);
|
|
152
|
+
await staple(dmg);
|
|
153
|
+
await assessGatekeeper(dmg, "open");
|
|
154
|
+
}
|
|
155
|
+
ctx.artifacts.push({ kind: "installer", path: dmg });
|
|
156
|
+
}
|
|
157
|
+
const size = await folderSize(appPath);
|
|
158
|
+
if (size) log.info(dim(`size ${size}`));
|
|
159
|
+
return appPath;
|
|
160
|
+
},
|
|
161
|
+
async () => {
|
|
162
|
+
|
|
163
|
+
const dev = await devIdentity();
|
|
164
|
+
return dev ? { sign: dev.hash, name: dev.name, kind: "identity" } : { sign: "-", name: "ad-hoc", kind: "adhoc" };
|
|
165
|
+
},
|
|
86
166
|
);
|
|
167
|
+
}
|
|
87
168
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
for (const addon of carriedNativeCode(resources))
|
|
93
|
-
await execOrThrow(["codesign", "--force", "--sign", sign, addon], { echo: false });
|
|
94
|
-
await execOrThrow(["codesign", "--force", "--sign", sign, join(contents, "Frameworks", NATIVE_LIB.macos)], {
|
|
95
|
-
echo: false,
|
|
96
|
-
});
|
|
97
|
-
await execOrThrow(["codesign", "--force", "--sign", sign, appPath], { echo: false });
|
|
98
|
-
const size = await folderSize(appPath);
|
|
99
|
-
if (size) log.info(dim(`size ${size}`));
|
|
100
|
-
return appPath;
|
|
169
|
+
/** shell.json `signing`: secure-storage keeps its items in-process only under a team's signature. */
|
|
170
|
+
function shellSigning(identity: ResolvedMacosIdentity): "team" | "identity" | "adhoc" {
|
|
171
|
+
if (identity.kind === "development" || identity.kind === "distribution") return "team";
|
|
172
|
+
return identity.kind === "identity" ? "identity" : "adhoc";
|
|
101
173
|
}
|
|
102
174
|
|
|
103
175
|
/**
|
|
@@ -7,6 +7,7 @@ import { CliError, log, ToolchainError } from "../lib/log.ts";
|
|
|
7
7
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
8
8
|
import { akanNativeHome } from "../lib/toolchains.ts";
|
|
9
9
|
import { hostArch } from "../lib/updates.ts";
|
|
10
|
+
import { signWindowsFiles } from "../lib/windowssigning.ts";
|
|
10
11
|
import { windowsVersion } from "./windows.ts";
|
|
11
12
|
|
|
12
13
|
/** Microsoft's permanent link to the Evergreen Bootstrapper (MicrosoftEdgeWebview2Setup.exe). */
|
|
@@ -41,6 +42,8 @@ export interface InstallerInput {
|
|
|
41
42
|
bootstrapper: string;
|
|
42
43
|
stopScript: string;
|
|
43
44
|
outFile: string;
|
|
45
|
+
/** A command that signs the uninstaller makensis writes into the setup program, `%1` its path. */
|
|
46
|
+
signUninstaller?: string;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** `$` is NSIS's variable sigil; the names that reach a script cannot hold a quote (config.ts app.name). */
|
|
@@ -96,6 +99,7 @@ RequestExecutionLevel user
|
|
|
96
99
|
|
|
97
100
|
Name "\${APP_NAME}"
|
|
98
101
|
OutFile "${nsis(input.outFile)}"
|
|
102
|
+
${input.signUninstaller ? `!uninstfinalize '${input.signUninstaller}' = 0\n` : ""}
|
|
99
103
|
InstallDir "$LOCALAPPDATA\\Programs\\\${APP_NAME}"
|
|
100
104
|
InstallDirRegKey HKCU "\${UNINSTALL_KEY}" "InstallLocation"
|
|
101
105
|
BrandingText "\${APP_NAME} ${nsis(input.version)}"
|
|
@@ -551,6 +555,22 @@ async function webview2Bootstrapper(): Promise<string> {
|
|
|
551
555
|
return file;
|
|
552
556
|
}
|
|
553
557
|
|
|
558
|
+
const SIGNING_ENV = "AKAN_NATIVE_WINDOWS_SIGNING";
|
|
559
|
+
|
|
560
|
+
/** The command `!uninstfinalize` runs on the uninstaller: this Bun on a script that signs it as the app was signed. */
|
|
561
|
+
function uninstallerSigner(gen: string, description: string): string {
|
|
562
|
+
const script = join(gen, "sign-uninstaller.ts");
|
|
563
|
+
writeFileSync(
|
|
564
|
+
script,
|
|
565
|
+
`// Generated by akan-native build. Do not edit.
|
|
566
|
+
import { signWindowsFiles } from ${JSON.stringify(join(import.meta.dir, "..", "lib", "windowssigning.ts"))};
|
|
567
|
+
|
|
568
|
+
await signWindowsFiles(JSON.parse(process.env.${SIGNING_ENV} ?? "{}"), [process.argv[2] ?? ""], ${JSON.stringify(description)});
|
|
569
|
+
`,
|
|
570
|
+
);
|
|
571
|
+
return `"${process.execPath}" "${script}" "%1"`;
|
|
572
|
+
}
|
|
573
|
+
|
|
554
574
|
/** Writes the setup program next to the app folder and answers its path. */
|
|
555
575
|
export async function buildWindowsInstaller(ctx: BuildContext, folder: string): Promise<string> {
|
|
556
576
|
const makensis = findMakensis();
|
|
@@ -564,7 +584,7 @@ export async function buildWindowsInstaller(ctx: BuildContext, folder: string):
|
|
|
564
584
|
const stopScript = join(gen, "stop-app.ps1");
|
|
565
585
|
writeFileSync(stopScript, STOP_SCRIPT);
|
|
566
586
|
const icon = join(gen, "app.ico");
|
|
567
|
-
const outFile = join(ctx.outDir, `${config.app.fileName}-${config.app.version}-${hostArch()}-setup.exe`);
|
|
587
|
+
const outFile = join(ctx.outDir, `${config.app.fileName}-${config.app.version}-${ctx.arch ?? hostArch()}-setup.exe`);
|
|
568
588
|
const script = join(gen, "installer.nsi");
|
|
569
589
|
const deepest = deepestPath(folder);
|
|
570
590
|
const longest = installedPathLength(config.app.name, config.app.version, deepest);
|
|
@@ -590,12 +610,19 @@ export async function buildWindowsInstaller(ctx: BuildContext, folder: string):
|
|
|
590
610
|
bootstrapper,
|
|
591
611
|
stopScript,
|
|
592
612
|
outFile,
|
|
613
|
+
...(ctx.windows?.signing ? { signUninstaller: uninstallerSigner(gen, `${config.app.name} Uninstall`) } : {}),
|
|
593
614
|
})}`,
|
|
594
615
|
);
|
|
595
616
|
log.step("installer: makensis");
|
|
596
|
-
const
|
|
617
|
+
const signing = ctx.windows?.signing;
|
|
618
|
+
const result = await exec([makensis, "/V2", script], {
|
|
619
|
+
echo: false,
|
|
620
|
+
|
|
621
|
+
...(signing ? { env: { [SIGNING_ENV]: JSON.stringify(signing) } } : {}),
|
|
622
|
+
});
|
|
597
623
|
if (result.code !== 0 || !existsSync(outFile))
|
|
598
624
|
throw new CliError(`makensis failed:\n${(result.stdout + result.stderr).trim().split("\n").slice(-20).join("\n")}`);
|
|
625
|
+
if (signing) await signWindowsFiles(signing, [outFile], `${config.app.name} Setup`);
|
|
599
626
|
log.info(`installer ${outFile} (${Math.round(readFileSync(outFile).length / 1024 / 1024)} MB)`);
|
|
600
627
|
return outFile;
|
|
601
628
|
}
|
|
@@ -7,6 +7,7 @@ import { ico, iconArt, windowIcon } from "../lib/icons.ts";
|
|
|
7
7
|
import type { Launched, LaunchOptions } from "../lib/launch.ts";
|
|
8
8
|
import { log } from "../lib/log.ts";
|
|
9
9
|
import type { BuildContext } from "../lib/prepare.ts";
|
|
10
|
+
import { peFiles, signWindowsFiles } from "../lib/windowssigning.ts";
|
|
10
11
|
import {
|
|
11
12
|
buildNativeLibrary,
|
|
12
13
|
compileExecutable,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
requireHost,
|
|
16
17
|
resourcesOf,
|
|
17
18
|
runExecutable,
|
|
19
|
+
targetArch,
|
|
18
20
|
writeDesktopResources,
|
|
19
21
|
} from "./desktop.ts";
|
|
20
22
|
import { buildWindowsInstaller } from "./windows-installer.ts";
|
|
@@ -39,7 +41,7 @@ export async function buildWindows(ctx: BuildContext): Promise<string> {
|
|
|
39
41
|
requireHost("windows");
|
|
40
42
|
const { outDir } = ctx;
|
|
41
43
|
const { config } = ctx.project;
|
|
42
|
-
const lib = await buildNativeLibrary("windows", !ctx.dev);
|
|
44
|
+
const lib = await buildNativeLibrary("windows", !ctx.dev, targetArch(ctx));
|
|
43
45
|
|
|
44
46
|
if (config.app.id.length > WEBVIEW2_ID_BUDGET) {
|
|
45
47
|
log.warn(
|
|
@@ -71,6 +73,15 @@ export async function buildWindows(ctx: BuildContext): Promise<string> {
|
|
|
71
73
|
const resources = resourcesOf("windows", dir);
|
|
72
74
|
writeDesktopResources(ctx, resources, "windows");
|
|
73
75
|
if (art) writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
|
|
76
|
+
const signing = ctx.windows?.signing;
|
|
77
|
+
if (signing) {
|
|
78
|
+
log.step("sign: Authenticode");
|
|
79
|
+
await signWindowsFiles(signing, peFiles(dir), config.app.name);
|
|
80
|
+
ctx.signedAs = "distribution";
|
|
81
|
+
} else if (!ctx.dev)
|
|
82
|
+
log.warn(
|
|
83
|
+
"not signed: SmartScreen warns on a downloaded copy (set AKAN_NATIVE_WINDOWS_CERTIFICATE, _THUMBPRINT or _SIGN_COMMAND)",
|
|
84
|
+
);
|
|
74
85
|
if (ctx.windows?.installer) ctx.artifacts.push({ kind: "installer", path: await buildWindowsInstaller(ctx, dir) });
|
|
75
86
|
return dir;
|
|
76
87
|
}
|