Skip to content

Commit 4fa668c

Browse files
committed
feat(pypi): environment markers are opaque conditions; build tools and backends come from the plan's generation; bundles keep patches and configopts
A PEP 508 marker becomes an opaque condition so the row is solver-excluded instead of an Always edge, a build backend or overlay build tool is named only when the plan's own toolchain hierarchy provides it, the request's package index reaches the plan in prepare, and a language bundle renders its patches, their checksums and configopts. Ported from develop.
1 parent 6ca1b18 commit 4fa668c

4 files changed

Lines changed: 131 additions & 23 deletions

File tree

‎src/package_emit.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ fn render_easyconfig(
258258
versionsuffix_line: &versionsuffix_line,
259259
toolchain_options_line: &toolchain_options_line,
260260
easyconfig_parameter_lines: &easyconfig_parameter_lines,
261+
patch_line: &patch_line,
262+
config_line: &config_line,
261263
build_dependencies: &build_dependencies,
262264
runtime_dependencies: &runtime_dependencies,
263265
moduleclass,
@@ -276,6 +278,8 @@ fn render_easyconfig(
276278
versionsuffix_line: &versionsuffix_line,
277279
toolchain_options_line: &toolchain_options_line,
278280
easyconfig_parameter_lines: &easyconfig_parameter_lines,
281+
patch_line: &patch_line,
282+
config_line: &config_line,
279283
build_dependencies: &build_dependencies,
280284
runtime_dependencies: &runtime_dependencies,
281285
moduleclass,
@@ -333,6 +337,8 @@ struct BundleFragments<'a> {
333337
versionsuffix_line: &'a str,
334338
toolchain_options_line: &'a str,
335339
easyconfig_parameter_lines: &'a str,
340+
patch_line: &'a str,
341+
config_line: &'a str,
336342
build_dependencies: &'a [String],
337343
runtime_dependencies: &'a [String],
338344
moduleclass: &'a str,
@@ -351,6 +357,8 @@ fn render_language_bundle(
351357
versionsuffix_line,
352358
toolchain_options_line,
353359
easyconfig_parameter_lines,
360+
patch_line,
361+
config_line,
354362
build_dependencies,
355363
runtime_dependencies,
356364
moduleclass,
@@ -382,6 +390,9 @@ fn render_language_bundle(
382390
};
383391
let exts = render_exts_list(plan, materialized, kind);
384392
let default_ext_opts = overlay_exts_default_options(plan, kind);
393+
// Bundle sources carry checksums inside exts_list. Top-level checksums
394+
// must still line up with `patches`, or EasyBuild has nothing to verify.
395+
let checksum_line = render_patch_checksums(&materialized.build.patches);
385396
let rendered = format!(
386397
"{easyblock_line}name = '{name}'\n\
387398
version = '{version}'\n\
@@ -393,6 +404,9 @@ toolchain = {{'name': '{toolchain_name}', 'version': '{toolchain_version}'}}\n\
393404
{easyconfig_parameter_lines}\
394405
{default_class}\
395406
{default_ext_opts}\
407+
{patch_line}\
408+
{checksum_line}\
409+
{config_line}\
396410
exts_list = {exts}\n\n\
397411
{build_dependencies}\
398412
{runtime_dependencies}\
@@ -916,6 +930,24 @@ fn is_tar_archive(filename: &str) -> bool {
916930
.any(|suffix| filename.ends_with(suffix))
917931
}
918932

933+
fn checksum_slot(digest: Option<&str>) -> String {
934+
match digest {
935+
Some(checksum) => format!("'{}'", escape_single(checksum)),
936+
None => "None".into(),
937+
}
938+
}
939+
940+
fn render_patch_checksums(patches: &[crate::package::PatchArtifact]) -> String {
941+
if patches.is_empty() {
942+
return String::new();
943+
}
944+
let checksums = patches
945+
.iter()
946+
.map(|patch| checksum_slot(patch.sha256.as_deref()))
947+
.collect::<Vec<_>>();
948+
format!("checksums = {}\n", render_multiline_list(&checksums))
949+
}
950+
919951
fn render_dependency(
920952
dependency: &crate::package::LockedDependency,
921953
package_toolchain: &Toolchain,

‎src/package_workflow.rs‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ pub fn prepare_new_package_plan(
228228
sbom = package_plan_to_cyclonedx(&plan)
229229
.map_err(|error| PackageWorkflowError::Sbom(error.to_string()))?;
230230
}
231+
plan.package_index = request.package_index.clone();
231232
Ok((plan, sbom))
232233
}
233234

@@ -292,16 +293,21 @@ pub fn complete_package_bundle_with_options(
292293
if matches!(plan.origin, PackageOrigin::Pypi | PackageOrigin::Cran) {
293294
promote_language_overlay_extras(&mut plan, candidates, stack_policy, hierarchy_fixture)?;
294295
}
296+
// Availability is generation-scoped: the later solve admits only
297+
// hierarchy members, so a CMake that exists only on foss-2023a must
298+
// not look available to a foss-2026.1 plan.
299+
let available = candidates_available_for_plan(&plan, candidates, hierarchy_fixture);
300+
let providers = crate::provides::language_provider_index(&available);
295301
if plan.origin == PackageOrigin::Pypi {
296-
inject_overlay_build_tools(&mut plan, candidates);
302+
inject_overlay_build_tools(&mut plan, &providers);
297303
}
298304
if plan.origin == PackageOrigin::Cargo {
299305
pin_binutils_to_gcccore(&mut plan, candidates, hierarchy_fixture);
300306
}
301307
adopt_moduleclass_from_tree(&mut plan, candidates);
302308
note_inferred_moduleclass(&mut plan);
303309
add_gcccore_binutils(&mut plan);
304-
add_build_backend_dependency(&mut plan, candidates);
310+
add_build_backend_dependency(&mut plan, &providers);
305311
drop_unavailable_build_requirements(&mut plan, candidates);
306312
let mut locks = Vec::new();
307313
for output in &plan.outputs {
@@ -453,7 +459,10 @@ fn drop_unavailable_build_requirements(
453459
/// names it: archspec declares `poetry.core.masonry.api` and its recipe
454460
/// carries `('poetry', '2.1.2')` beside binutils. `setuptools` is the
455461
/// exception, because the Python module already ships it.
456-
fn add_build_backend_dependency(plan: &mut PackagePlan, candidates: &[crate::domain::Candidate]) {
462+
fn add_build_backend_dependency(
463+
plan: &mut PackagePlan,
464+
providers: &std::collections::HashMap<String, &crate::domain::Candidate>,
465+
) {
457466
const BACKEND_MODULES: &[(&str, &str)] = &[
458467
("poetry", "poetry"),
459468
("hatchling", "hatchling"),
@@ -486,12 +495,10 @@ fn add_build_backend_dependency(plan: &mut PackagePlan, candidates: &[crate::dom
486495
.unwrap_or(dependency.name.as_str()),
487496
) == crate::provides::overlay_package_identity(module)
488497
});
489-
// Only what the tree can actually provide: naming a module the site does
490-
// not carry turns a buildable recipe into an unsatisfiable one.
491-
let available = candidates.iter().any(|candidate| {
492-
crate::provides::overlay_package_identity(&candidate.name)
493-
== crate::provides::overlay_package_identity(module)
494-
});
498+
// Only what this generation can actually provide: a backend shipped
499+
// only as a bundle extension still counts, and a first-class recipe
500+
// on another generation does not.
501+
let available = crate::provides::existing_language_provider_in(module, providers).is_some();
495502
if already || !available {
496503
return;
497504
}
@@ -642,7 +649,22 @@ fn pin_binutils_to_gcccore(
642649
}
643650
}
644651

645-
fn inject_overlay_build_tools(plan: &mut PackagePlan, candidates: &[crate::domain::Candidate]) {
652+
/// Candidates the plan's generation admits: its toolchain hierarchy only.
653+
fn candidates_available_for_plan(
654+
plan: &PackagePlan,
655+
candidates: &[crate::domain::Candidate],
656+
hierarchy_fixture: Option<&Path>,
657+
) -> Vec<crate::domain::Candidate> {
658+
match hierarchy_for_with_tree(&plan.build.toolchain, hierarchy_fixture, candidates) {
659+
Ok(hierarchy) => filter_candidates_in_hierarchy(candidates, &hierarchy),
660+
Err(_) => Vec::new(),
661+
}
662+
}
663+
664+
fn inject_overlay_build_tools(
665+
plan: &mut PackagePlan,
666+
providers: &std::collections::HashMap<String, &crate::domain::Candidate>,
667+
) {
646668
let meson = plan.build.build_systems.iter().any(|hint| {
647669
let hint = hint.to_ascii_lowercase();
648670
hint.contains("meson")
@@ -667,11 +689,7 @@ fn inject_overlay_build_tools(plan: &mut PackagePlan, candidates: &[crate::domai
667689
.unwrap_or(dependency.name.as_str()),
668690
) == crate::provides::overlay_package_identity(name)
669691
});
670-
let available = crate::provides::existing_language_provider(name, candidates).is_some()
671-
|| candidates.iter().any(|candidate| {
672-
crate::provides::overlay_package_identity(&candidate.name)
673-
== crate::provides::overlay_package_identity(name)
674-
});
692+
let available = crate::provides::existing_language_provider_in(name, providers).is_some();
675693
if already || !available {
676694
continue;
677695
}
@@ -818,8 +836,7 @@ pub fn plan_new_package_with(
818836
if request.easyconfig_roots.is_empty() {
819837
return Err(PackageWorkflowError::NoEasyconfigRoots);
820838
}
821-
let (mut plan, sbom) = prepare_new_package_plan(request)?;
822-
plan.package_index = request.package_index.clone();
839+
let (plan, sbom) = prepare_new_package_plan(request)?;
823840
let roots = request
824841
.easyconfig_roots
825842
.iter()

‎src/provides.rs‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
1414
use crate::domain::{Candidate, DepReq, ExtEntry};
1515
use std::collections::HashSet;
16+
use std::collections::HashMap;
1617

1718
/// Marker embedded in `easyconfig_path` for a virtual extension provide.
1819
pub const EXT_PROVIDE_MARKER: &str = "#ext:";
@@ -345,6 +346,45 @@ pub fn existing_language_provider<'a>(
345346
})
346347
}
347348

349+
/// Overlay-identity map for [`existing_language_provider_in`].
350+
///
351+
/// Bundle `exts_list` parents are keyed first so they win over a standalone
352+
/// first-class recipe, matching [`existing_language_provider`]. First-class
353+
/// names only fill identities no bundle already provides.
354+
pub fn language_provider_index(candidates: &[Candidate]) -> HashMap<String, &Candidate> {
355+
let mut index = HashMap::new();
356+
for candidate in candidates {
357+
if candidate.is_extension_provide() {
358+
continue;
359+
}
360+
for ext in &candidate.exts_list {
361+
if ext.version.is_empty() {
362+
continue;
363+
}
364+
index
365+
.entry(overlay_package_identity(&ext.name))
366+
.or_insert(candidate);
367+
}
368+
}
369+
for candidate in candidates {
370+
if candidate.is_extension_provide() {
371+
continue;
372+
}
373+
index
374+
.entry(overlay_package_identity(&candidate.name))
375+
.or_insert(candidate);
376+
}
377+
index
378+
}
379+
380+
/// [`existing_language_provider`] against a prebuilt index.
381+
pub fn existing_language_provider_in<'a>(
382+
name: &str,
383+
index: &HashMap<String, &'a Candidate>,
384+
) -> Option<&'a Candidate> {
385+
index.get(&overlay_package_identity(name)).copied()
386+
}
387+
348388
/// Collapse a selected extension provide to its parent bundle candidate.
349389
pub fn resolve_extension_provider<'a>(
350390
selected: &'a Candidate,

‎src/pypi.rs‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,20 @@ fn recipe_from_warehouse(value: &Value) -> Result<ForeignRecipe, ForeignError> {
135135
marker,
136136
original,
137137
} => {
138-
if let Some(marker) = marker {
138+
let condition = if let Some(marker) = marker {
139139
residuals.push(ForeignResidual {
140140
category: "pypi-marker".into(),
141141
severity: ResidualSeverity::Judgment,
142-
summary: format!("kept {name} with environment marker {marker}"),
142+
summary: format!(
143+
"{name} is gated by environment marker {marker} and does not constrain every profile"
144+
),
143145
evidence: Some(original.clone()),
144146
provenance: None,
145147
});
146-
}
148+
ConditionExpr::Opaque { source: marker }
149+
} else {
150+
ConditionExpr::Always
151+
};
147152
// The EasyBuild Python module installs setuptools, pip and
148153
// wheel itself, so a project that requires one already has it.
149154
// Emitting a dependency instead sends the solver looking for
@@ -166,7 +171,7 @@ fn recipe_from_warehouse(value: &Value) -> Result<ForeignRecipe, ForeignError> {
166171
pin,
167172
role: "run".into(),
168173
original_spec: Some(original),
169-
condition: ConditionExpr::Always,
174+
condition,
170175
provenance: Vec::new(),
171176
});
172177
}
@@ -210,18 +215,32 @@ fn recipe_from_warehouse(value: &Value) -> Result<ForeignRecipe, ForeignError> {
210215
Pep508::Requirement {
211216
name,
212217
pin,
218+
marker,
213219
original,
214-
..
215220
} => {
216221
if crate::provides::ignored_build_requirement(&name) {
217222
continue;
218223
}
224+
let condition = if let Some(marker) = marker {
225+
residuals.push(ForeignResidual {
226+
category: "pypi-marker".into(),
227+
severity: ResidualSeverity::Judgment,
228+
summary: format!(
229+
"{name} is gated by environment marker {marker} and does not constrain every profile"
230+
),
231+
evidence: Some(original.clone()),
232+
provenance: None,
233+
});
234+
ConditionExpr::Opaque { source: marker }
235+
} else {
236+
ConditionExpr::Always
237+
};
219238
dependencies.push(ForeignDep {
220239
name,
221240
pin,
222241
role: "build".into(),
223242
original_spec: Some(original),
224-
condition: ConditionExpr::Always,
243+
condition,
225244
provenance: Vec::new(),
226245
});
227246
}

0 commit comments

Comments
 (0)