Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repackage strategy #16073

Draft
wants to merge 6 commits into
base: develop2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions conan/cli/commands/create.py
Expand Up @@ -95,6 +95,9 @@ def create(conan_api, parser, *args):
else:
requires = [ref] if not is_build else None
tool_requires = [ref] if is_build else None
if conanfile.repackage: # Automatically allow repackaging for conan create
pr = profile_build if is_build else profile_host
pr.conf.update("&:tools.graph:repackage", True)
deps_graph = conan_api.graph.load_graph_requires(requires, tool_requires,
profile_host=profile_host,
profile_build=profile_build,
Expand Down
3 changes: 3 additions & 0 deletions conans/client/graph/compute_pid.py
Expand Up @@ -38,6 +38,9 @@ def compute_package_id(node, new_config, config_version):
else:
data[require] = req_info

if conanfile.repackage: # Make the package_id fully independent of dependencies versions
data, build_data = OrderedDict(), OrderedDict() # TODO, cleaner, now minimal diff

reqs_info = RequirementsInfo(data)
build_requires_info = RequirementsInfo(build_data)
python_requires = PythonRequiresInfo(python_requires, python_mode)
Expand Down
4 changes: 4 additions & 0 deletions conans/client/graph/graph.py
Expand Up @@ -108,6 +108,8 @@ def propagate_downstream(self, require, node, src_node=None):
self.transitive_deps.pop(require, None)
self.transitive_deps[require] = TransitiveRequirement(require, node)

if self.conanfile.repackage:
return
# Check if need to propagate downstream
if not self.dependants:
return
Expand Down Expand Up @@ -156,6 +158,8 @@ def check_downstream_exists(self, require):

# Seems the algrithm depth-first, would only have 1 dependant at most to propagate down
# at any given time
if self.conanfile.repackage:
return result
if not self.dependants:
return result
assert len(self.dependants) == 1
Expand Down
6 changes: 4 additions & 2 deletions conans/client/graph/graph_builder.py
Expand Up @@ -5,7 +5,7 @@
from conan.internal.cache.conan_reference_layout import BasicLayout
from conans.client.conanfile.configure import run_configure_method
from conans.client.graph.graph import DepsGraph, Node, CONTEXT_HOST, \
CONTEXT_BUILD, TransitiveRequirement, RECIPE_VIRTUAL
CONTEXT_BUILD, TransitiveRequirement, RECIPE_VIRTUAL, RECIPE_EDITABLE
from conans.client.graph.graph import RECIPE_PLATFORM
from conans.client.graph.graph_error import GraphLoopError, GraphConflictError, GraphMissingError, \
GraphRuntimeError, GraphError
Expand Down Expand Up @@ -52,7 +52,9 @@ def load_graph(self, root_node, profile_host, profile_build, graph_lock=None):
continue
new_node = self._expand_require(require, node, dep_graph, profile_host,
profile_build, graph_lock)
if new_node:
if new_node and (not new_node.conanfile.repackage
or new_node.recipe == RECIPE_EDITABLE or
new_node.conanfile.conf.get("tools.graph:repackage", check_type=bool)):
self._initialize_requires(new_node, dep_graph, graph_lock, profile_build,
profile_host)
open_requires.extendleft((r, new_node)
Expand Down
7 changes: 7 additions & 0 deletions conans/client/graph/install_graph.py
Expand Up @@ -462,6 +462,13 @@ def raise_errors(self):
missing.append(package)
elif package.binary == BINARY_INVALID:
invalid.append(package)
elif package.binary == BINARY_BUILD:
conanfile = package.nodes[0].conanfile
if conanfile.repackage and not conanfile.conf.get("tools.graph:repackage",
check_type=bool):
msg = f"The package '{ref}' is repackaging and building but it "\
"didn't enable 'tools.graph:repackage' to compute its dependencies"
raise ConanException(msg)

if invalid:
msg = ["There are invalid packages:"]
Expand Down
1 change: 1 addition & 0 deletions conans/model/conan_file.py
Expand Up @@ -47,6 +47,7 @@ class ConanFile:
default_options = None
default_build_options = None
package_type = None
repackage = False

implements = []

Expand Down
1 change: 1 addition & 0 deletions conans/model/conf.py
Expand Up @@ -83,6 +83,7 @@
"tools.files.download:retry_wait": "Seconds to wait between download attempts",
"tools.files.download:verify": "If set, overrides recipes on whether to perform SSL verification for their downloaded files. Only recommended to be set while testing",
"tools.graph:skip_binaries": "Allow the graph to skip binaries not needed in the current configuration (True by default)",
"tools.graph:repackage": "enable re-packaging",
"tools.gnu:make_program": "Indicate path to make program",
"tools.gnu:define_libcxx11_abi": "Force definition of GLIBCXX_USE_CXX11_ABI=1 for libstdc++11",
"tools.gnu:pkg_config": "Path to pkg-config executable used by PkgConfig build helper",
Expand Down
118 changes: 118 additions & 0 deletions test/integration/test_repackage.py
@@ -0,0 +1,118 @@
import os
import textwrap

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


def test_repackage():
c = TestClient()
app = textwrap.dedent("""
import os
from conan import ConanFile
from conan.tools.files import copy, save

class App(ConanFile):
name = "app"
version = "0.1"
package_type = "application"
repackage = True
requires = "pkga/0.1"
def package(self):
copy(self, "*", src=self.dependencies["pkga"].package_folder,
dst=self.package_folder)
save(self, os.path.join(self.package_folder, "app.exe"), "app")
""")

c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
.with_package_file("pkga.dll", "dll"),
"app/conanfile.py": app
})
c.run("create pkga")
c.run("create app") # -c tools.graph:repackage=True will be automatic
assert "app/0.1: package(): Packaged 1 '.dll' file: pkga.dll" in c.out

