diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e633c14..06c8cd3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,4 @@ - [boa] fix multi-output - [boa] fix keep run_export and existing spec when existing spec is not simple -- [mambabuild] allow testing multiple recipes (thanks @gabm) \ No newline at end of file +- [mambabuild] allow testing multiple recipes (thanks @gabm) diff --git a/boa/core/features.py b/boa/core/features.py new file mode 100644 index 00000000..4bf5d025 --- /dev/null +++ b/boa/core/features.py @@ -0,0 +1,14 @@ +def extract_features(feature_string): + if feature_string and len(feature_string): + assert feature_string.startswith("[") and feature_string.endswith("]") + features = [f.strip() for f in feature_string[1:-1].split(",")] + else: + features = [] + + selected_features = {} + for f in features: + if f.startswith("~"): + selected_features[f[1:]] = False + else: + selected_features[f] = True + return selected_features diff --git a/boa/core/metadata.py b/boa/core/metadata.py index 2a48fdeb..a8dd8a5e 100644 --- a/boa/core/metadata.py +++ b/boa/core/metadata.py @@ -157,6 +157,13 @@ def get_value(self, in_key: str, default: Any = None, autotype=True) -> Any: else: return section.get(key, default) + def rendered_meta(self): + res = self.meta.copy() + for typ in res.get("requirements", tuple()): + res["requirements"][typ] = [x.final_pin for x in self.get_dependencies(typ)] + + return res + @property def source_provided(self): return not bool(self.meta.get("source")) or ( diff --git a/boa/core/recipe_handling.py b/boa/core/recipe_handling.py index 9a870d80..ffd8a051 100644 --- a/boa/core/recipe_handling.py +++ b/boa/core/recipe_handling.py @@ -98,14 +98,11 @@ def _copy_output_recipe(m, dest_dir): def output_yaml(metadata, filename=None, suppress_outputs=False): - local_metadata = metadata.copy() - if ( - suppress_outputs - and local_metadata.is_output - and "outputs" in local_metadata.meta - ): - del local_metadata.meta["outputs"] - output = yaml.dump((local_metadata.meta), default_flow_style=False, indent=4) + local_metadata = metadata.rendered_meta().copy() + if suppress_outputs and metadata.is_output and "outputs" in local_metadata: + del local_metadata["outputs"] + + output = yaml.dump((local_metadata), default_flow_style=False, indent=4) if filename: if any(sep in filename for sep in ("\\", "/")): mkdir_p(os.path.dirname(filename)) diff --git a/boa/core/recipe_output.py b/boa/core/recipe_output.py index 728f2c1e..64032ad0 100644 --- a/boa/core/recipe_output.py +++ b/boa/core/recipe_output.py @@ -9,7 +9,7 @@ import sys from dataclasses import dataclass -from typing import Tuple +from typing import Tuple, List import rich from rich.table import Table @@ -39,7 +39,7 @@ class CondaBuildSpec: is_compiler: bool = False is_transitive_dependency: bool = False channel: str = "" - # final: String + features: List[str] = None from_run_export: bool = False from_pinnings: bool = False @@ -48,11 +48,19 @@ def __init__(self, ms): self.raw = ms self.splitted = ms.split() self.name = self.splitted[0] + if len(self.splitted) > 1: self.is_pin = self.splitted[1].startswith("PIN_") self.is_pin_compatible = self.splitted[1].startswith("PIN_COMPATIBLE") self.is_compiler = self.splitted[0].startswith("COMPILER_") + if not (self.is_pin or self.is_pin_compatible or self.is_compiler): + for idx, x in enumerate(self.splitted): + if x.startswith("["): + self.features = [f.strip() for f in x[1:-1].split(",")] + self.splitted = self.splitted[:idx] + break + self.is_simple = len(self.splitted) == 1 self.final = self.raw @@ -63,6 +71,17 @@ def __init__(self, ms): def final_name(self): return self.final.split(" ")[0] + @property + def final_pin(self): + if hasattr(self, "final_version"): + return f"{self.final_name} {self.final_version[0]} {self.final_version[1]}" + else: + return self.final + + @property + def final_triplet(self): + return f"{self.final_name}-{self.final_version[0]}-{self.final_version[1]}" + def loosen_spec(self): if self.is_compiler or self.is_pin: return @@ -153,6 +172,42 @@ def eval_pin_compatible(self, build, host): else self.name ) + def eval_features(self, feature_map): + active_features = [] + if not self.features: + return + + for f in self.features: + if ( + f[0] == "&" + and f[1:] in feature_map[f[1:]] + and feature_map[f[1:]]["activated"] + ): + active_features.append(f[1:]) + elif f[0] != "&": + active_features.append(f) + + # special handling with `static` feature + if "static" in active_features: + self.name += "-static" + active_features.remove("static") + + if len(active_features): + active_features = sorted(active_features) + feature_string = ( + "*" + "".join([f"+{feat}*" for feat in active_features]) + "*" + ) + version = "*" + if len(self.splitted) >= 2: + version = self.splitted[1] + else: + feature_string = "" + version = "" + if len(self.splitted) >= 2: + version = self.splitted[1] + + self.final = f"{self.name} {version} {feature_string}".strip() + class Output: def __init__( @@ -515,10 +570,9 @@ def propagate_run_exports(self, env, pkg_cache): continue if s.name in self.sections["build"].get("ignore_run_exports", []): continue + if hasattr(s, "final_version"): - final_triple = ( - f"{s.final_name}-{s.final_version[0]}-{s.final_version[1]}" - ) + final_triplet = s.final_triplet else: console.print(f"[red]{s} has no final version") continue @@ -532,7 +586,7 @@ def propagate_run_exports(self, env, pkg_cache): collected_run_exports.append(s.run_exports_info) else: path = Path(pkg_cache).joinpath( - final_triple, "info", "run_exports.json", + final_triplet, "info", "run_exports.json", ) if path.exists(): with open(path) as fi: @@ -582,6 +636,7 @@ def _solve_env(self, env, all_outputs): if self.requirements.get(env): console.print(f"Finalizing [yellow]{env}[/yellow] for {self.name}") specs = self.requirements[env] + for s in specs: if s.is_pin: s.eval_pin_subpackage(all_outputs) @@ -589,7 +644,8 @@ def _solve_env(self, env, all_outputs): s.eval_pin_compatible( self.requirements["build"], self.requirements["host"] ) - + if s.features: + s.eval_features(self.feature_map) # save finalized requirements in data for usage in metadata self.data["requirements"][env] = [s.final for s in self.requirements[env]] diff --git a/boa/core/run_build.py b/boa/core/run_build.py index 532a8825..d7854a6d 100644 --- a/boa/core/run_build.py +++ b/boa/core/run_build.py @@ -20,6 +20,7 @@ from boa.core.config import boa_config from boa.core.validation import validate, ValidationError, SchemaError from boa.tui.exceptions import BoaRunBuildException +from boa.core.features import extract_features from conda_build.utils import rm_rf import conda_build.jinja_context @@ -621,22 +622,6 @@ def build_recipe( return sorted_outputs -def extract_features(feature_string): - if feature_string and len(feature_string): - assert feature_string.startswith("[") and feature_string.endswith("]") - features = [f.strip() for f in feature_string[1:-1].split(",")] - else: - features = [] - - selected_features = {} - for f in features: - if f.startswith("~"): - selected_features[f[1:]] = False - else: - selected_features[f] = True - return selected_features - - def run_build(args): if getattr(args, "json", False): global console