diff --git a/Makefile.pre.in b/Makefile.pre.in index 03c894268fea72..40d67c1e220134 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -3379,7 +3379,7 @@ Python/thread.o: @THREADHEADERS@ $(srcdir)/Python/condvar.h # Module dependencies and platform-specific files cpython-sys: Modules/cpython-sys/Cargo.toml Modules/cpython-sys/build.rs Modules/cpython-sys/wrapper.h Modules/cpython-sys/parser.h - CARGO_TARGET_DIR=$(abs_builddir)/target PYTHON_BUILD_DIR=$(abs_builddir) PY_CC="$(CC)" PY_CPPFLAGS="$(CPPFLAGS)" PY_CFLAGS="$(CFLAGS)" LLVM_TARGET="$(LLVM_TARGET)" $(CARGO_HOME)/bin/cargo build --lib --locked --package cpython-sys --profile $(CARGO_PROFILE) $(if $(CARGO_TARGET),--target=$(CARGO_TARGET)) --manifest-path $(srcdir)/Cargo.toml + CARGO_TARGET_DIR=$(abs_builddir)/target PYTHON_BUILD_DIR=$(abs_builddir) PY_CC="$(CC)" PY_CPPFLAGS="$(CPPFLAGS)" PY_CFLAGS="$(CFLAGS)" LLVM_TARGET="$(LLVM_TARGET)" IPHONEOS_DEPLOYMENT_TARGET=$(IPHONEOS_DEPLOYMENT_TARGET) $(CARGO_HOME)/bin/cargo build --lib --locked --package cpython-sys --profile $(CARGO_PROFILE) $(if $(CARGO_TARGET),--target=$(CARGO_TARGET)) --manifest-path $(srcdir)/Cargo.toml RUST_STATICLIB_A= target/$(if $(CARGO_TARGET),$(CARGO_TARGET)/$(CARGO_TARGET_DIR),$(CARGO_TARGET_DIR))/libcpython_rust_staticlib.a diff --git a/Modules/cpython-build-helper/src/lib.rs b/Modules/cpython-build-helper/src/lib.rs index 94883aaf4f8d82..e715f0ecb8755d 100644 --- a/Modules/cpython-build-helper/src/lib.rs +++ b/Modules/cpython-build-helper/src/lib.rs @@ -3,33 +3,39 @@ use std::env; /// Print necessary link arguments for the library depending on the build /// configuration (static or shared) pub fn print_linker_args() { - let target = env::var("TARGET").unwrap_or_default(); + println!("cargo:rerun-if-env-changed=RUST_SHARED_BUILD"); + println!("cargo:rerun-if-env-changed=BLDSHARED_EXE"); + println!("cargo:rerun-if-env-changed=BLDSHARED_ARGS"); + println!("cargo:rerun-if-env-changed=LIBPYTHON"); + println!("cargo:rerun-if-env-changed=PY_CC"); + println!("cargo:rerun-if-env-changed=PYTHON_BUILD_DIR"); + println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS"); + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let shared_build = + env::var("RUST_SHARED_BUILD").unwrap_or_default() == "1" || target_os == "ios"; // On Apple platforms (macOS, iOS), Cargo's cdylib produces a Mach-O // dynamiclib (via -dynamiclib), but CPython's C extensions are built as // bundles (via -bundle). Unlike bundles, dynamiclibs require all symbols // to be resolved at link time. Pass -undefined dynamic_lookup so that // Python C API symbols are resolved at load time by the interpreter. - if target.contains("apple") { + if target_os == "macos" || target_os == "ios" { println!("cargo:rustc-cdylib-link-arg=-undefined"); println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); } + // Apple framework builds for iOS encode framework search/link flags in + // BLDSHARED_EXE. Skip the linker executable itself and filter out flags + // that conflict with Cargo's own cdylib invocation. + if shared_build && let Ok(args) = env::var("BLDSHARED_EXE") { + print_link_args(&args, true); + } + // Pass platform-specific shared link arguments (e.g. PY_CORE_LDFLAGS) // from the CPython build system. if let Ok(args) = env::var("BLDSHARED_ARGS") { - let args = shlex::split(&args).expect("Invalid BLDSHARED_ARGS"); - let mut iter = args.iter(); - while let Some(arg) = iter.next() { - // -bundle_loader is incompatible with Cargo's cdylib on macOS - // (it only works with -bundle, not -dynamiclib). Skip it and - // its argument. - if arg == "-bundle_loader" { - iter.next(); // skip the path argument - continue; - } - println!("cargo:rustc-cdylib-link-arg={}", arg); - } + print_link_args(&args, false); } // On Android (and Cygwin), extension modules must link against libpython. @@ -52,3 +58,67 @@ pub fn print_linker_args() { // Static linker configuration is in cpython-rust-staticlib } + +fn print_link_args(raw: &str, skip_first: bool) { + let mut args = shlex::split(raw).expect("Invalid linker args"); + if skip_first { + args = strip_linker_executable(args, env::var("PY_CC").ok().as_deref()); + } + + let build_dir = env::var("PYTHON_BUILD_DIR").ok(); + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + // -bundle_loader only works with -bundle, not with Cargo's + // cdylib mode (-dynamiclib). + "-bundle_loader" => { + i += 2; + } + // Cargo chooses the Mach-O output type for cdylib already. + "-bundle" | "-dynamiclib" => { + i += 1; + } + // dynamic_lookup is added explicitly for Apple targets above. + "-undefined" if i + 1 < args.len() && args[i + 1] == "dynamic_lookup" => { + i += 2; + } + "-F" if i + 1 < args.len() => { + let value = if args[i + 1] == "." { + build_dir.clone().unwrap_or_else(|| ".".to_string()) + } else { + args[i + 1].clone() + }; + println!("cargo:rustc-cdylib-link-arg=-F"); + println!("cargo:rustc-cdylib-link-arg={value}"); + i += 2; + } + _ => { + println!("cargo:rustc-cdylib-link-arg={}", args[i]); + i += 1; + } + } + } +} + +// BLDSHARED_EXE may start with a wrapper/compiler command such as +// "xcrun --sdk iphoneos clang" or "ccache clang". Strip that prefix so only +// linker flags are forwarded to rustc. +fn strip_linker_executable(args: Vec, compiler: Option<&str>) -> Vec { + if let Some(compiler) = compiler + && let Some(prefix) = compiler_command_prefix(compiler) + && args.as_slice().starts_with(prefix.as_slice()) + { + return args[prefix.len()..].to_vec(); + } + + if args.is_empty() { + return args; + } + args[1..].to_vec() +} + +fn compiler_command_prefix(raw: &str) -> Option> { + let tokens = shlex::split(raw)?; + let last_non_flag = tokens.iter().rposition(|token| !token.starts_with('-'))?; + Some(tokens.into_iter().take(last_non_flag + 1).collect()) +} diff --git a/Modules/cpython-sys/build.rs b/Modules/cpython-sys/build.rs index 701a3be550ae0b..a97c1413914a02 100644 --- a/Modules/cpython-sys/build.rs +++ b/Modules/cpython-sys/build.rs @@ -9,6 +9,7 @@ fn main() { .expect("expected Modules/cpython-sys to live under the source tree"); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); let builddir = env::var("PYTHON_BUILD_DIR").ok(); + emit_rerun_instructions(builddir.as_deref()); if gil_disabled(srcdir, builddir.as_deref()) { println!("cargo:rustc-cfg=py_gil_disabled"); } @@ -16,6 +17,29 @@ fn main() { generate_c_api_bindings(srcdir, builddir.as_deref(), out_path.as_path()); } +// Bindgen depends on build-time env and, on iOS, can also inherit the +// deployment target from the generated Makefile. Declare both so Cargo reruns +// the build script when those inputs change. +fn emit_rerun_instructions(builddir: Option<&str>) { + for var in [ + "IPHONEOS_DEPLOYMENT_TARGET", + "LLVM_TARGET", + "PYTHON_BUILD_DIR", + "PY_CC", + "PY_CPPFLAGS", + "PY_CFLAGS", + "TARGET", + "WASI_SDK_PATH", + ] { + println!("cargo:rerun-if-env-changed={var}"); + } + + if let Some(builddir) = builddir { + let makefile = Path::new(builddir).join("Makefile"); + println!("cargo:rerun-if-changed={}", makefile.display()); + } +} + fn gil_disabled(srcdir: &Path, builddir: Option<&str>) -> bool { let mut candidates = Vec::new(); if let Some(build) = builddir { @@ -39,16 +63,13 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat // Suppress all clang warnings (deprecation warnings, etc.) builder = builder.clang_arg("-w"); - // Tell clang the correct target triple for cross-compilation. - // LLVM_TARGET is the clang/LLVM triple which may differ from the Rust - // target (e.g. arm64-apple-macosx vs aarch64-apple-darwin, or - // riscv64-unknown-linux-gnu vs riscv64gc-unknown-linux-gnu). - // Falls back to Cargo's TARGET if LLVM_TARGET is not set. - let target = env::var("LLVM_TARGET") - .or_else(|_| env::var("TARGET")) - .unwrap_or_default(); - if !target.is_empty() { - builder = builder.clang_arg(format!("--target={}", target)); + // Tell clang the correct target triple for cross-compilation when we have + // an LLVM-specific triple. Otherwise let bindgen translate Cargo's TARGET + // itself (e.g. aarch64-apple-ios-sim -> arm64-apple-ios-simulator). + let cargo_target = env::var("TARGET").unwrap_or_default(); + let llvm_target = env::var("LLVM_TARGET").unwrap_or_default(); + if !llvm_target.is_empty() && llvm_target != cargo_target { + builder = builder.clang_arg(format!("--target={llvm_target}")); } // Extract cross-compilation flags from the C compiler command (PY_CC), @@ -88,7 +109,7 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat // WASI SDK: WASI_SDK_PATH is set by Tools/wasm/wasi/__main__.py. // The sysroot is at $WASI_SDK_PATH/share/wasi-sysroot. if !have_sysroot - && target.contains("wasi") + && cargo_target.contains("wasi") && let Ok(sdk_path) = env::var("WASI_SDK_PATH") { let sysroot = PathBuf::from(&sdk_path).join("share").join("wasi-sysroot"); @@ -104,7 +125,7 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat // The sysroot is a sibling of bin/: // .../toolchains/llvm/prebuilt//sysroot if !have_sysroot - && target.contains("android") + && cargo_target.contains("android") && let Ok(cc) = env::var("PY_CC") && let Some(parts) = shlex::split(&cc) && let Some(binary) = parts.first() @@ -129,6 +150,7 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat for dir in include_dirs { builder = builder.clang_arg(format!("-I{}", dir.display())); } + builder = add_target_clang_args(builder, builddir); let bindings = builder .allowlist_function("_?Py.*") @@ -145,3 +167,40 @@ fn generate_c_api_bindings(srcdir: &Path, builddir: Option<&str>, out_path: &Pat .write_to_file(out_path.join("c_api.rs")) .expect("Couldn't write bindings!"); } + +fn add_target_clang_args( + mut builder: bindgen::Builder, + builddir: Option<&str>, +) -> bindgen::Builder { + let target = env::var("TARGET").unwrap_or_default(); + if !target.contains("apple-ios") { + return builder; + } + + // For iOS targets, bindgen may parse headers with an iOS simulator/device + // target but without a deployment minimum, which disables TLS support. + let deployment_target = ios_deployment_target(builddir).unwrap_or_else(|| "13.0".to_string()); + builder = builder.clang_arg(format!("-mios-version-min={deployment_target}")); + builder +} + +fn ios_deployment_target(builddir: Option<&str>) -> Option { + if let Ok(value) = env::var("IPHONEOS_DEPLOYMENT_TARGET") + && !value.is_empty() + { + return Some(value); + } + + let builddir = builddir?; + let makefile = Path::new(builddir).join("Makefile"); + let text = std::fs::read_to_string(makefile).ok()?; + for line in text.lines() { + if let Some(value) = line.strip_prefix("IPHONEOS_DEPLOYMENT_TARGET=") { + let value = value.trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + } + None +} diff --git a/Modules/makesetup b/Modules/makesetup index 4b5085d843f1e7..76edc54988d845 100755 --- a/Modules/makesetup +++ b/Modules/makesetup @@ -307,7 +307,7 @@ sed -e 's/[ ]*#.*//' -e '/^[ ]*$/d' | BUILT_SHARED="$BUILT_SHARED $mod" # depends on the headers through cpython-sys rule="$rust_shared: cpython-sys \$(srcdir)/Cargo.toml \$(srcdir)/Cargo.lock \$(srcdir)/$srcdir/$manifest $prefixed_srcs \$(PYTHON_HEADERS) \$(MODULE_${mods_upper}_LDEPS) \$(LIBRARY)" - rule="$rule; CARGO_TARGET_DIR=\$(abs_builddir)/target PYTHON_BUILD_DIR=\$(abs_builddir) BLDSHARED_ARGS=\"\$(BLDSHARED_ARGS)\" LIBPYTHON=\"\$(LIBPYTHON)\" \$(if \$(CARGO_TARGET_LINKER_ENV),\$(CARGO_TARGET_LINKER_ENV)=\$(CC)) \$(CARGO_HOME)/bin/cargo build -vvv --lib --locked --package ${mod} --profile \$(CARGO_PROFILE) \$(if \$(CARGO_TARGET),--target=\$(CARGO_TARGET)) --manifest-path \$(srcdir)/Cargo.toml" + rule="$rule; CARGO_TARGET_DIR=\$(abs_builddir)/target PYTHON_BUILD_DIR=\$(abs_builddir) PY_CC=\"\$(CC)\" PY_CPPFLAGS=\"\$(CPPFLAGS)\" PY_CFLAGS=\"\$(CFLAGS)\" LLVM_TARGET=\"\$(LLVM_TARGET)\" RUST_SHARED_BUILD=\$(PY_ENABLE_SHARED) IPHONEOS_DEPLOYMENT_TARGET=\$(IPHONEOS_DEPLOYMENT_TARGET) BLDSHARED_EXE=\"\$(BLDSHARED_EXE)\" BLDSHARED_ARGS=\"\$(BLDSHARED_ARGS)\" LIBPYTHON=\"\$(LIBPYTHON)\" \$(if \$(CARGO_TARGET_LINKER_ENV),\$(CARGO_TARGET_LINKER_ENV)=\$(CC)) \$(CARGO_HOME)/bin/cargo build -vvv --lib --locked --package ${mod} --profile \$(CARGO_PROFILE) \$(if \$(CARGO_TARGET),--target=\$(CARGO_TARGET)) --manifest-path \$(srcdir)/Cargo.toml" echo "$rule" >>$rulesf echo "$file: $rust_shared; mv $rust_shared $file" >>$rulesf done