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

Piggyback migrator for boost unification #1668

Merged
merged 8 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conda_forge_tick/migrators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .mpi_pin_run_as_build import MPIPinRunAsBuildCleanup
from .qt_to_qt_main import QtQtMainMigrator
from .jpegturbo import JpegTurboMigrator
from .libboost import LibboostMigrator
from .migration_yaml import MigrationYaml, MigrationYamlCreator, merge_migrator_cbc
from .arch import ArchRebuild, OSXArm
from .pip_check import PipCheckMigrator
Expand Down
163 changes: 163 additions & 0 deletions conda_forge_tick/migrators/libboost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from conda_forge_tick.migrators.core import MiniMigrator
import os
import re


def _slice_into_output_sections(meta_yaml_lines, attrs):
"""
Turn a recipe into slices corresponding to the outputs.

To correctly process requirement sections from either
single or multi-output recipes, we need to be able to
restrict which lines we're operating on.

Takes a list of lines and returns a dict from each output name to
the list of lines where this output is described in the meta.yaml.
The result will always contain a "global" section (== everything
if there are no other outputs).
"""
outputs = attrs["meta_yaml"].get("outputs", [])
output_names = [o["name"] for o in outputs]
# if there are no outputs, there's only one section
if not output_names:
return {"global": meta_yaml_lines}
# output_names may contain duplicates; remove them but keep order
names = []
[names := names + [x] for x in output_names if x not in names]
num_outputs = len(names)
# add dummy for later use & reverse list for easier pop()ing
names += ["dummy"]
names.reverse()
# initialize
pos, prev, seek = 0, "global", names.pop()
sections = {}
for i, line in enumerate(meta_yaml_lines):
# assumes order of names matches their appearance in meta_yaml,
# and that they appear literally (i.e. no {{...}}) and without quotes
if f"- name: {seek}" in line:
# found the beginning of the next output;
# everything until here belongs to the previous one
sections[prev] = meta_yaml_lines[pos:i]
# update
pos, prev, seek = i, seek, names.pop()
if seek == "dummy":
# reached the last output; it goes until the end of the file
sections[prev] = meta_yaml_lines[pos:]
if len(sections) != num_outputs + 1:
raise RuntimeError("Could not find all output sections in meta.yaml!")

Check warning on line 47 in conda_forge_tick/migrators/libboost.py

View check run for this annotation

Codecov / codecov/patch

conda_forge_tick/migrators/libboost.py#L47

Added line #L47 was not covered by tests
return sections


def _process_section(name, attrs, lines):
"""
Migrate requirements per section.

We want to migrate as follows:
- rename boost to libboost-python
- if boost-cpp is only a host-dep, rename to libboost-headers
- if boost-cpp is _also_ a run-dep, rename it to libboost in host
and remove it in run.
"""
outputs = attrs["meta_yaml"].get("outputs", [])
if name == "global":
reqs = attrs["meta_yaml"].get("requirements", {})
else:
filtered = [o for o in outputs if o["name"] == name]
if len(filtered) == 0:
raise RuntimeError(f"Could not find output {name}!")

Check warning on line 67 in conda_forge_tick/migrators/libboost.py

View check run for this annotation

Codecov / codecov/patch

conda_forge_tick/migrators/libboost.py#L67

Added line #L67 was not covered by tests
reqs = filtered[0].get("requirements", {})

build_req = reqs.get("build", set()) or set()
host_req = reqs.get("host", set()) or set()
run_req = reqs.get("run", set()) or set()

is_boost_in_build = "boost-cpp" in build_req
is_boost_in_host = "boost-cpp" in host_req
is_boost_in_run = "boost-cpp" in run_req

# anything behind a comment needs to get replaced first, so it
# doesn't mess up the counts below
lines = _replacer(
lines,
r"^(?P<before>\s*\#.*)\b(boost-cpp)\b(?P<after>.*)$",
r"\g<before>libboost-devel\g<after>",
)

# boost-cpp, followed optionally by e.g. " =1.72.0" or " {{ boost_cpp }}"
p_base = r"boost-cpp(\s*[<>=]?=?[\d\.]+)?(\s+\{\{.*\}\})?"
p_selector = r"(\s+\#\s\[.*\])?"
if is_boost_in_build:
# if boost also occurs in build (assuming only once), replace it once
# but keep selectors (e.g. `# [build_platform != target_platform]`)
lines = _replacer(lines, p_base, "libboost-devel", max_times=1)

