Skip to content

Commit

Permalink
REF refactor tests to use rattler, add rattler solver
Browse files Browse the repository at this point in the history
  • Loading branch information
beckermr committed Jun 2, 2024
1 parent 407d737 commit af9c124
Show file tree
Hide file tree
Showing 5 changed files with 467 additions and 248 deletions.
32 changes: 25 additions & 7 deletions conda_forge_feedstock_check_solvable/check_solvable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import psutil
from ruamel.yaml import YAML

import conda_forge_feedstock_check_solvable.utils
from conda_forge_feedstock_check_solvable.mamba_solver import mamba_solver_factory
from conda_forge_feedstock_check_solvable.rattler_solver import rattler_solver_factory
from conda_forge_feedstock_check_solvable.utils import (
MAX_GLIBC_MINOR,
apply_pins,
Expand All @@ -22,13 +24,14 @@
)


def _func(feedstock_dir, additional_channels, build_platform, verbosity, conn):
def _func(feedstock_dir, additional_channels, build_platform, verbosity, solver, conn):
try:
res = _is_recipe_solvable(
feedstock_dir,
additional_channels=additional_channels,
build_platform=build_platform,
verbosity=verbosity,
solver=solver,
)
conn.send(res)
except Exception as e:
Expand All @@ -43,6 +46,7 @@ def is_recipe_solvable(
timeout=600,
build_platform=None,
verbosity=1,
solver="mamba",
) -> Tuple[bool, List[str], Dict[str, bool]]:
"""Compute if a recipe is solvable.
Expand All @@ -64,14 +68,16 @@ def is_recipe_solvable(
verbosity : int
An int indicating the level of verbosity from 0 (no output) to 3
(gobbs of output).
solver : str
The solver to use. One of `mamba` or `rattler`.
Returns
-------
solvable : bool
The logical AND of the solvability of the recipe on all platforms
in the CI scripts.
errors : list of str
A list of errors from mamba. Empty if recipe is solvable.
A list of errors from the solver. Empty if recipe is solvable.
solvable_by_variant : dict
A lookup by variant config that shows if a particular config is solvable
"""
Expand All @@ -86,6 +92,7 @@ def is_recipe_solvable(
additional_channels,
build_platform,
verbosity,
solver,
child_conn,
),
)
Expand Down Expand Up @@ -121,6 +128,7 @@ def is_recipe_solvable(
additional_channels=additional_channels,
build_platform=build_platform,
verbosity=verbosity,
solver=solver,
)

return res
Expand All @@ -131,9 +139,9 @@ def _is_recipe_solvable(
additional_channels=(),
build_platform=None,
verbosity=1,
solver="mamba",
) -> Tuple[bool, List[str], Dict[str, bool]]:
global VERBOSITY
VERBOSITY = verbosity
conda_forge_feedstock_check_solvable.utils.VERBOSITY = verbosity

build_platform = build_platform or {}

