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

graph build-order --order=configuration #15270

Merged
Merged
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
13 changes: 8 additions & 5 deletions conan/cli/commands/graph.py
Expand Up @@ -56,6 +56,8 @@ def graph_build_order(conan_api, parser, subparser, *args):
Compute the build order of a dependency graph.
"""
common_graph_args(subparser)
subparser.add_argument("--order", choices=['recipe', 'configuration'], default="recipe",
help='Select which order method')
args = parser.parse_args(*args)

# parameter validation
Expand Down Expand Up @@ -93,7 +95,7 @@ def graph_build_order(conan_api, parser, subparser, *args):

out = ConanOutput()
out.title("Computing the build order")
install_graph = InstallGraph(deps_graph)
install_graph = InstallGraph(deps_graph, order=args.order)
install_order_serialized = install_graph.install_build_order()

lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages,
Expand All @@ -110,11 +112,12 @@ def graph_build_order_merge(conan_api, parser, subparser, *args):
"""
subparser.add_argument("--file", nargs="?", action="append", help="Files to be merged")
args = parser.parse_args(*args)
if not args.file or len(args.file) < 2:
raise ConanException("At least 2 files are needed to be merged")

result = InstallGraph()
for f in args.file:
f = make_abs_path(f)
install_graph = InstallGraph.load(f)
result = InstallGraph.load(make_abs_path(args.file[0]))
for f in args.file[1:]:
install_graph = InstallGraph.load(make_abs_path(f))
result.merge(install_graph)

install_order_serialized = result.install_build_order()
Expand Down
133 changes: 126 additions & 7 deletions conans/client/graph/install_graph.py
Expand Up @@ -193,13 +193,123 @@ def deserialize(data, filename):
return result


class _InstallConfiguration:
""" Represents a single, unique PackageReference to be downloaded, built, etc.
Same PREF should only be built or downloaded once, but it is possible to have multiple
nodes in the DepsGraph that share the same PREF.
PREF could have PREV if to be downloaded (must be the same for all), but won't if to be built
"""
def __init__(self):
self.ref = None
self.package_id = None
self.prev = None
self.nodes = [] # GraphNode
self.binary = None # The action BINARY_DOWNLOAD, etc must be the same for all nodes
self.context = None # Same PREF could be in both contexts, but only 1 context is enough to
# be able to reproduce, typically host preferrably
self.options = [] # to be able to fire a build, the options will be necessary
self.filenames = [] # The build_order.json filenames e.g. "windows_build_order"
self.depends = [] # List of full prefs
self.overrides = Overrides()

@property
def pref(self):
return PkgReference(self.ref, self.package_id, self.prev)

@property
def conanfile(self):
return self.nodes[0].conanfile

@staticmethod
def create(node):
result = _InstallConfiguration()
result.ref = node.ref
result.package_id = node.pref.package_id
result.prev = node.pref.revision
result.binary = node.binary
result.context = node.context
# self_options are the minimum to reproduce state
result.options = node.conanfile.self_options.dumps().splitlines()
result.overrides = node.overrides()

result.nodes.append(node)
for dep in node.dependencies:
if dep.dst.binary != BINARY_SKIP:
if dep.dst.pref not in result.depends:
result.depends.append(dep.dst.pref)
return result

def add(self, node):
assert self.package_id == node.package_id, f"{self.pref}!={node.pref}"
assert self.binary == node.binary, f"Binary for {node}: {self.binary}!={node.binary}"
assert self.prev == node.prev
# The context might vary, but if same package_id, all fine
# assert self.context == node.context
self.nodes.append(node)

for dep in node.dependencies:
if dep.dst.binary != BINARY_SKIP:
if dep.dst.pref not in self.depends:
self.depends.append(dep.dst.pref)

def _build_args(self):
if self.binary != BINARY_BUILD:
return None
cmd = f"--require={self.ref}" if self.context == "host" else f"--tool-require={self.ref}"
cmd += f" --build={self.ref}"
if self.options:
cmd += " " + " ".join(f"-o {o}" for o in self.options)
if self.overrides:
cmd += f' --lockfile-overrides="{self.overrides}"'
return cmd

def serialize(self):
return {"ref": self.ref.repr_notime(),
"pref": self.pref.repr_notime(),
"package_id": self.pref.package_id,
"prev": self.pref.revision,
"context": self.context,
"binary": self.binary,
"options": self.options,
"filenames": self.filenames,
"depends": [d.repr_notime() for d in self.depends],
"overrides": self.overrides.serialize(),
"build_args": self._build_args()
}

@staticmethod
def deserialize(data, filename):
result = _InstallConfiguration()
result.ref = RecipeReference.loads(data["ref"])
result.package_id = data["package_id"]
result.prev = data["prev"]
result.binary = data["binary"]
result.context = data["context"]
result.options = data["options"]
result.filenames = data["filenames"] or [filename]
result.depends = [PkgReference.loads(p) for p in data["depends"]]
result.overrides = Overrides.deserialize(data["overrides"])
return result

def merge(self, other):
assert self.ref == other.ref
for d in other.depends:
if d not in self.depends:
self.depends.append(d)

for d in other.filenames:
if d not in self.filenames:
self.filenames.append(d)


class InstallGraph:
""" A graph containing the package references in order to be built/downloaded
"""

def __init__(self, deps_graph=None):
def __init__(self, deps_graph, order="recipe"):
self._nodes = {} # ref with rev: _InstallGraphNode

