restorekit-sys 0.4.1

Static FFI bindings to the idevicerestore C stack (self-contained build)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
//! Builds the idevicerestore C stack from vendored, pinned sources into a
//! staging prefix and links it statically, so the final binary is
//! self-contained (no Homebrew/apt runtime dependencies).
//!
//! External heavy deps (OpenSSL, zlib, libcurl) come from vendored Rust `-sys`
//! crates; libzip and the seven libimobiledevice-family libraries are built
//! from source here. idevicerestore itself ships no library, so its `.c`
//! sources are compiled directly (with `main` renamed out of the way).

use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    // docs.rs builds in a sandbox without the C toolchain, system libraries, or
    // network. `cargo doc` compiles the rlib but never links a final binary, so
    // the static C stack isn't needed there — skip building it entirely.
    if env::var_os("DOCS_RS").is_some() {
        return;
    }

    let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let vendor = manifest.join("vendor");
    ensure_submodules(&manifest, &vendor);

    let out = PathBuf::from(env::var("OUT_DIR").unwrap());
    let prefix = out.join("staging");
    std::fs::create_dir_all(prefix.join("lib/pkgconfig")).unwrap();
    std::fs::create_dir_all(prefix.join("include")).unwrap();

    // Includes/libs exposed by the vendored -sys build dependencies.
    let openssl_include = env::var("DEP_OPENSSL_INCLUDE").ok();
    let zlib_include = env::var("DEP_Z_INCLUDE").ok();
    let curl_include = env::var("DEP_CURL_INCLUDE").ok();

    let deps = Deps {
        prefix: prefix.clone(),
        openssl_include,
        zlib_include,
        curl_include,
        windows: env::var_os("CARGO_CFG_WINDOWS").is_some(),
    };

    // libzip (needs zlib) — from source via CMake.
    build_libzip(&vendor.join("libzip"), &deps);

    // The libimobiledevice family, in dependency order. The version is the
    // release tag each submodule is pinned at (see `git submodule status`); it
    // seeds `.tarball-version` when building outside a git checkout and must
    // satisfy the projects' inter-package `PKG_CHECK_MODULES` minimums.
    for (lib, version) in [
        ("libplist", "2.7.0"),
        ("libimobiledevice-glue", "1.3.2"),
        ("libusbmuxd", "2.1.1"),
        ("libirecovery", "1.3.1"),
        ("libtatsu", "1.0.5"),
        ("libimobiledevice", "1.4.0"),
    ] {
        build_autotools(&vendor.join(lib), lib, version, &deps);
    }

    // idevicerestore: compile its sources directly (no library upstream),
    // after copying them aside and applying our patches (see patches/).
    let idevicerestore_src =
        patch_idevicerestore_sources(&manifest, &vendor.join("idevicerestore"), &out);
    compile_idevicerestore(&idevicerestore_src, &deps);

    // usbmuxd server (Linux + Windows): embed the daemon event loop so the
    // binary is self-contained — no external usbmuxd process needed. It's what
    // idevicerestore talks to for the restore-mode phase.
    let target_os = env::var("CARGO_CFG_TARGET_OS");
    if matches!(target_os.as_deref(), Ok("linux") | Ok("windows")) {
        compile_usbmuxd(&vendor.join("usbmuxd"), &deps);
    }

    emit_link_directives(&prefix);
}

struct Deps {
    prefix: PathBuf,
    openssl_include: Option<String>,
    zlib_include: Option<String>,
    curl_include: Option<String>,
    /// Building for a Windows target (autotools run under MSYS2/MinGW, which
    /// wants POSIX-style paths).
    windows: bool,
}

impl Deps {
    /// A path in the form the C toolchain expects: MSYS2 POSIX on Windows,
    /// native otherwise.
    fn path(&self, p: &Path) -> String {
        if self.windows {
            to_msys(p)
        } else {
            p.display().to_string()
        }
    }

    /// pkg-config search path pointing at our staging prefix.
    fn pkg_config_path(&self) -> String {
        self.path(&self.prefix.join("lib/pkgconfig"))
    }

