Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conan/internal/model/conan_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class ConanFile:
default_options = None
default_build_options = None
package_type = None
# For packages that ship artifacts needed at runtime (shared libs, plugins, data) even if
# their ``package_type`` doesn't imply it, like a static-library also containing a shared one
runtime_artifacts = False
vendor = False
languages = []
implements = []
Expand Down
4 changes: 4 additions & 0 deletions conan/internal/model/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ def set_if_none(field, value):
if getattr(self, field) is None:
setattr(self, field, value)

if node.conanfile.runtime_artifacts:
# The dependency ships artifacts needed at runtime, no matter its package_type
set_if_none("_run", True)

if pkg_type is PackageType.APP:
# Change the default requires headers&libs to False for APPS
set_if_none("_headers", False)
Expand Down
26 changes: 26 additions & 0 deletions test/integration/graph/core/graph_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,32 @@ def test_multiple_levels_transitive_headers(self):
(libb, True, True, False, False),
(liba, True, True, False, False)])

@pytest.mark.parametrize("runtime_artifacts", [True, False])
def test_runtime_artifacts_from_static_lib(self, runtime_artifacts):
# app -> libb0.1 (static) -> liba0.1 (static, runtime_artifacts)
liba = GenConanfile("liba", "0.1").with_shared_option(False)
if runtime_artifacts:
liba = liba.with_class_attribute("runtime_artifacts=True")
self.recipe_conanfile("liba/0.1", liba)
self.recipe_cache("libb/0.1", ["liba/0.1"], option_shared=False)
consumer = self.recipe_consumer("app/0.1", ["libb/0.1"])

deps_graph = self.build_consumer(consumer)

assert 3 == len(deps_graph.nodes)
app = deps_graph.root
libb = app.edges[0].dst
liba = libb.edges[0].dst

self._check_node(app, "app/0.1", deps=[libb])
self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
self._check_node(liba, "liba/0.1#123", dependents=[libb])

# node, headers, lib, build, run
_check_transitive(app, [(libb, True, True, False, False),
(liba, False, True, False, runtime_artifacts)])
_check_transitive(libb, [(liba, True, True, False, runtime_artifacts)])


class TestLinearFourLevels(GraphManagerTest):
def test_default(self):
Expand Down
99 changes: 99 additions & 0 deletions test/integration/graph/test_runtime_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import re
import textwrap

import pytest

from conan.test.assets.genconanfile import GenConanfile
from conan.test.utils.tools import TestClient


liba = textwrap.dedent("""\
import os
from conan import ConanFile
from conan.tools.files import save

class LibA(ConanFile):
name = "liba"
version = "0.1"
package_type = "static-library"
runtime_artifacts = {runtime_artifacts}

def package(self):
# This static-library also ships a shared library, faked here with a script, that
# consumers link, and that has to be found at runtime
save(self, os.path.join(self.package_folder, "lib", "liba.a"), "fake static lib")
save(self, os.path.join(self.package_folder, "bin", "myshared.bat"),
"@echo off\\necho MYSHARED RUNTIME!!")
sh = os.path.join(self.package_folder, "bin", "myshared.sh")
save(self, sh, "echo MYSHARED RUNTIME!!")
os.chmod(sh, 0o777)
""")

app = textwrap.dedent("""\
import platform
from conan import ConanFile

class App(ConanFile):
settings = "os" # Necessary, or the "conanrun" env would be a .sh script in Windows
requires = "{requires}"

def build(self):
cmd = "myshared.bat" if platform.system() == "Windows" else "myshared.sh"
self.run(cmd, env="conanrun")
""")


def _not_found(out):
""" the shell couldn't find our script, that is, it was not in the PATH of the run
environment: 'not recognized' in cmd, 'not found' in sh (with or without 'command')
"""
return re.search(r"myshared\.(bat|sh).*(not recognized|not found)", out)


@pytest.mark.parametrize("runtime_artifacts", [True, False])
def test_runtime_artifacts(runtime_artifacts):
""" a package_type=static-library that also contains a shared library, which the consumer
has to find at runtime, so its location must be in the "conanrun" environment
"""
c = TestClient(light=True)
c.save({"liba/conanfile.py": liba.format(runtime_artifacts=runtime_artifacts),
"app/conanfile.py": app.format(requires="liba/0.1")})
c.run("create liba")

c.run("build app", assert_error=not runtime_artifacts)
if runtime_artifacts:
assert "MYSHARED RUNTIME!!" in c.out
else:
# Without "runtime_artifacts" the run trait is False, as it is a static-library, so the
# "bindirs" of liba are not added to the "conanrun" environment
assert "MYSHARED RUNTIME!!" not in c.out
assert _not_found(c.out)
assert "Error in build() method" in c.out


@pytest.mark.parametrize("runtime_artifacts", [True, False])
def test_runtime_artifacts_transitive(runtime_artifacts):
""" app -> libb (shared-library) -> liba (static-library shipping a shared library)
The libs of liba are linked inside libb, so app doesn't need them, but the shared library
that liba ships still has to be there at runtime, so its binary cannot be skipped
"""
c = TestClient(light=True)
c.save({"liba/conanfile.py": liba.format(runtime_artifacts=runtime_artifacts),
"libb/conanfile.py": GenConanfile("libb", "0.1").with_package_type("shared-library")
.with_requires("liba/0.1"),
"app/conanfile.py": app.format(requires="libb/0.1")})
c.run("create liba")
c.run("create libb")

c.run("build app", assert_error=not runtime_artifacts)
if runtime_artifacts:
# The liba binary is necessary at runtime, so it cannot be skipped
assert "Skipped binaries" not in c.out
assert "MYSHARED RUNTIME!!" in c.out
else:
# Without "runtime_artifacts" the run trait is False, and as liba is a static-library
# linked inside libb, Conan understands its binary is not necessary anymore and skips it
assert re.search(r"Skipped binaries(\s*)liba/0.1", c.out)
assert "MYSHARED RUNTIME!!" not in c.out
assert _not_found(c.out)
assert "Error in build() method" in c.out
Loading