Skip to content

Commit d65c146

Browse files
authored
feat: add dynamically generated sysconfig replacement mappings (#13441)
## Summary Implementation referenced in #12239 (comment) Closes #12919 #12901 This makes the sysconfig replacements mappings dynamically generated from https://github.com/astral-sh/python-build-standalone/blob/main/cpython-unix/targets.yml ## Test Plan cargo dev tests, and tested scenario from #12901 (comment)
1 parent 73eb2df commit d65c146

File tree

10 files changed

+387
-117
lines changed

10 files changed

+387
-117
lines changed

.github/workflows/sync-python-releases.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ jobs:
2828
env:
2929
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3030

31+
- name: Sync Sysconfig Targets
32+
run: ${{ github.workspace }}/crates/uv-dev/sync_sysconfig_targets.sh
33+
working-directory: ./crates/uv-dev
34+
env:
35+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
3137
- name: "Create Pull Request"
3238
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
3339
with:
3440
commit-message: "Sync latest Python releases"
3541
add-paths: |
3642
crates/uv-python/download-metadata.json
43+
crates/uv-dev/src/generate_sysconfig_mappings.rs
44+
crates/uv-python/src/sysconfig/generated_mappings.rs
3745
branch: "sync-python-releases"
3846
title: "Sync latest Python releases"
3947
body: "Automated update for Python releases."

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-dev/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ markdown = { version = "1.0.0" }
4444
owo-colors = { workspace = true }
4545
poloto = { version = "19.1.2", optional = true }
4646
pretty_assertions = { version = "1.4.1" }
47+
reqwest = { workspace = true }
4748
resvg = { version = "0.29.0", optional = true }
4849
schemars = { workspace = true }
4950
serde = { workspace = true }
5051
serde_json = { workspace = true }
52+
serde_yaml = { version = "0.9.34" }
5153
tagu = { version = "0.1.6", optional = true }
5254
textwrap = { workspace = true }
5355
tokio = { workspace = true }

crates/uv-dev/src/generate_all.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::Result;
44

55
use crate::{
66
generate_cli_reference, generate_env_vars_reference, generate_json_schema,
7-
generate_options_reference,
7+
generate_options_reference, generate_sysconfig_mappings,
88
};
99

1010
#[derive(clap::Args)]
@@ -26,10 +26,12 @@ pub(crate) enum Mode {
2626
DryRun,
2727
}
2828

29-
pub(crate) fn main(args: &Args) -> Result<()> {
29+
pub(crate) async fn main(args: &Args) -> Result<()> {
3030
generate_json_schema::main(&generate_json_schema::Args { mode: args.mode })?;
3131
generate_options_reference::main(&generate_options_reference::Args { mode: args.mode })?;
3232
generate_cli_reference::main(&generate_cli_reference::Args { mode: args.mode })?;
3333
generate_env_vars_reference::main(&generate_env_vars_reference::Args { mode: args.mode })?;
34+
generate_sysconfig_mappings::main(&generate_sysconfig_mappings::Args { mode: args.mode })
35+
.await?;
3436
Ok(())
3537
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
//! Generate sysconfig mappings for supported python-build-standalone *nix platforms.
2+
use anstream::println;
3+
use anyhow::{Result, bail};
4+
use pretty_assertions::StrComparison;
5+
use serde::Deserialize;
6+
use std::collections::BTreeMap;
7+
use std::fmt::Write;
8+
use std::path::PathBuf;
9+
10+
use crate::ROOT_DIR;
11+
use crate::generate_all::Mode;
12+
13+
/// Contains current supported targets
14+
const TARGETS_YML_URL: &str = "https://raw.githubusercontent.com/astral-sh/python-build-standalone/refs/tags/20250529/cpython-unix/targets.yml";
15+
16+
#[derive(clap::Args)]
17+
pub(crate) struct Args {
18+
#[arg(long, default_value_t, value_enum)]
19+
pub(crate) mode: Mode,
20+
}
21+
22+
#[derive(Debug, Deserialize)]
23+
struct TargetConfig {
24+
host_cc: Option<String>,
25+
host_cxx: Option<String>,
26+
target_cc: Option<String>,
27+
target_cxx: Option<String>,
28+
}
29+
30+
pub(crate) async fn main(args: &Args) -> Result<()> {
31+
let reference_string = generate().await?;
32+
let filename = "generated_mappings.rs";
33+
let reference_path = PathBuf::from(ROOT_DIR)
34+
.join("crates")
35+
.join("uv-python")
36+
.join("src")
37+
.join("sysconfig")
38+
.join(filename);
39+
40+
match args.mode {
41+
Mode::DryRun => {
42+
println!("{reference_string}");
43+
}
44+
Mode::Check => match fs_err::read_to_string(reference_path) {
45+
Ok(current) => {
46+
if current == reference_string {
47+
println!("Up-to-date: {filename}");
48+
} else {
49+
let comparison = StrComparison::new(&current, &reference_string);
50+
bail!(
51+
"{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{comparison}"
52+
);
53+
}
54+
}
55+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
56+
bail!("{filename} not found, please run `cargo dev generate-sysconfig-metadata`");
57+
}
58+
Err(err) => {
59+
bail!(
60+
"{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}"
61+
);
62+
}
63+
},
64+
Mode::Write => match fs_err::read_to_string(&reference_path) {
65+
Ok(current) => {
66+
if current == reference_string {
67+
println!("Up-to-date: {filename}");
68+
} else {
69+
println!("Updating: {filename}");
70+
fs_err::write(reference_path, reference_string.as_bytes())?;
71+
}
72+
}
73+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
74+
println!("Updating: {filename}");
75+
fs_err::write(reference_path, reference_string.as_bytes())?;
76+
}
77+
Err(err) => {
78+
bail!(
79+
"{filename} changed, please run `cargo dev generate-sysconfig-metadata`:\n{err}"
80+
);
81+
}
82+
},
83+
}
84+
85+
Ok(())
86+
}
87+
88+
async fn generate() -> Result<String> {
89+
println!("Downloading python-build-standalone cpython-unix/targets.yml ...");
90+
let body = reqwest::get(TARGETS_YML_URL).await?.text().await?;
91+
92+
let parsed: BTreeMap<String, TargetConfig> = serde_yaml::from_str(&body)?;
93+
94+
let mut replacements: BTreeMap<&str, BTreeMap<String, String>> = BTreeMap::new();
95+
96+
for targets_config in parsed.values() {
97+
for sysconfig_cc_entry in ["CC", "LDSHARED", "BLDSHARED", "LINKCC"] {
98+
if let Some(ref from_cc) = targets_config.host_cc {
99+
replacements
100+
.entry(sysconfig_cc_entry)
101+
.or_default()
102+
.insert(from_cc.to_string(), "cc".to_string());
103+
}
104+
if let Some(ref from_cc) = targets_config.target_cc {
105+
replacements
106+
.entry(sysconfig_cc_entry)
107+
.or_default()
108+
.insert(from_cc.to_string(), "cc".to_string());
109+
}
110+
}
111+
for sysconfig_cxx_entry in ["CXX", "LDCXXSHARED"] {
112+
if let Some(ref from_cxx) = targets_config.host_cxx {
113+
replacements
114+
.entry(sysconfig_cxx_entry)
115+
.or_default()
116+
.insert(from_cxx.to_string(), "c++".to_string());
117+
}
118+
if let Some(ref from_cxx) = targets_config.target_cxx {
119+
replacements
120+
.entry(sysconfig_cxx_entry)
121+
.or_default()
122+
.insert(from_cxx.to_string(), "c++".to_string());
123+
}
124+
}
125+
}
126+
127+
let mut output = String::new();
128+
129+
// Opening statements
130+
output.push_str("//! DO NOT EDIT\n");
131+
output.push_str("//!\n");
132+
output.push_str("//! Generated with `cargo run dev generate-sysconfig-metadata`\n");
133+
output.push_str("//! Targets from <https://github.com/astral-sh/python-build-standalone/blob/20250529/cpython-unix/targets.yml>\n");
134+
output.push_str("//!\n");
135+
136+
// Disable clippy/fmt
137+
output.push_str("#![allow(clippy::all)]\n");
138+
output.push_str("#![cfg_attr(any(), rustfmt::skip)]\n\n");
139+
140+
// Begin main code
141+
output.push_str("use std::collections::BTreeMap;\n");
142+
output.push_str("use std::sync::LazyLock;\n\n");
143+
output.push_str("use crate::sysconfig::replacements::{ReplacementEntry, ReplacementMode};\n\n");
144+
145+
output.push_str(
146+
"/// Mapping for sysconfig keys to lookup and replace with the appropriate entry.\n",
147+
);
148+
output.push_str("pub(crate) static DEFAULT_VARIABLE_UPDATES: LazyLock<BTreeMap<String, Vec<ReplacementEntry>>> = LazyLock::new(|| {\n");
149+
output.push_str(" BTreeMap::from_iter([\n");
150+
151+
// Add Replacement Entries for CC, CXX, etc.
152+
for (key, entries) in &replacements {
153+
writeln!(output, " (\"{key}\".to_string(), vec![")?;
154+
for (from, to) in entries {
155+
writeln!(
156+
output,
157+
" ReplacementEntry {{ mode: ReplacementMode::Partial {{ from: \"{from}\".to_string() }}, to: \"{to}\".to_string() }},"
158+
)?;
159+
}
160+
writeln!(output, " ]),")?;
161+
}
162+
163+
// Add AR case last
164+
output.push_str(" (\"AR\".to_string(), vec![\n");
165+
output.push_str(" ReplacementEntry {\n");
166+
output.push_str(" mode: ReplacementMode::Full,\n");
167+
output.push_str(" to: \"ar\".to_string(),\n");
168+
output.push_str(" },\n");
169+
output.push_str(" ]),\n");
170+
171+
// Closing
172+
output.push_str(" ])\n});\n");
173+
174+
Ok(output)
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use std::env;
180+
181+
use anyhow::Result;
182+
183+
use uv_static::EnvVars;
184+
185+
use crate::generate_all::Mode;
186+
187+
use super::{Args, main};
188+
189+
#[tokio::test]
190+
async fn test_generate_sysconfig_mappings() -> Result<()> {
191+
let mode = if env::var(EnvVars::UV_UPDATE_SCHEMA).as_deref() == Ok("1") {
192+
Mode::Write
193+
} else {
194+
Mode::Check
195+
};
196+
main(&Args { mode }).await
197+
}
198+
}

crates/uv-dev/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::generate_cli_reference::Args as GenerateCliReferenceArgs;
1111
use crate::generate_env_vars_reference::Args as GenerateEnvVarsReferenceArgs;
1212
use crate::generate_json_schema::Args as GenerateJsonSchemaArgs;
1313
use crate::generate_options_reference::Args as GenerateOptionsReferenceArgs;
14+
use crate::generate_sysconfig_mappings::Args as GenerateSysconfigMetadataArgs;
1415
#[cfg(feature = "render")]
1516
use crate::render_benchmarks::RenderBenchmarksArgs;
1617
use crate::wheel_metadata::WheelMetadataArgs;
@@ -22,6 +23,7 @@ mod generate_cli_reference;
2223
mod generate_env_vars_reference;
2324
mod generate_json_schema;
2425
mod generate_options_reference;
26+
mod generate_sysconfig_mappings;
2527
mod render_benchmarks;
2628
mod wheel_metadata;
2729

@@ -45,6 +47,8 @@ enum Cli {
4547
GenerateCliReference(GenerateCliReferenceArgs),
4648
/// Generate the environment variables reference for the documentation.
4749
GenerateEnvVarsReference(GenerateEnvVarsReferenceArgs),
50+
/// Generate the sysconfig metadata from derived targets.
51+
GenerateSysconfigMetadata(GenerateSysconfigMetadataArgs),
4852
#[cfg(feature = "render")]
4953
/// Render the benchmarks.
5054
RenderBenchmarks(RenderBenchmarksArgs),
@@ -57,11 +61,12 @@ pub async fn run() -> Result<()> {
5761
Cli::WheelMetadata(args) => wheel_metadata::wheel_metadata(args).await?,
5862
Cli::Compile(args) => compile::compile(args).await?,
5963
Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?,
60-
Cli::GenerateAll(args) => generate_all::main(&args)?,
64+
Cli::GenerateAll(args) => generate_all::main(&args).await?,
6165
Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
6266
Cli::GenerateOptionsReference(args) => generate_options_reference::main(&args)?,
6367
Cli::GenerateCliReference(args) => generate_cli_reference::main(&args)?,
6468
Cli::GenerateEnvVarsReference(args) => generate_env_vars_reference::main(&args)?,
69+
Cli::GenerateSysconfigMetadata(args) => generate_sysconfig_mappings::main(&args).await?,
6570
#[cfg(feature = "render")]
6671
Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?,
6772
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Fetch latest python-build-standalone tag
5+
latest_tag=$(curl -fsSL -H "Accept: application/json" https://github.com/astral-sh/python-build-standalone/releases/latest | jq -r .tag_name)
6+
7+
# Validate we got a tag name back
8+
if [[ -z "${latest_tag}" ]]; then
9+
echo "Error: Failed to fetch the latest tag from astral-sh/python-build-standalone." >&2
10+
exit 1
11+
fi
12+
13+
# Edit the sysconfig mapping endpoints
14+
sed -i.bak "s|refs/tags/[^/]\+/cpython-unix|refs/tags/${latest_tag}/cpython-unix|g" src/generate_sysconfig_mappings.rs && rm -f src/generate_sysconfig_mappings.rs.bak
15+
sed -i.bak "s|blob/[^/]\+/cpython-unix|blob/${latest_tag}/cpython-unix|g" src/generate_sysconfig_mappings.rs && rm -f src/generate_sysconfig_mappings.rs.bak
16+
17+
# Regenerate mappings in case there's any changes
18+
cargo dev generate-sysconfig-metadata

0 commit comments

Comments
 (0)