    /// Extra include dirs (staging + vendored -sys crates) as `-I` flags.
    fn cflags(&self) -> String {
        let mut flags = format!("-I{}", self.path(&self.prefix.join("include")));
        for inc in [
            &self.openssl_include,
            &self.zlib_include,
            &self.curl_include,
        ]
        .into_iter()
        .flatten()
        {
            flags.push_str(&format!(" -I{}", self.path(Path::new(inc))));
        }
        // Force the whole family's headers into static-linkage mode (no
        // `__declspec(dllimport)`) for every library we configure. CURL_STATICLIB
        // does the same for <curl/curl.h> (libtatsu links our static libcurl).
        if self.windows {
            for def in WINDOWS_STATIC_DEFINES {
                flags.push_str(&format!(" -D{def}"));
            }
            flags.push_str(" -DCURL_STATICLIB");
            // 64-bit file offsets: mingw's default `stat`/`off_t` are 32-bit, so
            // anything touching a >2 GB file (e.g. a ~20 GB IPSW) fails with
            // EOVERFLOW without this.
            flags.push_str(" -D_FILE_OFFSET_BITS=64");
        }
        // When the final artifact is a shared library (e.g. Tauri on Linux),
        // all static C code must be position-independent.
        if !self.windows {
            flags.push_str(" -fPIC");
        }
        flags
    }

    fn ldflags(&self) -> String {
        format!("-L{}", self.path(&self.prefix.join("lib")))
    }
}

/// The libimobiledevice family decorates its public API with
/// `__declspec(dllimport)` on Windows unless a per-library `*_STATIC` macro is
/// defined. We link the whole stack statically, so every consumer (each library
/// that includes an upstream header, plus idevicerestore) must define *all* of
/// these — otherwise the compiler emits `__imp_`-prefixed references that don't
/// exist in the static archives and the link fails. Each library's own
/// configure only sets its own macro, so we supply the full set globally.
const WINDOWS_STATIC_DEFINES: &[&str] = &[
    "LIBPLIST_STATIC",
    "LIMD_GLUE_STATIC",
    "LIBUSBMUXD_STATIC",
    "IRECV_STATIC",
    "LIBTATSU_STATIC",
    "LIBIMOBILEDEVICE_STATIC",
];

/// Translate `C:\a\b` → `/c/a/b` for MSYS2 tools.
fn to_msys(p: &Path) -> String {
    let s = p.to_string_lossy().replace('\\', "/");
    let b = s.as_bytes();
    if b.len() >= 2 && b[1] == b':' {
        format!("/{}{}", (b[0] as char).to_ascii_lowercase(), &s[2..])
    } else {
        s
    }
}

fn ensure_submodules(manifest: &Path, vendor: &Path) {
    // If the pinned sources are missing (fresh clone without --recurse), fetch them.
    if vendor.join("idevicerestore/src/idevicerestore.c").exists() {
        return;
    }
    // Walk up to the repo root to run submodule update.
    let repo_root = manifest
        .ancestors()
        .find(|p| p.join(".git").exists())
        .unwrap_or(manifest);
    let status = Command::new("git")
        .args(["submodule", "update", "--init", "--recursive"])
        .current_dir(repo_root)
        .status();
    if !matches!(status, Ok(s) if s.success()) {
        panic!(
            "vendored C sources missing and `git submodule update --init` failed; \
             run it manually in {}",
            repo_root.display()
        );
    }
}

/// Clean POSIX aclocal search path for the MSYS2 autotools.
///
/// Native cargo inherits `ACLOCAL_PATH` from the launching MSYS2 shell already
/// translated to Windows form (`C:\msys64\mingw64\share\aclocal;...`). When
/// build.rs re-enters MSYS2 via `sh`, aclocal reads that value back as a bogus
/// `/msys64/usr/share/aclocal` prefix and dies looking for its own macros
/// (e.g. `progtest.m4`), which aborts autoreconf. Forcing the correct POSIX
/// dirs sidesteps the Win32↔POSIX round-trip. `/usr/share/aclocal` holds the
/// automake/gettext macros; `/mingw64/share/aclocal` holds `pkg.m4` et al.
const MSYS_ACLOCAL_PATH: &str = "/usr/share/aclocal:/mingw64/share/aclocal";