self._order = order
self._node_cls = _InstallRecipeReference if order == "recipe" else _InstallConfiguration
self._is_test_package = False
if deps_graph is not None:
self._initialize_deps_graph(deps_graph)
Expand All @@ -217,6 +327,8 @@ def merge(self, other):
"""
@type other: InstallGraph
"""
if self._order != other._order:
raise ConanException(f"Cannot merge build-orders of `{self._order}!={other._order}")
for ref, install_node in other._nodes.items():
existing = self._nodes.get(ref)
if existing is None:
Expand All @@ -226,21 +338,28 @@ def merge(self, other):

@staticmethod
def deserialize(data, filename):
result = InstallGraph()
# Automatic deduction of the order based on the data
try:
order = "recipe" if "packages" in data[0][0] else "configuration"
except IndexError:
order = "recipe"
result = InstallGraph(None, order=order)
for level in data:
for item in level:
elem = _InstallRecipeReference.deserialize(item, filename)
result._nodes[elem.ref] = elem
elem = result._node_cls.deserialize(item, filename)
key = elem.ref if order == "recipe" else elem.pref
result._nodes[key] = elem
return result

def _initialize_deps_graph(self, deps_graph):
for node in deps_graph.ordered_iterate():
if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL) or node.binary == BINARY_SKIP:
continue

existing = self._nodes.get(node.ref)
key = node.ref if self._order == "recipe" else node.pref
existing = self._nodes.get(key)
if existing is None:
self._nodes[node.ref] = _InstallRecipeReference.create(node)
self._nodes[key] = self._node_cls.create(node)
else:
existing.add(node)

Expand Down
126 changes: 126 additions & 0 deletions conans/test/integration/command_v2/test_info_build_order.py
Expand Up @@ -62,6 +62,55 @@ def test_info_build_order():
assert bo_json == result


def test_info_build_order_configuration():
c = TestClient()
c.save({"dep/conanfile.py": GenConanfile(),
"pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"),
"consumer/conanfile.txt": "[requires]\npkg/0.1"})
c.run("export dep --name=dep --version=0.1")
c.run("export pkg --name=pkg --version=0.1")
c.run("graph build-order consumer --build=missing --order=configuration --format=json")
bo_json = json.loads(c.stdout)

result = [
[
{
"ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd",
"pref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709",
"depends": [],
"package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
'prev': None,
'filenames': [],
"context": "host",
"binary": "Build",
'build_args': '--require=dep/0.1 --build=dep/0.1',
"options": [],
"overrides": {}
}
],
[
{
"ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032",
"pref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1",
"depends": [
"dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709"
],
"package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1",
'prev': None,
'filenames': [],
"context": "host",
"binary": "Build",
'build_args': '--require=pkg/0.1 --build=pkg/0.1',
"options": [],
"overrides": {}

}
]
]

assert bo_json == result


def test_info_build_order_build_require():
c = TestClient()
c.save({"dep/conanfile.py": GenConanfile(),
Expand Down Expand Up @@ -252,6 +301,83 @@ def test_info_build_order_merge_multi_product():
assert bo_json == result


def test_info_build_order_merge_multi_product_configurations():
c = TestClient()
c.save({"dep/conanfile.py": GenConanfile(),
"pkg/conanfile.py": GenConanfile().with_requires("dep/0.1"),
"consumer1/conanfile.txt": "[requires]\npkg/0.1",
"consumer2/conanfile.txt": "[requires]\npkg/0.2"})
c.run("export dep --name=dep --version=0.1")
c.run("export pkg --name=pkg --version=0.1")
c.run("export pkg --name=pkg --version=0.2")
c.run("graph build-order consumer1 --build=missing --order=configuration --format=json",
redirect_stdout="bo1.json")
c.run("graph build-order consumer2 --build=missing --order=configuration --format=json",
redirect_stdout="bo2.json")
c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=json",
redirect_stdout="bo3.json")

bo_json = json.loads(c.load("bo3.json"))
result = [
[
{
"ref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd",
"pref": "dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709",
"package_id": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
"prev": None,
"context": "host",
"binary": "Build",
"options": [],
"filenames": [
"bo1",
"bo2"
],
"depends": [],
"overrides": {},
"build_args": "--require=dep/0.1 --build=dep/0.1"
}
],
[
{
"ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032",
"pref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1",
"package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1",
"prev": None,
"context": "host",
"binary": "Build",
"options": [],
"filenames": [
"bo1"
],
"depends": [
"dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709"
],
"overrides": {},
"build_args": "--require=pkg/0.1 --build=pkg/0.1"
},
{
"ref": "pkg/0.2#1ac8dd17c0f9f420935abd3b6a8fa032",
"pref": "pkg/0.2#1ac8dd17c0f9f420935abd3b6a8fa032:59205ba5b14b8f4ebc216a6c51a89553021e82c1",
"package_id": "59205ba5b14b8f4ebc216a6c51a89553021e82c1",
"prev": None,
"context": "host",
"binary": "Build",
"options": [],
"filenames": [
"bo2"
],
"depends": [
"dep/0.1#4d670581ccb765839f2239cc8dff8fbd:da39a3ee5e6b4b0d3255bfef95601890afd80709"
],
"overrides": {},
"build_args": "--require=pkg/0.2 --build=pkg/0.2"
}
]
]

assert bo_json == result


def test_info_build_order_merge_conditionals():
c = TestClient()
conanfile = textwrap.dedent("""
Expand Down