//! Generate sysconfig mappings for supported python-build-standalone *nix platforms. use anstream::println; use anyhow::{Result, bail}; use pretty_assertions::StrComparison; use serde::Deserialize; use std::collections::BTreeMap; use std::fmt::Write; use std::path::PathBuf; use crate::ROOT_DIR; use crate::generate_all::Mode; /// Contains current supported targets const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20251209/cpython-unix/targets.yml"; #[derive(clap::Args)] pub(crate) struct Args { #[arg(long, default_value_t, value_enum)] pub(crate) mode: Mode, } #[derive(Debug, Deserialize)] struct TargetConfig { host_cc: Option, host_cxx: Option, target_cc: Option, target_cxx: Option, } pub(crate) async fn main(args: &Args) -> Result<()> { let reference_string = generate().await?; let filename = "generated_mappings.rs"; let reference_path = PathBuf::from(ROOT_DIR) .join("crates") .join("uv-python") .join("src") .join("sysconfig") .join(filename); match args.mode { Mode::DryRun => { println!("{reference_string}"); } Mode::Check => match fs_err::read_to_string(reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { let comparison = StrComparison::new(¤t, &reference_string); bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{comparison}" ); } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { bail!("{filename} not found, please run `cargo dev generate-sysconfig-metadata`"); } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}" ); } }, Mode::Write => match fs_err::read_to_string(&reference_path) { Ok(current) => { if current == reference_string { println!("Up-to-date: {filename}"); } else { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } } Err(err) if err.kind() == std::io::ErrorKind::NotFound => { println!("Updating: {filename}"); fs_err::write(reference_path, reference_string.as_bytes())?; } Err(err) => { bail!( "{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}" ); } }, } Ok(()) } async fn generate() -> Result { println!("Downloading python-build-standalone cpython-unix/targets.yml ..."); let body = reqwest::get(TARGETS_YML_URL).await?.text().await?; let parsed: BTreeMap = serde_yaml::from_str(&body)?; let mut replacements: BTreeMap<&str, BTreeMap> = BTreeMap::new(); for targets_config in parsed.values() { for sysconfig_cc_entry in ["CC", "LDSHARED", "BLDSHARED", "LINKCC"] { if let Some(ref from_cc) = targets_config.host_cc { replacements .entry(sysconfig_cc_entry) .or_default() .insert(from_cc.to_owned(), "cc".to_string()); } if let Some(ref from_cc) = targets_config.target_cc { replacements .entry(sysconfig_cc_entry) .or_default() .insert(from_cc.to_owned(), "cc".to_string()); } } for sysconfig_cxx_entry in ["CXX", "LDCXXSHARED"] { if let Some(ref from_cxx) = targets_config.host_cxx { replacements .entry(sysconfig_cxx_entry) .or_default() .insert(from_cxx.to_owned(), "c++".to_string()); } if let Some(ref from_cxx) = targets_config.target_cxx { replacements .entry(sysconfig_cxx_entry) .or_default() .insert(from_cxx.to_owned(), "c++".to_string()); } } } let mut output = String::new(); // Opening statements output.push_str("//! DO NOT EDIT\n"); output.push_str("//!\n"); output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n"); output.push_str("//! Targets from \n"); output.push_str("//!\n"); // Disable clippy/fmt output.push_str("#![allow(clippy::all)]\n"); output.push_str("#![cfg_attr(any(), rustfmt::skip)]\n\n"); // Begin main code output.push_str("use std::collections::BTreeMap;\n"); output.push_str("use std::sync::LazyLock;\n\n"); output.push_str("use crate::sysconfig::replacements::{ReplacementEntry, ReplacementMode};\n\n"); output.push_str( "/// Mapping for sysconfig keys to lookup and replace with the appropriate entry.\n", ); output.push_str("pub(crate) static DEFAULT_VARIABLE_UPDATES: LazyLock>> = LazyLock::new(|| {\n"); output.push_str(" BTreeMap::from_iter([\n"); // Add Replacement Entries for CC, CXX, etc. for (key, entries) in &replacements { writeln!(output, " (\"{key}\".to_string(), vec![")?; for (from, to) in entries { writeln!( output, " ReplacementEntry {{ mode: ReplacementMode::Partial {{ from: \"{from}\".to_string() }}, to: \"{to}\".to_string() }}," )?; } writeln!(output, " ]),")?; } // Add AR case last output.push_str(" (\"AR\".to_string(), vec![\n"); output.push_str(" ReplacementEntry {\n"); output.push_str(" mode: ReplacementMode::Full,\n"); output.push_str(" to: \"ar\".to_string(),\n"); output.push_str(" },\n"); output.push_str(" ]),\n"); // Closing output.push_str(" ])\n});\n"); Ok(output) } #[cfg(test)] mod tests { use std::env; use anyhow::Result; use uv_static::EnvVars; use crate::generate_all::Mode; use super::{Args, main}; #[tokio::test] async fn test_generate_sysconfig_mappings() -> Result<()> { // Skip this test in CI to avoid redundancy with the dedicated CI job if env::var_os(EnvVars::CI).is_some() { return Ok(()); } let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") { Mode::Write } else { Mode::Check }; main(&Args { mode }).await } }