if is_boost_in_host and is_boost_in_run:
# presence in both means we want to replace with libboost, but only in host;
# because libboost-devel only exists from newest 1.82, we remove version pins;
# generally we assume there's one occurrence in host and on in run, but due
# to selectors, this may not be the case; to keep the logic tractable, we
# remove all occurrences but the first (and thus need to remove selectors too)
lines = _replacer(lines, p_base + p_selector, "libboost-devel", max_times=1)
# delete all other occurrences
lines = _replacer(lines, "boost-cpp", "")
elif is_boost_in_host and name == "global" and outputs:
# global build section for multi-output with no run-requirements;
# safer to use the full library here
lines = _replacer(lines, p_base + p_selector, "libboost-devel", max_times=1)
# delete all other occurrences
lines = _replacer(lines, "boost-cpp", "")
elif is_boost_in_host:
# here we know we can replace all with libboost-headers
lines = _replacer(lines, p_base, "libboost-headers")
elif is_boost_in_run and outputs:
# case of multi-output but with the host deps being only in
# global section; remove run-deps of boost-cpp nevertheless
lines = _replacer(lines, "boost-cpp", "")
# in any case, replace occurrences of "- boost"
lines = _replacer(lines, "- boost", "- libboost-python-devel")
lines = _replacer(lines, r"pin_compatible\([\"\']boost", "")
return lines


def _replacer(lines, from_this, to_that, max_times=None):
"""
Replaces one pattern with a string in a set of lines, up to max_times
"""
i = 0
new_lines = []
pat = re.compile(from_this)
for line in lines:
if pat.search(line) and (max_times is None or i < max_times):
i += 1
# if to_that is empty, discard line
if not to_that:
continue
line = pat.sub(to_that, line)
new_lines.append(line)
return new_lines


class LibboostMigrator(MiniMigrator):
def filter(self, attrs, not_bad_str_start=""):
host_req = (attrs.get("requirements", {}) or {}).get("host", set()) or set()
run_req = (attrs.get("requirements", {}) or {}).get("run", set()) or set()
all_req = set(host_req) | set(run_req)
# filter() returns True if we _don't_ want to migrate
return not bool({"boost", "boost-cpp"} & all_req)

def migrate(self, recipe_dir, attrs, **kwargs):
outputs = attrs["meta_yaml"].get("outputs", [])

fname = os.path.join(recipe_dir, "meta.yaml")
if os.path.exists(fname):
with open(fname) as fp:
lines = fp.readlines()

new_lines = []
sections = _slice_into_output_sections(lines, attrs)
for name, section in sections.items():
# _process_section returns list of lines already
new_lines += _process_section(name, attrs, section)

with open(fname, "w") as fp:
fp.write("".join(new_lines))
61 changes: 61 additions & 0 deletions tests/test_libboost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import pytest
from flaky import flaky

from conda_forge_tick.migrators import LibboostMigrator, Version
from test_migrators import run_test_migration


TEST_YAML_PATH = os.path.join(os.path.dirname(__file__), "test_yaml")


LIBBOOST = LibboostMigrator()
VERSION_WITH_LIBBOOST = Version(
set(),
piggy_back_migrations=[LIBBOOST],
)


@pytest.mark.parametrize(
"feedstock,new_ver",
[
# single output; no run-dep
("gudhi", "1.10.0"),
# single output; with run-dep
("carve", "1.10.0"),
# multiple outputs, many don't depend on boost; comment trickiness
("fenics", "1.10.0"),
# multiple outputs, jinja-style pinning
("poppler", "1.10.0"),
# multiple outputs, complicated selector & pinning combinations
("scipopt", "1.10.0"),
# testing boost -> libboost-python
("rdkit", "1.10.0"),
# interaction between boost & boost-cpp;
# multiple outputs but no host deps
("cctx", "1.10.0"),
],
)
def test_boost(feedstock, new_ver, tmpdir):
before = f"libboost_{feedstock}_before_meta.yaml"
with open(os.path.join(TEST_YAML_PATH, before)) as fp:
in_yaml = fp.read()

after = f"libboost_{feedstock}_after_meta.yaml"
with open(os.path.join(TEST_YAML_PATH, after)) as fp:
out_yaml = fp.read()

