Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
56 changes: 48 additions & 8 deletions conda_lock/conda_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from types import TracebackType
from typing import Any, Optional, Union
from urllib.parse import urlsplit
import warnings

import click
import yaml
Expand Down Expand Up @@ -117,8 +118,10 @@
KIND_EXPLICIT: Literal["explicit"] = "explicit"
KIND_LOCK: Literal["lock"] = "lock"
KIND_ENV: Literal["env"] = "env"
TKindAll = Union[Literal["explicit"], Literal["lock"], Literal["env"]]
TKindRendarable = Union[Literal["explicit"], Literal["lock"], Literal["env"]]
KIND_RECIPE: Literal["recipe"] = "recipe"
KINDS = [KIND_EXPLICIT, KIND_LOCK, KIND_ENV, KIND_RECIPE]
TKindAll = Union[KIND_EXPLICIT, KIND_LOCK, KIND_ENV, KIND_RECIPE]
TKindRendarable = Union[KIND_EXPLICIT, KIND_LOCK, KIND_ENV, KIND_RECIPE]


DEFAULT_KINDS: list[Union[Literal["explicit"], Literal["lock"]]] = [
Expand All @@ -135,6 +138,7 @@
KIND_EXPLICIT: "conda create --name YOURENV --file {lockfile}",
KIND_ENV: "conda env create --name YOURENV --file {lockfile}",
KIND_LOCK: "conda-lock install --name YOURENV {lockfile}",
KIND_RECIPE: "rattler build {lockfile}",
}

_implicit_cuda_message = """
Expand Down Expand Up @@ -497,6 +501,7 @@ def make_lock_files( # noqa: C901
filename_template=filename_template,
extras=extras,
check_input_hash=check_input_hash,
output_recipe=lock_spec.output_recipe,
)


Expand All @@ -508,6 +513,7 @@ def do_render(
extras: Optional[Set[str]] = None,
check_input_hash: bool = False,
override_platform: Optional[Sequence[str]] = None,
output_recipe: Optional[dict] = None,
) -> None:
"""Render the lock content for each platform in lockfile

