Skip to content

Commit

Permalink
Merge pull request #1992 from wolfv/lint-stdlib
Browse files Browse the repository at this point in the history
Add conda recipe format v2 stdlib lint support
  • Loading branch information
beckermr authored Aug 6, 2024
2 parents 039f4d9 + a56628c commit 66d7293
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 47 deletions.
13 changes: 11 additions & 2 deletions conda_smithy/lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,12 @@ def lintify_meta_yaml(

conda_build_config_filename = None
if recipe_dir:
cbc_file = "conda_build_config.yaml"
if is_rattler_build:
cbc_file = "variants.yaml"

conda_build_config_filename = find_local_config_file(
recipe_dir, "conda_build_config.yaml"
recipe_dir, cbc_file
)

if conda_build_config_filename:
Expand Down Expand Up @@ -351,7 +355,12 @@ def lintify_meta_yaml(

# 5: stdlib-related lints
lint_stdlib(
meta, requirements_section, conda_build_config_filename, lints, hints
meta,
requirements_section,
conda_build_config_filename,
lints,
hints,
is_rattler_build,
)

return lints, hints
Expand Down
138 changes: 94 additions & 44 deletions conda_smithy/linter/lints.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from conda.exceptions import InvalidVersionSpec
from conda.models.version import VersionOrder
from rattler_build_conda_compat.jinja.jinja import render_recipe_with_context
from rattler_build_conda_compat.loader import parse_recipe_config_file
from ruamel.yaml import CommentedSeq

from conda_smithy.linter import rattler_linter
Expand Down Expand Up @@ -717,56 +718,84 @@ def lint_go_licenses_are_bundled(


def lint_stdlib(
meta, requirements_section, conda_build_config_filename, lints, hints
meta,
requirements_section,
conda_build_config_filename,
lints,
hints,
is_rattler_build: bool = False,
):
global_build_reqs = requirements_section.get("build") or []
global_run_reqs = requirements_section.get("run") or []
global_constraints = requirements_section.get("run_constrained") or []
if is_rattler_build:
global_constraints = requirements_section.get("run_constraints") or []
else:
global_constraints = requirements_section.get("run_constrained") or []

if is_rattler_build:
jinja_stdlib_c = '`${{ stdlib("c") }}`'
else:
jinja_stdlib_c = '`{{ stdlib("c") }}`'

stdlib_lint = (
"This recipe is using a compiler, which now requires adding a build "
'dependence on `{{ stdlib("c") }}` as well. Note that this rule applies to '
f"dependence on {jinja_stdlib_c} as well. Note that this rule applies to "
"each output of the recipe using a compiler. For further details, please "
"see https://github.com/conda-forge/conda-forge.github.io/issues/2102."
)
pat_compiler_stub = re.compile(
"(m2w64_)?(c|cxx|fortran|rust)_compiler_stub"
)
outputs = get_section(meta, "outputs", lints)
if not is_rattler_build:
pat_compiler_stub = re.compile(
"(m2w64_)?(c|cxx|fortran|rust)_compiler_stub"
)
else:
pat_compiler_stub = re.compile(r"^\${{ compiler\(")

outputs = get_section(meta, "outputs", lints, is_rattler_build)
output_reqs = [x.get("requirements", {}) for x in outputs]

# deal with cb2 recipes (no build/host/run distinction)
output_reqs = [
{"host": x, "run": x} if isinstance(x, CommentedSeq) else x
for x in output_reqs
]
if not is_rattler_build:
output_reqs = [
{"host": x, "run": x} if isinstance(x, CommentedSeq) else x
for x in output_reqs
]

# collect output requirements
output_build_reqs = [x.get("build", []) or [] for x in output_reqs]
output_run_reqs = [x.get("run", []) or [] for x in output_reqs]
output_contraints = [
x.get("run_constrained", []) or [] for x in output_reqs
]
if is_rattler_build:
output_contraints = [
x.get("run_constraints", []) or [] for x in output_reqs
]
else:
output_contraints = [
x.get("run_constrained", []) or [] for x in output_reqs
]

# aggregate as necessary
all_build_reqs = [global_build_reqs] + output_build_reqs
all_build_reqs_flat = global_build_reqs
all_run_reqs_flat = global_run_reqs
all_contraints_flat = global_constraints
[all_build_reqs_flat := all_build_reqs_flat + x for x in output_build_reqs]
[all_run_reqs_flat := all_run_reqs_flat + x for x in output_run_reqs]
[all_contraints_flat := all_contraints_flat + x for x in output_contraints]

def flatten_reqs(reqs):
return itertools.chain.from_iterable(reqs)

all_build_reqs_flat += flatten_reqs(output_build_reqs)
all_run_reqs_flat += flatten_reqs(output_run_reqs)
all_contraints_flat += flatten_reqs(output_contraints)

# this check needs to be done per output --> use separate (unflattened) requirements
for build_reqs in all_build_reqs:
has_compiler = any(pat_compiler_stub.match(rq) for rq in build_reqs)
if has_compiler and "c_stdlib_stub" not in build_reqs:
stdlib_stub = "c_stdlib_stub" if not is_rattler_build else "${{ stdlib"
if has_compiler and stdlib_stub not in build_reqs:
if stdlib_lint not in lints:
lints.append(stdlib_lint)

sysroot_lint = (
"You're setting a requirement on sysroot_linux-<arch> directly; this should "
'now be done by adding a build dependence on `{{ stdlib("c") }}`, and '
f"now be done by adding a build dependence on {jinja_stdlib_c}, and "
"overriding `c_stdlib_version` in `recipe/conda_build_config.yaml` for the "
"respective platform as necessary. For further details, please see "
"https://github.com/conda-forge/conda-forge.github.io/issues/2102."
Expand All @@ -778,41 +807,62 @@ def lint_stdlib(

osx_lint = (
"You're setting a constraint on the `__osx` virtual package directly; this "
'should now be done by adding a build dependence on `{{ stdlib("c") }}`, '
f"should now be done by adding a build dependence on {jinja_stdlib_c}, "
"and overriding `c_stdlib_version` in `recipe/conda_build_config.yaml` for "
"the respective platform as necessary. For further details, please see "
"https://github.com/conda-forge/conda-forge.github.io/issues/2102."
)

to_check = all_run_reqs_flat + all_contraints_flat
if any(req.startswith("__osx >") for req in to_check):
if osx_lint not in lints:
lints.append(osx_lint)

# stdlib issues in CBC
cbc_lines = []
if conda_build_config_filename:
with open(conda_build_config_filename) as fh:
cbc_lines = fh.readlines()
# stdlib issues in CBC ( conda-build-config )
cbc_osx = {}

if is_rattler_build:
platform_namespace = {
"unix": True,
"osx": True,
"linux": False,
"win": False,
}

if conda_build_config_filename and os.path.exists(
conda_build_config_filename
):
cbc_osx = parse_recipe_config_file(
conda_build_config_filename,
platform_namespace,
allow_missing_selector=True,
)
else:
cbc_lines = []
if conda_build_config_filename:
with open(conda_build_config_filename) as fh:
cbc_lines = fh.readlines()

# filter on osx-relevant lines
pat = re.compile(
r"^([^:\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(?<!not\s)(linux|win)).*\]\s*$"
)
# remove lines with selectors that don't apply to osx, i.e. if they contain
# "not osx", "not unix", "linux" or "win"; this also removes trailing newlines.
# the regex here doesn't handle `or`-conjunctions, but the important thing for
# having a valid yaml after filtering below is that we avoid filtering lines with
# a colon (`:`), meaning that all yaml keys "survive". As an example, keys like
# c_stdlib_version can have `or`'d selectors, even if all values are arch-specific.
cbc_lines_osx = [pat.sub("", x) for x in cbc_lines]
cbc_content_osx = "\n".join(cbc_lines_osx)
cbc_osx = get_yaml().load(cbc_content_osx) or {}
# filter None values out of cbc_osx dict, can appear for example with
# ```
# c_stdlib_version: # [unix]
# - 2.17 # [linux]
# # note lack of osx
# ```

# filter on osx-relevant lines
pat = re.compile(
r"^([^:\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(?<!not\s)(linux|win)).*\]\s*$"
)
# remove lines with selectors that don't apply to osx, i.e. if they contain
# "not osx", "not unix", "linux" or "win"; this also removes trailing newlines.
# the regex here doesn't handle `or`-conjunctions, but the important thing for
# having a valid yaml after filtering below is that we avoid filtering lines with
# a colon (`:`), meaning that all yaml keys "survive". As an example, keys like
# c_stdlib_version can have `or`'d selectors, even if all values are arch-specific.
cbc_lines_osx = [pat.sub("", x) for x in cbc_lines]
cbc_content_osx = "\n".join(cbc_lines_osx)
cbc_osx = get_yaml().load(cbc_content_osx) or {}
# filter None values out of cbc_osx dict, can appear for example with
# ```
# c_stdlib_version: # [unix]
# - 2.17 # [linux]
# # note lack of osx
# ```
cbc_osx = dict(filter(lambda item: item[1] is not None, cbc_osx.items()))

def sort_osx(versions):
Expand Down
2 changes: 1 addition & 1 deletion conda_smithy/linter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def get_list_section(parent, name, lints, allow_single=False):
return [{}]


def find_local_config_file(recipe_dir, filename):
def find_local_config_file(recipe_dir: str, filename: str) -> Optional[str]:
# support
# 1. feedstocks
# 2. staged-recipes with custom conda-forge.yaml in recipe
Expand Down
3 changes: 3 additions & 0 deletions news/1992-lint-stdlib-v2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Added:**

* make `stdlib` linting work for v2 recipe format (#1992)
Loading

0 comments on commit 66d7293

Please sign in to comment.