Skip to content
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

NEW: Migrate NVIDIA redist feedstocks to use cf-nvidia-tools #3580

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
7 changes: 6 additions & 1 deletion conda_forge_tick/make_migrators.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
XzLibLzmaDevelMigrator,
make_from_lazy_json_data,
skip_migrator_due_to_schema,
AddNVIDIATools,
)
from conda_forge_tick.migrators.arch import OSXArm
from conda_forge_tick.migrators.migration_yaml import (
Expand Down Expand Up @@ -794,7 +795,11 @@ def initialize_migrators(
gx: nx.DiGraph,
dry_run: bool = False,
) -> MutableSequence[Migrator]:
migrators: List[Migrator] = []
migrators: List[Migrator] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should append this migrator to the list after the noarch python migrator. There are some conventions in the code about where each kind of migrator goes in the list.

AddNVIDIATools(
check_solvable=False,
),
]

add_arch_migrate(migrators, gx)

Expand Down
1 change: 1 addition & 0 deletions conda_forge_tick/migrators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@
from .version import Version
from .xz_to_liblzma_devel import XzLibLzmaDevelMigrator
from .noarch_python_min import NoarchPythonMinMigrator
from .nvtools import AddNVIDIATools
245 changes: 245 additions & 0 deletions conda_forge_tick/migrators/nvtools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import os.path
import logging
import copy
from typing import Any

from conda_forge_tick.contexts import ClonedFeedstockContext
import conda_forge_tick.migrators

from conda_forge_tick.migrators_types import AttrsTypedDict, MigrationUidTypedDict
from conda_forge_tick.utils import (
yaml_safe_dump,
yaml_safe_load,
get_bot_run_url,
)


def _file_contains(filename, string):
with open(filename, "r") as f:
return string in f.read()