/// Run autogen + configure + make + make install into the staging prefix.
fn build_autotools(src: &Path, name: &str, version: &str, deps: &Deps) {
    let marker = deps.prefix.join(format!(".built-{name}"));
    if marker.exists() {
        return;
    }
    println!("cargo:warning=building {name} from source");

    // Build out-of-tree in a per-OUT_DIR directory rather than in the vendored
    // source. libtool bakes the absolute install `libdir` into each `.la` file
    // at link time; an in-place build in the shared vendor tree lets one build
    // unit's OUT_DIR leak into another's `.la`, and since `cargo clean` only
    // wipes `target/`, stale paths survive there. A private build dir keeps
    // every unit (profile, workspace) self-contained and reproducible.
    let build_dir = deps.prefix.parent().unwrap().join("build").join(name);
    std::fs::create_dir_all(&build_dir).unwrap();

    // These projects derive PACKAGE_VERSION from `git describe`, falling back
    // to a `.tarball-version` file. The published crate ships the vendored
    // sources without their submodule `.git` links, so configure would abort
    // with "PACKAGE_VERSION is not defined" — supply the fallback.
    if !src.join(".git").exists() && !src.join(".tarball-version").exists() {
        std::fs::write(src.join(".tarball-version"), version).unwrap();
    }

    // Regenerate configure from the git checkout. On Windows we drive
    // `autoreconf` directly — the projects' hand-rolled autogen.sh runs a bare
    // `aclocal -I m4` that trips over MSYS2's gettext macro layout; autoreconf
    // discovers and orders the macros robustly. Elsewhere autogen.sh is fine.
    if !src.join("configure").exists() {
        if deps.windows {
            run(
                Command::new("sh")
                    .arg("-c")
                    .arg("autoreconf --install --force")
                    .current_dir(src)
                    .env("ACLOCAL_PATH", MSYS_ACLOCAL_PATH),
                &format!("{name} autoreconf"),
            );
        } else if src.join("autogen.sh").exists() {
            run(
                Command::new("./autogen.sh")
                    .current_dir(src)
                    .env("NOCONFIGURE", "1"),
                &format!("{name} autogen"),
            );
        }
    }

    let mut configure = shell(deps.windows, &deps.path(&src.join("configure")));
    configure
        .current_dir(&build_dir)
        .arg(format!("--prefix={}", deps.path(&deps.prefix)))
        .arg("--enable-static")
        .arg("--disable-shared")
        .arg("--without-cython")
        .env("PKG_CONFIG_PATH", deps.pkg_config_path())
        .env("CFLAGS", deps.cflags())
        .env("CPPFLAGS", deps.cflags())
        .env("LDFLAGS", deps.ldflags());
    // A stale-timestamp maintainer-mode rebuild can re-invoke aclocal from
    // configure/make, so keep the sanitized aclocal path in scope on Windows.
    if deps.windows {
        configure.env("ACLOCAL_PATH", MSYS_ACLOCAL_PATH);
        // Several of these projects also build command-line tools whose
        // Makefiles don't link the Windows system libraries they need, so they
        // fail on MinGW. Add them to LIBS (autoconf appends objects before LIBS,
        // so this resolves the tools' undefined symbols): ws2_32/iphlpapi for
        // sockets, ole32 for CoTaskMemFree (libimobiledevice's userpref.c uses
        // it via SHGetKnownFolderPath for the config dir), setupapi for
        // libirecovery's native win32 device enumeration (its configure only
        // adds -lsetupapi for host_os mingw32*, but under the MSYS2 shell the
        // host detects as msys, so irecovery.exe fails to link SetupDi*). The
        // static libraries we ultimately link are archives and unaffected.
        configure.env("LIBS", "-lws2_32 -liphlpapi -lole32 -lsetupapi");
    }
    run(&mut configure, &format!("{name} configure"));

    let jobs = env::var("NUM_JOBS").unwrap_or_else(|_| "4".into());
    let mut make = Command::new("make");
    make.current_dir(&build_dir).arg(format!("-j{jobs}"));
    if deps.windows {
        make.env("ACLOCAL_PATH", MSYS_ACLOCAL_PATH);
    }
    run(&mut make, &format!("{name} make"));
    // libirecovery installs a udev rule to a system dir by default, which fails
    // for a non-root user (e.g. CI). Redirect it into our staging prefix.
    let udevdir = deps.prefix.join("udev");
    std::fs::create_dir_all(&udevdir).ok();
    let mut make_install = Command::new("make");
    make_install
        .current_dir(&build_dir)
        .arg("install")
        .arg(format!("udevrulesdir={}", udevdir.display()));
    if deps.windows {
        make_install.env("ACLOCAL_PATH", MSYS_ACLOCAL_PATH);
    }
    run(&mut make_install, &format!("{name} make install"));
    std::fs::write(&marker, "").unwrap();
}

