craft-native 0.0.278 → 0.0.290

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/README.md CHANGED
@@ -668,3 +668,19 @@ Browser audio assigns one owner per sound ID. A newer play or stop invalidates
668
668
  pending media resolution for that ID; stopping all sounds, changing accounts,
669
669
  aborting a call or disposing the host also releases active audio. Superseded
670
670
  play calls reject with `AbortError`.
671
+
672
+ ### Packaging sandboxed macOS apps
673
+
674
+ Pass `macos.appBundlePath` to package a complete STX app without discarding its
675
+ resources. Distribution options also apply to prebuilt bundles: `buildNumber`,
676
+ `provisioningProfile`, `privacyManifest`, and the icon are embedded before signing.
677
+ Craft signs nested Mach-O code before the main bundle and verifies the result.
678
+ Use `helperEntitlements` for a compiled backend's runtime permissions; App Store
679
+ helpers inherit the main sandbox by default. `signingKeychain` selects the
680
+ keychain for both application and installer identities.
681
+
682
+ Craft's bundled-backend host reads `backend: { executable: 'stx-backend' }` from
683
+ `Contents/Resources/craft.json`. The native app remains the main process, starts
684
+ the helper with `--serve --managed`, and consumes its authenticated loopback URL.
685
+ The helper's standard input is a lifetime lease: it must stop when that pipe
686
+ closes. `craft --capabilities` advertises `bundleBackend` on macOS.
@@ -10,6 +10,10 @@ export interface AndroidEmulatorOptions {
10
10
  acceptLicenses?: boolean;
11
11
  /** Defaults to 10 minutes; hardware is capped there, software cold provisioning at 20 minutes. */
12
12
  bootTimeoutMs?: number;
13
+ /** Requested guest RAM, 1536–8192 MB (Android emulator limits); defaults to 2048. */
14
+ memoryMb?: number;
15
+ /** Requested guest CPU count, 1–8; defaults to 2. */
16
+ virtualCpus?: number;
13
17
  env?: Record<string, string | undefined>;
14
18
  }
