Skip to content
Open
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
1 change: 1 addition & 0 deletions src/build_context/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ impl BuildContextBuilder {
metadata24.requires_python.as_ref(),
&user_interpreters,
build_options.python.find_interpreter,
build_options.python.allow_prereleases,
has_import_lib_support,
);
resolver.resolve()
Expand Down
8 changes: 7 additions & 1 deletion src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ pub struct PythonOptions {
#[arg(short = 'f', long, conflicts_with = "interpreter")]
pub find_interpreter: bool,

/// Also build for prerelease (alpha/beta/rc) interpreters found by
/// `--find-interpreter`, which are skipped by default.
#[arg(long)]
pub allow_prereleases: bool,

/// Which kind of bindings to use.
#[arg(short, long, value_parser = ["pyo3", "pyo3-ffi", "cffi", "uniffi", "bin"])]
pub bindings: Option<String>,
Expand Down Expand Up @@ -594,7 +599,8 @@ mod tests {
let bridge = BridgeModel::Cffi;
let interpreter = vec![PathBuf::from("nonexistent-python-xyz")];

let resolver = InterpreterResolver::new(&target, &bridge, None, &interpreter, false, false);
let resolver =
InterpreterResolver::new(&target, &bridge, None, &interpreter, false, false, false);
let result = resolver.resolve();
let err_msg = result.unwrap_err().to_string();
assert_snapshot!(err_msg, @"Failed to find a python interpreter from `nonexistent-python-xyz`");
Expand Down
1 change: 1 addition & 0 deletions src/develop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> {
python: PythonOptions {
interpreter: vec![python.clone()],
find_interpreter: false,
allow_prereleases: false,
bindings,
},
platform: PlatformOptions {
Expand Down
44 changes: 44 additions & 0 deletions src/python_interpreter/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub(super) struct InterpreterMetadataMessage {
pub executable: Option<String>,
pub major: usize,
pub minor: usize,
pub releaselevel: String,
pub abiflags: Option<String>,
pub interpreter: String,
pub ext_suffix: Option<String>,
Expand Down Expand Up @@ -680,6 +681,7 @@ fn from_metadata_message(
runnable: true,
implementation_name: message.implementation_name,
soabi: message.soabi,
is_prerelease: message.releaselevel != "final",
}))
}

Expand Down Expand Up @@ -877,6 +879,7 @@ mod tests {
let message = |major, minor, platform: &str| InterpreterMetadataMessage {
major,
minor,
releaselevel: "final".to_string(),
interpreter: "cpython".to_string(),
implementation_name: "CPython".to_string(),
abiflags: None,
Expand Down Expand Up @@ -939,6 +942,7 @@ mod tests {
runnable: true,
implementation_name: "CPython".to_string(),
soabi: None,
is_prerelease: false,
}
);
}
Expand Down Expand Up @@ -984,6 +988,7 @@ mod tests {
runnable: true,
implementation_name: "CPython".to_string(),
soabi: None,
is_prerelease: false,
}
);
}
Expand All @@ -1004,6 +1009,7 @@ mod tests {
let message_314 = InterpreterMetadataMessage {
major: 3,
minor: 14,
releaselevel: "final".to_string(),
interpreter: "cpython".to_string(),
implementation_name: "CPython".to_string(),
abiflags: Some("".to_string()),
Expand All @@ -1025,6 +1031,7 @@ mod tests {
let message_314t = InterpreterMetadataMessage {
major: 3,
minor: 14,
releaselevel: "final".to_string(),
interpreter: "cpython".to_string(),
implementation_name: "CPython".to_string(),
abiflags: Some("t".to_string()),
Expand All @@ -1044,6 +1051,43 @@ mod tests {
assert!(interp.gil_disabled);
}

#[test]
fn test_interpreter_releaselevel_marks_prerelease() {
let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap();
let bridge = BridgeModel::PyO3(PyO3 {
crate_name: PyO3Crate::PyO3,
version: semver::Version::new(0, 26, 0),
stable_abi: None,
metadata: None,
});
let message = |releaselevel: &str| InterpreterMetadataMessage {
major: 3,
minor: 14,
releaselevel: releaselevel.to_string(),
interpreter: "cpython".to_string(),
implementation_name: "CPython".to_string(),
abiflags: Some("".to_string()),
ext_suffix: Some(".cpython-314-x86_64-linux-gnu.so".to_string()),
platform: "linux-x86_64".to_string(),
executable: None,
soabi: None,
gil_disabled: false,
system: "linux".to_string(),
};

for level in ["alpha", "beta", "candidate"] {
let interp = from_metadata_message("python3.14", &target, &bridge, message(level))
.unwrap()
.unwrap();
assert!(interp.is_prerelease, "{level} should be a prerelease");
}

let interp = from_metadata_message("python3.14", &target, &bridge, message("final"))
.unwrap()
.unwrap();
assert!(!interp.is_prerelease, "final should not be a prerelease");
}

#[test]
fn test_check_executables_single_missing() {
let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/python_interpreter/get_interpreter_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"executable": sys.executable or None,
"major": sys.version_info.major,
"minor": sys.version_info.minor,
"releaselevel": sys.version_info.releaselevel,
"abiflags": sysconfig.get_config_var("ABIFLAGS"),
"interpreter": platform.python_implementation().lower(),
"ext_suffix": ext_suffix,
Expand Down
6 changes: 6 additions & 0 deletions src/python_interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ pub struct PythonInterpreter {
pub implementation_name: String,
/// Comes from sysconfig var `SOABI`
pub soabi: Option<String>,
/// Whether this is a prerelease (alpha/beta/rc) interpreter, from
/// `sys.version_info.releaselevel`; `false` for non-probed placeholders
pub is_prerelease: bool,
}

impl Deref for PythonInterpreter {
Expand Down Expand Up @@ -318,6 +321,7 @@ impl PythonInterpreter {
runnable: false,
implementation_name,
soabi: None,
is_prerelease: false,
}
}

Expand Down Expand Up @@ -346,6 +350,7 @@ impl PythonInterpreter {
runnable: false,
implementation_name: "cpython".to_string(),
soabi: None,
is_prerelease: false,
}
}

Expand Down Expand Up @@ -551,6 +556,7 @@ mod tests {
runnable: false,
implementation_name: kind.to_string().to_ascii_lowercase(),
soabi: None,
is_prerelease: false,
}
}

Expand Down
27 changes: 25 additions & 2 deletions src/python_interpreter/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ pub struct InterpreterResolver<'a> {
requires_python: Option<&'a VersionSpecifiers>,
user_interpreters: &'a [PathBuf],
find_interpreter: bool,
allow_prereleases: bool,
has_import_lib_support: bool,
}

Expand All @@ -204,6 +205,7 @@ impl<'a> InterpreterResolver<'a> {
requires_python: Option<&'a VersionSpecifiers>,
user_interpreters: &'a [PathBuf],
find_interpreter: bool,
allow_prereleases: bool,
has_import_lib_support: bool,
) -> Self {
Self {
Expand All @@ -212,6 +214,7 @@ impl<'a> InterpreterResolver<'a> {
requires_python,
user_interpreters,
find_interpreter,
allow_prereleases,
has_import_lib_support,
}
}
Expand Down Expand Up @@ -334,6 +337,7 @@ impl<'a> InterpreterResolver<'a> {
runnable: false,
implementation_name,
soabi,
is_prerelease: false,
};
Ok((
vec![Candidate {
Expand Down Expand Up @@ -412,8 +416,26 @@ impl<'a> InterpreterResolver<'a> {
fn discover_native(&self, fixed_abi3: Option<(u8, u8)>) -> Result<DiscoveryResult> {
// --- Step 1+2: Find real interpreters with per-interpreter sysconfig fallback ---
let found = if self.find_interpreter {
super::discovery::find_all(self.target, self.bridge, self.requires_python)
.context("Finding python interpreters failed")?
let found = super::discovery::find_all(self.target, self.bridge, self.requires_python)
.context("Finding python interpreters failed")?;
// Skip prereleases (alpha/beta/rc) unless opted in, so `--find-interpreter`
// doesn't silently build wheels for preview Python (e.g. published to PyPI).
if self.allow_prereleases {
found
} else {
found
.into_iter()
.filter(|interp| {
if interp.is_prerelease {
eprintln!(
"⚠️ Skipping prerelease interpreter {interp}; \
pass `--allow-prereleases` to build for it"
);
}
!interp.is_prerelease
})
.collect()
}
} else {
self.find_specified_interpreters()?
};
Expand Down Expand Up @@ -862,6 +884,7 @@ impl<'a> InterpreterResolver<'a> {
runnable: false,
implementation_name: interpreter_kind.to_string().to_ascii_lowercase(),
soabi: soabi.cloned(),
is_prerelease: false,
})
}

Expand Down
4 changes: 4 additions & 0 deletions tests/cmd/build.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ Options:
-f, --find-interpreter
Find interpreters from the host machine

--allow-prereleases
Also build for prerelease (alpha/beta/rc) interpreters found by `--find-interpreter`,
which are skipped by default

-b, --bindings <BINDINGS>
Which kind of bindings to use

Expand Down
4 changes: 4 additions & 0 deletions tests/cmd/publish.stdout
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Options:
-f, --find-interpreter
Find interpreters from the host machine

--allow-prereleases
Also build for prerelease (alpha/beta/rc) interpreters found by `--find-interpreter`,
which are skipped by default

-b, --bindings <BINDINGS>
Which kind of bindings to use

Expand Down
Loading