fn build_libzip(src: &Path, deps: &Deps) {
    let marker = deps.prefix.join(".built-libzip");
    if marker.exists() {
        return;
    }
    println!("cargo:warning=building libzip from source");
    let mut cfg = cmake::Config::new(src);
    cfg.define("BUILD_SHARED_LIBS", "OFF")
        .define("BUILD_TOOLS", "OFF")
        .define("BUILD_EXAMPLES", "OFF")
        .define("BUILD_DOC", "OFF")
        .define("BUILD_REGRESS", "OFF")
        .define("ENABLE_COMMONCRYPTO", "OFF")
        .define("ENABLE_GNUTLS", "OFF")
        .define("ENABLE_MBEDTLS", "OFF")
        .define("ENABLE_OPENSSL", "OFF")
        .define("ENABLE_BZIP2", "OFF")
        .define("ENABLE_LZMA", "OFF")
        .define("ENABLE_ZSTD", "OFF")
        .define("CMAKE_INSTALL_PREFIX", &deps.prefix)
        .define("CMAKE_POSITION_INDEPENDENT_CODE", "ON");
    if let Some(inc) = &deps.zlib_include {
        cfg.define("ZLIB_INCLUDE_DIR", inc);
    }
    let dst = cfg.build();
    // cmake crate installs into <dst>; mirror into our prefix if separate.
    let _ = dst;
    std::fs::write(&marker, "").unwrap();
}

/// Copy idevicerestore's `src/` into OUT_DIR and apply the `.patch` files from
/// `patches/idevicerestore/` in filename order. Patching a throwaway copy
/// keeps the vendored submodule pristine (clean `git status`, clean submodule
/// pins) while the patches live in this repo, reviewed and versioned. Returns
/// the patched project root (containing `src/`).
fn patch_idevicerestore_sources(manifest: &Path, vendor_src: &Path, out: &Path) -> PathBuf {
    let patch_dir = manifest.join("patches/idevicerestore");
    println!("cargo:rerun-if-changed={}", patch_dir.display());

    let root = out.join("idevicerestore-patched");
    if root.exists() {
        std::fs::remove_dir_all(&root).unwrap();
    }
    let dst = root.join("src");
    std::fs::create_dir_all(&dst).unwrap();
    for entry in std::fs::read_dir(vendor_src.join("src")).unwrap() {
        let path = entry.unwrap().path();
        if path.is_file() {
            std::fs::copy(&path, dst.join(path.file_name().unwrap())).unwrap();
        }
    }

    let mut patches: Vec<PathBuf> = std::fs::read_dir(&patch_dir)
        .unwrap()
        .map(|e| e.unwrap().path())
        .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("patch"))
        .collect();
    patches.sort();
    for patch in patches {
        println!("cargo:rerun-if-changed={}", patch.display());
        run(
            Command::new("git")
                .args(["apply", "--whitespace=nowarn"])
                .arg(&patch)
                .current_dir(&root),
            &format!("apply {}", patch.file_name().unwrap().to_string_lossy()),
        );
    }
    root
}

