diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a1d38d79..276e617a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,7 +70,11 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: coveralls - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@main with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./coverage.lcov + # TODO: revert as soon as feasible; coveralls has an extended outage + # and is recommending this work-around for the time being, see + # https://status.coveralls.io/incidents/pdbt7vdzlvpj + fail-on-error: false diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py index 47ff94fe4..0a4584736 100644 --- a/conda_smithy/lint_recipe.py +++ b/conda_smithy/lint_recipe.py @@ -53,6 +53,7 @@ lint_noarch, lint_noarch_and_runtime_dependencies, lint_non_noarch_builds, + lint_osx_pins, lint_package_version, lint_pin_subpackages, lint_recipe_have_tests, @@ -88,32 +89,51 @@ NEEDED_FAMILIES = ["gpl", "bsd", "mit", "apache", "psf"] -def _get_forge_yaml(recipe_dir: Optional[str] = None) -> dict: +def _get_feedstock_config(recipe_dir: Optional[str] = None) -> dict: + feedstock_config_keys = {} if recipe_dir: - forge_yaml_filename = ( - glob(os.path.join(recipe_dir, "..", "conda-forge.yml")) - or glob( - os.path.join(recipe_dir, "conda-forge.yml"), - ) - or glob( - os.path.join(recipe_dir, "..", "..", "conda-forge.yml"), - ) - ) - if forge_yaml_filename: - with open(forge_yaml_filename[0], encoding="utf-8") as fh: - forge_yaml = get_yaml().load(fh) - else: - forge_yaml = {} - else: - forge_yaml = {} + feedstock_config_file = find_local_config_file(recipe_dir, "conda-forge.yml") + if feedstock_config_file: + with open(feedstock_config_file, encoding="utf-8") as fh: + feedstock_config_keys = get_yaml().load(fh) + + return feedstock_config_keys + + +def _get_recipe_config_keys(recipe_dir: Optional[str] = None) -> dict: + """ + This function maps the two different recipe config formats to their keys - return forge_yaml + Currently, the content of variant.yaml files is not validated; the only + relevant information in that case is whether the file has been found + (i.e. the entry of the dict for "variant.yaml" is not None). + """ + # mapping from possible filenames to their content; v1 recipes can use either format + recipe_config_keys = {"conda_build_config.yaml": None, "variants.yaml": None} + if recipe_dir: + for config_filename in recipe_config_keys.copy().keys(): + config_file = find_local_config_file(recipe_dir, config_filename) + if not config_file: + # file not found, leave as None + continue + # otherwise, the file exists; content is definitely not None + recipe_config_keys[config_filename] = {} + # the linter currently does not analyze content of variants.yaml directly + if config_filename == "conda_build_config.yaml": + with open(config_file, encoding="utf-8") as fh: + fh_data = fh.read() + if fh_data: + recipe_config_keys[config_filename] = set( + get_yaml().load(fh_data).keys() + ) + + return recipe_config_keys def lintify_forge_yaml(recipe_dir: Optional[str] = None) -> (list, list): - forge_yaml = _get_forge_yaml(recipe_dir) + feedstock_config_keys = _get_feedstock_config(recipe_dir) # This is where we validate against the jsonschema and execute our custom validators. - return validate_json_schema(forge_yaml) + return validate_json_schema(feedstock_config_keys) def lintify_meta_yaml( @@ -125,7 +145,7 @@ def lintify_meta_yaml( lints = [] hints = [] major_sections = list(meta.keys()) - lints_to_skip = (_get_forge_yaml(recipe_dir).get("linter") or {}).get("skip") or [] + lints_to_skip = _get_feedstock_config(recipe_dir).get("linter", {}).get("skip", []) # If the recipe_dir exists (no guarantee within this function) , we can # find the meta.yaml within it. @@ -264,37 +284,12 @@ def lintify_meta_yaml( noarch_value = build_section.get("noarch") lint_noarch(noarch_value, lints) - conda_build_config_filename = None - if recipe_dir: - cbc_file = "conda_build_config.yaml" - if recipe_version == 1: - cbc_file = "variants.yaml" - - conda_build_config_filename = find_local_config_file(recipe_dir, cbc_file) - - if conda_build_config_filename: - with open(conda_build_config_filename, encoding="utf-8") as fh: - fh_data = fh.read() - if fh_data: - conda_build_config_keys = set(get_yaml().load(fh_data).keys()) - else: - conda_build_config_keys = set() - else: - conda_build_config_keys = set() - - forge_yaml_filename = find_local_config_file(recipe_dir, "conda-forge.yml") - - if forge_yaml_filename: - with open(forge_yaml_filename, encoding="utf-8") as fh: - forge_yaml = get_yaml().load(fh) - else: - forge_yaml = {} - else: - conda_build_config_keys = set() - forge_yaml = {} + # Interlude: load feedstock and recipe config + feedstock_config_keys = _get_feedstock_config(recipe_dir) + recipe_config_keys = _get_recipe_config_keys(recipe_dir) # 18: noarch doesn't work with selectors for runtime dependencies - noarch_platforms = len(forge_yaml.get("noarch_platforms", [])) > 1 + noarch_platforms = len(feedstock_config_keys.get("noarch_platforms", [])) > 1 if "lint_noarch_selectors" not in lints_to_skip: if recipe_version == 1: raw_requirements_section = meta.get("requirements", {}) @@ -309,8 +304,8 @@ def lintify_meta_yaml( lint_noarch_and_runtime_dependencies( noarch_value, recipe_fname, - forge_yaml, - conda_build_config_keys, + feedstock_config_keys, + recipe_config_keys["conda_build_config.yaml"], lints, ) @@ -373,6 +368,27 @@ def lintify_meta_yaml( recipe_name, build_requirements, lints, recipe_version=recipe_version ) + # 30: two configuration files present + if sum(v is not None for v in recipe_config_keys.values()) > 1: + lints.append( + "Found two recipe configuration files, but you may only use one! " + "You may use `conda_build_config.yaml` for both v0 and v1 recipes, " + "while `variants.yaml` may only be used with v1 recipes" + ) + + # 31: stdlib-related lints + if "lint_stdlib" not in lints_to_skip: + for config_fn in recipe_config_keys.keys(): + lint_stdlib( + meta, + requirements_section, + recipe_dir, + config_fn, + lints, + # the version of the config file does not change the version of the recipe + recipe_version=recipe_version, + ) + # hints # 1: suggest pip hint_pip_usage(build_section, hints) @@ -399,18 +415,7 @@ def lintify_meta_yaml( # 5: hint pypi.io -> pypi.org hint_sources_should_not_mention_pypi_io_but_pypi_org(sources_section, hints) - # 6: stdlib-related lints - if "lint_stdlib" not in lints_to_skip: - lint_stdlib( - meta, - requirements_section, - conda_build_config_filename, - lints, - hints, - recipe_version=recipe_version, - ) - - # 7: warn of `name =version=build` specs, suggest `name version build` + # 6: warn of `name =version=build` specs, suggest `name version build` # see https://github.com/conda/conda-build/issues/5571#issuecomment-2604505922 if recipe_version == 0: hint_space_separated_specs( @@ -420,11 +425,11 @@ def lintify_meta_yaml( hints, ) - # 8. check for obsolete os_version + # 7. check for obsolete os_version if "hint_os_version" not in lints_to_skip: - hint_os_version(forge_yaml, hints) + hint_os_version(feedstock_config_keys, hints) - # 9. check for bld.bat with rattler-build + # 8. check for bld.bat with rattler-build hint_rattler_build_bld_bat(recipe_dir, hints, recipe_version) return lints, hints @@ -523,7 +528,7 @@ def run_conda_forge_specific( hints, recipe_version: int = 0, ): - lints_to_skip = (_get_forge_yaml(recipe_dir).get("linter") or {}).get("skip") or [] + lints_to_skip = _get_feedstock_config(recipe_dir).get("linter", {}).get("skip", []) # Retrieve sections from meta package_section = get_section(meta, "package", lints, recipe_version=recipe_version) @@ -693,7 +698,14 @@ def run_conda_forge_specific( "The recipe should not have an empty `conda_build_config.yaml` file." ) - # 14: Do not allow custom Github Actions workflows + # 14: incorrect configuration on osx for c_stdlib_version, MACOSX_SDK_VERSION etc. + # get recipe config files (we don't care about the content, only if it's non-None) + recipe_config_keys = _get_recipe_config_keys(recipe_dir) + for config_fn, content in recipe_config_keys.items(): + if content is not None: + lint_osx_pins(recipe_dir, config_fn, lints, recipe_version) + + # 15: Do not allow custom Github Actions workflows gha_workflows_dir = Path(recipe_dir or "", "..", ".github", "workflows") gha_workflows = [ *gha_workflows_dir.glob("*.yml"), diff --git a/conda_smithy/linter/lints.py b/conda_smithy/linter/lints.py index c726285f2..2d9221e5c 100644 --- a/conda_smithy/linter/lints.py +++ b/conda_smithy/linter/lints.py @@ -386,7 +386,7 @@ def lint_noarch_and_runtime_dependencies( if is_selector_line( line, allow_platforms=noarch_platforms, - allow_keys=conda_build_config_keys, + allow_keys=conda_build_config_keys or set(), ): lints.append( "`noarch` packages can't have selectors. If " @@ -770,12 +770,132 @@ def lint_go_licenses_are_bundled( ) +def lint_osx_pins(recipe_dir, recipe_config_filename, lints, recipe_version): + cbc_osx = {} + if recipe_dir is None or recipe_config_filename is None: + # nothing left to do + return + + recipe_config_file = os.path.join(recipe_dir, recipe_config_filename) + + if recipe_config_filename == "variants.yaml": + # definitely v1 recipe + platform_namespace = { + "unix": True, + "osx": True, + "linux": False, + "win": False, + } + + if os.path.exists(recipe_config_file): + cbc_osx = parse_recipe_config_file( + recipe_config_file, + platform_namespace, + allow_missing_selector=True, + ) + cbc_osx = ensure_standard_strings(cbc_osx) + else: + # may be v0 or v1 recipe, but recipe config is conda_build_config.yaml + cbc_lines = [] + if os.path.exists(recipe_config_file): + with open(recipe_config_file, encoding="utf-8") as fh: + cbc_lines = fh.readlines() + + # filter on osx-relevant lines + pat = re.compile( + r"^([^:\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(? VersionOrder(str(versions[1])): + versions = versions[::-1] + return versions + + baseline_version = ["11.0", "11.0"] + v_stdlib = sort_osx(cbc_osx.get("c_stdlib_version", baseline_version)) + sdk = sort_osx(cbc_osx.get("MACOSX_SDK_VERSION", baseline_version)) + + if "c_stdlib_version" in cbc_osx.keys(): + # only warn if version is below baseline + outdated_lint = ( + "You are setting `c_stdlib_version` on osx below the current global " + f"baseline in conda-forge ({baseline_version[0]})." + ) + if len(v_stdlib) == len(baseline_version): + # if length matches, compare individually + for v_std, v_base in zip(v_stdlib, baseline_version): + if VersionOrder(str(v_std)) < VersionOrder(str(v_base)): + if outdated_lint not in lints: + lints.append(outdated_lint) + elif len(v_stdlib) == 1: + # compare against first value (same baseline for x64/arm64) + if VersionOrder(str(v_stdlib[0])) < VersionOrder(str(baseline_version[0])): + if outdated_lint not in lints: + lints.append(outdated_lint) + + # warn if SDK is lower than v_stdlib + sdk_lint = ( + "You are setting `MACOSX_SDK_VERSION` below `c_stdlib_version`, " + "in conda_build_config.yaml which is not possible! Please ensure " + "`MACOSX_SDK_VERSION` is at least `c_stdlib_version` " + "(you can leave it out if it is equal).\n" + "If you are not setting `c_stdlib_version` yourself, this means " + "you are requesting a version below the current global baseline in " + f"conda-forge ({baseline_version[0]})." + ) + if len(sdk) == len(v_stdlib): + # if length matches, compare individually + for v_sdk, v_std in zip(sdk, v_stdlib): + # versions with a single dot may have been read as floats + if VersionOrder(str(v_sdk)) < VersionOrder(str(v_std)): + if sdk_lint not in lints: + lints.append(sdk_lint) + elif len(sdk) == 1: + # if length doesn't match, only warn if a single SDK version + # is lower than _all_ merged deployment targets + if all( + VersionOrder(str(sdk[0])) < VersionOrder(str(v_std)) for v_std in v_stdlib + ): + if sdk_lint not in lints: + lints.append(sdk_lint) + + def lint_stdlib( meta, requirements_section, - conda_build_config_filename, + recipe_dir, + recipe_config_filename, lints, - hints, recipe_version: int = 0, ): global_build_reqs = requirements_section.get("build") or [] @@ -879,153 +999,6 @@ def flatten_reqs(reqs): if osx_lint not in lints: lints.append(osx_lint) - # stdlib issues in CBC ( conda-build-config ) - cbc_osx = {} - - if recipe_version == 1: - 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, - ) - cbc_osx = ensure_standard_strings(cbc_osx) - else: - cbc_lines = [] - if conda_build_config_filename: - with open(conda_build_config_filename, encoding="utf-8") as fh: - cbc_lines = fh.readlines() - - # filter on osx-relevant lines - pat = re.compile( - r"^([^:\#]*?)\s+\#\s\[.*(not\s(osx|unix)|(? VersionOrder(str(versions[1])): - versions = versions[::-1] - return versions - - baseline_version = ["11.0", "11.0"] - v_stdlib = sort_osx(cbc_osx.get("c_stdlib_version", baseline_version)) - macdt = sort_osx(cbc_osx.get("MACOSX_DEPLOYMENT_TARGET", baseline_version)) - sdk = sort_osx(cbc_osx.get("MACOSX_SDK_VERSION", baseline_version)) - - if {"MACOSX_DEPLOYMENT_TARGET", "c_stdlib_version"} <= set(cbc_osx.keys()): - # both specified, check that they match - if len(v_stdlib) != len(macdt): - # if lengths aren't matching, assume it's a legal combination - # where one key is specified for less arches than the other and - # let the rerender deal with the details - pass - else: - mismatch_lint = ( - "Conflicting specification for minimum macOS deployment target!\n" - "If your conda_build_config.yaml sets `MACOSX_DEPLOYMENT_TARGET`, " - "please change the name of that key to `c_stdlib_version`!\n" - "Continuing with `max(c_stdlib_version, MACOSX_DEPLOYMENT_TARGET)`." - ) - merged_dt = [] - for v_std, v_mdt in zip(v_stdlib, macdt): - # versions with a single dot may have been read as floats - v_std, v_mdt = str(v_std), str(v_mdt) - if VersionOrder(v_std) != VersionOrder(v_mdt): - if mismatch_lint not in lints: - lints.append(mismatch_lint) - merged_dt.append( - v_mdt if VersionOrder(v_std) < VersionOrder(v_mdt) else v_std - ) - cbc_osx["merged"] = merged_dt - elif "MACOSX_DEPLOYMENT_TARGET" in cbc_osx.keys(): - cbc_osx["merged"] = macdt - # only MACOSX_DEPLOYMENT_TARGET, should be renamed - deprecated_dt = ( - "In your conda_build_config.yaml, please change the name of " - "`MACOSX_DEPLOYMENT_TARGET`, to `c_stdlib_version`!" - ) - if deprecated_dt not in hints: - lints.append(deprecated_dt) - elif "c_stdlib_version" in cbc_osx.keys(): - cbc_osx["merged"] = v_stdlib - # only warn if version is below baseline - outdated_lint = ( - "You are setting `c_stdlib_version` below the current global baseline " - "in conda-forge (11.0). If this is your intention, you also need to " - "override `MACOSX_DEPLOYMENT_TARGET` (with the same value) locally." - ) - if len(v_stdlib) == len(macdt): - # if length matches, compare individually - for v_std, v_mdt in zip(v_stdlib, macdt): - if VersionOrder(str(v_std)) < VersionOrder(str(v_mdt)): - if outdated_lint not in lints: - lints.append(outdated_lint) - elif len(v_stdlib) == 1: - # if length doesn't match, only warn if a single stdlib version - # is lower than _all_ baseline deployment targets - if all( - VersionOrder(str(v_stdlib[0])) < VersionOrder(str(v_mdt)) - for v_mdt in macdt - ): - if outdated_lint not in lints: - lints.append(outdated_lint) - - # warn if SDK is lower than merged v_stdlib/macdt - merged_dt = cbc_osx.get("merged", baseline_version) - sdk_lint = ( - "You are setting `MACOSX_SDK_VERSION` below `c_stdlib_version`, " - "in conda_build_config.yaml which is not possible! Please ensure " - "`MACOSX_SDK_VERSION` is at least `c_stdlib_version` " - "(you can leave it out if it is equal).\n" - "If you are not setting `c_stdlib_version` yourself, this means " - "you are requesting a version below the current global baseline in " - "conda-forge (11.0). If this is the intention, you also need to " - "override `c_stdlib_version` and `MACOSX_DEPLOYMENT_TARGET` locally." - ) - if len(sdk) == len(merged_dt): - # if length matches, compare individually - for v_sdk, v_mdt in zip(sdk, merged_dt): - # versions with a single dot may have been read as floats - v_sdk, v_mdt = str(v_sdk), str(v_mdt) - if VersionOrder(v_sdk) < VersionOrder(v_mdt): - if sdk_lint not in lints: - lints.append(sdk_lint) - elif len(sdk) == 1: - # if length doesn't match, only warn if a single SDK version - # is lower than _all_ merged deployment targets - if all( - VersionOrder(str(sdk[0])) < VersionOrder(str(v_mdt)) for v_mdt in merged_dt - ): - if sdk_lint not in lints: - lints.append(sdk_lint) - def lint_recipe_is_parsable( recipe_text: str, diff --git a/news/osx_lint.rst b/news/osx_lint.rst new file mode 100644 index 000000000..0b121ef32 --- /dev/null +++ b/news/osx_lint.rst @@ -0,0 +1,28 @@ +**Added:** + +* + +**Changed:** + +* The long-deprecated `MACOSX_DEPLOYMENT_TARGET` will not be taken into account anymore when rerendering a recipe (#2473). +* The linter now raises an error if `MACOSX_DEPLOYMENT_TARGET` is found in recipe configuration files (#2473). +* The linter will now also raise issues found in `conda_build_config.yaml` for v1 recipes (#2473). +* The linter will now raise if two different recipe configuration files are found (#2473). +* Issues related to `c_stdlib_version` / `MACOSX_SDK_VERSION` and `MACOSX_DEPLOYMENT_TARGET` have been + moved to the conda-forge-specific part of the linter (c.f. `conda smithy lint --conda-forge`) (#2473). + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py index 388c7b032..dd635cbdd 100644 --- a/tests/test_lint_recipe.py +++ b/tests/test_lint_recipe.py @@ -1,5 +1,6 @@ #!/usr/bin/env python import os +import platform import shutil import subprocess import tempfile @@ -262,6 +263,7 @@ def test_recipe_v1_osx_noarch_hint(): assert not any(h.startswith(avoid_message) for h in hints) +@pytest.mark.parametrize("recipe_version", [0, 1]) @pytest.mark.parametrize( "std_selector", ["unix", "linux or (osx and x86_64)"], @@ -275,57 +277,42 @@ def test_recipe_v1_osx_noarch_hint(): ids=["False", "True", "mixed"], ) @pytest.mark.parametrize( - "macdt,v_std,sdk,exp_lint", + "v_std,sdk,exp_lint", [ - # matching -> no warning - (["10.9", "11.0"], ["10.9", "11.0"], None, None), - # mismatched length -> no warning (leave it to rerender) - (["10.9", "11.0"], ["10.9"], None, None), - # mismatch between stdlib and deployment target -> warn - (["10.9", "11.0"], ["10.13", "11.0"], None, "Conflicting spec"), - (["10.13", "11.0"], ["10.13", "12.3"], None, "Conflicting spec"), - # only deployment target -> warn - (["11.0", "12.0"], None, None, "In your conda_build_config.yaml"), # only stdlib -> no warning - (None, ["11.0", "11.0"], None, None), - (None, ["11.1"], None, None), + (["11.0", "11.0"], None, None), + (["11.1"], None, None), # only stdlib, but outdated -> warn - (None, ["10.9", "11.0"], None, "You are"), - (None, ["10.9"], None, "You are"), - # sdk below stdlib / deployment target -> warn - (["10.13", "11.0"], ["10.13", "11.0"], ["10.12"], "You are"), - (["10.13", "11.0"], ["10.13", "11.0"], ["10.12", "12.0"], "You are"), - # sdk above stdlib / deployment target -> no warning - (["10.13", "11.0"], ["10.13", "11.0"], ["12.0", "12.0"], None), - # only one sdk version, not universally below deployment target - # -> no warning (because we don't know enough to diagnose) - (["10.13", "11.0"], ["10.13", "11.0"], ["10.15"], None), - # mismatched version + wrong sdk; requires merge logic to work before - # checking sdk version; to avoid unnecessary complexity in the exp_hint - # handling below, repeat same test twice with different expected hints - (["10.9", "11.0"], ["10.13", "11.0"], ["10.12"], "Conflicting spec"), - (["10.9", "11.0"], ["10.13", "11.0"], ["10.12"], "You are"), + (["10.9", "11.0"], None, "You are"), + (["10.9"], None, "You are"), + # sdk below stdlib -> warn + (["11.0", "12.0"], ["10.12"], "You are"), + (["11.0", "12.0"], ["10.12", "12.0"], "You are"), + # sdk above stdlib -> no warning + (["11.0", "12.0"], ["12.0", "12.0"], None), # only sdk -> no warning - (None, None, ["11.0"], None), - (None, None, ["11.1", "12.0"], None), + (None, ["11.0"], None), + (None, ["11.1", "12.0"], None), # only sdk, but below global baseline -> warning - (None, None, ["10.12"], "You are"), - (None, None, ["10.12", "11.0"], "You are"), + (None, ["10.12"], "You are"), + (None, ["10.12", "11.0"], "You are"), ], ) def test_cbc_osx_lints( - std_selector, with_linux, reverse_arch, macdt, v_std, sdk, exp_lint + recipe_version, std_selector, with_linux, reverse_arch, v_std, sdk, exp_lint ): with tmp_directory() as rdir: - with open(os.path.join(rdir, "meta.yaml"), "w") as fh: - fh.write("package:\n name: foo") + rdir = Path(rdir) + if recipe_version == 1: + rdir.joinpath("recipe.yaml").write_text("package:\n name: foo") + rdir.joinpath("conda-forge.yml").write_text( + "conda_build_tool: rattler-build" + ) + else: + rdir.joinpath("meta.yaml").write_text("package:\n name: foo") + + # conda_build_config can be used with both v0 and v1 recipes with open(os.path.join(rdir, "conda_build_config.yaml"), "a") as fh: - if macdt is not None: - fh.write(f"""\ -MACOSX_DEPLOYMENT_TARGET: # [osx] - - {macdt[0]} # [osx and {"arm64" if reverse_arch[0] else "x86_64"}] - - {macdt[1]} # [osx and {"x86_64" if reverse_arch[0] else "arm64"}] -""") if v_std is not None or with_linux: arch1 = "arm64" if reverse_arch[1] else "x86_64" arch2 = "x86_64" if reverse_arch[1] else "arm64" @@ -352,7 +339,7 @@ def test_cbc_osx_lints( """ ) # run the linter - lints, _ = linter.main(rdir, return_hints=True) + lints = linter.main(rdir, conda_forge=True) # show CBC/hints for debugging with open(os.path.join(rdir, "conda_build_config.yaml")) as fh: print("".join(fh.readlines())) @@ -412,47 +399,31 @@ def test_license_file_empty(recipe_version: int): ids=["False", "True", "mixed"], ) @pytest.mark.parametrize( - "macdt,v_std,sdk,exp_lint", + "v_std,sdk,exp_lint", [ - # matching -> no warning - (["10.9", "11.0"], ["10.9", "11.0"], None, None), - # mismatched length -> no warning (leave it to rerender) - (["10.9", "11.0"], ["10.9"], None, None), - # mismatch between stdlib and deployment target -> warn - (["10.9", "11.0"], ["10.13", "11.0"], None, "Conflicting spec"), - (["10.13", "11.0"], ["10.13", "12.3"], None, "Conflicting spec"), - # only deployment target -> warn - (["11.0", "12.0"], None, None, "In your conda_build_config.yaml"), # only stdlib -> no warning - (None, ["11.0", "11.0"], None, None), - (None, ["11.1"], None, None), + (["11.0", "11.0"], None, None), + (["11.1"], None, None), # only stdlib, but outdated -> warn - (None, ["10.9", "11.0"], None, "You are"), - (None, ["10.9"], None, "You are"), - # sdk below stdlib / deployment target -> warn - (["10.13", "11.0"], ["10.13", "11.0"], ["10.12"], "You are"), - (["10.13", "11.0"], ["10.13", "11.0"], ["10.12", "12.0"], "You are"), - # sdk above stdlib / deployment target -> no warning - (["10.13", "11.0"], ["10.13", "11.0"], ["12.0", "12.0"], None), - # only one sdk version, not universally below deployment target - # -> no warning (because we don't know enough to diagnose) - (["10.13", "11.0"], ["10.13", "11.0"], ["10.15"], None), - # mismatched version + wrong sdk; requires merge logic to work before - # checking sdk version; to avoid unnecessary complexity in the exp_hint - # handling below, repeat same test twice with different expected hints - (["10.9", "11.0"], ["10.13", "11.0"], ["10.12"], "Conflicting spec"), - (["10.9", "11.0"], ["10.13", "11.0"], ["10.12"], "You are"), + (["10.9", "11.0"], None, "You are"), + (["10.9"], None, "You are"), + # sdk below stdlib -> warn + (["11.0", "12.0"], ["10.12"], "You are"), + (["11.0", "12.0"], ["10.12", "12.0"], "You are"), + # sdk above stdlib -> no warning + (["11.0", "12.0"], ["12.0", "12.0"], None), # only sdk -> no warning - (None, None, ["11.0"], None), - (None, None, ["11.1", "12.0"], None), + (None, ["11.0"], None), + (None, ["11.1", "12.0"], None), # only sdk, but below global baseline -> warning - (None, None, ["10.12"], "You are"), - (None, None, ["10.12", "11.0"], "You are"), + (None, ["10.12"], "You are"), + (None, ["10.12", "11.0"], "You are"), ], ) -def test_v1_cbc_osx_hints( - std_selector, with_linux, reverse_arch, macdt, v_std, sdk, exp_lint +def test_cbc_osx_lints_variants_yaml( + std_selector, with_linux, reverse_arch, v_std, sdk, exp_lint ): + # v1 recipes using conda_build_config.yaml are covered by test_cbc_osx_lints with tmp_directory() as recipe_dir: recipe_dir = Path(recipe_dir) recipe_dir.joinpath("recipe.yaml").write_text("package:\n name: foo") @@ -462,14 +433,6 @@ def test_v1_cbc_osx_hints( ) with open(recipe_dir / "variants.yaml", "a") as fh: - if macdt is not None: - fh.write(textwrap.dedent(f"""\ - MACOSX_DEPLOYMENT_TARGET: - - if: osx - then: - - {macdt[0]} - - {macdt[1]} - """)) if v_std is not None or with_linux: arch1 = "arm64" if reverse_arch[1] else "x86_64" arch2 = "x86_64" if reverse_arch[1] else "arm64" @@ -508,10 +471,10 @@ def test_v1_cbc_osx_hints( then: {sdk[0]} """)) # run the linter - lints, _ = linter.main(recipe_dir, return_hints=True, feedstock_dir=recipe_dir) + lints = linter.main(recipe_dir, conda_forge=True) # show CBC/hints for debugging lines = recipe_dir.joinpath("variants.yaml").read_text().splitlines() - print("".join(lines)) + print("\n".join(lines)) print(lints) # validate against expectations @@ -526,6 +489,57 @@ def test_v1_cbc_osx_hints( assert any(lint.startswith(exp_lint) for lint in lints) +@pytest.mark.parametrize("recipe_version", [0, 1]) +def test_duplicated_recipe_configs(recipe_version): + recipe_content = "package:\n name: foo" + config_content = "c_stdlib_version:\n - 2.17" + with tmp_directory() as recipe_dir: + recipe_dir = Path(recipe_dir) + if recipe_version == 1: + recipe_dir.joinpath("recipe.yaml").write_text(recipe_content) + recipe_dir.joinpath("conda-forge.yml").write_text( + "conda_build_tool: rattler-build" + ) + else: + recipe_dir.joinpath("meta.yaml").write_text(recipe_content) + + # write both config formats + recipe_dir.joinpath("conda_build_config.yaml").write_text(config_content) + recipe_dir.joinpath("variants.yaml").write_text(config_content) + + # run the linter + lints = linter.main(recipe_dir) + assert any(lint.startswith("Found two recipe config") for lint in lints) + + +@pytest.mark.parametrize( + "recipe_version,config_file", + [(0, "conda_build_config.yaml"), (0, "variants.yaml"), (1, "variants.yaml")], +) +def test_lint_macdt(recipe_version, config_file): + recipe_content = "package:\n name: foo" + with tmp_directory() as recipe_dir: + recipe_dir = Path(recipe_dir) + if recipe_version == 1: + recipe_dir.joinpath("recipe.yaml").write_text(recipe_content) + recipe_dir.joinpath("conda-forge.yml").write_text( + "conda_build_tool: rattler-build" + ) + else: + recipe_dir.joinpath("meta.yaml").write_text(recipe_content) + + if config_file == "conda_build_config.yaml": + payload = "MACOSX_DEPLOYMENT_TARGET:\n - 10.13 # [osx]" + else: + payload = "MACOSX_DEPLOYMENT_TARGET:\n - if: osx\n then: 10.13" + + recipe_dir.joinpath(config_file).write_text(payload) + + # run the linter + lints = linter.main(recipe_dir, conda_forge=True) + assert any(lint.startswith("The MACOSX_DEPLOYMENT_TARGET") for lint in lints) + + class TestLinter(unittest.TestCase): def test_bad_top_level(self): meta = OrderedDict([["package", {}], ["build", {}], ["sources", {}]]) @@ -1738,6 +1752,7 @@ def test_recipe_v1_recipe_name(self): lints, _ = linter.lintify_meta_yaml(meta, recipe_version=1) self.assertNotIn(expected_message, lints) + @pytest.mark.skipif(platform.system() == "Windows", reason="Line-ending confusion") def test_end_empty_line(self): bad_contents = [ # No empty lines at the end of the file