Skip to content

Commit

Permalink
Merge pull request #1668 from h-vetinari/boost
Browse files Browse the repository at this point in the history
Piggyback migrator for boost unification
  • Loading branch information
h-vetinari authored Sep 28, 2023
2 parents c099e6c + 3d81ed1 commit b54a19f
Show file tree
Hide file tree
Showing 18 changed files with 2,500 additions and 0 deletions.
3 changes: 3 additions & 0 deletions conda_forge_tick/auto_tick.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
DependencyUpdateMigrator,
QtQtMainMigrator,
JpegTurboMigrator,
LibboostMigrator,
)

from conda_forge_feedstock_check_solvable import is_recipe_solvable
Expand Down Expand Up @@ -677,6 +678,8 @@ def add_rebuild_migration_yaml(
piggy_back_migrations.append(QtQtMainMigrator())
if migration_name == "jpeg_to_libjpeg_turbo":
piggy_back_migrations.append(JpegTurboMigrator())
if migration_name == "boost_cpp_to_libboost":
piggy_back_migrations.append(LibboostMigrator())
cycles = list(nx.simple_cycles(total_graph))
migrator = MigrationYaml(
migration_yaml,
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 @@ -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!")
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}!")
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

0 comments on commit b54a19f

Please sign in to comment.