# we can safely remove pkga
c.run("remove pkg* -c")
c.run("list app:*")
assert "pkga" not in c.out # The binary doesn't depend on pkga
c.run("install --requires=app/0.1 --deployer=full_deploy")
assert "pkga" not in c.out
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"

# we can create a modified pkga
c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
.with_package_file("pkga.dll", "newdll")})
c.run("create pkga")
# still using the re-packaged one
c.run("install --requires=app/0.1 --deployer=full_deploy")
assert "pkga" not in c.out
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"

# but we can force the expansion, still not the rebuild
c.run("install --requires=app/0.1 --deployer=full_deploy -c tools.graph:repackage=True")
assert "pkga" in c.out
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"

# and finally we can force the expansion and the rebuild
c.run("install --requires=app/0.1 --build=app* --deployer=full_deploy "
"-c tools.graph:repackage=True")
assert "pkga" in c.out
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "newdll"
# This shoulnd't happen, no visibility over transitive dependencies of app
assert not os.path.exists(os.path.join(c.current_folder, "full_deploy", "host", "pkga"))

# lets remove the binary
c.run("remove app:* -c")
c.run("install --requires=app/0.1", assert_error=True)
assert "Missing binary" in c.out
c.run("install --requires=app/0.1 --build=missing", assert_error=True)
assert "ERROR: The package 'app/0.1' is repackaging and building but it didn't " \
"enable 'tools.graph:repackage'" in c.out
c.run("install --requires=app/0.1 --build=missing -c tools.graph:repackage=True")
assert "pkga" in c.out # it works


def test_repackage_editable():
c = TestClient()
pkgb = textwrap.dedent("""
import os
from conan import ConanFile
from conan.tools.files import copy, save

class App(ConanFile):
name = "pkgb"
version = "0.1"
package_type = "shared-library"
repackage = True
requires = "pkga/0.1"
def layout(self):
self.folders.build = "build"
self.cpp.build.bindirs = ["build"]
def generate(self):
copy(self, "*", src=self.dependencies["pkga"].package_folder,
dst=self.build_folder)
def build(self):
save(self, os.path.join(self.build_folder, "pkgb.dll"), "dll")
""")

c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
.with_package_file("bin/pkga.dll", "d"),
"pkgb/conanfile.py": pkgb,
"app/conanfile.py": GenConanfile("app", "0.1").with_settings("os")
.with_requires("pkgb/0.1")
})
c.run("create pkga")
c.run("editable add pkgb")
c.run("install app -s os=Linux")
assert "pkga" in c.out
# The environment file of "app" doesn't have any visibility of the "pkga" paths
envfile_app = c.load("app/conanrunenv.sh")
assert "pkga" not in envfile_app
# But the environment file needed to build "pkgb" has visibility over the "pkga" paths
envfile_pkgb = c.load("pkgb/conanrunenv.sh")
assert "pkga" in envfile_pkgb