diff --git a/conda/resolve.py b/conda/resolve.py index f37cb0d99a5..66bdcd93998 100644 --- a/conda/resolve.py +++ b/conda/resolve.py @@ -619,10 +619,11 @@ def version_key(self, prec, vtype=None): version_comparator = VersionOrder(prec.get('version', '')) build_number = prec.get('build_number', 0) build_string = prec.get('build') + noarch = - int(prec.subdir == 'noarch') if self._channel_priority != ChannelPriority.DISABLED: - vkey = [valid, -channel_priority, version_comparator, build_number] + vkey = [valid, -channel_priority, version_comparator, build_number, noarch] else: - vkey = [valid, version_comparator, -channel_priority, build_number] + vkey = [valid, version_comparator, -channel_priority, build_number, noarch] if self._solver_ignore_timestamps: vkey.append(build_string) else: @@ -782,6 +783,7 @@ def generate_version_metrics(self, C, specs, include0=False): eqc = {} # channel eqv = {} # version eqb = {} # build number + eqa = {} # arch/noarch eqt = {} # timestamp sdict = {} # Dict[package_name, PackageRecord] @@ -807,20 +809,24 @@ def generate_version_metrics(self, C, specs, include0=False): if targets and any(prec == t for t in targets): continue if pkey is None: - ic = iv = ib = it = 0 + ic = iv = ib = it = ia = 0 # valid package, channel priority elif pkey[0] != version_key[0] or pkey[1] != version_key[1]: ic += 1 - iv = ib = it = 0 + iv = ib = it = ia = 0 # version elif pkey[2] != version_key[2]: iv += 1 - ib = it = 0 + ib = it = ia = 0 # build number elif pkey[3] != version_key[3]: ib += 1 + it = ia = 0 + # arch/noarch + elif pkey[4] != version_key[4]: + ia += 1 it = 0 - elif not self._solver_ignore_timestamps and pkey[4] != version_key[4]: + elif not self._solver_ignore_timestamps and pkey[5] != version_key[5]: it += 1 prec_sat_name = self.to_sat_name(prec) @@ -830,11 +836,13 @@ def generate_version_metrics(self, C, specs, include0=False): eqv[prec_sat_name] = iv if ib or include0: eqb[prec_sat_name] = ib + if ia or include0: + eqa[prec_sat_name] = ia if it or include0: eqt[prec_sat_name] = it pkey = version_key - return eqc, eqv, eqb, eqt + return eqc, eqv, eqb, eqa, eqt def dependency_sort(self, must_have): # type: (Dict[package_name, PackageRecord]) -> List[PackageRecord] @@ -1123,7 +1131,7 @@ def is_converged(solution): # Requested packages: maximize versions log.debug("Solve: maximize versions of requested packages") - eq_req_c, eq_req_v, eq_req_b, eq_req_t = r2.generate_version_metrics(C, specr) + eq_req_c, eq_req_v, eq_req_b, eq_req_a, eq_req_t = r2.generate_version_metrics(C, specr) solution, obj3a = C.minimize(eq_req_c, solution) solution, obj3 = C.minimize(eq_req_v, solution) log.debug('Initial package channel/version metric: %d/%d', obj3a, obj3) @@ -1150,6 +1158,11 @@ def is_converged(solution): solution, obj4 = C.minimize(eq_req_b, solution) log.debug('Initial package build metric: %d', obj4) + # prefer arch packages where available for requested specs + log.debug("Solve: prefer arch over noarch for requested packages") + solution, noarch_obj = C.minimize(eq_req_a, solution) + log.debug('Noarch metric: %d', noarch_obj) + # Optional installations: minimize count if not _remove: log.debug("Solve: minimize number of optional installations") @@ -1164,13 +1177,15 @@ def is_converged(solution): log.debug('Dependency update count: %d', obj50) # Remaining packages: maximize versions, then builds - log.debug("Solve: maximize versions and builds of indirect dependencies") - eq_c, eq_v, eq_b, eq_t = r2.generate_version_metrics(C, speca) + log.debug("Solve: maximize versions and builds of indirect dependencies. " + "Prefer arch over noarch where equivalent.") + eq_c, eq_v, eq_b, eq_a, eq_t = r2.generate_version_metrics(C, speca) solution, obj5a = C.minimize(eq_c, solution) solution, obj5 = C.minimize(eq_v, solution) solution, obj6 = C.minimize(eq_b, solution) - log.debug('Additional package channel/version/build metrics: %d/%d/%d', - obj5a, obj5, obj6) + solution, obj6a = C.minimize(eq_a, solution) + log.debug('Additional package channel/version/build/noarch metrics: %d/%d/%d/%d', + obj5a, obj5, obj6, obj6a) # Prune unnecessary packages log.debug("Solve: prune unnecessary packages") @@ -1178,8 +1193,7 @@ def is_converged(solution): solution, obj7 = C.minimize(eq_c, solution, trymax=True) log.debug('Weak dependency count: %d', obj7) - converged = is_converged(solution) - if not converged: + if not is_converged(solution): # Maximize timestamps eq_t.update(eq_req_t) solution, obj6t = C.minimize(eq_t, solution) diff --git a/tests/test_resolve.py b/tests/test_resolve.py index a70fbad1950..98173751b38 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -182,7 +182,7 @@ def test_generate_eq_1(): reduced_index = r.get_reduced_index((MatchSpec('anaconda'), )) r2 = Resolve(reduced_index, True) C = r2.gen_clauses() - eqc, eqv, eqb, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) + eqc, eqv, eqb, eqa, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) # Should satisfy the following criteria: # - lower versions of the same package should should have higher # coefficients. @@ -1160,7 +1160,7 @@ def test_channel_priority_2(): dists = this_r.get_reduced_index(spec) r2 = Resolve(dists, True, channels=channels) C = r2.gen_clauses() - eqc, eqv, eqb, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) + eqc, eqv, eqb, eqa, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) eqc = {key: value for key, value in iteritems(eqc)} pprint(eqc) assert eqc == { @@ -1312,7 +1312,7 @@ def test_channel_priority_2(): r2 = Resolve(dists, True, channels=channels) C = r2.gen_clauses() - eqc, eqv, eqb, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) + eqc, eqv, eqb, eqa, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) eqc = {key: value for key, value in iteritems(eqc)} assert eqc == {}, eqc installed_w_strict = [prec.dist_str() for prec in this_r.install(spec)] @@ -1336,7 +1336,7 @@ def test_channel_priority_2(): dists = this_r.get_reduced_index(spec) r2 = Resolve(dists, True, channels=channels) C = r2.gen_clauses() - eqc, eqv, eqb, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) + eqc, eqv, eqb, eqa, eqt = r2.generate_version_metrics(C, list(r2.groups.keys())) eqc = {key: value for key, value in iteritems(eqc)} pprint(eqc) assert eqc == { @@ -1882,3 +1882,210 @@ def test_get_reduced_index_broadening_preferred_solution(): assert d.version == '2.0', "top version should be 2.0, but is {}".format(d.version) elif d.name == 'bottom': assert d.version == '2.5', "bottom version should be 2.5, but is {}".format(d.version) + + +def test_arch_preferred_over_noarch_when_otherwise_equal(): + index = ( + PackageRecord(**{ + "build": "py36_0", + "build_number": 0, + "date": "2016-12-17", + "license": "BSD", + "md5": "9b4568068e3a7ac81be87902827d949e", + "name": "itsdangerous", + "size": 19688, + "version": "0.24" + }), + PackageRecord(**{ + "arch": None, + "binstar": { + "channel": "main", + "owner_id": "58596cc93d1b550ffad38672", + "package_id": "5898cb9d9aba4511169c383a" + }, + "build": "py_0", + "build_number": 0, + "has_prefix": False, + "license": "BSD", + "machine": None, + "md5": "917e90ca4e80324b77e8df449d07eefc", + "name": "itsdangerous", + "noarch": "python", + "operatingsystem": None, + "platform": None, + "requires": [], + "size": 14098, + "subdir": "noarch", + "target-triplet": "None-any-None", + "version": "0.24" + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['itsdangerous']) + for d in install: + assert d.subdir == context.subdir + + +def test_noarch_preferred_over_arch_when_version_greater(): + index = ( + PackageRecord(**{ + 'name': 'abc', + 'version': '2.0', + 'build': '0', + "subdir": "noarch", + 'build_number': 0, + }), + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['abc']) + for d in install: + assert d.subdir == 'noarch' + assert d.version == '2.0' + + +def test_noarch_preferred_over_arch_when_build_greater(): + index = ( + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '1', + "subdir": "noarch", + 'build_number': 1, + }), + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['abc']) + for d in install: + assert d.subdir == 'noarch' + assert d.build_number == 1 + + +def test_arch_preferred_over_noarch_when_otherwise_equal_dep(): + index = ( + PackageRecord(**{ + "build": "py36_0", + "build_number": 0, + "date": "2016-12-17", + "license": "BSD", + "md5": "9b4568068e3a7ac81be87902827d949e", + "name": "itsdangerous", + "size": 19688, + "version": "0.24" + }), + PackageRecord(**{ + "arch": None, + "binstar": { + "channel": "main", + "owner_id": "58596cc93d1b550ffad38672", + "package_id": "5898cb9d9aba4511169c383a" + }, + "build": "py_0", + "build_number": 0, + "has_prefix": False, + "license": "BSD", + "machine": None, + "md5": "917e90ca4e80324b77e8df449d07eefc", + "name": "itsdangerous", + "noarch": "python", + "operatingsystem": None, + "platform": None, + "requires": [], + "size": 14098, + "subdir": "noarch", + "target-triplet": "None-any-None", + "version": "0.24" + }), + PackageRecord(**{ + 'name': 'foo', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + 'depends': ['itsdangerous'], + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['foo']) + for d in install: + if d.name == 'itsdangerous': + assert d.subdir == context.subdir + + +def test_noarch_preferred_over_arch_when_version_greater_dep(): + index = ( + PackageRecord(**{ + 'name': 'abc', + 'version': '2.0', + 'build': '0', + "subdir": "noarch", + 'build_number': 0, + }), + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + }), + PackageRecord(**{ + 'name': 'foo', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + 'depends': ['abc'], + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['foo']) + for d in install: + if d.name == 'abc': + assert d.subdir == 'noarch' + assert d.version == '2.0' + + +def test_noarch_preferred_over_arch_when_build_greater_dep(): + index = ( + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '1', + "subdir": "noarch", + 'build_number': 1, + }), + PackageRecord(**{ + 'name': 'abc', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + }), + PackageRecord(**{ + 'name': 'foo', + 'version': '1.0', + 'build': '0', + "subdir": context.subdir, + 'build_number': 0, + 'depends': ['abc'], + }), + ) + r = Resolve(OrderedDict((prec, prec) for prec in index)) + install = r.install(['abc']) + for d in install: + if d.name == 'abc': + assert d.subdir == 'noarch' + assert d.build_number == 1