Skip to content

[bug] dependency resolution of header-only library is non-deterministic #11473

@spektrof

Description

@spektrof

Hi,

We'd like to have a deterministic dependency resolution process where we can get exactly the same transient dependencies what we used at building for the given package.

We are using the name/version@#revision:package_id as an input with the following conan configuration:

[general]
default_profile = default
compression_level = 9
sysrequires_sudo = True
request_timeout = 60
default_package_id_mode = package_revision_mode
parallel_download = 8
revisions_enabled = 1
full_transitive_package_id = 1

The package_revision_mode and full_transitive_package_id will provide us a fully deterministic binary model. However I noticed some non-deterministic result with header-only packages.

Let's say we have a package (pkgC) which depends on pkgB header only library and pkgB depends on pkgA header only library. We build these packages, upload to the artifactory. Then later, the recipe of pkgA has been changed, so we decide to rebuild pkgB and pkgC. This case the built pkgB is different, but actually it has the same reference and cause problems.

The pkgB has the same reference because the requires field is empty. The full_requires field list the name/version:package_id for each dependency, but this is not enough to get the proper revision of pkgA, and the output of conan lock create will be different after the rebuild.

The requires field of pkgC is correct, it's package id will be different after the second build as it's expected.

Environment Details (include every applicable attribute)

  • Operating System+version: RHEL7
  • Compiler+version: g++ (GCC) 8.2.1 20180905
  • Conan version: 1.48.1
  • Python version: 3.7.5

Steps to reproduce (Include if Applicable)

Instead of checking the lockfile, I provide an example where we can see that the pkgB was generated with the same reference, however it's dependency is different after the second build. I used a metadata conan generator to check that.

Set up conanfiles, generator, patch file:

recipes/pkgA/conanfile.py

from conans import ConanFile

class PkgA(ConanFile):
  name = "pkgA"
  settings = "os", "arch", "compiler"

  def package_id(self):
    self.info.header_only()

  def package_info(self):
    self.cpp_info.name = "pkgA"
    self.cpp_info.components["pkgA_lib"].name = "pkgA_lib"
    self.cpp_info.components["pkgA_lib"].includes = ["include"]
    self.cpp_info.components["pkgA_lib"].defines = ["PKGA_PUBLIC_DEFINE"]

recipes/pkgB/conanfile.py

from conans import ConanFile

class PkgB(ConanFile):
  name = "pkgB"
  settings = "os", "arch", "compiler"
  requires = "pkgA/1.0"

  def package_id(self):
    self.info.header_only()

  def package_info(self):
    self.cpp_info.name = "pkgB"
    self.cpp_info.components["pkgB_lib"].name = "pkgB_lib"
    self.cpp_info.components["pkgB_lib"].includes = ["include"]
    self.cpp_info.components["pkgB_lib"].requires = ["pkgA::pkgA_lib"]

recipes/pkgC/conanfile.py

from conans import ConanFile

class PkgC(ConanFile):
  name = "pkgC"
  settings = "os", "arch", "compiler", "build_type"
  requires = "pkgB/1.0"

  def package_info(self):
    self.cpp_info.name = "pkgC"
    self.cpp_info.components["pkgC_lib"].name = "pkgC_lib"
    self.cpp_info.components["pkgC_lib"].requires = ["pkgB::pkgB_lib"]

recipes/patches/pkgA.patch

--- a/recipes/pkgA/conanfile.py
+++ b/recipes/pkgA/conanfile.py
@@ -10,3 +10,4 @@ class PkgA(ConanFile):
     self.cpp_info.name = "pkgA"
     self.cpp_info.components["pkgA_lib"].name = "pkgA_lib"
     self.cpp_info.components["pkgA_lib"].includes = ["include"]
+    self.cpp_info.components["pkgA_lib"].defines = ["PKGA_PUBLIC_DEFINE"]

generator/conanfile.py

from conans.model import Generator
from conans.model.conan_generator import GeneratorComponentsMixin
from conans import ConanFile


