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
6 changes: 6 additions & 0 deletions conda_lock/conda_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def solve_conda(
planned = {}
for action in dry_run_install["actions"]["FETCH"]:
dependencies = {}
if action.get("depends") is None:
raise ValueError(f"No depends found for FETCH action {action}")
for dep in action.get("depends") or []:
matchspec = MatchSpec(dep) # pyright: ignore[reportArgumentType]
name = matchspec.name
Expand Down Expand Up @@ -259,6 +261,10 @@ def _reconstruct_fetch_actions(
else:
raise ValueError(f"Unable to extract the dist_name from {link_action}.")
repodata = _get_repodata_record(pkgs_dirs, dist_name)
if link_pkg_name == "pyzmq":
print(
f"In _reconstruct_fetch_actions for {link_pkg_name}, repodata: {repodata}"
)
if repodata is None:
raise FileNotFoundError(
f"Distribution '{dist_name}' not found in pkgs_dirs {pkgs_dirs}"
Expand Down
73 changes: 72 additions & 1 deletion conda_lock/lockfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,64 @@ class UnknownLockfileVersion(ValueError):
pass


class InconsistentCondaDependencies(ValueError):
"""Raised when conda dependencies in the lockfile are inconsistent."""

pass


def _verify_no_missing_conda_packages(content: Lockfile) -> None:
"""
Ensure all subdependencies of conda packages are also present as conda packages.

This does not check version constraints.

Raises:
InconsistentCondaDependencies: If any conda dependency is missing
"""
print("------------------VERIFYING CONDA DEPENDENCY CONSISTENCY------------------")
# Build a mapping of (name, platform) -> LockedDependency for conda packages
conda_packages: dict[tuple[str, str], LockedDependency] = {}
for package in content.package:
if package.manager == "conda":
conda_packages[(package.name, package.platform)] = package

# Iterate through the mapping while checking for missing dependencies
missing_dependencies: set[tuple[str, str]] = set()
for (_primary_dep_name, platform), dependency in conda_packages.items():
subdependencies = dependency.dependencies
satisfied_deps = []
for subdep_name in subdependencies:
if subdep_name.startswith("__"):
# Virtual packages like __linux are not real packages so not present
continue
if (subdep_name, platform) not in conda_packages:
missing_dependencies.add((subdep_name, platform))
else:
satisfied_deps.append(subdep_name)
if len(satisfied_deps) > 0:
print(f"Satisfied dependencies for {_primary_dep_name}: {satisfied_deps}")

if missing_dependencies:
error_msg = (
"Conda dependency consistency check failed. The following conda "
"subdependencies are missing from the lockfile:\n\n"
)
for current_platform in content.metadata.platforms:
missing_on_platform = [
(name, dep_platform)
for name, dep_platform in missing_dependencies
if dep_platform == current_platform
]
if missing_on_platform:
error_msg += f" {current_platform}:\n"
for subdep_name, _subdep_platform in sorted(missing_on_platform):
error_msg += f" - {subdep_name}\n"
error_msg += "\n\nThis indicates that the conda dependency graph is incomplete."
raise InconsistentCondaDependencies(error_msg)
print("------------------CONDA DEPENDENCY CONSISTENCY VERIFIED-------------------")


def _seperator_munge_get(
d: Mapping[str, Union[list[LockedDependency], LockedDependency]], key: str
) -> Union[list[LockedDependency], LockedDependency]:
Expand Down Expand Up @@ -182,6 +240,8 @@ def parse_conda_lock_file(path: pathlib.Path) -> Lockfile:
else:
raise UnknownLockfileVersion(f"{path} has unknown version {version}")
lockfile.toposort_inplace()
for p in lockfile.package:
assert len(p.categories) > 0, f"Package {p.name} has no categories"
return lockfile


Expand All @@ -193,6 +253,10 @@ def write_conda_lock_file(
) -> None:
content.alphasort_inplace()
content.filter_virtual_packages_inplace()

# Validate conda dependency consistency before writing
_verify_no_missing_conda_packages(content)

with path.open("w") as f:
if include_help_text:
categories: set[str] = {
Expand Down Expand Up @@ -247,5 +311,12 @@ def write_section(text: str) -> None:
conda-lock {metadata_flags}{" ".join("-f " + path for path in content.metadata.sources)} --lockfile {path.name}
"""
)
output = content.to_v1().dict_for_output()
pathlib.Path("outputv2.json").write_text(content.model_dump_json(indent=2))
content_v1 = content.to_v1()
pathlib.Path("outputv1.json").write_text(content_v1.model_dump_json(indent=2))
output = content_v1.dict_for_output()
yaml.dump(output, stream=f, sort_keys=False)

# Verify round-trip consistency by reading back the lockfile and checking again
parsed_lockfile = parse_conda_lock_file(path)
_verify_no_missing_conda_packages(parsed_lockfile)
6 changes: 5 additions & 1 deletion conda_lock/lockfile/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ def to_fetch_action(self) -> FetchAction:

channel_url = f"{base_url}/{self.platform}" # e.g. "https://user:pass@conda.anaconda.org/conda-forge/linux-64"

depends = [f"{k} {v}".strip() for k, v in self.dependencies.items()]
if self.name == "pyzmq":
print(f"In to_fetch_action for {self.name}, depends: {depends}")

fetch_action = FetchAction(
name=self.name,
version=self.version,
Expand All @@ -138,7 +142,7 @@ def to_fetch_action(self) -> FetchAction:
fn=filename_with_extension,
md5=self.hash.md5,
sha256=self.hash.sha256,
depends=[f"{k} {v}".strip() for k, v in self.dependencies.items()],
depends=depends,
constrains=[],
subdir=self.platform,
timestamp=0,
Expand Down
3 changes: 3 additions & 0 deletions conda_lock/lockfile/v2prelim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ def to_v1(self) -> list[LockedDependencyV1]:
can only contain a single category, we represent multiple categories as a list
of v1 dependencies that are identical except for the `category` field. The
`category` field runs over all categories."""
if len(self.categories) == 0:
print(f"In to_v1, no categories for {self.name}, adding to main")
self.categories = {"main"}
package_entries_per_category = [
LockedDependencyV1(
name=self.name,
Expand Down
Loading