Patch sysconfig data at install time (#9857)

## Summary

This PR reimplements
[`sysconfigpatcher`](https://github.com/bluss/sysconfigpatcher) in Rust
and applies it to our Python installations at install-time, ensuring
that the `sysconfig` data is more likely to be correct.

For now, we only rewrite prefixes (i.e., any path that starts with
`/install` gets rewritten to the correct absolute path for the current
machine).

Unlike `sysconfigpatcher`, this PR does not yet do any of the following:

- Patch `pkginfo` files.
- Change `clang` references to `cc`.

A few things that we should do as follow-ups, in my opinion:

1. Rewrite
[`AR`](https://github.com/bluss/sysconfigpatcher/blob/c1ebf8ab9274dcde255484d93ce0f1fd1f76a248/src/sysconfigpatcher.py#L61).
2. Remove `-isysroot`, which we already do for newer builds.
This commit is contained in:
Charlie Marsh
2024-12-13 14:36:22 -05:00
committed by GitHub
parent 5903ce5759
commit d2fb4c585d
13 changed files with 817 additions and 19 deletions
+213
View File
@@ -0,0 +1,213 @@
//! Patch `sysconfig` data in a Python installation.
//!
//! Inspired by: <https://github.com/bluss/sysconfigpatcher/blob/c1ebf8ab9274dcde255484d93ce0f1fd1f76a248/src/sysconfigpatcher.py#L137C1-L140C100>,
//! available under the MIT license:
//!
//! ```text
//! Copyright 2024 Ulrik Sverdrup "bluss"
//!
//! Permission is hereby granted, free of charge, to any person obtaining a copy of
//! this software and associated documentation files (the "Software"), to deal in
//! the Software without restriction, including without limitation the rights to
//! use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
//! the Software, and to permit persons to whom the Software is furnished to do so,
//! subject to the following conditions:
//!
//! The above copyright notice and this permission notice shall be included in all
//! copies or substantial portions of the Software.
//!
//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
//! FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
//! COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
//! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
//! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//! ```
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing::trace;
use crate::sysconfig::parser::{Error as ParseError, SysconfigData, Value};
mod cursor;
mod parser;
/// Update the `sysconfig` data in a Python installation.
pub(crate) fn update_sysconfig(
install_root: &Path,
major: u8,
minor: u8,
suffix: &str,
) -> Result<(), Error> {
// Find the `_sysconfigdata_` file in the Python installation.
let real_prefix = std::path::absolute(install_root)?;
let sysconfigdata = find_sysconfigdata(&real_prefix, major, minor, suffix)?;
trace!(
"Discovered `sysconfig` data at: {}",
sysconfigdata.display()
);
// Update the `_sysconfigdata_` file in-memory.
let contents = fs_err::read_to_string(&sysconfigdata)?;
let data = SysconfigData::from_str(&contents)?;
let data = patch_sysconfigdata(data, &real_prefix);
let contents = data.to_string_pretty()?;
// Write the updated `_sysconfigdata_` file.
let mut file = fs_err::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&sysconfigdata)?;
file.write_all(contents.as_bytes())?;
file.sync_data()?;
Ok(())
}
/// Find the `_sysconfigdata_` file in a Python installation.
///
/// For example, on macOS, returns `{real_prefix}/lib/python3.12/_sysconfigdata__darwin_darwin.py"`.
fn find_sysconfigdata(
real_prefix: &Path,
major: u8,
minor: u8,
suffix: &str,
) -> Result<PathBuf, Error> {
// Find the `lib` directory in the Python installation.
let lib = real_prefix
.join("lib")
.join(format!("python{major}.{minor}{suffix}"));
if !lib.exists() {
return Err(Error::MissingLib);
}
// Probe the `lib` directory for `_sysconfigdata_`.
for entry in lib.read_dir()? {
let entry = entry?;
if entry.path().extension().is_none_or(|ext| ext != "py") {
continue;
}
if !entry
.path()
.file_stem()
.and_then(|stem| stem.to_str())
.is_some_and(|stem| stem.starts_with("_sysconfigdata_"))
{
continue;
}
let metadata = entry.metadata()?;
if metadata.is_symlink() {
continue;
};
if metadata.is_file() {
return Ok(entry.path());
}
}
Err(Error::MissingSysconfigdata)
}
/// Patch the given `_sysconfigdata_` contents.
fn patch_sysconfigdata(mut data: SysconfigData, real_prefix: &Path) -> SysconfigData {
/// Update the `/install` prefix in a whitespace-separated string.
fn update_prefix(s: &str, real_prefix: &Path) -> String {
s.split_whitespace()
.map(|part| {
if let Some(rest) = part.strip_prefix("/install") {
if rest.is_empty() {
real_prefix.display().to_string()
} else {
real_prefix.join(&rest[1..]).display().to_string()
}
} else {
part.to_string()
}
})
.collect::<Vec<_>>()
.join(" ")
}
// Patch each value, as needed.
let mut count = 0;
for (key, value) in data.iter_mut() {
let Value::String(value) = value else {
continue;
};
let patched = update_prefix(value, real_prefix);
if *value != patched {
trace!("Updated `{key}` from `{value}` to `{patched}`");
count += 1;
*value = patched;
}
}
match count {
0 => trace!("No updates required"),
1 => trace!("Updated 1 value"),
n => trace!("Updated {n} values"),
}
// Mark the Python installation as standalone.
data.insert("PYTHON_BUILD_STANDALONE".to_string(), Value::Int(1));
data
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Python installation is missing a `lib` directory")]
MissingLib,
#[error("Python installation is missing a `_sysconfigdata_` file")]
MissingSysconfigdata,
#[error(transparent)]
Parse(#[from] ParseError),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::*;
#[test]
fn update_real_prefix() -> Result<(), Error> {
let sysconfigdata = [
("BASEMODLIBS", ""),
("BUILDPYTHON", "python.exe"),
("prefix", "/install/prefix"),
("exec_prefix", "/install/exec_prefix"),
("base", "/install/base"),
]
.into_iter()
.map(|(k, v)| (k.to_string(), Value::String(v.to_string())))
.collect::<SysconfigData>();
let real_prefix = Path::new("/real/prefix");
let data = patch_sysconfigdata(sysconfigdata, real_prefix);
insta::assert_snapshot!(data.to_string_pretty()?, @r###"
# system configuration generated and used by the sysconfig module
build_time_vars = {
"BASEMODLIBS": "",
"BUILDPYTHON": "python.exe",
"PYTHON_BUILD_STANDALONE": 1,
"base": "/real/prefix/base",
"exec_prefix": "/real/prefix/exec_prefix",
"prefix": "/real/prefix/prefix"
}
"###);
Ok(())
}
}