Skip to content

Commit

Permalink
Merge pull request #2878 from Hofer-Julian/feat/for_pinning
Browse files Browse the repository at this point in the history
feat: Add `for_pinning` support for `recipe.yaml`
  • Loading branch information
beckermr authored Aug 8, 2024
2 parents 485407c + a2626fa commit 4b7ef4d
Show file tree
Hide file tree
Showing 16 changed files with 659 additions and 88 deletions.
8 changes: 6 additions & 2 deletions conda_forge_tick/feedstock_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,16 @@ def populate_feedstock_attributes(
),
)
elif isinstance(recipe_yaml, str):
platform_arch = (
f"{plat}-{arch}"
if isinstance(plat, str) and isinstance(arch, str)
else None
)
variant_yamls.append(
parse_recipe_yaml(
recipe_yaml,
platform=plat,
platform_arch=platform_arch,
cbc_path=cbc_path,
arch=arch,
),
)

Expand Down
182 changes: 128 additions & 54 deletions conda_forge_tick/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,9 @@ def _render_meta_yaml(text: str, for_pinning: bool = False, **kwargs) -> str:

def parse_recipe_yaml(
text: str,
for_pinning=False,
platform=None,
arch=None,
cbc_path=None,
log_debug=False,
for_pinning: bool = False,
platform_arch: str | None = None,
cbc_path: str | None = None,
use_container: bool | None = None,
) -> "RecipeTypedDict":
"""Parse the recipe.yaml.
Expand All @@ -410,10 +408,8 @@ def parse_recipe_yaml(
The raw text in conda-forge feedstock recipe.yaml file
for_pinning : bool, optional
If True, render the recipe.yaml for pinning migrators, by default False.
platform : str, optional
The platform (e.g., 'linux', 'osx', 'win').
arch : str, optional
The CPU architecture (e.g., '64', 'aarch64').
platform_arch : str, optional
The platform and arch (e.g., 'linux-64', 'osx-arm64', 'win-64').
cbc_path : str, optional
The path to global pinning file.
log_debug : bool, optional
Expand All @@ -438,27 +434,23 @@ def parse_recipe_yaml(
return parse_recipe_yaml_containerized(
text,
for_pinning=for_pinning,
platform=platform,
arch=arch,
log_debug=log_debug,
platform_arch=platform_arch,
cbc_path=cbc_path,
)
else:
return parse_recipe_yaml_local(
text,
for_pinning=for_pinning,
platform=platform,
arch=arch,
log_debug=log_debug,
platform_arch=platform_arch,
cbc_path=cbc_path,
)


def parse_recipe_yaml_containerized(
text: str,
for_pinning=False,
platform=None,
arch=None,
cbc_path=None,
log_debug=False,
for_pinning: bool = False,
platform_arch: str | None = None,
cbc_path: str | None = None,
) -> "RecipeTypedDict":
"""Parse the recipe.yaml.
Expand All @@ -470,14 +462,10 @@ def parse_recipe_yaml_containerized(
The raw text in conda-forge feedstock recipe.yaml file
for_pinning : bool, optional
If True, render the recipe.yaml for pinning migrators, by default False.
platform : str, optional
The platform (e.g., 'linux', 'osx', 'win').
arch : str, optional
The CPU architecture (e.g., '64', 'aarch64').
platform_arch : str, optional
The platform and arch (e.g., 'linux-64', 'osx-arm64', 'win-64').
cbc_path : str, optional
The path to global pinning file.
log_debug : bool, optional
If True, print extra debugging info. Default is False.
Returns
-------
Expand All @@ -487,14 +475,11 @@ def parse_recipe_yaml_containerized(
"""
args = []

if platform is not None:
args += ["--platform", platform]
if platform_arch is not None:
args += ["--platform-arch", platform_arch]

if arch is not None:
args += ["--arch", arch]

if log_debug:
args += ["--log-debug"]
if cbc_path is not None:
args += ["--cbc-path", cbc_path]

if for_pinning:
args += ["--for-pinning"]
Expand All @@ -509,11 +494,9 @@ def parse_recipe_yaml_containerized(

def parse_recipe_yaml_local(
text: str,
for_pinning=False,
platform=None,
arch=None,
cbc_path=None,
log_debug=False,
for_pinning: bool = False,
platform_arch: str | None = None,
cbc_path: str | None = None,
) -> "RecipeTypedDict":
"""Parse the recipe.yaml.
Expand All @@ -523,14 +506,10 @@ def parse_recipe_yaml_local(
The raw text in conda-forge feedstock recipe.yaml file
for_pinning : bool, optional
If True, render the recipe.yaml for pinning migrators, by default False.
platform : str, optional
The platform (e.g., 'linux', 'osx', 'win').
arch : str, optional
The CPU architecture (e.g., '64', 'aarch64').
platform_arch : str, optional
The platform and arch (e.g., 'linux-64', 'osx-arm64', 'win-64').
cbc_path : str, optional
The path to global pinning file.
log_debug : bool, optional
If True, print extra debugging info. Default is False.
Returns
-------
Expand All @@ -539,14 +518,19 @@ def parse_recipe_yaml_local(
for some errors. Have fun.
"""

rendered_recipe = _render_recipe_yaml(text)
parsed_recipes = _parse_validated_recipes(rendered_recipe)
rendered_recipes = _render_recipe_yaml(
text, cbc_path=cbc_path, platform_arch=platform_arch
)
if for_pinning:
rendered_recipes = _process_recipe_for_pinning(rendered_recipes)
parsed_recipes = _parse_recipes(rendered_recipes)
return parsed_recipes


def _render_recipe_yaml(
text: str,
cbc_path=None,
platform_arch: str | None = None,
cbc_path: str | None = None,
) -> list[dict[str, Any]]:
"""
Renders the given recipe YAML text using the `rattler-build` command-line tool.
Expand All @@ -555,6 +539,8 @@ def _render_recipe_yaml(
----------
text : str
The recipe YAML text to render.
platform : str, optional
The platform (e.g., 'linux', 'osx', 'win').
cbc_path : str, optional
The path to global pinning file.
Expand All @@ -564,8 +550,13 @@ def _render_recipe_yaml(
The rendered recipe as a dictionary.
"""
variant_config_flags = [] if cbc_path is None else ["--variant-config", cbc_path]
build_platform_flags = (
[] if platform_arch is None else ["--build-platform", platform_arch]
)
res = subprocess.run(
["rattler-build", "build", "--render-only"] + variant_config_flags,
["rattler-build", "build", "--render-only"]
+ variant_config_flags
+ build_platform_flags,
stdout=subprocess.PIPE,
text=True,
input=text,
Expand All @@ -574,9 +565,41 @@ def _render_recipe_yaml(
return [output["recipe"] for output in json.loads(res.stdout)]


def _parse_validated_recipes(
def _process_recipe_for_pinning(recipes: list[dict[str, Any]]) -> list[dict[str, Any]]:
def replace_name_key(d: dict[str, Any]) -> Any:
for key, value in d.items():
if isinstance(value, dict):
if key in ["pin_subpackage", "pin_compatible"] and "name" in value:
# Create a new dictionary with 'package_name' first
new_value = {"package_name": value.pop("name")}
new_value.update(value)
d[key] = {"name": _munge_dict_repr(new_value)}
else:
replace_name_key(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
replace_name_key(item)
return d

return [replace_name_key(recipe) for recipe in recipes]


def _parse_recipes(
validated_recipes: list[dict[str, Any]],
) -> "RecipeTypedDict":
"""Parses validated recipes and transform them to fit `RecipeTypedDict`
Parameters
----------
validated_recipes : list[dict[str, Any]]
The recipes validated and rendered by `rattler-build`
Returns
-------
RecipeTypedDict
A dict conforming to conda-build's rendered output
"""
first = validated_recipes[0]
about = first["about"]
build = first["build"]
Expand All @@ -598,6 +621,9 @@ def _parse_validated_recipes(
"summary": about.get("summary"),
}
)

_parse_recipe_yaml_requirements(requirements)

build_data = (
None
if build is None or requirements is None
Expand Down Expand Up @@ -644,17 +670,17 @@ def _parse_validated_recipes(
None
if requirements_output is None
else {
"build": requirements_output.get("build"),
"host": requirements_output.get("host"),
"run": requirements_output.get("run"),
"build": requirements_output.get("build", []),
"host": requirements_output.get("host", []),
"run": requirements_output.get("run", []),
}
)
build_output_data = (
None
if run_exports_output is None
else {
"strong": run_exports_output.get("strong"),
"weak": run_exports_output.get("weak"),
"strong": run_exports_output.get("strong", []),
"weak": run_exports_output.get("weak", []),
}
)
output_data.append(
Expand All @@ -680,6 +706,54 @@ def _parse_validated_recipes(
return _remove_none_values(parsed_recipes)


def _parse_recipe_yaml_requirements(requirements) -> None:
"""Parse requirement section of render by rattler-build to fit `RecipeTypedDict`
When rendering the recipe by rattler build,
`requirements["run_exports"]["weak"]` gives a list looking like:
[
{
"pin_subpackage": {
"name": "slepc",
"lower_bound": "x.x.x.x.x.x",
"upper_bound": "x.x"
}
},
"numpy"
]
`run_exports["weak"]` of RecipeTypedDict looks like:
[
"slepc",
"numpy"
]
The same applies to "strong".
This function takes care of this transformation
requirements : dict
The requirements section of the recipe rendered by rattler-build.
This parameter will be modified by this function.
"""
if "run_exports" not in requirements:
return

run_exports = requirements["run_exports"]
for strength in ["strong", "weak"]:
original = run_exports.get(strength)
if isinstance(original, list):
result = []
for entry in original:
if isinstance(entry, str):
result.append(entry)
elif isinstance(entry, dict):
for key in ["pin_subpackage", "pin_compatible"]:
if key in entry and "name" in entry[key]:
result.append(entry[key]["name"])
run_exports[strength] = result


def _remove_none_values(d):
"""Recursively remove dictionary entries with None values."""
if not isinstance(d, dict):
Expand Down
32 changes: 11 additions & 21 deletions docker/run_bot_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,18 +392,16 @@ def _parse_meta_yaml(
def _parse_recipe_yaml(
*,
for_pinning,
platform,
arch,
log_debug,
platform_arch,
cbc_path,
):
from conda_forge_tick.utils import parse_recipe_yaml_local

return parse_recipe_yaml_local(
sys.stdin.read(),
for_pinning=for_pinning,
platform=platform,
arch=arch,
log_debug=log_debug,
platform_arch=platform_arch,
cbc_path=cbc_path,
)


Expand Down Expand Up @@ -500,33 +498,25 @@ def parse_meta_yaml(
help="Parse the recipe.yaml for pinning requirements.",
)
@click.option(
"--platform",
"--platform-arch",
type=str,
default=None,
help="The platform (e.g., 'linux', 'osx', 'win').",
help="The platform and arch (e.g., 'linux-64', 'osx-arm64', 'win-64').",
)
@click.option(
"--arch",
type=str,
default=None,
help="The CPU architecture (e.g., '64', 'aarch64').",
"--cbc-path", type=str, default=None, help="The path to global pinning file."
)
@click.option("--log-debug", is_flag=True, help="Log debug information.")
def parse_recipe_yaml(
log_level,
for_pinning,
platform,
arch,
log_debug,
platform_arch,
cbc_path,
):
return _run_bot_task(
_parse_recipe_yaml,
log_level=log_level,
existing_feedstock_node_attrs=None,
for_pinning=for_pinning,
platform=platform,
arch=arch,
log_debug=log_debug,
platform_arch=platform_arch,
cbc_path=cbc_path,
)


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ extend-exclude = ["conda_forge_tick/migrators/disabled/legacy.py"]

[tool.ruff.lint]
select = ["E", "F", "I", "W"]
ignore = ["E501"]
preview = true

[tool.ruff.lint.pycodestyle]
Expand Down
Loading

0 comments on commit 4b7ef4d

Please sign in to comment.