Expand All @@ -527,6 +533,8 @@ def do_render(
Do not re-render if specifications are unchanged
override_platform :
Generate only this subset of the platform files
output_recipe :
Additional content for rendering a rattler-build recipe
"""
platforms = lockfile.metadata.platforms
if override_platform is not None and len(override_platform) > 0:
Expand Down Expand Up @@ -583,11 +591,17 @@ def do_render(
extras=extras,
kind=kind,
platform=plat,
output_recipe=output_recipe,
)

filename += KIND_FILE_EXT[kind]
with open(filename, "w") as fo:
fo.write("\n".join(lockfile_contents) + "\n")
if kind == "recipe":
filename = f"{output_recipe['name']}-{output_recipe['version']}-{output_recipe['build']['string']}-{plat}.yaml"
with open(filename, "w") as fo:
fo.write("\n".join(lockfile_contents) + "\n")
else:
filename += KIND_FILE_EXT[kind]
with open(filename, "w") as fo:
fo.write("\n".join(lockfile_contents) + "\n")

print(
f" - Install lock using {'(see warning below)' if kind == 'env' else ''}:",
Expand Down Expand Up @@ -616,6 +630,7 @@ def render_lockfile_for_platform( # noqa: C901
kind: Union[Literal["env"], Literal["explicit"]],
platform: str,
suppress_warning_for_pip_and_explicit: bool = False,
output_recipe: Optional[dict] = None,
) -> list[str]:
"""
Render lock content into a single-platform lockfile that can be installed
Expand All @@ -636,6 +651,8 @@ def render_lockfile_for_platform( # noqa: C901
suppress_warning_for_pip_and_explicit :
When rendering internally for `conda-lock install`, we should suppress
the warning about pip dependencies not being supported by all tools.
output_recipe :
Additional content for rendering a rattler-build recipe
"""
lockfile_contents = [
"# Generated by conda-lock.",
Expand Down Expand Up @@ -757,6 +774,29 @@ def sanitize_lockfile_line(line: str) -> str:
"newer unified lockfile format (i.e. removing the --kind=explicit "
"argument."
)
elif kind == "recipe":
lockfile_contents = [
"package:",
f" name: {output_recipe['name']}",
f" version: {output_recipe['version']}",
f"build:",
f" string: {output_recipe['build']['string']}",
"requirements:",
" run_constraints:",
]
lockfile.alphasort_inplace()

excludes = output_recipe.get("exclude_patterns", [])
exclude_pattern = None
if excludes:
exclude_pattern = re.compile("|".join([f".*{e}.*" for e in excludes if e]))
for p in conda_deps:
# exclude virtual packages
if not p.name.startswith("__") and exclude_pattern and not exclude_pattern.match(p.name):
lockfile_contents.append(f" - {p.name} {p.version} {p.build}")

lockfile_contents.append("about:")
lockfile_contents.append(f" summary: A pinning package for a {output_recipe['name']} environment, to be used as a set of constraints at build time for other recipes.")
else:
raise ValueError(f"Unrecognised lock kind {kind}.")

Expand Down Expand Up @@ -1289,9 +1329,9 @@ def main() -> None:
"-k",
"--kind",
default=["lock"],
type=str,
type=click.Choice(KINDS),
multiple=True,
help="Kind of lock file(s) to generate [should be one of 'lock', 'explicit', or 'env'].",
help=f"Kind of lock file(s) to generate.",
)
@click.option(
"--filename-template",
Expand Down Expand Up @@ -1668,7 +1708,7 @@ def install(
"-k",
"--kind",
default=["explicit"],
type=click.Choice(["explicit", "env"]),
type=click.Choice(KINDS),
multiple=True,
help="Kind of lock file(s) to generate.",
)
Expand Down
2 changes: 1 addition & 1 deletion conda_lock/conda_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def solve_conda(
Channels to query

"""

conda_specs = [
_to_match_spec(dep.name, dep.version, dep.build, dep.conda_channel)
for dep in specs.values()
Expand Down Expand Up @@ -140,6 +139,7 @@ def solve_conda(
locked_dependency = LockedDependency(
name=action["name"],
version=action["version"],
build=action["build"],
manager="conda",
platform=platform,
dependencies=dependencies,
Expand Down
4 changes: 4 additions & 0 deletions conda_lock/lockfile/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ def to_fetch_action(self) -> FetchAction:
f"Expected '{self.platform}' but URL '{self.url}' contains '{parsed_platform}'."
)
filename_with_extension = path.name # e.g. "tzdata-2022g-h191b570_0.conda"
build = self.build or filename_with_extension.split("-")[-1].split(".")[0]
if not self.build:
self.build = build

# base_url is everything up to the platform directory
base_url_path = str(path.parent.parent) # e.g. "/conda-forge"
Expand All @@ -135,6 +138,7 @@ def to_fetch_action(self) -> FetchAction:
version=self.version,
channel=channel_url,
url=self.url,
build=build,
fn=filename_with_extension,
md5=self.hash.md5,
sha256=self.hash.sha256,
Expand Down
1 change: 1 addition & 0 deletions conda_lock/models/dry_run_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FetchAction(TypedDict):
timestamp: int
url: str
version: str
build: Optional[str]


class LinkAction(TypedDict):
Expand Down
1 change: 1 addition & 0 deletions conda_lock/models/lock_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class LockSpecification(BaseModel):
sources: list[pathlib.Path]
pip_repositories: list[PipRepository] = Field(default_factory=list)
allow_pypi_requests: bool = True
output_recipe: dict = {}

@property
def platforms(self) -> list[str]:
Expand Down
1 change: 1 addition & 0 deletions conda_lock/src_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,5 @@ def dep_has_category(d: Dependency, categories: Set[str]) -> bool:
pip_repositories=pip_repositories,
sources=aggregated_lock_spec.sources,
allow_pypi_requests=aggregated_lock_spec.allow_pypi_requests,
output_recipe=aggregated_lock_spec.output_recipe,
)
20 changes: 20 additions & 0 deletions conda_lock/src_parser/aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ def aggregate_lock_specs(
)
except ValueError as e:
raise ChannelAggregationError(*e.args)
try:
output_recipe = unify_output_recipe(
[lock_spec.output_recipe for lock_spec in lock_specs]
)
except ValueError as e:
raise ChannelAggregationError(*e.args)

return LockSpecification(
dependencies=dependencies,
Expand All @@ -64,6 +70,7 @@ def aggregate_lock_specs(
allow_pypi_requests=all(
lock_spec.allow_pypi_requests for lock_spec in lock_specs
),
output_recipe=output_recipe,
)


Expand Down Expand Up @@ -107,3 +114,16 @@ def unify_package_sources(
f"{collection} is not an ordered subset at the end of {result}"
)
return result

def unify_output_recipe(
collections: list[dict],
) -> dict:
"""Unify the output recipe from multiple lock specs.

The output recipe must be the same for all lock specs.
"""
if not collections:
return {}
if not all(c == collections[0] for c in collections):
raise ValueError("Output recipe must be the same for all lock specs.")
return collections[0]
2 changes: 2 additions & 0 deletions conda_lock/src_parser/environment_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def parse_environment_file(

# These extension fields are nonstandard
category: str = env_yaml_data.get("category") or "main"
output_recipe: dict = env_yaml_data.get("output_recipe", {})

# Parse with selectors for each target platform
dep_map = {
Expand All @@ -148,4 +149,5 @@ def parse_environment_file(
channels=channels, # type: ignore
pip_repositories=pip_repositories, # type: ignore
sources=[environment_file],
output_recipe=output_recipe,
)
Loading