@mcesystems/apple-kit 1.0.31 → 1.0.33
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 +284 -284
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types/utils/portManager.d.ts +68 -0
- package/dist/types/utils/portManager.d.ts.map +1 -0
- package/package.json +1 -1
- package/scripts/README.md +209 -209
- package/scripts/build-windows.sh.template +134 -134
- package/scripts/export-resources.mts +690 -690
- package/dist/resources/bin/darwin/idevice_id +0 -0
- package/dist/resources/bin/darwin/idevicedebug +0 -0
- package/dist/resources/bin/darwin/idevicediagnostics +0 -0
- package/dist/resources/bin/darwin/ideviceinfo +0 -0
- package/dist/resources/bin/darwin/ideviceinstaller +0 -0
- package/dist/resources/bin/darwin/idevicename +0 -0
- package/dist/resources/bin/darwin/idevicepair +0 -0
- package/dist/resources/bin/darwin/idevicescreenshot +0 -0
- package/dist/resources/bin/darwin/idevicesyslog +0 -0
- package/dist/resources/bin/darwin/iproxy +0 -0
- package/dist/resources/bin/darwin/libcrypto.3.dylib +0 -0
- package/dist/resources/bin/darwin/libimobiledevice-1.0.6.dylib +0 -0
- package/dist/resources/bin/darwin/libimobiledevice-glue-1.0.0.dylib +0 -0
- package/dist/resources/bin/darwin/liblzma.5.dylib +0 -0
- package/dist/resources/bin/darwin/libplist-2.0.4.dylib +0 -0
- package/dist/resources/bin/darwin/libssl.3.dylib +0 -0
- package/dist/resources/bin/darwin/libusbmuxd-2.0.7.dylib +0 -0
- package/dist/resources/bin/darwin/libzip.5.dylib +0 -0
- package/dist/resources/bin/darwin/libzstd.1.dylib +0 -0
- package/dist/resources/licenses/LGPL-2.1.txt +0 -33
|
@@ -1,690 +1,690 @@
|
|
|
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
|
-
readlinkSync,
|
|
28
|
-
writeFileSync,
|
|
29
|
-
} from "node:fs";
|
|
30
|
-
import path from "node:path";
|
|
31
|
-
import { fileURLToPath } from "node:url";
|
|
32
|
-
import { promisify } from "node:util";
|
|
33
|
-
|
|
34
|
-
// Get the directory of this script
|
|
35
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
-
const __dirname = path.dirname(__filename);
|
|
37
|
-
|
|
38
|
-
const execAsync = promisify(exec);
|
|
39
|
-
|
|
40
|
-
// ============================================================================
|
|
41
|
-
// Configuration
|
|
42
|
-
// ============================================================================
|
|
43
|
-
|
|
44
|
-
// Tools we need to bundle
|
|
45
|
-
const REQUIRED_TOOLS = [
|
|
46
|
-
"idevice_id",
|
|
47
|
-
"ideviceinfo",
|
|
48
|
-
"idevicepair",
|
|
49
|
-
"idevicename",
|
|
50
|
-
"idevicedebug",
|
|
51
|
-
"ideviceinstaller",
|
|
52
|
-
"iproxy",
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Optional tools (won't fail if not found)
|
|
56
|
-
const OPTIONAL_TOOLS = [
|
|
57
|
-
"ideviceactivation",
|
|
58
|
-
"idevicesyslog",
|
|
59
|
-
"idevicescreenshot",
|
|
60
|
-
"idevicediagnostics",
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
// Homebrew paths on macOS
|
|
64
|
-
const HOMEBREW_ARM_PATH = "/opt/homebrew";
|
|
65
|
-
const HOMEBREW_INTEL_PATH = "/usr/local";
|
|
66
|
-
|
|
67
|
-
// MSYS2 paths on Windows
|
|
68
|
-
const MSYS2_DEFAULT_PATH = "C:\\msys64";
|
|
69
|
-
|
|
70
|
-
// System library prefixes that should NOT be copied (they exist on all macOS)
|
|
71
|
-
const SYSTEM_LIB_PREFIXES = ["/System/Library/", "/usr/lib/", "/Library/Apple/"];
|
|
72
|
-
|
|
73
|
-
// ============================================================================
|
|
74
|
-
// Utility Functions
|
|
75
|
-
// ============================================================================
|
|
76
|
-
|
|
77
|
-
function printUsage(): void {
|
|
78
|
-
console.log(`
|
|
79
|
-
Usage: npx tsx export-resources.ts <target-path>
|
|
80
|
-
|
|
81
|
-
Arguments:
|
|
82
|
-
target-path Directory where resources will be exported
|
|
83
|
-
|
|
84
|
-
Options:
|
|
85
|
-
--skip-verify Skip verification step
|
|
86
|
-
|
|
87
|
-
Examples:
|
|
88
|
-
npx tsx export-resources.ts ./my-app/resources/apple-kit
|
|
89
|
-
npx tsx export-resources.ts /absolute/path/to/resources
|
|
90
|
-
|
|
91
|
-
The script will create the following structure:
|
|
92
|
-
<target-path>/
|
|
93
|
-
bin/
|
|
94
|
-
darwin/ (macOS binaries and dylibs)
|
|
95
|
-
windows/ (Windows binaries and DLLs)
|
|
96
|
-
licenses/
|
|
97
|
-
LGPL-2.1.txt
|
|
98
|
-
`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function resolveSymlink(filePath: string): string {
|
|
102
|
-
try {
|
|
103
|
-
const linkTarget = readlinkSync(filePath);
|
|
104
|
-
if (path.isAbsolute(linkTarget)) {
|
|
105
|
-
return linkTarget;
|
|
106
|
-
}
|
|
107
|
-
return path.resolve(path.dirname(filePath), linkTarget);
|
|
108
|
-
} catch {
|
|
109
|
-
return filePath; // Not a symlink
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Check if a library path is a system library that doesn't need to be copied
|
|
115
|
-
*/
|
|
116
|
-
function isSystemLibrary(libPath: string): boolean {
|
|
117
|
-
return SYSTEM_LIB_PREFIXES.some((prefix) => libPath.startsWith(prefix));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Check if a library path needs to be bundled (is from homebrew or similar)
|
|
122
|
-
*/
|
|
123
|
-
function needsBundling(libPath: string): boolean {
|
|
124
|
-
if (isSystemLibrary(libPath)) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
if (libPath.startsWith("@")) {
|
|
128
|
-
// Already a relocatable path
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
// Bundle anything from /opt/homebrew, /usr/local/opt, or /usr/local/Cellar
|
|
132
|
-
return (
|
|
133
|
-
libPath.includes("/opt/homebrew") ||
|
|
134
|
-
libPath.includes("/usr/local/opt") ||
|
|
135
|
-
libPath.includes("/usr/local/Cellar") ||
|
|
136
|
-
libPath.includes("/usr/local/lib")
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
interface DylibDependency {
|
|
141
|
-
originalPath: string;
|
|
142
|
-
name: string;
|
|
143
|
-
realPath: string;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get all dylib dependencies for a binary, including the original path as referenced
|
|
148
|
-
*/
|
|
149
|
-
async function getDylibDependencies(binaryPath: string): Promise<DylibDependency[]> {
|
|
150
|
-
const dylibs: DylibDependency[] = [];
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
154
|
-
const lines = stdout.split("\n");
|
|
155
|
-
|
|
156
|
-
for (const line of lines) {
|
|
157
|
-
// Match library paths (with or without version info in parentheses)
|
|
158
|
-
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
159
|
-
if (match) {
|
|
160
|
-
const libPath = match[1].trim();
|
|
161
|
-
if (needsBundling(libPath)) {
|
|
162
|
-
const realPath = resolveSymlink(libPath);
|
|
163
|
-
dylibs.push({
|
|
164
|
-
originalPath: libPath,
|
|
165
|
-
name: path.basename(libPath),
|
|
166
|
-
realPath: existsSync(realPath) ? realPath : libPath,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.warn(`Warning: Could not get dependencies for ${binaryPath}: ${error}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return dylibs;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Recursively collect ALL dylib dependencies
|
|
180
|
-
* Continues until no new dependencies are found
|
|
181
|
-
*/
|
|
182
|
-
async function collectAllDependencies(
|
|
183
|
-
initialBinaries: string[]
|
|
184
|
-
): Promise<Map<string, DylibDependency>> {
|
|
185
|
-
const allDylibs = new Map<string, DylibDependency>();
|
|
186
|
-
const processedPaths = new Set<string>();
|
|
187
|
-
const toProcess: string[] = [...initialBinaries];
|
|
188
|
-
|
|
189
|
-
console.log(" Scanning for dependencies...");
|
|
190
|
-
|
|
191
|
-
let iteration = 0;
|
|
192
|
-
while (toProcess.length > 0) {
|
|
193
|
-
iteration++;
|
|
194
|
-
const currentPath = toProcess.pop();
|
|
195
|
-
if (!currentPath) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (processedPaths.has(currentPath)) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
processedPaths.add(currentPath);
|
|
203
|
-
|
|
204
|
-
const deps = await getDylibDependencies(currentPath);
|
|
205
|
-
|
|
206
|
-
for (const dep of deps) {
|
|
207
|
-
if (!allDylibs.has(dep.name)) {
|
|
208
|
-
allDylibs.set(dep.name, dep);
|
|
209
|
-
// Add this dylib to be processed for its own dependencies
|
|
210
|
-
if (existsSync(dep.realPath)) {
|
|
211
|
-
toProcess.push(dep.realPath);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
console.log(` Found ${allDylibs.size} unique dylib dependencies (${iteration} files scanned)`);
|
|
218
|
-
return allDylibs;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Fix all library paths in a binary to use @loader_path
|
|
223
|
-
* This rewrites ALL non-system library references
|
|
224
|
-
*/
|
|
225
|
-
async function fixAllLibraryPaths(
|
|
226
|
-
binaryPath: string,
|
|
227
|
-
allDylibNames: Set<string>
|
|
228
|
-
): Promise<string[]> {
|
|
229
|
-
const unfixedPaths: string[] = [];
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
233
|
-
const lines = stdout.split("\n");
|
|
234
|
-
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
237
|
-
if (match) {
|
|
238
|
-
const libPath = match[1].trim();
|
|
239
|
-
|
|
240
|
-
// Skip system libraries and already-fixed paths
|
|
241
|
-
if (isSystemLibrary(libPath) || libPath.startsWith("@")) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const libName = path.basename(libPath);
|
|
246
|
-
|
|
247
|
-
// Check if we have this dylib in our collection
|
|
248
|
-
if (allDylibNames.has(libName)) {
|
|
249
|
-
try {
|
|
250
|
-
await execAsync(
|
|
251
|
-
`install_name_tool -change "${libPath}" "@loader_path/${libName}" "${binaryPath}"`
|
|
252
|
-
);
|
|
253
|
-
} catch (_error) {
|
|
254
|
-
console.warn(` Warning: Could not fix path ${libPath} in ${path.basename(binaryPath)}`);
|
|
255
|
-
unfixedPaths.push(libPath);
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
// This is a non-system library we don't have - this is a problem
|
|
259
|
-
unfixedPaths.push(libPath);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
} catch (error) {
|
|
264
|
-
console.warn(`Warning: Could not process ${binaryPath}: ${error}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return unfixedPaths;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Fix the install name (ID) of a dylib to use @loader_path
|
|
272
|
-
*/
|
|
273
|
-
async function fixDylibId(dylibPath: string): Promise<void> {
|
|
274
|
-
const dylibName = path.basename(dylibPath);
|
|
275
|
-
try {
|
|
276
|
-
await execAsync(`install_name_tool -id "@loader_path/${dylibName}" "${dylibPath}"`);
|
|
277
|
-
} catch {
|
|
278
|
-
console.warn(` Warning: Could not fix dylib ID for ${dylibName}`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Ad-hoc code sign a binary (required after modifying with install_name_tool on modern macOS)
|
|
284
|
-
*/
|
|
285
|
-
async function codesignBinary(binaryPath: string): Promise<boolean> {
|
|
286
|
-
try {
|
|
287
|
-
await execAsync(`codesign --force --sign - "${binaryPath}"`);
|
|
288
|
-
return true;
|
|
289
|
-
} catch (error) {
|
|
290
|
-
console.warn(` Warning: Could not code sign ${path.basename(binaryPath)}: ${error}`);
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Verify that a binary can be loaded and executed
|
|
297
|
-
*/
|
|
298
|
-
async function verifyBinary(
|
|
299
|
-
binaryPath: string,
|
|
300
|
-
binDir: string
|
|
301
|
-
): Promise<{ success: boolean; error?: string }> {
|
|
302
|
-
const binaryName = path.basename(binaryPath);
|
|
303
|
-
|
|
304
|
-
// Check that otool shows no broken paths
|
|
305
|
-
try {
|
|
306
|
-
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
307
|
-
const lines = stdout.split("\n");
|
|
308
|
-
|
|
309
|
-
for (const line of lines) {
|
|
310
|
-
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
311
|
-
if (match) {
|
|
312
|
-
const libPath = match[1].trim();
|
|
313
|
-
|
|
314
|
-
// Skip system libraries
|
|
315
|
-
if (isSystemLibrary(libPath)) {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// If it's an @loader_path reference, check the file exists
|
|
320
|
-
if (libPath.startsWith("@loader_path/")) {
|
|
321
|
-
const libName = libPath.replace("@loader_path/", "");
|
|
322
|
-
const fullPath = path.join(binDir, libName);
|
|
323
|
-
if (!existsSync(fullPath)) {
|
|
324
|
-
return { success: false, error: `Missing library: ${libName}` };
|
|
325
|
-
}
|
|
326
|
-
} else if (!libPath.startsWith("@")) {
|
|
327
|
-
// Absolute path that's not a system library - this is a problem
|
|
328
|
-
return { success: false, error: `Unfixed absolute path: ${libPath}` };
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
} catch (error) {
|
|
333
|
-
return { success: false, error: `otool failed: ${error}` };
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// For executable binaries (not dylibs), try to run them with --help or -h
|
|
337
|
-
if (!binaryName.endsWith(".dylib")) {
|
|
338
|
-
try {
|
|
339
|
-
// Use spawn with a timeout to test if the binary loads
|
|
340
|
-
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
|
|
341
|
-
const child = spawn(binaryPath, ["--help"], {
|
|
342
|
-
timeout: 5000,
|
|
343
|
-
env: { ...process.env, DYLD_LIBRARY_PATH: binDir },
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
let stderr = "";
|
|
347
|
-
child.stderr?.on("data", (data) => {
|
|
348
|
-
stderr += data.toString();
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
child.on("error", (err) => {
|
|
352
|
-
resolve({ success: false, error: `Spawn error: ${err.message}` });
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
child.on("close", (code) => {
|
|
356
|
-
// Exit codes 0, 1, or 2 are acceptable (0=success, 1=error, 2=usage)
|
|
357
|
-
// What we're checking is that the binary LOADS, not that --help works
|
|
358
|
-
if (code !== null && code <= 2) {
|
|
359
|
-
resolve({ success: true });
|
|
360
|
-
} else if (stderr.includes("Library not loaded") || stderr.includes("image not found")) {
|
|
361
|
-
resolve({ success: false, error: stderr.split("\n")[0] });
|
|
362
|
-
} else {
|
|
363
|
-
// Other exit codes might be fine too
|
|
364
|
-
resolve({ success: true });
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (!result.success) {
|
|
370
|
-
return result;
|
|
371
|
-
}
|
|
372
|
-
} catch (error) {
|
|
373
|
-
// If spawn fails entirely, that's a problem
|
|
374
|
-
return { success: false, error: `Execution test failed: ${error}` };
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return { success: true };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ============================================================================
|
|
382
|
-
// macOS Export
|
|
383
|
-
// ============================================================================
|
|
384
|
-
|
|
385
|
-
function getHomebrewPath(): string | null {
|
|
386
|
-
if (existsSync(path.join(HOMEBREW_ARM_PATH, "bin", "idevice_id"))) {
|
|
387
|
-
return HOMEBREW_ARM_PATH;
|
|
388
|
-
}
|
|
389
|
-
if (existsSync(path.join(HOMEBREW_INTEL_PATH, "bin", "idevice_id"))) {
|
|
390
|
-
return HOMEBREW_INTEL_PATH;
|
|
391
|
-
}
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
async function exportDarwinResources(targetDir: string, skipVerify: boolean): Promise<void> {
|
|
396
|
-
console.log("\n=== Exporting macOS (Darwin) Resources ===\n");
|
|
397
|
-
|
|
398
|
-
const homebrewPath = getHomebrewPath();
|
|
399
|
-
if (!homebrewPath) {
|
|
400
|
-
console.error("Error: Homebrew libimobiledevice not found.");
|
|
401
|
-
console.error("Please install it first: brew install libimobiledevice ideviceinstaller");
|
|
402
|
-
process.exit(1);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
console.log(`Found Homebrew at: ${homebrewPath}`);
|
|
406
|
-
|
|
407
|
-
const darwinBinDir = path.join(targetDir, "bin", "darwin");
|
|
408
|
-
mkdirSync(darwinBinDir, { recursive: true });
|
|
409
|
-
|
|
410
|
-
// =========================================================================
|
|
411
|
-
// Step 1: Copy all required and optional tools
|
|
412
|
-
// =========================================================================
|
|
413
|
-
console.log("\n--- Step 1: Copying binaries ---");
|
|
414
|
-
|
|
415
|
-
const binaryPaths: string[] = [];
|
|
416
|
-
const copiedBinaries: string[] = [];
|
|
417
|
-
|
|
418
|
-
// Copy required tools
|
|
419
|
-
for (const tool of REQUIRED_TOOLS) {
|
|
420
|
-
const toolPath = path.join(homebrewPath, "bin", tool);
|
|
421
|
-
if (existsSync(toolPath)) {
|
|
422
|
-
const realPath = resolveSymlink(toolPath);
|
|
423
|
-
binaryPaths.push(realPath);
|
|
424
|
-
|
|
425
|
-
const destPath = path.join(darwinBinDir, tool);
|
|
426
|
-
copyFileSync(realPath, destPath);
|
|
427
|
-
chmodSync(destPath, 0o755);
|
|
428
|
-
copiedBinaries.push(destPath);
|
|
429
|
-
console.log(` ✓ Copied ${tool}`);
|
|
430
|
-
} else {
|
|
431
|
-
console.error(` ✗ Required tool not found: ${tool}`);
|
|
432
|
-
console.error(" Please install: brew install libimobiledevice ideviceinstaller");
|
|
433
|
-
process.exit(1);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Copy optional tools
|
|
438
|
-
for (const tool of OPTIONAL_TOOLS) {
|
|
439
|
-
const toolPath = path.join(homebrewPath, "bin", tool);
|
|
440
|
-
if (existsSync(toolPath)) {
|
|
441
|
-
const realPath = resolveSymlink(toolPath);
|
|
442
|
-
binaryPaths.push(realPath);
|
|
443
|
-
|
|
444
|
-
const destPath = path.join(darwinBinDir, tool);
|
|
445
|
-
copyFileSync(realPath, destPath);
|
|
446
|
-
chmodSync(destPath, 0o755);
|
|
447
|
-
copiedBinaries.push(destPath);
|
|
448
|
-
console.log(` ✓ Copied ${tool} (optional)`);
|
|
449
|
-
} else {
|
|
450
|
-
console.log(` - Skipping ${tool} (optional, not installed)`);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// =========================================================================
|
|
455
|
-
// Step 2: Recursively collect ALL dylib dependencies
|
|
456
|
-
// =========================================================================
|
|
457
|
-
console.log("\n--- Step 2: Collecting dependencies ---");
|
|
458
|
-
|
|
459
|
-
const allDylibs = await collectAllDependencies(binaryPaths);
|
|
460
|
-
|
|
461
|
-
// =========================================================================
|
|
462
|
-
// Step 3: Copy all dylibs
|
|
463
|
-
// =========================================================================
|
|
464
|
-
console.log("\n--- Step 3: Copying dylibs ---");
|
|
465
|
-
|
|
466
|
-
const copiedDylibs: string[] = [];
|
|
467
|
-
const allDylibNames = new Set<string>();
|
|
468
|
-
|
|
469
|
-
for (const [dylibName, dep] of allDylibs) {
|
|
470
|
-
allDylibNames.add(dylibName);
|
|
471
|
-
const destPath = path.join(darwinBinDir, dylibName);
|
|
472
|
-
|
|
473
|
-
if (existsSync(dep.realPath)) {
|
|
474
|
-
copyFileSync(dep.realPath, destPath);
|
|
475
|
-
chmodSync(destPath, 0o755);
|
|
476
|
-
copiedDylibs.push(destPath);
|
|
477
|
-
console.log(` ✓ Copied ${dylibName}`);
|
|
478
|
-
} else {
|
|
479
|
-
console.warn(` ✗ Warning: Dylib not found: ${dep.realPath}`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// =========================================================================
|
|
484
|
-
// Step 4: Fix library paths in all copied files
|
|
485
|
-
// =========================================================================
|
|
486
|
-
console.log("\n--- Step 4: Fixing library paths ---");
|
|
487
|
-
|
|
488
|
-
const allCopiedFiles = [...copiedBinaries, ...copiedDylibs];
|
|
489
|
-
const unfixedPaths: Map<string, string[]> = new Map();
|
|
490
|
-
|
|
491
|
-
// First, fix dylib IDs
|
|
492
|
-
for (const dylibPath of copiedDylibs) {
|
|
493
|
-
await fixDylibId(dylibPath);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Then, fix all library references
|
|
497
|
-
for (const filePath of allCopiedFiles) {
|
|
498
|
-
const unfixed = await fixAllLibraryPaths(filePath, allDylibNames);
|
|
499
|
-
if (unfixed.length > 0) {
|
|
500
|
-
unfixedPaths.set(path.basename(filePath), unfixed);
|
|
501
|
-
}
|
|
502
|
-
console.log(` ✓ Fixed paths in ${path.basename(filePath)}`);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Report any unfixed paths
|
|
506
|
-
if (unfixedPaths.size > 0) {
|
|
507
|
-
console.log("\n ⚠ Warning: Some library paths could not be fixed:");
|
|
508
|
-
for (const [file, paths] of unfixedPaths) {
|
|
509
|
-
for (const p of paths) {
|
|
510
|
-
console.log(` ${file}: ${p}`);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// =========================================================================
|
|
516
|
-
// Step 5: Code sign all binaries (required on modern macOS)
|
|
517
|
-
// =========================================================================
|
|
518
|
-
console.log("\n--- Step 5: Code signing ---");
|
|
519
|
-
|
|
520
|
-
let signedCount = 0;
|
|
521
|
-
for (const filePath of allCopiedFiles) {
|
|
522
|
-
const success = await codesignBinary(filePath);
|
|
523
|
-
if (success) {
|
|
524
|
-
signedCount++;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
console.log(` ✓ Signed ${signedCount}/${allCopiedFiles.length} files`);
|
|
528
|
-
|
|
529
|
-
// =========================================================================
|
|
530
|
-
// Step 6: Verify binaries work
|
|
531
|
-
// =========================================================================
|
|
532
|
-
if (!skipVerify) {
|
|
533
|
-
console.log("\n--- Step 6: Verifying binaries ---");
|
|
534
|
-
|
|
535
|
-
let verifyErrors = 0;
|
|
536
|
-
for (const binaryPath of copiedBinaries) {
|
|
537
|
-
const result = await verifyBinary(binaryPath, darwinBinDir);
|
|
538
|
-
if (result.success) {
|
|
539
|
-
console.log(` ✓ ${path.basename(binaryPath)} OK`);
|
|
540
|
-
} else {
|
|
541
|
-
console.log(` ✗ ${path.basename(binaryPath)} FAILED: ${result.error}`);
|
|
542
|
-
verifyErrors++;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Also verify dylibs have valid paths
|
|
547
|
-
for (const dylibPath of copiedDylibs) {
|
|
548
|
-
const result = await verifyBinary(dylibPath, darwinBinDir);
|
|
549
|
-
if (!result.success) {
|
|
550
|
-
console.log(` ✗ ${path.basename(dylibPath)} FAILED: ${result.error}`);
|
|
551
|
-
verifyErrors++;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (verifyErrors > 0) {
|
|
556
|
-
console.log(`\n ⚠ ${verifyErrors} verification errors found`);
|
|
557
|
-
console.log(" Run with DYLD_PRINT_LIBRARIES=1 to debug library loading issues");
|
|
558
|
-
} else {
|
|
559
|
-
console.log("\n ✓ All binaries verified successfully");
|
|
560
|
-
}
|
|
561
|
-
} else {
|
|
562
|
-
console.log("\n--- Step 6: Verification skipped ---");
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
console.log(`\n✓ macOS resources exported to: ${darwinBinDir}`);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// ============================================================================
|
|
569
|
-
// Windows Export
|
|
570
|
-
// ============================================================================
|
|
571
|
-
|
|
572
|
-
async function exportWindowsResources(targetDir: string): Promise<void> {
|
|
573
|
-
console.log("\n=== Exporting Windows Resources ===\n");
|
|
574
|
-
|
|
575
|
-
// Check if we're on Windows
|
|
576
|
-
if (process.platform !== "win32") {
|
|
577
|
-
console.log("Note: Windows resources can only be built on Windows.");
|
|
578
|
-
console.log("Generating build script for later use...\n");
|
|
579
|
-
generateWindowsBuildScript(targetDir);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Check for MSYS2
|
|
584
|
-
const msys2Path = process.env.MSYS2_PATH || MSYS2_DEFAULT_PATH;
|
|
585
|
-
if (!existsSync(msys2Path)) {
|
|
586
|
-
console.error(`Error: MSYS2 not found at ${msys2Path}`);
|
|
587
|
-
console.error("Please install MSYS2 from https://www.msys2.org/");
|
|
588
|
-
console.error("Or set MSYS2_PATH environment variable to your MSYS2 installation.");
|
|
589
|
-
generateWindowsBuildScript(targetDir);
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Generate the build script
|
|
594
|
-
generateWindowsBuildScript(targetDir);
|
|
595
|
-
|
|
596
|
-
console.log("\nTo build Windows resources:");
|
|
597
|
-
console.log("1. Open MSYS2 MinGW 64-bit shell");
|
|
598
|
-
console.log(`2. Run: bash "${path.join(targetDir, "build-windows.sh")}"`);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function generateWindowsBuildScript(targetDir: string): void {
|
|
602
|
-
const windowsBinDir = path.join(targetDir, "bin", "windows");
|
|
603
|
-
mkdirSync(windowsBinDir, { recursive: true });
|
|
604
|
-
|
|
605
|
-
// Read the template file
|
|
606
|
-
const templatePath = path.join(__dirname, "build-windows.sh.template");
|
|
607
|
-
let buildScript = readFileSync(templatePath, "utf-8");
|
|
608
|
-
|
|
609
|
-
// Replace the placeholder with the actual target directory
|
|
610
|
-
buildScript = buildScript.replace("{{TARGET_DIR}}", windowsBinDir.replace(/\\/g, "/"));
|
|
611
|
-
|
|
612
|
-
const scriptPath = path.join(targetDir, "build-windows.sh");
|
|
613
|
-
writeFileSync(scriptPath, buildScript);
|
|
614
|
-
console.log(`✓ Generated Windows build script: ${scriptPath}`);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// ============================================================================
|
|
618
|
-
// License File
|
|
619
|
-
// ============================================================================
|
|
620
|
-
|
|
621
|
-
function createLicenseFile(targetDir: string): void {
|
|
622
|
-
const licensesDir = path.join(targetDir, "licenses");
|
|
623
|
-
mkdirSync(licensesDir, { recursive: true });
|
|
624
|
-
|
|
625
|
-
const lgplText = `GNU LESSER GENERAL PUBLIC LICENSE
|
|
626
|
-
Version 2.1, February 1999
|
|
627
|
-
|
|
628
|
-
libimobiledevice and related tools are licensed under the
|
|
629
|
-
GNU Lesser General Public License version 2.1.
|
|
630
|
-
|
|
631
|
-
For full license text, see:
|
|
632
|
-
https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
|
|
633
|
-
|
|
634
|
-
These binaries are bundled for convenience. The source code is available at:
|
|
635
|
-
- https://github.com/libimobiledevice/libimobiledevice
|
|
636
|
-
- https://github.com/libimobiledevice/libusbmuxd
|
|
637
|
-
- https://github.com/libimobiledevice/libplist
|
|
638
|
-
- https://github.com/libimobiledevice/libimobiledevice-glue
|
|
639
|
-
- https://github.com/libimobiledevice/ideviceinstaller
|
|
640
|
-
`;
|
|
641
|
-
|
|
642
|
-
const licensePath = path.join(licensesDir, "LGPL-2.1.txt");
|
|
643
|
-
writeFileSync(licensePath, lgplText);
|
|
644
|
-
console.log(`✓ Created license file: ${licensePath}`);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ============================================================================
|
|
648
|
-
// Main
|
|
649
|
-
// ============================================================================
|
|
650
|
-
|
|
651
|
-
async function main(): Promise<void> {
|
|
652
|
-
const args = process.argv.slice(2);
|
|
653
|
-
|
|
654
|
-
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
655
|
-
printUsage();
|
|
656
|
-
process.exit(args.length === 0 ? 1 : 0);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const skipVerify = args.includes("--skip-verify");
|
|
660
|
-
const targetPath = path.resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
661
|
-
|
|
662
|
-
console.log("=== Apple-kit Resources Export ===");
|
|
663
|
-
console.log(`Platform: ${process.platform}`);
|
|
664
|
-
console.log(`Target: ${targetPath}`);
|
|
665
|
-
|
|
666
|
-
// Create target directory
|
|
667
|
-
mkdirSync(targetPath, { recursive: true });
|
|
668
|
-
|
|
669
|
-
// Export based on platform
|
|
670
|
-
if (process.platform === "darwin") {
|
|
671
|
-
await exportDarwinResources(targetPath, skipVerify);
|
|
672
|
-
} else if (process.platform === "win32") {
|
|
673
|
-
await exportWindowsResources(targetPath);
|
|
674
|
-
} else {
|
|
675
|
-
console.log("\nNote: This script supports macOS and Windows.");
|
|
676
|
-
console.log("For Linux, install libimobiledevice-utils via your package manager.");
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Always create license file
|
|
680
|
-
console.log("");
|
|
681
|
-
createLicenseFile(targetPath);
|
|
682
|
-
|
|
683
|
-
console.log("\n=== Export Complete ===");
|
|
684
|
-
console.log(`Resources exported to: ${targetPath}`);
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
main().catch((error) => {
|
|
688
|
-
console.error("Export failed:", error);
|
|
689
|
-
process.exit(1);
|
|
690
|
-
});
|
|
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
|
+
readlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { promisify } from "node:util";
|
|
33
|
+
|
|
34
|
+
// Get the directory of this script
|
|
35
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
+
const __dirname = path.dirname(__filename);
|
|
37
|
+
|
|
38
|
+
const execAsync = promisify(exec);
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Configuration
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
// Tools we need to bundle
|
|
45
|
+
const REQUIRED_TOOLS = [
|
|
46
|
+
"idevice_id",
|
|
47
|
+
"ideviceinfo",
|
|
48
|
+
"idevicepair",
|
|
49
|
+
"idevicename",
|
|
50
|
+
"idevicedebug",
|
|
51
|
+
"ideviceinstaller",
|
|
52
|
+
"iproxy",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Optional tools (won't fail if not found)
|
|
56
|
+
const OPTIONAL_TOOLS = [
|
|
57
|
+
"ideviceactivation",
|
|
58
|
+
"idevicesyslog",
|
|
59
|
+
"idevicescreenshot",
|
|
60
|
+
"idevicediagnostics",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Homebrew paths on macOS
|
|
64
|
+
const HOMEBREW_ARM_PATH = "/opt/homebrew";
|
|
65
|
+
const HOMEBREW_INTEL_PATH = "/usr/local";
|
|
66
|
+
|
|
67
|
+
// MSYS2 paths on Windows
|
|
68
|
+
const MSYS2_DEFAULT_PATH = "C:\\msys64";
|
|
69
|
+
|
|
70
|
+
// System library prefixes that should NOT be copied (they exist on all macOS)
|
|
71
|
+
const SYSTEM_LIB_PREFIXES = ["/System/Library/", "/usr/lib/", "/Library/Apple/"];
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Utility Functions
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
function printUsage(): void {
|
|
78
|
+
console.log(`
|
|
79
|
+
Usage: npx tsx export-resources.ts <target-path>
|
|
80
|
+
|
|
81
|
+
Arguments:
|
|
82
|
+
target-path Directory where resources will be exported
|
|
83
|
+
|
|
84
|
+
Options:
|
|
85
|
+
--skip-verify Skip verification step
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
npx tsx export-resources.ts ./my-app/resources/apple-kit
|
|
89
|
+
npx tsx export-resources.ts /absolute/path/to/resources
|
|
90
|
+
|
|
91
|
+
The script will create the following structure:
|
|
92
|
+
<target-path>/
|
|
93
|
+
bin/
|
|
94
|
+
darwin/ (macOS binaries and dylibs)
|
|
95
|
+
windows/ (Windows binaries and DLLs)
|
|
96
|
+
licenses/
|
|
97
|
+
LGPL-2.1.txt
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveSymlink(filePath: string): string {
|
|
102
|
+
try {
|
|
103
|
+
const linkTarget = readlinkSync(filePath);
|
|
104
|
+
if (path.isAbsolute(linkTarget)) {
|
|
105
|
+
return linkTarget;
|
|
106
|
+
}
|
|
107
|
+
return path.resolve(path.dirname(filePath), linkTarget);
|
|
108
|
+
} catch {
|
|
109
|
+
return filePath; // Not a symlink
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a library path is a system library that doesn't need to be copied
|
|
115
|
+
*/
|
|
116
|
+
function isSystemLibrary(libPath: string): boolean {
|
|
117
|
+
return SYSTEM_LIB_PREFIXES.some((prefix) => libPath.startsWith(prefix));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a library path needs to be bundled (is from homebrew or similar)
|
|
122
|
+
*/
|
|
123
|
+
function needsBundling(libPath: string): boolean {
|
|
124
|
+
if (isSystemLibrary(libPath)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (libPath.startsWith("@")) {
|
|
128
|
+
// Already a relocatable path
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
// Bundle anything from /opt/homebrew, /usr/local/opt, or /usr/local/Cellar
|
|
132
|
+
return (
|
|
133
|
+
libPath.includes("/opt/homebrew") ||
|
|
134
|
+
libPath.includes("/usr/local/opt") ||
|
|
135
|
+
libPath.includes("/usr/local/Cellar") ||
|
|
136
|
+
libPath.includes("/usr/local/lib")
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface DylibDependency {
|
|
141
|
+
originalPath: string;
|
|
142
|
+
name: string;
|
|
143
|
+
realPath: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all dylib dependencies for a binary, including the original path as referenced
|
|
148
|
+
*/
|
|
149
|
+
async function getDylibDependencies(binaryPath: string): Promise<DylibDependency[]> {
|
|
150
|
+
const dylibs: DylibDependency[] = [];
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
154
|
+
const lines = stdout.split("\n");
|
|
155
|
+
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
// Match library paths (with or without version info in parentheses)
|
|
158
|
+
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
159
|
+
if (match) {
|
|
160
|
+
const libPath = match[1].trim();
|
|
161
|
+
if (needsBundling(libPath)) {
|
|
162
|
+
const realPath = resolveSymlink(libPath);
|
|
163
|
+
dylibs.push({
|
|
164
|
+
originalPath: libPath,
|
|
165
|
+
name: path.basename(libPath),
|
|
166
|
+
realPath: existsSync(realPath) ? realPath : libPath,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn(`Warning: Could not get dependencies for ${binaryPath}: ${error}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return dylibs;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Recursively collect ALL dylib dependencies
|
|
180
|
+
* Continues until no new dependencies are found
|
|
181
|
+
*/
|
|
182
|
+
async function collectAllDependencies(
|
|
183
|
+
initialBinaries: string[]
|
|
184
|
+
): Promise<Map<string, DylibDependency>> {
|
|
185
|
+
const allDylibs = new Map<string, DylibDependency>();
|
|
186
|
+
const processedPaths = new Set<string>();
|
|
187
|
+
const toProcess: string[] = [...initialBinaries];
|
|
188
|
+
|
|
189
|
+
console.log(" Scanning for dependencies...");
|
|
190
|
+
|
|
191
|
+
let iteration = 0;
|
|
192
|
+
while (toProcess.length > 0) {
|
|
193
|
+
iteration++;
|
|
194
|
+
const currentPath = toProcess.pop();
|
|
195
|
+
if (!currentPath) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (processedPaths.has(currentPath)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
processedPaths.add(currentPath);
|
|
203
|
+
|
|
204
|
+
const deps = await getDylibDependencies(currentPath);
|
|
205
|
+
|
|
206
|
+
for (const dep of deps) {
|
|
207
|
+
if (!allDylibs.has(dep.name)) {
|
|
208
|
+
allDylibs.set(dep.name, dep);
|
|
209
|
+
// Add this dylib to be processed for its own dependencies
|
|
210
|
+
if (existsSync(dep.realPath)) {
|
|
211
|
+
toProcess.push(dep.realPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(` Found ${allDylibs.size} unique dylib dependencies (${iteration} files scanned)`);
|
|
218
|
+
return allDylibs;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fix all library paths in a binary to use @loader_path
|
|
223
|
+
* This rewrites ALL non-system library references
|
|
224
|
+
*/
|
|
225
|
+
async function fixAllLibraryPaths(
|
|
226
|
+
binaryPath: string,
|
|
227
|
+
allDylibNames: Set<string>
|
|
228
|
+
): Promise<string[]> {
|
|
229
|
+
const unfixedPaths: string[] = [];
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
233
|
+
const lines = stdout.split("\n");
|
|
234
|
+
|
|
235
|
+
for (const line of lines) {
|
|
236
|
+
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
237
|
+
if (match) {
|
|
238
|
+
const libPath = match[1].trim();
|
|
239
|
+
|
|
240
|
+
// Skip system libraries and already-fixed paths
|
|
241
|
+
if (isSystemLibrary(libPath) || libPath.startsWith("@")) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const libName = path.basename(libPath);
|
|
246
|
+
|
|
247
|
+
// Check if we have this dylib in our collection
|
|
248
|
+
if (allDylibNames.has(libName)) {
|
|
249
|
+
try {
|
|
250
|
+
await execAsync(
|
|
251
|
+
`install_name_tool -change "${libPath}" "@loader_path/${libName}" "${binaryPath}"`
|
|
252
|
+
);
|
|
253
|
+
} catch (_error) {
|
|
254
|
+
console.warn(` Warning: Could not fix path ${libPath} in ${path.basename(binaryPath)}`);
|
|
255
|
+
unfixedPaths.push(libPath);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// This is a non-system library we don't have - this is a problem
|
|
259
|
+
unfixedPaths.push(libPath);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.warn(`Warning: Could not process ${binaryPath}: ${error}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return unfixedPaths;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Fix the install name (ID) of a dylib to use @loader_path
|
|
272
|
+
*/
|
|
273
|
+
async function fixDylibId(dylibPath: string): Promise<void> {
|
|
274
|
+
const dylibName = path.basename(dylibPath);
|
|
275
|
+
try {
|
|
276
|
+
await execAsync(`install_name_tool -id "@loader_path/${dylibName}" "${dylibPath}"`);
|
|
277
|
+
} catch {
|
|
278
|
+
console.warn(` Warning: Could not fix dylib ID for ${dylibName}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Ad-hoc code sign a binary (required after modifying with install_name_tool on modern macOS)
|
|
284
|
+
*/
|
|
285
|
+
async function codesignBinary(binaryPath: string): Promise<boolean> {
|
|
286
|
+
try {
|
|
287
|
+
await execAsync(`codesign --force --sign - "${binaryPath}"`);
|
|
288
|
+
return true;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.warn(` Warning: Could not code sign ${path.basename(binaryPath)}: ${error}`);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Verify that a binary can be loaded and executed
|
|
297
|
+
*/
|
|
298
|
+
async function verifyBinary(
|
|
299
|
+
binaryPath: string,
|
|
300
|
+
binDir: string
|
|
301
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
302
|
+
const binaryName = path.basename(binaryPath);
|
|
303
|
+
|
|
304
|
+
// Check that otool shows no broken paths
|
|
305
|
+
try {
|
|
306
|
+
const { stdout } = await execAsync(`otool -L "${binaryPath}"`);
|
|
307
|
+
const lines = stdout.split("\n");
|
|
308
|
+
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
const match = line.match(/^\s+(.+?)\s+\(/);
|
|
311
|
+
if (match) {
|
|
312
|
+
const libPath = match[1].trim();
|
|
313
|
+
|
|
314
|
+
// Skip system libraries
|
|
315
|
+
if (isSystemLibrary(libPath)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If it's an @loader_path reference, check the file exists
|
|
320
|
+
if (libPath.startsWith("@loader_path/")) {
|
|
321
|
+
const libName = libPath.replace("@loader_path/", "");
|
|
322
|
+
const fullPath = path.join(binDir, libName);
|
|
323
|
+
if (!existsSync(fullPath)) {
|
|
324
|
+
return { success: false, error: `Missing library: ${libName}` };
|
|
325
|
+
}
|
|
326
|
+
} else if (!libPath.startsWith("@")) {
|
|
327
|
+
// Absolute path that's not a system library - this is a problem
|
|
328
|
+
return { success: false, error: `Unfixed absolute path: ${libPath}` };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
return { success: false, error: `otool failed: ${error}` };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// For executable binaries (not dylibs), try to run them with --help or -h
|
|
337
|
+
if (!binaryName.endsWith(".dylib")) {
|
|
338
|
+
try {
|
|
339
|
+
// Use spawn with a timeout to test if the binary loads
|
|
340
|
+
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
|
|
341
|
+
const child = spawn(binaryPath, ["--help"], {
|
|
342
|
+
timeout: 5000,
|
|
343
|
+
env: { ...process.env, DYLD_LIBRARY_PATH: binDir },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
let stderr = "";
|
|
347
|
+
child.stderr?.on("data", (data) => {
|
|
348
|
+
stderr += data.toString();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
child.on("error", (err) => {
|
|
352
|
+
resolve({ success: false, error: `Spawn error: ${err.message}` });
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
child.on("close", (code) => {
|
|
356
|
+
// Exit codes 0, 1, or 2 are acceptable (0=success, 1=error, 2=usage)
|
|
357
|
+
// What we're checking is that the binary LOADS, not that --help works
|
|
358
|
+
if (code !== null && code <= 2) {
|
|
359
|
+
resolve({ success: true });
|
|
360
|
+
} else if (stderr.includes("Library not loaded") || stderr.includes("image not found")) {
|
|
361
|
+
resolve({ success: false, error: stderr.split("\n")[0] });
|
|
362
|
+
} else {
|
|
363
|
+
// Other exit codes might be fine too
|
|
364
|
+
resolve({ success: true });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (!result.success) {
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// If spawn fails entirely, that's a problem
|
|
374
|
+
return { success: false, error: `Execution test failed: ${error}` };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { success: true };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// macOS Export
|
|
383
|
+
// ============================================================================
|
|
384
|
+
|
|
385
|
+
function getHomebrewPath(): string | null {
|
|
386
|
+
if (existsSync(path.join(HOMEBREW_ARM_PATH, "bin", "idevice_id"))) {
|
|
387
|
+
return HOMEBREW_ARM_PATH;
|
|
388
|
+
}
|
|
389
|
+
if (existsSync(path.join(HOMEBREW_INTEL_PATH, "bin", "idevice_id"))) {
|
|
390
|
+
return HOMEBREW_INTEL_PATH;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function exportDarwinResources(targetDir: string, skipVerify: boolean): Promise<void> {
|
|
396
|
+
console.log("\n=== Exporting macOS (Darwin) Resources ===\n");
|
|
397
|
+
|
|
398
|
+
const homebrewPath = getHomebrewPath();
|
|
399
|
+
if (!homebrewPath) {
|
|
400
|
+
console.error("Error: Homebrew libimobiledevice not found.");
|
|
401
|
+
console.error("Please install it first: brew install libimobiledevice ideviceinstaller");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(`Found Homebrew at: ${homebrewPath}`);
|
|
406
|
+
|
|
407
|
+
const darwinBinDir = path.join(targetDir, "bin", "darwin");
|
|
408
|
+
mkdirSync(darwinBinDir, { recursive: true });
|
|
409
|
+
|
|
410
|
+
// =========================================================================
|
|
411
|
+
// Step 1: Copy all required and optional tools
|
|
412
|
+
// =========================================================================
|
|
413
|
+
console.log("\n--- Step 1: Copying binaries ---");
|
|
414
|
+
|
|
415
|
+
const binaryPaths: string[] = [];
|
|
416
|
+
const copiedBinaries: string[] = [];
|
|
417
|
+
|
|
418
|
+
// Copy required tools
|
|
419
|
+
for (const tool of REQUIRED_TOOLS) {
|
|
420
|
+
const toolPath = path.join(homebrewPath, "bin", tool);
|
|
421
|
+
if (existsSync(toolPath)) {
|
|
422
|
+
const realPath = resolveSymlink(toolPath);
|
|
423
|
+
binaryPaths.push(realPath);
|
|
424
|
+
|
|
425
|
+
const destPath = path.join(darwinBinDir, tool);
|
|
426
|
+
copyFileSync(realPath, destPath);
|
|
427
|
+
chmodSync(destPath, 0o755);
|
|
428
|
+
copiedBinaries.push(destPath);
|
|
429
|
+
console.log(` ✓ Copied ${tool}`);
|
|
430
|
+
} else {
|
|
431
|
+
console.error(` ✗ Required tool not found: ${tool}`);
|
|
432
|
+
console.error(" Please install: brew install libimobiledevice ideviceinstaller");
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Copy optional tools
|
|
438
|
+
for (const tool of OPTIONAL_TOOLS) {
|
|
439
|
+
const toolPath = path.join(homebrewPath, "bin", tool);
|
|
440
|
+
if (existsSync(toolPath)) {
|
|
441
|
+
const realPath = resolveSymlink(toolPath);
|
|
442
|
+
binaryPaths.push(realPath);
|
|
443
|
+
|
|
444
|
+
const destPath = path.join(darwinBinDir, tool);
|
|
445
|
+
copyFileSync(realPath, destPath);
|
|
446
|
+
chmodSync(destPath, 0o755);
|
|
447
|
+
copiedBinaries.push(destPath);
|
|
448
|
+
console.log(` ✓ Copied ${tool} (optional)`);
|
|
449
|
+
} else {
|
|
450
|
+
console.log(` - Skipping ${tool} (optional, not installed)`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// =========================================================================
|
|
455
|
+
// Step 2: Recursively collect ALL dylib dependencies
|
|
456
|
+
// =========================================================================
|
|
457
|
+
console.log("\n--- Step 2: Collecting dependencies ---");
|
|
458
|
+
|
|
459
|
+
const allDylibs = await collectAllDependencies(binaryPaths);
|
|
460
|
+
|
|
461
|
+
// =========================================================================
|
|
462
|
+
// Step 3: Copy all dylibs
|
|
463
|
+
// =========================================================================
|
|
464
|
+
console.log("\n--- Step 3: Copying dylibs ---");
|
|
465
|
+
|
|
466
|
+
const copiedDylibs: string[] = [];
|
|
467
|
+
const allDylibNames = new Set<string>();
|
|
468
|
+
|
|
469
|
+
for (const [dylibName, dep] of allDylibs) {
|
|
470
|
+
allDylibNames.add(dylibName);
|
|
471
|
+
const destPath = path.join(darwinBinDir, dylibName);
|
|
472
|
+
|
|
473
|
+
if (existsSync(dep.realPath)) {
|
|
474
|
+
copyFileSync(dep.realPath, destPath);
|
|
475
|
+
chmodSync(destPath, 0o755);
|
|
476
|
+
copiedDylibs.push(destPath);
|
|
477
|
+
console.log(` ✓ Copied ${dylibName}`);
|
|
478
|
+
} else {
|
|
479
|
+
console.warn(` ✗ Warning: Dylib not found: ${dep.realPath}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// =========================================================================
|
|
484
|
+
// Step 4: Fix library paths in all copied files
|
|
485
|
+
// =========================================================================
|
|
486
|
+
console.log("\n--- Step 4: Fixing library paths ---");
|
|
487
|
+
|
|
488
|
+
const allCopiedFiles = [...copiedBinaries, ...copiedDylibs];
|
|
489
|
+
const unfixedPaths: Map<string, string[]> = new Map();
|
|
490
|
+
|
|
491
|
+
// First, fix dylib IDs
|
|
492
|
+
for (const dylibPath of copiedDylibs) {
|
|
493
|
+
await fixDylibId(dylibPath);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Then, fix all library references
|
|
497
|
+
for (const filePath of allCopiedFiles) {
|
|
498
|
+
const unfixed = await fixAllLibraryPaths(filePath, allDylibNames);
|
|
499
|
+
if (unfixed.length > 0) {
|
|
500
|
+
unfixedPaths.set(path.basename(filePath), unfixed);
|
|
501
|
+
}
|
|
502
|
+
console.log(` ✓ Fixed paths in ${path.basename(filePath)}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Report any unfixed paths
|
|
506
|
+
if (unfixedPaths.size > 0) {
|
|
507
|
+
console.log("\n ⚠ Warning: Some library paths could not be fixed:");
|
|
508
|
+
for (const [file, paths] of unfixedPaths) {
|
|
509
|
+
for (const p of paths) {
|
|
510
|
+
console.log(` ${file}: ${p}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// =========================================================================
|
|
516
|
+
// Step 5: Code sign all binaries (required on modern macOS)
|
|
517
|
+
// =========================================================================
|
|
518
|
+
console.log("\n--- Step 5: Code signing ---");
|
|
519
|
+
|
|
520
|
+
let signedCount = 0;
|
|
521
|
+
for (const filePath of allCopiedFiles) {
|
|
522
|
+
const success = await codesignBinary(filePath);
|
|
523
|
+
if (success) {
|
|
524
|
+
signedCount++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
console.log(` ✓ Signed ${signedCount}/${allCopiedFiles.length} files`);
|
|
528
|
+
|
|
529
|
+
// =========================================================================
|
|
530
|
+
// Step 6: Verify binaries work
|
|
531
|
+
// =========================================================================
|
|
532
|
+
if (!skipVerify) {
|
|
533
|
+
console.log("\n--- Step 6: Verifying binaries ---");
|
|
534
|
+
|
|
535
|
+
let verifyErrors = 0;
|
|
536
|
+
for (const binaryPath of copiedBinaries) {
|
|
537
|
+
const result = await verifyBinary(binaryPath, darwinBinDir);
|
|
538
|
+
if (result.success) {
|
|
539
|
+
console.log(` ✓ ${path.basename(binaryPath)} OK`);
|
|
540
|
+
} else {
|
|
541
|
+
console.log(` ✗ ${path.basename(binaryPath)} FAILED: ${result.error}`);
|
|
542
|
+
verifyErrors++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Also verify dylibs have valid paths
|
|
547
|
+
for (const dylibPath of copiedDylibs) {
|
|
548
|
+
const result = await verifyBinary(dylibPath, darwinBinDir);
|
|
549
|
+
if (!result.success) {
|
|
550
|
+
console.log(` ✗ ${path.basename(dylibPath)} FAILED: ${result.error}`);
|
|
551
|
+
verifyErrors++;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (verifyErrors > 0) {
|
|
556
|
+
console.log(`\n ⚠ ${verifyErrors} verification errors found`);
|
|
557
|
+
console.log(" Run with DYLD_PRINT_LIBRARIES=1 to debug library loading issues");
|
|
558
|
+
} else {
|
|
559
|
+
console.log("\n ✓ All binaries verified successfully");
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
console.log("\n--- Step 6: Verification skipped ---");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(`\n✓ macOS resources exported to: ${darwinBinDir}`);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ============================================================================
|
|
569
|
+
// Windows Export
|
|
570
|
+
// ============================================================================
|
|
571
|
+
|
|
572
|
+
async function exportWindowsResources(targetDir: string): Promise<void> {
|
|
573
|
+
console.log("\n=== Exporting Windows Resources ===\n");
|
|
574
|
+
|
|
575
|
+
// Check if we're on Windows
|
|
576
|
+
if (process.platform !== "win32") {
|
|
577
|
+
console.log("Note: Windows resources can only be built on Windows.");
|
|
578
|
+
console.log("Generating build script for later use...\n");
|
|
579
|
+
generateWindowsBuildScript(targetDir);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Check for MSYS2
|
|
584
|
+
const msys2Path = process.env.MSYS2_PATH || MSYS2_DEFAULT_PATH;
|
|
585
|
+
if (!existsSync(msys2Path)) {
|
|
586
|
+
console.error(`Error: MSYS2 not found at ${msys2Path}`);
|
|
587
|
+
console.error("Please install MSYS2 from https://www.msys2.org/");
|
|
588
|
+
console.error("Or set MSYS2_PATH environment variable to your MSYS2 installation.");
|
|
589
|
+
generateWindowsBuildScript(targetDir);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Generate the build script
|
|
594
|
+
generateWindowsBuildScript(targetDir);
|
|
595
|
+
|
|
596
|
+
console.log("\nTo build Windows resources:");
|
|
597
|
+
console.log("1. Open MSYS2 MinGW 64-bit shell");
|
|
598
|
+
console.log(`2. Run: bash "${path.join(targetDir, "build-windows.sh")}"`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function generateWindowsBuildScript(targetDir: string): void {
|
|
602
|
+
const windowsBinDir = path.join(targetDir, "bin", "windows");
|
|
603
|
+
mkdirSync(windowsBinDir, { recursive: true });
|
|
604
|
+
|
|
605
|
+
// Read the template file
|
|
606
|
+
const templatePath = path.join(__dirname, "build-windows.sh.template");
|
|
607
|
+
let buildScript = readFileSync(templatePath, "utf-8");
|
|
608
|
+
|
|
609
|
+
// Replace the placeholder with the actual target directory
|
|
610
|
+
buildScript = buildScript.replace("{{TARGET_DIR}}", windowsBinDir.replace(/\\/g, "/"));
|
|
611
|
+
|
|
612
|
+
const scriptPath = path.join(targetDir, "build-windows.sh");
|
|
613
|
+
writeFileSync(scriptPath, buildScript);
|
|
614
|
+
console.log(`✓ Generated Windows build script: ${scriptPath}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ============================================================================
|
|
618
|
+
// License File
|
|
619
|
+
// ============================================================================
|
|
620
|
+
|
|
621
|
+
function createLicenseFile(targetDir: string): void {
|
|
622
|
+
const licensesDir = path.join(targetDir, "licenses");
|
|
623
|
+
mkdirSync(licensesDir, { recursive: true });
|
|
624
|
+
|
|
625
|
+
const lgplText = `GNU LESSER GENERAL PUBLIC LICENSE
|
|
626
|
+
Version 2.1, February 1999
|
|
627
|
+
|
|
628
|
+
libimobiledevice and related tools are licensed under the
|
|
629
|
+
GNU Lesser General Public License version 2.1.
|
|
630
|
+
|
|
631
|
+
For full license text, see:
|
|
632
|
+
https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
|
|
633
|
+
|
|
634
|
+
These binaries are bundled for convenience. The source code is available at:
|
|
635
|
+
- https://github.com/libimobiledevice/libimobiledevice
|
|
636
|
+
- https://github.com/libimobiledevice/libusbmuxd
|
|
637
|
+
- https://github.com/libimobiledevice/libplist
|
|
638
|
+
- https://github.com/libimobiledevice/libimobiledevice-glue
|
|
639
|
+
- https://github.com/libimobiledevice/ideviceinstaller
|
|
640
|
+
`;
|
|
641
|
+
|
|
642
|
+
const licensePath = path.join(licensesDir, "LGPL-2.1.txt");
|
|
643
|
+
writeFileSync(licensePath, lgplText);
|
|
644
|
+
console.log(`✓ Created license file: ${licensePath}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ============================================================================
|
|
648
|
+
// Main
|
|
649
|
+
// ============================================================================
|
|
650
|
+
|
|
651
|
+
async function main(): Promise<void> {
|
|
652
|
+
const args = process.argv.slice(2);
|
|
653
|
+
|
|
654
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
655
|
+
printUsage();
|
|
656
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const skipVerify = args.includes("--skip-verify");
|
|
660
|
+
const targetPath = path.resolve(args.find((a) => !a.startsWith("--")) || ".");
|
|
661
|
+
|
|
662
|
+
console.log("=== Apple-kit Resources Export ===");
|
|
663
|
+
console.log(`Platform: ${process.platform}`);
|
|
664
|
+
console.log(`Target: ${targetPath}`);
|
|
665
|
+
|
|
666
|
+
// Create target directory
|
|
667
|
+
mkdirSync(targetPath, { recursive: true });
|
|
668
|
+
|
|
669
|
+
// Export based on platform
|
|
670
|
+
if (process.platform === "darwin") {
|
|
671
|
+
await exportDarwinResources(targetPath, skipVerify);
|
|
672
|
+
} else if (process.platform === "win32") {
|
|
673
|
+
await exportWindowsResources(targetPath);
|
|
674
|
+
} else {
|
|
675
|
+
console.log("\nNote: This script supports macOS and Windows.");
|
|
676
|
+
console.log("For Linux, install libimobiledevice-utils via your package manager.");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Always create license file
|
|
680
|
+
console.log("");
|
|
681
|
+
createLicenseFile(targetPath);
|
|
682
|
+
|
|
683
|
+
console.log("\n=== Export Complete ===");
|
|
684
|
+
console.log(`Resources exported to: ${targetPath}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
main().catch((error) => {
|
|
688
|
+
console.error("Export failed:", error);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
});
|