From 10d667fba5076a4d55d3d6e929f22c4f70145f27 Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Sat, 16 Nov 2019 00:22:45 -0800 Subject: [PATCH] Support link mode control As part of developing PyOxidizer, I needed to force python3-sys to statically link against a Python library on Windows in a downstream crate of python3-sys. This requires the unstable `static-nobundle` link type so Cargo leaves symbols as unresolved when python3-sys is built. (Currently, the `static` linkage type verifies referenced symbols are present at crate build time.) See https://github.com/rust-lang/rust/issues/37403 for more. Look for comments by me (@indygreg) to describe the issue in more detail. This commit teaches python3-sys a pair of new build features which enable more explicit control over the linker directives emitted by its build script. If no directive is specified, `link-mode-default` is used and the existing logic for linker directive emission is used. If `link-mode-unresolved-static` is used and we're on Windows, we emit a `static-nobundle=pythonXY` linker directive and omit the location of the library. This effectively says "I depend on a static `pythonXY` library but don't resolve the symbols when you build me and require someone else to specify the location to that library." What PyOxidizer does is emit its own linker directive that defines the location of a static `pythonXY` library, satisfying the linker constraint and enabling the build to work. If a downstream crate doesn't do this, the build should fail due to a missing library or symbols. I have purposefully designed the crate features to be extensible. If we want to add additional, mutually exclusive features in the future, we could do that. e.g. we could add a `link-mode-static` that force emits a `rustc-link-lib=static=pythonXY` directive to force static linking, even if a shared library is detected. But I have no need for this today and don't want to complicate the code, so I haven't added it. To round out the new feature, features have been added to the cpython crate to toggle the new features. Because Python 2.7 is end of life, I have not implemented the new feature for Python 2.7. I suspect very few people will use this feature anyway and I'm pretty confident that nobody will request this feature on Python 2.7. I concede that adding this feature to the crate to support PyOxidizer's esoteric use case is a bit unfortunate. I really wish Cargo allowed a crate to wholesale replace the build script output of a dependency, as PyOxidizer could statically resolve the Python settings for python3-sys since it brings its own Python library. But Cargo doesn't have this feature. So I'm stuck having to land this feature in the upstream crate to avoid having to maintain a permanent fork of `rust-cpython`. Sorry :/ --- Cargo.toml | 4 ++++ build.rs | 11 ++++++++++- python3-sys/Cargo.toml | 24 +++++++++++++++++++++++ python3-sys/build.rs | 43 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7296427c..006e64c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,10 @@ extension-module = [ "python3-sys/extension-module" ] # or python3-sys. (honestly, we should probably merge both crates into 'python-sys') extension-module-2-7 = [ "python27-sys/extension-module" ] +# Use these features to explicitly control linking for Python 3. +# (See the documentation in python3-sys/Cargo.toml for more info.) +py-link-mode-default = [ "python3-sys/link-mode-default" ] +py-link-mode-unresolved-static = [ "python3-sys/link-mode-unresolved-static" ] # Optional features to support explicitly specifying python minor version. # If you don't care which minor version, just specify python3-sys as a diff --git a/build.rs b/build.rs index d1e0ca6f..701e32c3 100644 --- a/build.rs +++ b/build.rs @@ -10,7 +10,16 @@ const PYTHONSYS_ENV_VAR: &'static str = "DEP_PYTHON27_PYTHON_FLAGS"; const PYTHONSYS_ENV_VAR: &'static str = "DEP_PYTHON3_PYTHON_FLAGS"; fn main() { - // python{27,3.x}-sys/build.rs passes python interpreter compile flags via + if cfg!(feature="python27-sys") { + if env::var_os("CARGO_FEATURE_PY_LINK_MODE_DEFAULT").is_some() || + env::var_os("CARGO_FEATURE_PY_LINK_MODE_UNRESOLVED_STATIC").is_some() { + writeln!(std::io::stderr(), + "Cannot use link mode control with Python 2.7"); + std::process::exit(1); + } + } + + // python{27,3.x}-sys/build.rs passes python interpreter compile flags via // environment variable (using the 'links' mechanism in the cargo.toml). let flags = match env::var(PYTHONSYS_ENV_VAR) { Ok(flags) => flags, diff --git a/python3-sys/Cargo.toml b/python3-sys/Cargo.toml index a06a85b3..b6c1ec0a 100644 --- a/python3-sys/Cargo.toml +++ b/python3-sys/Cargo.toml @@ -37,6 +37,30 @@ default = ["python-3"] # so that the module can also be used with statically linked python interpreters. extension-module = [ ] +# This feature implies default linking behavior. +# +# If not an extension module or on Windows, the crate will link against +# pythonXY where XY is derived from the discovered Python version. The link +# type will be static, shared, or framework depending on the discovered Python. +# +# The path to pythonXY from the discovered Python install may also be +# added to the linker search path. +# +# This link mode is used by default unless an alternate link mode feature is +# used. +link-mode-default = [] + +# This feature forces Python symbols to be unresolved by emitting a +# `rustc-link-lib=static-nobundle=pythonXY` directive on Windows (which +# is the only platform where it makes sense). +# +# This mode is useful for scenarios where you want another crate to emit +# the linker directives that define the location of a static Python library. +# +# This mode is typically not needed, as Python distributions on Windows +# rarely use a static Python library. +link-mode-unresolved-static = [] + # Bind to any python 3.x. python-3 = [] diff --git a/python3-sys/build.rs b/python3-sys/build.rs index 5cfbb6a5..40602d80 100644 --- a/python3-sys/build.rs +++ b/python3-sys/build.rs @@ -316,16 +316,43 @@ fn configure_from_path(expected_version: &PythonVersion) -> Result