diff --git a/src/rez/config.py b/src/rez/config.py index 4309e2036..747b75cc3 100644 --- a/src/rez/config.py +++ b/src/rez/config.py @@ -400,6 +400,7 @@ def _parse_env_var(self, value): "alias_back": OptionalStr, "package_preprocess_function": OptionalStrOrFunction, "package_preprocess_mode": PreprocessMode_, + "error_on_missing_variant_requires": Bool, "context_tracking_host": OptionalStr, "variant_shortlinks_dirname": OptionalStr, "build_thread_count": BuildThreadCount_, diff --git a/src/rez/data/tests/solver/packages/missing_variant_requires/1/package.py b/src/rez/data/tests/solver/packages/missing_variant_requires/1/package.py new file mode 100644 index 000000000..6cf364e53 --- /dev/null +++ b/src/rez/data/tests/solver/packages/missing_variant_requires/1/package.py @@ -0,0 +1,10 @@ +name = "missing_variant_requires" +version = "1" + +def commands(): + pass + +variants = [ + ["noexist"], + ["nada"] +] diff --git a/src/rez/rezconfig.py b/src/rez/rezconfig.py index 81f7347f1..96fbfea66 100644 --- a/src/rez/rezconfig.py +++ b/src/rez/rezconfig.py @@ -499,6 +499,17 @@ # this value is False. allow_unversioned_packages = True +# Defines whether a resolve should immediately fail if any variants have a required package that can't be found. +# This can be useful to disable if you have packages that aren't available to all users. +# It is enabled by default. If a variant has requires that cannot be found , it will error immediately rather than +# trying the other variants. +# If disabled, it will try other variants before giving up. +# +# .. warning:: +# Memcached isn't tested with scenarios where you expect users to have access to different sets of packages. +# It expects that every user can access the same set of packages, which may cause incorrect resolves +# when this option is disabled. +error_on_missing_variant_requires = True ############################################################################### # Environment Resolution @@ -657,7 +668,6 @@ # - "override": Package's preprocess function completely overrides the global preprocess. package_preprocess_mode = "override" - ############################################################################### # Context Tracking ############################################################################### diff --git a/src/rez/solver.py b/src/rez/solver.py index 2ec0f35fd..d91d9890f 100644 --- a/src/rez/solver.py +++ b/src/rez/solver.py @@ -1383,10 +1383,16 @@ def _create_phase(status=None): # Raise with more info when match found searched = "; ".join(self.solver.package_paths) requested = ", ".join(requesters) + + fail_message = ("package family not found: {}, was required by: {} (searched: {})" + .format(req.name, requested, searched)) + # TODO: Test with memcached to see if this can cause any conflicting behaviour + # where a package may show as missing/available inadvertently + if not config.error_on_missing_variant_requires: + print(fail_message, file=sys.stderr) + return _create_phase(SolverStatus.failed) raise PackageFamilyNotFoundError( - "package family not found: %s, " - "was required by: %s (searched: %s)" - % (req.name, requested, searched)) + fail_message) scopes.append(scope) if self.pr: @@ -2398,7 +2404,10 @@ def _get_failed_phase(self, index=None): except IndexError: raise IndexError("failure index out of range") - fail_description = phase.failure_reason.description() + if phase.failure_reason is None: + fail_description = "Solver failed with unknown reason." + else: + fail_description = phase.failure_reason.description() if prepend_abort_reason and self.abort_reason: fail_description = "%s:\n%s" % (self.abort_reason, fail_description) diff --git a/src/rez/tests/test_completion.py b/src/rez/tests/test_completion.py index 87be442fd..ef8c390c6 100644 --- a/src/rez/tests/test_completion.py +++ b/src/rez/tests/test_completion.py @@ -52,7 +52,7 @@ def _eq(prefix, expected_completions): _eq("", ["bahish", "nada", "nopy", "pybah", "pydad", "pyfoo", "pymum", "pyodd", "pyson", "pysplit", "python", "pyvariants", "test_variant_split_start", "test_variant_split_mid1", - "test_variant_split_mid2", "test_variant_split_end"]) + "test_variant_split_mid2", "test_variant_split_end", "missing_variant_requires"]) _eq("py", ["pybah", "pydad", "pyfoo", "pymum", "pyodd", "pyson", "pysplit", "python", "pyvariants"]) _eq("pys", ["pyson", "pysplit"]) diff --git a/src/rez/tests/test_packages.py b/src/rez/tests/test_packages.py index b94a0dde9..11dc60c1f 100644 --- a/src/rez/tests/test_packages.py +++ b/src/rez/tests/test_packages.py @@ -57,7 +57,8 @@ 'late_binding-1.0', 'timestamped-1.0.5', 'timestamped-1.0.6', 'timestamped-1.1.0', 'timestamped-1.1.1', 'timestamped-1.2.0', 'timestamped-2.0.0', 'timestamped-2.1.0', 'timestamped-2.1.5', - 'multi-1.0', 'multi-1.1', 'multi-1.2', 'multi-2.0' + 'multi-1.0', 'multi-1.1', 'multi-1.2', 'multi-2.0', + 'missing_variant_requires-1' ]) diff --git a/src/rez/tests/test_solver.py b/src/rez/tests/test_solver.py index 6422bcf07..82ee932d1 100644 --- a/src/rez/tests/test_solver.py +++ b/src/rez/tests/test_solver.py @@ -7,6 +7,7 @@ """ from __future__ import print_function +import rez.exceptions from rez.vendor.version.requirement import Requirement from rez.solver import Solver, Cycle, SolverStatus from rez.config import config @@ -214,6 +215,7 @@ def test_07(self): def test_08(self): """Cyclic failures.""" + def _test(*pkgs): s = self._fail(*pkgs) self.assertTrue(isinstance(s.failure_reason(), Cycle)) @@ -248,6 +250,14 @@ def test_11_variant_splitting(self): "test_variant_split_mid2-2.0[0]", "test_variant_split_start-1.0[1]"]) + def test_12_missing_variant_requires(self): + config.override("error_on_missing_variant_requires", True) + with self.assertRaises(rez.exceptions.PackageFamilyNotFoundError): + self._solve(["missing_variant_requires"], []) + + config.override("error_on_missing_variant_requires", False) + self._solve(["missing_variant_requires"], ["nada[]", "missing_variant_requires-1[1]"]) + if __name__ == '__main__': unittest.main()