@mcesystems/apple-kit 1.0.40 → 1.0.41

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.
@@ -1,856 +1,859 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Export libimobiledevice resources to a specified path
4
- *
5
- * This script exports the libimobiledevice binaries and their dependencies
6
- * to a target directory. The resources can then be used by applications
7
- * that depend on apple-kit.
8
- *
9
- * Usage:
10
- * npx tsx export-resources.ts <target-path>
11
- *
12
- * Example:
13
- * npx tsx export-resources.ts /path/to/my-app/resources/apple-kit
14
- *
15
- * Platform support:
16
- * - macOS: Copies binaries from Homebrew installation
17
- * - Windows: Builds from source using MSYS2/MinGW (requires manual setup)
18
- */
19
-
20
- import { exec, spawn } from "node:child_process";
21
- import {
22
- chmodSync,
23
- copyFileSync,
24
- existsSync,
25
- mkdirSync,
26
- readFileSync,
27
- readdirSync,
28
- readlinkSync,
29
- rmSync,
30
- unlinkSync,
31
- writeFileSync,
32
- } from "node:fs";
33
- import path from "node:path";
34
- import { fileURLToPath } from "node:url";
35
- import { promisify } from "node:util";
36
-
37
- // Get the directory of this script
38
- const __filename = fileURLToPath(import.meta.url);
39
- const __dirname = path.dirname(__filename);
40
-
41
- const execAsync = promisify(exec);
42
-
43
- // ============================================================================
44
- // Configuration
45
- // ============================================================================
46
-
47
- // Tools we need to bundle
48
- const REQUIRED_TOOLS = [
49
- "idevice_id",
50
- "ideviceinfo",
51
- "idevicepair",
52
- "idevicename",
53
- "idevicedebug",
54
- "ideviceinstaller",
55
- "iproxy",
56
- ];
57
-
58
- // Optional tools (won't fail if not found)
59
- const OPTIONAL_TOOLS = [
60
- "ideviceactivation",
61
- "idevicesyslog",
62
- "idevicescreenshot",
63
- "idevicediagnostics",
64
- ];
65
-
66
- // Homebrew paths on macOS
67
- const HOMEBREW_ARM_PATH = "/opt/homebrew";
68
- const HOMEBREW_INTEL_PATH = "/usr/local";
69
-
70
- // MSYS2 paths on Windows
71
- const MSYS2_DEFAULT_PATH = "C:\\msys64";
72
-
73
- // Pre-built resources configuration
74
- const PREBUILT_RESOURCES_DIR = path.join(__dirname, "..", "prebuilt");
75
- const LIBIMOBILEDEVICE_VERSION = "1.0.0";
76
-
77
- // System library prefixes that should NOT be copied (they exist on all macOS)
78
- const SYSTEM_LIB_PREFIXES = ["/System/Library/", "/usr/lib/", "/Library/Apple/"];
79
-
80
- // ============================================================================
81
- // Utility Functions
82
- // ============================================================================
83
-
84
- function printUsage(): void {
85
- console.log(`
86
- Usage: npx tsx export-resources.ts <target-path>
87
-
88
- Arguments:
89
- target-path Directory where resources will be exported
90
-
91
- Options:
92
- --skip-verify Skip verification step
93
-
94
- Examples:
95
- npx tsx export-resources.ts ./my-app/resources/apple-kit
96
- npx tsx export-resources.ts /absolute/path/to/resources
97
-
98
- The script will create the following structure:
99
- <target-path>/
100
- bin/
101
- darwin/ (macOS binaries and dylibs)
102
- windows/ (Windows binaries and DLLs)
103
- licenses/
104
- LGPL-2.1.txt
105
- `);
106
- }
107
-
108
- function resolveSymlink(filePath: string): string {
109
- try {
110
- const linkTarget = readlinkSync(filePath);
111
- if (path.isAbsolute(linkTarget)) {
112
- return linkTarget;
113
- }
114
- return path.resolve(path.dirname(filePath), linkTarget);
115
- } catch {
116
- return filePath; // Not a symlink
117
- }
118
- }
119
-
120
- /**
121
- * Check if a library path is a system library that doesn't need to be copied
122
- */
123
- function isSystemLibrary(libPath: string): boolean {
124
- return SYSTEM_LIB_PREFIXES.some((prefix) => libPath.startsWith(prefix));
125
- }
126
-
127
- /**
128
- * Check if a library path needs to be bundled (is from homebrew or similar)
129
- */
130
- function needsBundling(libPath: string): boolean {
131
- if (isSystemLibrary(libPath)) {
132
- return false;
133
- }
134
- if (libPath.startsWith("@")) {
135
- // Already a relocatable path
136
- return false;
137
- }
138
- // Bundle anything from /opt/homebrew, /usr/local/opt, or /usr/local/Cellar
139
- return (
140
- libPath.includes("/opt/homebrew") ||
141
- libPath.includes("/usr/local/opt") ||
142
- libPath.includes("/usr/local/Cellar") ||
143
- libPath.includes("/usr/local/lib")
144
- );
145
- }
146
-
147
- interface DylibDependency {
148
- originalPath: string;
149
- name: string;
150
- realPath: string;
151
- }
152
-
153
- /**
154
- * Get all dylib dependencies for a binary, including the original path as referenced
155
- */
156
- async function getDylibDependencies(binaryPath: string): Promise<DylibDependency[]> {
157
- const dylibs: DylibDependency[] = [];
158
-
159
- try {
160
- const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
161
- const lines = stdout.split("\n");
162
-
163
- for (const line of lines) {
164
- // Match library paths (with or without version info in parentheses)
165
- const match = line.match(/^\s+(.+?)\s+\(/);
166
- if (match) {
167
- const libPath = match[1].trim();
168
- if (needsBundling(libPath)) {
169
- const realPath = resolveSymlink(libPath);
170
- dylibs.push({
171
- originalPath: libPath,
172
- name: path.basename(libPath),
173
- realPath: existsSync(realPath) ? realPath : libPath,
174
- });
175
- }
176
- }
177
- }
178
- } catch (error) {
179
- console.warn(`Warning: Could not get dependencies for ${binaryPath}: ${error}`);
180
- }
181
-
182
- return dylibs;
183
- }
184
-
185
- /**
186
- * Recursively collect ALL dylib dependencies
187
- * Continues until no new dependencies are found
188
- */
189
- async function collectAllDependencies(
190
- initialBinaries: string[]
191
- ): Promise<Map<string, DylibDependency>> {
192
- const allDylibs = new Map<string, DylibDependency>();
193
- const processedPaths = new Set<string>();
194
- const toProcess: string[] = [...initialBinaries];
195
-
196
- console.log(" Scanning for dependencies...");
197
-
198
- let iteration = 0;
199
- while (toProcess.length > 0) {
200
- iteration++;
201
- const currentPath = toProcess.pop();
202
- if (!currentPath) {
203
- continue;
204
- }
205
-
206
- if (processedPaths.has(currentPath)) {
207
- continue;
208
- }
209
- processedPaths.add(currentPath);
210
-
211
- const deps = await getDylibDependencies(currentPath);
212
-
213
- for (const dep of deps) {
214
- if (!allDylibs.has(dep.name)) {
215
- allDylibs.set(dep.name, dep);
216
- // Add this dylib to be processed for its own dependencies
217
- if (existsSync(dep.realPath)) {
218
- toProcess.push(dep.realPath);
219
- }
220
- }
221
- }
222
- }
223
-
224
- console.log(` Found ${allDylibs.size} unique dylib dependencies (${iteration} files scanned)`);
225
- return allDylibs;
226
- }
227
-
228
- /**
229
- * Fix all library paths in a binary to use @loader_path
230
- * This rewrites ALL non-system library references
231
- */
232
- async function fixAllLibraryPaths(
233
- binaryPath: string,
234
- allDylibNames: Set<string>
235
- ): Promise<string[]> {
236
- const unfixedPaths: string[] = [];
237
-
238
- try {
239
- const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
240
- const lines = stdout.split("\n");
241
-
242
- for (const line of lines) {
243
- const match = line.match(/^\s+(.+?)\s+\(/);
244
- if (match) {
245
- const libPath = match[1].trim();
246
-
247
- // Skip system libraries and already-fixed paths
248
- if (isSystemLibrary(libPath) || libPath.startsWith("@")) {
249
- continue;
250
- }
251
-
252
- const libName = path.basename(libPath);
253
-
254
- // Check if we have this dylib in our collection
255
- if (allDylibNames.has(libName)) {
256
- try {
257
- await execAsync(
258
- `install_name_tool -change "${libPath}" "@loader_path/${libName}" "${binaryPath}"`
259
- );
260
- } catch (_error) {
261
- console.warn(` Warning: Could not fix path ${libPath} in ${path.basename(binaryPath)}`);
262
- unfixedPaths.push(libPath);
263
- }
264
- } else {
265
- // This is a non-system library we don't have - this is a problem
266
- unfixedPaths.push(libPath);
267
- }
268
- }
269
- }
270
- } catch (error) {
271
- console.warn(`Warning: Could not process ${binaryPath}: ${error}`);
272
- }
273
-
274
- return unfixedPaths;
275
- }
276
-
277
- /**
278
- * Fix the install name (ID) of a dylib to use @loader_path
279
- */
280
- async function fixDylibId(dylibPath: string): Promise<void> {
281
- const dylibName = path.basename(dylibPath);
282
- try {
283
- await execAsync(`install_name_tool -id "@loader_path/${dylibName}" "${dylibPath}"`);
284
- } catch {
285
- console.warn(` Warning: Could not fix dylib ID for ${dylibName}`);
286
- }
287
- }
288
-
289
- /**
290
- * Ad-hoc code sign a binary (required after modifying with install_name_tool on modern macOS)
291
- */
292
- async function codesignBinary(binaryPath: string): Promise<boolean> {
293
- try {
294
- await execAsync(`codesign --force --sign - "${binaryPath}"`);
295
- return true;
296
- } catch (error) {
297
- console.warn(` Warning: Could not code sign ${path.basename(binaryPath)}: ${error}`);
298
- return false;
299
- }
300
- }
301
-
302
- /**
303
- * Verify that a binary can be loaded and executed
304
- */
305
- async function verifyBinary(
306
- binaryPath: string,
307
- binDir: string
308
- ): Promise<{ success: boolean; error?: string; }> {
309
- const binaryName = path.basename(binaryPath);
310
-
311
- // Check that otool shows no broken paths
312
- try {
313
- const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
314
- const lines = stdout.split("\n");
315
-
316
- for (const line of lines) {
317
- const match = line.match(/^\s+(.+?)\s+\(/);
318
- if (match) {
319
- const libPath = match[1].trim();
320
-
321
- // Skip system libraries
322
- if (isSystemLibrary(libPath)) {
323
- continue;
324
- }
325
-
326
- // If it's an @loader_path reference, check the file exists
327
- if (libPath.startsWith("@loader_path/")) {
328
- const libName = libPath.replace("@loader_path/", "");
329
- const fullPath = path.join(binDir, libName);
330
- if (!existsSync(fullPath)) {
331
- return { success: false, error: `Missing library: ${libName}` };
332
- }
333
- } else if (!libPath.startsWith("@")) {
334
- // Absolute path that's not a system library - this is a problem
335
- return { success: false, error: `Unfixed absolute path: ${libPath}` };
336
- }
337
- }
338
- }
339
- } catch (error) {
340
- return { success: false, error: `otool failed: ${error}` };
341
- }
342
-
343
- // For executable binaries (not dylibs), try to run them with --help or -h
344
- if (!binaryName.endsWith(".dylib")) {
345
- try {
346
- // Use spawn with a timeout to test if the binary loads
347
- const result = await new Promise<{ success: boolean; error?: string; }>((resolve) => {
348
- const child = spawn(binaryPath, ["--help"], {
349
- timeout: 5000,
350
- env: { ...process.env, DYLD_LIBRARY_PATH: binDir },
351
- });
352
-
353
- let stderr = "";
354
- child.stderr?.on("data", (data) => {
355
- stderr += data.toString();
356
- });
357
-
358
- child.on("error", (err) => {
359
- resolve({ success: false, error: `Spawn error: ${err.message}` });
360
- });
361
-
362
- child.on("close", (code) => {
363
- // Exit codes 0, 1, or 2 are acceptable (0=success, 1=error, 2=usage)
364
- // What we're checking is that the binary LOADS, not that --help works
365
- if (code !== null && code <= 2) {
366
- resolve({ success: true });
367
- } else if (stderr.includes("Library not loaded") || stderr.includes("image not found")) {
368
- resolve({ success: false, error: stderr.split("\n")[0] });
369
- } else {
370
- // Other exit codes might be fine too
371
- resolve({ success: true });
372
- }
373
- });
374
- });
375
-
376
- if (!result.success) {
377
- return result;
378
- }
379
- } catch (error) {
380
- // If spawn fails entirely, that's a problem
381
- return { success: false, error: `Execution test failed: ${error}` };
382
- }
383
- }
384
-
385
- return { success: true };
386
- }
387
-
388
- // ============================================================================
389
- // macOS Export
390
- // ============================================================================
391
-
392
- function getHomebrewPath(): string | null {
393
- if (existsSync(path.join(HOMEBREW_ARM_PATH, "bin", "idevice_id"))) {
394
- return HOMEBREW_ARM_PATH;
395
- }
396
- if (existsSync(path.join(HOMEBREW_INTEL_PATH, "bin", "idevice_id"))) {
397
- return HOMEBREW_INTEL_PATH;
398
- }
399
- return null;
400
- }
401
-
402
- async function exportDarwinResources(targetDir: string, skipVerify: boolean): Promise<void> {
403
- console.log("\n=== Exporting macOS (Darwin) Resources ===\n");
404
-
405
- const homebrewPath = getHomebrewPath();
406
- if (!homebrewPath) {
407
- console.error("Error: Homebrew libimobiledevice not found.");
408
- console.error("Please install it first: brew install libimobiledevice ideviceinstaller");
409
- process.exit(1);
410
- }
411
-
412
- console.log(`Found Homebrew at: ${homebrewPath}`);
413
-
414
- const darwinBinDir = path.join(targetDir, "bin", "darwin");
415
- mkdirSync(darwinBinDir, { recursive: true });
416
-
417
- // =========================================================================
418
- // Step 1: Copy all required and optional tools
419
- // =========================================================================
420
- console.log("\n--- Step 1: Copying binaries ---");
421
-
422
- const binaryPaths: string[] = [];
423
- const copiedBinaries: string[] = [];
424
-
425
- // Copy required tools
426
- for (const tool of REQUIRED_TOOLS) {
427
- const toolPath = path.join(homebrewPath, "bin", tool);
428
- if (existsSync(toolPath)) {
429
- const realPath = resolveSymlink(toolPath);
430
- binaryPaths.push(realPath);
431
-
432
- const destPath = path.join(darwinBinDir, tool);
433
- copyFileSync(realPath, destPath);
434
- chmodSync(destPath, 0o755);
435
- copiedBinaries.push(destPath);
436
- console.log(` ✓ Copied ${tool}`);
437
- } else {
438
- console.error(` ✗ Required tool not found: ${tool}`);
439
- console.error(" Please install: brew install libimobiledevice ideviceinstaller");
440
- process.exit(1);
441
- }
442
- }
443
-
444
- // Copy optional tools
445
- for (const tool of OPTIONAL_TOOLS) {
446
- const toolPath = path.join(homebrewPath, "bin", tool);
447
- if (existsSync(toolPath)) {
448
- const realPath = resolveSymlink(toolPath);
449
- binaryPaths.push(realPath);
450
-
451
- const destPath = path.join(darwinBinDir, tool);
452
- copyFileSync(realPath, destPath);
453
- chmodSync(destPath, 0o755);
454
- copiedBinaries.push(destPath);
455
- console.log(` ✓ Copied ${tool} (optional)`);
456
- } else {
457
- console.log(` - Skipping ${tool} (optional, not installed)`);
458
- }
459
- }
460
-
461
- // =========================================================================
462
- // Step 2: Recursively collect ALL dylib dependencies
463
- // =========================================================================
464
- console.log("\n--- Step 2: Collecting dependencies ---");
465
-
466
- const allDylibs = await collectAllDependencies(binaryPaths);
467
-
468
- // =========================================================================
469
- // Step 3: Copy all dylibs
470
- // =========================================================================
471
- console.log("\n--- Step 3: Copying dylibs ---");
472
-
473
- const copiedDylibs: string[] = [];
474
- const allDylibNames = new Set<string>();
475
-
476
- for (const [dylibName, dep] of allDylibs) {
477
- allDylibNames.add(dylibName);
478
- const destPath = path.join(darwinBinDir, dylibName);
479
-
480
- if (existsSync(dep.realPath)) {
481
- copyFileSync(dep.realPath, destPath);
482
- chmodSync(destPath, 0o755);
483
- copiedDylibs.push(destPath);
484
- console.log(` ✓ Copied ${dylibName}`);
485
- } else {
486
- console.warn(` ✗ Warning: Dylib not found: ${dep.realPath}`);
487
- }
488
- }
489
-
490
- // =========================================================================
491
- // Step 4: Fix library paths in all copied files
492
- // =========================================================================
493
- console.log("\n--- Step 4: Fixing library paths ---");
494
-
495
- const allCopiedFiles = [...copiedBinaries, ...copiedDylibs];
496
- const unfixedPaths: Map<string, string[]> = new Map();
497
-
498
- // First, fix dylib IDs
499
- for (const dylibPath of copiedDylibs) {
500
- await fixDylibId(dylibPath);
501
- }
502
-
503
- // Then, fix all library references
504
- for (const filePath of allCopiedFiles) {
505
- const unfixed = await fixAllLibraryPaths(filePath, allDylibNames);
506
- if (unfixed.length > 0) {
507
- unfixedPaths.set(path.basename(filePath), unfixed);
508
- }
509
- console.log(` ✓ Fixed paths in ${path.basename(filePath)}`);
510
- }
511
-
512
- // Report any unfixed paths
513
- if (unfixedPaths.size > 0) {
514
- console.log("\n ⚠ Warning: Some library paths could not be fixed:");
515
- for (const [file, paths] of unfixedPaths) {
516
- for (const p of paths) {
517
- console.log(` ${file}: ${p}`);
518
- }
519
- }
520
- }
521
-
522
- // =========================================================================
523
- // Step 5: Code sign all binaries (required on modern macOS)
524
- // =========================================================================
525
- console.log("\n--- Step 5: Code signing ---");
526
-
527
- let signedCount = 0;
528
- for (const filePath of allCopiedFiles) {
529
- const success = await codesignBinary(filePath);
530
- if (success) {
531
- signedCount++;
532
- }
533
- }
534
- console.log(` ✓ Signed ${signedCount}/${allCopiedFiles.length} files`);
535
-
536
- // =========================================================================
537
- // Step 6: Verify binaries work
538
- // =========================================================================
539
- if (!skipVerify) {
540
- console.log("\n--- Step 6: Verifying binaries ---");
541
-
542
- let verifyErrors = 0;
543
- for (const binaryPath of copiedBinaries) {
544
- const result = await verifyBinary(binaryPath, darwinBinDir);
545
- if (result.success) {
546
- console.log(` ✓ ${path.basename(binaryPath)} OK`);
547
- } else {
548
- console.log(` ✗ ${path.basename(binaryPath)} FAILED: ${result.error}`);
549
- verifyErrors++;
550
- }
551
- }
552
-
553
- // Also verify dylibs have valid paths
554
- for (const dylibPath of copiedDylibs) {
555
- const result = await verifyBinary(dylibPath, darwinBinDir);
556
- if (!result.success) {
557
- console.log(` ✗ ${path.basename(dylibPath)} FAILED: ${result.error}`);
558
- verifyErrors++;
559
- }
560
- }
561
-
562
- if (verifyErrors > 0) {
563
- console.log(`\n ⚠ ${verifyErrors} verification errors found`);
564
- console.log(" Run with DYLD_PRINT_LIBRARIES=1 to debug library loading issues");
565
- } else {
566
- console.log("\n ✓ All binaries verified successfully");
567
- }
568
- } else {
569
- console.log("\n--- Step 6: Verification skipped ---");
570
- }
571
-
572
- console.log(`\n✓ macOS resources exported to: ${darwinBinDir}`);
573
- }
574
-
575
- // ============================================================================
576
- // Windows Export
577
- // ============================================================================
578
-
579
- /**
580
- * Get the path to the pre-built Windows resources archive
581
- */
582
- function getPrebuiltWindowsArchivePath(): string {
583
- return path.join(PREBUILT_RESOURCES_DIR, `libimobiledevice-windows-v${LIBIMOBILEDEVICE_VERSION}.tar.gz`);
584
- }
585
-
586
- /**
587
- * Check if pre-built Windows resources exist
588
- */
589
- function hasPrebuiltWindowsResources(): boolean {
590
- const archivePath = getPrebuiltWindowsArchivePath();
591
- return existsSync(archivePath);
592
- }
593
-
594
- /**
595
- * Extract pre-built Windows resources to target directory
596
- */
597
- async function extractPrebuiltWindowsResources(targetDir: string): Promise<void> {
598
- const archivePath = getPrebuiltWindowsArchivePath();
599
- const windowsBinDir = path.join(targetDir, "bin", "windows");
600
-
601
- console.log(`Extracting pre-built resources from: ${path.basename(archivePath)}`);
602
- mkdirSync(windowsBinDir, { recursive: true });
603
-
604
- // Use tar command to extract (available on Windows 10+ and via MSYS2)
605
- try {
606
- await execAsync(`tar -xzf "${archivePath}" -C "${windowsBinDir}"`);
607
- console.log(`✓ Extracted pre-built Windows resources to: ${windowsBinDir}`);
608
- } catch (error) {
609
- throw new Error(`Failed to extract pre-built resources: ${error}`);
610
- }
611
- }
612
-
613
- /**
614
- * Create a tar.gz archive of the built Windows resources
615
- */
616
- async function createWindowsResourcesArchive(targetDir: string): Promise<void> {
617
- const windowsBinDir = path.join(targetDir, "bin", "windows");
618
- const archivePath = getPrebuiltWindowsArchivePath();
619
-
620
- // Ensure prebuilt directory exists
621
- mkdirSync(PREBUILT_RESOURCES_DIR, { recursive: true });
622
-
623
- console.log(`\nCreating pre-built resources archive: ${path.basename(archivePath)}`);
624
-
625
- // Get list of files in windows bin directory
626
- const files = readdirSync(windowsBinDir);
627
- if (files.length === 0) {
628
- console.warn("Warning: No files to archive in Windows bin directory");
629
- return;
630
- }
631
-
632
- // Use tar command to create archive
633
- try {
634
- await execAsync(`tar -czf "${archivePath}" -C "${windowsBinDir}" .`);
635
- console.log(`✓ Created pre-built resources archive: ${archivePath}`);
636
- } catch (error) {
637
- console.warn(`Warning: Failed to create archive: ${error}`);
638
- }
639
- }
640
-
641
- /**
642
- * Clean up build artifacts (build script and limd-build folder)
643
- */
644
- function cleanupWindowsBuildArtifacts(targetDir: string): void {
645
- const scriptPath = path.join(targetDir, "build-windows.sh");
646
- const buildDir = path.join(targetDir, "limd-build");
647
-
648
- console.log("\nCleaning up build artifacts...");
649
-
650
- // Remove build script
651
- if (existsSync(scriptPath)) {
652
- unlinkSync(scriptPath);
653
- console.log(" ✓ Removed build-windows.sh");
654
- }
655
-
656
- // Remove limd-build directory
657
- if (existsSync(buildDir)) {
658
- rmSync(buildDir, { recursive: true, force: true });
659
- console.log(" ✓ Removed limd-build directory");
660
- }
661
- }
662
-
663
- async function exportWindowsResources(targetDir: string): Promise<void> {
664
- console.log("\n=== Exporting Windows Resources ===\n");
665
-
666
- // Check if we're on Windows
667
- if (process.platform !== "win32") {
668
- // On non-Windows, check for pre-built resources first
669
- if (hasPrebuiltWindowsResources()) {
670
- console.log("Found pre-built Windows resources, extracting...");
671
- await extractPrebuiltWindowsResources(targetDir);
672
- return;
673
- }
674
-
675
- console.log("Note: Windows resources can only be built on Windows.");
676
- console.log("Generating build script for later use...\n");
677
- generateWindowsBuildScript(targetDir);
678
- return;
679
- }
680
-
681
- // On Windows, check for pre-built resources first
682
- if (hasPrebuiltWindowsResources()) {
683
- console.log("Found pre-built Windows resources, extracting...");
684
- await extractPrebuiltWindowsResources(targetDir);
685
- return;
686
- }
687
-
688
- console.log("No pre-built resources found, building from source...");
689
-
690
- // Check for MSYS2
691
- const msys2Path = process.env.MSYS2_PATH || MSYS2_DEFAULT_PATH;
692
- if (!existsSync(msys2Path)) {
693
- console.error(`Error: MSYS2 not found at ${msys2Path}`);
694
- console.error("Please install MSYS2 from https://www.msys2.org/");
695
- console.error("Or set MSYS2_PATH environment variable to your MSYS2 installation.");
696
- generateWindowsBuildScript(targetDir);
697
- return;
698
- }
699
-
700
- // Generate the build script
701
- generateWindowsBuildScript(targetDir);
702
-
703
- // Run the build script through MSYS2's bash
704
- const scriptPath = path.join(targetDir, "build-windows.sh");
705
- await runMsys2Build(msys2Path, scriptPath);
706
-
707
- // Clean up build artifacts
708
- cleanupWindowsBuildArtifacts(targetDir);
709
-
710
- // Create archive for future use
711
- await createWindowsResourcesArchive(targetDir);
712
- }
713
-
714
- async function runMsys2Build(msys2Path: string, scriptPath: string): Promise<void> {
715
- console.log("\nRunning Windows build through MSYS2...");
716
- console.log("(This may take 10-30 minutes depending on your system)\n");
717
-
718
- const envPath = path.join(msys2Path, "usr", "bin", "env.exe");
719
- const bashPath = path.join(msys2Path, "usr", "bin", "bash.exe");
720
-
721
- if (!existsSync(envPath)) {
722
- console.error(`Error: MSYS2 env not found at ${envPath}`);
723
- console.error("Please ensure MSYS2 is properly installed.");
724
- process.exit(1);
725
- }
726
-
727
- if (!existsSync(bashPath)) {
728
- console.error(`Error: MSYS2 bash not found at ${bashPath}`);
729
- console.error("Please ensure MSYS2 is properly installed.");
730
- process.exit(1);
731
- }
732
-
733
- // Convert Windows path to MSYS2-compatible path
734
- const msysScriptPath = scriptPath.replace(/\\/g, "/").replace(/^([A-Z]):/, (_, drive) => `/${drive.toLowerCase()}`);
735
-
736
- return new Promise((resolve, reject) => {
737
- // Use MSYS2's env.exe to properly set MSYSTEM for the MinGW64 environment
738
- // This ensures the PATH and other environment variables are set correctly
739
- const child = spawn(envPath, [
740
- "MSYSTEM=MINGW64",
741
- "CHERE_INVOKING=1",
742
- bashPath,
743
- "-l",
744
- "-c",
745
- `bash "${msysScriptPath}"`,
746
- ], {
747
- stdio: "inherit",
748
- });
749
-
750
- child.on("error", (err) => {
751
- console.error(`Failed to start MSYS2 build: ${err.message}`);
752
- reject(err);
753
- });
754
-
755
- child.on("close", (code) => {
756
- if (code === 0) {
757
- console.log("\n✓ Windows build completed successfully");
758
- resolve();
759
- } else {
760
- console.error(`\n Windows build failed with exit code ${code}`);
761
- reject(new Error(`Build failed with exit code ${code}`));
762
- }
763
- });
764
- });
765
- }
766
-
767
- function generateWindowsBuildScript(targetDir: string): void {
768
- const windowsBinDir = path.join(targetDir, "bin", "windows");
769
- mkdirSync(windowsBinDir, { recursive: true });
770
-
771
- // Read the template file
772
- const templatePath = path.join(__dirname, "build-windows.sh.template");
773
- let buildScript = readFileSync(templatePath, "utf-8");
774
-
775
- // Replace the placeholder with the actual target directory
776
- buildScript = buildScript.replace("{{TARGET_DIR}}", windowsBinDir.replace(/\\/g, "/"));
777
-
778
- const scriptPath = path.join(targetDir, "build-windows.sh");
779
- writeFileSync(scriptPath, buildScript);
780
- console.log(`✓ Generated Windows build script: ${scriptPath}`);
781
- }
782
-
783
- // ============================================================================
784
- // License File
785
- // ============================================================================
786
-
787
- function createLicenseFile(targetDir: string): void {
788
- const licensesDir = path.join(targetDir, "licenses");
789
- mkdirSync(licensesDir, { recursive: true });
790
-
791
- const lgplText = `GNU LESSER GENERAL PUBLIC LICENSE
792
- Version 2.1, February 1999
793
-
794
- libimobiledevice and related tools are licensed under the
795
- GNU Lesser General Public License version 2.1.
796
-
797
- For full license text, see:
798
- https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
799
-
800
- These binaries are bundled for convenience. The source code is available at:
801
- - https://github.com/libimobiledevice/libimobiledevice
802
- - https://github.com/libimobiledevice/libusbmuxd
803
- - https://github.com/libimobiledevice/libplist
804
- - https://github.com/libimobiledevice/libimobiledevice-glue
805
- - https://github.com/libimobiledevice/ideviceinstaller
806
- `;
807
-
808
- const licensePath = path.join(licensesDir, "LGPL-2.1.txt");
809
- writeFileSync(licensePath, lgplText);
810
- console.log(`✓ Created license file: ${licensePath}`);
811
- }
812
-
813
- // ============================================================================
814
- // Main
815
- // ============================================================================
816
-
817
- async function main(): Promise<void> {
818
- const args = process.argv.slice(2);
819
-
820
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
821
- printUsage();
822
- process.exit(args.length === 0 ? 1 : 0);
823
- }
824
-
825
- const skipVerify = args.includes("--skip-verify");
826
- const targetPath = path.resolve(args.find((a) => !a.startsWith("--")) || ".");
827
-
828
- console.log("=== Apple-kit Resources Export ===");
829
- console.log(`Platform: ${process.platform}`);
830
- console.log(`Target: ${targetPath}`);
831
-
832
- // Create target directory
833
- mkdirSync(targetPath, { recursive: true });
834
-
835
- // Export based on platform
836
- if (process.platform === "darwin") {
837
- await exportDarwinResources(targetPath, skipVerify);
838
- } else if (process.platform === "win32") {
839
- await exportWindowsResources(targetPath);
840
- } else {
841
- console.log("\nNote: This script supports macOS and Windows.");
842
- console.log("For Linux, install libimobiledevice-utils via your package manager.");
843
- }
844
-
845
- // Always create license file
846
- console.log("");
847
- createLicenseFile(targetPath);
848
-
849
- console.log("\n=== Export Complete ===");
850
- console.log(`Resources exported to: ${targetPath}`);
851
- }
852
-
853
- main().catch((error) => {
854
- console.error("Export failed:", error);
855
- process.exit(1);
856
- });
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Export libimobiledevice resources to a specified path
4
+ *
5
+ * This script exports the libimobiledevice binaries and their dependencies
6
+ * to a target directory. The resources can then be used by applications
7
+ * that depend on apple-kit.
8
+ *
9
+ * Usage:
10
+ * npx tsx export-resources.ts <target-path>
11
+ *
12
+ * Example:
13
+ * npx tsx export-resources.ts /path/to/my-app/resources/apple-kit
14
+ *
15
+ * Platform support:
16
+ * - macOS: Copies binaries from Homebrew installation
17
+ * - Windows: Builds from source using MSYS2/MinGW (requires manual setup)
18
+ */
19
+
20
+ import { exec, spawn } from "node:child_process";
21
+ import {
22
+ chmodSync,
23
+ copyFileSync,
24
+ existsSync,
25
+ mkdirSync,
26
+ readFileSync,
27
+ readdirSync,
28
+ readlinkSync,
29
+ rmSync,
30
+ unlinkSync,
31
+ writeFileSync,
32
+ } from "node:fs";
33
+ import path from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+ import { promisify } from "node:util";
36
+
37
+ // Get the directory of this script
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = path.dirname(__filename);
40
+
41
+ const execAsync = promisify(exec);
42
+
43
+ // ============================================================================
44
+ // Configuration
45
+ // ============================================================================
46
+
47
+ // Tools we need to bundle
48
+ const REQUIRED_TOOLS = [
49
+ "idevice_id",
50
+ "ideviceinfo",
51
+ "idevicepair",
52
+ "idevicename",
53
+ "idevicedebug",
54
+ "ideviceinstaller",
55
+ "iproxy",
56
+ ];
57
+
58
+ // Optional tools (won't fail if not found)
59
+ const OPTIONAL_TOOLS = [
60
+ "ideviceactivation",
61
+ "idevicesyslog",
62
+ "idevicescreenshot",
63
+ "idevicediagnostics",
64
+ "idevicerestore", // For device erase/restore functionality
65
+ ];
66
+
67
+ // Homebrew paths on macOS
68
+ const HOMEBREW_ARM_PATH = "/opt/homebrew";
69
+ const HOMEBREW_INTEL_PATH = "/usr/local";
70
+
71
+ // MSYS2 paths on Windows
72
+ const MSYS2_DEFAULT_PATH = "C:\\msys64";
73
+
74
+ // Pre-built resources configuration
75
+ const PREBUILT_RESOURCES_DIR = path.join(__dirname, "..", "prebuilt");
76
+ const LIBIMOBILEDEVICE_VERSION = "1.0.0";
77
+
78
+ // System library prefixes that should NOT be copied (they exist on all macOS)
79
+ const SYSTEM_LIB_PREFIXES = ["/System/Library/", "/usr/lib/", "/Library/Apple/"];
80
+
81
+ // ============================================================================
82
+ // Utility Functions
83
+ // ============================================================================
84
+
85
+ function printUsage(): void {
86
+ console.log(`
87
+ Usage: npx tsx export-resources.ts <target-path>
88
+
89
+ Arguments:
90
+ target-path Directory where resources will be exported
91
+
92
+ Options:
93
+ --skip-verify Skip verification step
94
+
95
+ Examples:
96
+ npx tsx export-resources.ts ./my-app/resources/apple-kit
97
+ npx tsx export-resources.ts /absolute/path/to/resources
98
+
99
+ The script will create the following structure:
100
+ <target-path>/
101
+ bin/
102
+ darwin/ (macOS binaries and dylibs)
103
+ windows/ (Windows binaries and DLLs)
104
+ licenses/
105
+ LGPL-2.1.txt
106
+ `);
107
+ }
108
+
109
+ function resolveSymlink(filePath: string): string {
110
+ try {
111
+ const linkTarget = readlinkSync(filePath);
112
+ if (path.isAbsolute(linkTarget)) {
113
+ return linkTarget;
114
+ }
115
+ return path.resolve(path.dirname(filePath), linkTarget);
116
+ } catch {
117
+ return filePath; // Not a symlink
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Check if a library path is a system library that doesn't need to be copied
123
+ */
124
+ function isSystemLibrary(libPath: string): boolean {
125
+ return SYSTEM_LIB_PREFIXES.some((prefix) => libPath.startsWith(prefix));
126
+ }
127
+
128
+ /**
129
+ * Check if a library path needs to be bundled (is from homebrew or similar)
130
+ */
131
+ function needsBundling(libPath: string): boolean {
132
+ if (isSystemLibrary(libPath)) {
133
+ return false;
134
+ }
135
+ if (libPath.startsWith("@")) {
136
+ // Already a relocatable path
137
+ return false;
138
+ }
139
+ // Bundle anything from /opt/homebrew, /usr/local/opt, or /usr/local/Cellar
140
+ return (
141
+ libPath.includes("/opt/homebrew") ||
142
+ libPath.includes("/usr/local/opt") ||
143
+ libPath.includes("/usr/local/Cellar") ||
144
+ libPath.includes("/usr/local/lib")
145
+ );
146
+ }
147
+
148
+ interface DylibDependency {
149
+ originalPath: string;
150
+ name: string;
151
+ realPath: string;
152
+ }
153
+
154
+ /**
155
+ * Get all dylib dependencies for a binary, including the original path as referenced
156
+ */
157
+ async function getDylibDependencies(binaryPath: string): Promise<DylibDependency[]> {
158
+ const dylibs: DylibDependency[] = [];
159
+
160
+ try {
161
+ const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
162
+ const lines = stdout.split("\n");
163
+
164
+ for (const line of lines) {
165
+ // Match library paths (with or without version info in parentheses)
166
+ const match = line.match(/^\s+(.+?)\s+\(/);
167
+ if (match) {
168
+ const libPath = match[1].trim();
169
+ if (needsBundling(libPath)) {
170
+ const realPath = resolveSymlink(libPath);
171
+ dylibs.push({
172
+ originalPath: libPath,
173
+ name: path.basename(libPath),
174
+ realPath: existsSync(realPath) ? realPath : libPath,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ } catch (error) {
180
+ console.warn(`Warning: Could not get dependencies for ${binaryPath}: ${error}`);
181
+ }
182
+
183
+ return dylibs;
184
+ }
185
+
186
+ /**
187
+ * Recursively collect ALL dylib dependencies
188
+ * Continues until no new dependencies are found
189
+ */
190
+ async function collectAllDependencies(
191
+ initialBinaries: string[]
192
+ ): Promise<Map<string, DylibDependency>> {
193
+ const allDylibs = new Map<string, DylibDependency>();
194
+ const processedPaths = new Set<string>();
195
+ const toProcess: string[] = [...initialBinaries];
196
+
197
+ console.log(" Scanning for dependencies...");
198
+
199
+ let iteration = 0;
200
+ while (toProcess.length > 0) {
201
+ iteration++;
202
+ const currentPath = toProcess.pop();
203
+ if (!currentPath) {
204
+ continue;
205
+ }
206
+
207
+ if (processedPaths.has(currentPath)) {
208
+ continue;
209
+ }
210
+ processedPaths.add(currentPath);
211
+
212
+ const deps = await getDylibDependencies(currentPath);
213
+
214
+ for (const dep of deps) {
215
+ if (!allDylibs.has(dep.name)) {
216
+ allDylibs.set(dep.name, dep);
217
+ // Add this dylib to be processed for its own dependencies
218
+ if (existsSync(dep.realPath)) {
219
+ toProcess.push(dep.realPath);
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ console.log(` Found ${allDylibs.size} unique dylib dependencies (${iteration} files scanned)`);
226
+ return allDylibs;
227
+ }
228
+
229
+ /**
230
+ * Fix all library paths in a binary to use @loader_path
231
+ * This rewrites ALL non-system library references
232
+ */
233
+ async function fixAllLibraryPaths(
234
+ binaryPath: string,
235
+ allDylibNames: Set<string>
236
+ ): Promise<string[]> {
237
+ const unfixedPaths: string[] = [];
238
+
239
+ try {
240
+ const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
241
+ const lines = stdout.split("\n");
242
+
243
+ for (const line of lines) {
244
+ const match = line.match(/^\s+(.+?)\s+\(/);
245
+ if (match) {
246
+ const libPath = match[1].trim();
247
+
248
+ // Skip system libraries and already-fixed paths
249
+ if (isSystemLibrary(libPath) || libPath.startsWith("@")) {
250
+ continue;
251
+ }
252
+
253
+ const libName = path.basename(libPath);
254
+
255
+ // Check if we have this dylib in our collection
256
+ if (allDylibNames.has(libName)) {
257
+ try {
258
+ await execAsync(
259
+ `install_name_tool -change "${libPath}" "@loader_path/${libName}" "${binaryPath}"`
260
+ );
261
+ } catch (_error) {
262
+ console.warn(` Warning: Could not fix path ${libPath} in ${path.basename(binaryPath)}`);
263
+ unfixedPaths.push(libPath);
264
+ }
265
+ } else {
266
+ // This is a non-system library we don't have - this is a problem
267
+ unfixedPaths.push(libPath);
268
+ }
269
+ }
270
+ }
271
+ } catch (error) {
272
+ console.warn(`Warning: Could not process ${binaryPath}: ${error}`);
273
+ }
274
+
275
+ return unfixedPaths;
276
+ }
277
+
278
+ /**
279
+ * Fix the install name (ID) of a dylib to use @loader_path
280
+ */
281
+ async function fixDylibId(dylibPath: string): Promise<void> {
282
+ const dylibName = path.basename(dylibPath);
283
+ try {
284
+ await execAsync(`install_name_tool -id "@loader_path/${dylibName}" "${dylibPath}"`);
285
+ } catch {
286
+ console.warn(` Warning: Could not fix dylib ID for ${dylibName}`);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Ad-hoc code sign a binary (required after modifying with install_name_tool on modern macOS)
292
+ */
293
+ async function codesignBinary(binaryPath: string): Promise<boolean> {
294
+ try {
295
+ await execAsync(`codesign --force --sign - "${binaryPath}"`);
296
+ return true;
297
+ } catch (error) {
298
+ console.warn(` Warning: Could not code sign ${path.basename(binaryPath)}: ${error}`);
299
+ return false;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Verify that a binary can be loaded and executed
305
+ */
306
+ async function verifyBinary(
307
+ binaryPath: string,
308
+ binDir: string
309
+ ): Promise<{ success: boolean; error?: string }> {
310
+ const binaryName = path.basename(binaryPath);
311
+
312
+ // Check that otool shows no broken paths
313
+ try {
314
+ const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
315
+ const lines = stdout.split("\n");
316
+
317
+ for (const line of lines) {
318
+ const match = line.match(/^\s+(.+?)\s+\(/);
319
+ if (match) {
320
+ const libPath = match[1].trim();
321
+
322
+ // Skip system libraries
323
+ if (isSystemLibrary(libPath)) {
324
+ continue;
325
+ }
326
+
327
+ // If it's an @loader_path reference, check the file exists
328
+ if (libPath.startsWith("@loader_path/")) {
329
+ const libName = libPath.replace("@loader_path/", "");
330
+ const fullPath = path.join(binDir, libName);
331
+ if (!existsSync(fullPath)) {
332
+ return { success: false, error: `Missing library: ${libName}` };
333
+ }
334
+ } else if (!libPath.startsWith("@")) {
335
+ // Absolute path that's not a system library - this is a problem
336
+ return { success: false, error: `Unfixed absolute path: ${libPath}` };
337
+ }
338
+ }
339
+ }
340
+ } catch (error) {
341
+ return { success: false, error: `otool failed: ${error}` };
342
+ }
343
+
344
+ // For executable binaries (not dylibs), try to run them with --help or -h
345
+ if (!binaryName.endsWith(".dylib")) {
346
+ try {
347
+ // Use spawn with a timeout to test if the binary loads
348
+ const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
349
+ const child = spawn(binaryPath, ["--help"], {
350
+ timeout: 5000,
351
+ env: { ...process.env, DYLD_LIBRARY_PATH: binDir },
352
+ });
353
+
354
+ let stderr = "";
355
+ child.stderr?.on("data", (data) => {
356
+ stderr += data.toString();
357
+ });
358
+
359
+ child.on("error", (err) => {
360
+ resolve({ success: false, error: `Spawn error: ${err.message}` });
361
+ });
362
+
363
+ child.on("close", (code) => {
364
+ // Exit codes 0, 1, or 2 are acceptable (0=success, 1=error, 2=usage)
365
+ // What we're checking is that the binary LOADS, not that --help works
366
+ if (code !== null && code <= 2) {
367
+ resolve({ success: true });
368
+ } else if (stderr.includes("Library not loaded") || stderr.includes("image not found")) {
369
+ resolve({ success: false, error: stderr.split("\n")[0] });
370
+ } else {
371
+ // Other exit codes might be fine too
372
+ resolve({ success: true });
373
+ }
374
+ });
375
+ });
376
+
377
+ if (!result.success) {
378
+ return result;
379
+ }
380
+ } catch (error) {
381
+ // If spawn fails entirely, that's a problem
382
+ return { success: false, error: `Execution test failed: ${error}` };
383
+ }
384
+ }
385
+
386
+ return { success: true };
387
+ }
388
+
389
+ // ============================================================================
390
+ // macOS Export
391
+ // ============================================================================
392
+
393
+ function getHomebrewPath(): string | null {
394
+ if (existsSync(path.join(HOMEBREW_ARM_PATH, "bin", "idevice_id"))) {
395
+ return HOMEBREW_ARM_PATH;
396
+ }
397
+ if (existsSync(path.join(HOMEBREW_INTEL_PATH, "bin", "idevice_id"))) {
398
+ return HOMEBREW_INTEL_PATH;
399
+ }
400
+ return null;
401
+ }
402
+
403
+ async function exportDarwinResources(targetDir: string, skipVerify: boolean): Promise<void> {
404
+ console.log("\n=== Exporting macOS (Darwin) Resources ===\n");
405
+
406
+ const homebrewPath = getHomebrewPath();
407
+ if (!homebrewPath) {
408
+ console.error("Error: Homebrew libimobiledevice not found.");
409
+ console.error("Please install it first: brew install libimobiledevice ideviceinstaller");
410
+ process.exit(1);
411
+ }
412
+
413
+ console.log(`Found Homebrew at: ${homebrewPath}`);
414
+
415
+ const darwinBinDir = path.join(targetDir, "bin", "darwin");
416
+ mkdirSync(darwinBinDir, { recursive: true });
417
+
418
+ // =========================================================================
419
+ // Step 1: Copy all required and optional tools
420
+ // =========================================================================
421
+ console.log("\n--- Step 1: Copying binaries ---");
422
+
423
+ const binaryPaths: string[] = [];
424
+ const copiedBinaries: string[] = [];
425
+
426
+ // Copy required tools
427
+ for (const tool of REQUIRED_TOOLS) {
428
+ const toolPath = path.join(homebrewPath, "bin", tool);
429
+ if (existsSync(toolPath)) {
430
+ const realPath = resolveSymlink(toolPath);
431
+ binaryPaths.push(realPath);
432
+
433
+ const destPath = path.join(darwinBinDir, tool);
434
+ copyFileSync(realPath, destPath);
435
+ chmodSync(destPath, 0o755);
436
+ copiedBinaries.push(destPath);
437
+ console.log(` ✓ Copied ${tool}`);
438
+ } else {
439
+ console.error(` ✗ Required tool not found: ${tool}`);
440
+ console.error(" Please install: brew install libimobiledevice ideviceinstaller");
441
+ process.exit(1);
442
+ }
443
+ }
444
+
445
+ // Copy optional tools
446
+ for (const tool of OPTIONAL_TOOLS) {
447
+ const toolPath = path.join(homebrewPath, "bin", tool);
448
+ if (existsSync(toolPath)) {
449
+ const realPath = resolveSymlink(toolPath);
450
+ binaryPaths.push(realPath);
451
+
452
+ const destPath = path.join(darwinBinDir, tool);
453
+ copyFileSync(realPath, destPath);
454
+ chmodSync(destPath, 0o755);
455
+ copiedBinaries.push(destPath);
456
+ console.log(` ✓ Copied ${tool} (optional)`);
457
+ } else {
458
+ console.log(` - Skipping ${tool} (optional, not installed)`);
459
+ }
460
+ }
461
+
462
+ // =========================================================================
463
+ // Step 2: Recursively collect ALL dylib dependencies
464
+ // =========================================================================
465
+ console.log("\n--- Step 2: Collecting dependencies ---");
466
+
467
+ const allDylibs = await collectAllDependencies(binaryPaths);
468
+
469
+ // =========================================================================
470
+ // Step 3: Copy all dylibs
471
+ // =========================================================================
472
+ console.log("\n--- Step 3: Copying dylibs ---");
473
+
474
+ const copiedDylibs: string[] = [];
475
+ const allDylibNames = new Set<string>();
476
+
477
+ for (const [dylibName, dep] of allDylibs) {
478
+ allDylibNames.add(dylibName);
479
+ const destPath = path.join(darwinBinDir, dylibName);
480
+
481
+ if (existsSync(dep.realPath)) {
482
+ copyFileSync(dep.realPath, destPath);
483
+ chmodSync(destPath, 0o755);
484
+ copiedDylibs.push(destPath);
485
+ console.log(` ✓ Copied ${dylibName}`);
486
+ } else {
487
+ console.warn(` ✗ Warning: Dylib not found: ${dep.realPath}`);
488
+ }
489
+ }
490
+
491
+ // =========================================================================
492
+ // Step 4: Fix library paths in all copied files
493
+ // =========================================================================
494
+ console.log("\n--- Step 4: Fixing library paths ---");
495
+
496
+ const allCopiedFiles = [...copiedBinaries, ...copiedDylibs];
497
+ const unfixedPaths: Map<string, string[]> = new Map();
498
+
499
+ // First, fix dylib IDs
500
+ for (const dylibPath of copiedDylibs) {
501
+ await fixDylibId(dylibPath);
502
+ }
503
+
504
+ // Then, fix all library references
505
+ for (const filePath of allCopiedFiles) {
506
+ const unfixed = await fixAllLibraryPaths(filePath, allDylibNames);
507
+ if (unfixed.length > 0) {
508
+ unfixedPaths.set(path.basename(filePath), unfixed);
509
+ }
510
+ console.log(` ✓ Fixed paths in ${path.basename(filePath)}`);
511
+ }
512
+
513
+ // Report any unfixed paths
514
+ if (unfixedPaths.size > 0) {
515
+ console.log("\n ⚠ Warning: Some library paths could not be fixed:");
516
+ for (const [file, paths] of unfixedPaths) {
517
+ for (const p of paths) {
518
+ console.log(` ${file}: ${p}`);
519
+ }
520
+ }
521
+ }
522
+
523
+ // =========================================================================
524
+ // Step 5: Code sign all binaries (required on modern macOS)
525
+ // =========================================================================
526
+ console.log("\n--- Step 5: Code signing ---");
527
+
528
+ let signedCount = 0;
529
+ for (const filePath of allCopiedFiles) {
530
+ const success = await codesignBinary(filePath);
531
+ if (success) {
532
+ signedCount++;
533
+ }
534
+ }
535
+ console.log(` ✓ Signed ${signedCount}/${allCopiedFiles.length} files`);
536
+
537
+ // =========================================================================
538
+ // Step 6: Verify binaries work
539
+ // =========================================================================
540
+ if (!skipVerify) {
541
+ console.log("\n--- Step 6: Verifying binaries ---");
542
+
543
+ let verifyErrors = 0;
544
+ for (const binaryPath of copiedBinaries) {
545
+ const result = await verifyBinary(binaryPath, darwinBinDir);
546
+ if (result.success) {
547
+ console.log(` ✓ ${path.basename(binaryPath)} OK`);
548
+ } else {
549
+ console.log(` ✗ ${path.basename(binaryPath)} FAILED: ${result.error}`);
550
+ verifyErrors++;
551
+ }
552
+ }
553
+
554
+ // Also verify dylibs have valid paths
555
+ for (const dylibPath of copiedDylibs) {
556
+ const result = await verifyBinary(dylibPath, darwinBinDir);
557
+ if (!result.success) {
558
+ console.log(` ✗ ${path.basename(dylibPath)} FAILED: ${result.error}`);
559
+ verifyErrors++;
560
+ }
561
+ }
562
+
563
+ if (verifyErrors > 0) {
564
+ console.log(`\n ${verifyErrors} verification errors found`);
565
+ console.log(" Run with DYLD_PRINT_LIBRARIES=1 to debug library loading issues");
566
+ } else {
567
+ console.log("\n ✓ All binaries verified successfully");
568
+ }
569
+ } else {
570
+ console.log("\n--- Step 6: Verification skipped ---");
571
+ }
572
+
573
+ console.log(`\n✓ macOS resources exported to: ${darwinBinDir}`);
574
+ }
575
+
576
+ // ============================================================================
577
+ // Windows Export
578
+ // ============================================================================
579
+
580
+ /**
581
+ * Get the path to the pre-built Windows resources archive
582
+ */
583
+ function getPrebuiltWindowsArchivePath(): string {
584
+ return path.join(
585
+ PREBUILT_RESOURCES_DIR,
586
+ `libimobiledevice-windows-v${LIBIMOBILEDEVICE_VERSION}.tar.gz`
587
+ );
588
+ }
589
+
590
+ /**
591
+ * Check if pre-built Windows resources exist
592
+ */
593
+ function hasPrebuiltWindowsResources(): boolean {
594
+ const archivePath = getPrebuiltWindowsArchivePath();
595
+ return existsSync(archivePath);
596
+ }
597
+
598
+ /**
599
+ * Extract pre-built Windows resources to target directory
600
+ */
601
+ async function extractPrebuiltWindowsResources(targetDir: string): Promise<void> {
602
+ const archivePath = getPrebuiltWindowsArchivePath();
603
+ const windowsBinDir = path.join(targetDir, "bin", "windows");
604
+
605
+ console.log(`Extracting pre-built resources from: ${path.basename(archivePath)}`);
606
+ mkdirSync(windowsBinDir, { recursive: true });
607
+
608
+ // Use tar command to extract (available on Windows 10+ and via MSYS2)
609
+ try {
610
+ await execAsync(`tar -xzf "${archivePath}" -C "${windowsBinDir}"`);
611
+ console.log(`✓ Extracted pre-built Windows resources to: ${windowsBinDir}`);
612
+ } catch (error) {
613
+ throw new Error(`Failed to extract pre-built resources: ${error}`);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Create a tar.gz archive of the built Windows resources
619
+ */
620
+ async function createWindowsResourcesArchive(targetDir: string): Promise<void> {
621
+ const windowsBinDir = path.join(targetDir, "bin", "windows");
622
+ const archivePath = getPrebuiltWindowsArchivePath();
623
+
624
+ // Ensure prebuilt directory exists
625
+ mkdirSync(PREBUILT_RESOURCES_DIR, { recursive: true });
626
+
627
+ console.log(`\nCreating pre-built resources archive: ${path.basename(archivePath)}`);
628
+
629
+ // Get list of files in windows bin directory
630
+ const files = readdirSync(windowsBinDir);
631
+ if (files.length === 0) {
632
+ console.warn("Warning: No files to archive in Windows bin directory");
633
+ return;
634
+ }
635
+
636
+ // Use tar command to create archive
637
+ try {
638
+ await execAsync(`tar -czf "${archivePath}" -C "${windowsBinDir}" .`);
639
+ console.log(`✓ Created pre-built resources archive: ${archivePath}`);
640
+ } catch (error) {
641
+ console.warn(`Warning: Failed to create archive: ${error}`);
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Clean up build artifacts (build script and limd-build folder)
647
+ */
648
+ function cleanupWindowsBuildArtifacts(targetDir: string): void {
649
+ const scriptPath = path.join(targetDir, "build-windows.sh");
650
+ const buildDir = path.join(targetDir, "limd-build");
651
+
652
+ console.log("\nCleaning up build artifacts...");
653
+
654
+ // Remove build script
655
+ if (existsSync(scriptPath)) {
656
+ unlinkSync(scriptPath);
657
+ console.log(" ✓ Removed build-windows.sh");
658
+ }
659
+
660
+ // Remove limd-build directory
661
+ if (existsSync(buildDir)) {
662
+ rmSync(buildDir, { recursive: true, force: true });
663
+ console.log(" ✓ Removed limd-build directory");
664
+ }
665
+ }
666
+
667
+ async function exportWindowsResources(targetDir: string): Promise<void> {
668
+ console.log("\n=== Exporting Windows Resources ===\n");
669
+
670
+ // Check if we're on Windows
671
+ if (process.platform !== "win32") {
672
+ // On non-Windows, check for pre-built resources first
673
+ if (hasPrebuiltWindowsResources()) {
674
+ console.log("Found pre-built Windows resources, extracting...");
675
+ await extractPrebuiltWindowsResources(targetDir);
676
+ return;
677
+ }
678
+
679
+ console.log("Note: Windows resources can only be built on Windows.");
680
+ console.log("Generating build script for later use...\n");
681
+ generateWindowsBuildScript(targetDir);
682
+ return;
683
+ }
684
+
685
+ // On Windows, check for pre-built resources first
686
+ if (hasPrebuiltWindowsResources()) {
687
+ console.log("Found pre-built Windows resources, extracting...");
688
+ await extractPrebuiltWindowsResources(targetDir);
689
+ return;
690
+ }
691
+
692
+ console.log("No pre-built resources found, building from source...");
693
+
694
+ // Check for MSYS2
695
+ const msys2Path = process.env.MSYS2_PATH || MSYS2_DEFAULT_PATH;
696
+ if (!existsSync(msys2Path)) {
697
+ console.error(`Error: MSYS2 not found at ${msys2Path}`);
698
+ console.error("Please install MSYS2 from https://www.msys2.org/");
699
+ console.error("Or set MSYS2_PATH environment variable to your MSYS2 installation.");
700
+ generateWindowsBuildScript(targetDir);
701
+ return;
702
+ }
703
+
704
+ // Generate the build script
705
+ generateWindowsBuildScript(targetDir);
706
+
707
+ // Run the build script through MSYS2's bash
708
+ const scriptPath = path.join(targetDir, "build-windows.sh");
709
+ await runMsys2Build(msys2Path, scriptPath);
710
+
711
+ // Clean up build artifacts
712
+ cleanupWindowsBuildArtifacts(targetDir);
713
+
714
+ // Create archive for future use
715
+ await createWindowsResourcesArchive(targetDir);
716
+ }
717
+
718
+ async function runMsys2Build(msys2Path: string, scriptPath: string): Promise<void> {
719
+ console.log("\nRunning Windows build through MSYS2...");
720
+ console.log("(This may take 10-30 minutes depending on your system)\n");
721
+
722
+ const envPath = path.join(msys2Path, "usr", "bin", "env.exe");
723
+ const bashPath = path.join(msys2Path, "usr", "bin", "bash.exe");
724
+
725
+ if (!existsSync(envPath)) {
726
+ console.error(`Error: MSYS2 env not found at ${envPath}`);
727
+ console.error("Please ensure MSYS2 is properly installed.");
728
+ process.exit(1);
729
+ }
730
+
731
+ if (!existsSync(bashPath)) {
732
+ console.error(`Error: MSYS2 bash not found at ${bashPath}`);
733
+ console.error("Please ensure MSYS2 is properly installed.");
734
+ process.exit(1);
735
+ }
736
+
737
+ // Convert Windows path to MSYS2-compatible path
738
+ const msysScriptPath = scriptPath
739
+ .replace(/\\/g, "/")
740
+ .replace(/^([A-Z]):/, (_, drive) => `/${drive.toLowerCase()}`);
741
+
742
+ return new Promise((resolve, reject) => {
743
+ // Use MSYS2's env.exe to properly set MSYSTEM for the MinGW64 environment
744
+ // This ensures the PATH and other environment variables are set correctly
745
+ const child = spawn(
746
+ envPath,
747
+ ["MSYSTEM=MINGW64", "CHERE_INVOKING=1", bashPath, "-l", "-c", `bash "${msysScriptPath}"`],
748
+ {
749
+ stdio: "inherit",
750
+ }
751
+ );
752
+
753
+ child.on("error", (err) => {
754
+ console.error(`Failed to start MSYS2 build: ${err.message}`);
755
+ reject(err);
756
+ });
757
+
758
+ child.on("close", (code) => {
759
+ if (code === 0) {
760
+ console.log("\n Windows build completed successfully");
761
+ resolve();
762
+ } else {
763
+ console.error(`\n✗ Windows build failed with exit code ${code}`);
764
+ reject(new Error(`Build failed with exit code ${code}`));
765
+ }
766
+ });
767
+ });
768
+ }
769
+
770
+ function generateWindowsBuildScript(targetDir: string): void {
771
+ const windowsBinDir = path.join(targetDir, "bin", "windows");
772
+ mkdirSync(windowsBinDir, { recursive: true });
773
+
774
+ // Read the template file
775
+ const templatePath = path.join(__dirname, "build-windows.sh.template");
776
+ let buildScript = readFileSync(templatePath, "utf-8");
777
+
778
+ // Replace the placeholder with the actual target directory
779
+ buildScript = buildScript.replace("{{TARGET_DIR}}", windowsBinDir.replace(/\\/g, "/"));
780
+
781
+ const scriptPath = path.join(targetDir, "build-windows.sh");
782
+ writeFileSync(scriptPath, buildScript);
783
+ console.log(`✓ Generated Windows build script: ${scriptPath}`);
784
+ }
785
+
786
+ // ============================================================================
787
+ // License File
788
+ // ============================================================================
789
+
790
+ function createLicenseFile(targetDir: string): void {
791
+ const licensesDir = path.join(targetDir, "licenses");
792
+ mkdirSync(licensesDir, { recursive: true });
793
+
794
+ const lgplText = `GNU LESSER GENERAL PUBLIC LICENSE
795
+ Version 2.1, February 1999
796
+
797
+ libimobiledevice and related tools are licensed under the
798
+ GNU Lesser General Public License version 2.1.
799
+
800
+ For full license text, see:
801
+ https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
802
+
803
+ These binaries are bundled for convenience. The source code is available at:
804
+ - https://github.com/libimobiledevice/libimobiledevice
805
+ - https://github.com/libimobiledevice/libusbmuxd
806
+ - https://github.com/libimobiledevice/libplist
807
+ - https://github.com/libimobiledevice/libimobiledevice-glue
808
+ - https://github.com/libimobiledevice/ideviceinstaller
809
+ `;
810
+
811
+ const licensePath = path.join(licensesDir, "LGPL-2.1.txt");
812
+ writeFileSync(licensePath, lgplText);
813
+ console.log(`✓ Created license file: ${licensePath}`);
814
+ }
815
+
816
+ // ============================================================================
817
+ // Main
818
+ // ============================================================================
819
+
820
+ async function main(): Promise<void> {
821
+ const args = process.argv.slice(2);
822
+
823
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
824
+ printUsage();
825
+ process.exit(args.length === 0 ? 1 : 0);
826
+ }
827
+
828
+ const skipVerify = args.includes("--skip-verify");
829
+ const targetPath = path.resolve(args.find((a) => !a.startsWith("--")) || ".");
830
+
831
+ console.log("=== Apple-kit Resources Export ===");
832
+ console.log(`Platform: ${process.platform}`);
833
+ console.log(`Target: ${targetPath}`);
834
+
835
+ // Create target directory
836
+ mkdirSync(targetPath, { recursive: true });
837
+
838
+ // Export based on platform
839
+ if (process.platform === "darwin") {
840
+ await exportDarwinResources(targetPath, skipVerify);
841
+ } else if (process.platform === "win32") {
842
+ await exportWindowsResources(targetPath);
843
+ } else {
844
+ console.log("\nNote: This script supports macOS and Windows.");
845
+ console.log("For Linux, install libimobiledevice-utils via your package manager.");
846
+ }
847
+
848
+ // Always create license file
849
+ console.log("");
850
+ createLicenseFile(targetPath);
851
+
852
+ console.log("\n=== Export Complete ===");
853
+ console.log(`Resources exported to: ${targetPath}`);
854
+ }
855
+
856
+ main().catch((error) => {
857
+ console.error("Export failed:", error);
858
+ process.exit(1);
859
+ });