-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Recipe modifications #47
Changes from all commits
9677830
b9a7120
bb08bea
dcbbee7
806530e
1a8f6e5
facd822
106df2c
d0856ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
from __future__ import annotations | ||
|
||
import copy | ||
import hashlib | ||
import io | ||
import logging | ||
import re | ||
from typing import TYPE_CHECKING, Any, Literal | ||
|
||
import requests | ||
from ruamel.yaml import YAML | ||
|
||
from rattler_build_conda_compat.jinja.jinja import jinja_env, load_recipe_context | ||
from rattler_build_conda_compat.recipe_sources import Source, get_all_sources | ||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
yaml = YAML() | ||
yaml.preserve_quotes = True | ||
yaml.width = 4096 | ||
yaml.indent(mapping=2, sequence=4, offset=2) | ||
|
||
|
||
def _update_build_number_in_context(recipe: dict[str, Any], new_build_number: int) -> bool: | ||
for key in recipe.get("context", {}): | ||
if key.startswith("build_") or key == "build": | ||
recipe["context"][key] = new_build_number | ||
return True | ||
return False | ||
|
||
|
||
def _update_build_number_in_recipe(recipe: dict[str, Any], new_build_number: int) -> bool: | ||
is_modified = False | ||
if "build" in recipe and "number" in recipe["build"]: | ||
recipe["build"]["number"] = new_build_number | ||
is_modified = True | ||
|
||
if "outputs" in recipe: | ||
for output in recipe["outputs"]: | ||
if "build" in output and "number" in output["build"]: | ||
output["build"]["number"] = new_build_number | ||
is_modified = True | ||
|
||
return is_modified | ||
|
||
|
||
def update_build_number(file: Path, new_build_number: int = 0) -> str: | ||
""" | ||
Update the build number in the recipe file. | ||
|
||
Arguments: | ||
---------- | ||
* `file` - The path to the recipe file. | ||
* `new_build_number` - The new build number to use. (default: 0) | ||
|
||
Returns: | ||
-------- | ||
The updated recipe as a string. | ||
""" | ||
with file.open("r") as f: | ||
data = yaml.load(f) | ||
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesnt it make more sense to leave the loading of the yaml to the caller? I can imagine that if we have more modification functions they will all require loading the yaml. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, and no - we do want to minimally change the YAML so I think that should all be handled by us (leave comments in tact, order, formatting, ...). |
||
build_number_modified = _update_build_number_in_context(data, new_build_number) | ||
if not build_number_modified: | ||
_update_build_number_in_recipe(data, new_build_number) | ||
|
||
with io.StringIO() as f: | ||
yaml.dump(data, f) | ||
return f.getvalue() | ||
|
||
|
||
class CouldNotUpdateVersionError(Exception): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wdyt about moving errors in separate modules? |
||
NO_CONTEXT = "Could not find context in recipe" | ||
NO_VERSION = "Could not find version in recipe context" | ||
|
||
def __init__(self, message: str = "Could not update version") -> None: | ||
self.message = message | ||
super().__init__(self.message) | ||
|
||
|
||
class Hash: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe let's move it in separate module? like hash.py for example? |
||
def __init__(self, hash_type: Literal["md5", "sha256"], hash_value: str) -> None: | ||
self.hash_type = hash_type | ||
self.hash_value = hash_value | ||
|
||
def __str__(self) -> str: | ||
return f"{self.hash_type}: {self.hash_value}" | ||
|
||
|
||
def _has_jinja_version(url: str) -> bool: | ||
"""Check if the URL has a jinja `${{ version }}` in it.""" | ||
pattern = r"\${{\s*version" | ||
return re.search(pattern, url) is not None | ||
|
||
|
||
def update_hash(source: Source, url: str, hash_: Hash | None) -> None: | ||
""" | ||
Update the sha256 hash in the source dictionary. | ||
|
||
Arguments: | ||
---------- | ||
* `source` - The source dictionary to update. | ||
* `url` - The URL to download and hash (if no hash is provided). | ||
* `hash_` - The hash to use. If not provided, the file will be downloaded and `sha256` hashed. | ||
""" | ||
if "md5" in source: | ||
del source["md5"] | ||
if "sha256" in source: | ||
del source["sha256"] | ||
|
||
if hash_ is not None: | ||
source[hash_.hash_type] = hash_.hash_value | ||
else: | ||
# download and hash the file | ||
hasher = hashlib.sha256() | ||
logger.info("Retrieving and hashing %s", url) | ||
with requests.get(url, stream=True, timeout=100) as r: | ||
for chunk in r.iter_content(chunk_size=4096): | ||
hasher.update(chunk) | ||
source["sha256"] = hasher.hexdigest() | ||
|
||
|
||
def update_version(file: Path, new_version: str, hash_: Hash | None) -> str: | ||
""" | ||
Update the version in the recipe file. | ||
|
||
Arguments: | ||
---------- | ||
* `file` - The path to the recipe file. | ||
* `new_version` - The new version to use. | ||
* `hash_type` - The hash type to use. If not provided, the file will be downloaded and `sha256` hashed. | ||
|
||
Returns: | ||
-------- | ||
The updated recipe as a string. | ||
""" | ||
|
||
with file.open("r") as f: | ||
data = yaml.load(f) | ||
Comment on lines
+140
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, maybe it would make sense to have a variant that takes the loaded dictionary as input. |
||
|
||
if "context" not in data: | ||
raise CouldNotUpdateVersionError(CouldNotUpdateVersionError.NO_CONTEXT) | ||
if "version" not in data["context"]: | ||
raise CouldNotUpdateVersionError(CouldNotUpdateVersionError.NO_VERSION) | ||
|
||
data["context"]["version"] = new_version | ||
|
||
# set up the jinja context | ||
env = jinja_env() | ||
context = copy.deepcopy(data.get("context", {})) | ||
context_variables = load_recipe_context(context, env) | ||
# for r-recipes we add the default `cran_mirror` variable | ||
context_variables["cran_mirror"] = "https://cran.r-project.org" | ||
|
||
for source in get_all_sources(data): | ||
# render the whole URL and find the hash | ||
if "url" not in source: | ||
continue | ||
|
||
url = source["url"] | ||
if isinstance(url, list): | ||
url = url[0] | ||
|
||
if not _has_jinja_version(url): | ||
continue | ||
|
||
template = env.from_string(url) | ||
rendered_url = template.render(context_variables) | ||
|
||
update_hash(source, rendered_url, hash_) | ||
|
||
with io.StringIO() as f: | ||
yaml.dump(data, f) | ||
return f.getvalue() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# set the build number to something | ||
context: | ||
build: 0 | ||
|
||
package: | ||
name: recipe_1 | ||
version: "0.1.0" | ||
|
||
build: | ||
number: ${{ build }} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# set the build number to something | ||
context: | ||
build: 123 | ||
|
||
package: | ||
name: recipe_1 | ||
version: "0.1.0" | ||
|
||
build: | ||
number: ${{ build }} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# set the build number to something | ||
package: | ||
name: recipe_1 | ||
version: "0.1.0" | ||
|
||
# set the build number to something directly in the recipe text | ||
build: | ||
number: 0 | ||
|
||
source: | ||
- url: foo |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# set the build number to something | ||
package: | ||
name: recipe_1 | ||
version: "0.1.0" | ||
|
||
# set the build number to something directly in the recipe text | ||
build: | ||
number: 321 | ||
|
||
source: | ||
- url: foo |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
context: | ||
name: xtensor | ||
version: "0.25.0" | ||
|
||
|
||
package: | ||
name: ${{ name|lower }} | ||
version: ${{ version }} | ||
|
||
source: | ||
url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz | ||
sha256: 32d5d9fd23998c57e746c375a544edf544b74f0a18ad6bc3c38cbba968d5e6c7 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
context: | ||
name: xtensor | ||
version: "0.23.5" | ||
|
||
|
||
package: | ||
name: ${{ name|lower }} | ||
version: ${{ version }} | ||
|
||
source: | ||
url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz | ||
sha256: 0811011e448628f0dfa6ebb5e3f76dc7bf6a15ee65ea9c5a277b12ea976d35bc |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
context: | ||
name: xtensor | ||
version: "0.25.0" | ||
|
||
|
||
package: | ||
name: ${{ name|lower }} | ||
version: ${{ version }} | ||
|
||
source: | ||
# please update the version here. | ||
- if: target_platform == linux-64 | ||
then: | ||
url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz | ||
sha256: 32d5d9fd23998c57e746c375a544edf544b74f0a18ad6bc3c38cbba968d5e6c7 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
context: | ||
name: xtensor | ||
version: "0.23.5" | ||
|
||
|
||
package: | ||
name: ${{ name|lower }} | ||
version: ${{ version }} | ||
|
||
source: | ||
# please update the version here. | ||
- if: target_platform == linux-64 | ||
then: | ||
url: https://github.com/xtensor-stack/xtensor/archive/${{ version }}.tar.gz | ||
sha256: 0811011e448628f0dfa6ebb5e3f76dc7bf6a15ee65ea9c5a277b12ea976d35bc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wdyt about extracting YAML loader configuration in one place so it can be used by other loaders?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I also want to get rid of any
PyYAML
and just useruamel.YAML
everywhere.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, that's the next PR that I wanted to send, so let's maybe wait until then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good to me