@@ -106,6 +106,12 @@ pub struct RenderConfig {
106106 /// like `MACOSX_DEPLOYMENT_TARGET` on macOS that have default values but can be
107107 /// customized via variant config.
108108 pub os_env_var_keys : HashSet < String > ,
109+ /// An optional prefix to prepend to the auto-generated build string.
110+ /// When set, the build string becomes `{prefix}_{default_build_string}`.
111+ pub build_string_prefix : Option < String > ,
112+ /// An optional build number override.
113+ /// When set, this replaces the build number from the recipe.
114+ pub build_number_override : Option < u64 > ,
109115}
110116
111117impl Default for RenderConfig {
@@ -118,6 +124,8 @@ impl Default for RenderConfig {
118124 build_platform : rattler_conda_types:: Platform :: current ( ) ,
119125 host_platform : rattler_conda_types:: Platform :: current ( ) ,
120126 os_env_var_keys : HashSet :: new ( ) ,
127+ build_string_prefix : None ,
128+ build_number_override : None ,
121129 }
122130 }
123131}
@@ -175,6 +183,18 @@ impl RenderConfig {
175183 self . os_env_var_keys = keys;
176184 self
177185 }
186+
187+ /// Set a prefix to prepend to the auto-generated build string.
188+ pub fn with_build_string_prefix ( mut self , prefix : impl Into < String > ) -> Self {
189+ self . build_string_prefix = Some ( prefix. into ( ) ) ;
190+ self
191+ }
192+
193+ /// Override the build number from the recipe.
194+ pub fn with_build_number_override ( mut self , build_number : u64 ) -> Self {
195+ self . build_number_override = Some ( build_number) ;
196+ self
197+ }
178198}
179199
180200/// Result of rendering a recipe with a specific variant combination
@@ -978,7 +998,7 @@ fn render_with_empty_combinations(
978998 results[ i] . pin_subpackages = pin_subpackages;
979999
9801000 // Finalize build string with complete pin information
981- finalize_build_string_single ( & mut results[ i] ) ?;
1001+ finalize_build_string_single ( & mut results[ i] , config ) ?;
9821002 }
9831003
9841004 Ok ( results)
@@ -988,11 +1008,25 @@ fn render_with_empty_combinations(
9881008///
9891009/// This computes the hash from the variant (which includes pin information)
9901010/// and resolves the build string for one variant.
991- fn finalize_build_string_single ( result : & mut RenderedVariant ) -> Result < ( ) , RenderError > {
1011+ fn finalize_build_string_single (
1012+ result : & mut RenderedVariant ,
1013+ config : & RenderConfig ,
1014+ ) -> Result < ( ) , RenderError > {
1015+ let build_string_prefix = config. build_string_prefix . as_deref ( ) ;
9921016 let noarch = result. recipe . build . noarch . unwrap_or ( NoArchType :: none ( ) ) ;
9931017
9941018 // Compute hash from the variant (which now includes pin_subpackage information)
995- let hash_info = HashInfo :: from_variant ( & result. variant , & noarch) ;
1019+ let mut hash_info = HashInfo :: from_variant ( & result. variant , & noarch) ;
1020+
1021+ // Prepend the user-provided build string prefix to the hash prefix so it
1022+ // becomes part of the default build string format: {prefix}h{hash}_{number}.
1023+ if let Some ( prefix) = build_string_prefix {
1024+ if hash_info. prefix . is_empty ( ) {
1025+ hash_info. prefix = format ! ( "{prefix}_" ) ;
1026+ } else {
1027+ hash_info. prefix = format ! ( "{prefix}_{}" , hash_info. prefix) ;
1028+ }
1029+ }
9961030
9971031 // If build string is not set (Default), or if it needs resolving
9981032 if matches ! (
@@ -1018,6 +1052,11 @@ fn finalize_build_string_single(result: &mut RenderedVariant) -> Result<(), Rend
10181052
10191053 let eval_ctx = EvaluationContext :: from_variables ( variables) ;
10201054
1055+ // Apply the build number override if provided.
1056+ if let Some ( bn) = config. build_number_override {
1057+ result. recipe . build . number = Some ( bn) ;
1058+ }
1059+
10211060 // Resolve the build string template with the hash
10221061 result. recipe . build . string . resolve (
10231062 & hash_info,
@@ -1395,7 +1434,7 @@ fn render_with_variants(
13951434 results[ i] . pin_subpackages = pin_subpackages;
13961435
13971436 // Finalize build string with complete pin information
1398- finalize_build_string_single ( & mut results[ i] ) ?;
1437+ finalize_build_string_single ( & mut results[ i] , & config ) ?;
13991438 }
14001439
14011440 Ok ( results)
@@ -3035,4 +3074,201 @@ postgresql:
30353074 // postgresql 16 should be skipped (<=16), 17 should build
30363075 assert_eq ! ( skip_flags, vec![ ( "16" . into( ) , true ) , ( "17" . into( ) , false ) ] ) ;
30373076 }
3077+
3078+ #[ test]
3079+ fn test_build_string_prefix_simple ( ) {
3080+ let recipe_yaml = r#"
3081+ package:
3082+ name: test-pkg
3083+ version: "1.0.0"
3084+ "# ;
3085+ let stage0_recipe = stage0:: parse_recipe_or_multi_from_source ( recipe_yaml) . unwrap ( ) ;
3086+ let variant_config = VariantConfig :: default ( ) ;
3087+
3088+ let config = RenderConfig :: new ( ) . with_build_string_prefix ( "foobar" ) ;
3089+ let rendered =
3090+ render_recipe_with_variant_config ( & stage0_recipe, & variant_config, config) . unwrap ( ) ;
3091+
3092+ assert_eq ! ( rendered. len( ) , 1 ) ;
3093+ let build_string = rendered[ 0 ]
3094+ . recipe
3095+ . build
3096+ . string
3097+ . as_resolved ( )
3098+ . unwrap ( )
3099+ . to_string ( ) ;
3100+ assert ! (
3101+ build_string. starts_with( "foobar_h" ) ,
3102+ "build string should start with 'foobar_h', got '{build_string}'"
3103+ ) ;
3104+ }
3105+
3106+ #[ test]
3107+ fn test_build_string_prefix_with_variants ( ) {
3108+ let recipe_yaml = r#"
3109+ package:
3110+ name: test-pkg
3111+ version: "1.0.0"
3112+
3113+ requirements:
3114+ host:
3115+ - python ${{ python }}
3116+ run:
3117+ - python
3118+ "# ;
3119+ let variant_yaml = r#"
3120+ python:
3121+ - "3.9.*"
3122+ - "3.10.*"
3123+ "# ;
3124+ let stage0_recipe = stage0:: parse_recipe_or_multi_from_source ( recipe_yaml) . unwrap ( ) ;
3125+ let variant_config = VariantConfig :: from_yaml_str ( variant_yaml) . unwrap ( ) ;
3126+
3127+ let config = RenderConfig :: new ( ) . with_build_string_prefix ( "myprefix" ) ;
3128+ let rendered =
3129+ render_recipe_with_variant_config ( & stage0_recipe, & variant_config, config) . unwrap ( ) ;
3130+
3131+ assert_eq ! ( rendered. len( ) , 2 ) ;
3132+ for variant in & rendered {
3133+ let build_string = variant
3134+ . recipe
3135+ . build
3136+ . string
3137+ . as_resolved ( )
3138+ . unwrap ( )
3139+ . to_string ( ) ;
3140+ assert ! (
3141+ build_string. starts_with( "myprefix_" ) ,
3142+ "build string should start with 'myprefix_', got '{build_string}'"
3143+ ) ;
3144+ }
3145+
3146+ // The two variants should produce different build strings
3147+ let bs0 = rendered[ 0 ] . recipe . build . string . as_resolved ( ) . unwrap ( ) ;
3148+ let bs1 = rendered[ 1 ] . recipe . build . string . as_resolved ( ) . unwrap ( ) ;
3149+ assert_ne ! (
3150+ bs0, bs1,
3151+ "different variants should produce different build strings"
3152+ ) ;
3153+ }
3154+
3155+ #[ test]
3156+ fn test_build_string_prefix_absent_gives_default ( ) {
3157+ let recipe_yaml = r#"
3158+ package:
3159+ name: test-pkg
3160+ version: "1.0.0"
3161+ "# ;
3162+ let stage0_recipe = stage0:: parse_recipe_or_multi_from_source ( recipe_yaml) . unwrap ( ) ;
3163+ let variant_config = VariantConfig :: default ( ) ;
3164+
3165+ let with_prefix = render_recipe_with_variant_config (
3166+ & stage0_recipe,
3167+ & variant_config,
3168+ RenderConfig :: new ( ) . with_build_string_prefix ( "pfx" ) ,
3169+ )
3170+ . unwrap ( ) ;
3171+ let without_prefix =
3172+ render_recipe_with_variant_config ( & stage0_recipe, & variant_config, RenderConfig :: new ( ) )
3173+ . unwrap ( ) ;
3174+
3175+ let bs_with = with_prefix[ 0 ] . recipe . build . string . as_resolved ( ) . unwrap ( ) ;
3176+ let bs_without = without_prefix[ 0 ] . recipe . build . string . as_resolved ( ) . unwrap ( ) ;
3177+
3178+ // The prefixed version should contain the unprefixed version
3179+ assert ! (
3180+ bs_with. starts_with( "pfx_" ) ,
3181+ "prefixed build string should start with 'pfx_', got '{bs_with}'"
3182+ ) ;
3183+ assert ! (
3184+ bs_with. ends_with( bs_without) ,
3185+ "prefixed '{bs_with}' should end with default '{bs_without}'"
3186+ ) ;
3187+ }
3188+
3189+ #[ test]
3190+ fn test_build_number_override ( ) {
3191+ let recipe_yaml = r#"
3192+ package:
3193+ name: test-pkg
3194+ version: "1.0.0"
3195+
3196+ build:
3197+ number: 5
3198+ "# ;
3199+ let stage0_recipe = stage0:: parse_recipe_or_multi_from_source ( recipe_yaml) . unwrap ( ) ;
3200+ let variant_config = VariantConfig :: default ( ) ;
3201+
3202+ // Without override — should use the recipe's build number (5)
3203+ let rendered_default =
3204+ render_recipe_with_variant_config ( & stage0_recipe, & variant_config, RenderConfig :: new ( ) )
3205+ . unwrap ( ) ;
3206+ assert_eq ! ( rendered_default[ 0 ] . recipe. build. number, Some ( 5 ) ) ;
3207+
3208+ // With override — should use 42
3209+ let rendered_override = render_recipe_with_variant_config (
3210+ & stage0_recipe,
3211+ & variant_config,
3212+ RenderConfig :: new ( ) . with_build_number_override ( 42 ) ,
3213+ )
3214+ . unwrap ( ) ;
3215+ assert_eq ! ( rendered_override[ 0 ] . recipe. build. number, Some ( 42 ) ) ;
3216+
3217+ // Build string should contain the overridden build number
3218+ let bs = rendered_override[ 0 ]
3219+ . recipe
3220+ . build
3221+ . string
3222+ . as_resolved ( )
3223+ . unwrap ( ) ;
3224+ assert ! (
3225+ bs. ends_with( "_42" ) ,
3226+ "build string should end with '_42', got '{bs}'"
3227+ ) ;
3228+ }
3229+
3230+ #[ test]
3231+ fn test_build_number_override_default_recipe ( ) {
3232+ let recipe_yaml = r#"
3233+ package:
3234+ name: test-pkg
3235+ version: "1.0.0"
3236+ "# ;
3237+ let stage0_recipe = stage0:: parse_recipe_or_multi_from_source ( recipe_yaml) . unwrap ( ) ;
3238+ let variant_config = VariantConfig :: default ( ) ;
3239+
3240+ // Recipe has no build number (defaults to 0)
3241+ let rendered_default =
3242+ render_recipe_with_variant_config ( & stage0_recipe, & variant_config, RenderConfig :: new ( ) )
3243+ . unwrap ( ) ;
3244+ let bs_default = rendered_default[ 0 ]
3245+ . recipe
3246+ . build
3247+ . string
3248+ . as_resolved ( )
3249+ . unwrap ( ) ;
3250+ assert ! (
3251+ bs_default. ends_with( "_0" ) ,
3252+ "default build string should end with '_0', got '{bs_default}'"
3253+ ) ;
3254+
3255+ // Override to 7
3256+ let rendered_override = render_recipe_with_variant_config (
3257+ & stage0_recipe,
3258+ & variant_config,
3259+ RenderConfig :: new ( ) . with_build_number_override ( 7 ) ,
3260+ )
3261+ . unwrap ( ) ;
3262+ assert_eq ! ( rendered_override[ 0 ] . recipe. build. number, Some ( 7 ) ) ;
3263+ let bs_override = rendered_override[ 0 ]
3264+ . recipe
3265+ . build
3266+ . string
3267+ . as_resolved ( )
3268+ . unwrap ( ) ;
3269+ assert ! (
3270+ bs_override. ends_with( "_7" ) ,
3271+ "overridden build string should end with '_7', got '{bs_override}'"
3272+ ) ;
3273+ }
30383274}
0 commit comments