/// Compile idevicerestore's own C sources into a static archive and link it.
fn compile_idevicerestore(src: &Path, deps: &Deps) {
    let src_dir = src.join("src");
    let mut build = cc::Build::new();
    build
        .include(&src_dir)
        .include(deps.prefix.join("include"))
        // idevicerestore uses -Wno-multichar for its FourCC constants.
        .flag_if_supported("-Wno-multichar")
        .flag_if_supported("-Wno-deprecated-declarations")
        // Rename idevicerestore.c's main() so it doesn't clash with Rust's.
        .define("main", "idevicerestore_cli_main_unused")
        .define("HAVE_OPENSSL", None)
        // Function-detection defines normally emitted into autotools' config.h.
        // strcspn is standard C and present everywhere (incl. mingw).
        .define("HAVE_STRCSPN", None)
        // Feature checks configure.ac runs against libimobiledevice; our
        // vendored copy (1.4.0) has all of them. Without HAVE_IDEVICE_E_TIMEOUT
        // a quiet FDR channel returns IDEVICE_E_TIMEOUT (-7) which fdr.c then
        // treats as fatal, killing restores mid-transfer; HAVE_REVERSE_PROXY
        // selects libimobiledevice's reverse proxy over the legacy FDR path.
        .define("HAVE_IDEVICE_E_TIMEOUT", None)
        .define("HAVE_RESTORE_E_RECEIVE_TIMEOUT", None)
        .define("HAVE_ENUM_IDEVICE_CONNECTION_TYPE", None)
        .define("HAVE_REVERSE_PROXY", None)
        .define("PACKAGE_NAME", "\"idevicerestore\"")
        .define("PACKAGE_VERSION", "\"restorekit-vendored\"")
        .define("PACKAGE_STRING", "\"idevicerestore (restorekit)\"")
        .define("PACKAGE_URL", "\"https://libimobiledevice.org\"")
        .define(
            "PACKAGE_BUGREPORT",
            "\"https://github.com/libimobiledevice/idevicerestore/issues\"",
        );
    if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
        build.define("_DARWIN_BETTER_REALPATH", None);
    }
    if deps.windows {
        // idevicerestore includes the family's headers; match the static
        // linkage the libraries were built with so it doesn't emit dllimport
        // references.
        for def in WINDOWS_STATIC_DEFINES {
            build.define(def, None);
        }
        // Link our static libcurl without dllimport stubs (see cflags()).
        build.define("CURL_STATICLIB", None);
        // 64-bit file offsets so stat()/seek on a ~20 GB IPSW don't overflow
        // mingw's 32-bit defaults (see cflags()).
        build.define("_FILE_OFFSET_BITS", "64");
        // realpath/strsep/mkstemp are absent on mingw; idevicerestore ships
        // WIN32 fallbacks guarded by `#ifndef HAVE_*`, so leave these undefined.
    } else {
        build
            .define("HAVE_REALPATH", None)
            .define("HAVE_STRSEP", None)
            .define("HAVE_MKSTEMP", None);
    }
    for inc in [
        &deps.openssl_include,
        &deps.zlib_include,
        &deps.curl_include,
    ]
    .into_iter()
    .flatten()
    {
        build.include(inc);
    }

    let mut count = 0;
    for entry in std::fs::read_dir(&src_dir).unwrap() {
        let path = entry.unwrap().path();
        if path.extension().and_then(|e| e.to_str()) == Some("c") {
            build.file(&path);
            count += 1;
        }
    }
    assert!(
        count > 0,
        "no idevicerestore .c sources found in {src_dir:?}"
    );

    // Our log-capture shim (routes idevicerestore's logger to a Rust sink).
    let shim = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("csrc/log_capture.c");
    println!("cargo:rerun-if-changed={}", shim.display());
    build.file(&shim);

    build.compile("idevicerestore");
}

/// Compile usbmuxd's server sources (minus main.c/preflight.c) plus our shim
/// into a static archive so restorekit can run an in-process usbmuxd on Linux.
fn compile_usbmuxd(src: &Path, deps: &Deps) {
    let src_dir = src.join("src");
    let mut build = cc::Build::new();
    build
        .include(&src_dir)
        .include(deps.prefix.join("include"))
        .flag_if_supported("-Wno-unused-parameter")
        .flag_if_supported("-Wno-sign-compare")
        // usbmuxd's log.c defines `unsigned int log_level` which clashes with
        // idevicerestore's identically-named global. Rename it at the
        // preprocessor level to avoid a linker duplicate-symbol error.
        .define("log_level", "usbmuxd_log_level")
        .define("PACKAGE_NAME", "\"usbmuxd\"")
        .define("PACKAGE_VERSION", "\"restorekit-embedded\"")
        .define("PACKAGE_URL", "\"https://libimobiledevice.org\"")
        .define(
            "PACKAGE_BUGREPORT",
            "\"https://github.com/libimobiledevice/usbmuxd/issues\"",
        );
    // Do NOT define HAVE_LIBIMOBILEDEVICE — avoids pulling in preflight/lockdown.

    if deps.windows {
        // POSIX→Winsock compat shim headers first, plus the family's static
        // macros and 64-bit file offsets (see cflags()).
        build.include(PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("csrc/win_shim"));
        for def in WINDOWS_STATIC_DEFINES {
            build.define(def, None);
        }
        build
            .define("_FILE_OFFSET_BITS", "64")
            .define("WIN32_LEAN_AND_MEAN", None)
            // Our win_shim provides a localtime_r that bridges Winsock's 32-bit
            // tv_sec; take that path rather than the plain-localtime one.
            .define("HAVE_LOCALTIME_R", None)
            // mingw-w64 has clock_gettime; use it instead of utils.c's macOS
            // mach_absolute_time fallback.
            .define("HAVE_CLOCK_GETTIME", None);
    } else {
        build
            .define("HAVE_PPOLL", None)
            .define("HAVE_CLOCK_GETTIME", None)
            .define("HAVE_LOCALTIME_R", None);
    }

    // libusb-1.0 headers (usbmuxd includes <libusb.h> directly).
    if let Ok(output) = std::process::Command::new("pkg-config")
        .args(["--cflags-only-I", "libusb-1.0"])
        .output()
    {
        let flags = String::from_utf8_lossy(&output.stdout);
        for flag in flags.split_whitespace() {
            if let Some(dir) = flag.strip_prefix("-I") {
                build.include(dir);
            }
        }
    }

    let skip = ["main.c", "preflight.c"];
    let mut count = 0;
    for entry in std::fs::read_dir(&src_dir).unwrap() {
        let path = entry.unwrap().path();
        if path.extension().and_then(|e| e.to_str()) == Some("c") {
            let name = path.file_name().unwrap().to_str().unwrap();
            if skip.contains(&name) {
                continue;
            }
            build.file(&path);
            count += 1;
        }
    }
    assert!(count > 0, "no usbmuxd .c sources found in {src_dir:?}");

    // Our shim that provides main-loop functions + preflight stubs.
    let shim = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("csrc/usbmuxd_server.c");
    println!("cargo:rerun-if-changed={}", shim.display());
    build.file(&shim);

    build.compile("usbmuxd_server");
}

