Skip to content

Commit 04b9763

Browse files
memshardedperseoGIAbrilRBS
authored
Package vendor new feature (#16073)
* wip * wip * wip * fix * fix test * Updated graph serialization and display for re-package nodes * Fix broken tests * Make graph command check tools.graph:repackage is passed when trying to compute graph on a repackage dependency * Fix integration test, changed error output * renamed to bundle * fixes * fix test * rename conf to build_bundle * renames * avoid propagate options * Update conans/client/graph/graph.py Co-authored-by: Rubén Rincón Blanco <git@rinconblanco.es> --------- Co-authored-by: PerseoGI <perseog@jfrog.com> Co-authored-by: Rubén Rincón Blanco <git@rinconblanco.es>
1 parent ac7c6ad commit 04b9763

File tree

11 files changed

+214
-8
lines changed

11 files changed

+214
-8
lines changed

conan/cli/commands/create.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def create(conan_api, parser, *args):
9595
else:
9696
requires = [ref] if not is_build else None
9797
tool_requires = [ref] if is_build else None
98+
if conanfile.vendor: # Automatically allow repackaging for conan create
99+
pr = profile_build if is_build else profile_host
100+
pr.conf.update("&:tools.graph:vendor", "build")
98101
deps_graph = conan_api.graph.load_graph_requires(requires, tool_requires,
99102
profile_host=profile_host,
100103
profile_build=profile_build,

conan/cli/formatters/graph/info_graph_html.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@
144144
if (node.recipe == "Platform") {
145145
font.background = "Violet";
146146
}
147+
if (node.vendor) {
148+
borderColor = "Red";
149+
shapeProperties = {borderDashes: [3,5]};
150+
borderWidth = 2;
151+
}
147152
nodes.push({
148153
id: node_id,
149154
font: font,
@@ -225,6 +230,14 @@
225230
font: {size: 35, color: "white"},
226231
color: {border: "SkyBlue", background: "Black"}
227232
});
233+
counter++;
234+
235+
legend_nodes.push({x: x + counter*step, y: y, shape: "box",
236+
label: "vendor", font: {size: 35},
237+
color: {border: "Red"},
238+
shapeProperties: {borderDashes: [3,5]},
239+
borderWidth: 2
240+
});
228241
return {nodes: new vis.DataSet(legend_nodes)};
229242
}
230243
let error = document.getElementById("error");

conans/client/graph/compute_pid.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def compute_package_id(node, new_config, config_version):
3838
else:
3939
data[require] = req_info
4040

41+
if conanfile.vendor: # Make the package_id fully independent of dependencies versions
42+
data, build_data = OrderedDict(), OrderedDict() # TODO, cleaner, now minimal diff
43+
4144
reqs_info = RequirementsInfo(data)
4245
build_requires_info = RequirementsInfo(build_data)
4346
python_requires = PythonRequiresInfo(python_requires, python_mode)

conans/client/graph/graph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def propagate_downstream(self, require, node, src_node=None):
108108
self.transitive_deps.pop(require, None)
109109
self.transitive_deps[require] = TransitiveRequirement(require, node)
110110

111+
if self.conanfile.vendor:
112+
return
111113
# Check if need to propagate downstream
112114
if not self.dependants:
113115
return
@@ -154,6 +156,8 @@ def check_downstream_exists(self, require):
154156
# Check if need to propagate downstream
155157
# Then propagate downstream
156158

159+
if self.conanfile.vendor:
160+
return result
157161
# Seems the algrithm depth-first, would only have 1 dependant at most to propagate down
158162
# at any given time
159163
if not self.dependants:

conans/client/graph/graph_binaries.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,15 @@ def _evaluate_node(self, node, build_mode, remotes, update):
181181
node.build_allowed = True
182182
node.binary = BINARY_BUILD if not node.cant_build else BINARY_INVALID
183183

184+
if node.binary == BINARY_BUILD:
185+
conanfile = node.conanfile
186+
if conanfile.vendor and not conanfile.conf.get("tools.graph:vendor", choices=("build",)):
187+
node.conanfile.info.invalid = f"The package '{conanfile.ref}' is a vendoring one, " \
188+
f"needs to be built from source, but it " \
189+
"didn't enable 'tools.graph:vendor=build' to compute " \
190+
"its dependencies"
191+
node.binary = BINARY_INVALID
192+
184193
def _process_node(self, node, build_mode, remotes, update):
185194
# Check that this same reference hasn't already been checked
186195
if self._evaluate_is_cached(node):

conans/client/graph/graph_builder.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from conan.internal.cache.conan_reference_layout import BasicLayout
66
from conans.client.conanfile.configure import run_configure_method
77
from conans.client.graph.graph import DepsGraph, Node, CONTEXT_HOST, \
8-
CONTEXT_BUILD, TransitiveRequirement, RECIPE_VIRTUAL
8+
CONTEXT_BUILD, TransitiveRequirement, RECIPE_VIRTUAL, RECIPE_EDITABLE
99
from conans.client.graph.graph import RECIPE_PLATFORM
1010
from conans.client.graph.graph_error import GraphLoopError, GraphConflictError, GraphMissingError, \
1111
GraphRuntimeError, GraphError
@@ -52,7 +52,10 @@ def load_graph(self, root_node, profile_host, profile_build, graph_lock=None):
5252
continue
5353
new_node = self._expand_require(require, node, dep_graph, profile_host,
5454
profile_build, graph_lock)
55-
if new_node:
55+
if new_node and (not new_node.conanfile.vendor
56+
or new_node.recipe == RECIPE_EDITABLE or
57+
new_node.conanfile.conf.get("tools.graph:vendor",
58+
choices=("build",))):
5659
self._initialize_requires(new_node, dep_graph, graph_lock, profile_build,
5760
profile_host)
5861
open_requires.extendleft((r, new_node)
@@ -386,6 +389,7 @@ def _create_new_node(self, node, require, graph, profile_host, profile_build, gr
386389
@staticmethod
387390
def _compute_down_options(node, require, new_ref):
388391
# The consumer "up_options" are the options that come from downstream to this node
392+
visible = require.visible and not node.conanfile.vendor
389393
if require.options is not None:
390394
# If the consumer has specified "requires(options=xxx)", we need to use it
391395
# It will have less priority than downstream consumers
@@ -395,11 +399,11 @@ def _compute_down_options(node, require, new_ref):
395399
# options["dep"].opt=value only propagate to visible and host dependencies
396400
# we will evaluate if necessary a potential "build_options", but recall that it is
397401
# now possible to do "self.build_requires(..., options={k:v})" to specify it
398-
if require.visible:
402+
if visible:
399403
# Only visible requirements in the host context propagate options from downstream
400404
down_options.update_options(node.conanfile.up_options)
401405
else:
402-
if require.visible:
406+
if visible:
403407
down_options = node.conanfile.up_options
404408
elif not require.build: # for requires in "host", like test_requires, pass myoptions
405409
down_options = node.conanfile.private_up_options

conans/model/conan_file.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ class ConanFile:
4747
default_options = None
4848
default_build_options = None
4949
package_type = None
50+
vendor = False
5051
languages = []
51-
5252
implements = []
5353

5454
provides = None
@@ -170,6 +170,7 @@ def serialize(self):
170170
result["label"] = self.display_name
171171
if self.info is not None:
172172
result["info"] = self.info.serialize()
173+
result["vendor"] = self.vendor
173174
return result
174175

175176
@property

conans/model/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"tools.files.download:retry": "Number of retries in case of failure when downloading",
8484
"tools.files.download:retry_wait": "Seconds to wait between download attempts",
8585
"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",
86+
"tools.graph:vendor": "(Experimental) If 'build', enables the computation of dependencies of vendoring packages to build them",
8687
"tools.graph:skip_binaries": "Allow the graph to skip binaries not needed in the current configuration (True by default)",
8788
"tools.gnu:make_program": "Indicate path to make program",
8889
"tools.gnu:define_libcxx11_abi": "Force definition of GLIBCXX_USE_CXX11_ABI=1 for libstdc++11",

test/integration/command/info/info_test.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from conan.cli.exit_codes import ERROR_GENERAL
66
from conans.model.recipe_ref import RecipeReference
7-
from conan.test.utils.tools import TestClient, GenConanfile, TurboTestClient
7+
from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, GenConanfile, TurboTestClient
88

99

1010
class TestBasicCliOutput:
@@ -425,3 +425,27 @@ class Pkg(ConanFile):
425425
assert "pkg/0.1@user" in c.out
426426
c.run("graph info . --channel=channel")
427427
assert "pkg/0.1@user/channel" in c.out
428+
429+
430+
def test_graph_info_bundle():
431+
c = TestClient(light=True)
432+
c.save({"subfolder/conanfile.py": GenConanfile("liba", "1.0")})
433+
c.run("create ./subfolder")
434+
conanfile = textwrap.dedent("""
435+
from conan import ConanFile
436+
class RepackageRecipe(ConanFile):
437+
name = "lib"
438+
version = "1.0"
439+
def requirements(self):
440+
self.requires("liba/1.0")
441+
vendor = True
442+
""")
443+
c.save({"conanfile.py": conanfile})
444+
c.run("create .")
445+
c.save({"conanfile.py": GenConanfile("consumer", "1.0").with_requires("lib/1.0")})
446+
447+
c.run("graph info . --build='lib*'")
448+
c.assert_listed_binary({"lib/1.0": (NO_SETTINGS_PACKAGE_ID, "Invalid")})
449+
450+
c.run("graph info . -c tools.graph:vendor=build --build='lib*'")
451+
c.assert_listed_binary({"lib/1.0": (NO_SETTINGS_PACKAGE_ID, "Build")})

test/integration/command_v2/test_inspect.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ def test_basic_inspect():
2222
" shared: ['True', 'False']",
2323
'package_type: None',
2424
'requires: []',
25-
'revision_mode: hash']
25+
'revision_mode: hash',
26+
'vendor: False'
27+
]
2628

2729

2830
def test_options_description():
@@ -92,6 +94,7 @@ def test_normal_inspect():
9294
'package_type: None',
9395
'requires: []',
9496
'revision_mode: hash',
97+
'vendor: False',
9598
'version: 1.0']
9699

