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.
Files changed (39) hide show
  1. package/client/translator.ts +26 -0
  2. package/index.ts +29 -4
  3. package/package.json +1 -1
  4. package/server/rscWorker.tsx +12 -1
  5. package/server/rscWorkerHost.ts +20 -1
  6. package/server/webRouter.ts +4 -1
  7. package/types/client/translator.d.ts +1 -0
  8. package/types/index.d.ts +28 -4
  9. package/types/server/rscWorkerHost.d.ts +1 -0
  10. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +20 -4
  11. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +7 -0
  12. package/types/vendor/@akanjs/native/packages/cli/src/lib/addons.d.ts +19 -0
  13. package/types/vendor/@akanjs/native/packages/cli/src/lib/macossigning.d.ts +95 -0
  14. package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +21 -1
  15. package/types/vendor/@akanjs/native/packages/cli/src/lib/signing.d.ts +2 -2
  16. package/types/vendor/@akanjs/native/packages/cli/src/lib/toolchains.d.ts +9 -0
  17. package/types/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.d.ts +30 -0
  18. package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop.d.ts +8 -2
  19. package/types/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.d.ts +11 -0
  20. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +2 -0
  21. package/ui/System/Client.tsx +9 -16
  22. package/ui/System/SSR.tsx +1 -1
  23. package/vendor/@akanjs/native/packages/cli/src/api.ts +53 -7
  24. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +38 -4
  25. package/vendor/@akanjs/native/packages/cli/src/config.ts +7 -0
  26. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +217 -0
  27. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +1 -0
  28. package/vendor/@akanjs/native/packages/cli/src/lib/macossigning.ts +357 -0
  29. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +34 -1
  30. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +1 -0
  31. package/vendor/@akanjs/native/packages/cli/src/lib/signing.ts +4 -3
  32. package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +24 -0
  33. package/vendor/@akanjs/native/packages/cli/src/lib/windowssigning.ts +155 -0
  34. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +30 -13
  35. package/vendor/@akanjs/native/packages/cli/src/platforms/linux-appimage.ts +84 -0
  36. package/vendor/@akanjs/native/packages/cli/src/platforms/linux.ts +4 -1
  37. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +115 -43
  38. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +29 -2
  39. 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 architecture of the Bun that compiles (and so
74
- * runs) the app, which can differ from rustc's host (x64 Bun on an ARM64 PC, under emulation).
75
- * Null when it is rustc's host, so the usual target/<profile> folder is used.
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[process.arch] ?? process.arch}-${TRIPLE_OS[os]}`;
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("native: cargo build (TAO + WRY)");
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
- /** bun build --compile of the main thread entry and the plugin host Worker into `outfile`. */
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
- writeFileSync(
66
- join(contents, "Info.plist"),
67
- toPlist({
68
- CFBundleDevelopmentRegion: "en",
69
- CFBundleExecutable: exe,
70
- CFBundleIdentifier: config.app.id,
71
- CFBundleName: config.app.name,
72
- CFBundleDisplayName: config.app.name,
73
- CFBundlePackageType: "APPL",
74
- CFBundleShortVersionString: config.app.version,
75
- CFBundleVersion: String(config.app.build),
76
- CFBundleInfoDictionaryVersion: "6.0",
77
- LSMinimumSystemVersion: MACOS_MIN,
78
- NSHighResolutionCapable: true,
79
- NSSupportsAutomaticGraphicsSwitching: true,
80
- ...(art ? { CFBundleIconFile: "AppIcon" } : {}),
81
- ...(config.deepLinks.schemes.length
82
- ? { CFBundleURLTypes: [{ CFBundleURLName: config.app.id, CFBundleURLSchemes: config.deepLinks.schemes }] }
83
- : {}),
84
- ...usage,
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
- const sign = identity?.hash ?? "-";
89
- log.step(`sign: ${identity ? identity.name : "ad-hoc"}`);
90
-
91
- await execOrThrow(["xattr", "-cr", appPath], { echo: false });
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 result = await exec([makensis, "/V2", script], { echo: false });
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
  }