Skip to content

Commit

Permalink
Zig for manylinux compliance without docker
Browse files Browse the repository at this point in the history
This allows compiling for a specified manylinux version by using `zig cc`s custom glibc targeting (https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html). You just add `--zig`, and it will target the policy you want, defaulting to manylinux2010. Example from ubuntu 20.04:

```
$ cargo run -- build -m test-crates/pyo3-pure/Cargo.toml -i python --no-sdist 2> /dev/null
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.6
🐍 Not using a specific python interpreter (With abi3, an interpreter is only required on windows)
📖 Found type stub file at pyo3_pure.pyi
📦 Built wheel for abi3 Python ≥ 3.6 to /home/konsti/maturin/test-crates/pyo3-pure/target/wheels/pyo3_pure-2.1.2-cp36-abi3-manylinux_2_24_x86_64.whl
$ cargo run -- build -m test-crates/pyo3-pure/Cargo.toml -i python --no-sdist --zig 2> /dev/null
🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.6
🐍 Not using a specific python interpreter (With abi3, an interpreter is only required on windows)
📖 Found type stub file at pyo3_pure.pyi
📦 Built wheel for abi3 Python ≥ 3.6 to /home/konsti/maturin/test-crates/pyo3-pure/target/wheels/pyo3_pure-2.1.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
```

Maturin has to be installed with `maturin[zig]` to get a compatible zig version.

Still missing:
 * musl (code is there, but untested)
 * pyproject.toml integration (do we want/need that?)
 * Documentation in the user guide
  • Loading branch information
konstin committed Dec 27, 2021
1 parent 133b3f9 commit a55de9e
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 17 deletions.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ classifiers = [
]
dependencies = ["toml~=0.10.2"]

[project.optional-dependencies]
zig = [
"ziglang~=0.9.0",
]

