Skip to content

Commit 809e112

Browse files
authored
feat: Allow for build_string_prefix passed into packages (#2384)
1 parent 617f3b9 commit 809e112

11 files changed

Lines changed: 812 additions & 78 deletions

File tree

crates/rattler_build_recipe/src/variant_render.rs

Lines changed: 240 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

111117
impl 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
}

docs/reference/cli/rattler-build/build.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ rattler-build build [OPTIONS]
6565
: Continue building even if (one) of the packages fails to build. This is useful when building many packages with `--recipe-dir`.`
6666

6767
## Modifying result
68+
- <a id="arg---build-num" href="#arg---build-num">`--build-num <BUILD_NUM>`</a>
69+
: Override the build number for all outputs (defaults to the build number in the recipe)
70+
- <a id="arg---build-string-prefix" href="#arg---build-string-prefix">`--build-string-prefix <BUILD_STRING_PREFIX>`</a>
71+
: Prefix to prepend to the auto-generated build string (e.g. `--build-string-prefix my_prefix` produces `my_prefix_h1234_0`)
6872
- <a id="arg---package-format" href="#arg---package-format">`--package-format <PACKAGE_FORMAT>`</a>
6973
: The package format to use for the build. Can be one of `tar-bz2` or
7074
`conda`. You can also add a compression level to the package format,
@@ -94,8 +98,6 @@ e.g. `tar-bz2:<number>` (from 1 to 9) or `conda:<number>` (from -7 to
9498
: Allow symlinks in packages on Windows (defaults to false - symlinks are forbidden on Windows)
9599
- <a id="arg---exclude-newer" href="#arg---exclude-newer">`--exclude-newer <EXCLUDE_NEWER>`</a>
96100
: Exclude packages newer than this date from the solver, in RFC3339 format (e.g. 2024-03-15T12:00:00Z)
97-
- <a id="arg---build-num" href="#arg---build-num">`--build-num <BUILD_NUM>`</a>
98-
: Override the build number for all outputs (defaults to the build number in the recipe)
99101

100102
## Sandbox arguments
101103
- <a id="arg---sandbox" href="#arg---sandbox">`--sandbox`</a>

docs/reference/cli/rattler-build/publish.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,15 +100,15 @@ e.g. `tar-bz2:<number>` (from 1 to 9) or `conda:<number>` (from -7 to
100100
: Allow symlinks in packages on Windows (defaults to false - symlinks are forbidden on Windows)
101101
- <a id="arg---exclude-newer" href="#arg---exclude-newer">`--exclude-newer <EXCLUDE_NEWER>`</a>
102102
: Exclude packages newer than this date from the solver, in RFC3339 format (e.g. 2024-03-15T12:00:00Z)
103-
- <a id="arg---build-num" href="#arg---build-num">`--build-num <BUILD_NUM>`</a>
104-
: Override the build number for all outputs (defaults to the build number in the recipe)
105103

106104
## Publishing
107105
- <a id="arg---to" href="#arg---to">`--to <TO>`</a>
108106
: The channel or URL to publish the package to
109107
<br>**required**: `true`
110108
- <a id="arg---build-number" href="#arg---build-number">`--build-number <BUILD_NUMBER>`</a>
111109
: Override the build number for all outputs. Use an absolute value (e.g., `--build-number=12`) or a relative bump (e.g., `--build-number=+1`). When using a relative bump, the highest build number from the target channel is used as the base
110+
- <a id="arg---build-string-prefix" href="#arg---build-string-prefix">`--build-string-prefix <BUILD_STRING_PREFIX>`</a>
111+
: Prefix to prepend to the auto-generated build string (e.g. `--build-string-prefix my_prefix` produces `my_prefix_h1234_0`)
112112
- <a id="arg---force" href="#arg---force">`--force`</a>
113113
: Force upload even if the package already exists (not recommended - may break lockfiles). Only works with S3, filesystem, Anaconda.org, and prefix.dev channels
114114
- <a id="arg---generate-attestation" href="#arg---generate-attestation">`--generate-attestation`</a>

py-rattler-build/rust/src/cli_api.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::error::RattlerBuildError;
1919
use crate::run_async_task;
2020

2121
#[pyfunction]
22-
#[pyo3(signature = (recipes, up_to, build_platform, target_platform, host_platform, channel, variant_config, variant_overrides=None, ignore_recipe_variants=false, render_only=false, with_solve=false, keep_build=false, no_build_id=false, package_format=None, compression_threads=None, io_concurrency_limit=None, no_include_recipe=false, test=None, output_dir=None, auth_file=None, channel_priority=None, skip_existing=None, noarch_build_platform=None, allow_insecure_host=None, continue_on_failure=false, error_prefix_in_binary=false, allow_symlinks_on_windows=false, allow_absolute_license_paths=false, exclude_newer=None, build_num=None, use_bz2=true, use_zstd=true, use_sharded=true))]
22+
#[pyo3(signature = (recipes, up_to, build_platform, target_platform, host_platform, channel, variant_config, variant_overrides=None, ignore_recipe_variants=false, render_only=false, with_solve=false, keep_build=false, no_build_id=false, package_format=None, compression_threads=None, io_concurrency_limit=None, no_include_recipe=false, test=None, output_dir=None, auth_file=None, channel_priority=None, skip_existing=None, noarch_build_platform=None, allow_insecure_host=None, continue_on_failure=false, error_prefix_in_binary=false, allow_symlinks_on_windows=false, allow_absolute_license_paths=false, exclude_newer=None, build_num=None, build_string_prefix=None, use_bz2=true, use_zstd=true, use_sharded=true))]
2323
#[allow(clippy::too_many_arguments)]
2424
pub fn build_recipes_py(
2525
recipes: Vec<PathBuf>,
@@ -52,6 +52,7 @@ pub fn build_recipes_py(
5252
allow_absolute_license_paths: bool,
5353
exclude_newer: Option<chrono::DateTime<chrono::Utc>>,
5454
build_num: Option<u64>,
55+
build_string_prefix: Option<String>,
5556
use_bz2: bool,
5657
use_zstd: bool,
5758
use_sharded: bool,
@@ -137,6 +138,7 @@ pub fn build_recipes_py(
137138
allow_absolute_license_paths,
138139
exclude_newer,
139140
build_num,
141+
build_string_prefix,
140142
None, // markdown_summary
141143
);
142144

py-rattler-build/rust/src/render.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@ pub struct PyRenderConfig {
2525
#[pymethods]
2626
impl PyRenderConfig {
2727
/// Create a new render configuration with default settings
28+
#[allow(clippy::too_many_arguments)]
2829
#[new]
29-
#[pyo3(signature = (target_platform=None, build_platform=None, host_platform=None, experimental=false, recipe_path=None, extra_context=None))]
30+
#[pyo3(signature = (target_platform=None, build_platform=None, host_platform=None, experimental=false, recipe_path=None, extra_context=None, build_string_prefix=None, build_number_override=None))]
3031
fn new(
3132
target_platform: Option<String>,
3233
build_platform: Option<String>,
3334
host_platform: Option<String>,
3435
experimental: bool,
3536
recipe_path: Option<PathBuf>,
3637
extra_context: Option<Bound<'_, PyDict>>,
38+
build_string_prefix: Option<String>,
39+
build_number_override: Option<u64>,
3740
) -> PyResult<Self> {
3841
let target_platform = target_platform
3942
.map(|p| p.parse::<Platform>())
@@ -83,6 +86,8 @@ impl PyRenderConfig {
8386
build_platform,
8487
host_platform,
8588
os_env_var_keys,
89+
build_string_prefix,
90+
build_number_override,
8691
},
8792
})
8893
}

0 commit comments

Comments
 (0)