Skip to content

Commit 10d667f

Browse files
committed
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 rust-lang/rust#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 :/
1 parent dabc06c commit 10d667f

File tree

4 files changed

+73
-9
lines changed

4 files changed

+73
-9
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ extension-module = [ "python3-sys/extension-module" ]
6565
# or python3-sys. (honestly, we should probably merge both crates into 'python-sys')
6666
extension-module-2-7 = [ "python27-sys/extension-module" ]
6767

68+
# Use these features to explicitly control linking for Python 3.
69+
# (See the documentation in python3-sys/Cargo.toml for more info.)
70+
py-link-mode-default = [ "python3-sys/link-mode-default" ]
71+
py-link-mode-unresolved-static = [ "python3-sys/link-mode-unresolved-static" ]
6872

6973
# Optional features to support explicitly specifying python minor version.
7074
# If you don't care which minor version, just specify python3-sys as a

build.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ const PYTHONSYS_ENV_VAR: &'static str = "DEP_PYTHON27_PYTHON_FLAGS";
1010
const PYTHONSYS_ENV_VAR: &'static str = "DEP_PYTHON3_PYTHON_FLAGS";
1111

1212
fn main() {
13-
// python{27,3.x}-sys/build.rs passes python interpreter compile flags via
13+
if cfg!(feature="python27-sys") {
14+
if env::var_os("CARGO_FEATURE_PY_LINK_MODE_DEFAULT").is_some() ||
15+
env::var_os("CARGO_FEATURE_PY_LINK_MODE_UNRESOLVED_STATIC").is_some() {
16+
writeln!(std::io::stderr(),
17+
"Cannot use link mode control with Python 2.7");
18+
std::process::exit(1);
19+
}
20+
}
21+
22+
// python{27,3.x}-sys/build.rs passes python interpreter compile flags via
1423
// environment variable (using the 'links' mechanism in the cargo.toml).
1524
let flags = match env::var(PYTHONSYS_ENV_VAR) {
1625
Ok(flags) => flags,

python3-sys/Cargo.toml

+24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,30 @@ default = ["python-3"]
3737
# so that the module can also be used with statically linked python interpreters.
3838
extension-module = [ ]
3939

40+
# This feature implies default linking behavior.
41+
#
42+
# If not an extension module or on Windows, the crate will link against
43+
# pythonXY where XY is derived from the discovered Python version. The link
44+
# type will be static, shared, or framework depending on the discovered Python.
45+
#
46+
# The path to pythonXY from the discovered Python install may also be
47+
# added to the linker search path.
48+
#
49+
# This link mode is used by default unless an alternate link mode feature is
50+
# used.
51+
link-mode-default = []
52+
53+
# This feature forces Python symbols to be unresolved by emitting a
54+
# `rustc-link-lib=static-nobundle=pythonXY` directive on Windows (which
55+
# is the only platform where it makes sense).
56+
#
57+
# This mode is useful for scenarios where you want another crate to emit
58+
# the linker directives that define the location of a static Python library.
59+
#
60+
# This mode is typically not needed, as Python distributions on Windows
61+
# rarely use a static Python library.
62+
link-mode-unresolved-static = []
63+
4064
# Bind to any python 3.x.
4165
python-3 = []
4266

python3-sys/build.rs

+35-8
Original file line numberDiff line numberDiff line change
@@ -316,16 +316,43 @@ fn configure_from_path(expected_version: &PythonVersion) -> Result<String, Strin
316316
let exec_prefix: &str = &lines[3];
317317

318318
let is_extension_module = env::var_os("CARGO_FEATURE_EXTENSION_MODULE").is_some();
319-
if !is_extension_module || cfg!(target_os="windows") {
320-
println!("{}", get_rustc_link_lib(&interpreter_version,
321-
ld_version, enable_shared == "1").unwrap());
322-
if libpath != "None" {
323-
println!("cargo:rustc-link-search=native={}", libpath);
324-
} else if cfg!(target_os="windows") {
325-
println!("cargo:rustc-link-search=native={}\\libs", exec_prefix);
326-
}
319+
let mut link_mode_default = env::var_os("CARGO_FEATURE_LINK_MODE_DEFAULT").is_some();
320+
let link_mode_unresolved_static = env::var_os("CARGO_FEATURE_LINK_MODE_UNRESOLVED_STATIC").is_some();
321+
322+
if link_mode_default && link_mode_unresolved_static {
323+
return Err("link-mode-default and link-mode-unresolved-static are mutually exclusive".to_owned());
327324
}
328325

326+
if !link_mode_default && !link_mode_unresolved_static {
327+
link_mode_default = true;
328+
}
329+
330+
if link_mode_default {
331+
if !is_extension_module || cfg!(target_os="windows") {
332+
println!("{}", get_rustc_link_lib(&interpreter_version,
333+
ld_version, enable_shared == "1").unwrap());
334+
if libpath != "None" {
335+
println!("cargo:rustc-link-search=native={}", libpath);
336+
} else if cfg!(target_os="windows") {
337+
println!("cargo:rustc-link-search=native={}\\libs", exec_prefix);
338+
}
339+
}
340+
}
341+
else if link_mode_unresolved_static {
342+
if cfg!(target_os="windows") {
343+
// static-nobundle requires a Nightly rustc up to at least
344+
// Rust 1.39 (https://github.com/rust-lang/rust/issues/37403).
345+
//
346+
// We need to use static linking on Windows to prevent symbol
347+
// name mangling. Otherwise Rust will prefix extern {} symbols
348+
// with __imp_. But if we used normal "static," we need a
349+
// pythonXY.lib at build time to package into the rlib.
350+
//
351+
// static-nobundle removes the build-time library requirement,
352+
// allowing a downstream consumer to provide the pythonXY library.
353+
println!("cargo:rustc-link-lib=static-nobundle=pythonXY");
354+
}
355+
}
329356

330357
if let PythonVersion { major: 3, minor: some_minor} = interpreter_version {
331358
if env::var_os("CARGO_FEATURE_PEP_384").is_some() {

0 commit comments

Comments
 (0)