use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
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();
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(),
};
build_libzip(&vendor.join("libzip"), &deps);
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);
}
let idevicerestore_src =
patch_idevicerestore_sources(&manifest, &vendor.join("idevicerestore"), &out);
compile_idevicerestore(&idevicerestore_src, &deps);
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>,
windows: bool,
}
impl Deps {
fn path(&self, p: &Path) -> String {
if self.windows {
to_msys(p)
} else {
p.display().to_string()
}
}
fn pkg_config_path(&self) -> String {
self.path(&self.prefix.join("lib/pkgconfig"))
}
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))));
}
if self.windows {
for def in WINDOWS_STATIC_DEFINES {
flags.push_str(&format!(" -D{def}"));
}
flags.push_str(" -DCURL_STATICLIB");
flags.push_str(" -D_FILE_OFFSET_BITS=64");
}
if !self.windows {
flags.push_str(" -fPIC");
}
flags
}
fn ldflags(&self) -> String {
format!("-L{}", self.path(&self.prefix.join("lib")))
}
}
const WINDOWS_STATIC_DEFINES: &[&str] = &[
"LIBPLIST_STATIC",
"LIMD_GLUE_STATIC",
"LIBUSBMUXD_STATIC",
"IRECV_STATIC",
"LIBTATSU_STATIC",
"LIBIMOBILEDEVICE_STATIC",
];
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 vendor.join("idevicerestore/src/idevicerestore.c").exists() {
return;
}
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()
);
}
}
const MSYS_ACLOCAL_PATH: &str = "/usr/share/aclocal:/mingw64/share/aclocal";
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");
let build_dir = deps.prefix.parent().unwrap().join("build").join(name);
std::fs::create_dir_all(&build_dir).unwrap();
if !src.join(".git").exists() && !src.join(".tarball-version").exists() {
std::fs::write(src.join(".tarball-version"), version).unwrap();
}
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());
if deps.windows {
configure.env("ACLOCAL_PATH", MSYS_ACLOCAL_PATH);
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"));
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();
let _ = dst;
std::fs::write(&marker, "").unwrap();
}
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
}
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"))
.flag_if_supported("-Wno-multichar")
.flag_if_supported("-Wno-deprecated-declarations")
.define("main", "idevicerestore_cli_main_unused")
.define("HAVE_OPENSSL", None)
.define("HAVE_STRCSPN", None)
.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 {
for def in WINDOWS_STATIC_DEFINES {
build.define(def, None);
}
build.define("CURL_STATICLIB", None);
build.define("_FILE_OFFSET_BITS", "64");
} 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:?}"
);
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");
}
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")
.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\"",
);
if deps.windows {
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)
.define("HAVE_LOCALTIME_R", None)
.define("HAVE_CLOCK_GETTIME", None);
} else {
build
.define("HAVE_PPOLL", None)
.define("HAVE_CLOCK_GETTIME", None)
.define("HAVE_LOCALTIME_R", None);
}
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:?}");
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()
);
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}");
}
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") {
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");
}
}
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)
}
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}");
}
}