Repository: conda/conda

Issue:
Unintentional build downgrades in mixed feature settings
Under certain limited circumstances involving features, `conda` unexpectedly downgrades to an earlier build of a package. The conditions are as follows:

- The current version of a package `foo` has multiple builds with distinct build numbers.
- Newer builds depend on a second package `bar`, while older builds do not.
- Some of the valid choices for the `bar` dependency have features, and some do not.

When those conditions are met, the command
```
conda create -n errtest foo
```
will select an older build of `foo`, without the `bar` dependency, instead of the latest build. (Contact me or @msarahan for a specific test case.)

Currently, this is not occurring with conda 4.4. The reasons for this are not fully clear, because the bug has been located and exists in 4.3 and 4.4. Bug fix coming.



Discussion:
To help illustrate the issue, here is a preliminary version of the test that should fail with the current version of `Resolve`, and which will be fixed.
```
def test_unintentional_feature_downgrade():
    # With the bug in place, this bad build of scipy
    # will be selected for install instead of a later
    # build of scipy 0.11.0.
    index2 = index.copy()
    bad_dist = Dist('scipy-0.11.0-np17py33_x0.tar.bz2')
    index2[bad_dist] = IndexRecord(**{
        'build': 'np17py33_x0',
        'build_number': 0,
        'depends': ['python 3.3*'],
        'name': 'scipy',
        'version': '0.11.0',
    })
    r = Resolve(index2)
    install = r.install(['scipy 0.11.0'])
    assert bad_dist not in install
    assert any(d.name == 'numpy' for d in install)
```

Resolving patch:
diff --git a/conda/resolve.py b/conda/resolve.py
--- a/conda/resolve.py
+++ b/conda/resolve.py
@@ -628,7 +628,14 @@ def generate_feature_metric(self, C):
         for name, group in iteritems(self.groups):
             nf = [len(self.features(dist)) for dist in group]
             maxf = max(nf)
-            eq.update({dist.full_name: maxf-fc for dist, fc in zip(group, nf) if fc < maxf})
+            if maxf > 0:
+                eq.update({dist.full_name: maxf-fc for dist, fc in zip(group, nf) if maxf > fc})
+                # This entry causes conda to weight the absence of a package the same
+                # as if a non-featured version were instaslled. If this were not here,
+                # conda will sometimes select an earlier build of a requested package
+                # if doing so can eliminate this package as a dependency.
+                # https://github.com/conda/conda/issues/6765
+                eq['!' + self.push_MatchSpec(C, name)] = maxf
             total += maxf
         return eq, total
 


Test patch:
diff --git a/tests/test_resolve.py b/tests/test_resolve.py
--- a/tests/test_resolve.py
+++ b/tests/test_resolve.py
@@ -766,6 +766,28 @@ def test_install_package_with_feature():
     r.install(['mypackage','feature 1.0'])
 
 
+def test_unintentional_feature_downgrade():
+    # See https://github.com/conda/conda/issues/6765
+    # With the bug in place, this bad build of scipy
+    # will be selected for install instead of a later
+    # build of scipy 0.11.0.
+    good_rec = index[Dist('scipy-0.11.0-np17py33_3.tar.bz2')]
+    bad_deps = tuple(d for d in good_rec.depends
+                     if not d.startswith('numpy'))
+    bad_rec = IndexRecord.from_objects(good_rec,
+                  build=good_rec.build.replace('_3','_x0'),
+                  build_number=0, depends=bad_deps,
+                  fn=good_rec.fn.replace('_3','_x0'),
+                  url=good_rec.url.replace('_3','_x0'))
+    bad_dist = Dist(bad_rec)
+    index2 = index.copy()
+    index2[bad_dist] = bad_rec
+    r = Resolve(index2)
+    install = r.install(['scipy 0.11.0'])
+    assert bad_dist not in install
+    assert any(d.name == 'numpy' for d in install)
+
+
 def test_circular_dependencies():
     index2 = index.copy()
     index2['package1-1.0-0.tar.bz2'] = IndexRecord(**{