|
| 1 | +//! Patch `sysconfig` data in a Python installation. |
| 2 | +//! |
| 3 | +//! Inspired by: <https://github.com/bluss/sysconfigpatcher/blob/c1ebf8ab9274dcde255484d93ce0f1fd1f76a248/src/sysconfigpatcher.py#L137C1-L140C100>, |
| 4 | +//! available under the MIT license: |
| 5 | +//! |
| 6 | +//! ```text |
| 7 | +//! Copyright 2024 Ulrik Sverdrup "bluss" |
| 8 | +//! |
| 9 | +//! Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 10 | +//! this software and associated documentation files (the "Software"), to deal in |
| 11 | +//! the Software without restriction, including without limitation the rights to |
| 12 | +//! use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 13 | +//! the Software, and to permit persons to whom the Software is furnished to do so, |
| 14 | +//! subject to the following conditions: |
| 15 | +//! |
| 16 | +//! The above copyright notice and this permission notice shall be included in all |
| 17 | +//! copies or substantial portions of the Software. |
| 18 | +//! |
| 19 | +//! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 20 | +//! IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
| 21 | +//! FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| 22 | +//! COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| 23 | +//! IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| 24 | +//! CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 25 | +//! ``` |
| 26 | +
|
| 27 | +use std::io::Write; |
| 28 | +use std::path::{Path, PathBuf}; |
| 29 | +use std::str::FromStr; |
| 30 | +use tracing::trace; |
| 31 | + |
| 32 | +use crate::sysconfig::parser::{Error as ParseError, SysconfigData, Value}; |
| 33 | + |
| 34 | +mod cursor; |
| 35 | +mod parser; |
| 36 | + |
| 37 | +/// Update the `sysconfig` data in a Python installation. |
| 38 | +pub(crate) fn update_sysconfig(install_root: &Path, major: u8, minor: u8) -> Result<(), Error> { |
| 39 | + // Find the `_sysconfigdata_` file in the Python installation. |
| 40 | + let real_prefix = std::path::absolute(install_root)?; |
| 41 | + let sysconfigdata = find_sysconfigdata(&real_prefix, major, minor)?; |
| 42 | + trace!( |
| 43 | + "Discovered `sysconfig` data at: {}", |
| 44 | + sysconfigdata.display() |
| 45 | + ); |
| 46 | + |
| 47 | + // Update the `_sysconfigdata_` file in-memory. |
| 48 | + let contents = std::fs::read_to_string(&sysconfigdata)?; |
| 49 | + let data = patch_sysconfigdata(&contents, &real_prefix)?; |
| 50 | + let contents = data.to_string_pretty()?; |
| 51 | + |
| 52 | + // Write the updated `_sysconfigdata_` file. |
| 53 | + let mut file = std::fs::OpenOptions::new() |
| 54 | + .write(true) |
| 55 | + .truncate(true) |
| 56 | + .create(true) |
| 57 | + .open(&sysconfigdata)?; |
| 58 | + file.write_all(contents.as_bytes())?; |
| 59 | + file.sync_data()?; |
| 60 | + |
| 61 | + Ok(()) |
| 62 | +} |
| 63 | + |
| 64 | +/// Find the `_sysconfigdata_` file in a Python installation. |
| 65 | +/// |
| 66 | +/// For example, on macOS, returns `{real_prefix}/lib/python3.12/_sysconfigdata__darwin_darwin.py"`. |
| 67 | +fn find_sysconfigdata(real_prefix: &Path, major: u8, minor: u8) -> Result<PathBuf, Error> { |
| 68 | + // Find the `lib` directory in the Python installation. |
| 69 | + let lib = real_prefix |
| 70 | + .join("lib") |
| 71 | + .join(format!("python{major}.{minor}")); |
| 72 | + if !lib.exists() { |
| 73 | + return Err(Error::MissingLib); |
| 74 | + } |
| 75 | + |
| 76 | + // Probe the `lib` directory for `_sysconfigdata_`. |
| 77 | + for entry in lib.read_dir()? { |
| 78 | + let entry = entry?; |
| 79 | + |
| 80 | + if entry.path().extension().is_none_or(|ext| ext != "py") { |
| 81 | + continue; |
| 82 | + } |
| 83 | + |
| 84 | + if !entry |
| 85 | + .path() |
| 86 | + .file_stem() |
| 87 | + .and_then(|stem| stem.to_str()) |
| 88 | + .is_some_and(|stem| stem.starts_with("_sysconfigdata_")) |
| 89 | + { |
| 90 | + continue; |
| 91 | + } |
| 92 | + |
| 93 | + let metadata = entry.metadata()?; |
| 94 | + if metadata.is_symlink() { |
| 95 | + continue; |
| 96 | + }; |
| 97 | + |
| 98 | + if metadata.is_file() { |
| 99 | + return Ok(entry.path()); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + Err(Error::MissingSysconfigdata) |
| 104 | +} |
| 105 | + |
| 106 | +/// Patch the given `_sysconfigdata_` contents. |
| 107 | +fn patch_sysconfigdata(contents: &str, real_prefix: &Path) -> Result<SysconfigData, Error> { |
| 108 | + /// Update the `/install` prefix in a whitespace-separated string. |
| 109 | + fn update_prefix(s: &str, real_prefix: &Path) -> String { |
| 110 | + s.split_whitespace() |
| 111 | + .map(|part| { |
| 112 | + if let Some(rest) = part.strip_prefix("/install") { |
| 113 | + if rest.is_empty() { |
| 114 | + real_prefix.display().to_string() |
| 115 | + } else { |
| 116 | + real_prefix.join(&rest[1..]).display().to_string() |
| 117 | + } |
| 118 | + } else { |
| 119 | + part.to_string() |
| 120 | + } |
| 121 | + }) |
| 122 | + .collect::<Vec<_>>() |
| 123 | + .join(" ") |
| 124 | + } |
| 125 | + |
| 126 | + // Parse the `_sysconfigdata_` file. |
| 127 | + let mut data = SysconfigData::from_str(contents)?; |
| 128 | + |
| 129 | + // Patch each value, as needed. |
| 130 | + let mut count = 0; |
| 131 | + for (key, value) in data.iter_mut() { |
| 132 | + let Value::String(value) = value else { |
| 133 | + continue; |
| 134 | + }; |
| 135 | + let patched = update_prefix(value, real_prefix); |
| 136 | + if *value != patched { |
| 137 | + trace!("Updated `{key}` from `{value}` to `{patched}`"); |
| 138 | + count += 1; |
| 139 | + *value = patched; |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + match count { |
| 144 | + 0 => trace!("No updates required"), |
| 145 | + 1 => trace!("Updated 1 value"), |
| 146 | + n => trace!("Updated {n} values"), |
| 147 | + } |
| 148 | + |
| 149 | + Ok(data) |
| 150 | +} |
| 151 | + |
| 152 | +#[derive(thiserror::Error, Debug)] |
| 153 | +pub enum Error { |
| 154 | + #[error(transparent)] |
| 155 | + Io(#[from] std::io::Error), |
| 156 | + #[error("Python installation is missing a `lib` directory")] |
| 157 | + MissingLib, |
| 158 | + #[error("Python installation is missing a `_sysconfigdata_` file")] |
| 159 | + MissingSysconfigdata, |
| 160 | + #[error(transparent)] |
| 161 | + Parse(#[from] ParseError), |
| 162 | + #[error(transparent)] |
| 163 | + Json(#[from] serde_json::Error), |
| 164 | +} |
0 commit comments