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

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.24",
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.24",
31
+ "mirinjs": "0.1.0-alpha.24"
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.24",
35
+ "@mirinjs/win32-x64": "0.1.0-alpha.24",
36
+ "@mirinjs/win32-arm64": "0.1.0-alpha.24",
37
+ "@mirinjs/linux-arm64": "0.1.0-alpha.24",
38
+ "@mirinjs/linux-x64": "0.1.0-alpha.24"
39
39
  },
40
40
  "publishConfig": {
41
41
  "access": "public"
package/src/build.ts CHANGED
@@ -22,7 +22,7 @@ import { buildAppBundle } 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";
@@ -297,6 +297,7 @@ export async function build(
297
297
  cefLocales,
298
298
  version,
299
299
  icon: appIconPath,
300
+ iconFallback: resolveIconFallback(config.icon, projectDir),
300
301
  signIdentity,
301
302
  urlSchemes: config.urlSchemes,
302
303
  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";
package/src/dev.ts CHANGED
@@ -30,7 +30,7 @@ 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";
@@ -155,6 +155,7 @@ export async function dev(projectDir = process.cwd(), options?: DevOptions): Pro
155
155
  helperBin: artifacts.helperBin,
156
156
  cefPath: artifacts.cefPath,
157
157
  icon: iconSrc,
158
+ iconFallback: resolveIconFallback(config.icon, projectDir),
158
159
  urlSchemes: config.urlSchemes,
159
160
  });
160
161
  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
+ }