diff --git a/src/cache.rs b/src/cache.rs index a807eea51..7732089fa 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -212,6 +212,35 @@ impl Output { let mut env_vars = env_vars::vars(&self, "BUILD"); env_vars.extend(env_vars::os_vars(self.prefix(), &target_platform)); + // We need to keep the PKG_NAME and PKG_VERSION for the build script consistent + // across different cache builds. To this end we use the top level metadata. + env_vars.insert( + "PKG_NAME".to_string(), + Some( + self.recipe + .top_level_recipe_metadata + .as_ref() + .map(|x| x.name.clone()) + .unwrap_or("cache".to_string()), + ), + ); + env_vars.insert( + "PKG_VERSION".to_string(), + Some( + self.recipe + .top_level_recipe_metadata + .as_ref() + .map(|x| x.version.clone()) + .flatten() + .map(|v| v.to_string()) + .unwrap_or("0.0.0".to_string()), + ), + ); + + env_vars.insert("PKG_HASH".to_string(), Some("".to_string())); + env_vars.insert("PKG_BUILDNUM".to_string(), Some("0".to_string())); + env_vars.insert("PKG_BUILD_STRING".to_string(), Some("".to_string())); + // Reindex the channels let channels = build_reindexed_channels(&self.build_configuration, tool_configuration) .await diff --git a/src/recipe/parser.rs b/src/recipe/parser.rs index fe27d0058..60762d3f4 100644 --- a/src/recipe/parser.rs +++ b/src/recipe/parser.rs @@ -3,6 +3,7 @@ //! This phase parses YAML and [`SelectorConfig`] into a [`Recipe`], where //! if-selectors are handled and any jinja string is processed, resulting in a rendered recipe. use indexmap::IndexMap; +use package::TopLevelRecipe; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::fmt::Debug; @@ -64,6 +65,9 @@ pub struct Recipe { pub context: IndexMap, /// The package information pub package: Package, + /// Contents of the top-level recipe key - only used in multi-output recipes + #[serde(default, skip_serializing_if = "Option::is_none")] + pub top_level_recipe_metadata: Option, /// The cache build that should be used for this package /// This is the same for all outputs of a recipe #[serde(default, skip_serializing_if = "Option::is_none")] @@ -256,6 +260,7 @@ impl Recipe { let mut about = About::default(); let mut cache = None; let mut extra = IndexMap::default(); + let mut top_level_recipe = Option::::None; rendered_node .iter() @@ -272,6 +277,9 @@ impl Recipe { "The recipe field is only allowed in conjunction with multiple outputs" )]) } + "__top_level_recipe" => { + top_level_recipe = value.try_convert(key_str)?; + } "cache" => { if experimental { cache = Some(value.try_convert(key_str)?) @@ -318,6 +326,7 @@ impl Recipe { let recipe = Recipe { schema_version, + top_level_recipe_metadata: top_level_recipe, context, package: package.ok_or_else(|| { vec![_partialerror!( diff --git a/src/recipe/parser/build.rs b/src/recipe/parser/build.rs index 46e1c4e29..42082835c 100644 --- a/src/recipe/parser/build.rs +++ b/src/recipe/parser/build.rs @@ -114,6 +114,9 @@ pub struct Build { /// Include files in the package #[serde(default, skip_serializing_if = "GlobVec::is_empty")] pub files: GlobVec, + /// Use the cache output for the build or not + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub use_cache: Option, } /// The build string can be either a user specified string, a resolved string or derived from the variant. diff --git a/src/recipe/parser/output.rs b/src/recipe/parser/output.rs index 3fe378a25..d48982647 100644 --- a/src/recipe/parser/output.rs +++ b/src/recipe/parser/output.rs @@ -246,7 +246,9 @@ pub fn find_outputs_from_src(src: S) -> Result, Parsing } } - output_map.remove("recipe"); + if let Some(recipe_node) = output_map.remove("recipe") { + output_map.insert("__top_level_recipe".into(), recipe_node); + } let recipe = match Node::try_from(output_node) { Ok(node) => node, diff --git a/src/recipe/parser/package.rs b/src/recipe/parser/package.rs index a833440b6..455d412dc 100644 --- a/src/recipe/parser/package.rs +++ b/src/recipe/parser/package.rs @@ -15,6 +15,16 @@ use crate::{ use super::FlattenErrors; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TopLevelRecipe { + /// The name of the recipe (used in the cache build) + pub name: String, + + /// The version of the recipe (used as default in the outputs) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, +} + /// A recipe package information. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Package { @@ -89,6 +99,50 @@ impl TryConvertNode for RenderedMappingNode { } } +impl TryConvertNode for RenderedNode { + fn try_convert(&self, name: &str) -> Result> { + self.as_mapping() + .ok_or_else(|| vec![_partialerror!(*self.span(), ErrorKind::ExpectedMapping,)]) + .and_then(|m| m.try_convert(name)) + } +} + +impl TryConvertNode for RenderedMappingNode { + fn try_convert(&self, name: &str) -> Result> { + let mut name_val = None; + let mut version = None; + + self.iter() + .map(|(key, value)| { + let key_str = key.as_str(); + match key_str { + "name" => name_val = value.try_convert(key_str)?, + "version" => version = value.try_convert(key_str)?, + invalid => { + return Err(vec![_partialerror!( + *key.span(), + ErrorKind::InvalidField(invalid.to_string().into()), + help = format!("valid fields for `{name}` are `name` and `version`") + )]) + } + } + Ok(()) + }) + .flatten_errors()?; + + let Some(name) = name_val else { + return Err(vec![_partialerror!( + *self.span(), + ErrorKind::MissingField("name".into()), + label = "add the field `name` in between here", + help = format!("the field `name` is required for `{name}`") + )]); + }; + + Ok(TopLevelRecipe { name, version }) + } +} + /// A package information used for [`Output`] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct OutputPackage {