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 +16 -0
- package/dist/android/src/emulator.d.ts +4 -0
- package/dist/android/src/index.js +14 -5
- package/dist/android/templates/CraftNativeShell.kt.template +12 -5
- package/dist/android/templates/CraftSnapshotFile.kt.template +26 -0
- package/dist/android/templates/MainActivityNative.kt.template +16 -0
- package/dist/browser/storage.d.ts +5 -3
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +31 -7
- package/dist/cli.js +103 -23
- package/dist/index.cjs +100 -23
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +103 -23
- package/dist/ios/templates/CraftApp.swift +44 -5
- package/dist/ios/templates/CraftHybrid.swift +21 -0
- package/dist/ios/templates/CraftNativeScreen.swift +116 -8
- package/dist/package.d.ts +10 -0
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
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",
|
|
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
|
|
311
|
-
memoryMb
|
|
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", "-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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;
|
|
21
|
-
lock?: (<T>(name: string, operation: () => T | Promise<T
|
|
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) =>
|
|
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
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
"--
|
|
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.
|
|
2948
|
+
var version = "0.0.290";
|
|
2869
2949
|
|
|
2870
2950
|
// bin/cli.ts
|
|
2871
2951
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|