run_test_migration(
m=VERSION_WITH_LIBBOOST,
inp=in_yaml,
output=out_yaml,
kwargs={"new_version": new_ver},
prb="Dependencies have been updated if changed",
mr_out={
"migrator_name": "Version",
"migrator_version": VERSION_WITH_LIBBOOST.migrator_version,
"version": new_ver,
},
tmpdir=tmpdir,
should_filter=False,
)
55 changes: 55 additions & 0 deletions tests/test_yaml/libboost_carve_after_meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% set name = "carve" %}
{% set version = "1.10.0" %}

package:
name: {{ name|lower }}
version: {{ version }}

source:
# fake source url to get version migrator to pass
url: https://github.com/scipy/scipy/archive/refs/tags/v{{ version }}.tar.gz
sha256: 3f9e587a96844a9b4ee7f998cfe4dc3964dc95c4ca94d7de6a77bffb99f873da
# url: https://github.com/ngodber/carve/archive/v{{ version }}.tar.gz
# sha256: 20481918af488fc92694bf1d5bdd6351ad73a0b64fbe4373e1f829a7b0eeff63

build:
number: 0
run_exports:
- {{ pin_subpackage("carve", max_pin="x.x") }}

requirements:
build:
- cmake
- make # [unix]
- {{ compiler('c') }}
- {{ compiler('cxx') }}
host:
- libboost-devel
run:

test:
commands:
- test -f ${PREFIX}/bin/slice # [unix]
- test -f ${PREFIX}/bin/intersect # [unix]
- test -f ${PREFIX}/bin/triangulate # [unix]
- test -f ${PREFIX}/bin/convert # [unix]
- test -f ${PREFIX}/lib/libcarve${SHLIB_EXT} # [unix]
- if not exist %LIBRARY_PREFIX%\bin\carve.dll exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\slice.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\intersect.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\triangulate.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\convert.exe exit 1 # [win]

about:
home: https://github.com/PyMesh/carve
license: GPL-2.0-or-later
license_family: GPL
license_file: LICENSE
summary: Carve computes boolean operations between sets of arbitrary closed and open surfaces
description: |
Carve computes boolean operations between sets of arbitrary closed and open surfaces faster, more robustly and with fewer restrictions than comparable software.
dev_url: https://github.com/PyMesh/carve

extra:
recipe-maintainers:
- ngodber
56 changes: 56 additions & 0 deletions tests/test_yaml/libboost_carve_before_meta.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{% set name = "carve" %}
{% set version = "1.9.0" %}

package:
name: {{ name|lower }}
version: {{ version }}

source:
# fake source url to get version migrator to pass
url: https://github.com/scipy/scipy/archive/refs/tags/v{{ version }}.tar.gz
sha256: b6d893dc7dcd4138b9e9df59a13c59695e50e80dc5c2cacee0674670693951a1
# url: https://github.com/ngodber/carve/archive/v{{ version }}.tar.gz
# sha256: 20481918af488fc92694bf1d5bdd6351ad73a0b64fbe4373e1f829a7b0eeff63

build:
number: 1
run_exports:
- {{ pin_subpackage("carve", max_pin="x.x") }}

requirements:
build:
- cmake
- make # [unix]
- {{ compiler('c') }}
- {{ compiler('cxx') }}
host:
- boost-cpp
run:
- boost-cpp

test:
commands:
- test -f ${PREFIX}/bin/slice # [unix]
- test -f ${PREFIX}/bin/intersect # [unix]
- test -f ${PREFIX}/bin/triangulate # [unix]
- test -f ${PREFIX}/bin/convert # [unix]
- test -f ${PREFIX}/lib/libcarve${SHLIB_EXT} # [unix]
- if not exist %LIBRARY_PREFIX%\bin\carve.dll exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\slice.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\intersect.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\triangulate.exe exit 1 # [win]
- if not exist %LIBRARY_PREFIX%\bin\convert.exe exit 1 # [win]

about:
home: https://github.com/PyMesh/carve
license: GPL-2.0-or-later
license_family: GPL
license_file: LICENSE
summary: Carve computes boolean operations between sets of arbitrary closed and open surfaces
description: |
Carve computes boolean operations between sets of arbitrary closed and open surfaces faster, more robustly and with fewer restrictions than comparable software.
dev_url: https://github.com/PyMesh/carve

extra:
recipe-maintainers:
- ngodber
Loading
Loading