fn emit_link_directives(prefix: &Path) {
    println!(
        "cargo:rustc-link-search=native={}",
        prefix.join("lib").display()
    );

    // Static libimobiledevice stack + libzip, most-dependent first.
    for lib in [
        "imobiledevice-1.0",
        "tatsu",
        "irecovery-1.0",
        "usbmuxd-2.0",
        "imobiledevice-glue-1.0",
        "plist-2.0",
        "zip",
    ] {
        println!("cargo:rustc-link-lib=static={lib}");
    }

    // openssl/zlib/curl come from the vendored -sys crates' own link directives.

    // System libraries and frameworks.
    let target_os = env::var("CARGO_CFG_TARGET_OS");
    if target_os.as_deref() == Ok("macos") {
        for fw in ["CoreFoundation", "IOKit", "Security"] {
            println!("cargo:rustc-link-lib=framework={fw}");
        }
    } else if target_os.as_deref() == Ok("windows") {
        // libusb (static, from MSYS2) plus the Win32 libraries it, libcurl, and
        // OpenSSL pull in. libusb-1.0.a lives in the MinGW prefix, not our
        // staging dir, so point rustc's linker at that libdir (via pkg-config).
        if let Some(libdir) = mingw_libusb_libdir() {
            println!("cargo:rustc-link-search=native={libdir}");
        }
        println!("cargo:rustc-link-lib=static=usb-1.0");
        for lib in [
            "setupapi", "ole32", "ws2_32", "crypt32", "secur32", "bcrypt", "iphlpapi", "userenv",
            "advapi32",
        ] {
            println!("cargo:rustc-link-lib=dylib={lib}");
        }
    } else {
        println!("cargo:rustc-link-lib=dylib=usb-1.0");
        println!("cargo:rustc-link-lib=dylib=pthread");
    }
}

/// Ask pkg-config where MSYS2's static libusb (`libusb-1.0.a`) lives, so the
/// final rustc link can find it. Returns a native path rustc's linker accepts.
fn mingw_libusb_libdir() -> Option<String> {
    let out = Command::new("pkg-config")
        .args(["--variable=libdir", "libusb-1.0"])
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    let dir = String::from_utf8_lossy(&out.stdout).trim().to_string();
    (!dir.is_empty()).then_some(dir)
}

/// A Command that runs `script` — directly on Unix, or via MSYS2's `sh` on
/// Windows (where Rust's `Command` can't exec a shell script itself).
fn shell(windows: bool, script: &str) -> Command {
    if windows {
        let mut c = Command::new("sh");
        c.arg(script);
        c
    } else {
        Command::new(script)
    }
}

fn run(cmd: &mut Command, what: &str) {
    let status = cmd
        .status()
        .unwrap_or_else(|e| panic!("failed to spawn {what}: {e}"));
    if !status.success() {
        panic!("{what} failed with {status}");
    }
}