Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete abi3 support with PyO3 0.13 #394

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions src/build_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ pub enum BridgeModel {
/// providing crate, e.g. pyo3.
Bindings(String),
/// `Bindings`, but specifically for pyo3 with feature flags that allow building a single wheel
/// for all cpython versions (pypy still needs multiple versions)
BindingsAbi3,
/// for all cpython versions (pypy still needs multiple versions).
/// The numbers are the minimum major and minor version
BindingsAbi3(u8, u8),
}

impl BridgeModel {
Expand Down Expand Up @@ -130,7 +131,12 @@ impl BuildContext {
let wheels = match &self.bridge {
BridgeModel::Cffi => vec![(self.build_cffi_wheel()?, "py3".to_string(), None)],
BridgeModel::Bin => vec![(self.build_bin_wheel()?, "py3".to_string(), None)],
BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3 => self.build_binding_wheels()?,
BridgeModel::Bindings(_) => self.build_binding_wheels()?,
BridgeModel::BindingsAbi3(major, minor) => vec![(
self.build_binding_wheel_abi3(*major, *minor)?,
format!("cp{}{}", major, minor),
None,
)],
};

Ok(wheels)
Expand All @@ -157,6 +163,45 @@ impl BuildContext {
}
}

/// For abi3 we only need to build a single wheel and we don't even need a python interpreter
/// for it
pub fn build_binding_wheel_abi3(&self, major: u8, min_minor: u8) -> Result<PathBuf> {
let artifact = self.compile_cdylib(None, Some(&self.module_name))?;

let platform = self.target.get_platform_tag(&self.manylinux);
let tag = format!("cp{}{}-abi3-{}", major, min_minor, platform);

let mut writer = WheelWriter::new(
&tag,
&self.out,
&self.metadata21,
&self.scripts,
&[tag.clone()],
)?;

write_bindings_module(
&mut writer,
&self.project_layout,
&self.module_name,
&artifact,
None,
&self.target,
false,
)
.context("Failed to add the files to the wheel")?;

let wheel_path = writer.finish()?;

println!(
"📦 Built wheel for abi3 Python ≥ {}.{} to {}",
major,
min_minor,
wheel_path.display()
);

Ok(wheel_path)
}

/// Builds wheels for a Cargo project for all given python versions.
/// Return type is the same as [BuildContext::build_wheels()]
///
Expand All @@ -172,8 +217,7 @@ impl BuildContext {
let artifact =
self.compile_cdylib(Some(&python_interpreter), Some(&self.module_name))?;

let tag = python_interpreter
.get_tag(&self.manylinux, self.bridge == BridgeModel::BindingsAbi3);
let tag = python_interpreter.get_tag(&self.manylinux);

let mut writer = WheelWriter::new(
&tag,
Expand All @@ -188,9 +232,9 @@ impl BuildContext {
&self.project_layout,
&self.module_name,
&artifact,
python_interpreter,
Some(&python_interpreter),
&self.target,
false,
self.bridge == BridgeModel::BindingsAbi3,
)
.context("Failed to add the files to the wheel")?;

Expand Down
83 changes: 33 additions & 50 deletions src/build_options.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::build_context::{BridgeModel, ProjectLayout};
use crate::python_interpreter::Interpreter;
use crate::BuildContext;
use crate::CargoToml;
use crate::Manylinux;
Expand Down Expand Up @@ -180,22 +179,34 @@ impl BuildOptions {
}

/// pyo3 supports building abi3 wheels if the unstable-api feature is not selected
fn has_abi3(cargo_metadata: &Metadata) -> Result<bool> {
let pyo3_packages = cargo_metadata
.packages
fn has_abi3(cargo_metadata: &Metadata) -> Result<Option<(u8, u8)>> {
let resolve = cargo_metadata
.resolve
.as_ref()
.context("Expected cargo to return metadata with resolve")?;
let pyo3_packages = resolve
.nodes
.iter()
.filter(|package| package.name == "pyo3")
.filter(|package| cargo_metadata[&package.id].name == "pyo3")
.collect::<Vec<_>>();
match pyo3_packages.as_slice() {
[pyo3_crate] => {
let root_id = cargo_metadata
.resolve
.as_ref()
.and_then(|resolve| resolve.root.as_ref())
.context("Expected cargo to return a root package")?;
// Check that we have a pyo3 version with abi3 and that abi3 is selected
Ok(pyo3_crate.features.contains_key("abi3")
&& !cargo_metadata[&root_id].features.contains_key("abi3"))
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
// This parser abi3-py{major}{minor} and returns the minimal (major, minor) tuple
Ok(pyo3_crate
.features
.iter()
.filter(|x| x.starts_with("abi3-py") && x.len() == "abi3-pyxx".len())
.map(|x| {
Ok((
(x.as_bytes()[7] as char).to_string().parse::<u8>()?,
(x.as_bytes()[8] as char).to_string().parse::<u8>()?,
))
})
.collect::<Result<Vec<(u8, u8)>>>()
.context("Bogus pyo3 cargo features")?
.into_iter()
.min())
}
_ => bail!(format!(
"Expected exactly one pyo3 dependency, found {}",
Expand Down Expand Up @@ -268,9 +279,12 @@ pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<Br
);
}

if has_abi3(&cargo_metadata)? {
println!("🔗 Found pyo3 bindings with abi3 support");
return Ok(BridgeModel::BindingsAbi3);
if let Some((major, minor)) = has_abi3(&cargo_metadata)? {
println!(
"🔗 Found pyo3 bindings with abi3 support for Python ≥ {}.{}",
major, minor
);
return Ok(BridgeModel::BindingsAbi3(major, minor));
} else {
println!("🔗 Found pyo3 bindings");
return Ok(bridge);
Expand Down Expand Up @@ -315,37 +329,6 @@ pub fn find_interpreter(

Ok(interpreter)
}
BridgeModel::BindingsAbi3 => {
let interpreter = if interpreter.is_empty() {
// Since we do not autodetect pypy anyway, we only need one interpreter.
// Ideally we'd pick the lowest supported version, but there's no way to
// specify that yet in pyo3 yet
let interpreter =
PythonInterpreter::check_executable(target.get_python(), &target, &bridge)
.context(format_err!(err_message))?
.ok_or_else(|| format_err!(err_message))?;
vec![interpreter]
} else {
let interpreter =
PythonInterpreter::check_executables(&interpreter, &target, &bridge)
.context("The given list of python interpreters is invalid")?;
// It's ok if there are pypy versions, but there should be either no or exactly one
// cpython version. We can build wheels for more since the minimum version is in
// the tag, but that is unnecessary
if interpreter
.iter()
.filter(|python| python.interpreter == Interpreter::CPython)
.count()
> 1
{
println!("⚠ You have more than one cpython version specified when compiling with abi3, \
while you only need to lowest compatible version to build a single abi3 wheel.");
}
interpreter
};

Ok(interpreter)
}
BridgeModel::Cffi => {
let executable = if interpreter.is_empty() {
target.get_python()
Expand All @@ -363,7 +346,7 @@ pub fn find_interpreter(

Ok(vec![interpreter])
}
BridgeModel::Bin => Ok(vec![]),
BridgeModel::Bin | BridgeModel::BindingsAbi3(_, _) => Ok(vec![]),
}
}

Expand Down Expand Up @@ -447,11 +430,11 @@ mod test {

assert!(matches!(
find_bridge(&pyo3_pure, None),
Ok(BridgeModel::BindingsAbi3)
Ok(BridgeModel::BindingsAbi3(3, 6))
));
assert!(matches!(
find_bridge(&pyo3_pure, Some("pyo3")),
Ok(BridgeModel::BindingsAbi3)
Ok(BridgeModel::BindingsAbi3(3, 6))
));
assert!(find_bridge(&pyo3_pure, Some("rust-cpython")).is_err());
}
Expand Down
11 changes: 9 additions & 2 deletions src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn compile(
// TODO: What do we do when there are multiple bin targets?
match bindings_crate {
BridgeModel::Bin => shared_args.push("--bins"),
BridgeModel::Cffi | BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3 => {
BridgeModel::Cffi | BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3(_, _) => {
shared_args.push("--lib")
}
}
Expand All @@ -44,7 +44,7 @@ pub fn compile(

// https://github.com/PyO3/pyo3/issues/88#issuecomment-337744403
if context.target.is_macos() {
if let BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3 = bindings_crate {
if let BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3(_, _) = bindings_crate {
let mac_args = &["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"];
rustc_args.extend(mac_args);
}
Expand Down Expand Up @@ -78,6 +78,13 @@ pub fn compile(
build_command.env("PYO3_PYTHON", &python_interpreter.executable);
}

if let BridgeModel::BindingsAbi3(_, _) = bindings_crate {
// If the target is not Windows, set PYO3_NO_PYTHON to speed up the build
if !context.target.is_windows() {
build_command.env("PYO3_NO_PYTHON", "1");
}
}

// rust-cpython, and legacy pyo3 versions
build_command.env("PYTHON_SYS_EXECUTABLE", &python_interpreter.executable);
}
Expand Down
28 changes: 22 additions & 6 deletions src/develop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub fn develop(
true,
)?;
}
BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3 => {
BridgeModel::Bindings(_) => {
let artifact = build_context
.compile_cdylib(Some(&interpreter), Some(&build_context.module_name))
.context(context)?;
Expand All @@ -91,20 +91,36 @@ pub fn develop(
&build_context.project_layout,
&build_context.module_name,
&artifact,
&interpreter,
Some(&interpreter),
&target,
true,
)?;
}
BridgeModel::BindingsAbi3(_, _) => {
let artifact = build_context
.compile_cdylib(None, Some(&build_context.module_name))
.context(context)?;

write_bindings_module(
&mut builder,
&build_context.project_layout,
&build_context.module_name,
&artifact,
None,
&target,
true,
build_context.bridge == BridgeModel::BindingsAbi3,
)?;
}
}

// Write dist-info directory so pip can interact with it
let tags = match build_context.bridge {
BridgeModel::Bindings(_) => {
vec![build_context.interpreter[0].get_tag(&build_context.manylinux, false)]
vec![build_context.interpreter[0].get_tag(&build_context.manylinux)]
}
BridgeModel::BindingsAbi3 => {
vec![build_context.interpreter[0].get_tag(&build_context.manylinux, true)]
BridgeModel::BindingsAbi3(major, minor) => {
let platform = target.get_platform_tag(&build_context.manylinux);
vec![format!("cp{}{}-abi3-{}", major, minor, platform)]
}
BridgeModel::Bin | BridgeModel::Cffi => {
build_context
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,11 @@ fn pep517(subcommand: PEP517Command) -> Result<()> {
let context = build_options.into_build_context(true, strip)?;
let tags = match context.bridge {
BridgeModel::Bindings(_) => {
vec![context.interpreter[0].get_tag(&context.manylinux, false)]
vec![context.interpreter[0].get_tag(&context.manylinux)]
}
BridgeModel::BindingsAbi3 => {
vec![context.interpreter[0].get_tag(&context.manylinux, true)]
BridgeModel::BindingsAbi3(major, minor) => {
let platform = context.target.get_platform_tag(&context.manylinux);
vec![format!("cp{}{}-abi3-{}", major, minor, platform)]
}
BridgeModel::Bin | BridgeModel::Cffi => {
context.target.get_universal_tags(&context.manylinux).1
Expand Down
17 changes: 14 additions & 3 deletions src/module_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,22 @@ pub fn write_bindings_module(
project_layout: &ProjectLayout,
module_name: &str,
artifact: &Path,
python_interpreter: &PythonInterpreter,
python_interpreter: Option<&PythonInterpreter>,
target: &Target,
develop: bool,
abi3: bool,
) -> Result<()> {
let so_filename = python_interpreter.get_library_name(&module_name, abi3);
let so_filename = match python_interpreter {
Some(python_interpreter) => python_interpreter.get_library_name(&module_name),
// abi3
None => {
if target.is_freebsd() || target.is_unix() {
format!("{base}.abi3.so", base = module_name)
} else {
// Apparently there is no tag for abi3 on windows
format!("{base}.pyd", base = module_name)
}
}
};

match project_layout {
ProjectLayout::Mixed(ref python_module) => {
Expand Down
22 changes: 4 additions & 18 deletions src/python_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,18 +359,11 @@ impl PythonInterpreter {
/// Don't ask me why or how, this is just what setuptools uses so I'm also going to use
///
/// If abi3 is true, cpython wheels use the generic abi3 with the given version as minimum
pub fn get_tag(&self, manylinux: &Manylinux, abi3: bool) -> String {
pub fn get_tag(&self, manylinux: &Manylinux) -> String {
match self.interpreter {
Interpreter::CPython => {
let platform = self.target.get_platform_tag(manylinux);
if abi3 {
format!(
"cp{major}{minor}-abi3-{platform}",
major = self.major,
minor = self.minor,
platform = platform
)
} else if self.target.is_unix() {
if self.target.is_unix() {
format!(
"cp{major}{minor}-cp{major}{minor}{abiflags}-{platform}",
major = self.major,
Expand Down Expand Up @@ -433,19 +426,12 @@ impl PythonInterpreter {
///
/// The pypy3 value appears to be wrong for Windows: instead of
/// e.g., ".pypy3-70-x86_64-linux-gnu.so", it is just ".pyd".
pub fn get_library_name(&self, base: &str, abi3: bool) -> String {
pub fn get_library_name(&self, base: &str) -> String {
match self.interpreter {
Interpreter::CPython => {
let platform = self.target.get_shared_platform_tag();

if abi3 {
if self.target.is_freebsd() || self.target.is_unix() {
format!("{base}.abi3.so", base = base)
} else {
// Apparently there is no tag for abi3 on windows
format!("{base}.pyd", base = base)
}
} else if self.target.is_freebsd() {
if self.target.is_freebsd() {
format!(
"{base}.cpython-{major}{minor}{abiflags}.so",
base = base,
Expand Down
Loading