class metadata(Generator, GeneratorComponentsMixin):
  name = "metadata-generator-impl"

  @property
  def filename(self):
    return "metadata.json"

  @property
  def content(self):
    metadata = ""
    for pkg_name, tmp in self.deps_build_info.dependencies:
        cpp_info = self.deps_build_info.__getitem__(pkg_name)
        if cpp_info:
            self._validate_components(cpp_info)
            components = self._get_components(cpp_info.name, cpp_info)
            components = [component_cpp_info for _, component_cpp_info, _ in components]
            for component in components:
                metadata += f"name={component.name}\tdefines={component.defines}\n"
    return metadata

class MetadataGenerator(ConanFile):
  name = "metadata-generator"

conanfile.txt

[requires]
pkgB/1.0@
[build_requires]
metadata-generator/1.0

Steps:

$ conan config set general.default_package_id_mode=package_revision_mode 
$ conan config set general.full_transitive_package_id=1 
$ conan config set general.revisions_enabled =1

# first build:
$ # clear conan cache

$ conan create generator 1.0@
$ conan export recipes/pkgA 1.0@
$ conan create recipes/pkgA 1.0@ --profile=gcc82
$ conan get --raw pkgA/1.0@#6c2fca0730865fbf57482deb43c9062f:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9

$ conan export recipes/pkgB 1.0@
$ conan create recipes/pkgB 1.0@ --profile=gcc82
$ conan get --raw pkgB/1.0@#123809c765b8bad4dfc8505b052a4103:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 | sha256sum

$ conan export recipes/pkgC 1.0@
$ conan create recipes/pkgC 1.0@ --profile=gcc82
$ conan get --raw pkgC/1.0@#1d3807edca2b13ae6dccb1f91e5b07b3:3bdd940f0c25858757b4e9abda69765a09365e0d

$ conan install conanfile.txt --generator metadata --profile gcc82 --install-folder first/

# second build:
$ # clear conan cache

$ patch -i $(pwd)/recipes/patches/pkgA.patch -d recipes/pkgA --force

$ conan create generator 1.0@
$ conan export recipes/pkgA 1.0@
$ conan create recipes/pkgA 1.0@ --profile=gcc82
$ conan get --raw pkgA/1.0@#710ff3b2e7fcf71167b8b7a9ba020df8:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9

$ conan export recipes/pkgB 1.0@
$ conan create recipes/pkgB 1.0@ --profile=gcc82
$ conan get --raw pkgB/1.0@#123809c765b8bad4dfc8505b052a4103:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 | sha256sum

$ conan export recipes/pkgC 1.0@
$ conan create recipes/pkgC 1.0@ --profile=gcc82
$ cconan get --raw pkgC/1.0@#1d3807edca2b13ae6dccb1f91e5b07b3:55f61778153282d366ef98c2abd4a9f01653ea4c

$ conan install conanfile.txt --generator metadata --profile gcc82 --install-folder second/

$ diff first/metadata.json second/metadata.json

Result:

pkgC:

The requires field of pkgC was correct:

# firstly built
[requires]
pkgA/1.0#6c2fca0730865fbf57482deb43c9062f:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#ecba8fbd8681f38474db4111a4d63dbc
pkgB/1.0#123809c765b8bad4dfc8505b052a4103:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#be0bcfd43bd2b86ebd3f1c797edcda72

# secondly built
[requires]
pkgA/1.0#710ff3b2e7fcf71167b8b7a9ba020df8:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#af49baea296039974f542fbe69036ca2
pkgB/1.0#123809c765b8bad4dfc8505b052a4103:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9#be0bcfd43bd2b86ebd3f1c797edcda72

pkgB:

705e3e6e36f8192c0cb62a620ea783eafe95b627c4b7a984ef2e203fdbebaa60   # sha256sum for firstly built B
705e3e6e36f8192c0cb62a620ea783eafe95b627c4b7a984ef2e203fdbebaa60   # sha256sum for secondly built B

2c2 # diff
< name=pkgA_lib defines=[]
---
> name=pkgA_lib defines=['PKGA_PUBLIC_DEFINE']


As we can see the reference of pkgB was the same, however the secondly built pkgA has an additional defines which can change the behaviour of that library. Picking up the latest pkgA silently (if our project depends on pkgB directly and not depending on pkgC) is not acceptable for us.

Is there a way to prevent these scenarios?

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions