@craft-native/ios 0.0.368 → 0.0.374

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/dist/cli.js CHANGED
@@ -3,12 +3,118 @@
3
3
 
4
4
  // src/cli.ts
5
5
  import { parseArgs } from "util";
6
- import { resolve as resolve2 } from "path";
6
+ import { resolve as resolve3 } from "path";
7
7
 
8
8
  // src/index.ts
9
9
  import { gzipSync } from "zlib";
10
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync as readdirSync2, rmSync, statSync, writeFileSync } from "fs";
11
- import { dirname as dirname2, extname, join as join3, resolve, sep as sep2 } from "path";
10
+
11
+ // src/runtime-profile.ts
12
+ import { createHash } from "crypto";
13
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
14
+ import { tmpdir } from "os";
15
+ import { join, resolve } from "path";
16
+ var iosPermissionNames = ["speech", "microphone", "camera", "photos", "location", "contacts", "calendar", "bluetooth", "motion", "biometric", "music", "health", "nfc"];
17
+ function iosPermissions(config) {
18
+ const enabled = {
19
+ speech: config.enableSpeechRecognition,
20
+ microphone: config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording,
21
+ camera: config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR,
22
+ photos: config.enableCamera || config.enableVideoRecording,
23
+ location: config.enableGeolocation || config.enableBackgroundLocation,
24
+ contacts: config.enableContacts,
25
+ calendar: config.enableCalendar,
26
+ bluetooth: config.enableBluetooth || config.enableHeartRate,
27
+ motion: config.enableMotionSensors,
28
+ biometric: config.enableBiometric,
29
+ music: config.enableAppleMusic,
30
+ health: config.enableHealthKit,
31
+ nfc: config.enableNFC
32
+ };
33
+ return iosPermissionNames.filter((name) => enabled[name] === true);
34
+ }
35
+ function iosPermissionMask(config) {
36
+ return iosPermissions(config).reduce((mask, name) => mask | 1 << iosPermissionNames.indexOf(name), 0);
37
+ }
38
+ function renderPermissionSource(source, config) {
39
+ const enabled = new Set(iosPermissions(config));
40
+ return source.replace(/#if CRAFT_(?:PERMISSION_([A-Z]+)|APPLE_(MUSIC))\b/g, (_match, permission, music) => {
41
+ const name = music ? "music" : permission.toLowerCase();
42
+ if (!iosPermissionNames.includes(name))
43
+ throw new Error(`Unknown Swift permission gate: ${name}`);
44
+ return `#if ${enabled.has(name) ? "true" : "false"}`;
45
+ });
46
+ }
47
+ var archives = ["libcraft-ios.a", "libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"];
48
+ var digest = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
49
+ function verifyRuntimeProfile(directory, config) {
50
+ if (!existsSync(join(directory, "ios-permissions.json")))
51
+ throw new Error("Unprofiled Zig runtime: use runtimeSource to build archives for this app");
52
+ const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
53
+ if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
54
+ throw new Error("Zig runtime permission profile does not match craft.config.json; rebuild it for this app");
55
+ if (!existsSync(join(directory, archives[0])) || !archives.slice(1).some((name) => existsSync(join(directory, name))))
56
+ throw new Error("Zig runtime must include device and simulator archives");
57
+ for (const name of archives) {
58
+ if (!existsSync(join(directory, name)) && !profile.sha256?.[name])
59
+ continue;
60
+ if (!existsSync(join(directory, name)) || profile.sha256?.[name] !== digest(join(directory, name)))
61
+ throw new Error(`Zig runtime archive identity mismatch: ${name}; rebuild it for this app`);
62
+ }
63
+ }
64
+ function writeInstalledProfile(directory, config, source) {
65
+ const paths = ["device/libcraft-ios.a", "simulator/libcraft-ios.a"];
66
+ writeFileSync(join(directory, "profile.json"), JSON.stringify({
67
+ version: 1,
68
+ mask: iosPermissionMask(config),
69
+ source,
70
+ sha256: Object.fromEntries(paths.map((path) => [path, digest(join(directory, path))]))
71
+ }, null, 2));
72
+ }
73
+ function verifyInstalledProfile(directory, config) {
74
+ if (!existsSync(join(directory, "profile.json")))
75
+ throw new Error("Unprofiled Zig runtime: regenerate this project with runtimeSource before omitting permission descriptions");
76
+ const profile = JSON.parse(readFileSync(join(directory, "profile.json"), "utf8"));
77
+ if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
78
+ throw new Error("Installed Zig runtime permission profile is stale; rebuild it for craft.config.json");
79
+ for (const path of ["device/libcraft-ios.a", "simulator/libcraft-ios.a"]) {
80
+ if (!existsSync(join(directory, path)) || profile.sha256?.[path] !== digest(join(directory, path)))
81
+ throw new Error(`Installed Zig runtime identity mismatch: ${path}; rebuild it for this app`);
82
+ }
83
+ }
84
+ async function withAppRuntime(source, config, use) {
85
+ const root = resolve(source);
86
+ const manifest = readFileSync(join(root, "build.zig.zon"), "utf8");
87
+ const version = /\.minimum_zig_version\s*=\s*"([^"]+)"/.exec(manifest)?.[1];
88
+ if (!version)
89
+ throw new Error("Craft Zig source must declare its pinned minimum_zig_version");
90
+ const zig = process.env.CRAFT_ZIG || "zig";
91
+ const actual = Bun.spawnSync([zig, "version"], { stdout: "pipe", stderr: "pipe" });
92
+ if (actual.exitCode !== 0 || actual.stdout.toString().trim() !== version)
93
+ throw new Error(`Per-app iOS runtime requires Zig ${version}; set CRAFT_ZIG to that executable`);
94
+ const output = mkdtempSync(join(tmpdir(), "craft-ios-runtime-"));
95
+ try {
96
+ const names = iosPermissions(config);
97
+ const command = [zig, "build", "build-ios-all", `-Dios-permissions=${names.join(",") || "none"}`, "-Djs-runtime=false", "-Doptimize=ReleaseFast", "--prefix", output];
98
+ const child = Bun.spawn(command, { cwd: root, stdout: "inherit", stderr: "inherit" });
99
+ if (await child.exited !== 0)
100
+ throw new Error("Per-app iOS Zig runtime build failed; installed runtime was not replaced");
101
+ const directory = join(output, "lib");
102
+ const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
103
+ if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
104
+ throw new Error("Craft Zig source did not build the requested iOS permission profile");
105
+ profile.sha256 = Object.fromEntries(archives.map((name) => [name, digest(join(directory, name))]));
106
+ writeFileSync(join(directory, "ios-permissions.json"), `${JSON.stringify(profile, null, 2)}
107
+ `);
108
+ verifyRuntimeProfile(directory, config);
109
+ return await use(directory);
110
+ } finally {
111
+ rmSync(output, { recursive: true, force: true });
112
+ }
113
+ }
114
+
115
+ // src/index.ts
116
+ import { cpSync, existsSync as existsSync2, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
117
+ import { dirname as dirname2, extname, join as join4, resolve as resolve2, sep as sep2 } from "path";
12
118
  var {$ } = globalThis.Bun;
13
119
 
14
120
  // src/app-shortcuts.ts
@@ -67,7 +173,7 @@ ${entries}
67
173
 
68
174
  // src/source-audit.ts
69
175
  import { readdirSync } from "fs";
70
- import { join, relative, sep } from "path";
176
+ import { join as join2, relative, sep } from "path";
71
177
  var defaultIgnoredPaths = [
72
178
  ".git",
73
179
  "node_modules",
@@ -87,7 +193,7 @@ function isIgnored(path, ignoredPaths) {
87
193
  }
88
194
  function visit(root, current, ignoredPaths, found) {
89
195
  for (const entry of readdirSync(current, { withFileTypes: true })) {
90
- const absolute = join(current, entry.name);
196
+ const absolute = join2(current, entry.name);
91
197
  const relativePath = relative(root, absolute);
92
198
  if (isIgnored(relativePath, ignoredPaths))
93
199
  continue;
@@ -114,27 +220,27 @@ ${sources.map((path) => `- ${path}`).join(`
114
220
  }
115
221
  }
116
222
  // src/testing.ts
117
- import { dirname, join as join2 } from "path";
118
- var templates = join2(dirname(import.meta.dir), "templates/testing");
223
+ import { dirname, join as join3 } from "path";
224
+ var templates = join3(dirname(import.meta.dir), "templates/testing");
119
225
 
120
226
  // src/index.ts
121
227
  function compressNativeSources(target) {
122
228
  if (statSync(target).isDirectory()) {
123
229
  for (const entry of readdirSync2(target))
124
- compressNativeSources(join3(target, entry));
230
+ compressNativeSources(join4(target, entry));
125
231
  return;
126
232
  }
127
233
  if (!/\.(?:js|json)$/.test(target))
128
234
  return;
129
- const bytes = readFileSync(target);
235
+ const bytes = readFileSync2(target);
130
236
  if (bytes.length < 1024 || bytes[0] === 31 && bytes[1] === 139)
131
237
  return;
132
238
  const packed = gzipSync(bytes, { level: 9 });
133
239
  if (packed.length < bytes.length)
134
- writeFileSync(target, packed);
240
+ writeFileSync2(target, packed);
135
241
  }
136
242
  function syncNativeFonts(fonts, output) {
137
- const destination = join3(output, "NativeFonts");
243
+ const destination = join4(output, "NativeFonts");
138
244
  const names = new Set;
139
245
  const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
140
246
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
@@ -142,32 +248,32 @@ function syncNativeFonts(fonts, output) {
142
248
  if (names.has(family.toLowerCase()))
143
249
  throw new Error(`Duplicate native font name: ${family}`);
144
250
  names.add(family.toLowerCase());
145
- if ([entry.src, entry.license].some((file) => file && resolve(file).startsWith(resolve(destination) + sep2)))
251
+ if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep2)))
146
252
  throw new Error("Native font sources must be outside the generated NativeFonts directory");
147
253
  const extension = extname(entry.src).toLowerCase();
148
- if (![".ttf", ".otf"].includes(extension) || !existsSync(entry.src) || !statSync(entry.src).isFile())
254
+ if (![".ttf", ".otf"].includes(extension) || !existsSync2(entry.src) || !statSync(entry.src).isFile())
149
255
  throw new Error(`Native font ${family} must reference an existing TTF or OTF file: ${entry.src}`);
150
- if (entry.license && (!existsSync(entry.license) || !statSync(entry.license).isFile()))
256
+ if (entry.license && (!existsSync2(entry.license) || !statSync(entry.license).isFile()))
151
257
  throw new Error(`Native font license not found: ${entry.license}`);
152
258
  return { family, ...entry, filename: `${family}${extension}` };
153
259
  });
154
- rmSync(destination, { recursive: true, force: true });
260
+ rmSync2(destination, { recursive: true, force: true });
155
261
  if (!files.length)
156
262
  return [];
157
263
  mkdirSync(destination, { recursive: true });
158
264
  for (const file of files) {
159
- cpSync(file.src, join3(destination, file.filename));
265
+ cpSync(file.src, join4(destination, file.filename));
160
266
  if (file.license)
161
- cpSync(file.license, join3(destination, `${file.family}.LICENSE.txt`));
267
+ cpSync(file.license, join4(destination, `${file.family}.LICENSE.txt`));
162
268
  }
163
269
  return files.map((file) => `NativeFonts/${file.filename}`);
164
270
  }
165
- var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
271
+ var TEMPLATES_DIR = join4(dirname2(import.meta.dir), "templates");
166
272
  function resolveCraftVersion(startDir = import.meta.dir) {
167
- let directory = resolve(startDir);
273
+ let directory = resolve2(startDir);
168
274
  while (true) {
169
275
  try {
170
- const packageJSON = JSON.parse(readFileSync(join3(directory, "package.json"), "utf8"));
276
+ const packageJSON = JSON.parse(readFileSync2(join4(directory, "package.json"), "utf8"));
171
277
  if ((packageJSON.name === "@craft-native/ios" || packageJSON.name === "craft-native") && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
172
278
  return packageJSON.version;
173
279
  } catch {}
@@ -319,29 +425,29 @@ function hybridConfigProblems(config) {
319
425
  return problems;
320
426
  }
321
427
  function installNativeBundle(source, output) {
322
- if (!existsSync(source))
428
+ if (!existsSync2(source))
323
429
  throw new Error(`Native bundle not found: ${source}`);
324
- const single = join3(output, "dist", "native-screen.js");
325
- const routes = join3(output, "dist", "native-screens");
430
+ const single = join4(output, "dist", "native-screen.js");
431
+ const routes = join4(output, "dist", "native-screens");
326
432
  mkdirSync(dirname2(single), { recursive: true });
327
433
  if (statSync(source).isDirectory()) {
328
434
  if (!readdirSync2(source).some((name) => name.endsWith(".js")))
329
435
  throw new Error(`Native route bundles not found in ${source}`);
330
- if (resolve(source) !== resolve(routes)) {
331
- rmSync(routes, { recursive: true, force: true });
436
+ if (resolve2(source) !== resolve2(routes)) {
437
+ rmSync2(routes, { recursive: true, force: true });
332
438
  cpSync(source, routes, { recursive: true });
333
439
  }
334
440
  compressNativeSources(routes);
335
- rmSync(single, { force: true });
441
+ rmSync2(single, { force: true });
336
442
  return;
337
443
  }
338
- if (resolve(source) !== resolve(single))
444
+ if (resolve2(source) !== resolve2(single))
339
445
  cpSync(source, single);
340
446
  compressNativeSources(single);
341
- rmSync(routes, { recursive: true, force: true });
447
+ rmSync2(routes, { recursive: true, force: true });
342
448
  }
343
449
  function hasInstalledNativeBundle(output) {
344
- return existsSync(join3(output, "dist", "native-screen.js")) || existsSync(join3(output, "dist", "native-screens"));
450
+ return existsSync2(join4(output, "dist", "native-screen.js")) || existsSync2(join4(output, "dist", "native-screens"));
345
451
  }
346
452
  function runPostGenerateHook(hook, output, phase = "build") {
347
453
  if (!hook)
@@ -447,23 +553,10 @@ function renderAppearance(config) {
447
553
  return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
448
554
  return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
449
555
  }
450
- var LINKED_PURPOSES = {
451
- NSSpeechRecognitionUsageDescription: "speech recognition",
452
- NSMicrophoneUsageDescription: "the microphone",
453
- NSCameraUsageDescription: "the camera",
454
- NSPhotoLibraryUsageDescription: "your photo library",
455
- NSLocationWhenInUseUsageDescription: "your location",
456
- NSContactsUsageDescription: "your contacts",
457
- NSCalendarsUsageDescription: "your calendar",
458
- NSBluetoothAlwaysUsageDescription: "Bluetooth",
459
- NSMotionUsageDescription: "motion data",
460
- NSFaceIDUsageDescription: "Face ID",
461
- NSAppleMusicUsageDescription: "your music"
462
- };
463
556
  function renderUsageDescriptions(config) {
464
557
  const entries = [
465
558
  [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
466
- [config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
559
+ [config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
467
560
  [config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
468
561
  [config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
469
562
  [config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
@@ -486,8 +579,7 @@ function renderUsageDescriptions(config) {
486
579
  return [key, custom];
487
580
  if (enabled)
488
581
  return [key, value];
489
- const linked = LINKED_PURPOSES[key];
490
- return linked ? [key, `${config.appName} asks for ${linked} only in a feature that needs it.`] : null;
582
+ return null;
491
583
  }).filter((entry) => entry !== null).map(([key, value]) => plistString(key, value)).join(`
492
584
  `);
493
585
  }
@@ -652,9 +744,11 @@ function renderRuntimeSettings() {
652
744
  `);
653
745
  }
654
746
  function renderFeatureSettings(config) {
655
- const conditions = [];
747
+ const conditions = iosPermissions(config).filter((name) => name !== "music").map((name) => `CRAFT_PERMISSION_${name.toUpperCase()}`);
656
748
  if (config.enableScheduledWorkouts)
657
749
  conditions.push("CRAFT_SCHEDULED_WORKOUTS");
750
+ if (config.enableAppleMusic)
751
+ conditions.push("CRAFT_APPLE_MUSIC");
658
752
  if (!conditions.length)
659
753
  return "";
660
754
  return ` SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) ${conditions.join(" ")}"`;
@@ -729,7 +823,7 @@ function pngHasAlpha(bytes) {
729
823
  return false;
730
824
  }
731
825
  function writeOpaqueIcon(source, target) {
732
- const bytes = new Uint8Array(readFileSync(source));
826
+ const bytes = new Uint8Array(readFileSync2(source));
733
827
  if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
734
828
  cpSync(source, target);
735
829
  return;
@@ -738,27 +832,27 @@ function writeOpaqueIcon(source, target) {
738
832
  const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
739
833
  const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
740
834
  const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
741
- rmSync(flattened, { force: true });
835
+ rmSync2(flattened, { force: true });
742
836
  if (toPng.exitCode !== 0) {
743
837
  cpSync(source, target);
744
838
  console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
745
839
  }
746
840
  }
747
841
  function renderAssetCatalog(output, config) {
748
- const catalog = join3(output, "Assets.xcassets");
749
- const appIcon = join3(catalog, "AppIcon.appiconset");
750
- const launchBackground = join3(catalog, "LaunchBackground.colorset");
842
+ const catalog = join4(output, "Assets.xcassets");
843
+ const appIcon = join4(catalog, "AppIcon.appiconset");
844
+ const launchBackground = join4(catalog, "LaunchBackground.colorset");
751
845
  mkdirSync(appIcon, { recursive: true });
752
846
  mkdirSync(launchBackground, { recursive: true });
753
- writeFileSync(join3(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
847
+ writeFileSync2(join4(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
754
848
  `);
755
849
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
756
850
  if (config.appIconPath) {
757
- if (!existsSync(config.appIconPath))
851
+ if (!existsSync2(config.appIconPath))
758
852
  throw new Error(`App icon not found: ${config.appIconPath}`);
759
- writeOpaqueIcon(config.appIconPath, join3(appIcon, iconFilename));
853
+ writeOpaqueIcon(config.appIconPath, join4(appIcon, iconFilename));
760
854
  }
761
- writeFileSync(join3(appIcon, "Contents.json"), `${JSON.stringify({
855
+ writeFileSync2(join4(appIcon, "Contents.json"), `${JSON.stringify({
762
856
  images: iconFilename ? [{
763
857
  filename: iconFilename,
764
858
  idiom: "universal",
@@ -785,7 +879,7 @@ function renderAssetCatalog(output, config) {
785
879
  idiom: "universal"
786
880
  });
787
881
  }
788
- writeFileSync(join3(launchBackground, "Contents.json"), `${JSON.stringify({
882
+ writeFileSync2(join4(launchBackground, "Contents.json"), `${JSON.stringify({
789
883
  colors,
790
884
  info: { author: "xcode", version: 1 }
791
885
  }, null, 2)}
@@ -794,8 +888,8 @@ function renderAssetCatalog(output, config) {
794
888
  }
795
889
  var LAUNCH_LOGO = "LaunchLogo";
796
890
  function renderLaunchLogo(catalog, config) {
797
- const imageset = join3(catalog, `${LAUNCH_LOGO}.imageset`);
798
- rmSync(imageset, { recursive: true, force: true });
891
+ const imageset = join4(catalog, `${LAUNCH_LOGO}.imageset`);
892
+ rmSync2(imageset, { recursive: true, force: true });
799
893
  if (!config.splashImagePath)
800
894
  return;
801
895
  mkdirSync(imageset, { recursive: true });
@@ -804,20 +898,20 @@ function renderLaunchLogo(catalog, config) {
804
898
  for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
805
899
  if (!source)
806
900
  continue;
807
- if (!existsSync(source))
901
+ if (!existsSync2(source))
808
902
  throw new Error(`Splash image not found: ${source}`);
809
903
  const extension = source.split(".").pop().toLowerCase();
810
904
  if (extension === "svg" || extension === "pdf")
811
905
  vector = true;
812
906
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
813
- cpSync(source, join3(imageset, filename));
907
+ cpSync(source, join4(imageset, filename));
814
908
  images.push({
815
909
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
816
910
  filename,
817
911
  idiom: "universal"
818
912
  });
819
913
  }
820
- writeFileSync(join3(imageset, "Contents.json"), `${JSON.stringify({
914
+ writeFileSync2(join4(imageset, "Contents.json"), `${JSON.stringify({
821
915
  images,
822
916
  info: { author: "xcode", version: 1 },
823
917
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -928,18 +1022,18 @@ function renderOfflinePage(config) {
928
1022
  `;
929
1023
  }
930
1024
  function syncWebAssets(source, output) {
931
- const sourcePath = resolve(source);
932
- if (!existsSync(sourcePath))
1025
+ const sourcePath = resolve2(source);
1026
+ if (!existsSync2(sourcePath))
933
1027
  throw new Error(`Web asset path not found: ${source}`);
934
- const distDir = join3(output, "dist");
935
- rmSync(distDir, { recursive: true, force: true });
1028
+ const distDir = join4(output, "dist");
1029
+ rmSync2(distDir, { recursive: true, force: true });
936
1030
  mkdirSync(distDir, { recursive: true });
937
1031
  if (statSync(sourcePath).isDirectory()) {
938
1032
  cpSync(sourcePath, distDir, { recursive: true });
939
1033
  } else {
940
- cpSync(sourcePath, join3(distDir, "index.html"));
1034
+ cpSync(sourcePath, join4(distDir, "index.html"));
941
1035
  }
942
- if (!existsSync(join3(distDir, "index.html"))) {
1036
+ if (!existsSync2(join4(distDir, "index.html"))) {
943
1037
  throw new Error(`Web asset directory must contain index.html: ${source}`);
944
1038
  }
945
1039
  }
@@ -949,7 +1043,7 @@ function resolveRuntimeDir(override) {
949
1043
  const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
950
1044
  if (!dir)
951
1045
  return null;
952
- if (!existsSync(dir)) {
1046
+ if (!existsSync2(dir)) {
953
1047
  const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
954
1048
  throw new Error(`${source} ${dir}, which does not exist.`);
955
1049
  }
@@ -959,43 +1053,71 @@ var RUNTIME_ARCHIVES = {
959
1053
  device: ["libcraft-ios.a"],
960
1054
  simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
961
1055
  };
962
- async function installRuntime(output, runtimeDir) {
1056
+ async function installRuntime(output, runtimeDir, config) {
1057
+ if (config)
1058
+ verifyRuntimeProfile(runtimeDir, config);
963
1059
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
964
- const present = archives.filter((a) => existsSync(join3(runtimeDir, a)));
1060
+ const present = archives.filter((a) => existsSync2(join4(runtimeDir, a)));
965
1061
  if (present.length === 0) {
966
1062
  throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
967
1063
  }
968
1064
  return { sdk, archives, present };
969
1065
  });
970
- const dest = join3(output, "Runtime");
971
- rmSync(dest, { recursive: true, force: true });
972
- for (const { sdk, archives, present } of resolved) {
973
- const sdkDir = join3(dest, sdk);
974
- mkdirSync(sdkDir, { recursive: true });
975
- const target = join3(sdkDir, "libcraft-ios.a");
976
- if (present.length === 1) {
977
- const missing = archives.filter((a) => !present.includes(a));
978
- if (missing.length > 0) {
979
- console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1066
+ const installed = join4(output, "Runtime");
1067
+ const staging = mkdtempSync2(join4(output, ".craft-runtime-"));
1068
+ const dest = join4(staging, "next");
1069
+ const previous = join4(staging, "previous");
1070
+ try {
1071
+ for (const { sdk, archives, present } of resolved) {
1072
+ const sdkDir = join4(dest, sdk);
1073
+ mkdirSync(sdkDir, { recursive: true });
1074
+ const target = join4(sdkDir, "libcraft-ios.a");
1075
+ if (present.length === 1) {
1076
+ const missing = archives.filter((a) => !present.includes(a));
1077
+ if (missing.length > 0) {
1078
+ console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1079
+ }
1080
+ cpSync(join4(runtimeDir, present[0]), target);
1081
+ } else {
1082
+ await $`lipo -create ${present.map((a) => join4(runtimeDir, a))} -output ${target}`.quiet();
980
1083
  }
981
- cpSync(join3(runtimeDir, present[0]), target);
982
- } else {
983
- await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
984
1084
  }
1085
+ if (config)
1086
+ writeInstalledProfile(dest, config);
1087
+ if (existsSync2(installed))
1088
+ renameSync(installed, previous);
1089
+ try {
1090
+ renameSync(dest, installed);
1091
+ } catch (error) {
1092
+ if (existsSync2(previous))
1093
+ renameSync(previous, installed);
1094
+ throw error;
1095
+ }
1096
+ return true;
1097
+ } finally {
1098
+ rmSync2(staging, { recursive: true, force: true });
985
1099
  }
986
- return true;
987
1100
  }
988
- async function refreshRuntime(output, override) {
989
- if (!existsSync(join3(output, "Runtime")))
1101
+ async function refreshRuntime(output, config, override) {
1102
+ if (!existsSync2(join4(output, "Runtime")))
990
1103
  return;
991
1104
  const dir = resolveRuntimeDir(override);
992
1105
  if (!dir) {
1106
+ verifyInstalledProfile(join4(output, "Runtime"), config);
993
1107
  console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
994
1108
  return;
995
1109
  }
996
- await installRuntime(output, dir);
1110
+ await installRuntime(output, dir, config);
997
1111
  console.log(" Refreshed the Zig runtime from", dir);
998
1112
  }
1113
+ async function installAppRuntime(output, source, config) {
1114
+ return withAppRuntime(source, config, async (directory) => {
1115
+ verifyRuntimeProfile(directory, config);
1116
+ await installRuntime(output, directory, config);
1117
+ writeInstalledProfile(join4(output, "Runtime"), config, resolve2(source));
1118
+ return true;
1119
+ });
1120
+ }
999
1121
  function productName(name) {
1000
1122
  const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
1001
1123
  const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
@@ -1006,45 +1128,63 @@ function productName(name) {
1006
1128
  function writeShell(output, config) {
1007
1129
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
1008
1130
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
1131
+ const writeSource = (path, source) => {
1132
+ const rendered = renderPermissionSource(source, config);
1133
+ if (!existsSync2(path) || readFileSync2(path, "utf8") !== rendered)
1134
+ writeFileSync2(path, rendered);
1135
+ };
1009
1136
  const name = productName(config.appName);
1010
1137
  const shortcutSource = appShortcutsSwift(config.appShortcuts, config.urlSchemes);
1011
- mkdirSync(join3(output, "Sources"), { recursive: true });
1012
- mkdirSync(join3(output, "Shared"), { recursive: true });
1013
- const swiftTemplate = readFileSync(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1138
+ mkdirSync(join4(output, "Sources"), { recursive: true });
1139
+ mkdirSync(join4(output, "Shared"), { recursive: true });
1140
+ const swiftTemplate = readFileSync2(join4(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1014
1141
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
1015
- writeFileSync(join3(output, "Sources", `${name}App.swift`), swiftSource);
1016
- writeFileSync(join3(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
1017
- cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
1018
- cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
1019
- cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
1020
- cpSync(join3(TEMPLATES_DIR, "CraftHealth.swift"), join3(output, "Sources", "CraftHealth.swift"));
1021
- cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
1022
- cpSync(join3(TEMPLATES_DIR, "CraftBluetooth.swift"), join3(output, "Sources", "CraftBluetooth.swift"));
1023
- cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
1024
- cpSync(join3(TEMPLATES_DIR, "CraftRecordingArchive.swift"), join3(output, "Sources", "CraftRecordingArchive.swift"));
1025
- cpSync(join3(TEMPLATES_DIR, "CraftPersistentStorage.swift"), join3(output, "Sources", "CraftPersistentStorage.swift"));
1026
- cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
1027
- cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
1028
- cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
1029
- cpSync(join3(TEMPLATES_DIR, "CraftNativeServices.swift"), join3(output, "Sources", "CraftNativeServices.swift"));
1030
- cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
1031
- cpSync(join3(TEMPLATES_DIR, "CraftVoiceRecording.swift"), join3(output, "Sources", "CraftVoiceRecording.swift"));
1032
- cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
1033
- cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
1034
- writeFileSync(join3(output, "Sources", "CraftCrashReporter.swift"), readFileSync(join3(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
1035
- cpSync(join3(TEMPLATES_DIR, "CraftLiveActivities.swift"), join3(output, "Sources", "CraftLiveActivities.swift"));
1036
- cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
1142
+ writeSource(join4(output, "Sources", `${name}App.swift`), swiftSource);
1143
+ writeSource(join4(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
1144
+ writeSource(join4(output, "Sources", "CraftNativeScreen.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeScreen.swift"), "utf8"));
1145
+ writeSource(join4(output, "Sources", "CraftNativeFlatList.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeFlatList.swift"), "utf8"));
1146
+ writeSource(join4(output, "Sources", "CraftNativeActions.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeActions.swift"), "utf8"));
1147
+ writeSource(join4(output, "Sources", "CraftHealth.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHealth.swift"), "utf8"));
1148
+ writeSource(join4(output, "Sources", "CraftMusic.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftMusic.swift"), "utf8"));
1149
+ writeSource(join4(output, "Sources", "CraftBluetooth.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftBluetooth.swift"), "utf8"));
1150
+ writeSource(join4(output, "Sources", "CraftHeartRate.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHeartRate.swift"), "utf8"));
1151
+ writeSource(join4(output, "Sources", "CraftRecordingArchive.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftRecordingArchive.swift"), "utf8"));
1152
+ writeSource(join4(output, "Sources", "CraftPersistentStorage.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftPersistentStorage.swift"), "utf8"));
1153
+ writeSource(join4(output, "Sources", "CraftScheduledWorkouts.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), "utf8"));
1154
+ writeSource(join4(output, "Sources", "CraftNativeMutation.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeMutation.swift"), "utf8"));
1155
+ writeSource(join4(output, "Sources", "CraftNativeUI.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeUI.swift"), "utf8"));
1156
+ writeSource(join4(output, "Sources", "CraftNativeServices.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeServices.swift"), "utf8"));
1157
+ writeSource(join4(output, "Sources", "CraftNativeFiles.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeFiles.swift"), "utf8"));
1158
+ writeSource(join4(output, "Sources", "CraftVoiceRecording.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftVoiceRecording.swift"), "utf8"));
1159
+ writeSource(join4(output, "Sources", "CraftWebAudio.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftWebAudio.swift"), "utf8"));
1160
+ writeSource(join4(output, "Sources", "CraftHybrid.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHybrid.swift"), "utf8"));
1161
+ writeSource(join4(output, "Sources", "CraftCrashReporter.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
1162
+ writeSource(join4(output, "Sources", "CraftLiveActivities.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftLiveActivities.swift"), "utf8"));
1163
+ writeSource(join4(output, "Shared", "CraftActivityAttributes.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftActivityAttributes.swift"), "utf8"));
1037
1164
  for (const [enabled, folder, template, filename] of [
1038
1165
  [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
1039
1166
  [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
1040
1167
  ]) {
1041
1168
  if (!enabled)
1042
1169
  continue;
1043
- mkdirSync(join3(output, folder), { recursive: true });
1044
- const source = readFileSync(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
1045
- writeFileSync(join3(output, folder, filename), source);
1170
+ mkdirSync(join4(output, folder), { recursive: true });
1171
+ const source = readFileSync2(join4(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
1172
+ writeSource(join4(output, folder, filename), source);
1046
1173
  }
1047
1174
  }
1175
+ function refreshUsageDescriptions(output, config) {
1176
+ const path = join4(output, "Info.plist");
1177
+ if (!existsSync2(path))
1178
+ return;
1179
+ const managed = /\s*<key>(?:NS(?:SpeechRecognition|Microphone|Camera|PhotoLibrary|LocationWhenInUse|LocationAlwaysAndWhenInUse|Contacts|Calendars|BluetoothAlways|Motion|LocalNetwork|HealthShare|HealthUpdate|FaceID|AppleMusic)|NFCReader)UsageDescription<\/key>\s*<string>[\s\S]*?<\/string>/g;
1180
+ const plist = readFileSync2(path, "utf8").replace(managed, "");
1181
+ const end = plist.lastIndexOf("</dict>");
1182
+ if (end < 0)
1183
+ throw new Error("Info.plist must contain a root dictionary");
1184
+ writeFileSync2(path, `${plist.slice(0, end).trimEnd()}
1185
+ ${renderUsageDescriptions(config)}
1186
+ ${plist.slice(end)}`);
1187
+ }
1048
1188
  async function init(options) {
1049
1189
  const { name: displayName, bundleId, teamId, output } = options;
1050
1190
  const name = productName(displayName);
@@ -1052,11 +1192,11 @@ async function init(options) {
1052
1192
  \u26A1 Initializing Craft iOS project: ${displayName}`);
1053
1193
  console.log(` Output: ${output}
1054
1194
  `);
1055
- const dirs = [output, join3(output, "Sources"), join3(output, "Shared"), join3(output, "dist")];
1195
+ const dirs = [output, join4(output, "Sources"), join4(output, "Shared"), join4(output, "dist")];
1056
1196
  if (options.config?.enableWatchApp)
1057
- dirs.push(join3(output, "WatchApp"), join3(output, "WatchExtension"));
1197
+ dirs.push(join4(output, "WatchApp"), join4(output, "WatchExtension"));
1058
1198
  for (const dir of dirs) {
1059
- if (!existsSync(dir)) {
1199
+ if (!existsSync2(dir)) {
1060
1200
  mkdirSync(dir, { recursive: true });
1061
1201
  }
1062
1202
  }
@@ -1086,10 +1226,20 @@ async function init(options) {
1086
1226
  `));
1087
1227
  if (Object.keys(config.shareStorage ?? {}).length)
1088
1228
  config.enableSecureStorage = true;
1089
- writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1229
+ const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1230
+ const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE;
1231
+ if (runtimeSource && runtimeDir)
1232
+ throw new Error("Choose runtimeSource or runtimeDir, not both");
1233
+ const hasRuntime = runtimeSource ? await installAppRuntime(output, runtimeSource, config) : runtimeDir ? await installRuntime(output, runtimeDir, config) : false;
1234
+ if (hasRuntime) {
1235
+ console.log(" Linked the Zig runtime from", runtimeSource ?? runtimeDir);
1236
+ } else {
1237
+ rmSync2(join4(output, "Runtime"), { recursive: true, force: true });
1238
+ }
1239
+ writeFileSync2(join4(output, "craft.config.json"), JSON.stringify(config, null, 2));
1090
1240
  writeShell(output, config);
1091
1241
  const nativeFontFiles = syncNativeFonts(config.nativeFonts, output);
1092
- const infoPlistTemplate = readFileSync(join3(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1242
+ const infoPlistTemplate = readFileSync2(join4(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1093
1243
  const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NATIVE_FONTS\}\}/g, nativeFontFiles.length ? ` <key>UIAppFonts</key>
1094
1244
  <array>
1095
1245
  ${plistArray(nativeFontFiles, 2)}
@@ -1097,8 +1247,8 @@ ${plistArray(nativeFontFiles, 2)}
1097
1247
  <true/>
1098
1248
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1099
1249
  <true/>` : "");
1100
- writeFileSync(join3(output, "Info.plist"), infoPlist);
1101
- const projectYmlTemplate = readFileSync(join3(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1250
+ writeFileSync2(join4(output, "Info.plist"), infoPlist);
1251
+ const projectYmlTemplate = readFileSync2(join4(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1102
1252
  const nativeDependencies = [];
1103
1253
  if (config.enableLiveActivities)
1104
1254
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -1153,38 +1303,31 @@ ${plistArray(nativeFontFiles, 2)}
1153
1303
  APPLICATION_EXTENSION_API_ONLY: YES
1154
1304
  SKIP_INSTALL: YES`);
1155
1305
  }
1156
- const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1157
- const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
1158
- if (hasRuntime) {
1159
- console.log(" Linked the Zig runtime from", runtimeDir);
1160
- } else {
1161
- rmSync(join3(output, "Runtime"), { recursive: true, force: true });
1162
- }
1163
1306
  const projectYml = projectYmlTemplate.replace(/\{\{NATIVE_FONT_RESOURCES\}\}/g, nativeFontFiles.length ? ` - path: NativeFonts
1164
1307
  type: folder
1165
1308
  buildPhase: resources` : "").replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{FEATURE_SETTINGS\}\}/g, renderFeatureSettings(config)).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1166
1309
  ${nativeDependencies.join(`
1167
1310
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1168
1311
  `));
1169
- writeFileSync(join3(output, "project.yml"), projectYml);
1170
- writeFileSync(join3(output, "Craft.entitlements"), renderEntitlements(config));
1171
- writeFileSync(join3(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1312
+ writeFileSync2(join4(output, "project.yml"), projectYml);
1313
+ writeFileSync2(join4(output, "Craft.entitlements"), renderEntitlements(config));
1314
+ writeFileSync2(join4(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1172
1315
  renderAssetCatalog(output, config);
1173
1316
  if (config.enableLiveActivities) {
1174
- mkdirSync(join3(output, "WidgetExtension"), { recursive: true });
1175
- const widgetInfo = readFileSync(join3(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1176
- writeFileSync(join3(output, "WidgetExtension", "Info.plist"), widgetInfo);
1317
+ mkdirSync(join4(output, "WidgetExtension"), { recursive: true });
1318
+ const widgetInfo = readFileSync2(join4(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1319
+ writeFileSync2(join4(output, "WidgetExtension", "Info.plist"), widgetInfo);
1177
1320
  }
1178
1321
  if (config.enableWatchApp) {
1179
- const watchInfo = readFileSync(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1180
- writeFileSync(join3(output, "WatchApp", "Info.plist"), watchInfo);
1181
- writeFileSync(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1182
- const watchExtensionInfo = readFileSync(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{HEALTH_USAGE_DESCRIPTIONS\}\}/g, config.enableHealthKit ? `${plistString("NSHealthShareUsageDescription", `${displayName} reads health data you choose to share.`)}
1322
+ const watchInfo = readFileSync2(join4(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1323
+ writeFileSync2(join4(output, "WatchApp", "Info.plist"), watchInfo);
1324
+ writeFileSync2(join4(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1325
+ const watchExtensionInfo = readFileSync2(join4(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{HEALTH_USAGE_DESCRIPTIONS\}\}/g, config.enableHealthKit ? `${plistString("NSHealthShareUsageDescription", `${displayName} reads health data you choose to share.`)}
1183
1326
  ${plistString("NSHealthUpdateUsageDescription", `${displayName} writes health data only with your permission.`)}` : "");
1184
- writeFileSync(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1185
- writeFileSync(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1327
+ writeFileSync2(join4(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1328
+ writeFileSync2(join4(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1186
1329
  }
1187
- writeFileSync(join3(output, "dist", "index.html"), renderOfflinePage(config));
1330
+ writeFileSync2(join4(output, "dist", "index.html"), renderOfflinePage(config));
1188
1331
  if (usesNativeBundle(config)) {
1189
1332
  if (config.nativeBundle)
1190
1333
  installNativeBundle(config.nativeBundle, output);
@@ -1211,11 +1354,11 @@ async function build(options) {
1211
1354
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
1212
1355
  console.log(`
1213
1356
  \uD83D\uDCE6 Building Craft iOS project...`);
1214
- const configPath = join3(output, "craft.config.json");
1215
- if (!existsSync(configPath)) {
1357
+ const configPath = join4(output, "craft.config.json");
1358
+ if (!existsSync2(configPath)) {
1216
1359
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1217
1360
  }
1218
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1361
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1219
1362
  if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
1220
1363
  throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
1221
1364
  }
@@ -1240,7 +1383,7 @@ async function build(options) {
1240
1383
  config.devServerURL = devServer;
1241
1384
  const origin = new URL(devServer).origin;
1242
1385
  config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
1243
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1386
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
1244
1387
  console.log(` Dev server: ${devServer}`);
1245
1388
  }
1246
1389
  if (htmlPath) {
@@ -1256,7 +1399,20 @@ async function build(options) {
1256
1399
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
1257
1400
  }
1258
1401
  }
1259
- await refreshRuntime(output, options.runtimeDir);
1402
+ const profilePath = join4(output, "Runtime", "profile.json");
1403
+ const savedProfile = existsSync2(profilePath) ? JSON.parse(readFileSync2(profilePath, "utf8")) : undefined;
1404
+ const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE ?? savedProfile?.source;
1405
+ if (runtimeSource && existsSync2(join4(output, "Runtime"))) {
1406
+ if (options.runtimeDir)
1407
+ throw new Error("Choose runtimeSource or runtimeDir, not both");
1408
+ await installAppRuntime(output, runtimeSource, config);
1409
+ } else {
1410
+ if (savedProfile && savedProfile.mask !== iosPermissionMask(config))
1411
+ throw new Error("Installed Zig runtime permission profile is stale; provide runtimeSource to rebuild it");
1412
+ await refreshRuntime(output, config, options.runtimeDir);
1413
+ }
1414
+ writeShell(output, config);
1415
+ refreshUsageDescriptions(output, config);
1260
1416
  if (!generateProject)
1261
1417
  return;
1262
1418
  try {
@@ -1283,7 +1439,7 @@ async function open(options) {
1283
1439
  if (!xcodeproj) {
1284
1440
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
1285
1441
  }
1286
- const projectPath = join3(output, xcodeproj);
1442
+ const projectPath = join4(output, xcodeproj);
1287
1443
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1288
1444
  await $`open ${projectPath}`;
1289
1445
  }
@@ -1337,8 +1493,8 @@ async function showSimulator() {
1337
1493
  const selected = await $`xcode-select -p`.quiet().nothrow();
1338
1494
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1339
1495
  if (developer) {
1340
- const app = join3(developer, "Applications", "Simulator.app");
1341
- if (existsSync(app)) {
1496
+ const app = join4(developer, "Applications", "Simulator.app");
1497
+ if (existsSync2(app)) {
1342
1498
  await $`open ${app}`.quiet().nothrow();
1343
1499
  return;
1344
1500
  }
@@ -1353,12 +1509,12 @@ async function run(options) {
1353
1509
  if (!xcodeproj) {
1354
1510
  throw new Error(`No Xcode project found in ${output}`);
1355
1511
  }
1356
- const projectPath = join3(output, xcodeproj);
1512
+ const projectPath = join4(output, xcodeproj);
1357
1513
  const appName = xcodeproj.replace(".xcodeproj", "");
1358
- const configPath = join3(output, "craft.config.json");
1359
- if (!existsSync(configPath))
1514
+ const configPath = join4(output, "craft.config.json");
1515
+ if (!existsSync2(configPath))
1360
1516
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1361
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1517
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1362
1518
  if (simulator) {
1363
1519
  console.log("\uD83D\uDCF1 Building and running on simulator...");
1364
1520
  const device = await pickSimulator();
@@ -1368,10 +1524,10 @@ async function run(options) {
1368
1524
  console.log(` Device: ${device.name} (${device.runtime})`);
1369
1525
  try {
1370
1526
  await bootSimulator(device);
1371
- const derivedData = join3(output, "build");
1372
- const product = join3(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1527
+ const derivedData = join4(output, "build");
1528
+ const product = join4(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1373
1529
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1374
- if (!existsSync(product)) {
1530
+ if (!existsSync2(product)) {
1375
1531
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
1376
1532
  }
1377
1533
  await $`xcrun simctl install ${device.udid} ${product}`;
@@ -1477,7 +1633,7 @@ async function main() {
1477
1633
  });
1478
1634
  break;
1479
1635
  case "audit-sources": {
1480
- const root = resolve2(values["app-root"] || ".");
1636
+ const root = resolve3(values["app-root"] || ".");
1481
1637
  assertNoApplicationSwiftSources(root);
1482
1638
  console.log(`\u2705 No application-owned Swift sources found in ${root}`);
1483
1639
  break;