use std::env;
use std::process::Command;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
struct SdkVersion {
major: u32,
minor: u32,
}
impl SdkVersion {
const fn new(major: u32, minor: u32) -> Self {
Self { major, minor }
}
}
impl std::fmt::Display for SdkVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
fn parse_sdk_version(raw: &str) -> Option<SdkVersion> {
let mut parts = raw.trim().split('.');
let major = parts.next()?.trim().parse().ok()?;
let minor = match parts.next() {
Some(minor) => minor.trim().parse().ok()?,
None => 0,
};
Some(SdkVersion::new(major, minor))
}
fn detect_sdk_version() -> Option<SdkVersion> {
let output = Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-version"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
parse_sdk_version(&String::from_utf8_lossy(&output.stdout))
}
const SWIFT_DEPLOYMENT_TARGET: &str = "13.0";
const VERSION_FEATURES: [(&str, SdkVersion, Option<&str>); 7] = [
("CARGO_FEATURE_MACOS_13_0", SdkVersion::new(13, 0), None),
(
"CARGO_FEATURE_MACOS_14_0",
SdkVersion::new(14, 0),
Some("SCREENCAPTUREKIT_HAS_MACOS14_SDK"),
),
(
"CARGO_FEATURE_MACOS_14_2",
SdkVersion::new(14, 2),
Some("SCREENCAPTUREKIT_HAS_MACOS14_2_SDK"),
),
(
"CARGO_FEATURE_MACOS_14_4",
SdkVersion::new(14, 4),
Some("SCREENCAPTUREKIT_HAS_MACOS14_4_SDK"),
),
(
"CARGO_FEATURE_MACOS_15_0",
SdkVersion::new(15, 0),
Some("SCREENCAPTUREKIT_HAS_MACOS15_SDK"),
),
(
"CARGO_FEATURE_MACOS_15_2",
SdkVersion::new(15, 2),
Some("SCREENCAPTUREKIT_HAS_MACOS15_2_SDK"),
),
(
"CARGO_FEATURE_MACOS_26_0",
SdkVersion::new(26, 0),
Some("SCREENCAPTUREKIT_HAS_MACOS26_SDK"),
),
];
fn configure_swift_version_defines(sdk_version: Option<SdkVersion>) -> Vec<String> {
let mut define_flags: Vec<String> = Vec::new();
if sdk_version.is_some_and(|version| version >= SdkVersion::new(13, 1)) {
define_flags.push("-DSCREENCAPTUREKIT_HAS_MACOS13_1_SDK".to_string());
}
let mut stubbed_features: Vec<&str> = Vec::new();
for (cargo_feature, min_sdk, swift_define) in VERSION_FEATURES {
if env::var(cargo_feature).is_err() {
continue;
}
if sdk_version.is_some_and(|v| v >= min_sdk) {
if let Some(swift_define) = swift_define {
define_flags.push(format!("-D{swift_define}"));
}
} else {
stubbed_features.push(cargo_feature.trim_start_matches("CARGO_FEATURE_"));
}
}
if !stubbed_features.is_empty() {
warn_or_fail_for_stub_mode(sdk_version, &stubbed_features);
}
define_flags
}
fn warn_or_fail_for_stub_mode(sdk_version: Option<SdkVersion>, stubbed_features: &[&str]) {
let feature_list = stubbed_features.join(", ").to_lowercase();
let detected = sdk_version.map_or_else(|| "unknown".to_string(), |v| v.to_string());
panic!(
"screencapturekit: Cargo feature(s) [{feature_list}] require a newer macOS SDK \
than the detected SDK ({detected}). Install a matching Xcode, set DEVELOPER_DIR \
or SDKROOT to it, or remove the unsupported feature(s)."
);
}
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-env-changed=DEVELOPER_DIR");
println!("cargo:rerun-if-env-changed=SDKROOT");
if env::var("DOCS_RS").is_ok() {
return;
}
println!("cargo:rustc-link-lib=framework=ScreenCaptureKit");
let swift_dir = "swift-bridge";
let out_dir = env::var("OUT_DIR").unwrap();
println!("cargo:rerun-if-changed={swift_dir}");
if let Ok(output) = Command::new("swiftlint")
.args(["lint"])
.current_dir(swift_dir)
.output()
{
if !output.status.success() {
eprintln!(
"SwiftLint warnings:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
}
let sdk_version = detect_sdk_version();
assert!(
!sdk_version.is_some_and(|version| version < SdkVersion::new(13, 0)),
"screencapturekit: the Swift bridge requires the macOS 13.0 SDK or later; \
install a newer Xcode or point DEVELOPER_DIR/SDKROOT at it"
);
let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let swift_arch = match target_arch.as_str() {
"x86_64" => "x86_64",
"aarch64" => "arm64",
other => panic!(
"screencapturekit: unsupported target arch '{other}'. \
Expected x86_64 or aarch64."
),
};
let swift_triple = format!("{swift_arch}-apple-macosx{SWIFT_DEPLOYMENT_TARGET}");
let define_flags = configure_swift_version_defines(sdk_version);
let variant = if define_flags.is_empty() {
"base".to_string()
} else {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in define_flags.iter().flat_map(|flag| flag.bytes().chain([0])) {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("defines-{hash:016x}")
};
let sdk_key = sdk_version.map_or_else(
|| "unknown-sdk".to_string(),
|version| format!("sdk{}-{}", version.major, version.minor),
);
let swift_build_dir = format!("{out_dir}/swift-build-{swift_arch}-{sdk_key}-{variant}");
let mut swift_args: Vec<&str> = vec![
"build",
"-c",
"release",
"--triple",
&swift_triple,
"--package-path",
swift_dir,
"--scratch-path",
&swift_build_dir,
];
for flag in &define_flags {
swift_args.push("-Xswiftc");
swift_args.push(flag);
}
let output = Command::new("swift")
.args(&swift_args)
.output()
.expect("Failed to build Swift bridge");
if !output.status.success() {
eprintln!(
"Swift build STDOUT:\n{}",
String::from_utf8_lossy(&output.stdout)
);
eprintln!(
"Swift build STDERR:\n{}",
String::from_utf8_lossy(&output.stderr)
);
panic!(
"Swift build failed with exit code: {:?}",
output.status.code()
);
}
link_swift_bridge(&swift_build_dir);
}
fn link_swift_bridge(swift_build_dir: &str) {
println!("cargo:rustc-link-search=native={swift_build_dir}/release");
println!("cargo:rustc-link-lib=static=ScreenCaptureKitBridge");
println!("cargo:rustc-link-lib=framework=Foundation");
println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=CoreMedia");
println!("cargo:rustc-link-lib=framework=IOSurface");
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
match Command::new("xcode-select").arg("-p").output() {
Ok(output) if output.status.success() => {
let xcode_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let swift_lib_path = format!(
"{xcode_path}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx"
);
println!("cargo:rustc-link-arg=-Wl,-rpath,{swift_lib_path}");
let swift_lib_path_new =
format!("{xcode_path}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx");
println!("cargo:rustc-link-arg=-Wl,-rpath,{swift_lib_path_new}");
}
Ok(output) => {
println!(
"cargo:warning=`xcode-select -p` exited non-zero (status={:?}); \
the Swift Concurrency rpath will not be baked in. The resulting \
binary may fail at load time with `dyld: Library not loaded` \
unless Swift's concurrency runtime is on the system search \
path. Install the full Xcode (not just Command Line Tools), \
or set DEVELOPER_DIR to a valid Xcode path.",
output.status.code()
);
}
Err(err) => {
println!(
"cargo:warning=`xcode-select` could not be invoked ({err}); \
the Swift Concurrency rpath will not be baked in. The \
resulting binary may fail at load time with `dyld: Library \
not loaded`. Install Xcode and ensure xcode-select is on PATH."
);
}
}
}