97100

@@ -137,7 +140,8 @@ class Pkg(ConanFile):
137140
"False, 'test': False, 'force': False, 'direct': True, 'build': "
138141
"False, 'transitive_headers': None, 'transitive_libs': None, 'headers': "
139142
"True, 'package_id_mode': None, 'visible': True}]",
140-
'revision_mode: hash'] == tc.out.splitlines()
143+
'revision_mode: hash',
144+
'vendor: False'] == tc.out.splitlines()
141145

142146

143147
def test_pythonrequires_remote():
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import os
2+
import textwrap
3+
4+
from conan.test.assets.genconanfile import GenConanfile
5+
from conan.test.utils.tools import TestClient
6+
7+
8+
def test_package_vendor():
9+
c = TestClient()
10+
app = textwrap.dedent("""
11+
import os
12+
from conan import ConanFile
13+
from conan.tools.files import copy, save
14+
15+
class App(ConanFile):
16+
name = "app"
17+
version = "0.1"
18+
package_type = "application"
19+
vendor = True
20+
requires = "pkga/0.1"
21+
def package(self):
22+
copy(self, "*", src=self.dependencies["pkga"].package_folder,
23+
dst=self.package_folder)
24+
save(self, os.path.join(self.package_folder, "app.exe"), "app")
25+
""")
26+
27+
c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
28+
.with_package_file("pkga.dll", "dll"),
29+
"app/conanfile.py": app
30+
})
31+
c.run("create pkga")
32+
c.run("create app") # -c tools.graph:vendor=build will be automatic
33+
assert "app/0.1: package(): Packaged 1 '.dll' file: pkga.dll" in c.out
34+
35+
# we can safely remove pkga
36+
c.run("remove pkg* -c")
37+
c.run("list app:*")
38+
assert "pkga" not in c.out # The binary doesn't depend on pkga
39+
c.run("install --requires=app/0.1 --deployer=full_deploy")
40+
assert "pkga" not in c.out
41+
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
42+
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"
43+
44+
# we can create a modified pkga
45+
c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
46+
.with_package_file("pkga.dll", "newdll")})
47+
c.run("create pkga")
48+
# still using the re-packaged one
49+
c.run("install --requires=app/0.1 --deployer=full_deploy")
50+
assert "pkga" not in c.out
51+
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
52+
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"
53+
54+
# but we can force the expansion, still not the rebuild
55+
c.run("install --requires=app/0.1 --deployer=full_deploy -c tools.graph:vendor=build")
56+
assert "pkga" in c.out
57+
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
58+
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"
59+
60+
# and finally we can force the expansion and the rebuild
61+
c.run("install --requires=app/0.1 --build=app* --deployer=full_deploy "
62+
"-c tools.graph:vendor=build")
63+
assert "pkga" in c.out
64+
assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
65+
assert c.load("full_deploy/host/app/0.1/pkga.dll") == "newdll"
66+
# This shoulnd't happen, no visibility over transitive dependencies of app
67+
assert not os.path.exists(os.path.join(c.current_folder, "full_deploy", "host", "pkga"))
68+
69+
# lets remove the binary
70+
c.run("remove app:* -c")
71+
c.run("install --requires=app/0.1", assert_error=True)
72+
assert "Missing binary" in c.out
73+
c.run("install --requires=app/0.1 --build=missing", assert_error=True)
74+
assert "app/0.1: Invalid: The package 'app/0.1' is a vendoring one, needs to be built " \
75+
"from source, but it didn't enable 'tools.graph:vendor=build'" in c.out
76+
77+
c.run("install --requires=app/0.1 --build=missing -c tools.graph:vendor=build")
78+
assert "pkga" in c.out # it works
79+
80+
81+
def test_package_vendor_editable():
82+
c = TestClient()
83+
pkgb = textwrap.dedent("""
84+
import os
85+
from conan import ConanFile
86+
from conan.tools.files import copy, save
87+
88+
class App(ConanFile):
89+
name = "pkgb"
90+
version = "0.1"
91+
package_type = "shared-library"
92+
vendor = True
93+
requires = "pkga/0.1"
94+
def layout(self):
95+
self.folders.build = "build"
96+
self.cpp.build.bindirs = ["build"]
97+
def generate(self):
98+
copy(self, "*", src=self.dependencies["pkga"].package_folder,
99+
dst=self.build_folder)
100+
def build(self):
101+
save(self, os.path.join(self.build_folder, "pkgb.dll"), "dll")
102+
""")
103+
104+
c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
105+
.with_package_file("bin/pkga.dll", "d"),
106+
"pkgb/conanfile.py": pkgb,
107+
"app/conanfile.py": GenConanfile("app", "0.1").with_settings("os")
108+
.with_requires("pkgb/0.1")
109+
})
110+
c.run("create pkga")
111+
c.run("editable add pkgb")
112+
c.run("install app -s os=Linux")
113+
assert "pkga" in c.out
114+
# The environment file of "app" doesn't have any visibility of the "pkga" paths
115+
envfile_app = c.load("app/conanrunenv.sh")
116+
assert "pkga" not in envfile_app
117+
# But the environment file needed to build "pkgb" has visibility over the "pkga" paths
118+
envfile_pkgb = c.load("pkgb/conanrunenv.sh")
119+
assert "pkga" in envfile_pkgb
120+
121+
122+
def test_vendor_dont_propagate_options():
123+
c = TestClient()
124+
app = GenConanfile("app", "0.1").with_requires("pkga/0.1").with_class_attribute("vendor=True")
125+
c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_shared_option(False),
126+
"app/conanfile.py": app,
127+
"consumer/conanfile.txt": "[requires]\napp/0.1",
128+
"consumer_shared/conanfile.txt": "[requires]\napp/0.1\n[options]\n*:shared=True"
129+
})
130+
c.run("create pkga")
131+
c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Build")})
132+
c.run("create app")
133+
c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"),
134+
"app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")})
135+
c.run("install consumer --build=app/* -c tools.graph:vendor=build")
136+
c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"),
137+
"app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")})
138+
c.run("install consumer_shared --build=app/* -c tools.graph:vendor=build")
139+
c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"),
140+
"app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")})

0 commit comments

Comments
 (0)