Skip to content

Commit 0a00fb0

Browse files
committed
fix: retarget locked dependency toolchains
1 parent 1f3113f commit 0a00fb0

7 files changed

Lines changed: 283 additions & 43 deletions

File tree

‎src/eb_emit.rs‎

Lines changed: 249 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ pub struct EmitParams {
3737
/// Otherwise the operator already present on the source version (if any) is
3838
/// preserved and only the version token is replaced.
3939
pub dep_versions: HashMap<String, String>,
40+
/// Solver-selected dependency toolchains keyed by package name.
41+
/// Explicit tuples are retargeted, and selections outside the output
42+
/// hierarchy are made explicit in otherwise implicit tuples.
43+
pub dep_toolchains: HashMap<String, Toolchain>,
4044
/// New sha256 for the source tarball, used only when `version` changes.
4145
/// When `None` and the version changes, the source checksum entry's key
4246
/// is still renamed to the new versioned tarball name, but the checksum
@@ -92,9 +96,21 @@ pub fn emit_next_generation(source: &str, params: &EmitParams) -> Result<EmitRes
9296
if params.version.is_some() {
9397
text = rewrite_string_assign(&text, "version", &app_version)?;
9498
}
95-
if !params.dep_versions.is_empty() {
96-
text = rewrite_dep_list_versions(&text, "dependencies", &params.dep_versions)?;
97-
text = rewrite_dep_list_versions(&text, "builddependencies", &params.dep_versions)?;
99+
if !params.dep_versions.is_empty() || !params.dep_toolchains.is_empty() {
100+
text = rewrite_dep_list_selections(
101+
&text,
102+
"dependencies",
103+
&params.dep_versions,
104+
&params.dep_toolchains,
105+
&params.toolchain,
106+
)?;
107+
text = rewrite_dep_list_selections(
108+
&text,
109+
"builddependencies",
110+
&params.dep_versions,
111+
&params.dep_toolchains,
112+
&params.toolchain,
113+
)?;
98114
}
99115

100116
let mut warnings = Vec::new();
@@ -438,18 +454,25 @@ fn list_is_nonempty(src: &str, key: &str) -> bool {
438454
matches!(find_list_span(src, key), Ok(Some((s, e))) if !src[s..e].trim().is_empty())
439455
}
440456

441-
/// Rewrite version strings inside `key = [ ... ]` for named deps present in `overrides`.
442-
fn rewrite_dep_list_versions(
457+
/// Apply solver-selected versions and toolchains inside `key = [ ... ]`.
458+
fn rewrite_dep_list_selections(
443459
src: &str,
444460
key: &str,
445-
overrides: &HashMap<String, String>,
461+
version_overrides: &HashMap<String, String>,
462+
toolchain_overrides: &HashMap<String, Toolchain>,
463+
target_toolchain: &Toolchain,
446464
) -> Result<String, EmitError> {
447465
let Some((list_open_end, list_close_start)) = find_list_span(src, key)? else {
448466
// No such list — nothing to rewrite (not an error).
449467
return Ok(src.to_string());
450468
};
451469
let body = &src[list_open_end..list_close_start];
452-
let new_body = rewrite_dep_tuples_in_body(body, overrides)?;
470+
let new_body = rewrite_dep_tuples_in_body(
471+
body,
472+
version_overrides,
473+
toolchain_overrides,
474+
target_toolchain,
475+
)?;
453476
let mut out = String::with_capacity(src.len() + 32);
454477
out.push_str(&src[..list_open_end]);
455478
out.push_str(&new_body);
@@ -583,41 +606,221 @@ fn rewrite_source_checksum(
583606
Ok((out, stale))
584607
}
585608

609+
#[derive(Debug)]
610+
struct QuotedToken {
611+
start: usize,
612+
end: usize,
613+
depth: usize,
614+
quote: char,
615+
}
616+
586617
fn rewrite_dep_tuples_in_body(
587618
body: &str,
588-
overrides: &HashMap<String, String>,
619+
version_overrides: &HashMap<String, String>,
620+
toolchain_overrides: &HashMap<String, Toolchain>,
621+
target_toolchain: &Toolchain,
589622
) -> Result<String, EmitError> {
590-
// ('Name', 'ver') or ("Name", "ver") — no backreferences (regex crate).
591-
// Replace only the version (second) string when name is in overrides.
592-
let re = regex::Regex::new(
593-
r#"\(\s*'(?P<n1>[^']+)'\s*,\s*'(?P<v1>[^']+)'|\(\s*"(?P<n2>[^"]+)"\s*,\s*"(?P<v2>[^"]+)""#,
594-
)
595-
.map_err(|e| EmitError::Rewrite(e.to_string()))?;
623+
let mut rewritten = body.to_string();
624+
for (start, end) in dependency_tuple_spans(body)?.into_iter().rev() {
625+
let tuple = &body[start..end];
626+
let replacement = rewrite_dependency_tuple(
627+
tuple,
628+
version_overrides,
629+
toolchain_overrides,
630+
target_toolchain,
631+
)?;
632+
if replacement != tuple {
633+
rewritten.replace_range(start..end, &replacement);
634+
}
635+
}
636+
Ok(rewritten)
637+
}
596638

597-
let out = re.replace_all(body, |caps: &regex::Captures| {
598-
let full = caps.get(0).unwrap().as_str();
599-
let (name, ver, ver_name) = if let Some(n) = caps.name("n1") {
600-
(n.as_str(), caps.name("v1").unwrap().as_str(), "v1")
601-
} else {
602-
(
603-
caps.name("n2").unwrap().as_str(),
604-
caps.name("v2").unwrap().as_str(),
605-
"v2",
606-
)
607-
};
608-
if let Some(new_ver) = overrides.get(name) {
609-
let applied = apply_version_override(ver, new_ver);
610-
let ver_m = caps.name(ver_name).unwrap();
611-
let start = ver_m.start() - caps.get(0).unwrap().start();
612-
let end = ver_m.end() - caps.get(0).unwrap().start();
613-
let mut s = full.to_string();
614-
s.replace_range(start..end, &applied);
615-
s
616-
} else {
617-
full.to_string()
639+
fn dependency_tuple_spans(body: &str) -> Result<Vec<(usize, usize)>, EmitError> {
640+
let bytes = body.as_bytes();
641+
let mut spans = Vec::new();
642+
let mut depth = 0usize;
643+
let mut start = None;
644+
let mut quote = None;
645+
let mut escaped = false;
646+
let mut comment = false;
647+
for (index, byte) in bytes.iter().copied().enumerate() {
648+
if comment {
649+
if byte == b'\n' {
650+
comment = false;
651+
}
652+
continue;
653+
}
654+
if let Some(active_quote) = quote {
655+
if escaped {
656+
escaped = false;
657+
} else if byte == b'\\' {
658+
escaped = true;
659+
} else if byte == active_quote {
660+
quote = None;
661+
}
662+
continue;
663+
}
664+
match byte {
665+
b'#' => comment = true,
666+
b'\'' | b'"' => quote = Some(byte),
667+
b'(' => {
668+
if depth == 0 {
669+
start = Some(index);
670+
}
671+
depth += 1;
672+
}
673+
b')' => {
674+
if depth == 0 {
675+
return Err(EmitError::Rewrite(
676+
"unbalanced dependency tuple closing parenthesis".into(),
677+
));
678+
}
679+
depth -= 1;
680+
if depth == 0 {
681+
spans.push((start.take().expect("tuple start"), index + 1));
682+
}
683+
}
684+
_ => {}
685+
}
686+
}
687+
if depth != 0 || quote.is_some() {
688+
return Err(EmitError::Rewrite(
689+
"unclosed dependency tuple or string".into(),
690+
));
691+
}
692+
Ok(spans)
693+
}
694+
695+
fn rewrite_dependency_tuple(
696+
tuple: &str,
697+
version_overrides: &HashMap<String, String>,
698+
toolchain_overrides: &HashMap<String, Toolchain>,
699+
target_toolchain: &Toolchain,
700+
) -> Result<String, EmitError> {
701+
let (tokens, outer_commas) = dependency_tuple_tokens(tuple)?;
702+
let top = tokens
703+
.iter()
704+
.filter(|token| token.depth == 1)
705+
.collect::<Vec<_>>();
706+
if top.len() < 2 {
707+
return Ok(tuple.to_string());
708+
}
709+
let name = &tuple[top[0].start..top[0].end];
710+
let version_override = version_overrides.get(name);
711+
let toolchain_override = toolchain_overrides.get(name);
712+
if version_override.is_none() && toolchain_override.is_none() {
713+
return Ok(tuple.to_string());
714+
}
715+
716+
let mut edits = Vec::new();
717+
if let Some(version) = version_override {
718+
edits.push((
719+
top[1].start,
720+
top[1].end,
721+
apply_version_override(&tuple[top[1].start..top[1].end], version),
722+
));
723+
}
724+
if let Some(toolchain) = toolchain_override {
725+
let nested = tokens
726+
.iter()
727+
.filter(|token| token.depth == 2)
728+
.collect::<Vec<_>>();
729+
if nested.len() >= 2 {
730+
edits.push((nested[0].start, nested[0].end, toolchain.name.clone()));
731+
edits.push((nested[1].start, nested[1].end, toolchain.version.clone()));
732+
} else if dependency_toolchain_must_be_explicit(toolchain, target_toolchain) {
733+
if outer_commas >= 3 {
734+
return Err(EmitError::Rewrite(format!(
735+
"dependency {name} has an unsupported explicit toolchain expression"
736+
)));
737+
}
738+
let quote = top[0].quote;
739+
let suffix = if outer_commas == 1 {
740+
format!(
741+
", {quote}{quote}, ({quote}{}{quote}, {quote}{}{quote})",
742+
toolchain.name, toolchain.version
743+
)
744+
} else {
745+
format!(
746+
", ({quote}{}{quote}, {quote}{}{quote})",
747+
toolchain.name, toolchain.version
748+
)
749+
};
750+
edits.push((tuple.len() - 1, tuple.len() - 1, suffix));
751+
}
752+
}
753+
754+
edits.sort_by(|left, right| right.0.cmp(&left.0));
755+
let mut rewritten = tuple.to_string();
756+
for (start, end, replacement) in edits {
757+
rewritten.replace_range(start..end, &replacement);
758+
}
759+
Ok(rewritten)
760+
}
761+
762+
fn dependency_tuple_tokens(tuple: &str) -> Result<(Vec<QuotedToken>, usize), EmitError> {
763+
let bytes = tuple.as_bytes();
764+
let mut tokens = Vec::new();
765+
let mut depth = 0usize;
766+
let mut quote = None;
767+
let mut token_start = 0usize;
768+
let mut escaped = false;
769+
let mut comment = false;
770+
let mut outer_commas = 0usize;
771+
for (index, byte) in bytes.iter().copied().enumerate() {
772+
if comment {
773+
if byte == b'\n' {
774+
comment = false;
775+
}
776+
continue;
777+
}
778+
if let Some(active_quote) = quote {
779+
if escaped {
780+
escaped = false;
781+
} else if byte == b'\\' {
782+
escaped = true;
783+
} else if byte == active_quote {
784+
tokens.push(QuotedToken {
785+
start: token_start,
786+
end: index,
787+
depth,
788+
quote: active_quote as char,
789+
});
790+
quote = None;
791+
}
792+
continue;
618793
}
619-
});
620-
Ok(out.into_owned())
794+
match byte {
795+
b'#' => comment = true,
796+
b'\'' | b'"' => {
797+
quote = Some(byte);
798+
token_start = index + 1;
799+
}
800+
b'(' => depth += 1,
801+
b')' => depth = depth.saturating_sub(1),
802+
b',' if depth == 1 => outer_commas += 1,
803+
_ => {}
804+
}
805+
}
806+
if quote.is_some() {
807+
return Err(EmitError::Rewrite(
808+
"unclosed string in dependency tuple".into(),
809+
));
810+
}
811+
Ok((tokens, outer_commas))
812+
}
813+
814+
fn dependency_toolchain_must_be_explicit(selected: &Toolchain, target: &Toolchain) -> bool {
815+
if crate::hierarchy::is_system_toolchain(target) {
816+
return !crate::hierarchy::is_system_toolchain(selected);
817+
}
818+
if crate::hierarchy::is_system_toolchain(selected) {
819+
return true;
820+
}
821+
crate::hierarchy::hierarchy_for(target, None)
822+
.map(|hierarchy| !hierarchy.contains(selected))
823+
.unwrap_or_else(|_| !crate::hierarchy::toolchains_match(selected, target))
621824
}
622825

623826
#[cfg(test)]
@@ -665,6 +868,7 @@ builddependencies = [
665868
toolchain: foss("2025b"),
666869
version: None,
667870
dep_versions: HashMap::new(),
871+
dep_toolchains: HashMap::new(),
668872
source_checksum: None,
669873
};
670874
let r = emit_next_generation(MINIMAL, &params).expect("emit");
@@ -701,6 +905,7 @@ builddependencies = [
701905
toolchain: foss("2025b"),
702906
version: Some("2025.0".into()),
703907
dep_versions: HashMap::new(),
908+
dep_toolchains: HashMap::new(),
704909
source_checksum: None,
705910
};
706911
let r = emit_next_generation(MINIMAL, &params).expect("emit");
@@ -722,6 +927,7 @@ builddependencies = [
722927
toolchain: foss("2025b"),
723928
version: Some("2025.0".into()),
724929
dep_versions: deps,
930+
dep_toolchains: HashMap::new(),
725931
source_checksum: None,
726932
};
727933
let r = emit_next_generation(MINIMAL, &params).expect("emit");
@@ -742,6 +948,7 @@ builddependencies = [
742948
toolchain: foss("2025b"),
743949
version: None,
744950
dep_versions: deps,
951+
dep_toolchains: HashMap::new(),
745952
source_checksum: None,
746953
};
747954
let r = emit_next_generation(WITH_BUILDDEPS, &params).expect("emit");
@@ -761,6 +968,7 @@ builddependencies = [
761968
toolchain: foss("2025b"),
762969
version: None,
763970
dep_versions: deps,
971+
dep_toolchains: HashMap::new(),
764972
source_checksum: None,
765973
};
766974
let r = emit_next_generation(WITH_BUILDDEPS, &params).expect("emit");
@@ -782,6 +990,7 @@ builddependencies = [
782990
toolchain: foss("2025b"),
783991
version: Some("2.0".into()),
784992
dep_versions: HashMap::new(),
993+
dep_toolchains: HashMap::new(),
785994
source_checksum: None,
786995
};
787996
let r = emit_next_generation(src, &params).expect("emit");
@@ -823,6 +1032,7 @@ dependencies = [
8231032
toolchain: nvhpc("25.11-CUDA-12.8.0"),
8241033
version: Some("5.0.7".into()),
8251034
dep_versions: HashMap::new(),
1035+
dep_toolchains: HashMap::new(),
8261036
source_checksum: Some(
8271037
"119f2009936a403334d0df3c0d74d5595a32d99497f9b1d41e90019fee2fc2dd".into(),
8281038
),
@@ -849,6 +1059,7 @@ dependencies = [
8491059
toolchain: nvhpc("25.11-CUDA-12.8.0"),
8501060
version: Some("5.0.7".into()),
8511061
dep_versions: HashMap::new(),
1062+
dep_toolchains: HashMap::new(),
8521063
source_checksum: None,
8531064
};
8541065
let r = emit_next_generation(WITH_CHECKSUMS, &params).expect("emit");
@@ -867,6 +1078,7 @@ dependencies = [
8671078
toolchain: nvhpc("25.11-CUDA-12.8.0"),
8681079
version: None,
8691080
dep_versions: HashMap::new(),
1081+
dep_toolchains: HashMap::new(),
8701082
source_checksum: None,
8711083
};
8721084
let r = emit_next_generation(WITH_CHECKSUMS, &params).expect("emit");

‎src/manifest.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn package_plan_from_foreign(recipe: &ForeignRecipe, toolchain: &Toolchain)
3535
name: dependency.name.clone(),
3636
eb_name: None,
3737
constraint: canonical_version_constraint(recipe.format, dependency.pin.as_deref()),
38+
toolchain: None,
3839
roles: dependency_roles(&dependency.role),
3940
condition: dependency.condition.clone(),
4041
virtual_capability: foreign_virtual_capability(&dependency.name),

0 commit comments

Comments
 (0)