Repository: conan-io/conan

Issue:
[bug] --build=missing does not honor CompatiblePackage
### Environment Details (include every applicable attribute)
  * Operating System+version: Windows 10
  * Compiler+version: Visual Studio 2019
  * Conan version: 1.20.4
  * Python version: 3.8

### Steps to reproduce (Include if Applicable)
I have one recipe for `libA`, that defines a package compatibility range such as 

```
def package_id(self):
  if self.settings.compiler == "Visual Studio" and self.settings.compiler.version == "16":
    for version in ("15", "14"):
      compatible_pkg = CompatiblePackage(self)
      compatible_pkg.settings.compiler.version = version
      self.compatible_packages.append(compatible_pkg)
```

E.g. such that the package is binary downward compatible. If a consumer builds with VS2019, the package gets also accepted if it was build with VS2015 or VS2017.

This works fine if I have a consumer that uses libA (`self.requires.add("libA/0.1.1@user/stable")`) and do a:
```
conan install . 
```
It finds that package that was build with VS2015 and uses it.

However, if I specify 
```
conan install -b missing .
```
the package won't be accepted and Conan tries to rebuild `libA` with VS2019.
I would expect, that `libA` is not rebuild, because a matching package is available.

This behavior is especially "annoying", if the consumer requires a lot of packages, on of which are actually missing, but many of them are available as compatible packages Then all packages are rebuild instead of the single, actually missing package.
[bug] [windows] Path added to virtualenv is not considered when looking for a program 
Although there is a major refactor related to Conan virtualenvs (https://github.com/conan-io/conan/issues/5821) this issue can be addressed first:

 1. Something wrong with the path for CMD: https://github.com/conan-io/conan/blob/4217bb326e02e14c8168b080ef1e7a07c18795c8/conans/test/functional/generators/virtualenv_test.py#L263


Discussion:
Thanks for the report. Checked that the behavior is that one.

What I am currently struggling is to decide if it is a bug or a feature. The ``CompatiblePackage`` feature is provided as a **fallback**, its semantic is that the binary for your configuration is missing, but it can use another binary built with other configuration.

If we changed the behavior, and it didn't build from sources for the VS2019 compiler, then, suddenly there is no way to transitively build from sources those dependencies with the new VS2019 compiler version, which is a flow that makes a lot of sense. 

Maybe we need to think of other ways to enable both flows (maybe a ``--build=incompatible`` or the like), but so far we can't conclude this is a bug.
Yes, I figured that it might not be a bug.
I agree that it should be possible to force a rebuild of all packages which are not an exact match.
But at the same time it should also be possible to rebuild only actually missing packages, which is unfortunately not possible right now.

Maybe it is necessary to introduce another keyword.
But I guess then it would be more intuitive if `missing` only builds the actually missing packages, and `incompatible` also builds those which do not have the same package ID.
Then both could be combined to build both missing and incompatible packages.

However that would be breaking the current behavior of the `missing` keyword.
Also I am not sure if `incompatible` would be the right keyword, since these packages are compatible, but don't have the same package ID.


I'll take a look.
Thanks! 💯 

Ask if you need any help
It is caused since 'where' on Windows returns multiple matched results in priority order.

Resolving patch:
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -96,7 +96,7 @@ def _evaluate_cache_pkg(self, node, package_layout, pref, metadata, remote, remo
             node.prev = metadata.packages[pref.id].revision
             assert node.prev, "PREV for %s is None: %s" % (str(pref), metadata.dumps())
 
-    def _evaluate_remote_pkg(self, node, pref, remote, remotes, build_mode):
+    def _evaluate_remote_pkg(self, node, pref, remote, remotes):
         remote_info = None
         if remote:
             try:
@@ -126,11 +126,8 @@ def _evaluate_remote_pkg(self, node, pref, remote, remotes, build_mode):
             recipe_hash = remote_info.recipe_hash
         else:
             recipe_hash = None
-            if build_mode.allowed(node.conanfile):
-                node.binary = BINARY_BUILD
-            else:
-                node.binary = BINARY_MISSING
             node.prev = None
+            node.binary = BINARY_MISSING
 
         return recipe_hash, remote
 
@@ -162,33 +159,37 @@ def _evaluate_node(self, node, build_mode, update, remotes):
         if locked and locked.pref.id == node.package_id:
             pref = locked.pref  # Keep the locked with PREV
             self._process_node(node, pref, build_mode, update, remotes)
+            if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
+                node.binary = BINARY_BUILD
         else:
             assert node.prev is None, "Non locked node shouldn't have PREV in evaluate_node"
             pref = PackageReference(node.ref, node.package_id)
             self._process_node(node, pref, build_mode, update, remotes)
-            if node.binary == BINARY_MISSING and node.conanfile.compatible_packages:
-                compatible_build_mode = BuildMode(None, self._out)
-                for compatible_package in node.conanfile.compatible_packages:
-                    package_id = compatible_package.package_id()
-                    if package_id == node.package_id:
-                        node.conanfile.output.info("Compatible package ID %s equal to the default "
-                                                   "package ID" % package_id)
-                        continue
-                    pref = PackageReference(node.ref, package_id)
-                    node.binary = None  # Invalidate it
-                    # NO Build mode
-                    self._process_node(node, pref, compatible_build_mode, update, remotes)
-                    if node.binary and node.binary != BINARY_MISSING:
-                        node.conanfile.output.info("Main binary package '%s' missing. Using "
-                                                   "compatible package '%s'"
-                                                   % (node.package_id, package_id))
-                        node._package_id = package_id
-                        # So they are available in package_info() method
-                        node.conanfile.settings.values = compatible_package.settings
-                        node.conanfile.options.values = compatible_package.options
-                        break
-                    else:
-                        node.binary = BINARY_MISSING
+            if node.binary == BINARY_MISSING:
+                if node.conanfile.compatible_packages:
+                    compatible_build_mode = BuildMode(None, self._out)
+                    for compatible_package in node.conanfile.compatible_packages:
+                        package_id = compatible_package.package_id()
+                        if package_id == node.package_id:
+                            node.conanfile.output.info("Compatible package ID %s equal to the "
+                                                       "default package ID" % package_id)
+                            continue
+                        pref = PackageReference(node.ref, package_id)
+                        node.binary = None  # Invalidate it
+                        # NO Build mode
+                        self._process_node(node, pref, compatible_build_mode, update, remotes)
+                        assert node.binary is not None
+                        if node.binary != BINARY_MISSING:
+                            node.conanfile.output.info("Main binary package '%s' missing. Using "
+                                                       "compatible package '%s'"
+                                                       % (node.package_id, package_id))
+                            node._package_id = package_id
+                            # So they are available in package_info() method
+                            node.conanfile.settings.values = compatible_package.settings
+                            node.conanfile.options.values = compatible_package.options
+                            break
+                if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
+                    node.binary = BINARY_BUILD
 
     def _process_node(self, node, pref, build_mode, update, remotes):
         # Check that this same reference hasn't already been checked
@@ -221,7 +222,7 @@ def _process_node(self, node, pref, build_mode, update, remotes):
             recipe_hash = None
         else:  # Binary does NOT exist locally
             # Returned remote might be different than the passed one if iterating remotes
-            recipe_hash, remote = self._evaluate_remote_pkg(node, pref, remote, remotes, build_mode)
+            recipe_hash, remote = self._evaluate_remote_pkg(node, pref, remote, remotes)
 
         if build_mode.outdated:
             if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE):


Test patch:
diff --git a/conans/test/functional/package_id/compatible_test.py b/conans/test/functional/package_id/compatible_test.py
--- a/conans/test/functional/package_id/compatible_test.py
+++ b/conans/test/functional/package_id/compatible_test.py
@@ -440,3 +440,28 @@ def package_info(self):
                       client.out)
         self.assertIn("consumer/0.1@user/stable: Package 'fca9e94084ed6fe0ca149dc9c2d54c0f336f0d7e'"
                       " created", client.out)
+
+    def build_missing_test(self):
+        # https://github.com/conan-io/conan/issues/6133
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+
+            class Conan(ConanFile):
+                settings = "os"
+
+                def package_id(self):
+                   if self.settings.os == "Windows":
+                       compatible = self.info.clone()
+                       compatible.settings.os = "Linux"
+                       self.compatible_packages.append(compatible)
+                """)
+
+        client.save({"conanfile.py": conanfile})
+        client.run("create . pkg/0.1@user/testing -s os=Linux")
+
+        client.save({"conanfile.py": GenConanfile().with_require_plain("pkg/0.1@user/testing")})
+        client.run("install . -s os=Windows --build=missing")
+        self.assertIn("pkg/0.1@user/testing:cb054d0b3e1ca595dc66bc2339d40f1f8f04ab31 - Cache",
+                      client.out)
+        self.assertIn("pkg/0.1@user/testing: Already installed!", client.out)