[project.urls]
"Source Code" = "https://github.com/PyO3/maturin"
Issues = "https://github.com/PyO3/maturin/issues"
Expand Down
3 changes: 2 additions & 1 deletion src/auditwheel/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ pub fn auditwheel_rs(
policy.fixup_musl_libc_so_name(target.target_arch());

if let Some(highest_policy) = highest_policy {
if policy.priority < highest_policy.priority {
// Don't recommend manylinux1 because rust doesn't support it anymore
if policy.priority < highest_policy.priority && highest_policy.name != "manylinux_2_5" {
println!(
"📦 Wheel is eligible for a higher priority tag. \
You requested {} but this wheel is eligible for {}",
Expand Down
2 changes: 1 addition & 1 deletion src/auditwheel/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl Display for Policy {
f.write_str(&self.name)
} else {
f.write_fmt(format_args!(
"{}(aka {})",
"{} (aka {})",
&self.name,
self.aliases.join(",")
))
Expand Down
4 changes: 3 additions & 1 deletion src/build_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ pub struct BuildContext {
pub release: bool,
/// Strip the library for minimum file size
pub strip: bool,
/// Whether to skip checking the linked libraries for manylinux/musllinux compliance
/// Skip checking the linked libraries for manylinux/musllinux compliance
pub skip_auditwheel: bool,
/// When compiling for manylinux, use zig as linker to ensure glibc version compliance
pub zig: bool,
/// Whether to use the the manylinux/musllinux or use the native linux tag (off)
pub platform_tag: Option<PlatformTag>,
/// Extra arguments that will be passed to cargo as `cargo rustc [...] [arg1] [arg2] --`
Expand Down
34 changes: 27 additions & 7 deletions src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ pub struct BuildOptions {
/// Don't check for manylinux compliance
#[structopt(long = "skip-auditwheel")]
pub skip_auditwheel: bool,
/// For manylinux targets, use zig to ensure compliance for the chosen manylinux version
///
/// Default to manylinux2010/manylinux_2_12 if you do not specify an `--compatibility`
///
/// Make sure you installed zig with `pip install maturin[zig]`
#[structopt(long)]
pub zig: bool,
/// The --target option for cargo
#[structopt(long, name = "TRIPLE", env = "CARGO_BUILD_TARGET")]
pub target: Option<String>,
Expand Down Expand Up @@ -96,6 +103,7 @@ impl Default for BuildOptions {
manifest_path: PathBuf::from("Cargo.toml"),
out: None,
skip_auditwheel: false,
zig: false,
target: None,
cargo_extra_args: Vec::new(),
rustc_extra_args: Vec::new(),
Expand Down Expand Up @@ -271,14 +279,25 @@ impl BuildOptions {
let strip = pyproject.map(|x| x.strip()).unwrap_or_default() || strip;
let skip_auditwheel =
pyproject.map(|x| x.skip_auditwheel()).unwrap_or_default() || self.skip_auditwheel;
let platform_tag = self.platform_tag.or_else(|| {
pyproject.and_then(|x| {
if x.compatibility().is_some() {
args_from_pyproject.push("compatibility");
}
x.compatibility()
let platform_tag = self
.platform_tag
.or_else(|| {
pyproject.and_then(|x| {
if x.compatibility().is_some() {
args_from_pyproject.push("compatibility");
}
x.compatibility()
})
})
});
.or(
// With zig we can compile to any glibc version that we want, so we pick the lowest
// one supported by the rust compiler
if self.zig {
Some(PlatformTag::manylinux2010())
} else {
None
},
);
if platform_tag == Some(PlatformTag::manylinux1()) {
eprintln!("⚠️ Warning: manylinux1 is unsupported by the Rust compiler.");
}
Expand All @@ -302,6 +321,7 @@ impl BuildOptions {
release,
strip,
skip_auditwheel,
zig: self.zig,
platform_tag,
cargo_extra_args,
rustc_extra_args,
Expand Down
77 changes: 75 additions & 2 deletions src/compile.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use crate::build_context::BridgeModel;
use crate::python_interpreter::InterpreterKind;
use crate::BuildContext;
use crate::PythonInterpreter;
use crate::{BuildContext, PlatformTag};
use anyhow::{anyhow, bail, Context, Result};
use fat_macho::FatWriter;
use fs_err::{self as fs, File};
use std::collections::HashMap;
use std::env;
use std::io::{BufReader, Read};
#[cfg(target_family = "unix")]
use std::fs::OpenOptions;
use std::io::{BufReader, Read, Write};
#[cfg(target_family = "unix")]
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
Expand Down Expand Up @@ -101,11 +105,70 @@ fn compile_universal2(
Ok(result)
}

/// We want to use `zig cc` as linker and c compiler. We want to call `python -m ziglang cc`, but
/// cargo only accepts a path to an executable as linker, so we add a wrapper script. We then also
/// use the wrapper script to pass arguments and substitute an unsupported argument.
///
/// We create different files for different args because otherwise cargo might skip recompiling even
/// if the linker target changed
fn prepare_zig_linker(context: &BuildContext) -> Result<PathBuf> {
let (zig_linker, target) = match context.platform_tag {
None | Some(PlatformTag::Linux) => (
"./zigcc-gnu.sh".to_string(),
"native-native-gnu".to_string(),
),
Some(PlatformTag::Musllinux { x, y }) => (
format!("./zigcc-musl-{}-{}.sh", x, y),
format!("native-native-musl.{}.{}", x, y),
),
Some(PlatformTag::Manylinux { x, y }) => (
format!("./zigcc-gnu-{}-{}.sh", x, y),
format!("native-native-gnu.{}.{}", x, y),
),
};

let zig_linker_dir = dirs::cache_dir()
// If the really is no cache dir, cwd will also do
.unwrap_or_else(|| PathBuf::from("."))
.join(env!("CARGO_PKG_NAME"))
.join(env!("CARGO_PKG_VERSION"));
fs::create_dir_all(&zig_linker_dir)?;
let zig_linker = zig_linker_dir.join(zig_linker);

#[cfg(target_family = "unix")]
let mut custom_linker_file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o700)
.open(&zig_linker)?;
#[cfg(not(target_family = "unix"))]
let mut custom_linker_file = File::create(&zig_linker)?;
// https://github.com/ziglang/zig/issues/10050#issuecomment-956204098
custom_linker_file.write_all(
format!(
r##"#!/bin/bash
python -m ziglang cc ${{@/-lgcc_s/-lunwind}} -target {}
"##,
target
)
.as_bytes(),
)?;
drop(custom_linker_file);
Ok(zig_linker)
}

fn compile_target(
context: &BuildContext,
python_interpreter: Option<&PythonInterpreter>,
bindings_crate: &BridgeModel,
) -> Result<HashMap<String, PathBuf>> {
let zig_linker = if context.zig && context.target.is_linux() {
Some(prepare_zig_linker(context).context("Failed to create zig linker wrapper")?)
} else {
None
};

let mut shared_args = vec!["--manifest-path", context.manifest_path.to_str().unwrap()];

shared_args.extend(context.cargo_extra_args.iter().map(String::as_str));
Expand Down Expand Up @@ -207,6 +270,16 @@ fn compile_target(
// but forwarding stderr is still useful in case there some non-json error
.stderr(Stdio::inherit());

if let Some(zig_linker) = zig_linker {
build_command.env("CC", &zig_linker);
let env_target = context
.target
.target_triple()
.to_uppercase()
.replace("-", "_");
build_command.env(format!("CARGO_TARGET_{}_LINKER", env_target), &zig_linker);
}

if let BridgeModel::BindingsAbi3(_, _) = bindings_crate {
let is_pypy = python_interpreter
.map(|p| p.interpreter_kind == InterpreterKind::PyPy)
Expand Down
3 changes: 2 additions & 1 deletion src/develop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub fn develop(
manifest_path: manifest_file.to_path_buf(),
out: None,
skip_auditwheel: false,
zig: false,
target: None,
cargo_extra_args,
rustc_extra_args,
Expand Down Expand Up @@ -111,7 +112,7 @@ pub fn develop(
// Y U NO accept windows path prefix, pip?
// Anyways, here's shepmasters stack overflow solution
// https://stackoverflow.com/a/50323079/3549270
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
fn adjust_canonicalization(p: impl AsRef<Path>) -> String {
p.as_ref().display().to_string()
}
Expand Down
6 changes: 3 additions & 3 deletions src/module_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ use fs_err::File;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::ffi::OsStr;
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
use std::fs::OpenOptions;
use std::io;
use std::io::{Read, Write};
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
Expand Down Expand Up @@ -164,7 +164,7 @@ impl ModuleWriter for PathWriter {

// We only need to set the executable bit on unix
let mut file = {
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
{
OpenOptions::new()
.create(true)
Expand Down
2 changes: 1 addition & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub mod other;
// Y U NO accept windows path prefix, pip?
// Anyways, here's shepmasters stack overflow solution
// https://stackoverflow.com/a/50323079/3549270
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
pub fn adjust_canonicalization(p: impl AsRef<Path>) -> String {
p.as_ref().display().to_string()
}
Expand Down

0 comments on commit a55de9e

Please sign in to comment.