1- use std:: fs ;
1+ use std:: { fs , ops :: Range } ;
22
33use fbuild_core:: path:: NormalizedPath ;
44
@@ -13,6 +13,116 @@ fn repo_root() -> NormalizedPath {
1313 )
1414}
1515
16+ fn yaml_indent ( line : & str ) -> usize {
17+ line. len ( ) - line. trim_start ( ) . len ( )
18+ }
19+
20+ fn yaml_mapping_entry ( line : & str ) -> Option < ( & str , & str ) > {
21+ let content = line. trim_start ( ) ;
22+ if content. is_empty ( ) || content. starts_with ( [ '#' , '-' ] ) {
23+ return None ;
24+ }
25+ let ( key, value) = content. split_once ( ':' ) ?;
26+ Some ( ( key. trim ( ) , value. trim ( ) ) )
27+ }
28+
29+ fn yaml_scalar ( value : & str ) -> & str {
30+ value
31+ . strip_prefix ( '"' )
32+ . and_then ( |value| value. strip_suffix ( '"' ) )
33+ . unwrap_or ( value)
34+ }
35+
36+ fn yaml_significant ( line : & str ) -> bool {
37+ let content = line. trim_start ( ) ;
38+ !content. is_empty ( ) && !content. starts_with ( '#' )
39+ }
40+
41+ fn yaml_mapping_block_in_scope (
42+ lines : & [ & str ] ,
43+ mut scope : Range < usize > ,
44+ mut entry_indent : usize ,
45+ path : & [ & str ] ,
46+ ) -> Option < ( Range < usize > , usize ) > {
47+ for key in path {
48+ let entry = scope. clone ( ) . find ( |& index| {
49+ yaml_indent ( lines[ index] ) == entry_indent
50+ && yaml_mapping_entry ( lines[ index] ) == Some ( ( * key, "" ) )
51+ } ) ?;
52+ let end = ( ( entry + 1 ) ..scope. end )
53+ . find ( |& index| {
54+ yaml_significant ( lines[ index] ) && yaml_indent ( lines[ index] ) <= entry_indent
55+ } )
56+ . unwrap_or ( scope. end ) ;
57+ scope = ( entry + 1 ) ..end;
58+ entry_indent += 2 ;
59+ }
60+
61+ Some ( ( scope, entry_indent) )
62+ }
63+
64+ fn yaml_mapping_block ( lines : & [ & str ] , path : & [ & str ] ) -> Option < ( Range < usize > , usize ) > {
65+ yaml_mapping_block_in_scope ( lines, 0 ..lines. len ( ) , 0 , path)
66+ }
67+
68+ fn yaml_mapping_value_in_scope < ' a > (
69+ lines : & [ & ' a str ] ,
70+ scope : Range < usize > ,
71+ entry_indent : usize ,
72+ path : & [ & str ] ,
73+ ) -> Option < & ' a str > {
74+ let ( key, parent_path) = path. split_last ( ) ?;
75+ let ( scope, entry_indent) =
76+ yaml_mapping_block_in_scope ( lines, scope, entry_indent, parent_path) ?;
77+ scope
78+ . filter_map ( |index| {
79+ ( yaml_indent ( lines[ index] ) == entry_indent)
80+ . then ( || yaml_mapping_entry ( lines[ index] ) )
81+ . flatten ( )
82+ } )
83+ . find_map ( |( candidate, value) | ( candidate == * key) . then ( || yaml_scalar ( value) ) )
84+ }
85+
86+ fn yaml_mapping_value < ' a > ( lines : & [ & ' a str ] , path : & [ & str ] ) -> Option < & ' a str > {
87+ yaml_mapping_value_in_scope ( lines, 0 ..lines. len ( ) , 0 , path)
88+ }
89+
90+ fn yaml_sequence_values < ' a > ( lines : & [ & ' a str ] , path : & [ & str ] ) -> Option < Vec < & ' a str > > {
91+ let ( scope, item_indent) = yaml_mapping_block ( lines, path) ?;
92+ Some (
93+ scope
94+ . filter_map ( |index| {
95+ ( yaml_indent ( lines[ index] ) == item_indent)
96+ . then ( || lines[ index] . trim_start ( ) . strip_prefix ( "- " ) )
97+ . flatten ( )
98+ . map ( yaml_scalar)
99+ } )
100+ . collect ( ) ,
101+ )
102+ }
103+
104+ fn yaml_step_mapping_value < ' a > (
105+ lines : & [ & ' a str ] ,
106+ step_name : & str ,
107+ path : & [ & str ] ,
108+ ) -> Option < & ' a str > {
109+ let ( steps, item_indent) = yaml_mapping_block ( lines, & [ "jobs" , "build" , "steps" ] ) ?;
110+ let step = steps. clone ( ) . find ( |& index| {
111+ if yaml_indent ( lines[ index] ) != item_indent {
112+ return false ;
113+ }
114+ let Some ( item) = lines[ index] . trim_start ( ) . strip_prefix ( "- " ) else {
115+ return false ;
116+ } ;
117+ yaml_mapping_entry ( item)
118+ . is_some_and ( |( key, value) | key == "name" && yaml_scalar ( value) == step_name)
119+ } ) ?;
120+ let step_end = ( ( step + 1 ) ..steps. end )
121+ . find ( |& index| yaml_significant ( lines[ index] ) && yaml_indent ( lines[ index] ) <= item_indent)
122+ . unwrap_or ( steps. end ) ;
123+ yaml_mapping_value_in_scope ( lines, ( step + 1 ) ..step_end, item_indent + 2 , path)
124+ }
125+
16126#[ test]
17127fn pyo3_029_policy_stays_target_python_independent ( ) {
18128 // FastLED/fbuild#1025: keep every cross-build branch explicit until
@@ -93,3 +203,62 @@ fn pyo3_029_policy_stays_target_python_independent() {
93203 ) ;
94204 }
95205}
206+
207+ #[ test]
208+ fn native_release_workflow_uses_current_cross_toolchains ( ) {
209+ let root = repo_root ( ) ;
210+ let workflow =
211+ fs:: read_to_string ( root. join ( ".github/workflows/template_native_build.yml" ) ) . unwrap ( ) ;
212+ let release_workflow =
213+ fs:: read_to_string ( root. join ( ".github/workflows/release-auto.yml" ) ) . unwrap ( ) ;
214+ let workflow_lines = workflow. lines ( ) . collect :: < Vec < _ > > ( ) ;
215+ let release_workflow_lines = release_workflow. lines ( ) . collect :: < Vec < _ > > ( ) ;
216+
217+ assert_eq ! (
218+ yaml_step_mapping_value( & workflow_lines, "Setup soldr" , & [ "with" , "version" ] ) ,
219+ Some ( "0.9.6" ) ,
220+ "the setup-soldr step needs soldr >= 0.9.5 for catalogue-v2 Apple SDK assets"
221+ ) ;
222+ assert_eq ! (
223+ yaml_mapping_value(
224+ & workflow_lines,
225+ & [ "jobs" , "build" , "env" , "SOLDR_TOOLCHAIN_ORIGIN" ]
226+ ) ,
227+ Some ( "https://zackees.github.io/soldr-toolchain" ) ,
228+ "Apple SDK prepare and build steps must share a job-scoped catalogue origin"
229+ ) ;
230+ for target in [
231+ "x86_64_unknown_linux_musl" ,
232+ "aarch64_unknown_linux_musl" ,
233+ "x86_64_apple_darwin" ,
234+ "aarch64_apple_darwin" ,
235+ ] {
236+ let target_cflags = format ! ( "CFLAGS_{target}" ) ;
237+ assert_eq ! (
238+ yaml_mapping_value(
239+ & workflow_lines,
240+ & [ "jobs" , "build" , "env" , target_cflags. as_str( ) ]
241+ ) ,
242+ Some ( "-Wno-error=date-time" ) ,
243+ "zig cross builds need a job-scoped mimalloc-pprof diagnostic override: {target_cflags}"
244+ ) ;
245+ }
246+ for job_limit in [ "CARGO_BUILD_JOBS" , "SOLDR_JOBS" ] {
247+ assert_eq ! (
248+ yaml_mapping_value( & workflow_lines, & [ "jobs" , "build" , "env" , job_limit] ) ,
249+ Some ( "1" ) ,
250+ "native release lanes need a job-scoped hosted-runner memory limit: {job_limit}"
251+ ) ;
252+ }
253+ let release_paths = yaml_sequence_values ( & release_workflow_lines, & [ "on" , "push" , "paths" ] )
254+ . expect ( "release workflow must define on.push.paths" ) ;
255+ for release_input in [
256+ ".github/workflows/release-auto.yml" ,
257+ ".github/workflows/template_native_build.yml" ,
258+ ] {
259+ assert ! (
260+ release_paths. contains( & release_input) ,
261+ "release workflow fixes must retrigger an incomplete publication: {release_input}"
262+ ) ;
263+ }
264+ }
0 commit comments