diff --git a/conda_libmamba_solver/solver.py b/conda_libmamba_solver/solver.py index 6b14203f..98f78825 100644 --- a/conda_libmamba_solver/solver.py +++ b/conda_libmamba_solver/solver.py @@ -402,10 +402,16 @@ def _solve_attempt( def _specs_to_tasks(self, in_state: SolverInputState, out_state: SolverOutputState): log.debug("Creating tasks for %s specs", len(out_state.specs)) if in_state.is_removing: - return self._specs_to_tasks_remove(in_state, out_state) - if self._called_from_conda_build(): - return self._specs_to_tasks_conda_build(in_state, out_state) - return self._specs_to_tasks_add(in_state, out_state) + tasks = self._specs_to_tasks_remove(in_state, out_state) + elif self._called_from_conda_build(): + tasks = self._specs_to_tasks_conda_build(in_state, out_state) + else: + tasks = self._specs_to_tasks_add(in_state, out_state) + log.debug( + "Created following tasks:\n%s", + json.dumps({k[0]: v for k, v in tasks.items()}, indent=2), + ) + return tasks @staticmethod def _spec_to_str(spec): @@ -456,7 +462,7 @@ def _specs_to_tasks_add(self, in_state: SolverInputState, out_state: SolverOutpu # logic considers should be the target version for each package in the environment # and requested changes. We are _not_ following those targets here, but we do iterate # over the list to decide what to do with each package. - for name, _classic_logic_spec in out_state.specs.items(): + for name, _classic_logic_spec in sorted(out_state.specs.items()): if name.startswith("__"): continue # ignore virtual packages installed: PackageRecord = in_state.installed.get(name) diff --git a/conda_libmamba_solver/state.py b/conda_libmamba_solver/state.py index dd6edc47..93acb2c6 100644 --- a/conda_libmamba_solver/state.py +++ b/conda_libmamba_solver/state.py @@ -231,8 +231,9 @@ def installed(self) -> Mapping[str, PackageRecord]: """ This exposes the installed packages in the prefix. Note that a ``PackageRecord`` can generate an equivalent ``MatchSpec`` object with ``.to_match_spec()``. + Records are toposorted. """ - return MappingProxyType(self.prefix_data._prefix_records) + return MappingProxyType(dict(sorted(self.prefix_data._prefix_records.items()))) @property def history(self) -> Mapping[str, MatchSpec]: @@ -261,7 +262,7 @@ def virtual(self) -> Mapping[str, MatchSpec]: cannot be (un)installed, they only represent constrains for other packages. By convention, their names start with a double underscore. """ - return MappingProxyType(self._virtual) + return MappingProxyType(dict(sorted(self._virtual.items()))) @property def aggressive_updates(self) -> Mapping[str, MatchSpec]: @@ -281,12 +282,14 @@ def always_update(self) -> Mapping[str, MatchSpec]: - almost all packages if update_all is true - etc """ - pkgs = {pkg: MatchSpec(pkg) for pkg in self.aggressive_updates if pkg in self.installed} + installed = self.installed + pinned = self.pinned + pkgs = {pkg: MatchSpec(pkg) for pkg in self.aggressive_updates if pkg in installed} if context.auto_update_conda and paths_equal(self.prefix, context.root_prefix): pkgs.setdefault("conda", MatchSpec("conda")) if self.update_modifier.UPDATE_ALL: - for pkg in self.installed: - if pkg != "python" and pkg not in self.pinned: + for pkg in installed: + if pkg != "python" and pkg not in pinned: pkgs.setdefault(pkg, MatchSpec(pkg)) return MappingProxyType(pkgs) diff --git a/news/378-sort-installed b/news/378-sort-installed new file mode 100644 index 00000000..5fc31e4b --- /dev/null +++ b/news/378-sort-installed @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Ensure specs, installed and virtual packages are always sorted to avoid injecting accidental randomness in the solve process. (#378) + +### Deprecations + +* + +### Docs + +* + +### Other + +*