@mirinjs/cli 0.1.0-alpha.23 → 0.1.0-alpha.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirinjs/cli",
3
- "version": "0.1.0-alpha.23",
3
+ "version": "0.1.0-alpha.25",
4
4
  "description": "CLI for mirin apps: dev, build, init.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,15 +27,15 @@
27
27
  "bun": ">=1.2.0"
28
28
  },
29
29
  "dependencies": {
30
- "create-mirinjs": "0.1.0-alpha.23",
31
- "mirinjs": "0.1.0-alpha.23"
30
+ "create-mirinjs": "0.1.0-alpha.25",
31
+ "mirinjs": "0.1.0-alpha.25"
32
32
  },
33
33
  "optionalDependencies": {
34
- "@mirinjs/darwin-arm64": "0.1.0-alpha.23",
35
- "@mirinjs/win32-x64": "0.1.0-alpha.23",
36
- "@mirinjs/win32-arm64": "0.1.0-alpha.23",
37
- "@mirinjs/linux-arm64": "0.1.0-alpha.23",
38
- "@mirinjs/linux-x64": "0.1.0-alpha.23"
34
+ "@mirinjs/darwin-arm64": "0.1.0-alpha.25",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.25",
36
+ "@mirinjs/win32-arm64": "0.1.0-alpha.25",
37
+ "@mirinjs/linux-arm64": "0.1.0-alpha.25",
38
+ "@mirinjs/linux-x64": "0.1.0-alpha.25"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
package/src/build.ts CHANGED
@@ -18,11 +18,11 @@ import { join } from "node:path";
18
18
  import { $ } from "bun";
19
19
  import { resolveArtifacts } from "./artifacts.ts";
20
20
  import { buildLinuxBundle } from "./bundle/linux/index.ts";
21
- import { buildAppBundle } from "./bundle/macos/index.ts";
21
+ import { buildAppBundle, stampMacSdk } from "./bundle/macos/index.ts";
22
22
  import { normalizeCefLocales } from "./bundle/shared/cef-locales.ts";
23
23
  import { buildWindowsBundle } from "./bundle/windows/index.ts";
24
24
  import { compileWorkers, normalizeSidecars } from "./extras.ts";
25
- import { resolveIconSource } from "./icons/sources.ts";
25
+ import { resolveIconFallback, resolveIconSource } from "./icons/sources.ts";
26
26
  import { makeWindowsIcon } from "./icons/windows/index.ts";
27
27
  import { buildLinuxPackages, resolveLinuxFormats } from "./package/linux/index.ts";
28
28
  import { sweepBuildTemps } from "./temps.ts";
@@ -221,6 +221,7 @@ export async function build(
221
221
  await $`bun build --compile --minify ${target} ${artifacts.hostEntry} --outfile ${hostExe}`.cwd(
222
222
  projectDir,
223
223
  );
224
+ await stampMacSdk(hostExe);
224
225
  if (IS_WINDOWS) {
225
226
  patchPeToGuiSubsystem(hostExe);
226
227
  await brandWindowsExe(hostExe, {
@@ -297,6 +298,7 @@ export async function build(
297
298
  cefLocales,
298
299
  version,
299
300
  icon: appIconPath,
301
+ iconFallback: resolveIconFallback(config.icon, projectDir),
300
302
  signIdentity,
301
303
  urlSchemes: config.urlSchemes,
302
304
  resources: { ...resources, sidecars },
@@ -52,6 +52,8 @@ export interface BundleOptions {
52
52
  * .icon document (the only form carrying appearance variants). Optional.
53
53
  */
54
54
  icon?: string;
55
+ /** `.icns`-capable source used when an `.icon` document can't be compiled. */
56
+ iconFallback?: string;
55
57
  /** Codesign identity; "-" (default) is ad-hoc. Set to a Developer ID to ship. */
56
58
  signIdentity?: string;
57
59
  /** Custom URL schemes -> Info.plist CFBundleURLTypes (deep links). */
@@ -144,7 +146,12 @@ interface IconKeys {
144
146
  * `Assets.car` (macOS 26+ light/dark/tinted) and contributes the `.icns` actool
145
147
  * derives from it; every other source takes the plain iconutil path.
146
148
  */
147
- async function writeIcon(iconSrc: string, resources: string, work: string): Promise<IconKeys> {
149
+ async function writeIcon(
150
+ iconSrc: string,
151
+ resources: string,
152
+ work: string,
153
+ fallback?: string,
154
+ ): Promise<IconKeys> {
148
155
  if (!existsSync(iconSrc)) {
149
156
  console.warn(`[mirin] icon not found, skipping: ${iconSrc}`);
150
157
  return {};
@@ -152,10 +159,16 @@ async function writeIcon(iconSrc: string, resources: string, work: string): Prom
152
159
 
153
160
  const icns = join(resources, "icon.icns");
154
161
  if (isIconComposerDoc(iconSrc)) {
155
- // An Icon Composer document can't go through iconutil, so the only .icns
156
- // available for it is the one actool derives.
162
+ // An Icon Composer document can't go through iconutil, so the .icns comes
163
+ // from actool — or, when no installed Xcode can compile the document, from
164
+ // the fallback source. A bundle must never ship iconless.
157
165
  const catalog = await writeAppearanceCatalog(iconSrc, resources, work);
158
- if (catalog?.icns) cpSync(catalog.icns, icns);
166
+ if (catalog?.icns) {
167
+ cpSync(catalog.icns, icns);
168
+ } else if (fallback && existsSync(fallback)) {
169
+ console.warn(`[mirin] falling back to ${fallback} for the macOS icon.`);
170
+ await writeIcns(fallback, icns, resources);
171
+ }
159
172
  return { iconFile: existsSync(icns) ? "icon" : undefined, iconName: catalog?.name };
160
173
  }
161
174
 
@@ -215,7 +228,7 @@ export async function buildAppBundle(opts: BundleOptions): Promise<{ app: string
215
228
  // only set the icon keys when an icon was actually produced.
216
229
  const iconWork = join(opts.outDir, ".mirin-icon");
217
230
  const { iconFile, iconName } = opts.icon
218
- ? await writeIcon(opts.icon, join(contents, "Resources"), iconWork)
231
+ ? await writeIcon(opts.icon, join(contents, "Resources"), iconWork, opts.iconFallback)
219
232
  : ({} as IconKeys);
220
233
  rmSync(iconWork, { recursive: true, force: true });
221
234
  const version = opts.version ?? "0.0.1";
@@ -1 +1,2 @@
1
1
  export * from "./app.ts";
2
+ export { stampMacSdk } from "./sdk-stamp.ts";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Stamp the compiled host with the macOS 26 SDK version.
3
+ *
4
+ * `bun build --compile` clones the Bun runtime binary, so the host inherits
5
+ * Bun's LC_BUILD_VERSION (SDK 15.x). AppKit gates the macOS 26 window design
6
+ * ("linked-on-or-after") on the MAIN executable's SDK, which leaves mirin apps
7
+ * with the pre-Tahoe compatibility chrome — flat traffic lights, old window
8
+ * metrics. Rewriting the load command opts the app into the current design.
9
+ * Must run before codesigning; vtool invalidates the signature.
10
+ */
11
+
12
+ import { existsSync, renameSync } from "node:fs";
13
+ import { $ } from "bun";
14
+
15
+ /** Matches the LSMinimumSystemVersion mirin writes into Info.plist. */
16
+ const MIN_OS = "13.0";
17
+ /** The design-gate threshold; the number is declarative, no SDK needs to exist locally. */
18
+ const SDK = "26.0";
19
+
20
+ /** Best-effort: a missing vtool (bare machines) keeps Bun's original header. */
21
+ export async function stampMacSdk(hostExe: string): Promise<void> {
22
+ if (process.platform !== "darwin") return;
23
+ if (!Bun.which("vtool")) {
24
+ console.warn("[mirin] vtool not found — the app keeps the compatibility window design.");
25
+ return;
26
+ }
27
+ const out = `${hostExe}.sdkstamp`;
28
+ const res =
29
+ await $`vtool -set-build-version macos ${MIN_OS} ${SDK} -replace -output ${out} ${hostExe}`
30
+ .nothrow()
31
+ .quiet();
32
+ if (res.exitCode !== 0 || !existsSync(out)) {
33
+ console.warn(`[mirin] vtool failed (exit ${res.exitCode}) — keeping Bun's SDK header.`);
34
+ return;
35
+ }
36
+ renameSync(out, hostExe);
37
+ await $`chmod +x ${hostExe}`.quiet();
38
+ }
package/src/dev.ts CHANGED
@@ -23,14 +23,14 @@ import {
23
23
  resolveLinuxDesktopIconPng,
24
24
  resolveLinuxIconPng,
25
25
  } from "./bundle/linux/index.ts";
26
- import { buildAppBundle } from "./bundle/macos/index.ts";
26
+ import { buildAppBundle, stampMacSdk } from "./bundle/macos/index.ts";
27
27
  import { buildWindowsBundle } from "./bundle/windows/index.ts";
28
28
 
29
29
  const IS_WINDOWS = process.platform === "win32";
30
30
  const IS_LINUX = process.platform === "linux";
31
31
 
32
32
  import { compileWorkers, normalizeSidecars } from "./extras.ts";
33
- import { resolveIconSource } from "./icons/sources.ts";
33
+ import { resolveIconFallback, resolveIconSource } from "./icons/sources.ts";
34
34
  import { createReporter } from "./shared/report.ts";
35
35
  import { DevSession } from "./shared/session.ts";
36
36
  import { sweepBuildTemps } from "./temps.ts";
@@ -104,6 +104,7 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
104
104
  projectDir,
105
105
  ),
106
106
  );
107
+ await stampMacSdk(hostExe);
107
108
  await reporter.build(
108
109
  $`bun build ${mainEntry} --target=bun --outfile ${workerJs}`.cwd(projectDir),
109
110
  );
@@ -155,6 +156,7 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
155
156
  helperBin: artifacts.helperBin,
156
157
  cefPath: artifacts.cefPath,
157
158
  icon: iconSrc,
159
+ iconFallback: resolveIconFallback(config.icon, projectDir),
158
160
  urlSchemes: config.urlSchemes,
159
161
  });
160
162
  bundlePhase?.ok();
@@ -21,21 +21,27 @@ export const isIconComposerDoc = (path: string): boolean => path.endsWith(".icon
21
21
  const ICON_NAME = "AppIcon";
22
22
 
23
23
  /**
24
- * Locate `actool`. `xcrun` honours the selected developer dir, which is often
25
- * Command Line Tools (no actool), so fall back to a full Xcode if one is
26
- * installed but not selected.
24
+ * Every actool on this machine, newest Xcode first. `xcrun` alone is not
25
+ * enough: it honours the selected developer dir, which is often Command Line
26
+ * Tools (no actool) or an Xcode too old for Icon Composer documents.
27
27
  */
28
- async function findActool(): Promise<string | undefined> {
28
+ async function findActools(): Promise<string[]> {
29
+ const found: string[] = [];
30
+ const push = (path: string) => {
31
+ if (existsSync(path) && !found.includes(path)) found.push(path);
32
+ };
33
+ // CI images install versioned Xcodes (Xcode_26.1.app, …); newest first, since
34
+ // only recent actools can compile an Icon Composer document.
35
+ const apps = (await $`ls -d /Applications/Xcode*.app`.nothrow().quiet()).stdout
36
+ .toString()
37
+ .split("\n")
38
+ .filter(Boolean)
39
+ .sort()
40
+ .reverse();
41
+ for (const app of apps) push(join(app.trim(), "Contents/Developer/usr/bin/actool"));
29
42
  const xcrun = await $`xcrun --find actool`.nothrow().quiet();
30
- if (xcrun.exitCode === 0) {
31
- const path = xcrun.stdout.toString().trim();
32
- if (path && existsSync(path)) return path;
33
- }
34
- for (const app of ["/Applications/Xcode.app", "/Applications/Xcode-beta.app"]) {
35
- const path = join(app, "Contents/Developer/usr/bin/actool");
36
- if (existsSync(path)) return path;
37
- }
38
- return undefined;
43
+ if (xcrun.exitCode === 0) push(xcrun.stdout.toString().trim());
44
+ return found;
39
45
  }
40
46
 
41
47
  export interface AppearanceCatalog {
@@ -59,11 +65,11 @@ export async function writeAppearanceCatalog(
59
65
  // what the caller passed and of the subprocess working directory.
60
66
  const resources = resolve(resourcesDir);
61
67
  const work = resolve(workDir);
62
- const actool = await findActool();
63
- if (!actool) {
68
+ const actools = await findActools();
69
+ if (actools.length === 0) {
64
70
  console.warn(
65
71
  "[mirin] actool not found — appearance variants need a full Xcode " +
66
- "(Command Line Tools alone is not enough); shipping the default icon only.",
72
+ "(Command Line Tools alone is not enough).",
67
73
  );
68
74
  return undefined;
69
75
  }
@@ -74,23 +80,30 @@ export async function writeAppearanceCatalog(
74
80
  const doc = join(work, `${ICON_NAME}.icon`);
75
81
  cpSync(iconDoc, doc, { recursive: true });
76
82
 
77
- const compiled = join(work, "compiled");
78
- mkdirSync(compiled, { recursive: true });
79
- const res =
80
- await $`${actool} --output-format human-readable-text --notices --warnings --app-icon ${ICON_NAME} --output-partial-info-plist ${join(work, "partial.plist")} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target 26.0 --platform macosx --compile ${compiled} ${doc}`
81
- .nothrow()
82
- .quiet();
83
-
84
- const car = join(compiled, "Assets.car");
85
- if (res.exitCode !== 0 || !existsSync(car)) {
86
- console.warn(
87
- `[mirin] actool failed (exit ${res.exitCode}) — shipping the default icon only.\n` +
88
- res.stdout.toString().trim(),
89
- );
90
- return undefined;
83
+ // Old actools exit 0 for an .icon document while emitting no catalog (they
84
+ // warn that the platform can't target macOS 26), so success is judged by the
85
+ // artifact, not the exit code — and every installed Xcode gets a turn.
86
+ const car = join(work, "compiled", "Assets.car");
87
+ let lastOutput = "";
88
+ for (const actool of actools) {
89
+ const compiled = join(work, "compiled");
90
+ rmSync(compiled, { recursive: true, force: true });
91
+ mkdirSync(compiled, { recursive: true });
92
+ const res =
93
+ await $`${actool} --output-format human-readable-text --notices --warnings --app-icon ${ICON_NAME} --output-partial-info-plist ${join(work, "partial.plist")} --enable-on-demand-resources NO --target-device mac --minimum-deployment-target 26.0 --platform macosx --compile ${compiled} ${doc}`
94
+ .nothrow()
95
+ .quiet();
96
+ lastOutput = res.stdout.toString().trim();
97
+ if (existsSync(car)) {
98
+ cpSync(car, join(resources, "Assets.car"));
99
+ const icns = join(compiled, `${ICON_NAME}.icns`);
100
+ return { name: ICON_NAME, icns: existsSync(icns) ? icns : undefined };
101
+ }
91
102
  }
92
103
 
93
- cpSync(car, join(resources, "Assets.car"));
94
- const icns = join(compiled, `${ICON_NAME}.icns`);
95
- return { name: ICON_NAME, icns: existsSync(icns) ? icns : undefined };
104
+ console.warn(
105
+ `[mirin] no installed Xcode could compile the .icon document (tried ${actools.length}).\n` +
106
+ lastOutput,
107
+ );
108
+ return undefined;
96
109
  }
@@ -23,3 +23,16 @@ export function resolveIconSource(
23
23
  const picked = typeof icon === "string" ? icon : (icon[platform] ?? icon.default);
24
24
  return picked ? join(projectDir, picked) : undefined;
25
25
  }
26
+
27
+ /**
28
+ * The `.icns`-capable fallback behind a macOS `.icon` document: the `default`
29
+ * entry when per-platform sources are configured. A failed catalog compile
30
+ * must degrade to a plain icon, never to an iconless bundle.
31
+ */
32
+ export function resolveIconFallback(
33
+ icon: string | IconSources | undefined,
34
+ projectDir: string,
35
+ ): string | undefined {
36
+ if (!icon || typeof icon === "string") return undefined;
37
+ return icon.default ? join(projectDir, icon.default) : undefined;
38
+ }