def _insert_requirements_build(meta):
insert_before: bool = True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we can add this after compilers and like (assuming I'm understanding the intent of this parameter correctly)

Suggested change
insert_before: bool = True
insert_before: bool = False

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a function parameter. It is a variable which holds the state of "whether we are currently inserting lines into the variable named 'before'".

We're not going to do that because trying to locate the compiler packages and insert the new dependency after them is technically challenging and not necessary for a working migration. Adding the complexity of inserting after compiler packages instead of just inserting into requirements.build makes additional assumptions including 1. that the package has compiler packages listed in the top-level requirements.build section 2. the compiler packages are already sorted to the top of the requirements.build section

before: list[str] = []
after: list[str] = []
requirements_found = False
build_found = False
with open(meta, "r") as f:
for line in f:
if build_found:
insert_before = False
elif line.startswith("requirements"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if build_found:
insert_before = False
elif line.startswith("requirements"):
if line.startswith("requirements"):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional. Once you find the requirements.build section, you don't need to do any more searching for it because you already know where to split the text. Maybe I could refactor to not execute insert_before=False, but I'm not sure it's worth the effort?

requirements_found = True
elif requirements_found and line.lstrip().startswith("build"):
build_found = True

if insert_before:
before.append(line)
else:
after.append(line)

if not (build_found or requirements_found):
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is the idea of this check that we are skipping metapackages? Or are there other cases this would be used for?

May help to have a comment above clarifying intent

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we didn't find the requirements.build section, then we cannot add our new dependency to the file.


with open(meta, "w") as f:
f.writelines(before + [" - cf-nvidia-tools # [linux]\n"] + after)


def _insert_build_script(meta):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding this to the recipe's test/commands section?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. There's no reason to wait until the test phase to check whether the binaries have the expected glibc symbols. We can save everyone time by failing as early as possible. Additionally, I think that the environment variable c_stdlib_version is not set during the test phase, and it's required for this check.

insert_before: bool = True
before: list[str] = []
after: list[str] = []
script_found = False
build_found = False
with open(meta, "r") as f:
for line in f:
if not insert_before:
pass
elif line.startswith("build"):
build_found = True
elif build_found and line.lstrip().startswith("script"):
script_found = True
elif build_found and script_found:
if line.lstrip().startswith("-"):
# inside script
pass
else:
# empty script section?
insert_before = False
if insert_before:
before.append(line)
else:
after.append(line)

if not (script_found or build_found):
return

with open(meta, "w") as f:
f.writelines(
before
+ [' - check-glibc "$PREFIX"/lib/*.so.* "$PREFIX"/bin/* # [linux]\n']
+ after
)


class AddNVIDIATools(conda_forge_tick.migrators.Migrator):
"""Add the cf-nvidia-tools package to NVIDIA redist feedstocks.

In order to ensure that NVIDIA's redistributed binaries (redists) are being packaged
correctly, NVIDIA has created a package containing a collection of tools to perform
common actions for NVIDIA recipes.

At this time, the package may be used to check Linux binaries for their minimum glibc
requirement in order to ensure that the correct metadata is being used in the conda
package.

This migrator will attempt to add this glibc check to all feedstocks which download any
artifacts from https://developer.download.nvidia.com. The check involves adding
something like:

```bash
check-glibc "$PREFIX"/lib/*.so.* "$PREFIX"/bin/*
```

to the build script after the package artifacts have been installed.

> [!NOTE]
> A human needs to verify that the glob expression is checking all of the correct
> artifacts!

More information about cf-nvidia-tools is available in the feedstock's
[README](https://github.com/conda-forge/cf-nvidia-tools-feedstock/tree/main/recipe).

Please ping carterbox for questions.
"""

name = "NVIDIA Tools Migrator"

rerender = True

max_solver_attempts = 3

migrator_version = 0

allow_empty_commits = False

allowed_schema_versions = [0]

def filter(self, attrs: "AttrsTypedDict", not_bad_str_start: str = "") -> bool:
"""If true don't act upon node

Parameters
----------
attrs : dict
The node attributes
not_bad_str_start : str, optional
If the 'bad' notice starts with the string then it is not
to be excluded. For example, rebuild migrations don't need
to worry about if the upstream can be fetched. Defaults to ``''``

Returns
-------
bool :
True if node is to be skipped
"""
return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the filter method always needs calls to super and to check the schema version. Check some of the other migrators for examples.

attrs["archived"]
or "https://developer.download.nvidia.com" not in attrs["source"]["url"]
)

def migrate(
self, recipe_dir: str, attrs: AttrsTypedDict, **kwargs: Any
) -> MigrationUidTypedDict:
"""Perform the migration, updating the ``meta.yaml``

Parameters
----------
recipe_dir : str
The directory of the recipe
attrs : dict
The node attributes

Returns
-------
namedtuple or bool:
If namedtuple continue with PR, if False scrap local folder
"""
# STEP 0: Bump the build number
self.set_build_number(os.path.join(recipe_dir, "meta.yaml"))

# STEP 1: Add cf-nvidia-tools to build requirements
meta = os.path.join(recipe_dir, "meta.yaml")
if _file_contains(meta, "cf-nvidia-tools"):
logging.debug("cf-nvidia-tools already in meta.yaml; not adding again.")
else:
_insert_requirements_build(meta)
logging.debug("cf-nvidia-tools added to meta.yaml.")

# STEP 2: Add check-glibc to the build script
build = os.path.join(recipe_dir, "build.sh")
if os.path.isfile(build):
if _file_contains(build, "check-glibc"):
logging.debug(
"build.sh already contains check-glibc; not adding again."
)
else:
with open(build, "a") as file:
file.write('\ncheck-glibc "$PREFIX"/lib/*.so.* "$PREFIX"/bin/*\n')
logging.debug("Added check-glibc to build.sh")
else:
if _file_contains(meta, "check-glibc"):
logging.debug(
"meta.yaml already contains check-glibc; not adding again."
)
else:
_insert_build_script(meta)
logging.debug("Added check-glibc to meta.yaml")

# STEP 3: Remove os_version keys from conda-forge.yml
config = os.path.join(recipe_dir, "..", "conda-forge.yml")
with open(config) as f:
y = yaml_safe_load(f)
y_orig = copy.deepcopy(y)
y.pop("os_version", None)
if y_orig != y:
with open(config, "w") as f:
yaml_safe_dump(y, f)
Comment on lines +198 to +206
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are dropping the os_version specification of the Docker image to use?

Wonder if it makes to add/confirm {{ stdlib("c") }} is used and c_stdlib_version is set correctly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's not necessary to force use an old container.

The tool fails if c_stdlib_version is not set, so indirectly this is already checked?


return self.migrator_uid(attrs)

def pr_body(
self, feedstock_ctx: ClonedFeedstockContext, add_label_text=True
) -> str:
"""Create a PR message body

Returns
-------
body: str
The body of the PR message
:param feedstock_ctx:
"""
body = f"{AddNVIDIATools.__doc__}\n\n"

if add_label_text:
body += (
"If this PR was opened in error or needs to be updated please add "
"the `bot-rerun` label to this PR. The bot will close this PR and "
"schedule another one. If you do not have permissions to add this "
"label, you can use the phrase "
"<code>@<space/>conda-forge-admin, please rerun bot</code> "
"in a PR comment to have the `conda-forge-admin` add it for you.\n\n"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if we should add some text on how to handle issues or ask for help in resolving

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look at line 113. The description directs users (us) to ping me for questions.

)

body += (
"<sub>"
"This PR was created by the [regro-cf-autotick-bot](https://github.com/regro/cf-scripts). "
"The **regro-cf-autotick-bot** is a service to automatically "
"track the dependency graph, migrate packages, and "
"propose package version updates for conda-forge. "
"Feel free to drop us a line if there are any "
"[issues](https://github.com/regro/cf-scripts/issues)! "
+ f"This PR was generated by {get_bot_run_url()} - please use this URL for debugging."
+ "</sub>"
)
return body