@@ -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+
586617fn 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" ) ;
0 commit comments