diff --git a/src/recipe/custom_yaml.rs b/src/recipe/custom_yaml.rs index 2d50d190c..ba96fde8d 100644 --- a/src/recipe/custom_yaml.rs +++ b/src/recipe/custom_yaml.rs @@ -154,7 +154,7 @@ impl Render for Node { impl Render for ScalarNode { fn render(&self, jinja: &Jinja, _name: &str) -> Result> { - let rendered = jinja.render_str(self.as_str()).map_err(|err| { + let (rendered, may_coerce) = jinja.render_str(self.as_str()).map_err(|err| { vec![_partialerror!( *self.span(), ErrorKind::JinjaRendering(err), @@ -165,7 +165,7 @@ impl Render for ScalarNode { Ok(Node::from(ScalarNode::new( *self.span(), rendered, - self.may_coerce, + self.may_coerce && may_coerce, ))) } } @@ -176,7 +176,7 @@ impl Render> for ScalarNode { jinja: &Jinja, _name: &str, ) -> Result, Vec> { - let rendered = jinja.render_str(self.as_str()).map_err(|err| { + let (rendered, may_coerce) = jinja.render_str(self.as_str()).map_err(|err| { vec![_partialerror!( *self.span(), ErrorKind::JinjaRendering(err), @@ -190,7 +190,7 @@ impl Render> for ScalarNode { Ok(Some(ScalarNode::new( *self.span(), rendered, - self.may_coerce, + self.may_coerce && may_coerce, ))) } } @@ -387,7 +387,16 @@ impl TryFrom<&marked_yaml::Node> for Node { pub struct ScalarNode { span: marked_yaml::Span, value: String, - may_coerce: bool, + pub(crate) may_coerce: bool, +} + +/// Convert a string to a boolean +pub fn string_to_bool(value: &str) -> Option { + match value { + "true" | "True" | "TRUE" => Some(true), + "false" | "False" | "FALSE" => Some(false), + _ => None, + } } impl ScalarNode { @@ -426,11 +435,8 @@ impl ScalarNode { if !self.may_coerce { return None; } - match self.value.as_str() { - "true" | "True" | "TRUE" => Some(true), - "false" | "False" | "FALSE" => Some(false), - _ => None, - } + + string_to_bool(&self.value) } /// Convert the scalar node to an integer and follow coercion rules @@ -1378,3 +1384,34 @@ impl TryConvertNode for RenderedScalarNode { .map_err(|e| vec![e]) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_to_bool_true_values() { + assert_eq!(string_to_bool("true"), Some(true)); + assert_eq!(string_to_bool("True"), Some(true)); + assert_eq!(string_to_bool("TRUE"), Some(true)); + } + + #[test] + fn test_string_to_bool_false_values() { + assert_eq!(string_to_bool("false"), Some(false)); + assert_eq!(string_to_bool("False"), Some(false)); + assert_eq!(string_to_bool("FALSE"), Some(false)); + } + + #[test] + fn test_string_to_bool_none_values() { + assert_eq!(string_to_bool("tRuE"), None); + assert_eq!(string_to_bool("fAlSe"), None); + assert_eq!(string_to_bool("yes"), None); + assert_eq!(string_to_bool("no"), None); + assert_eq!(string_to_bool("1"), None); + assert_eq!(string_to_bool("0"), None); + assert_eq!(string_to_bool(""), None); + assert_eq!(string_to_bool(" true "), None); + } +} diff --git a/src/recipe/custom_yaml/rendered.rs b/src/recipe/custom_yaml/rendered.rs index 22eb2fc0b..a2183e2c3 100644 --- a/src/recipe/custom_yaml/rendered.rs +++ b/src/recipe/custom_yaml/rendered.rs @@ -133,6 +133,71 @@ impl RenderedNode { _ => None, } } + + pub fn from_jinja_value( + source: String, + value: minijinja::Value, + span: Span, + coercible: bool, + ) -> Result { + if !coercible { + return Ok(RenderedNode::Scalar(RenderedScalarNode::new( + span, + source, + value.to_string(), + false, + ))); + } + + match value.kind() { + minijinja::value::ValueKind::Map => { + let mut rendered = IndexMap::new(); + for (key, value) in value.try_iter().unwrap().map(|v| { + let key = v.get_attr("key").unwrap(); + let value = v.get_attr("value").unwrap(); + (key, value) + }) { + let key = + RenderedScalarNode::new(span, key.to_string(), key.to_string(), false); + let value = RenderedNode::from_jinja_value(source.clone(), value, span, true)?; + rendered.insert(key, value); + } + Ok(RenderedNode::Mapping(RenderedMappingNode::new( + span, rendered, + ))) + } + minijinja::value::ValueKind::Seq => { + let mut rendered: Vec = Vec::new(); + for elem in value.try_iter().unwrap() { + let node = RenderedNode::from_jinja_value(source.clone(), elem, span, true)?; + rendered.push(node); + } + Ok(RenderedNode::Sequence(RenderedSequenceNode::from(rendered))) + } + minijinja::value::ValueKind::String + | minijinja::value::ValueKind::Bool + | minijinja::value::ValueKind::Number => { + let value = value.to_string(); + if value.is_empty() { + return Ok(RenderedNode::Null(RenderedScalarNode::new( + span, + source, + String::new(), + false, + ))); + } + Ok(RenderedNode::Scalar(RenderedScalarNode::new( + span, source, value, true, + ))) + } + minijinja::value::ValueKind::None | minijinja::value::ValueKind::Undefined => Ok( + RenderedNode::Null(RenderedScalarNode::new(span, source, String::new(), false)), + ), + _ => { + todo!("Other types not supported yet"); + } + } + } } impl HasSpan for RenderedNode { @@ -662,26 +727,21 @@ impl Render for Node { impl Render for ScalarNode { fn render(&self, jinja: &Jinja, _name: &str) -> Result> { - let rendered = jinja.render_str(self.as_str()).map_err(|err| { + let (value, can_coerce) = jinja.render_to_value(self).map_err(|err| { vec![_partialerror!( *self.span(), ErrorKind::JinjaRendering(err), label = jinja_error_to_label(&err), )] })?; - // unsure whether this should be allowed to coerce // check if it's quoted? - let rendered = RenderedScalarNode::new( - *self.span(), - self.as_str().to_string(), - rendered, - self.may_coerce, - ); - if rendered.is_empty() { - Ok(RenderedNode::Null(rendered)) - } else { - Ok(RenderedNode::Scalar(rendered)) - } + Ok(RenderedNode::from_jinja_value( + self.to_string(), + value, + *self.span(), + self.may_coerce && can_coerce, + ) + .unwrap()) } } @@ -691,7 +751,7 @@ impl Render> for ScalarNode { jinja: &Jinja, _name: &str, ) -> Result, Vec> { - let rendered = jinja.render_str(self.as_str()).map_err(|err| { + let (rendered, may_coerce) = jinja.render_str(self.as_str()).map_err(|err| { vec![_partialerror!( *self.span(), ErrorKind::JinjaRendering(err), @@ -703,7 +763,7 @@ impl Render> for ScalarNode { *self.span(), self.as_str().to_string(), rendered, - self.may_coerce, + self.may_coerce && may_coerce, ); if rendered.is_empty() { @@ -814,3 +874,230 @@ impl Render for SequenceNodeInternal { Ok(RenderedSequenceNode::from(rendered)) } } + +#[cfg(test)] +mod tests { + use super::*; + use marked_yaml::Span; + use minijinja::Value; + + fn blank_span() -> Span { + Span::new_blank() + } + + #[test] + fn test_from_jin_value_not_coercible() { + let val = Value::from_safe_string("test".to_string()); + let node = + RenderedNode::from_jinja_value("test_source".to_string(), val, blank_span(), false) + .unwrap(); + match node { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source"); + assert_eq!(s.as_str(), "test"); + assert!(!s.may_coerce); + } + _ => panic!("Expected ScalarNode"), + } + } + + #[test] + fn test_from_jinja_value_coercible_string() { + let val = Value::from_safe_string("hello".to_string()); + let node = RenderedNode::from_jinja_value( + "test_source_hello".to_string(), + val, + blank_span(), + true, + ) + .unwrap(); + match node { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_hello"); + assert_eq!(s.as_str(), "hello"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode"), + } + } + + #[test] + fn test_from_jinja_value_coercible_empty_string_to_null() { + // minijinja::Value::from_safe_string("") results in a string value whose .to_string() is "" + let val = Value::from_safe_string("".to_string()); + let node = RenderedNode::from_jinja_value( + "test_source_empty".to_string(), + val, + blank_span(), + true, + ) + .unwrap(); + match node { + RenderedNode::Null(s) => { + assert_eq!(s.source(), "test_source_empty"); + assert_eq!(s.as_str(), ""); + assert!(!s.may_coerce); + } + _ => panic!("Expected NullNode, got {:?}", node), + } + } + + #[test] + fn test_from_jinja_value_coercible_bool() { + let val_true = Value::from(true); + let node_true = RenderedNode::from_jinja_value( + "test_source_true".to_string(), + val_true, + blank_span(), + true, + ) + .unwrap(); + match node_true { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_true"); + assert_eq!(s.as_str(), "true"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for true"), + } + + let val_false = Value::from(false); + let node_false = RenderedNode::from_jinja_value( + "test_source_false".to_string(), + val_false, + blank_span(), + true, + ) + .unwrap(); + match node_false { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_false"); + assert_eq!(s.as_str(), "false"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for false"), + } + } + + #[test] + fn test_from_jinja_value_coercible_number() { + let val_int = Value::from(123); + let node_int = RenderedNode::from_jinja_value( + "test_source_int".to_string(), + val_int, + blank_span(), + true, + ) + .unwrap(); + match node_int { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_int"); + assert_eq!(s.as_str(), "123"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for integer"), + } + + let val_float = Value::from(45.67); + let node_float = RenderedNode::from_jinja_value( + "test_source_float".to_string(), + val_float, + blank_span(), + true, + ) + .unwrap(); + match node_float { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_float"); + assert_eq!(s.as_str(), "45.67"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for float"), + } + } + + #[test] + fn test_from_jinja_value_coercible_none_undefined() { + let val_none = Value::from(()); + let node_none = RenderedNode::from_jinja_value( + "test_source_none".to_string(), + val_none, + blank_span(), + true, + ) + .unwrap(); + match node_none { + RenderedNode::Null(s) => { + assert_eq!(s.source(), "test_source_none"); + assert_eq!(s.as_str(), ""); + assert!(!s.may_coerce); + } + _ => panic!("Expected NullNode for None"), + } + + let val_undefined = Value::UNDEFINED; + let node_undefined = RenderedNode::from_jinja_value( + "test_source_undefined".to_string(), + val_undefined, + blank_span(), + true, + ) + .unwrap(); + match node_undefined { + RenderedNode::Null(s) => { + assert_eq!(s.source(), "test_source_undefined"); + assert_eq!(s.as_str(), ""); + assert!(!s.may_coerce); + } + _ => panic!("Expected NullNode for Undefined"), + } + } + + #[test] + fn test_from_jinja_value_coercible_sequence() { + let val_seq = Value::from(vec![ + Value::from_safe_string("apple".to_string()), + Value::from(true), + Value::from(100), + ]); + let node_seq = RenderedNode::from_jinja_value( + "test_source_seq".to_string(), + val_seq, + blank_span(), + true, + ) + .unwrap(); + match node_seq { + RenderedNode::Sequence(seq) => { + assert_eq!(seq.len(), 3); + match &seq[0] { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_seq"); + assert_eq!(s.as_str(), "apple"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for seq[0]"), + } + + match &seq[1] { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_seq"); + assert_eq!(s.as_str(), "true"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for seq[1]"), + } + + match &seq[2] { + RenderedNode::Scalar(s) => { + assert_eq!(s.source(), "test_source_seq"); + assert_eq!(s.as_str(), "100"); + assert!(s.may_coerce); + } + _ => panic!("Expected ScalarNode for seq[2]"), + } + } + _ => panic!("Expected SequenceNode"), + } + } +} diff --git a/src/recipe/jinja.rs b/src/recipe/jinja.rs index 6b43c652f..9462f0edd 100644 --- a/src/recipe/jinja.rs +++ b/src/recipe/jinja.rs @@ -17,6 +17,7 @@ use crate::render::pin::PinArgs; pub use crate::render::pin::{Pin, PinExpression}; pub use crate::selectors::SelectorConfig; +use super::custom_yaml::ScalarNode; use super::parser::{Dependency, PinCompatible, PinSubpackage}; use super::variable::Variable; @@ -54,6 +55,38 @@ pub struct Jinja { context: BTreeMap, } +/// If we have a template that is _only_ an expression, we want to strip the +/// `${{` and `}}` from it so that we can evaluate it as an expression instead of +/// rendering it as a string. +/// +/// The function checks for: +/// - Whitespace before/after the expression +/// - Single expression with no nested expressions +/// - Proper opening `${{` and closing `}}` +fn strip_expression(template: &str) -> Option<&str> { + let trimmed = template.trim(); + if !trimmed.starts_with("${{") || !trimmed.ends_with("}}") { + return None; + } + + // Extract content between ${{ and }} + let content = &trimmed[3..trimmed.len() - 2]; + let content_trimmed = content.trim(); + + // If, after stripping ${{}} and trimming, the expression is empty, + // it's not a valid expression to compile. Treat it as if it wasn't an expression block. + if content_trimmed.is_empty() { + return None; + } + + // Check for nested expressions + if content_trimmed.contains("${{") || content_trimmed.contains("}}") { + return None; + } + + Some(content_trimmed) +} + impl Jinja { /// Create a new Jinja instance with the given selector configuration. pub fn new(config: SelectorConfig) -> Self { @@ -94,9 +127,54 @@ impl Jinja { &mut self.context } + /// Render the given template to a Jinja value. + pub fn render_to_value( + &self, + template_node: &ScalarNode, + ) -> Result<(Value, bool), minijinja::Error> { + let template_str = template_node.as_str(); + if let Some(simple_expr) = strip_expression(template_str) { + let expr = self.env.compile_expression(simple_expr)?; + let evaled = expr.eval(self.context())?; + // If the expression evaluated to a String, it's considered a final string value. + // The associated bool (false) means it should not be further type-coerced by recipe logic. + if evaled.kind() == minijinja::value::ValueKind::String { + return Ok((evaled, false)); + } else { + // For non-string results (bool, number, etc.), its further coercibility + // depends on the original YAML scalar's coercibility. + return Ok((evaled, template_node.may_coerce)); + } + } + + // Otherwise just render it as string + let rendered_as_string = self.env.render_str(template_str, &self.context)?; + Ok(( + Value::from(rendered_as_string), + !template_str.contains("${{") && template_node.may_coerce, + )) + } + /// Render a template with the current context. - pub fn render_str(&self, template: &str) -> Result { - self.env.render_str(template, &self.context) + pub fn render_str(&self, template: &str) -> Result<(String, bool), minijinja::Error> { + if let Some(simple_expr) = strip_expression(template) { + let expr = self.env.compile_expression(simple_expr)?; + let evaled = expr.eval(self.context())?; + if evaled.kind() == minijinja::value::ValueKind::String { + return Ok(( + evaled + .as_str() + .expect("Value kind String implies as_str is Some") + .to_string(), + false, + )); + } else { + return Ok((evaled.to_string(), true)); + } + } + + let rendered_string = self.env.render_str(template, &self.context)?; + Ok((rendered_string, !template.contains("${{"))) } /// Render, compile and evaluate a expr string with the current context. @@ -741,7 +819,7 @@ impl Env { match std::env::var(env_var) { Ok(r) => Ok(Value::from(r)), Err(_) => match default_value { - Some(default_value) => Ok(Value::from(default_value)), + Some(default_value) => Ok(Value::from_safe_string(default_value)), None => Err(minijinja::Error::new( minijinja::ErrorKind::InvalidOperation, format!("Environment variable {env_var} not found"), @@ -1366,4 +1444,23 @@ mod tests { default_compiler(platform, "cuda").unwrap().to_string() ); } + + #[test] + fn test_strip_expression() { + // Valid cases + assert_eq!(strip_expression("${{ expr }}"), Some("expr")); + assert_eq!(strip_expression(" ${{ expr }} "), Some("expr")); + assert_eq!(strip_expression("${{expr}}"), Some("expr")); + assert_eq!( + strip_expression("${{ expr with spaces }}"), + Some("expr with spaces") + ); + + // Invalid cases + assert_eq!(strip_expression("not an expression"), None); + assert_eq!(strip_expression("${{ nested ${{ expr }} }}"), None); + assert_eq!(strip_expression("${{ unmatched"), None); + assert_eq!(strip_expression("unmatched }}"), None); + assert_eq!(strip_expression("text ${{ expr }} text"), None); + } } diff --git a/src/recipe/parser.rs b/src/recipe/parser.rs index 2f2e51ca1..84b637190 100644 --- a/src/recipe/parser.rs +++ b/src/recipe/parser.rs @@ -55,6 +55,8 @@ pub use self::{ use crate::recipe::{custom_yaml::Node, variable::Variable}; +use super::custom_yaml::string_to_bool; + /// A recipe that has been parsed and validated. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Recipe { @@ -149,19 +151,29 @@ impl Recipe { help = "`context` values must always be scalars (booleans, integers or strings) or uniform lists of scalars" )] })?; - let rendered: Option = val.render(jinja, &format!("context.{}", k.as_str()))?; - if let Some(rendered) = rendered { - let variable = if let Some(value) = rendered.as_bool() { - Variable::from(value) - } else if let Some(value) = rendered.as_integer() { - Variable::from(value) + + let (value, can_coerce) = jinja.render_to_value(val).map_err(|err| { + vec![_partialerror!( + *val.span(), + ErrorKind::JinjaRendering(err), + help = "failed to render jinja expression" + )] + })?; + + // See if we have to coerce a string-type to a boolean or integer + if can_coerce && value.as_str().is_some() { + // let's see if the value should be an integer or a boolean + let stringified = value.to_string(); + if let Some(boolean) = string_to_bool(&stringified) { + return Ok(Some(Variable::from(boolean))); + } else if let Ok(integer) = stringified.parse::() { + return Ok(Some(Variable::from(integer))); } else { - Variable::from_string(&rendered) - }; - Ok(Some(variable)) - } else { - Ok(None) + return Ok(Some(Variable::from_string(&stringified))); + } } + + Ok(Some(Variable::from_value(value))) } /// Create recipes from a YAML [`Node`] structure. @@ -182,7 +194,6 @@ impl Recipe { // add context values let mut context: IndexMap = IndexMap::new(); - if let Some(context_map) = root_node.get("context") { let context_map = context_map.as_mapping().ok_or_else(|| { vec![_partialerror!( @@ -234,6 +245,7 @@ impl Recipe { continue; }; context.insert(k.as_str().to_string(), variable.clone()); + // also immediately insert into jinja context so that the value can be used // in later jinja expressions jinja diff --git a/src/recipe/parser/build.rs b/src/recipe/parser/build.rs index 3b1749435..e9cb9189e 100644 --- a/src/recipe/parser/build.rs +++ b/src/recipe/parser/build.rs @@ -172,7 +172,7 @@ impl BuildString { pub fn resolve(&self, hash: &HashInfo, build_number: u64, jinja: &Jinja) -> Cow<'_, str> { match self { // TODO - BuildString::UserSpecified(template) => jinja.render_str(template).unwrap().into(), + BuildString::UserSpecified(template) => jinja.render_str(template).unwrap().0.into(), BuildString::Resolved(s) => s.as_str().into(), BuildString::Derived => Self::compute(hash, build_number).into(), } diff --git a/src/recipe/snapshots/rattler_build__recipe__parser__tests__jinja_error.snap b/src/recipe/snapshots/rattler_build__recipe__parser__tests__jinja_error.snap index 55c8a3da3..0ca6be7c7 100644 --- a/src/recipe/snapshots/rattler_build__recipe__parser__tests__jinja_error.snap +++ b/src/recipe/snapshots/rattler_build__recipe__parser__tests__jinja_error.snap @@ -6,7 +6,7 @@ expression: err Error: × failed to render Jinja expression: unknown function: zcompiler is unknown - │ (in :1) + │ (in :1) ╭─[7:7] 6 │ host: 7 │ - ${{ zcompiler('c') }} diff --git a/src/recipe/variable.rs b/src/recipe/variable.rs index 7214582b6..8323b60bc 100644 --- a/src/recipe/variable.rs +++ b/src/recipe/variable.rs @@ -61,6 +61,11 @@ impl Variable { pub fn from_string(value: &str) -> Self { Variable(Value::from_safe_string(value.to_string())) } + + /// Create a variable from a `minijinja::Value` + pub fn from_value(value: Value) -> Self { + Variable(value) + } } impl Display for Variable { diff --git a/src/script/mod.rs b/src/script/mod.rs index 4e37f6899..480206355 100644 --- a/src/script/mod.rs +++ b/src/script/mod.rs @@ -219,7 +219,7 @@ impl Script { if let Some(jinja_context) = jinja_context { match script_content? { ResolvedScriptContents::Inline(script) => { - let rendered = jinja_context.render_str(&script).map_err(|e| { + let (rendered, _) = jinja_context.render_str(&script).map_err(|e| { std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to render jinja template in build `script`: {}", e), diff --git a/test-data/recipes/cuda_version/recipe.yaml b/test-data/recipes/cuda_version/recipe.yaml new file mode 100644 index 000000000..f0b23675b --- /dev/null +++ b/test-data/recipes/cuda_version/recipe.yaml @@ -0,0 +1,15 @@ +context: + cuda_version: ${{ env.get("RAPIDS_CUDA_VERSION") }} + cuda_major: ${{ (env.get("RAPIDS_CUDA_VERSION") | split("."))[0] }} + +package: + name: test-package + version: 1.0 + +build: + script: + - if: cuda_major == "12" + then: + - echo "cuda_major is a STRING and is 12" + else: + - echo "cuda_major is NOT a STRING or is not 12 (value was ${{ cuda_major }})" diff --git a/test/end-to-end/test_jinja_yaml_parsing.py b/test/end-to-end/test_jinja_yaml_parsing.py new file mode 100644 index 000000000..16c1461da --- /dev/null +++ b/test/end-to-end/test_jinja_yaml_parsing.py @@ -0,0 +1,147 @@ +from pathlib import Path +import tempfile + +from helpers import RattlerBuild + + +def test_scenario_a_int_no_quotes(rattler_build: RattlerBuild): + """ + Test case 'a': + var: ${{ 1234 }} + renders to: + var: 1234 (int) + """ + recipe_content = """ +context: + var: ${{ 1234 }} +package: + name: test_package + version: 0.1.0 +build: + script: echo "hello" +""" + with tempfile.TemporaryDirectory() as tmpdir: + recipe_dir = Path(tmpdir) + recipe_file = recipe_dir / "recipe.yaml" + with open(recipe_file, "w") as f: + f.write(recipe_content) + + output_dir = Path(tmpdir) / "output" + output_dir.mkdir() + + rendered_recipe = rattler_build.render(recipe_dir, output_dir) + assert isinstance(rendered_recipe, list) + assert len(rendered_recipe) > 0 + final_context = rendered_recipe[0].get("recipe", {}).get("context", {}) + + assert "var" in final_context + assert final_context["var"] == 1234 + assert isinstance(final_context["var"], int) + + +def test_scenario_b_str_with_quotes_around_jinja(rattler_build: RattlerBuild): + """ + Test case 'b': + var: "${{ '1234' }}" + renders to: + var: "1234" (str) + """ + recipe_content = """ +context: + var: "${{ '1234' }}" +package: + name: test_package_b + version: 0.1.0 +build: + script: echo "hello" +""" + with tempfile.TemporaryDirectory() as tmpdir: + recipe_dir = Path(tmpdir) + recipe_file = recipe_dir / "recipe.yaml" + with open(recipe_file, "w") as f: + f.write(recipe_content) + + output_dir = Path(tmpdir) / "output" + output_dir.mkdir() + + rendered_recipe = rattler_build.render(recipe_dir, output_dir) + + assert isinstance(rendered_recipe, list) + assert len(rendered_recipe) > 0 + final_context = rendered_recipe[0].get("recipe", {}).get("context", {}) + + assert "var" in final_context + assert final_context["var"] == "1234" + assert isinstance(final_context["var"], str) + + +def test_scenario_c_int_with_quotes_around_jinja(rattler_build: RattlerBuild): + """ + Test case 'c': + var: "${{ 1234 }}" + renders to: + var: 1234 (int) + """ + recipe_content = """ +context: + var: "${{ 1234 }}" +package: + name: test_package_c + version: 0.1.0 +build: + script: echo "hello" +""" + with tempfile.TemporaryDirectory() as tmpdir: + recipe_dir = Path(tmpdir) + recipe_file = recipe_dir / "recipe.yaml" + with open(recipe_file, "w") as f: + f.write(recipe_content) + + output_dir = Path(tmpdir) / "output" + output_dir.mkdir() + + rendered_recipe = rattler_build.render(recipe_dir, output_dir) + + assert isinstance(rendered_recipe, list) + assert len(rendered_recipe) > 0 + final_context = rendered_recipe[0].get("recipe", {}).get("context", {}) + + assert "var" in final_context + assert final_context["var"] == 1234 + assert isinstance(final_context["var"], int) + + +def test_scenario_d_str_no_quotes(rattler_build: RattlerBuild): + """ + Test case 'd': + var: ${{ "1234" }} + renders to: + var: "1234" (str) + """ + recipe_content = """ +context: + var: ${{ "1234" }} +package: + name: test_package_d + version: 0.1.0 +build: + script: echo "hello" +""" + with tempfile.TemporaryDirectory() as tmpdir: + recipe_dir = Path(tmpdir) + recipe_file = recipe_dir / "recipe.yaml" + with open(recipe_file, "w") as f: + f.write(recipe_content) + + output_dir = Path(tmpdir) / "output" + output_dir.mkdir() + + rendered_recipe = rattler_build.render(recipe_dir, output_dir) + + assert isinstance(rendered_recipe, list) + assert len(rendered_recipe) > 0 + final_context = rendered_recipe[0].get("recipe", {}).get("context", {}) + + assert "var" in final_context + assert final_context["var"] == "1234" + assert isinstance(final_context["var"], str) diff --git a/test/end-to-end/test_simple.py b/test/end-to-end/test_simple.py index 46a5ff475..6bef155ae 100644 --- a/test/end-to-end/test_simple.py +++ b/test/end-to-end/test_simple.py @@ -1662,6 +1662,20 @@ def test_relative_file_loading( ) +def test_cuda_version( + rattler_build: RattlerBuild, recipes: Path, tmp_path: Path, monkeypatch, capfd +): + monkeypatch.setenv("RAPIDS_CUDA_VERSION", "12.0") + rattler_build.build(recipes / "cuda_version/recipe.yaml", tmp_path) + captured = capfd.readouterr() + assert "cuda_major is a STRING and is 12" in captured.err + + monkeypatch.setenv("RAPIDS_CUDA_VERSION", "11.8") + rattler_build.build(recipes / "cuda_version/recipe.yaml", tmp_path) + captured = capfd.readouterr() + assert "cuda_major is NOT a STRING or is not 12 (value was 11)" in captured.err + + @pytest.mark.parametrize( "interpreter", [