Repository: conan-io/conan

Issue:
[bug][triaging] shared_library_package_id() makes conan create call fail with empty error message
<!--
  Please don't forget to update the issue title.
  Include all applicable information to help us reproduce your problem.

  To help us debug your issue please explain:
-->

### Environment Details
  * Operating System+version: macOS Catalina 10.15.4
  * Compiler+version: apple-clang 10.0
  * Conan version: 1.26.0 (error not exclusive to this version)
  * Python version: 3.7.7

### Steps to reproduce
The minimal code which displays the problem, along with more details and reproduction steps is uploaded [here](https://github.com/jbatnozic/conan-create-error-demo-2).

Basically, the issue is that if you have a chain of dependencies which are by default static, except the top-level one which you want to build as shared, you have to call shared_library_package_id() in its package_id method so that all package IDs are generated properly. However, if any of the upstream dependencies are not available in cache and need to be built locally, package creation fails with an empty error message.

### Logs (Executed commands with output)
[conan-log.txt](https://github.com/conan-io/conan/files/4845941/conan-log.txt)

<!--
  Your log content should be related to the bug description, it can be:
  - Conan command output
  - Server output (Artifactory, conan_server)
-->



Discussion:
Hi! Thanks for submitting a reproducible example. I've removed some bits that are not needed to reproduce the bug. I'll add it as a test to reproduce (and fix) it in the codebase.

**package1.py**
```python
from conans import ConanFile

class Package1Conan(ConanFile):
    name = "package-1"
    settings = "os", "arch", "compiler", "build_type"
    options = {"shared": [True, False]}
    default_options = {"shared": False}
```

**package2.py**
```python
from conans import ConanFile

class Package2Conan(ConanFile):
    name = "package-2"
    settings = "os", "arch", "compiler", "build_type"
    options = {"shared": [True, False]}
    default_options = {"shared": False}
    requires = "package-1/1.0@error/test"
```

**package3.py**
```python
from conans import ConanFile, CMake, tools

class Package3Conan(ConanFile):
    name = "package-3"
    settings = "os", "arch", "compiler", "build_type"
    options = {"shared": [True, False]}
    default_options = {"shared": False}

    requires = "package-2/1.0@error/test"

    def package_id(self):
        self.info.shared_library_package_id()
```

Prepare the cache:

```
conan create package1.py  package-1/1.0@error/test
conan create package2.py package-2/1.0@error/test
```

---

### Failure 1:

```
conan create ./package-3/ 1.0@error/test -o package-3:shared=True --build=package-1
```

```
package-1/1.0@error/test: Forced build from source
Installing package: package-3/1.0@error/test
Requirements
    package-1/1.0@error/test from local cache - Cache
    package-2/1.0@error/test from local cache - Cache
    package-3/1.0@error/test from local cache - Cache
Packages
    package-1/1.0@error/test:103f6067a947f366ef91fc1b7da351c588d1827f - Build
    package-2/1.0@error/test:36138b371baf7b74ebc20a3bb39e86331f10e311 - Cache
    package-3/1.0@error/test:Package_ID_unknown - Unknown
...
package-3/1.0@error/test: Unknown binary for package-3/1.0@error/test, computing updated ID
package-3/1.0@error/test: Updated ID: Package_ID_unknown
Traceback (most recent call last):
  File "/Users/jgsogo/dev/conan/conan/conans/client/command.py", line 2051, in run
    method(args[0][1:])
  File "/Users/jgsogo/dev/conan/conan/conans/client/command.py", line 371, in create
    info = self._conan.create(args.path, name, version, user, channel,
  File "/Users/jgsogo/dev/conan/conan/conans/client/conan_api.py", line 94, in wrapper
    return f(api, *args, **kwargs)
  File "/Users/jgsogo/dev/conan/conan/conans/client/conan_api.py", line 374, in create
    create(self.app, ref, graph_info, remotes, update, build_modes,
  File "/Users/jgsogo/dev/conan/conan/conans/client/cmd/create.py", line 45, in create
    deps_install(app=app,
  File "/Users/jgsogo/dev/conan/conan/conans/client/manager.py", line 76, in deps_install
    installer.install(deps_graph, remotes, build_modes, update, keep_build=keep_build,
  File "/Users/jgsogo/dev/conan/conan/conans/client/installer.py", line 295, in install
    self._build(nodes_by_level, keep_build, root_node, graph_info, remotes, build_mode, update)
  File "/Users/jgsogo/dev/conan/conan/conans/client/installer.py", line 421, in _build
    self._binaries_analyzer.reevaluate_node(node, remotes, build_mode, update)
  File "/Users/jgsogo/dev/conan/conan/conans/client/graph/graph_binaries.py", line 360, in reevaluate_node
    assert node.package_id != PACKAGE_ID_UNKNOWN
```

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
@@ -291,17 +291,17 @@ def _compute_package_id(self, node, default_package_id_mode, default_python_requ
         # A bit risky to be done now
         conanfile = node.conanfile
         neighbors = node.neighbors()
+
+        direct_reqs, indirect_reqs = self.package_id_transitive_reqs(node)
+
+        # FIXME: Conan v2.0 This is introducing a bug for backwards compatibility, it will add
+        #   only the requirements available in the 'neighbour.info' object, not all the closure
         if not self._fixed_package_id:
-            direct_reqs = []  # of PackageReference
-            indirect_reqs = set()  # of PackageReference, avoid duplicates
+            old_indirect = set()
             for neighbor in neighbors:
-                ref, nconan = neighbor.ref, neighbor.conanfile
-                direct_reqs.append(neighbor.pref)
-                indirect_reqs.update(nconan.info.requires.refs())
-            # Make sure not duplicated
+                old_indirect.update((p.ref, p.id) for p in neighbor.conanfile.info.requires.refs())
+            indirect_reqs = set(p for p in indirect_reqs if (p.ref, p.id) in old_indirect)
             indirect_reqs.difference_update(direct_reqs)
-        else:
-            direct_reqs, indirect_reqs = self.package_id_transitive_reqs(node)
 
         python_requires = getattr(conanfile, "python_requires", None)
         if python_requires:
diff --git a/conans/client/installer.py b/conans/client/installer.py
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -400,14 +400,13 @@ def _build(self, nodes_by_level, keep_build, root_node, graph_info, remotes, bui
         self._raise_missing(missing)
         processed_package_refs = set()
         self._download(downloads, processed_package_refs)
-        fix_package_id = self._cache.config.full_transitive_package_id
 
         for level in nodes_by_level:
             for node in level:
                 ref, conan_file = node.ref, node.conanfile
                 output = conan_file.output
 
-                self._propagate_info(node, using_build_profile, fix_package_id)
+                self._propagate_info(node, using_build_profile)
                 if node.binary == BINARY_EDITABLE:
                     self._handle_node_editable(node, graph_info)
                     # Need a temporary package revision for package_revision_mode
@@ -419,11 +418,13 @@ def _build(self, nodes_by_level, keep_build, root_node, graph_info, remotes, bui
                     assert ref.revision is not None, "Installer should receive RREV always"
                     if node.binary == BINARY_UNKNOWN:
                         self._binaries_analyzer.reevaluate_node(node, remotes, build_mode, update)
+                        if node.binary == BINARY_MISSING:
+                            self._raise_missing([node])
                     _handle_system_requirements(conan_file, node.pref, self._cache, output)
                     self._handle_node_cache(node, keep_build, processed_package_refs, remotes)
 
         # Finally, propagate information to root node (ref=None)
-        self._propagate_info(root_node, using_build_profile, fix_package_id)
+        self._propagate_info(root_node, using_build_profile)
 
     def _handle_node_editable(self, node, graph_info):
         # Get source of information
@@ -513,13 +514,12 @@ def _build_package(self, node, output, keep_build, remotes):
             node.graph_lock_node.modified = GraphLockNode.MODIFIED_BUILT
         return pref
 
-    def _propagate_info(self, node, using_build_profile, fixed_package_id):
-        if fixed_package_id:
-            # if using config.full_transitive_package_id, it is necessary to recompute
-            # the node transitive information necessary to compute the package_id
-            # as it will be used by reevaluate_node() when package_revision_mode is used and
-            # PACKAGE_ID_UNKNOWN happens due to unknown revisions
-            self._binaries_analyzer.package_id_transitive_reqs(node)
+    def _propagate_info(self, node, using_build_profile):
+        # it is necessary to recompute
+        # the node transitive information necessary to compute the package_id
+        # as it will be used by reevaluate_node() when package_revision_mode is used and
+        # PACKAGE_ID_UNKNOWN happens due to unknown revisions
+        self._binaries_analyzer.package_id_transitive_reqs(node)
         # Get deps_cpp_info from upstream nodes
         node_order = [n for n in node.public_closure if n.binary != BINARY_SKIP]
         # List sort is stable, will keep the original order of the closure, but prioritize levels


Test patch:
diff --git a/conans/test/functional/package_id/package_id_requires_modes_test.py b/conans/test/functional/package_id/package_id_requires_modes_test.py
--- a/conans/test/functional/package_id/package_id_requires_modes_test.py
+++ b/conans/test/functional/package_id/package_id_requires_modes_test.py
@@ -592,3 +592,35 @@ def package_revision_mode_editable_test(self):
         client2.save({"conanfile.py": GenConanfile().with_require_plain("dep2/1.0@user/testing")})
         client2.run('create . consumer/1.0@user/testing --build')
         self.assertIn("consumer/1.0@user/testing: Created", client2.out)
+
+
+class PackageRevisionModeTestCase(unittest.TestCase):
+
+    def test_transtive_package_revision_mode(self):
+        t = TestClient()
+        t.save({
+            'package1.py': GenConanfile("pkg1"),
+            'package2.py': GenConanfile("pkg2").with_require_plain("pkg1/1.0"),
+            'package3.py': textwrap.dedent("""
+                from conans import ConanFile
+                class Recipe(ConanFile):
+                    requires = "pkg2/1.0"
+                    def package_id(self):
+                        self.info.requires["pkg1"].package_revision_mode()
+            """)
+        })
+        t.run("create package1.py pkg1/1.0@")
+        t.run("create package2.py pkg2/1.0@")
+
+        # If we only build pkg1, we get a new packageID for pkg3
+        t.run("create package3.py pkg3/1.0@ --build=pkg1", assert_error=True)
+        self.assertIn("pkg3/1.0:Package_ID_unknown - Unknown", t.out)
+        self.assertIn("pkg3/1.0: Updated ID: 283642385cc7b64ec7b5903f6895107e0848d238", t.out)
+        self.assertIn("ERROR: Missing binary: pkg3/1.0:283642385cc7b64ec7b5903f6895107e0848d238",
+                      t.out)
+
+        # If we build both, we get the new package
+        t.run("create package3.py pkg3/1.0@ --build=pkg1 --build=pkg3")
+        self.assertIn("pkg3/1.0:Package_ID_unknown - Unknown", t.out)
+        self.assertIn("pkg3/1.0: Updated ID: 283642385cc7b64ec7b5903f6895107e0848d238", t.out)
+        self.assertIn("pkg3/1.0: Package '283642385cc7b64ec7b5903f6895107e0848d238' created", t.out)