diff --git a/scripts/zwik_client.py b/scripts/zwik_client.py index 27cb5b7..54888de 100644 --- a/scripts/zwik_client.py +++ b/scripts/zwik_client.py @@ -28,7 +28,7 @@ from logging.handlers import RotatingFileHandler from typing import Optional -__version__ = "5.16" +__version__ = "5.17" min_supported_conda_version = "4.5.4" max_supported_conda_version = "24.3.0" min_supported_bootstrap_version = 7 @@ -820,16 +820,58 @@ def conda_envs_dir(self): @property def yaml_hash(self): + from conda import CondaError + if not self._yaml_hash: import hashlib hash_md5 = hashlib.md5() - with open(self.yaml_path, "r") as f: - for line in f.readlines(): - hash_md5.update(line.encode("utf-8")) + + if self.env_data: + if "channels" in self.env_data: + for channel in self.env_data["channels"]: + hash_md5.update(str(channel).encode("utf-8")) + + if "dependencies" in self.env_data: + env_deps = self.get_dependencies(additional_dependencies=[]) + + # Convert the MatchSpec objects to strings and sort them. + # This allows the user to reorder the yaml file and not + # trigger regeneration + deps_to_hash = sorted([str(spec) for spec in env_deps.specs]) + + for dep in deps_to_hash: + hash_md5.update(dep.encode("utf-8")) + else: + raise CondaError( + "env_data could not be loaded, " + "check if zwik_environment.yaml is defined." + ) + self._yaml_hash = hash_md5.hexdigest() + return self._yaml_hash + def get_legacy_yaml_hash(self): + import hashlib + + hash_md5 = hashlib.md5() + with open(self.yaml_path, "r") as f: + in_allow_unsafe_block = False + for line in f.readlines(): + if line.startswith("allow_unsafe:"): + in_allow_unsafe_block = True + continue + + elif in_allow_unsafe_block and re.match(r"^[^\s#]", line): + # Reached a new root-level key, stop skipping + in_allow_unsafe_block = False + + if not in_allow_unsafe_block: + hash_md5.update(line.encode("utf-8")) + + return hash_md5.hexdigest() + @property def env_name(self): return self.lockfile_hash @@ -893,9 +935,24 @@ def read_version_lock(self): raise LockfileError("lock file seems corrupt") with open(path, "r") as fp: data = yaml.load(fp) + if "yaml_hash" not in data: raise LockfileError("lock file seems incomplete") + + # 1. Check against the modern hash first + hash_matches = False if data["yaml_hash"] == self.yaml_hash: + hash_matches = True + else: + # 2. Fallback: Check if it's a legacy lock file (<= 5.16) + from conda.exports import VersionOrder + + script_ver = data.get("script_version", "0") + if VersionOrder(str(script_ver)) <= VersionOrder("5.16"): + if data["yaml_hash"] == self.get_legacy_yaml_hash(): + hash_matches = True + + if hash_matches: channel_alias = ( data.get("channel_alias"), data.get("channel_alias").replace("http:", "https:"), @@ -904,12 +961,11 @@ def read_version_lock(self): raise LockfileError("lock file conda alias mismatch") lock_file_channels = ";".join(data.get("channels", [])) env_channels = ";".join(self.channels) - # also compare with list of default channels - # for backwards compatibility def_channels = ";".join(self.settings.default_channels) if lock_file_channels not in (env_channels, def_channels): raise LockfileError("lock file conda channel mismatch") return data + log.info( "The lock file is not aligned with the actual environment file" ) @@ -917,7 +973,7 @@ def read_version_lock(self): log.info("No version lock file found") return None - def write_version_lock(self, lock_dep, obsolete_pkgs=(), unsafe_pkgs=()): + def write_version_lock(self, lock_dep): import getpass import hashlib import io @@ -939,14 +995,6 @@ def write_version_lock(self, lock_dep, obsolete_pkgs=(), unsafe_pkgs=()): "dependencies": sorted(lock_dep), } - labels = {} - for p in obsolete_pkgs: - labels[p] = "obsolete" - for p in unsafe_pkgs: - labels[p] = "unsafe" - if labels: - data["labels"] = labels - stream = io.StringIO() yaml.dump(data, stream) output = stream.getvalue() @@ -1012,16 +1060,17 @@ def create_lockfile(self, additional_dependencies=()): from conda.exceptions import ( PackagesNotFoundError, ResolvePackageNotFound, + UnavailableInvalidChannel, UnsatisfiableError, ) from conda.exports import subdir obsolete_pkgs = set() - unsafe_pkgs = set() + unsafe_pkgs = {} last_exception = None - # First check only the original urls, then also the obsolete labels - # and finally also unsafe labels + for labels in ((), ("obsolete",), ("obsolete", "unsafe")): + log.debug("Checking labels %s", labels) solver = self.get_solver(dependencies, labels) try: link_precs = solver.solve_final_state() @@ -1030,35 +1079,37 @@ def create_lockfile(self, additional_dependencies=()): for prec in link_precs: split_channel = prec.channel.name.split("/") if len(split_channel) > 1: - # Format is /labels/