Expand Down Expand Up @@ -187,6 +195,7 @@ def _is_recipe_solvable(
build_platform.get(f"{platform}_{arch}", f"{platform}_{arch}")
),
additional_channels=additional_channels,
solver_backend=solver,
)
solvable = solvable and _solvable
cbc_name = os.path.basename(cbc_fname).rsplit(".", maxsplit=1)[0]
Expand All @@ -205,6 +214,7 @@ def _is_recipe_solvable_on_platform(
arch,
build_platform_arch=None,
additional_channels=(),
solver_backend="mamba",
):
# parse the channel sources from the CBC
parser = YAML(typ="jinja2")
Expand Down Expand Up @@ -281,13 +291,21 @@ def _is_recipe_solvable_on_platform(

# now we loop through each one and check if we can solve it
# we check run and host and ignore the rest
print_debug("getting mamba solver")
print_debug("getting solver")
with suppress_output():
solver = mamba_solver_factory(tuple(channel_sources), f"{platform}-{arch}")
build_solver = mamba_solver_factory(
if solver_backend == "rattler":
solver_factory = rattler_solver_factory
elif solver_backend == "mamba":
solver_factory = mamba_solver_factory
else:
raise ValueError(f"Unknown solver backend {solver_backend}")

solver = solver_factory(tuple(channel_sources), f"{platform}-{arch}")
build_solver = solver_factory(
tuple(channel_sources),
f"{build_platform}-{build_arch}",
)

solvable = True
errors = []
outnames = [m.name() for m, _, _ in metas]
Expand Down
127 changes: 127 additions & 0 deletions conda_forge_feedstock_check_solvable/rattler_solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import asyncio
import copy
import os
import pprint
from typing import List

import cachetools.func
from rattler import Channel, MatchSpec, Platform, RepoDataRecord, solve

from conda_forge_feedstock_check_solvable.utils import (
DEFAULT_RUN_EXPORTS,
get_run_exports,
print_debug,
print_warning,
)


class RattlerSolver:
def __init__(self, channels, platform_arch) -> None:
_channels = []
for c in channels:
if c == "defaults":
_channels.append("https://repo.anaconda.com/pkgs/main")
_channels.append("https://repo.anaconda.com/pkgs/r")
_channels.append("https://repo.anaconda.com/pkgs/msys2")
else:
_channels.append(c)
self.channels = [Channel(c) for c in _channels]
self.platform_arch = platform_arch
self.platforms = [Platform(self.platform_arch), Platform("noarch")]

def solve(
self,
specs: List[str],
get_run_exports: bool = False,
ignore_run_exports_from: List[str] = None,
ignore_run_exports: List[str] = None,
constraints=None,
):
ignore_run_exports_from = ignore_run_exports_from or []
ignore_run_exports = ignore_run_exports or []
success = False
err = None
run_exports = copy.deepcopy(DEFAULT_RUN_EXPORTS)

try:
_specs = [MatchSpec(s) for s in specs]

print_debug(
"RATTLER running solver for specs \n\n%s\n", pprint.pformat(_specs)
)

solution = asyncio.run(
solve(
channels=self.channels,
specs=_specs,
platforms=self.platforms,
# virtual_packages=self.virtual_packages,
)
)
success = True
str_solution = [
f"{record.name.normalized} {record.version} {record.build}"
for record in solution
]

if get_run_exports:
run_exports = self._get_run_exports(
solution,
_specs,
[MatchSpec(igrf) for igrf in ignore_run_exports_from],
[MatchSpec(igr) for igr in ignore_run_exports],
)

except Exception as e:
err = str(e)
print_warning(
"RATTLER failed to solve specs \n\n%s\n\nfor channels "
"\n\n%s\n\nThe reported errors are:\n\n%s\n",
pprint.pformat(_specs),
pprint.pformat(self.channels),
err,
)
success = False
run_exports = copy.deepcopy(DEFAULT_RUN_EXPORTS)
str_solution = None

if get_run_exports:
return success, err, str_solution, run_exports
else:
return success, err, str_solution

def _get_run_exports(
self,
repodata_records: List[RepoDataRecord],
_specs: List[MatchSpec],
ignore_run_exports_from: List[MatchSpec],
ignore_run_exports: List[MatchSpec],
):
"""Given a set of repodata records, produce a
dict with the weak and strong run exports for the packages.
We only look up export data for things explicitly listed in the original
specs.
"""
names = {s.name for s in _specs}
ign_rex_from = {s.name for s in ignore_run_exports_from}
ign_rex = {s.name for s in ignore_run_exports}
run_exports = copy.deepcopy(DEFAULT_RUN_EXPORTS)
for record in repodata_records:
lt_name = record.name
if lt_name in names and lt_name not in ign_rex_from:
channel_url = record.channel
subdir = record.subdir
file_name = record.file_name
rx = get_run_exports(os.path.join(channel_url, subdir), file_name)
for key in rx:
rx[key] = {v for v in rx[key] if v not in ign_rex}
for key in DEFAULT_RUN_EXPORTS:
run_exports[key] |= rx[key]

return run_exports


@cachetools.func.ttl_cache(maxsize=8, ttl=60)
def rattler_solver_factory(channels, platform):
return RattlerSolver(list(channels), platform)
13 changes: 12 additions & 1 deletion conda_forge_feedstock_check_solvable/virtual_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,23 @@ def add_package(self, package: FakePackage, subdirs: Iterable[str] = ()):

def _write_subdir(self, subdir):
packages = {}
out = {"info": {"subdir": subdir}, "packages": packages}
out = {
"info": {"subdir": subdir},
"packages": packages,
"paxkages.conda": {},
"removed": [],
"repodata_version": 1,
}
for pkg, subdirs in self.packages_by_subdir.items():
if subdir not in subdirs:
continue
fname, info_dict = pkg.to_repodata_entry()
info_dict["subdir"] = subdir
if subdir == "noarch":
info_dict["noarch"] = "generic"
else:
if "noarch" in info_dict:
del info_dict["noarch"]
packages[fname] = info_dict

(self.base_path / subdir).mkdir(exist_ok=True)
Expand Down
Loading

0 comments on commit af9c124

Please sign in to comment.