15
19
  export interface AndroidEmulatorSession {
@@ -198,6 +198,12 @@ async function createAndroidEmulator(options) {
198
198
  const maximumBootTimeoutMs = options.acceleration === "software" ? 1200000 : 600000;
199
199
  if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
200
200
  throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`);
201
+ const memoryMb = options.memoryMb ?? 2048;
202
+ if (!Number.isSafeInteger(memoryMb) || memoryMb < 1536 || memoryMb > 8192)
203
+ throw new Error("Android memoryMb must be an integer between 1536 and 8192 megabytes");
204
+ const virtualCpus = options.virtualCpus ?? 2;
205
+ if (!Number.isSafeInteger(virtualCpus) || virtualCpus < 1 || virtualCpus > 8)
206
+ throw new Error("Android virtualCpus must be an integer between 1 and 8");
201
207
  const port = options.port ?? 5558;
202
208
  if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0)
203
209
  throw new Error("Android console ports must be even and between 5554 and 5682");
@@ -299,7 +305,7 @@ async function createAndroidEmulator(options) {
299
305
  const hostCpus = cpus();
300
306
  cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0);
301
307
  idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0);
302
- child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", "2048", "-cores", "2", "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
308
+ child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", String(memoryMb), "-cores", String(virtualCpus), "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
303
309
  identity = {
304
310
  image: options.image,
305
311
  acceleration: options.acceleration,
@@ -307,8 +313,8 @@ async function createAndroidEmulator(options) {
307
313
  bootTimeoutMs,
308
314
  bootStartedAt: new Date(bootStartedAt).toISOString(),
309
315
  host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
310
- virtualCpus: 2,
311
- memoryMb: 2048,
316
+ virtualCpus,
317
+ memoryMb,
312
318
  serial,
313
319
  avdDirectory: env.ANDROID_AVD_HOME,
314
320
  emulatorPid: child.pid
@@ -335,11 +341,13 @@ async function createAndroidEmulator(options) {
335
341
  const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
336
342
  const playStore = requiresPlayStore ? await run([adb, "-s", serial, "shell", "pm", "path", "com.android.vending"], undefined, probeTimeout(), true) : undefined;
337
343
  const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
338
- const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
344
+ const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-v", "threadtime"], undefined, probeTimeout(), true);
339
345
  const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
340
346
  const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
341
347
  const temporaryHome = homeActivity.startsWith("com.android.sdksetup/") || homeActivity.startsWith("com.google.android.googlesdksetup/") || /^com\.android\.settings\/(?:\.|com\.android\.settings\.)FallbackHome$/.test(homeActivity) || /^android\/.*(?:Resolver|Chooser)Activity$/.test(homeActivity);
342
- booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && (!requiresPlayStore || playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome;
348
+ const windows = await run([adb, "-s", serial, "shell", "dumpsys", "window"], undefined, probeTimeout(), true);
349
+ const focusedPackage = windows.stdout.match(/^\s*mCurrentFocus\s*=\s*Window\{[^}\r\n]*\bu\d+\s+([\w.]+)\//m)?.[1];
350
+ booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && (!requiresPlayStore || playStore?.code === 0 && /^package:\/\S+/m.test(playStore.stdout)) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !temporaryHome && windows.code === 0 && focusedPackage === homeActivity.split("/")[0];
343
351
  }
344
352
  }
345
353
  const checkedAt = Date.now();
@@ -845,6 +853,7 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
845
853
  };
846
854
  write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
847
855
  write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
856
+ write("CraftSnapshotFile.kt.template", "CraftSnapshotFile.kt");
848
857
  write("CraftNativeImageCache.kt.template", "CraftNativeImageCache.kt");
849
858
  write("CraftNativeHaptics.kt.template", "CraftNativeHaptics.kt");
850
859
  write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
@@ -270,7 +270,12 @@ internal object CraftNativeStorage {
270
270
 
271
271
  }
272
272
 
273
- internal class CraftNativeStores private constructor(context: Context, private val secureEnabled: Boolean) {
273
+ internal class CraftNativeStores private constructor(
274
+ context: Context,
275
+ private val secureEnabled: Boolean,
276
+ private val snapshotFiles: CraftSnapshotFile,
277
+ ) {
278
+ private constructor(context: Context, secureEnabled: Boolean) : this(context, secureEnabled, CraftSnapshotFile())
274
279
  private val appContext = context.applicationContext
275
280
  private val storage: SharedPreferences = appContext.getSharedPreferences("craft-native-storage", Context.MODE_PRIVATE)
276
281
  private val snapshotsDir = File(appContext.filesDir, "craft-snapshots").apply { mkdirs() }
@@ -355,20 +360,22 @@ internal class CraftNativeStores private constructor(context: Context, private v
355
360
  fun setSnapshot(name: String, json: String?, from: String?, done: (Boolean) -> Unit) {
356
361
  writer.execute {
357
362
  val file = snapshotFile(name)
363
+ val previous = snapshotCache[name]
358
364
  val ok = runCatching {
359
365
  if (json == null) {
360
366
  !file.exists() || file.delete()
361
367
  } else {
362
- val temporary = File(file.parentFile, "${file.name}.tmp")
363
- temporary.writeText(json)
364
- temporary.renameTo(file) || run { file.writeText(json); temporary.delete(); true }
368
+ snapshotFiles.set(file, json)
369
+ true
365
370
  }
366
371
  }.getOrDefault(false)
367
372
  if (ok) {
368
373
  if (json == null) snapshotCache.remove(name) else snapshotCache[name] = json
369
374
  }
370
375
  main.post {
371
- if (ok) changed("snapshots", name, json, null)
376
+ // Isolates optimistically display their write. Restore the
377
+ // confirmed value before rejecting an unsuccessful checkpoint.
378
+ changed("snapshots", name, if (ok) json else previous, null)
372
379
  done(ok)
373
380
  }
374
381
  }
@@ -0,0 +1,26 @@
1
+ package {{PACKAGE_NAME}}
2
+
3
+ import java.io.File
4
+ import java.io.FileOutputStream
5
+ import java.nio.file.Files
6
+ import java.nio.file.StandardCopyOption
7
+ import java.util.UUID
8
+
9
+ /** One checkpoint replacement. Filesystem failures must leave the previous checkpoint intact. */
10
+ internal class CraftSnapshotFile(
11
+ private val write: (File, ByteArray) -> Unit = { file, bytes ->
12
+ FileOutputStream(file).use { stream -> stream.write(bytes); stream.fd.sync() }
13
+ },
14
+ private val replace: (File, File) -> Unit = { source, destination ->
15
+ Files.move(source.toPath(), destination.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING)
16
+ },
17
+ ) {
18
+ fun set(destination: File, json: String) {
19
+ val temporary = File(destination.parentFile, ".craft-snapshot-${UUID.randomUUID()}.tmp")
20
+ val bytes = json.toByteArray(Charsets.UTF_8)
21
+ try {
22
+ write(temporary, bytes)
23
+ replace(temporary, destination)
24
+ } finally { temporary.delete() }
25
+ }
26
+ }
@@ -4661,6 +4661,7 @@ class MainActivity : AppCompatActivity() {
4661
4661
  capabilities: ${capabilityNames()},
4662
4662
  initialAppState: ${JSONObject.quote(nativeAppState)},
4663
4663
  colorScheme: ${JSONObject.quote(colorScheme())},
4664
+ viewportWidth: ${resources.configuration.screenWidthDp},
4664
4665
  appearance: ${appearance()},
4665
4666
  appURL: ${JSONObject.quote(config.optString("devServerURL"))},
4666
4667
  appVersion: ${JSONObject.quote(appVersion().first)},
@@ -4909,6 +4910,21 @@ class MainActivity : AppCompatActivity() {
4909
4910
  override fun onConfigurationChanged(newConfig: Configuration) {
4910
4911
  super.onConfigurationChanged(newConfig)
4911
4912
  publishAppearance()
4913
+ publishViewport(newConfig)
4914
+ }
4915
+
4916
+ /// The width the screens last heard, in dp (a web page's CSS pixels).
4917
+ private var reportedViewportWidth = -1
4918
+
4919
+ /// Rotation, split screen or a resized window: the bundle's width classes
4920
+ /// (`md:`, `lg:`) follow the window's new width.
4921
+ private fun publishViewport(config: Configuration) {
4922
+ val width = config.screenWidthDp
4923
+ if (width <= 0 || width == reportedViewportWidth) return
4924
+ reportedViewportWidth = width
4925
+ if (!::root.isInitialized) return
4926
+ val payload = JSONObject().put("width", width).put("height", config.screenHeightDp)
4927
+ allScreens().forEach { screen -> if (!screen.headless) send(screen, "VIEWPORT", payload) }
4912
4928
  }
4913
4929
 
4914
4930
  private fun publishAppearance() {
@@ -17,8 +17,10 @@ export interface BrowserStorageOptions {
17
17
  * or in another tab: called once the new account's storage is in place.
18
18
  */
19
19
  sessionChanged?: (token: string | null, previous: string | null) => void;
20
- /** A host lock primitive; browsers use their native Web Locks implementation. */
21
- lock?: (<T>(name: string, operation: () => T | Promise<T>) => Promise<T>) | null;
20
+ /** A host lock primitive; honor signal cancellation while queued, as native Web Locks do. */
21
+ lock?: (<T>(name: string, operation: () => T | Promise<T>, options?: {
22
+ signal?: AbortSignal;
23
+ }) => Promise<T>) | null;
22
24
  }
23
25
  export interface BrowserStorage {
24
26
  atomicStorage: boolean;
@@ -37,7 +39,7 @@ export interface BrowserStorage {
37
39
  keys: () => string[];
38
40
  };
39
41
  };
40
- call: (module: string, method: string, args: unknown[]) => Promise<unknown>;
42
+ call: (module: string, method: string, args: unknown[], signal?: AbortSignal) => Promise<unknown>;
41
43
  accountChanged: () => Promise<void>;
42
44
  scope: () => string;
43
45
  }
package/dist/browser.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface BrowserPlatformOptions {
18
18
  export interface BrowserPlatform {
19
19
  craft: Record<string, unknown>;
20
20
  capabilities: string[];
21
+ /** Queued storage calls abort without writing; cancellation cannot undo an already committed synchronous write. */
21
22
  call: (module: string, method: string, args: unknown[], signal: AbortSignal) => Promise<unknown>;
22
23
  navigate: (path: string, replace?: boolean) => void;
23
24
  emit: (listener: (type: string, payload: unknown) => void) => () => void;
package/dist/browser.js CHANGED
@@ -63,12 +63,35 @@ function secureUUID() {
63
63
  }
64
64
 
65
65
  // src/browser/storage.ts
66
+ async function waitForAccount(operation, signal) {
67
+ if (!signal)
68
+ return operation;
69
+ let abort;
70
+ try {
71
+ return await Promise.race([operation, new Promise((resolve, reject) => {
72
+ abort = () => reject(signal.reason);
73
+ if (signal.aborted)
74
+ abort();
75
+ else
76
+ signal.addEventListener("abort", abort, { once: true });
77
+ })]);
78
+ } finally {
79
+ signal.removeEventListener("abort", abort);
80
+ }
81
+ }
66
82
  async function createBrowserStorage(options) {
67
83
  const { storage, namespace, tokenKey, changed } = options;
68
84
  const sessionTokenKey = options.sessionTokenKey || "auth.token";
69
85
  const prefix = `${namespace}:`;
70
- const lock = options.lock === null ? undefined : options.lock ?? (typeof navigator !== "undefined" && typeof navigator.locks?.request === "function" ? (name, operation) => navigator.locks.request(name, operation) : undefined);
71
- const exclusive = (operation) => lock ? lock(`${namespace}:storage`, operation) : Promise.resolve().then(operation);
86
+ const lock = options.lock === null ? undefined : options.lock ?? (typeof navigator !== "undefined" && typeof navigator.locks?.request === "function" ? (name, operation, settings) => navigator.locks.request(name, settings ?? {}, operation) : undefined);
87
+ const exclusive = (operation, signal) => {
88
+ const guarded = () => {
89
+ signal?.throwIfAborted();
90
+ return operation();
91
+ };
92
+ signal?.throwIfAborted();
93
+ return lock ? lock(`${namespace}:storage`, guarded, { signal }) : Promise.resolve().then(guarded);
94
+ };
72
95
  const scopeChanged = () => Object.assign(new Error("The storage account changed"), { code: "STORAGE_SCOPE_CHANGED" });
73
96
  const pending = new Map;
74
97
  let token = storage.getItem(tokenKey);
@@ -215,10 +238,11 @@ async function createBrowserStorage(options) {
215
238
  keys: () => available() ? keys(key("snapshot:")).map((name) => name.slice(key("snapshot:").length)) : []
216
239
  }
217
240
  },
218
- async call(module, method, args) {
241
+ async call(module, method, args, signal) {
242
+ signal?.throwIfAborted();
219
243
  const captured = capture();
220
244
  const wasAvailable = available();
221
- await accountChanged();
245
+ await waitForAccount(accountChanged(), signal);
222
246
  const name = String(args[0] ?? "");
223
247
  if (module === "Storage") {
224
248
  if (!wasAvailable)
@@ -259,7 +283,7 @@ async function createBrowserStorage(options) {
259
283
  return true;
260
284
  }
261
285
  throw Object.assign(new Error(`Unsupported browser storage operation ${method}`), { code: "UNSUPPORTED" });
262
- });
286
+ }, signal);
263
287
  }
264
288
  if (module === "SecureStorage") {
265
289
  if (!wasAvailable)
@@ -284,7 +308,7 @@ async function createBrowserStorage(options) {
284
308
  return true;
285
309
  }
286
310
  throw Object.assign(new Error(`Unsupported browser storage operation ${module}.${method}`), { code: "UNSUPPORTED" });
287
- });
311
+ }, signal);
288
312
  if (method === "clear" || name === sessionTokenKey && (method === "set" || method === "remove"))
289
313
  await accountChanged();
290
314
  return result;
@@ -608,7 +632,7 @@ async function createBrowserPlatform(options = {}) {
608
632
  const first = object(args[0]);
609
633
  if (module === "Storage" || module === "SecureStorage") {
610
634
  const before = persistence.craft.secureStorage.getSync(sessionTokenKey);
611
- const answer = await persistence.call(module, method, args);
635
+ const answer = await persistence.call(module, method, args, signal);
612
636
  if (before !== persistence.craft.secureStorage.getSync(sessionTokenKey))
613
637
  await resetAccount();
614
638
  return answer;
package/dist/cli.js CHANGED
@@ -130,12 +130,15 @@ import { spawn } from "child_process";
130
130
  import { createHash } from "crypto";
131
131
  import {
132
132
  chmodSync,
133
+ closeSync,
133
134
  copyFileSync,
134
135
  cpSync,
135
136
  existsSync as existsSync4,
136
137
  lstatSync,
137
138
  mkdirSync,
139
+ openSync,
138
140
  readFileSync as readFileSync2,
141
+ readSync,
139
142
  readdirSync as readdirSync2,
140
143
  rmSync as rmSync2,
141
144
  writeFileSync as writeFileSync3
@@ -243,6 +246,8 @@ async function packageMacOS(config, outDir) {
243
246
  const { name, version, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
244
247
  const opts = config.macos || {};
245
248
  const hardenedRuntime = !opts.appStore;
249
+ if (opts.appStore && (!opts.entitlements || !opts.provisioningProfile))
250
+ return [{ success: false, platform: "macos", format: "app", error: "App Store packaging requires macos.entitlements and macos.provisioningProfile" }];
246
251
  const appBundlePath = join2(outDir, `${name}.app`);
247
252
  const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
248
253
  name,
@@ -268,25 +273,42 @@ async function packageMacOS(config, outDir) {
268
273
  });
269
274
  return results;
270
275
  }
271
- if (opts.signIdentity) {
272
- const signed = await runTool("codesign", codesignArguments({
273
- path: appBundlePath,
274
- identity: opts.signIdentity,
275
- entitlements: opts.entitlements,
276
- hardenedRuntime
277
- }));
278
- if (!signed.success) {
279
- results.push({ success: false, platform: "macos", format: "app", error: signed.error });
280
- return results;
276
+ try {
277
+ const contents = join2(appBundlePath, "Contents");
278
+ if (opts.provisioningProfile) {
279
+ const profile = join2(contents, "embedded.provisionprofile");
280
+ copyFileSync(opts.provisioningProfile, profile);
281
+ chmodSync(profile, 420);
282
+ }
283
+ if (opts.privacyManifest)
284
+ copyFileSync(opts.privacyManifest, join2(contents, "Resources", "PrivacyInfo.xcprivacy"));
285
+ if (opts.appBundlePath) {
286
+ const metadata = {
287
+ CFBundleVersion: opts.buildNumber,
288
+ LSApplicationCategoryType: opts.category,
289
+ LSMinimumSystemVersion: opts.minimumSystemVersion,
290
+ LSUIElement: opts.menuBarOnly
291
+ };
292
+ if (config.iconPath) {
293
+ const icon = basename(config.iconPath);
294
+ copyFileSync(config.iconPath, join2(contents, "Resources", icon));
295
+ metadata.CFBundleIconFile = icon;
296
+ }
297
+ for (const [key, value] of Object.entries(metadata)) {
298
+ if (value === undefined)
299
+ continue;
300
+ const result = await runTool("plutil", ["-replace", key, typeof value === "boolean" ? "-bool" : "-string", String(value), join2(contents, "Info.plist")]);
301
+ if (!result.success)
302
+ throw new Error(result.error);
303
+ }
281
304
  }
282
- } else if (opts.appStore) {
283
- results.push({
284
- success: false,
285
- platform: "macos",
286
- format: "pkg",
287
- error: 'App Store packaging requires macos.signIdentity (a "3rd Party Mac Developer Application" identity)'
288
- });
289
- return results;
305
+ if (opts.signIdentity) {
306
+ await signMacOSAppBundle(appBundlePath, bundleId, opts, hardenedRuntime);
307
+ } else if (opts.appStore) {
308
+ throw new Error('App Store packaging requires macos.signIdentity (a "3rd Party Mac Developer Application" identity)');
309
+ }
310
+ } catch (error) {
311
+ return [{ success: false, platform: "macos", format: "app", error: error.message }];
290
312
  }
291
313
  results.push({ success: true, platform: "macos", format: "app", outputPath: appBundlePath });
292
314
  if (opts.dmg !== false) {
@@ -310,7 +332,8 @@ async function packageMacOS(config, outDir) {
310
332
  identifier: bundleId,
311
333
  version,
312
334
  appStore: opts.appStore === true,
313
- installerIdentity: opts.installerIdentity
335
+ installerIdentity: opts.installerIdentity,
336
+ signingKeychain: opts.signingKeychain
314
337
  });
315
338
  results.push({
316
339
  success: pkgResult.success,
@@ -550,6 +573,59 @@ ${body}
550
573
  </dict>
551
574
  </plist>`;
552
575
  }
576
+ async function signMacOSAppBundle(app, bundleId, opts, hardenedRuntime) {
577
+ const scratch = createTempDir("craft-sign-");
578
+ try {
579
+ let helperEntitlements = opts.helperEntitlements;
580
+ if (opts.appStore && !helperEntitlements) {
581
+ helperEntitlements = join2(scratch, "inherit.plist");
582
+ writeFileSync3(helperEntitlements, '<?xml version="1.0"?><plist version="1.0"><dict><key>com.apple.security.app-sandbox</key><true/><key>com.apple.security.inherit</key><true/></dict></plist>');
583
+ }
584
+ const extracted = await runTool("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", join2(app, "Contents", "Info.plist")]);
585
+ if (!extracted.success)
586
+ throw new Error(extracted.error);
587
+ const main = join2(app, "Contents", "MacOS", extracted.stdout.trim());
588
+ const sign = async (path, entitlements, identifier) => {
589
+ const signed = await runTool("codesign", codesignArguments({ path, identity: opts.signIdentity, entitlements, hardenedRuntime, identifier, keychain: opts.signingKeychain, deep: false }));
590
+ if (!signed.success)
591
+ throw new Error(signed.error);
592
+ };
593
+ const walk = async (directory) => {
594
+ for (const entry of readdirSync2(directory)) {
595
+ const path = join2(directory, entry);
596
+ const stat = lstatSync(path);
597
+ if (stat.isSymbolicLink())
598
+ continue;
599
+ if (stat.isDirectory()) {
600
+ await walk(path);
601
+ if (/\.(?:app|framework|xpc|appex|bundle)$/.test(entry))
602
+ await sign(path, /\.(?:app|xpc|appex)$/.test(entry) ? helperEntitlements : undefined);
603
+ } else if (path !== main && stat.isFile() && isMachO(path)) {
604
+ const executable = (stat.mode & 73) !== 0 && !/\.(?:dylib|so)$/.test(entry);
605
+ await sign(path, executable ? helperEntitlements : undefined, `${bundleId}.${entry}`);
606
+ }
607
+ }
608
+ };
609
+ await walk(join2(app, "Contents"));
610
+ await sign(app, opts.entitlements);
611
+ const verified = await runTool("codesign", ["--verify", "--deep", "--strict", app]);
612
+ if (!verified.success)
613
+ throw new Error(verified.error);
614
+ } finally {
615
+ removeTempDir(scratch);
616
+ }
617
+ }
618
+ function isMachO(path) {
619
+ const fd = openSync(path, "r");
620
+ try {
621
+ const magic = Buffer.alloc(4);
622
+ if (readSync(fd, magic, 0, 4, 0) !== 4)
623
+ return false;
624
+ return [4277009102, 3472551422, 4277009103, 3489328638, 3405691582, 3199925962, 3405691583, 3216703178].includes(magic.readUInt32BE());
625
+ } finally {
626
+ closeSync(fd);
627
+ }
628
+ }
553
629
  function codesignArguments(opts) {
554
630
  return [
555
631
  "--force",
@@ -558,7 +634,9 @@ function codesignArguments(opts) {
558
634
  ...opts.identity === "-" ? [] : ["--timestamp"],
559
635
  ...opts.hardenedRuntime ? ["--options", "runtime"] : [],
560
636
  ...opts.entitlements ? ["--entitlements", opts.entitlements] : [],
561
- "--deep",
637
+ ...opts.identifier ? ["--identifier", opts.identifier] : [],
638
+ ...opts.keychain ? ["--keychain", opts.keychain] : [],
639
+ ...opts.deep === false ? [] : ["--deep"],
562
640
  opts.path
563
641
  ];
564
642
  }
@@ -569,6 +647,7 @@ function productbuildArguments(opts) {
569
647
  "/Applications",
570
648
  "--sign",
571
649
  opts.identity,
650
+ ...opts.keychain ? ["--keychain", opts.keychain] : [],
572
651
  opts.outputPath
573
652
  ];
574
653
  }
@@ -686,7 +765,7 @@ function runTool(tool, args) {
686
765
  });
687
766
  proc.on("close", (code) => {
688
767
  if (code === 0)
689
- resolve({ success: true });
768
+ resolve({ success: true, stdout });
690
769
  else
691
770
  resolve({ success: false, error: formatPackagingCommandError(tool, code, stdout, stderr) });
692
771
  });
@@ -760,7 +839,8 @@ async function createPKG(opts) {
760
839
  const built = await runTool("productbuild", productbuildArguments({
761
840
  appBundlePath: opts.appBundlePath,
762
841
  outputPath: opts.outputPath,
763
- identity: opts.installerIdentity
842
+ identity: opts.installerIdentity,
843
+ keychain: opts.signingKeychain
764
844
  }));
765
845
  return built.success ? { success: true, outputPath: opts.outputPath } : { success: false, error: built.error };
766
846
  }
@@ -2865,7 +2945,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2865
2945
  `);
2866
2946
  }
2867
2947
  // package.json
2868
- var version = "0.0.278";
2948
+ var version = "0.0.290";
2869
2949
 
2870
2950
  // bin/cli.ts
2871
2951
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];