Repository: conan-io/conan
Pull request #18390 (2025-05-30 17:00:13)

Feature: workspace create
Changelog: Feature: New ``conan workspace create`` orchestrated
Changelog: Fix: Removed ``Workspaces`` product definition and make ``conan workspace build`` work computing the right build-order

Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/18364





Patch:
diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py
index 909148e14..1b2c46ee8 100644
--- a/conan/api/conan_api.py
+++ b/conan/api/conan_api.py
@@ -7,6 +7,7 @@ from conan.api.subapi.cache import CacheAPI
 from conan.api.subapi.command import CommandAPI
 from conan.api.subapi.local import LocalAPI
 from conan.api.subapi.lockfile import LockfileAPI
+from conan.api.subapi.report import ReportAPI
 from conan.api.subapi.workspace import WorkspaceAPI
 from conan.api.subapi.config import ConfigAPI
 from conan.api.subapi.download import DownloadAPI
@@ -72,6 +73,7 @@ class ConanAPI:
         self.audit = AuditAPI(self)
         # Now, lazy loading of editables
         self.workspace = WorkspaceAPI(self)
+        self.report = ReportAPI(self)
 
         _check_conan_version(self)
 
diff --git a/conan/api/output.py b/conan/api/output.py
index cf291ec98..535586c00 100644
--- a/conan/api/output.py
+++ b/conan/api/output.py
@@ -175,6 +175,12 @@ class ConanOutput:
 
         return self
 
+    def box(self, msg):
+        color = Color.BRIGHT_GREEN
+        self.writeln("\n**************************************************", fg=color)
+        self.writeln(f'*{msg: ^48}*', fg=color)
+        self.writeln(f"**************************************************\n", fg=color)
+
     def _write_message(self, msg, fg=None, bg=None, newline=True):
         if isinstance(msg, dict):
             # For traces we can receive a dict already, we try to transform then into more natural
diff --git a/conan/api/subapi/command.py b/conan/api/subapi/command.py
index abf5dd9cc..272b5e099 100644
--- a/conan/api/subapi/command.py
+++ b/conan/api/subapi/command.py
@@ -1,4 +1,5 @@
 import os
+import shlex
 
 from conan.api.output import ConanOutput
 from conan.errors import ConanException
@@ -12,7 +13,7 @@ class CommandAPI:
 
     def run(self, cmd):
         if isinstance(cmd, str):
-            cmd = cmd.split()
+            cmd = shlex.split(cmd)
         if isinstance(cmd, list):
             current_cmd = cmd[0]
             args = cmd[1:]
diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py
index f7342e475..78bd3fec5 100644
--- a/conan/api/subapi/config.py
+++ b/conan/api/subapi/config.py
@@ -66,11 +66,11 @@ class ConfigAPI:
 
         # Basic checks of the package: correct package_type and no-dependencies
         deps_graph.report_graph_error()
-        pkg = deps_graph.root.dependencies[0].dst
+        pkg = deps_graph.root.edges[0].dst
         ConanOutput().info(f"Configuration from package: {pkg}")
         if pkg.conanfile.package_type is not PackageType.CONF:
             raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"')
-        if pkg.dependencies:
+        if pkg.edges:
             raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies")
 
         # The computation of the "package_id" and the download of the package is done as usual
diff --git a/conan/api/subapi/report.py b/conan/api/subapi/report.py
new file mode 100644
index 000000000..f1a2a1c8e
--- /dev/null
+++ b/conan/api/subapi/report.py
@@ -0,0 +1,144 @@
+import base64
+import os
+from io import StringIO
+
+from conan.api.output import ConanOutput
+from conan.errors import ConanException
+from conan.api.model import RecipeReference
+from conan.internal.conan_app import ConanApp
+from conan.internal.errors import conanfile_exception_formatter
+from conan.internal.graph.graph import CONTEXT_HOST
+from conan.internal.graph.profile_node_definer import initialize_conanfile_profile
+from conan.internal.source import config_source
+from conan.internal.util.runners import conan_run
+
+
+class ReportAPI:
+    def __init__(self, conan_api):
+        self.conan_api = conan_api
+
+    def diff(self, old_reference, new_reference, remotes, old_path=None, new_path=None, cwd=None):
+        """
+        Compare two recipes and return the differences.
+        :param old_reference: The reference of the old recipe.
+        :param new_reference: The reference of the new recipe.
+        :param remotes: List of remotes to search for the recipes.
+        :param old_path: Optional path to the old recipe's conanfile.py.
+        :param new_path: Optional path to the new recipe's conanfile.py.
+        :param cwd: Current working directory, used to resolve paths.
+        :return: A dictionary with the differences between the two recipes.
+        """
+
+        def _source(path_to_conanfile, reference):
+            if path_to_conanfile is None:
+                export_ref, cache_path = _get_ref_from_cache_or_remote(self.conan_api, reference, remotes)
+            else:
+                export_ref, cache_path = _export_recipe_from_path(self.conan_api, path_to_conanfile,
+                                                                  reference, remotes, cwd)
+            exported_path = self.conan_api.local.get_conanfile_path(cache_path, cwd, py=True)
+            _configure_source(self.conan_api, exported_path, export_ref, remotes)
+            return export_ref, cache_path
+
+
+        old_export_ref, old_cache_path = _source(old_path, old_reference)
+        new_export_ref, new_cache_path = _source(new_path, new_reference)
+
+        old_diff_path = os.path.abspath(os.path.join(old_cache_path, os.path.pardir)).replace("\\",
+                                                                                              "/")
+        new_diff_path = os.path.abspath(os.path.join(new_cache_path, os.path.pardir)).replace("\\",
+                                                                                              "/")
+
+        src_prefix = base64.b64encode(str(new_export_ref.repr_notime()).encode()).decode() + "/"
+        dst_prefix = base64.b64encode(str(old_export_ref.repr_notime()).encode()).decode() + "/"
+
+        command = (f'git diff --no-index --src-prefix "{src_prefix}" --dst-prefix "{dst_prefix}" '
+                   f'"{old_diff_path}" "{new_diff_path}"')
+
+        ConanOutput().info(
+            f"Generating diff from {old_export_ref.repr_notime()} to {new_export_ref.repr_notime()} (this might take a while)")
+        ConanOutput().info(command)
+
+        stdout, stderr = StringIO(), StringIO()
+        conan_run(command, stdout=stdout, stderr=stderr)
+        diff = stdout.getvalue()
+
+        if old_path:
+            self.conan_api.remove.recipe(old_export_ref)
+        if new_path:
+            self.conan_api.remove.recipe(new_export_ref)
+
+        return {
+            "diff": diff,
+            "old_export_ref": old_export_ref,
+            "new_export_ref": new_export_ref,
+            "old_cache_path": old_diff_path,
+            "new_cache_path": new_diff_path,
+            "src_prefix": src_prefix,
+            "dst_prefix": dst_prefix,
+        }
+
+def _configure_source(conan_api, conanfile_path, ref, remotes):
+    app = ConanApp(conan_api)
+    conanfile = app.loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version),
+                                         user=ref.user, channel=ref.channel, graph_lock=None,
+                                         remotes=remotes)
+    # This profile is empty, but with the conf from global.conf
+    profile = conan_api.profiles.get_profile([])
+    initialize_conanfile_profile(conanfile, profile, profile, CONTEXT_HOST, False)
+    # This is important, otherwise the ``conan source`` doesn't define layout and fails
+    if hasattr(conanfile, "layout"):
+        with conanfile_exception_formatter(conanfile, "layout"):
+            conanfile.layout()
+
+    recipe_layout = app.cache.recipe_layout(ref)
+    export_source_folder = recipe_layout.export_sources()
+    source_folder = recipe_layout.source()
+
+    conanfile.folders.set_base_source(source_folder)
+    conanfile.folders.set_base_export_sources(export_source_folder)
+    conanfile.folders.set_base_recipe_metadata(recipe_layout.metadata())
+    config_source(export_source_folder, conanfile, conan_api.config.hook_manager)
+
+def _get_ref_from_cache_or_remote(conan_api, reference, enabled_remotes):
+    ref = RecipeReference.loads(reference)
+    full_ref, matching_remote = None, False
+    # The first remote is None, which means local cache for the list subapi methods
+    for remote in [None] + enabled_remotes:
+        if ref.revision:
+            no_rrev_ref = RecipeReference.loads(reference)
+            no_rrev_ref.revision = None
+            try:
+                remote_revisions = conan_api.list.recipe_revisions(no_rrev_ref, remote)
+                if ref in remote_revisions:
+                    full_ref = ref
+                    matching_remote = remote
+                    break
+            except:
+                continue
+        else:
+            try:
+                latest_recipe_revision = conan_api.list.latest_recipe_revision(ref, remote)
+            except:
+                continue
+            if full_ref is None or (latest_recipe_revision.timestamp > full_ref.timestamp):
+                full_ref = latest_recipe_revision
+                matching_remote = remote
+    if full_ref is None or matching_remote is False:
+        raise ConanException(f"No matching reference for {reference} in remotes.\n"
+                             "If you want to check against a local recipe, add an additional --{old,new}-path arg.\n")
+    if matching_remote is not None:
+        conan_api.download.recipe(full_ref, matching_remote)
+    cache_path = conan_api.cache.export_path(full_ref)
+    return full_ref, cache_path
+
+
+def _export_recipe_from_path(conan_api, path_to_conanfile, reference, enabled_remotes, cwd=None):
+    path = conan_api.local.get_conanfile_path(path_to_conanfile, cwd, py=True)
+    ref = RecipeReference.loads(reference)
+    export_ref, conanfile = conan_api.export.export(path=path,
+                                                    name=ref.name, version=str(ref.version),
+                                                    user=ref.user, channel=ref.channel,
+                                                    lockfile=None,
+                                                    remotes=enabled_remotes)
+    cache_path = conan_api.cache.export_path(export_ref)
+    return export_ref, cache_path
diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py
index 96e00f77e..25e2cae6e 100644
--- a/conan/api/subapi/workspace.py
+++ b/conan/api/subapi/workspace.py
@@ -8,8 +8,10 @@ from conan import ConanFile
 from conan.api.model import RecipeReference
 from conan.api.output import ConanOutput
 from conan.cli import make_abs_path
+from conan.cli.printers.graph import print_graph_basic, print_graph_packages
 from conan.errors import ConanException
 from conan.internal.conan_app import ConanApp
+from conan.internal.graph.install_graph import ProfileArgs
 from conan.internal.model.workspace import Workspace, WORKSPACE_YML, WORKSPACE_PY, WORKSPACE_FOLDER
 from conan.tools.scm import Git
 from conan.internal.graph.graph import RECIPE_EDITABLE, DepsGraph, CONTEXT_HOST, RECIPE_VIRTUAL, Node, \
@@ -74,6 +76,7 @@ class WorkspaceAPI:
     TEST_ENABLED = False
 
     def __init__(self, conan_api):
+        self._enabled = True
         self._conan_api = conan_api
         self._folder = _find_ws_folder()
         if self._folder:
@@ -85,6 +88,9 @@ class WorkspaceAPI:
                 ConanOutput().warning(f"Workspace is a dev-only feature, exclusively for testing")
                 self._ws = _load_workspace(self._folder, conan_api)  # Error if not loading
 
+    def enable(self, value):
+        self._enabled = value
+
     @property
     def name(self):
         self._check_ws()
@@ -94,14 +100,14 @@ class WorkspaceAPI:
         """
         @return: the current workspace folder where the conanws.yml or conanws.py is located
         """
+        self._check_ws()
         return self._folder
 
-    @property
-    def editable_packages(self):
+    def packages(self):
         """
         @return: Returns {RecipeReference: {"path": full abs-path, "output_folder": abs-path}}
         """
-        if not self._folder:
+        if not self._folder or not self._enabled:
             return
         editables = self._ws.packages()
         editables = {RecipeReference.loads(r): v.copy() for r, v in editables.items()}
@@ -115,21 +121,6 @@ class WorkspaceAPI:
                                                                    v["output_folder"]))
         return editables
 
-    def select_editables(self, paths):
-        filtered_refs = [self.editable_from_path(p) for p in paths or []]
-        editables = self.editable_packages
-        requires = [ref for ref in editables]
-        if filtered_refs:
-            ConanOutput().info(f"Filtering and installing only selected editable packages")
-            requires = [ref for ref in requires if ref in filtered_refs]
-            ConanOutput().info(f"Filtered references: {requires}")
-        return requires
-
-    @property
-    def products(self):
-        self._check_ws()
-        return self._ws.products()
-
     def open(self, require, remotes, cwd=None):
         app = ConanApp(self._conan_api)
         ref = RecipeReference.loads(require)
@@ -167,7 +158,7 @@ class WorkspaceAPI:
                                  f"'{WORKSPACE_PY}' or '{WORKSPACE_YML}' file")
 
     def add(self, path, name=None, version=None, user=None, channel=None, cwd=None,
-            output_folder=None, remotes=None, product=False):
+            output_folder=None, remotes=None):
         """
         Add a new editable package to the current workspace (the current workspace must exist)
         @param path: The path to the folder containing the conanfile.py that defines the package
@@ -178,7 +169,6 @@ class WorkspaceAPI:
         @param cwd:
         @param output_folder:
         @param remotes:
-        @param product:
         @return: The reference of the added package
         """
         self._check_ws()
@@ -191,7 +181,7 @@ class WorkspaceAPI:
         ref.validate_ref()
         output_folder = make_abs_path(output_folder) if output_folder else None
         # Check the conanfile is there, and name/version matches
-        self._ws.add(ref, full_path, output_folder, product)
+        self._ws.add(ref, full_path, output_folder)
         return ref
 
     @staticmethod
@@ -230,17 +220,10 @@ class WorkspaceAPI:
         self._check_ws()
         return {"name": self.name,
                 "folder": self._folder,
-                "products": self.products,
                 "packages": self._ws.packages()}
 
-    def editable_from_path(self, path):
-        editables = self._ws.packages()
-        for ref, info in editables.items():
-            if info["path"].replace("\\", "/") == path:
-                return RecipeReference.loads(ref)
-
-    def collapse_editables(self, deps_graph, profile_host, profile_build):
-        ConanOutput().title("Collapsing workspace editables")
+    def super_build_graph(self, deps_graph, profile_host, profile_build):
+        ConanOutput().title("Collapsing workspace packages")
 
         root_class = self._ws.root_conanfile()
         if root_class is not None:
@@ -282,3 +265,56 @@ class WorkspaceAPI:
             result.add_edge(root, t.node, r)
 
         return result
+
+    def export(self, lockfile=None, remotes=None):
+        self._check_ws()
+        exported = []
+        for ref, info in self.packages().items():
+            exported_ref = self._conan_api.export.export(info["path"], ref.name, str(ref.version),
+                                                         ref.user, ref.channel,
+                                                         lockfile=lockfile, remotes=remotes)
+            ref, _ = exported_ref
+            exported.append(ref)
+        return exported
+
+
+    def select_packages(self, packages):
+        self._check_ws()
+        editable = self.packages()
+        packages = packages or []
+        selected_editables = {}
+        for ref, info in editable.items():
+            if packages and not any(ref.matches(p, False) for p in packages):
+                continue
+            selected_editables[ref] = info
+        if not selected_editables:
+            raise ConanException("There are no selected packages defined in the workspace")
+
+        return selected_editables
+
+    def build_order(self, packages, profile_host, profile_build, build_mode, lockfile, remotes,
+                    profile_args, update=False):
+        ConanOutput().title(f"Computing dependency graph for each package")
+        conan_api = self._conan_api
+        from conan.internal.graph.install_graph import InstallGraph
+        install_order = InstallGraph(None)
+
+        for ref, info in packages.items():
+            ConanOutput().title(f"Computing the dependency graph for package: {ref}")
+
+            deps_graph = conan_api.graph.load_graph_requires([ref], None,
+                                                             profile_host, profile_build, lockfile,
+                                                             remotes, update)
+            deps_graph.report_graph_error()
+            print_graph_basic(deps_graph)
+            conan_api.graph.analyze_binaries(deps_graph, build_mode, remotes=remotes, update=update,
+                                             lockfile=lockfile)
+            print_graph_packages(deps_graph)
+
+            ConanOutput().success(f"\nAggregating build-order for package: {ref}")
+            install_graph = InstallGraph(deps_graph, order_by="recipe",
+                                         profile_args=ProfileArgs.from_args(profile_args))
+            install_graph.raise_errors()
+            install_order.merge(install_graph)
+
+        return install_order
diff --git a/conan/cli/commands/create.py b/conan/cli/commands/create.py
index ec566ecac..f4ea4ff51 100644
--- a/conan/cli/commands/create.py
+++ b/conan/cli/commands/create.py
@@ -107,8 +107,8 @@ def create(conan_api, parser, *args):
         if args.test_folder is None else args.test_folder
     test_conanfile_path = _get_test_conanfile_path(test_package_folder, path)
     # If the user provide --test-missing and the binary was not built from source, skip test_package
-    if args.test_missing and deps_graph.root.dependencies\
-            and deps_graph.root.dependencies[0].dst.binary != "Build":
+    if args.test_missing and deps_graph.root.edges\
+            and deps_graph.root.edges[0].dst.binary != "Build":
         test_conanfile_path = None  # disable it
 
     if test_conanfile_path:
diff --git a/conan/cli/commands/report.py b/conan/cli/commands/report.py
new file mode 100644
index 000000000..84b8fc552
--- /dev/null
+++ b/conan/cli/commands/report.py
@@ -0,0 +1,61 @@
+import os
+
+from conan.cli.formatters.report import format_diff_html, format_diff_txt, format_diff_json
+from conan.api.conan_api import ConanAPI
+from conan.cli.command import conan_command, conan_subcommand
+
+
+
+@conan_command(group="Security")
+def report(conan_api: ConanAPI, parser, *args):
+    """
+    Gets information about the recipe and its sources.
+    """
+
+
+@conan_subcommand(formatters={"text": format_diff_txt,
+                              "json": format_diff_json,
+                              "html": format_diff_html})
+def report_diff(conan_api, parser, subparser, *args):
+    """
+    Get the difference between two recipes with their sources.
+    It can be used to compare two different versions of the same recipe, or two different recipe revisions.
+
+    Each old/new recipe can be specified by a path to a conanfile.py and a companion reference,
+    or by a reference only.
+
+    If only a reference is specified, it will be searched in the local cache,
+    or downloaded from the specified remotes. If no revision is specified, the latest revision will be used.
+    """
+
+    ref_help = ("{type} reference, e.g. 'mylib/1.0'. "
+                "If used on its own, it can contain a revision, which will be resolved to the latest one if not provided, "
+                "but it will be ignored if a path is specified. "
+                "If used with a path, it will be used to create the reference for the recipe to be compared.")
+
+    subparser.add_argument("-op", "--old-path", help="Path to the old recipe if comparing a local recipe is desired")
+    subparser.add_argument("-or", "--old-reference", help=ref_help.format(type="Old"), required=True)
+
+    subparser.add_argument("-np", "--new-path", help="Path to the new recipe if comparing a local recipe is desired")
+    subparser.add_argument("-nr", "--new-reference", help=ref_help.format(type="New"), required=True)
+
+    subparser.add_argument("-r", "--remote", action="append", default=None,
+                       help='Look in the specified remote or remotes server')
+
+    args = parser.parse_args(*args)
+
+    cwd = os.getcwd()
+    enabled_remotes = conan_api.remotes.list(args.remote or "*")
+
+    result = conan_api.report.diff(args.old_reference, args.new_reference, enabled_remotes,
+                                   old_path=args.old_path, new_path=args.new_path, cwd=cwd)
+    return {
+        "conan_api": conan_api,
+        "diff": result["diff"],
+        "old_export_ref": result["old_export_ref"],
+        "new_export_ref": result["new_export_ref"],
+        "old_cache_path": result["old_cache_path"],
+        "new_cache_path": result["new_cache_path"],
+        "src_prefix": result["src_prefix"],
+        "dst_prefix": result["dst_prefix"],
+    }
diff --git a/conan/cli/commands/workspace.py b/conan/cli/commands/workspace.py
index 9704ad2df..3c88ae39b 100644
--- a/conan/cli/commands/workspace.py
+++ b/conan/cli/commands/workspace.py
@@ -2,6 +2,7 @@ import json
 import os
 
 from conan.api.conan_api import ConanAPI
+from conan.api.model import RecipeReference
 from conan.api.output import ConanOutput, cli_out_write
 from conan.api.subapi.workspace import WorkspaceAPI
 from conan.cli import make_abs_path
@@ -11,6 +12,7 @@ from conan.cli.commands.list import print_serial
 from conan.cli.printers import print_profiles
 from conan.cli.printers.graph import print_graph_packages, print_graph_basic
 from conan.errors import ConanException
+from conan.internal.graph.install_graph import ProfileArgs
 
 
 @conan_subcommand(formatters={"text": cli_out_write})
@@ -20,8 +22,6 @@ def workspace_root(conan_api: ConanAPI, parser, subparser, *args):  # noqa
     """
     parser.parse_args(*args)
     ws = conan_api.workspace
-    if not ws.folder():
-        raise ConanException("No workspace defined, conanws.py file not found")
     return ws.folder()
 
 
@@ -59,7 +59,6 @@ def workspace_add(conan_api: ConanAPI, parser, subparser, *args):
                        help='Look in the specified remote or remotes server')
     group.add_argument("-nr", "--no-remote", action="store_true",
                        help='Do not use remote, resolve exclusively in the cache')
-    subparser.add_argument("--product", action="store_true", help="Add the package as a product")
     args = parser.parse_args(*args)
     if args.path and args.ref:
         raise ConanException("Do not use both 'path' and '--ref' argument")
@@ -71,7 +70,7 @@ def workspace_add(conan_api: ConanAPI, parser, subparser, *args):
         path = conan_api.workspace.open(args.ref, remotes, cwd=cwd)
     ref = conan_api.workspace.add(path,
                                   args.name, args.version, args.user, args.channel,
-                                  cwd, args.output_folder, remotes=remotes, product=args.product)
+                                  cwd, args.output_folder, remotes=remotes)
     ConanOutput().success("Reference '{}' added to workspace".format(ref))
 
 
@@ -109,10 +108,21 @@ def workspace_info(conan_api: ConanAPI, parser, subparser, *args):  # noqa
 @conan_subcommand()
 def workspace_build(conan_api: ConanAPI, parser, subparser, *args):
     """
-    Build the current workspace, starting from the "products"
+    Call "conan build" for packages in the workspace, in the right order
     """
-    subparser.add_argument("path", nargs="?",
-                           help='Path to a package folder in the user workspace')
+    _install_build(conan_api, parser, subparser, True, *args)
+
+
+@conan_subcommand()
+def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Call "conan install" for packages in the workspace, in the right order
+    """
+    _install_build(conan_api, parser, subparser, False, *args)
+
+
+def _install_build(conan_api: ConanAPI, parser, subparser, build, *args):
+    subparser.add_argument("--pkg", action="append", help='Define specific packages')
     add_common_install_arguments(subparser)
     add_lockfile_args(subparser)
     args = parser.parse_args(*args)
@@ -122,58 +132,61 @@ def workspace_build(conan_api: ConanAPI, parser, subparser, *args):
     # The lockfile by default if not defined will be read from the root workspace folder
     ws_folder = conan_api.workspace.folder()
     lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder,
-                                               cwd=None,
-                                               partial=args.lockfile_partial, overrides=overrides)
+                                               cwd=None, partial=args.lockfile_partial,
+                                               overrides=overrides)
     profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
     print_profiles(profile_host, profile_build)
 
-    build_mode = args.build or []
-    if "editable" not in build_mode:
+    buildmode = args.build
+    if build and (not buildmode or "editable" not in buildmode):
         ConanOutput().info("Adding '--build=editable' as build mode")
-        build_mode.append("editable")
-
-    if args.path:
-        products = [args.path]
-    else:  # all products
-        products = conan_api.workspace.products
-        if not products:
-            raise ConanException("There are no products defined in the workspace, can't build\n"
-                                 "You can use 'conan build <path> --build=editable' to build")
-        ConanOutput().title(f"Building workspace products {products}")
-
-    editables = conan_api.workspace.editable_packages
-    # TODO: This has to be improved to avoid repetition when there are multiple products
-    for product in products:
-        ConanOutput().subtitle(f"Building workspace product: {product}")
-        product_ref = conan_api.workspace.editable_from_path(product)
-        if product_ref is None:
-            raise ConanException(f"Product '{product}' not defined in the workspace as editable")
-        editable = editables[product_ref]
-        editable_path = editable["path"]
-        deps_graph = conan_api.graph.load_graph_consumer(editable_path, None, None, None, None,
-                                                         profile_host, profile_build, lockfile,
-                                                         remotes, args.update)
-        deps_graph.report_graph_error()
-        print_graph_basic(deps_graph)
-        conan_api.graph.analyze_binaries(deps_graph, build_mode, remotes=remotes, update=args.update,
-                                         lockfile=lockfile)
-        print_graph_packages(deps_graph)
-        conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes)
-        conan_api.install.install_consumer(deps_graph, None, os.path.dirname(editable_path),
-                                           editable.get("output_folder"))
-        ConanOutput().title(f"Calling build() for the product {product_ref}")
-        conanfile = deps_graph.root.conanfile
-        conan_api.local.build(conanfile)
+        buildmode = buildmode or []
+        buildmode.append("editable")
+
+    all_editables = conan_api.workspace.packages()
+    packages = conan_api.workspace.select_packages(args.pkg)
+    ConanOutput().box("Workspace computing the build order")
+    install_order = conan_api.workspace.build_order(packages, profile_host, profile_build, buildmode,
+                                                    lockfile, remotes, args, update=args.update)
+
+    msg = "build" if build else "install"
+    ConanOutput().box(f"Workspace {msg}ing each package")
+    order = install_order.install_build_order()
+
+    profile_args = ProfileArgs.from_args(args)
+    for level in order["order"]:
+        for elem in level:
+            ref = RecipeReference.loads(elem["ref"])
+            for package_level in elem["packages"]:
+                for package in package_level:
+                    if package["binary"] == "Build":  # Build external to Workspace
+                        cmd = f'install {package["build_args"]} {profile_args}'
+                        ConanOutput().box(f"Workspace building external {ref}")
+                        ConanOutput().info(f"Command: {cmd}\n")
+                        conan_api.command.run(cmd)
+                    elif package["binary"] in ("Editable", "EditableBuild"):
+                        path = all_editables[ref]["path"]
+                        output_folder = all_editables[ref].get("output_folder")
+                        build_arg = "--build-require" if package["context"] == "build" else ""
+                        ref_args = " ".join(f"--{k}={getattr(ref, k)}"
+                                            for k in ("name", "version", "user", "channel")
+                                            if getattr(ref, k, None))
+                        of_arg = f'-of="{output_folder}"' if output_folder else ""
+                        # TODO: Missing --lockfile-overrides arg here
+                        command = "build" if build else "install"
+                        cmd = f'{command} "{path}" {profile_args} {build_arg} {ref_args} {of_arg}'
+                        ConanOutput().box(f"Workspace {command}: {ref}")
+                        ConanOutput().info(f"Command: {cmd}\n")
+                        conan_api.command.run(cmd)
 
 
 @conan_subcommand()
-def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
+def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args):
     """
     Install the workspace as a monolith, installing only external dependencies to the workspace,
     generating a single result (generators, etc) for the whole workspace.
     """
-    subparser.add_argument("path", nargs="*",
-                           help="Install only these editable packages, not all")
+    subparser.add_argument("--pkg", action="append", help='Define specific packages')
     subparser.add_argument("-g", "--generator", action="append", help='Generators to use')
     subparser.add_argument("-of", "--output-folder",
                            help='The root output folder for generated and build files')
@@ -193,11 +206,8 @@ def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
     profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
     print_profiles(profile_host, profile_build)
 
-    conan_api.workspace.info()  # FIXME: Just to force error if WS not enabled
     # Build a dependency graph with all editables as requirements
-    requires = conan_api.workspace.select_editables(args.path)
-    if not requires:
-        raise ConanException("This workspace cannot be installed, it doesn't have any editable")
+    requires = conan_api.workspace.select_packages(args.pkg)
     deps_graph = conan_api.graph.load_graph_requires(requires, [],
                                                      profile_host, profile_build, lockfile,
                                                      remotes, args.build, args.update)
@@ -205,7 +215,7 @@ def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
     print_graph_basic(deps_graph)
 
     # Collapsing the graph
-    ws_graph = conan_api.workspace.collapse_editables(deps_graph, profile_host, profile_build)
+    ws_graph = conan_api.workspace.super_build_graph(deps_graph, profile_host, profile_build)
     ConanOutput().subtitle("Collapsed graph")
     print_graph_basic(ws_graph)
 
@@ -219,7 +229,7 @@ def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
 
 
 @conan_subcommand()
-def workspace_clean(conan_api: ConanAPI, parser, subparser, *args):
+def workspace_clean(conan_api: ConanAPI, parser, subparser, *args):  # noqa
     """
     Clean the temporary build folders when possible
     """
@@ -239,6 +249,92 @@ def workspace_init(conan_api: ConanAPI, parser, subparser, *args):
     conan_api.workspace.init(args.path)
 
 
+@conan_subcommand()
+def workspace_create(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Call "conan create" for packages in the workspace, in the correct order.
+    Packages will be created in the Conan cache, not locally
+    """
+    subparser.add_argument("--pkg", action="append", help='Define specific packages')
+    add_common_install_arguments(subparser)
+    add_lockfile_args(subparser)
+    args = parser.parse_args(*args)
+    # Basic collaborators: remotes, lockfile, profiles
+    remotes = conan_api.remotes.list(args.remote) if not args.no_remote else []
+    overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None
+    # The lockfile by default if not defined will be read from the root workspace folder
+    ws_folder = conan_api.workspace.folder()
+    lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder,
+                                               cwd=None,
+                                               partial=args.lockfile_partial, overrides=overrides)
+    profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
+    print_profiles(profile_host, profile_build)
+
+    build_mode = args.build if args.build else []
+    ConanOutput().box("Exporting workspace recipes to Conan cache")
+    exported_refs = conan_api.workspace.export()
+    build_mode.extend(f"missing:{r}" for r in exported_refs)
+
+    all_packages = conan_api.workspace.packages()
+    packages = conan_api.workspace.select_packages(args.pkg)
+
+    # If we don't disable the workspace, then, the packages are not created in the Conan cache,
+    # but locally in the user folders, are they are intercepted as editables
+    conan_api.workspace.enable(False)
+
+    install_order = conan_api.workspace.build_order(packages, profile_host, profile_build,
+                                                    build_mode, lockfile, remotes, args,
+                                                    update=args.update)
+
+    ConanOutput().box("Workspace creating each package")
+    order = install_order.install_build_order()
+
+    profile_args = ProfileArgs.from_args(args)
+    for level in order["order"]:
+        for elem in level:
+            ref = RecipeReference.loads(elem["ref"])
+            for package_level in elem["packages"]:
+                for package in package_level:
+                    if package["binary"] not in ("Build", "EditableBuild"):
+                        ConanOutput().info(f"Skip build for {ref} binary: {package['binary']}")
+                        continue
+
+                    if ref not in all_packages:
+                        # Build external to Workspace
+                        cmd = f'install {package["build_args"]} {profile_args}'
+                        ConanOutput().box(f"Workspace building external {ref}")
+                        ConanOutput().info(f"Build command: {cmd}\n")
+                        conan_api.command.run(cmd)
+                    else:  # Package in workspace
+                        path = packages[ref]["path"]
+                        # TODO: Missing --lockfile-overrides arg here
+                        build = "--build-require" if package["context"] == "build" else ""
+                        ref_args = " ".join(f"--{k}={getattr(ref, k)}"
+                                            for k in ("name", "version", "user", "channel")
+                                            if getattr(ref, k, None))
+                        cmd = f'create "{path}" {profile_args} {build} {ref_args}'
+                        ConanOutput().box(f"Workspace create {ref}")
+                        ConanOutput().info(f"Conan create command: {cmd}\n")
+                        conan_api.command.run(cmd)
+
+
+@conan_subcommand()
+def workspace_source(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Call the source() method of packages in the workspace
+    """
+    subparser.add_argument("--pkg", action="append", help='Define specific packages')
+    args = parser.parse_args(*args)
+
+    remotes = conan_api.remotes.list()  # In case "python_requires" are needed
+    packages = conan_api.workspace.select_packages(args.pkg)
+
+    ConanOutput().box("Workspace getting sources")
+    for pkg, info in packages.items():
+        conan_api.local.source(info["path"], name=pkg.name, version=str(pkg.version),
+                               user=pkg.user, channel=pkg.channel, remotes=remotes)
+
+
 @conan_command(group="Consumer")
 def workspace(conan_api, parser, *args):  # noqa
     """
diff --git a/conan/cli/formatters/report/__init__.py b/conan/cli/formatters/report/__init__.py
new file mode 100644
index 000000000..a9d2dd675
--- /dev/null
+++ b/conan/cli/formatters/report/__init__.py
@@ -0,0 +1,1 @@
+from .diff import format_diff_html, format_diff_txt, format_diff_json
diff --git a/conan/cli/formatters/report/diff.py b/conan/cli/formatters/report/diff.py
new file mode 100644
index 000000000..b64259d7a
--- /dev/null
+++ b/conan/cli/formatters/report/diff.py
@@ -0,0 +1,95 @@
+import json
+import os
+import base64
+
+from jinja2 import Template
+
+from conan.api.output import cli_out_write
+from conan.cli.formatters.report.diff_html import diff_html
+
+def _generate_json(result):
+    diff_text = result["diff"]
+    src_prefix = result["src_prefix"]
+    dst_prefix = result["dst_prefix"]
+    ret = {}
+    current_filename = None
+    for line in diff_text.splitlines():
+        if line.startswith("diff --git "):
+            src_filename, dst_filename = _get_filenames(line, src_prefix, dst_prefix)
+            current_filename = src_filename
+            ret[current_filename] = [line]
+        else:
+            ret[current_filename].append(line)
+    return ret
+
+def _get_filenames(line, src_prefix, dst_prefix):
+    """
+    Extracts the source and destination filenames from a diff line.
+    """
+    src_index = line.find(src_prefix)
+    dst_index = line.find(dst_prefix)
+
+    if src_index == -1 or dst_index == -1:
+        return None, None
+
+    src_filename = line[src_index + len(src_prefix) - 1:dst_index - 1].strip()
+    dst_filename = line[dst_index + len(dst_prefix) - 1:].strip()
+
+    return src_filename, dst_filename
+
+def _render_diff(content, template, template_folder, **kwargs):
+    from conan import __version__
+    template = Template(template, autoescape=True)
+    def _safe_filename(filename):
+        # Calculate base64 of the filename
+        return base64.b64encode(filename.encode(), altchars=b'-_').decode()
+
+    def _get_diff_filename(line):
+        return _get_filenames(line, kwargs["src_prefix"], kwargs["dst_prefix"])[0]
+
+    def _remove_prefixes(line):
+        return line.replace(kwargs["src_prefix"][:-1], "").replace(kwargs["dst_prefix"][:-1], "")
+
+    def _replace_cache_paths(line):
+        return line.replace(kwargs["old_cache_path"], "(old)").replace(kwargs["new_cache_path"], "(new)")
+
+    def _replace_paths(line):
+        return _remove_prefixes(_replace_cache_paths(line))
+
+    return template.render(content=content,
+                           base_template_path=template_folder, version=__version__,
+                           safe_filename=_safe_filename,
+                           replace_paths=_replace_paths,
+                           replace_cache_paths=_replace_cache_paths,
+                           remove_prefixes=_remove_prefixes,
+                           get_diff_filename=_get_diff_filename,
+                           **kwargs)
+
+def format_diff_html(result):
+    conan_api = result["conan_api"]
+
+    template_folder = os.path.join(conan_api.cache_folder, "templates")
+    user_template = os.path.join(template_folder, "diff.html")
+    template = diff_html
+    if os.path.isfile(user_template):
+        with open(user_template, 'r', encoding="utf-8", newline="") as handle:
+            template = handle.read()
+
+    content = _generate_json(result)
+
+    cli_out_write(_render_diff(content, template, template_folder,
+                               old_reference=result["old_export_ref"],
+                               new_reference=result["new_export_ref"],
+                               old_cache_path=result["old_cache_path"],
+                               new_cache_path=result["new_cache_path"],
+                               src_prefix=result["src_prefix"],
+                               dst_prefix=result["dst_prefix"]))
+
+
+def format_diff_txt(result):
+    diff_text = result["diff"]
+    cli_out_write(diff_text)
+
+
+def format_diff_json(result):
+    cli_out_write(json.dumps(_generate_json(result), indent=2))
diff --git a/conan/cli/formatters/report/diff_html.py b/conan/cli/formatters/report/diff_html.py
new file mode 100644
index 000000000..1e0c3f173
--- /dev/null
+++ b/conan/cli/formatters/report/diff_html.py
@@ -0,0 +1,138 @@
+diff_html = r"""
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>{{ old_reference }} - {{ new_reference }}</title>
+        <style>
+            body { font-family: monospace; margin: 0px; }
+            .container { display: flex; height: 100%; }
+            .sidebar {
+                min-width: 20%;
+                max-width: 20%;
+                padding: 10px;
+                overflow-y: scroll;
+                background: #f4f4f4;
+                border-right: 1px solid #ccc;
+            }
+            .sidebar li { line-height: 1.5; }
+            .content {
+                padding: 20px;
+                background: #fff;
+                overflow-y: scroll;
+            }
+            .content span {
+                white-space: pre-wrap;
+            }
+            .add { background-color: #76ffbb; }
+            .del { background-color: #fdb9c1; }
+            .context, .diff-content { background-color: #f8f8f8; }
+            .filename { background-color: #f0f0f0; }
+            a:visited {
+                color: blue;
+            }
+        </style>
+        <script>
+            let includeSearchQuery = "";
+            let excludeSearchQuery = "";
+
+            async function onExcludeSearchInput(event) {
+                excludeSearchQuery = event.currentTarget.value.toLowerCase();
+                onSearchInput(event);
+            }
+
+            async function onIncludeSearchInput(event) {
+                includeSearchQuery = event.currentTarget.value.toLowerCase();
+                onSearchInput(event);
+            }
+
+            async function onSearchInput(event) {
+                const sidebar = document.querySelectorAll(".sidebar li");
+                const content = document.querySelectorAll(".content .diff-content");
+
+                sidebar.forEach(async function(item) {
+                    const text = item.textContent.toLowerCase();
+                    const shouldInclude = includeSearchQuery === "" || text.includes(includeSearchQuery);
+                    const shouldExclude = excludeSearchQuery !== "" && text.includes(excludeSearchQuery);
+
+                    if (shouldInclude) {
+                        if (shouldExclude) {
+                            item.style.display = "none";
+                        } else {
+                            item.style.display = "list-item";
+                        }
+                    } else {
+                        item.style.display = "none";
+                    }
+
+                });
+
+                content.forEach(async function(item) {
+                    const filename = document.getElementById(item.id + "_filename");
+                    const text = filename.dataset.replacedPaths.toLowerCase();
+                    const shouldInclude = includeSearchQuery === "" || text.includes(includeSearchQuery);
+                    const shouldExclude = excludeSearchQuery !== "" && text.includes(excludeSearchQuery);
+
+                    if (shouldInclude) {
+                        if (shouldExclude) {
+                            item.style.display = "none";
+                        } else {
+                            item.style.display = "block";
+                        }
+                    } else {
+                        item.style.display = "none";
+                    }
+                });
+            }
+        </script>
+    </head>
+    <body>
+        <div class='container'>
+            <div class='sidebar'>
+                <div style="white-space: nowrap;">
+                    <span class="del">--- (old): <b>{{ old_reference.repr_notime() }}</b></span>
+                    <br/>
+                    <span class="add">+++ (new): <b>{{ new_reference.repr_notime() }}</b></span>
+                </div>
+                <h2>File list:</h2>
+                <input type="text" id="search-include" placeholder="Include search..." oninput="onIncludeSearchInput(event)" />
+                <input type="text" id="search-exclude" placeholder="Exclude search..." oninput="onExcludeSearchInput(event)" />
+                <ul>
+                    {%- for filename in content.keys() %}
+                        <li><a href="#diff_{{- safe_filename(filename) -}}" class="side-link">{{ replace_cache_paths(filename) }}</a></li>
+                    {%- endfor %}
+                </ul>
+            </div>
+            <div class='content'>
+                <div><!--placeholder-->
+                {%- for filename, lines in content.items() -%}
+                    </div>
+                    <div id="diff_{{ safe_filename(filename) }}" class="diff-content">
+                    {%- for line in lines -%}
+                        {%- if loop.first -%}
+                            <hr/>
+                            <h3 id="diff_{{ safe_filename(filename) }}_filename" class="filename" data-replaced-paths="{{ replace_cache_paths(filename) }}">{{ remove_prefixes(line) }}</h3>
+                        {%- elif line.startswith('+++') %}
+                            <span class="add">{{ replace_paths(line) }}</span>
+                            <br/>
+                        {%- elif line.startswith('---') %}
+                            <span class="del">{{ replace_paths(line) }}</span>
+                            <br/>
+                        {%- elif line.startswith('+') %}
+                            <span class="add">{{ line }}</span>
+                            <br/>
+                        {%- elif line.startswith('-') %}
+                            <span class="del">{{ line }}</span>
+                            <br/>
+                        {%- else %}
+                            <span class="context">{{ line }}</span>
+                            <br/>
+                        {%- endif %}
+                    {%- endfor -%}
+                {%- endfor -%}
+                <hr/>
+                </div>
+            </div>
+        </div>
+    </body>
+</html>
+"""
diff --git a/conan/internal/api/new/workspace.py b/conan/internal/api/new/workspace.py
index d89c6acef..95ea84111 100644
--- a/conan/internal/api/new/workspace.py
+++ b/conan/internal/api/new/workspace.py
@@ -11,8 +11,6 @@ packages:
     path: libb
   app1/0.1:
     path: app1
-products:
-- app1
 """
 
 cmake = """\
diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py
index 0ca4df974..84ed3c3ae 100644
--- a/conan/internal/conan_app.py
+++ b/conan/internal/conan_app.py
@@ -54,7 +54,7 @@ class ConanBasicApp:
         # Handle remote connections
         self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder)
         global_editables = conan_api.local.editable_packages
-        ws_editables = conan_api.workspace.editable_packages
+        ws_editables = conan_api.workspace.packages()
         self.editable_packages = global_editables.update_copy(ws_editables)
 
 
diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py
index 8e4dc2e29..a8d9369cb 100644
--- a/conan/internal/graph/graph.py
+++ b/conan/internal/graph/graph.py
@@ -1,5 +1,6 @@
 from collections import OrderedDict
 
+from conan.api.output import ConanOutput
 from conan.internal.graph.graph_error import GraphError
 from conan.api.model import PkgReference
 from conan.api.model import RecipeReference
@@ -61,15 +62,22 @@ class Node:
 
         # real graph model
         self.transitive_deps = OrderedDict()  # of _TransitiveRequirement
-        self.dependencies = []  # Ordered Edges
+        self.edges = []  # Ordered Edges
         self.dependants = []  # Edges
         self.error = None
         self.should_build = False  # If the --build or policy wants to build this binary
         self.build_allowed = False
         self.is_conf = False
-        self.replaced_requires = {}  # To track the replaced requires for self.dependencies[old-ref]
+        self.replaced_requires = {}  # To track the replaced requires for self.edges[old-ref]
         self.skipped_build_requires = False
 
+    @property
+    def dependencies(self):
+        ConanOutput().warning("Node.dependencies is private and shouldn't be used. It is now "
+                              "node.edges. Please fix your code, Node.dependencies will be removed "
+                              "in future versions", warn_tag="deprecated")
+        return self.edges
+
     def subgraph(self):
         nodes = [self]
         opened = [self]
@@ -233,12 +241,12 @@ class Node:
 
     def add_edge(self, edge):
         if edge.src == self:
-            self.dependencies.append(edge)
+            self.edges.append(edge)
         else:
             self.dependants.append(edge)
 
     def neighbors(self):
-        return [edge.dst for edge in self.dependencies]
+        return [edge.dst for edge in self.edges]
 
     def inverse_neighbors(self):
         return [edge.src for edge in self.dependants]
diff --git a/conan/internal/graph/graph_binaries.py b/conan/internal/graph/graph_binaries.py
index 37ef04173..386f69476 100644
--- a/conan/internal/graph/graph_binaries.py
+++ b/conan/internal/graph/graph_binaries.py
@@ -48,8 +48,8 @@ class GraphBinariesAnalyzer:
         with_deps_to_build = False
         # check dependencies, if they are being built, "cascade" will try to build this one too
         if build_mode.cascade:
-            with_deps_to_build = any(dep.dst.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD)
-                                     for dep in node.dependencies)
+            with_deps_to_build = any(edge.dst.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD)
+                                     for edge in node.edges)
         if build_mode.forced(conanfile, ref, with_deps_to_build):
             node.should_build = True
             conanfile.output.info('Forced build from source')
diff --git a/conan/internal/graph/install_graph.py b/conan/internal/graph/install_graph.py
index 7d7d9aec7..41879aec8 100644
--- a/conan/internal/graph/install_graph.py
+++ b/conan/internal/graph/install_graph.py
@@ -171,7 +171,7 @@ class _InstallRecipeReference:
             install_pkg_ref = _InstallPackageReference.create(node)
             self.packages[node.package_id] = install_pkg_ref
 
-        for dep in node.dependencies:
+        for dep in node.edges:
             if dep.dst.binary != BINARY_SKIP:
                 if dep.dst.ref == node.ref:  # If the node is itself, then it is internal dep
                     install_pkg_ref.depends.append(dep.dst.pref.package_id)
@@ -270,7 +270,7 @@ class _InstallConfiguration:
         result.info = node.conanfile.info.serialize()
 
         result.nodes.append(node)
-        for dep in node.dependencies:
+        for dep in node.edges:
             if dep.dst.binary != BINARY_SKIP:
                 if dep.dst.pref not in result.depends:
                     result.depends.append(dep.dst.pref)
@@ -284,10 +284,10 @@ class _InstallConfiguration:
         # 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)
+        for edge in node.edges:
+            if edge.dst.binary != BINARY_SKIP:
+                if edge.dst.pref not in self.depends:
+                    self.depends.append(edge.dst.pref)
 
     def _build_args(self):
         if self.binary not in (BINARY_BUILD, BINARY_EDITABLE_BUILD):
@@ -359,6 +359,9 @@ class ProfileArgs:
                             getattr(args, f"{f}_{context}") or []]
         return ProfileArgs(" ".join(pr_args))
 
+    def __str__(self):
+        return self._args
+
     @staticmethod
     def deserialize(data):
         return ProfileArgs(data.get("args"))
diff --git a/conan/internal/model/workspace.py b/conan/internal/model/workspace.py
index 35ff09777..2fe436564 100644
--- a/conan/internal/model/workspace.py
+++ b/conan/internal/model/workspace.py
@@ -38,15 +38,13 @@ class Workspace:
             raise ConanException("Invalid yml format at {}: {}".format(WORKSPACE_YML, e))
         return data or {}
 
-    def add(self, ref, path, output_folder, product=False):
+    def add(self, ref, path, output_folder):
         assert os.path.isfile(path)
         path = self._conan_rel_path(os.path.dirname(path))
         editable = {"path": path}
         if output_folder:
             editable["output_folder"] = self._conan_rel_path(output_folder)
         self.conan_data.setdefault("packages", {})[str(ref)] = editable
-        if product:
-            self.conan_data.setdefault("products", []).append(path)
         save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data))
 
     def remove(self, path):
@@ -59,8 +57,6 @@ class Workspace:
         if not found_ref:
             raise ConanException(f"No editable package to remove from this path: {path}")
         self.conan_data["packages"].pop(found_ref)
-        if path in self.conan_data.get("products", []):
-            self.conan_data["products"].remove(path)
         save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data))
         return found_ref
 
@@ -88,9 +84,6 @@ class Workspace:
     def packages(self):
         return self.conan_data.get("packages", {})
 
-    def products(self):
-        return self.conan_data.get("products", [])
-
     def load_conanfile(self, conanfile_path):
         conanfile_path = os.path.join(self.folder, conanfile_path, "conanfile.py")
         from conan.internal.loader import ConanFileLoader
diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py
index c5efca07c..17dbf31da 100644
--- a/conan/tools/apple/xcodedeps.py
+++ b/conan/tools/apple/xcodedeps.py
@@ -109,7 +109,6 @@ class XcodeDeps(object):
         self._conanfile = conanfile
         self.configuration = conanfile.settings.get_safe("build_type")
         arch = conanfile.settings.get_safe("arch")
-        self.os_version = conanfile.settings.get_safe("os.version")
         self.architecture = _to_apple_arch(arch, default=arch)
         self.os_version = conanfile.settings.get_safe("os.version")
         self.sdk = conanfile.settings.get_safe("os.sdk")
diff --git a/conan/tools/apple/xcodetoolchain.py b/conan/tools/apple/xcodetoolchain.py
index 269f6e08d..194aa3af6 100644
--- a/conan/tools/apple/xcodetoolchain.py
+++ b/conan/tools/apple/xcodetoolchain.py
@@ -13,7 +13,7 @@ class XcodeToolchain(object):
 
     _vars_xconfig = textwrap.dedent("""\
         // Definition of toolchain variables
-        {macosx_deployment_target}
+        {apple_deployment_target}
         {clang_cxx_library}
         {clang_cxx_language_standard}
         """)
@@ -63,9 +63,17 @@ class XcodeToolchain(object):
         return cppstd
 
     @property
-    def _macosx_deployment_target(self):
-        return 'MACOSX_DEPLOYMENT_TARGET{}={}'.format(_xcconfig_conditional(self._conanfile.settings, self.configuration),
-                                                      self.os_version) if self.os_version else ""
+    def _apple_deployment_target(self):
+        deployment_target_key = {
+            "Macos": "MACOSX_DEPLOYMENT_TARGET",
+            "iOS": "IPHONEOS_DEPLOYMENT_TARGET",
+            "tvOS": "TVOS_DEPLOYMENT_TARGET",
+            "watchOS": "WATCHOS_DEPLOYMENT_TARGET",
+            "visionOS": "XROS_DEPLOYMENT_TARGET",
+        }.get(str(self._conanfile.settings.get_safe("os")))
+        return '{}{}={}'.format(deployment_target_key,
+                                _xcconfig_conditional(self._conanfile.settings, self.configuration),
+                                self.os_version) if self.os_version else ""
 
     @property
     def _clang_cxx_library(self):
@@ -83,7 +91,7 @@ class XcodeToolchain(object):
 
     @property
     def _vars_xconfig_content(self):
-        ret = self._vars_xconfig.format(macosx_deployment_target=self._macosx_deployment_target,
+        ret = self._vars_xconfig.format(apple_deployment_target=self._apple_deployment_target,
                                         clang_cxx_library=self._clang_cxx_library,
                                         clang_cxx_language_standard=self._clang_cxx_language_standard)
         return ret
diff --git a/conan/tools/gnu/pkgconfigdeps.py b/conan/tools/gnu/pkgconfigdeps.py
index 70d85e5fb..95b2174fd 100644
--- a/conan/tools/gnu/pkgconfigdeps.py
+++ b/conan/tools/gnu/pkgconfigdeps.py
@@ -259,7 +259,8 @@ class _PCFilesDeps:
         # Second, let's load the root package's PC file ONLY
         # if it does not already exist in components one
         # Issue related: https://github.com/conan-io/conan/issues/10341
-        if pkg_name not in pc_files:
+        should_skip_main = self._get_property("pkg_config_name", self._dep) == "none"
+        if pkg_name not in pc_files and not should_skip_main:
             cpp_info = self._dep.cpp_info
             # At first, let's check if we have defined some global requires, e.g., "other::cmp1"
             # Note: If DEP has components, they'll be the requirements == pc_files.keys()
diff --git a/conan/tools/sbom/cyclonedx.py b/conan/tools/sbom/cyclonedx.py
index e15585601..19b1993e0 100644
--- a/conan/tools/sbom/cyclonedx.py
+++ b/conan/tools/sbom/cyclonedx.py
@@ -41,11 +41,11 @@ def cyclonedx_1_4(conanfile, name=None, add_build=False, add_tests=False, **kwar
     if has_special_root_node:
         deps = {"ref": special_id,
                 "dependsOn": [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}"
-                              for d in graph.root.dependencies]}
+                              for d in graph.root.edges]}
         dependencies.append(deps)
     for c in nodes:
         deps = {"ref": f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}"}
-        dep = [d for d in c.dependencies if (d.dst.context == "host" or add_build) and (not d.dst.test or add_tests)]
+        dep = [d for d in c.edges if (d.dst.context == "host" or add_build) and (not d.dst.test or add_tests)]
 
         depends_on = [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}" for d in dep]
         if depends_on:
@@ -132,11 +132,11 @@ def cyclonedx_1_6(conanfile, name=None, add_build=False, add_tests=False, **kwar
     if has_special_root_node:
         deps = {"ref": special_id,
                 "dependsOn": [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}"
-                              for d in graph.root.dependencies]}
+                              for d in graph.root.edges]}
         dependencies.append(deps)
     for c in nodes:
         deps = {"ref": f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}"}
-        dep = [d for d in c.dependencies if (d.dst.context == "host" or add_build) and (not d.dst.test or add_tests)]
+        dep = [d for d in c.edges if (d.dst.context == "host" or add_build) and (not d.dst.test or add_tests)]
 
         depends_on = [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}" for d in dep]
         if depends_on:


Test patch:
diff --git a/.github/actions/test-coverage/action.yml b/.github/actions/test-coverage/action.yml
index 70f4e93ac..86127f8d0 100644
--- a/.github/actions/test-coverage/action.yml
+++ b/.github/actions/test-coverage/action.yml
@@ -30,7 +30,7 @@ runs:
         if: ${{ inputs.tests }}
         shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
         run: |
-          pytest ${{ inputs.tests }} -rf --durations=${{ inputs.duration }} -n ${{ inputs.workers }} ${{ github.ref == 'refs/heads/develop2' && '--cov=conan --cov=conans --cov=test --cov-report=' || '' }}
+          pytest ${{ inputs.tests }} --durations=${{ inputs.duration }} -n ${{ inputs.workers }} ${{ github.ref == 'refs/heads/develop2' && '--cov=conan --cov=conans --cov=test --cov-report=' || '' }}
 
       - name: Rename coverage file
         if: github.ref == 'refs/heads/develop2'
diff --git a/conan/test/utils/file_server.py b/conan/test/utils/file_server.py
index f8605c558..53aa12034 100644
--- a/conan/test/utils/file_server.py
+++ b/conan/test/utils/file_server.py
@@ -40,7 +40,7 @@ class TestFileServer:
         def head(folder, file):
             exists = os.path.exists(os.path.join(store, folder, file))
             if exists:
-                return True
+                return bottle.HTTPResponse(status=200)
             return bottle.HTTPError(404, "Not found")
 
         @app.route("/forbidden", method=["GET"])
diff --git a/test/functional/command/report_test.py b/test/functional/command/report_test.py
new file mode 100644
index 000000000..fa3b0270c
--- /dev/null
+++ b/test/functional/command/report_test.py
@@ -0,0 +1,107 @@
+import json
+import os
+import textwrap
+
+import pytest
+
+from conan.test.utils.tools import TestClient
+
+
+@pytest.mark.parametrize("old_args", [
+    "-op=v1 -or=pkg/1.0",
+    "-or=pkg/1.0"
+])
+@pytest.mark.parametrize("new_args", [
+    "-np=v2 -nr=pkg/2.0",
+    "-nr=pkg/2.0#{revision}",
+])
+@pytest.mark.parametrize("formatter", [
+    "-f=json --out-file=output.json",
+    "-f=html --out-file=output.html"
+])
+@pytest.mark.tool("git")
+def test_compare_paths(old_args, new_args, formatter):
+    tc = TestClient(light=True, default_server_user=True)
+
+    patch_file = textwrap.dedent("""
+    diff --git a/foo.txt b/foo.txt
+    new file mode 100644
+    index 0000000..e311fbb
+    --- /dev/null
+    +++ b/foo.txt
+    @@ -0,0 +1 @@
+    +{version}
+    """)
+
+    conandata_yml = textwrap.dedent("""
+    patches:
+      "{version}":
+        - patch_file: "patches/patch.patch"
+    """)
+
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+    from conan.tools.files import apply_conandata_patches, export_conandata_patches, save
+
+    class TestConan(ConanFile):
+        name = "pkg"
+
+        exports = "{version}.txt"
+
+        def export_sources(self):
+            export_conandata_patches(self)
+
+        def source(self):
+            save(self, "myfile.txt", "{version}")
+            if self.version == "2.0":
+                save(self, "new-file-for-v2.txt", "a new file for the new version")
+            apply_conandata_patches(self)
+    """)
+
+    tc.save({
+        "v1/conanfile.py": conanfile.format(version="v1"),
+        "v1/conandata.yml": conandata_yml.format(version="1.0"),
+        "v1/v1.txt": "1.0",
+        "v1/patches/patch.patch": patch_file.format(version="1.0"),
+
+        "v2/conanfile.py": conanfile.format(version="v2"),
+        "v2/conandata.yml": conandata_yml.format(version="2.0"),
+        "v2/v2.txt": "2.0",
+        "v2/patches/patch.patch": patch_file.format(version="2.0"),
+    })
+
+    tc.run("create v1 --version=1.0")
+    v1_layout = tc.exported_layout()
+    v1_path = v1_layout.export()
+
+    tc.run("create v2 --version=2.0")
+    v2_revision = tc.exported_layout().reference.revision
+
+    tc.run("upload * -r=default -c")
+    tc.run("remove * -c")
+
+    new_args = new_args.format(revision=v2_revision)
+
+    tc.run(f'report diff {old_args} {new_args} {formatter}')
+
+    if "json" in formatter:
+        output_json = json.loads(tc.load("output.json"))
+        assert any(os.path.join(v1_path, "conanfile.py").replace("\\", "/") in k for k in output_json.keys())
+        assert any(os.path.join(v1_path, "conanmanifest.txt").replace("\\", "/") in k for k in output_json.keys())
+        # We have patch information
+        assert any(os.path.abspath(os.path.join(v1_path, "..", "es", "patches", "patch.patch")).replace("\\", "/") in k for k in output_json.keys())
+        # '/private/var/folders/lw/6bflvp3s3t5b56n2p_bj_vx80000gn/T/tmptcto18wfconans/path with spaces/.conan2/p/pkg8fd97e4b9cf55/es/patches/patch.patch'
+        # We have exports information
+        assert any(os.path.abspath(os.path.join(v1_path, "..", "e", "v1.txt")).replace("\\", "/") in k for k in output_json.keys())
+        # New files
+        assert any("new-file-for-v2.txt" in k for k in output_json.keys())
+
+    elif "html" in formatter:
+        output_html = tc.load("output.html")
+        # We have patch information
+        assert f"""(new)/es/patches/patch.patch""" in output_html
+        # We have exports information
+        assert f"""(old)/e/v1.txt""" in output_html
+        # New files appear both in the menu and in the diff
+        assert output_html.count("(new)/s/new-file-for-v2.txt") == 3
+
diff --git a/test/functional/workspace/test_workspace.py b/test/functional/workspace/test_workspace.py
index 873a13320..a6f80072c 100644
--- a/test/functional/workspace/test_workspace.py
+++ b/test/functional/workspace/test_workspace.py
@@ -1,6 +1,5 @@
 import os
 import platform
-import textwrap
 
 import pytest
 
@@ -20,7 +19,7 @@ def test_build():
     c.save({}, clean_first=True)
     c.run("new workspace -d requires=mymath/0.1")
     c.run("workspace build")
-    assert "Calling build() for the product app1/0.1" in c.out
+    assert "conanfile.py (app1/0.1): Calling build()" in c.out
     assert "conanfile.py (app1/0.1): Running CMake.build()" in c.out
     # it works without failing
 
@@ -35,7 +34,7 @@ def test_metabuild():
 
     c.save({}, clean_first=True)
     c.run("new workspace -d requires=mymath/0.1")
-    c.run("workspace install")
+    c.run("workspace super-install")
     assert os.path.exists(os.path.join(c.current_folder, "CMakeUserPresets.json"))
     build_folder = "build/Release" if platform.system() != "Windows" else "build"
     assert os.path.exists(os.path.join(c.current_folder, build_folder, "generators"))
diff --git a/test/integration/graph/core/graph_manager_base.py b/test/integration/graph/core/graph_manager_base.py
index 7c56c29d2..fb899112a 100644
--- a/test/integration/graph/core/graph_manager_base.py
+++ b/test/integration/graph/core/graph_manager_base.py
@@ -134,7 +134,7 @@ class GraphManagerTest(unittest.TestCase):
         if conanfile:
             self.assertEqual(conanfile.name, ref.name)
 
-        self.assertEqual(len(node.dependencies), len(deps))
+        self.assertEqual(len(node.edges), len(deps))
         for d in node.neighbors():
             assert d in deps
 
diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py
index 4b351bc07..a010780d7 100644
--- a/test/integration/graph/core/graph_manager_test.py
+++ b/test/integration/graph/core/graph_manager_test.py
@@ -38,7 +38,7 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
+        libb = app.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb])
         self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app])
@@ -66,8 +66,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -90,8 +90,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -112,8 +112,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         for r, t in libb.transitive_deps.items():
             assert r.package_id_mode == "minor_mode"
@@ -139,8 +139,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -161,8 +161,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -187,8 +187,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -210,8 +210,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -234,8 +234,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -256,8 +256,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -280,8 +280,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -303,8 +303,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -335,8 +335,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         # node, headers, lib, build, run
         _check_transitive(app, [(libb, True, True, False, False)])
@@ -354,7 +354,7 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        cmake = app.dependencies[0].dst
+        cmake = app.edges[0].dst
 
         # node, headers, lib, build, run
         run = package_type in ("application", "shared-library", "build-scripts")
@@ -369,7 +369,7 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
+        liba = app.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[liba])
         self._check_node(liba, "liba/0.1#123", dependents=[app])
@@ -388,8 +388,8 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -413,9 +413,9 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        libb = liba.dependencies[0].dst
-        libc = liba.dependencies[1].dst
+        liba = app.edges[0].dst
+        libb = liba.edges[0].dst
+        libc = liba.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[liba])
         self._check_node(liba, "liba/0.1#123", deps=[libb, libc], dependents=[app])
@@ -444,10 +444,10 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        liba = libd.dependencies[0].dst
-        libb = liba.dependencies[0].dst
-        libc = liba.dependencies[1].dst
+        libd = app.edges[0].dst
+        liba = libd.edges[0].dst
+        libb = liba.edges[0].dst
+        libc = liba.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[liba], dependents=[app])
@@ -481,10 +481,10 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        liba = libd.dependencies[0].dst
-        libb = liba.dependencies[0].dst
-        libc = liba.dependencies[1].dst
+        libd = app.edges[0].dst
+        liba = libd.edges[0].dst
+        libb = liba.edges[0].dst
+        libc = liba.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[liba], dependents=[app])
@@ -518,9 +518,9 @@ class TestLinear(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -545,9 +545,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -571,9 +571,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -607,9 +607,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -643,9 +643,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -678,9 +678,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -711,9 +711,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -745,9 +745,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -780,9 +780,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -816,9 +816,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -847,9 +847,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -880,9 +880,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -912,9 +912,9 @@ class TestLinearFourLevels(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc, liba])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -946,10 +946,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -980,10 +980,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1012,10 +1012,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1045,10 +1045,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1079,10 +1079,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1112,10 +1112,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1144,10 +1144,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1177,10 +1177,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1207,10 +1207,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1237,10 +1237,10 @@ class TestLinearFiveLevelsHeaders(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1273,10 +1273,10 @@ class TestLinearFiveLevelsLibraries(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1307,10 +1307,10 @@ class TestLinearFiveLevelsLibraries(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = libd.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libc = libd.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libc], dependents=[app])
@@ -1339,9 +1339,9 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb, libc])
@@ -1371,9 +1371,9 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb, libc])
@@ -1396,8 +1396,8 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
+        liba = app.edges[0].dst
+        libc = app.edges[1].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[liba, libc])
@@ -1423,8 +1423,8 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        liba = app.dependencies[1].dst
+        libc = app.edges[0].dst
+        liba = app.edges[1].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[liba, libc])
@@ -1450,10 +1450,10 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
-        liba1 = libc.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
+        liba1 = libc.edges[0].dst
 
         assert liba is liba1
 
@@ -1484,11 +1484,11 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libb = libd.dependencies[0].dst
-        libc = libd.dependencies[1].dst
-        liba = libb.dependencies[0].dst
-        liba2 = libc.dependencies[0].dst
+        libd = app.edges[0].dst
+        libb = libd.edges[0].dst
+        libc = libd.edges[1].dst
+        liba = libb.edges[0].dst
+        liba2 = libc.edges[0].dst
 
         assert liba is liba2
 
@@ -1520,10 +1520,10 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
-        liba2 = libc.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba1 = libb.edges[0].dst
+        liba2 = libc.edges[0].dst
 
         assert liba1 is not liba2
 
@@ -1555,9 +1555,9 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba1 = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app])
@@ -1579,9 +1579,9 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba1 = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app])
@@ -1608,10 +1608,10 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libb = libd.dependencies[0].dst
-        libc = libd.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libb = libd.edges[0].dst
+        libc = libd.edges[1].dst
+        liba1 = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libd])
         self._check_node(libd, "libd/0.1#123", deps=[libb, libc], dependents=[app])
@@ -1635,10 +1635,10 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         libc = deps_graph.root
-        liba = libc.dependencies[0].dst
-        libb = libc.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
-        zlib = liba.dependencies[0].dst
+        liba = libc.edges[0].dst
+        libb = libc.edges[1].dst
+        liba1 = libb.edges[0].dst
+        zlib = liba.edges[0].dst
 
         assert liba is liba1
         # TODO: No Revision??? Because of consumer?
@@ -1670,10 +1670,10 @@ class TestDiamond(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        spdlog = liba.dependencies[0].dst
-        fmt01 = spdlog.dependencies[0].dst
-        fmt02 = app.dependencies[1].dst
+        liba = app.edges[0].dst
+        spdlog = liba.edges[0].dst
+        fmt01 = spdlog.edges[0].dst
+        fmt02 = app.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[liba, fmt02])
         self._check_node(liba, "liba/0.1#123", deps=[spdlog], dependents=[app])
@@ -1703,12 +1703,12 @@ class TestDiamondMultiple(GraphManagerTest):
 
         self.assertEqual(7, len(deps_graph.nodes))
         app = deps_graph.root
-        libe = app.dependencies[0].dst
-        libf = app.dependencies[1].dst
-        libd = libe.dependencies[0].dst
-        libb = libd.dependencies[0].dst
-        libc = libd.dependencies[1].dst
-        liba = libc.dependencies[0].dst
+        libe = app.edges[0].dst
+        libf = app.edges[1].dst
+        libd = libe.edges[0].dst
+        libb = libd.edges[0].dst
+        libc = libd.edges[1].dst
+        liba = libc.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libe, libf])
         self._check_node(libe, "libe/0.1#123", deps=[libd], dependents=[app])
@@ -1742,12 +1742,12 @@ class TestDiamondMultiple(GraphManagerTest):
 
         self.assertEqual(7, len(deps_graph.nodes))
         app = deps_graph.root
-        libe = app.dependencies[0].dst
-        libf = app.dependencies[1].dst
-        libd = libe.dependencies[0].dst
-        libb = libd.dependencies[0].dst
-        libc = libd.dependencies[1].dst
-        liba = libc.dependencies[0].dst
+        libe = app.edges[0].dst
+        libf = app.edges[1].dst
+        libd = libe.edges[0].dst
+        libb = libd.edges[0].dst
+        libc = libd.edges[1].dst
+        liba = libc.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libe, libf])
         self._check_node(libe, "libe/0.1#123", deps=[libd], dependents=[app])
@@ -1780,10 +1780,10 @@ class TestDiamondMultiple(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        libd = app.dependencies[2].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        libd = app.edges[2].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc, libd])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -1806,10 +1806,10 @@ class TestDiamondMultiple(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        libd = app.dependencies[2].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        libd = app.edges[2].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc, libd])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -1831,9 +1831,9 @@ class TestDiamondMultiple(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        libb = app.dependencies[2].dst
+        libd = app.edges[0].dst
+        libc = app.edges[1].dst
+        libb = app.edges[2].dst
 
         self._check_node(app, "app/0.1", deps=[libd, libc, libb])
         self._check_node(libd, "libd/0.1#123", dependents=[app, libc])
@@ -1856,9 +1856,9 @@ class TestDiamondMultiple(GraphManagerTest):
         self.assertEqual(4, len(deps_graph.nodes))
 
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -1880,9 +1880,9 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = app.dependencies[1].dst
-        liba2 = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = app.edges[1].dst
+        liba2 = libb.edges[0].dst
 
         assert liba is liba2
 
@@ -1905,7 +1905,7 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
+        libb = app.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb])
@@ -1925,10 +1925,10 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
-        liba2 = libc.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba1 = libb.edges[0].dst
+        liba2 = libc.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app])
@@ -1956,8 +1956,8 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        dep1 = app.dependencies[0].dst
-        dep2 = app.dependencies[1].dst
+        dep1 = app.edges[0].dst
+        dep2 = app.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[dep1, dep2])
         self._check_node(dep1, "dep1/2.0#123", deps=[], dependents=[app, dep2])
@@ -1976,8 +1976,8 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        dep1 = app.dependencies[0].dst
-        dep2 = app.dependencies[1].dst
+        dep1 = app.edges[0].dst
+        dep2 = app.edges[1].dst
         self._check_node(app, "app/0.1", deps=[dep1, dep2])
         self._check_node(dep1, "dep1/2.0#123", deps=[], dependents=[app])
         # dep2 no dependency, it was not resolved due to conflict
@@ -1996,9 +1996,9 @@ class TransitiveOverridesGraphTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba2 = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba2 = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb, liba2])
@@ -2021,8 +2021,8 @@ class PureOverrideTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb])
@@ -2052,8 +2052,8 @@ class PureOverrideTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libb])
@@ -2077,9 +2077,9 @@ class PureOverrideTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libc])
@@ -2103,9 +2103,9 @@ class PureOverrideTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         # TODO: No Revision??? Because of consumer?
         self._check_node(app, "app/0.1", deps=[libc])
@@ -2128,9 +2128,9 @@ class PureOverrideTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         libc = deps_graph.root
-        liba = libc.dependencies[0].dst
-        libb = libc.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
+        liba = libc.edges[0].dst
+        libb = libc.edges[1].dst
+        liba1 = libb.edges[0].dst
 
         assert liba is liba1
         # TODO: No Revision??? Because of consumer?
@@ -2162,10 +2162,10 @@ class PackageIDDeductions(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         project = deps_graph.root
-        app1 = project.dependencies[0].dst
-        app2 = project.dependencies[1].dst
-        lib = app1.dependencies[0].dst
-        lib2 = app2.dependencies[0].dst
+        app1 = project.edges[0].dst
+        app2 = project.edges[1].dst
+        lib = app1.edges[0].dst
+        lib2 = app2.edges[0].dst
 
         assert lib is lib2
 
@@ -2202,10 +2202,10 @@ class TestProjectApp(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         project = deps_graph.root
-        app1 = project.dependencies[0].dst
-        app2 = project.dependencies[1].dst
-        lib = app1.dependencies[0].dst
-        lib2 = app2.dependencies[0].dst
+        app1 = project.edges[0].dst
+        app2 = project.edges[1].dst
+        lib = app1.edges[0].dst
+        lib2 = app2.edges[0].dst
 
         assert lib is lib2
 
@@ -2252,10 +2252,10 @@ class TestProjectApp(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         project = deps_graph.root
-        app1 = project.dependencies[0].dst
-        app2 = project.dependencies[1].dst
-        lib = app1.dependencies[0].dst
-        lib2 = app2.dependencies[0].dst
+        app1 = project.edges[0].dst
+        app2 = project.edges[1].dst
+        lib = app1.edges[0].dst
+        lib2 = app2.edges[0].dst
 
         assert lib is lib2
 
@@ -2305,10 +2305,10 @@ class TestProjectApp(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         project = deps_graph.root
-        app1 = project.dependencies[0].dst
-        app2 = project.dependencies[1].dst
-        lib1 = app1.dependencies[0].dst
-        lib2 = app2.dependencies[0].dst
+        app1 = project.edges[0].dst
+        app2 = project.edges[1].dst
+        lib1 = app1.edges[0].dst
+        lib2 = app2.edges[0].dst
 
         assert lib1 is not lib2
 
diff --git a/test/integration/graph/core/test_alias.py b/test/integration/graph/core/test_alias.py
index 7be872ab7..496cc3418 100644
--- a/test/integration/graph/core/test_alias.py
+++ b/test/integration/graph/core/test_alias.py
@@ -16,7 +16,7 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
+        liba = app.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[app])
         self._check_node(app, "app/0.1", deps=[liba])
@@ -32,8 +32,8 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        libb = app.dependencies[1].dst
+        liba = app.edges[0].dst
+        libb = app.edges[1].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[app, libb])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -60,8 +60,8 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        libb = app.dependencies[1].dst
+        liba = app.edges[0].dst
+        libb = app.edges[1].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[app, libb])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -90,9 +90,9 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -119,9 +119,9 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -147,7 +147,7 @@ class TestAlias(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
+        liba = app.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[app])
         self._check_node(app, "app/0.1", deps=[liba])
@@ -172,9 +172,9 @@ class AliasBuildRequiresTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba_build = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba_build = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb])
         self._check_node(liba_build, "liba/0.2#123", dependents=[app])
diff --git a/test/integration/graph/core/test_build_requires.py b/test/integration/graph/core/test_build_requires.py
index b806d09c9..4a7bf8fdd 100644
--- a/test/integration/graph/core/test_build_requires.py
+++ b/test/integration/graph/core/test_build_requires.py
@@ -45,7 +45,7 @@ class BuildRequiresGraphTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        cmake = app.dependencies[0].dst
+        cmake = app.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[cmake], dependents=[])
         self._check_node(cmake, "cmake/0.1#123", deps=[], dependents=[app])
@@ -58,8 +58,8 @@ class BuildRequiresGraphTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app])
@@ -90,9 +90,9 @@ class BuildRequiresGraphTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
-        cmakelib = cmake.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
+        cmakelib = cmake.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[], settings={"os": "Linux"})
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app],
@@ -120,9 +120,9 @@ class BuildRequiresGraphTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake2 = lib.dependencies[0].dst
-        cmake1 = cmake2.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake2 = lib.edges[0].dst
+        cmake1 = cmake2.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake2], dependents=[app])
@@ -143,9 +143,9 @@ class BuildRequiresGraphTest(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
-        zlib = cmake.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
+        zlib = cmake.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app])
@@ -173,11 +173,11 @@ class TestBuildRequiresTransitivityDiamond(GraphManagerTest):
 
         self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
-        mingw = lib.dependencies[1].dst
-        zlib1 = cmake.dependencies[0].dst
-        zlib2 = mingw.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
+        mingw = lib.edges[1].dst
+        zlib1 = cmake.edges[0].dst
+        zlib2 = mingw.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake, mingw], dependents=[app])
@@ -212,11 +212,11 @@ class TestBuildRequiresTransitivityDiamond(GraphManagerTest):
 
         self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
-        mingw = lib.dependencies[1].dst
-        zlib1 = cmake.dependencies[0].dst
-        zlib2 = mingw.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
+        mingw = lib.edges[1].dst
+        zlib1 = cmake.edges[0].dst
+        zlib2 = mingw.edges[0].dst
 
         assert zlib1 is not zlib2
 
@@ -242,9 +242,9 @@ class TestBuildRequiresTransitivityDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         cheetah = deps_graph.root
-        gazelle = cheetah.dependencies[0].dst
-        grass2 = cheetah.dependencies[1].dst
-        grass1 = gazelle.dependencies[0].dst
+        gazelle = cheetah.edges[0].dst
+        grass2 = cheetah.edges[1].dst
+        grass1 = gazelle.edges[0].dst
         self._check_node(cheetah, "cheetah/0.1", deps=[gazelle, grass2])
         self._check_node(gazelle, "gazelle/0.1#123", deps=[grass1], dependents=[cheetah])
         self._check_node(grass1, "grass/0.1#123", deps=[], dependents=[gazelle])
@@ -261,9 +261,9 @@ class TestBuildRequiresVisible(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[libc], dependents=[])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -289,7 +289,7 @@ class TestTestRequire(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        gtest = app.dependencies[0].dst
+        gtest = app.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[gtest], dependents=[])
         self._check_node(gtest, "gtest/0.1#123", deps=[], dependents=[app])
@@ -305,8 +305,8 @@ class TestTestRequire(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        gtest = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        gtest = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app])
@@ -324,8 +324,8 @@ class TestTestRequire(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        gtest = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        gtest = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app])
@@ -356,9 +356,9 @@ class TestTestRequire(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        gtest = lib.dependencies[0].dst
-        gtestlib = gtest.dependencies[0].dst
+        lib = app.edges[0].dst
+        gtest = lib.edges[0].dst
+        gtestlib = gtest.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[], settings={"os": "Linux"})
         self._check_node(lib, "lib/0.1#123", deps=[gtest], dependents=[app],
@@ -393,10 +393,10 @@ class TestTestRequire(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        gtest = lib.dependencies[1].dst
-        zlib = gtest.dependencies[0].dst
-        zlib2 = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        gtest = lib.edges[1].dst
+        zlib = gtest.edges[0].dst
+        zlib2 = lib.edges[0].dst
         assert zlib is zlib2
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
@@ -419,9 +419,9 @@ class TestTestRequire(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         opencv = deps_graph.root
-        gtest14 = opencv.dependencies[0].dst
-        abseil = gtest14.dependencies[0].dst
-        gtest11 = abseil.dependencies[0].dst
+        gtest14 = opencv.edges[0].dst
+        abseil = gtest14.edges[0].dst
+        gtest11 = abseil.edges[0].dst
 
         self._check_node(opencv, "opencv/1.0@", deps=[gtest14], dependents=[])
         self._check_node(gtest14, "gtest/1.14#123", deps=[abseil], dependents=[opencv])
@@ -435,13 +435,13 @@ class TestTestRequiresProblemsShared(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
         lib_c = deps_graph.root
         if not reverse:
-            lib_a = lib_c.dependencies[0].dst
-            util = lib_a.dependencies[0].dst
-            util2 = lib_c.dependencies[1].dst
+            lib_a = lib_c.edges[0].dst
+            util = lib_a.edges[0].dst
+            util2 = lib_c.edges[1].dst
         else:
-            util = lib_c.dependencies[0].dst
-            lib_a = lib_c.dependencies[1].dst
-            util2 = lib_a.dependencies[0].dst
+            util = lib_c.edges[0].dst
+            lib_a = lib_c.edges[1].dst
+            util2 = lib_a.edges[0].dst
         assert util is util2
 
         self._check_node(lib_c, "lib_c/0.1@", deps=[lib_a, util], dependents=[])
@@ -582,8 +582,8 @@ class BuildRequiresPackageIDTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app])
@@ -600,8 +600,8 @@ class BuildRequiresPackageIDTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app])
@@ -616,7 +616,7 @@ class BuildRequiresPackageIDTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
+        lib = app.edges[0].dst
         assert lib.package_id == "2813db72897dd13aca2af071efe8ecb116f679ed"
         assert lib.package_id != NO_SETTINGS_PACKAGE_ID
 
@@ -634,8 +634,8 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        cmake = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        cmake = lib.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[cmake], dependents=[app])
@@ -663,17 +663,17 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(8 + 4, len(deps_graph.nodes))
         app = deps_graph.root
-        liba = app.dependencies[0].dst
-        libb = liba.dependencies[0].dst
-        libsfun = libb.dependencies[0].dst
-        libx = libsfun.dependencies[0].dst
-        liby = libx.dependencies[0].dst
-        libz = liby.dependencies[0].dst
-        sfun = libb.dependencies[1].dst
-        libsfun_build = sfun.dependencies[0].dst
-        libx_build = libsfun_build.dependencies[0].dst
-        liby_build = libx_build.dependencies[0].dst
-        libz_build = liby_build.dependencies[0].dst
+        liba = app.edges[0].dst
+        libb = liba.edges[0].dst
+        libsfun = libb.edges[0].dst
+        libx = libsfun.edges[0].dst
+        liby = libx.edges[0].dst
+        libz = liby.edges[0].dst
+        sfun = libb.edges[1].dst
+        libsfun_build = sfun.edges[0].dst
+        libx_build = libsfun_build.edges[0].dst
+        liby_build = libx_build.edges[0].dst
+        libz_build = liby_build.edges[0].dst
 
         # TODO non-build-requires
 
@@ -747,9 +747,9 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        cmake1 = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        cmake1 = libb.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[libb, libc], dependents=[])
         self._check_node(libb, "libb/0.1#123", deps=[cmake1], dependents=[app])
@@ -776,11 +776,11 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
-        libd = app.dependencies[0].dst
-        libe = app.dependencies[1].dst
-        libb = libd.dependencies[0].dst
-        libc = libe.dependencies[0].dst
-        cmake1 = libb.dependencies[0].dst
+        libd = app.edges[0].dst
+        libe = app.edges[1].dst
+        libb = libd.edges[0].dst
+        libc = libe.edges[0].dst
+        cmake1 = libb.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[libd, libe], dependents=[])
         self._check_node(libd, "libd/0.1#123", deps=[libb], dependents=[app])
@@ -803,9 +803,9 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        protobuf_host = libb.dependencies[0].dst
-        protobuf_build = libb.dependencies[1].dst
+        libb = app.edges[0].dst
+        protobuf_host = libb.edges[0].dst
+        protobuf_build = libb.edges[1].dst
 
         self._check_node(app, "app/0.1@", deps=[libb], dependents=[])
         self._check_node(libb, "libb/0.1#123", deps=[protobuf_host, protobuf_build],
@@ -834,9 +834,9 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        protobuf_host = libb.dependencies[0].dst
-        protobuf_build = libb.dependencies[1].dst
+        libb = app.edges[0].dst
+        protobuf_host = libb.edges[0].dst
+        protobuf_build = libb.edges[1].dst
 
         self._check_node(app, "app/0.1@", deps=[libb], dependents=[])
         self._check_node(libb, "libb/0.1#123", deps=[protobuf_host, protobuf_build],
@@ -870,7 +870,7 @@ class PublicBuildRequiresTest(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        tool = app.dependencies[0].dst
+        tool = app.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[tool], dependents=[])
         self._check_node(tool, "gtest/0.1#123", deps=[], dependents=[app])
@@ -890,9 +890,9 @@ class TestLoops(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        tool = app.dependencies[0].dst
-        tool2 = tool.dependencies[0].dst
-        tool3 = tool2.dependencies[0].dst
+        tool = app.edges[0].dst
+        tool2 = tool.edges[0].dst
+        tool3 = tool2.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[tool], dependents=[])
         self._check_node(tool, "cmake/0.1#123", deps=[tool2], dependents=[app])
@@ -912,11 +912,11 @@ class TestLoops(GraphManagerTest):
         # Build requires always apply to the consumer
         self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
-        cmake = app.dependencies[0].dst
-        gtest = cmake.dependencies[0].dst
-        cmake2 = gtest.dependencies[0].dst
-        gtest2 = cmake2.dependencies[0].dst
-        cmake3 = gtest2.dependencies[0].dst
+        cmake = app.edges[0].dst
+        gtest = cmake.edges[0].dst
+        cmake2 = gtest.edges[0].dst
+        gtest2 = cmake2.edges[0].dst
+        cmake3 = gtest2.edges[0].dst
 
         assert deps_graph.error.ancestor == cmake
         assert deps_graph.error.node == cmake3
diff --git a/test/integration/graph/core/test_options.py b/test/integration/graph/core/test_options.py
index 086e53be2..04be8d51d 100644
--- a/test/integration/graph/core/test_options.py
+++ b/test/integration/graph/core/test_options.py
@@ -17,8 +17,8 @@ class TestOptions(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -42,8 +42,8 @@ class TestOptions(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        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])
@@ -68,10 +68,10 @@ class TestOptions(GraphManagerTest):
         deps_graph = self.build_consumer(consumer)
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
-        liba2 = libc.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
+        liba2 = libc.edges[0].dst
         assert liba is liba2
         self._check_node(app, "app/0.1", deps=[libc, libb])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -82,10 +82,10 @@ class TestOptions(GraphManagerTest):
         deps_graph = self.build_consumer(consumer)
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
-        liba2 = libc.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = app.edges[1].dst
+        liba = libb.edges[0].dst
+        liba2 = libc.edges[0].dst
         assert liba is liba2
         self._check_node(app, "app/0.1", deps=[libc, libb])
         self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -107,10 +107,10 @@ class TestOptions(GraphManagerTest):
 
             self.assertEqual(4, len(deps_graph.nodes))
             app = deps_graph.root
-            libb = app.dependencies[0].dst
-            libc = app.dependencies[1].dst
-            liba = libb.dependencies[0].dst
-            liba2 = libc.dependencies[0].dst
+            libb = app.edges[0].dst
+            libc = app.edges[1].dst
+            liba = libb.edges[0].dst
+            liba2 = libc.edges[0].dst
             assert liba is liba2
             self._check_node(app, "app/0.1", deps=[libc, libb])
             self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -125,10 +125,10 @@ class TestOptions(GraphManagerTest):
 
             self.assertEqual(4, len(deps_graph.nodes))
             app = deps_graph.root
-            libc = app.dependencies[0].dst
-            libb = app.dependencies[1].dst
-            liba = libb.dependencies[0].dst
-            liba2 = libc.dependencies[0].dst
+            libc = app.edges[0].dst
+            libb = app.edges[1].dst
+            liba = libb.edges[0].dst
+            liba2 = libc.edges[0].dst
             assert liba is liba2
             self._check_node(app, "app/0.1", deps=[libc, libb])
             self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[app])
@@ -149,11 +149,11 @@ class TestBuildRequireOptions(GraphManagerTest):
 
         self.assertEqual(6, len(deps_graph.nodes))
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        protobuf_host = lib.dependencies[0].dst
-        protobuf_build = lib.dependencies[1].dst
-        zlib_shared = protobuf_host.dependencies[0].dst
-        zlib_static = protobuf_build.dependencies[0].dst
+        lib = app.edges[0].dst
+        protobuf_host = lib.edges[0].dst
+        protobuf_build = lib.edges[1].dst
+        zlib_shared = protobuf_host.edges[0].dst
+        zlib_static = protobuf_build.edges[0].dst
 
         self._check_node(app, "app/0.1@", deps=[lib], dependents=[])
         self._check_node(lib, "lib/0.1#123", deps=[protobuf_host, protobuf_build], dependents=[app])
diff --git a/test/integration/graph/core/test_provides.py b/test/integration/graph/core/test_provides.py
index adf2b9bb8..3da1c1d9a 100644
--- a/test/integration/graph/core/test_provides.py
+++ b/test/integration/graph/core/test_provides.py
@@ -21,7 +21,7 @@ class TestProvidesTest(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
+        libb = app.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb])
         self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app])
@@ -38,8 +38,8 @@ class TestProvidesTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libb])
         self._check_node(libb, "libb/0.1#123", deps=[libc], dependents=[app])
@@ -64,8 +64,8 @@ class TestProvidesTest(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[], dependents=[app])
@@ -83,8 +83,8 @@ class TestProvidesTest(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
 
         app = deps_graph.root
-        br = app.dependencies[0].dst
-        br_lib = br.dependencies[0].dst
+        br = app.edges[0].dst
+        br_lib = br.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[br])
         self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app])
@@ -109,10 +109,10 @@ class TestProvidesTest(GraphManagerTest):
 
         self.assertEqual(5, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba1 = libb.dependencies[0].dst
-        libd2 = libc.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba1 = libb.edges[0].dst
+        libd2 = libc.edges[0].dst
         self._check_node(app, "app/0.1", deps=[libb, libc])
         self._check_node(libb, "libb/0.1#123", deps=[liba1], dependents=[app])
         self._check_node(libc, "libc/0.1#123", deps=[libd2], dependents=[app])
@@ -136,9 +136,9 @@ class TestProvidesTest(GraphManagerTest):
         self.assertEqual(4, len(deps_graph.nodes))
 
         app = deps_graph.root
-        libc = app.dependencies[0].dst
-        libb = libc.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libc = app.edges[0].dst
+        libb = libc.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[libc])
         self._check_node(libc, "libc/0.1#123", deps=[libb], dependents=[app])
@@ -158,8 +158,8 @@ class ProvidesBuildRequireTest(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
 
         app = deps_graph.root
-        br = app.dependencies[0].dst
-        br_lib = br.dependencies[0].dst
+        br = app.edges[0].dst
+        br_lib = br.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[br])
         self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app])
@@ -180,8 +180,8 @@ class ProvidesBuildRequireTest(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
 
         app = deps_graph.root
-        lib = app.dependencies[0].dst
-        br = lib.dependencies[0].dst
+        lib = app.edges[0].dst
+        br = lib.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[lib])
         self._check_node(lib, "lib/0.1#123", deps=[br], dependents=[app])
@@ -205,8 +205,8 @@ class ProvidesBuildRequireTest(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
 
         app = deps_graph.root
-        br = app.dependencies[0].dst
-        br_lib = br.dependencies[0].dst
+        br = app.edges[0].dst
+        br_lib = br.edges[0].dst
 
         self._check_node(app, "app/0.1", deps=[br])
         self._check_node(br, "br/0.1#123", deps=[br_lib], dependents=[app])
@@ -226,8 +226,8 @@ class ProvidesBuildRequireTest(GraphManagerTest):
         self.assertEqual(3, len(deps_graph.nodes))
 
         app = deps_graph.root
-        br1 = app.dependencies[0].dst
-        br2 = app.dependencies[1].dst
+        br1 = app.edges[0].dst
+        br2 = app.edges[1].dst
 
         self._check_node(app, "app/0.1", deps=[br1, br2])
         self._check_node(br1, "br1/0.1#123", deps=[], dependents=[app])
diff --git a/test/integration/graph/core/test_version_ranges.py b/test/integration/graph/core/test_version_ranges.py
index 7e4a9ec04..c49cfdb7d 100644
--- a/test/integration/graph/core/test_version_ranges.py
+++ b/test/integration/graph/core/test_version_ranges.py
@@ -19,7 +19,7 @@ class TestVersionRanges(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
+        libb = app.edges[0].dst
 
         self._check_node(libb, "libb/0.2#123", dependents=[app])
         self._check_node(app, "app/0.1", deps=[libb])
@@ -42,7 +42,7 @@ class TestVersionRanges(GraphManagerTest):
             deps_graph = self.build_consumer(consumer)
             self.assertEqual(2, len(deps_graph.nodes))
             app = deps_graph.root
-            libb = app.dependencies[0].dst
+            libb = app.edges[0].dst
 
             self._check_node(libb, f"libb/{solution}#123", dependents=[app])
             self._check_node(app, "app/0.1", deps=[libb])
@@ -115,9 +115,9 @@ class TestVersionRangesDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.2#123", dependents=[libb, libc], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -137,9 +137,9 @@ class TestVersionRangesDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.2#123", dependents=[libb, libc], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -158,9 +158,9 @@ class TestVersionRangesDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb, libc], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -181,9 +181,9 @@ class TestVersionRangesDiamond(GraphManagerTest):
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
         app.enabled_remotes = [Remote("foo", None)]
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -203,9 +203,9 @@ class TestVersionRangesDiamond(GraphManagerTest):
 
         self.assertEqual(4, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        libc = app.dependencies[1].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        libc = app.edges[1].dst
+        liba = libb.edges[0].dst
 
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -225,8 +225,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.2#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -243,8 +243,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -261,8 +261,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.1#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -281,7 +281,7 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(2, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
+        libb = app.edges[0].dst
 
     def test_transitive_fixed_conflict_forced(self):
         # app ---> libb/0.1 -----------> liba/1.2
@@ -295,8 +295,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/1.2#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -315,8 +315,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.3#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
@@ -338,8 +338,8 @@ class TestVersionRangesOverridesDiamond(GraphManagerTest):
 
         self.assertEqual(3, len(deps_graph.nodes))
         app = deps_graph.root
-        libb = app.dependencies[0].dst
-        liba = libb.dependencies[0].dst
+        libb = app.edges[0].dst
+        liba = libb.edges[0].dst
 
         self._check_node(liba, "liba/0.2#123", dependents=[libb, app], deps=[])
         self._check_node(libb, "libb/0.1#123", dependents=[app], deps=[liba])
diff --git a/test/integration/toolchains/apple/test_xcodetoolchain.py b/test/integration/toolchains/apple/test_xcodetoolchain.py
index ffae38c49..8941f162b 100644
--- a/test/integration/toolchains/apple/test_xcodetoolchain.py
+++ b/test/integration/toolchains/apple/test_xcodetoolchain.py
@@ -1,4 +1,6 @@
 import platform
+import re
+import textwrap
 
 import pytest
 
@@ -83,3 +85,45 @@ def test_flags_generated_if_only_defines():
     assert "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) MYDEFINITION" in conan_global_flags
     conan_global_file = client.load("conan_config.xcconfig")
     assert '#include "conan_global_flags.xcconfig"' in conan_global_file
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS")
+@pytest.mark.parametrize("os_name, sdk, min_version, deployment_target_flag", [
+    ("Macos", None, "11.0", "MACOSX_DEPLOYMENT_TARGET"),
+    ("iOS", "iphoneos", "18.0", "IPHONEOS_DEPLOYMENT_TARGET"),
+    ("tvOS", "appletvos", "18.4", "TVOS_DEPLOYMENT_TARGET"),
+    ("watchOS", "watchos", "9.0", "WATCHOS_DEPLOYMENT_TARGET"),
+    ("visionOS", "xros", "2.0", "XROS_DEPLOYMENT_TARGET"),
+])
+def test_xcodetoolchain_xcconfig_deplyment_target(os_name, sdk, min_version, deployment_target_flag):
+    client = TestClient()
+
+    conanfile = textwrap.dedent(f"""
+        import os
+        from conan import ConanFile
+        from conan.tools.apple import XcodeToolchain
+        from conan.tools.files import save
+        class MyApplicationConan(ConanFile):
+            name = "myapplication"
+            version = "1.0"
+            settings = "os", "compiler", "build_type", "arch"
+            def generate(self):
+                tc = XcodeToolchain(self)
+                tc.generate()
+                # Private access to get the generated xcconfig filename
+                # It changes based on the settings, so this is the less fragile way to get it
+                save(self, os.path.join(self.generators_folder, "name.txt"), tc._vars_xconfig_filename)
+        """)
+
+    client.save({"conanfile.py": conanfile})
+
+    settings = f"-s os={os_name} -s os.version={min_version}"
+    if sdk:
+        settings += f" -s os.sdk={sdk}"
+
+    client.run(f"install . -s build_type=Release {settings} --build=missing")
+
+    xcconfig_name = client.load("name.txt").strip()
+    xcconfig = client.load(xcconfig_name)
+    match = re.search(f"^{deployment_target_flag}.+={min_version}$", xcconfig, re.MULTILINE)
+    assert match is not None
diff --git a/test/integration/toolchains/gnu/test_pkgconfigdeps.py b/test/integration/toolchains/gnu/test_pkgconfigdeps.py
index e98c607ac..de7face01 100644
--- a/test/integration/toolchains/gnu/test_pkgconfigdeps.py
+++ b/test/integration/toolchains/gnu/test_pkgconfigdeps.py
@@ -1228,3 +1228,55 @@ def test_pkg_with_duplicated_component_requires():
     pc_content = client.load("mylib-myfirstcomp.pc")
     assert "Requires: mylib-mycomponent" == get_requires_from_content(pc_content)
 
+
+def test_pkg_skip_component():
+    conanfile_a = textwrap.dedent("""
+        from conan import ConanFile
+        class PkgConfigConan(ConanFile):
+            name = "pkg_a"
+            version = "0.1"
+            def package_info(self):
+                self.cpp_info.set_property("pkg_config_name", "none")
+        """)
+    conanfile_b = textwrap.dedent("""
+        from conan import ConanFile
+        class PkgConfigConan(ConanFile):
+            name = "pkg_b"
+            version = "0.1"
+            requires = "pkg_a/0.1"
+            def package_info(self):
+                self.cpp_info.components["cmp1"].set_property("pkg_config_name", "b-cmp1")
+        """)
+
+    conanfile_c = textwrap.dedent("""
+            from conan import ConanFile
+            class PkgConfigConan(ConanFile):
+                name = "pkg_c"
+                version = "0.1"
+                requires = "pkg_b/0.1"
+                def package_info(self):
+                    self.cpp_info.components["cmp2"].set_property("pkg_config_name", "none")
+            """)
+    tc = TestClient(light=True)
+    tc.save({"a/conanfile.py": conanfile_a,
+             "b/conanfile.py": conanfile_b,
+             "c/conanfile.py": conanfile_c})
+    tc.run("create a")
+    tc.run("create b")
+    tc.run("create c")
+
+    tc.run("install --requires=pkg_c/0.1 --generator=PkgConfigDeps -of=out")
+    install_contents = os.listdir(os.path.join(tc.current_folder, "out"))
+    assert "pkg_a.pc" not in install_contents
+    assert "pkg_b.pc" in install_contents
+    pkg_b_content = tc.load(os.path.join("out", "pkg_b.pc"))
+    pkg_b_requires = get_requires_from_content(pkg_b_content)
+    assert "b-cmp1" in pkg_b_requires
+    assert "pkg_a" not in pkg_b_requires
+    assert "none" not in pkg_b_requires
+    assert "b-cmp1.pc" in install_contents
+    b_cmp1_content = tc.load(os.path.join("out", "b-cmp1.pc"))
+    assert "Requires:" not in b_cmp1_content
+    assert "pkg_c.pc" in install_contents
+    # Components can not skip the PC file creation
+    assert "none.pc" in install_contents
diff --git a/test/integration/workspace/test_workspace.py b/test/integration/workspace/test_workspace.py
index a2b7d870a..3d3b27172 100644
--- a/test/integration/workspace/test_workspace.py
+++ b/test/integration/workspace/test_workspace.py
@@ -21,13 +21,13 @@ class TestWorkspaceRoot:
         c = TestClient(light=True)
         # Just check the root command works
         c.run("workspace root", assert_error=True)
-        assert "ERROR: No workspace defined, conanws.py file not found" in c.out
+        assert "ERROR: Workspace not defined, please create" in c.out
 
         # error, conanws/ folder does not contain conanws.[py | yml]
         c.save({"conanws/test.txt": ""})
         with c.chdir("conanws"):
             c.run("workspace root", assert_error=True)
-            assert "ERROR: No workspace defined, conanws.py file not found" in c.out
+            assert "ERROR: Workspace not defined, please create" in c.out
             c.save({"conanws.yml": ""})
             c.run("workspace root")
             assert c.current_folder in c.stdout
@@ -236,7 +236,7 @@ class TestAddRemove:
         c = TestClient(light=True)
         c.save({"conanws.yml": "",
                 "mydeppkg/conanfile.py": GenConanfile("mydeppkg", "0.1")})
-        c.run("workspace add mydeppkg --product")
+        c.run("workspace add mydeppkg")
         c.run("workspace remove mydeppkg")
         c.run("workspace info")
         assert "mydeppkg" not in c.out
@@ -368,31 +368,6 @@ class TestOpenAdd:
 
 
 class TestWorkspaceBuild:
-    def test_dynamic_products(self):
-        c = TestClient(light=True)
-
-        workspace = textwrap.dedent("""\
-            import os
-            from conan import Workspace
-
-            class MyWorkspace(Workspace):
-                def products(self):
-                    result = []
-                    for f in os.listdir(self.folder):
-                        if os.path.isdir(os.path.join(self.folder, f)):
-                            if f.startswith("product"):
-                                result.append(f)
-                    return result
-            """)
-
-        c.save({"conanws.py": workspace,
-                "lib1/conanfile.py": GenConanfile("lib1", "0.1"),
-                "product_app1/conanfile.py": GenConanfile("app1", "0.1")})
-        c.run("workspace add lib1")
-        c.run("workspace add product_app1")
-        c.run("workspace info --format=json")
-        info = json.loads(c.stdout)
-        assert info["products"] == ["product_app1"]
 
     def test_build(self):
         c = TestClient(light=True)
@@ -402,27 +377,25 @@ class TestWorkspaceBuild:
                 "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!")
                .with_requires("pkga/0.1")})
         c.run("workspace add pkga")
-        c.run("workspace add pkgb --product")
-        c.run("workspace info --format=json")
-        assert json.loads(c.stdout)["products"] == ["pkgb"]
+        c.run("workspace add pkgb")
         c.run("workspace build")
         c.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709",
                                              "EditableBuild")})
-        assert "pkga/0.1: WARN: BUILD PKGA!" in c.out
+        assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out
         assert "conanfile.py (pkgb/0.1): WARN: BUILD PKGB!" in c.out
 
         # It is also possible to build a specific package by path
         # equivalent to ``conan build <path> --build=editable``
         # This can be done even if it is not a product
-        c.run("workspace build pkgc", assert_error=True)
-        assert "ERROR: Product 'pkgc' not defined in the workspace as editable" in c.out
-        c.run("workspace build pkgb")
+        c.run("workspace build --pkg=pkgc/*", assert_error=True)
+        assert "ERROR: There are no selected packages defined in the workspace" in c.out
+        c.run("workspace build --pkg=pkgb/*")
         c.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709",
                                              "EditableBuild")})
-        assert "pkga/0.1: WARN: BUILD PKGA!" in c.out
+        assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out
         assert "conanfile.py (pkgb/0.1): WARN: BUILD PKGB!" in c.out
 
-        c.run("workspace build pkga")
+        c.run("workspace build --pkg=pkga/*")
         assert "conanfile.py (pkga/0.1): Calling build()" in c.out
         assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out
 
@@ -430,7 +403,51 @@ class TestWorkspaceBuild:
         c = TestClient(light=True)
         c.save({"conanws.yml": ""})
         c.run("workspace build", assert_error=True)
-        assert "There are no products defined in the workspace, can't build" in c.out
+        assert "ERROR: There are no selected packages defined in the workspace" in c.out
+
+    def test_build_external_missing(self):
+        c = TestClient(light=True)
+        c.save({"conanws.yml": ""})
+
+        c.save({"mymath/conanfile.py": GenConanfile("mymath", "0.1").with_build_msg("MyMATH!!"),
+                "pkga/conanfile.py": GenConanfile("pkga", "0.1").with_build_msg("BUILD PKGA!")
+                                                                .with_requires("mymath/0.1"),
+                "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!")
+               .with_requires("pkga/0.1")})
+        c.run("export mymath")
+        c.run("workspace add pkga")
+        c.run("workspace add pkgb")
+        c.run("workspace build", assert_error=True)
+        assert "ERROR: Missing prebuilt package for 'mymath/0.1'" in c.out
+        c.run("workspace build --build=missing")
+        assert "Workspace building external mymath/0.1" in c.out
+
+    def test_build_dynamic_name_version(self):
+        conanfile = textwrap.dedent("""\
+            from conan import ConanFile
+            class MyConan(ConanFile):
+                def build(self):
+                    self.output.info(f"Building {self.name} AND {self.version}!!!")
+            """)
+        workspace = textwrap.dedent("""\
+            import os
+            from conan import Workspace
+
+            class MyWorkspace(Workspace):
+                def packages(self):
+                    result = {}
+                    for f in os.listdir(self.folder):
+                        if os.path.isdir(os.path.join(self.folder, f)):
+                            result[f"{f}/0.1"] = {"path": f}
+                    return result
+           """)
+        c = TestClient(light=True)
+        c.save({"conanws.py": workspace,
+                "pkga/conanfile.py": conanfile})
+        c.run("workspace info")
+        assert "pkga/0.1" in c.out
+        c.run("workspace build")
+        assert "conanfile.py (pkga/0.1): Building pkga AND 0.1!!!" in c.out
 
 
 class TestNew:
@@ -469,7 +486,7 @@ class TestMeta:
                clean_first=True)
         c.run("workspace add liba")
         c.run("workspace add libb")
-        c.run("workspace install -g CMakeDeps -g CMakeToolchain -of=build --envs-generation=false")
+        c.run("workspace super-install -g CMakeDeps -g CMakeToolchain -of=build --envs-generation=false")
         assert "Workspace conanws.py not found in the workspace folder, using default" in c.out
         files = os.listdir(os.path.join(c.current_folder, "build"))
         assert "conan_toolchain.cmake" in files
@@ -498,7 +515,7 @@ class TestMeta:
         c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"),
                 "conanws.py": conanfilews})
         c.run("workspace add dep")
-        c.run("workspace install -of=build")
+        c.run("workspace super-install -of=build")
         files = os.listdir(os.path.join(c.current_folder, "build"))
         assert "conandeps.props" in files
 
@@ -518,10 +535,10 @@ class TestMeta:
         c.save({"conanws.yml": "conanfilews: myconanfilews.py",
                 "dep/conanfile.py": GenConanfile("dep", "0.1"),
                 "conanws.py": conanfilews})
-        c.run("workspace install", assert_error=True)
-        assert "ERROR: This workspace cannot be installed, it doesn't have any editable" in c.out
+        c.run("workspace super-install", assert_error=True)
+        assert "ERROR: There are no selected packages defined in the workspace" in c.out
         c.run("workspace add dep")
-        c.run("workspace install", assert_error=True)
+        c.run("workspace super-install", assert_error=True)
         assert "ERROR: Conanfile in conanws.py shouldn't have 'requires'" in c.out
 
     def test_install_partial(self):
@@ -539,8 +556,8 @@ class TestMeta:
         c.run("workspace add liba")
         c.run("workspace add libb")
         c.run("workspace add libc")
-        for arg in ("libb", "libb liba"):
-            c.run(f"workspace install {arg} -g CMakeDeps -of=build")
+        for arg in ("--pkg=libb/*", "--pkg=libb/* --pkg=liba/*"):
+            c.run(f"workspace super-install {arg} -g CMakeDeps -of=build")
             assert "dep1/0.1" in c.out
             assert "dep2/0.1" not in c.out
             assert "libc/0.1" not in c.out
@@ -580,9 +597,9 @@ class TestClean:
         pkgb = GenConanfile("pkgb", "0.1").with_requires("pkga/0.1").with_settings("build_type")
         c.save({"pkga/conanfile.py": pkga ,
                 "pkgb/conanfile.py": pkgb,
-                "pkgc/conanfile.py": GenConanfile("pkgc", "0.1")})
+                "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_requires("pkgb/0.1")})
         c.run("workspace add pkga -of=build/pkga")
-        c.run("workspace add pkgb -of=build/pkgb --product")
+        c.run("workspace add pkgb -of=build/pkgb")
         c.run("workspace add pkgc")
         c.run("workspace build")
         assert os.path.exists(os.path.join(c.current_folder, "build", "pkga"))
@@ -622,11 +639,9 @@ def test_relative_paths():
     # cd mywks
     with c.chdir("mywks"):
         c.run("workspace add ../liba")
-        c.run("workspace add ../app1 --product")
+        c.run("workspace add ../app1")
         c.run("workspace info")
         expected = textwrap.dedent("""\
-            products
-              ../app1
             packages
               app1/0.1
                 path: ../app1
@@ -639,11 +654,9 @@ def test_relative_paths():
     # cd otherwks
     with c.chdir("otherwks"):
         c.run("workspace add ../other/libb")
-        c.run("workspace add ../other/app2 --product")
+        c.run("workspace add ../other/app2")
         c.run("workspace info")
         expected = textwrap.dedent("""\
-            products
-              ../other/app2
             packages
               app2/0.1
                 path: ../other/app2
@@ -659,31 +672,265 @@ def test_host_build_require():
     c = TestClient()
     protobuf = textwrap.dedent("""\
         from conan import ConanFile
-        from conan.tools.files import save
+        from conan.tools.files import save, copy
         class Protobuf(ConanFile):
             name = "protobuf"
             version = "0.1"
             settings = "os"
 
             def layout(self):
-                self.folders.build = f"mybuild_folder_{self.settings.os}"
+                self.folders.build = f"mybuild/folder_{self.settings.os}"
+                self.cpp.build.bindirs = ["."]
 
             def build(self):
                 save(self, "myprotobuf.txt", f"MYOS={self.settings.os}!!!")
+
+            def package(self):
+                copy(self, "myprotobuf.txt", src=self.build_folder, dst=self.package_folder)
+
+            def package_info(self):
+                self.cpp_info.bindirs = ["."]
         """)
     app = textwrap.dedent("""\
+        import os
         from conan import ConanFile
+        from conan.tools.files import load
         class Protobuf(ConanFile):
             name = "app"
             version = "0.1"
             requires = "protobuf/0.1"
             tool_requires = "protobuf/0.1"
+
+            def build(self):
+                binhost = self.dependencies.host["protobuf"].cpp_info.bindir
+                binbuild = self.dependencies.build["protobuf"].cpp_info.bindir
+                host = load(self, os.path.join(binhost, "myprotobuf.txt"))
+                build = load(self, os.path.join(binbuild, "myprotobuf.txt"))
+                self.output.info(f"BUILDING WITH BINHOST: {host}")
+                self.output.info(f"BUILDING WITH BINBUILD: {build}")
         """)
     c.save({"protobuf/conanfile.py": protobuf,
             "app/conanfile.py": app})
     c.run("workspace init .")
     c.run("workspace add protobuf")
+    c.run("workspace add app")
     c.run("install app --build=editable -s:b os=Linux -s:h os=Windows")
 
-    assert c.load("protobuf/mybuild_folder_Linux/myprotobuf.txt") == "MYOS=Linux!!!"
-    assert c.load("protobuf/mybuild_folder_Windows/myprotobuf.txt") == "MYOS=Windows!!!"
+    assert c.load("protobuf/mybuild/folder_Linux/myprotobuf.txt") == "MYOS=Linux!!!"
+    assert c.load("protobuf/mybuild/folder_Windows/myprotobuf.txt") == "MYOS=Windows!!!"
+
+    shutil.rmtree(os.path.join(c.current_folder, "protobuf", "mybuild"))
+    assert not os.path.exists(os.path.join(c.current_folder, "protobuf", "mybuild"))
+    c.run("workspace build -s:b os=Linux -s:h os=Windows")
+    assert "conanfile.py (app/0.1): BUILDING WITH BINHOST: MYOS=Windows!!!" in c.out
+    assert "conanfile.py (app/0.1): BUILDING WITH BINBUILD: MYOS=Linux!!!" in c.out
+    assert c.load("protobuf/mybuild/folder_Linux/myprotobuf.txt") == "MYOS=Linux!!!"
+    assert c.load("protobuf/mybuild/folder_Windows/myprotobuf.txt") == "MYOS=Windows!!!"
+
+    shutil.rmtree(os.path.join(c.current_folder, "protobuf", "mybuild"))
+    assert not os.path.exists(os.path.join(c.current_folder, "protobuf", "mybuild"))
+    c.run("workspace create -s:b os=Linux -s:h os=Windows")
+    assert "app/0.1: BUILDING WITH BINHOST: MYOS=Windows!!!" in c.out
+    assert "app/0.1: BUILDING WITH BINBUILD: MYOS=Linux!!!" in c.out
+
+
+class TestCreate:
+    def test_create(self):
+        c = TestClient(light=True, default_server_user=True)
+        c.save({"conanfile.py": GenConanfile("zlib", "0.1").with_build_msg("BUILD ZLIB!")})
+        c.run("export .")
+        c.save({"conanws.yml": ""}, clean_first=True)
+        c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_requires("zlib/0.1")
+                                                                .with_build_msg("BUILD PKGA!"),
+                "pkga/test_package/conanfile.py": GenConanfile().with_test("pass"),
+                "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_build_msg("BUILD PKGB!")
+               .with_requires("pkga/0.1"),
+                "pkgb/test_package/conanfile.py": GenConanfile().with_test("pass"),
+                "pkgc/conanfile.py": GenConanfile("pkgc", "0.1").with_build_msg("BUILD PKGC!")
+               .with_requires("pkgb/0.1"),
+                "pkgc/test_package/conanfile.py": GenConanfile().with_test("pass"),
+                })
+        c.run("workspace add pkga")
+        c.run("workspace add pkgb")
+        c.run("workspace add pkgc")
+        c.run("workspace create --build=zlib/*")
+        assert str(c.out).count("zlib/0.1: WARN: BUILD ZLIB!") == 1
+        assert str(c.out).count("pkga/0.1: WARN: BUILD PKGA!") == 1
+        assert str(c.out).count("pkgb/0.1: WARN: BUILD PKGB!") == 1
+        assert str(c.out).count("pkgc/0.1: WARN: BUILD PKGC!") == 1
+        assert "pkga/0.1 (test package): Running test()" in c.out
+        assert "pkgb/0.1 (test package): Running test()" in c.out
+        assert "pkgc/0.1 (test package): Running test()" in c.out
+
+        # A second conan workspace create will not re-build
+        c.run("workspace create")
+        assert "WARN: BUILD ZLIB!" not in c.out
+        assert "WARN: BUILD PKGA!" not in c.out
+        assert "WARN: BUILD PKGB!" not in c.out
+        assert "WARN: BUILD PKBC!" not in c.out
+        assert "pkga/0.1 (test package): Running test()" not in c.out
+        assert "pkgb/0.1 (test package): Running test()" not in c.out
+        assert "pkgc/0.1 (test package): Running test()" not in c.out
+
+        c.run("upload zlib/* -r=default -c")
+        c.run("remove zlib/* -c")
+        c.run("remove pkgc/* -c")
+        c.run("workspace create")
+        assert "WARN: BUILD ZLIB!" not in c.out
+        assert "pkgc/0.1: WARN: BUILD PKGC!" in c.out
+
+
+    def test_create_dynamic_name(self):
+        workspace = textwrap.dedent("""\
+            import os
+            from conan import Workspace
+
+            class MyWorkspace(Workspace):
+                def packages(self):
+                    result = {}
+                    for f in os.listdir(self.folder):
+                        if os.path.isdir(os.path.join(self.folder, f)):
+                            result[f"{f}/0.1"] = {"path": f}
+                    return result
+           """)
+        c = TestClient(light=True)
+        c.save({"conanws.py": workspace,
+                "pkga/conanfile.py": GenConanfile()})
+        c.run("workspace info")
+        assert "pkga/0.1" in c.out
+        c.run("workspace create")
+        assert "Workspace create pkga/0.1" in c.out
+
+
+    def test_host_build_require(self):
+        c = TestClient()
+        protobuf = textwrap.dedent("""\
+            from conan import ConanFile
+            class Protobuf(ConanFile):
+                name = "protobuf"
+                version = "0.1"
+                settings = "os"
+
+                def build(self):
+                    self.output.info(f"Building for: {self.settings.os}!!!")
+            """)
+        app = textwrap.dedent("""\
+            from conan import ConanFile
+            class Protobuf(ConanFile):
+                name = "app"
+                version = "0.1"
+                requires = "protobuf/0.1"
+                tool_requires = "protobuf/0.1"
+            """)
+        c.save({"protobuf/conanfile.py": protobuf,
+                "app/conanfile.py": app})
+        c.run("workspace init .")
+        c.run("workspace add protobuf")
+        c.run("workspace add app")
+
+        c.run("workspace create -s:b os=Linux -s:h os=Windows")
+
+        assert "protobuf/0.1: Building for: Windows!!!" in c.out
+        assert "protobuf/0.1: Building for: Linux!!!" in c.out
+
+
+class TestSource:
+    def test_source(self):
+        c = TestClient(light=True)
+        c.save({"conanws.yml": ""})
+
+        pkga = textwrap.dedent("""\
+            from conan import ConanFile
+            class TestPackage(ConanFile):
+                name = "pkga"
+                version = "0.1"
+                def source(self):
+                    self.output.info("Executing SOURCE!!!")
+            """)
+        pkgb = textwrap.dedent("""\
+            from conan import ConanFile
+            class TestPackage(ConanFile):
+                name = "pkgb"
+                version = "0.1"
+                requires = "pkga/0.1"
+                def source(self):
+                    self.output.info("Executing SOURCE!!!")
+            """)
+        pkgc = textwrap.dedent("""\
+            from conan import ConanFile
+            class TestPackage(ConanFile):
+                name = "pkgc"
+                version = "0.1"
+                requires = "pkgb/0.1"
+                def source(self):
+                    self.output.info("Executing SOURCE!!!")
+            """)
+        c.save({"pkga/conanfile.py": pkga,
+                "pkgb/conanfile.py": pkgb,
+                "pkgc/conanfile.py": pkgc})
+        c.run("workspace add pkga")
+        c.run("workspace add pkgb")
+        c.run("workspace add pkgc")
+        c.run("workspace source")
+        assert "conanfile.py (pkga/0.1): Executing SOURCE!!!" in c.out
+        assert "conanfile.py (pkgb/0.1): Executing SOURCE!!!" in c.out
+        assert "conanfile.py (pkgc/0.1): Executing SOURCE!!!" in c.out
+
+
+class TestInstall:
+    def test_install(self):
+        c = TestClient()
+        c.save({"conanws.yml": ""})
+
+        pkg = textwrap.dedent("""\
+            from conan import ConanFile
+            class Package(ConanFile):
+                name = "{}"
+                version = "0.1"
+                settings = "os", "build_type"
+                {}
+                generators = "CMakeToolchain"
+            """)
+
+        c.save({"pkga/conanfile.py": pkg.format("pkga", ''),
+                "pkgb/conanfile.py": pkg.format("pkgb", 'requires="pkga/0.1"'),
+                "pkgc/conanfile.py": pkg.format("pkgc", 'requires="pkgb/0.1"')})
+        c.run("workspace add pkga")
+        c.run("workspace add pkgb")
+        c.run("workspace add pkgc")
+        c.run("workspace install")
+        assert "conanfile.py (pkga/0.1): CMakeToolchain generated" in c.out
+        assert "conanfile.py (pkgb/0.1): CMakeToolchain generated" in c.out
+        assert "conanfile.py (pkgc/0.1): CMakeToolchain generated" in c.out
+
+
+def test_keep_core_conf():
+    c = TestClient()
+    myprofile = textwrap.dedent("""\
+        [settings]
+        os=FreeBSD
+        arch=armv7
+        """)
+    conanfile = textwrap.dedent("""\
+        from conan import ConanFile
+
+        class Pkg(ConanFile):
+            name = "pkga"
+            version = "0.1"
+            settings = "os", "arch"
+
+            def generate(self):
+                self.output.info(f"Generating!: {self.settings.os}-{self.settings.arch}!!!!")
+        """)
+    c.save_home({"profiles/myprofile": myprofile})
+    c.run("profile show ")
+    c.save({"conanws.yml": "",
+            "pkga/conanfile.py": conanfile})
+    c.run("workspace add pkga")
+    # The injected -cc is still applied to every "conan install"
+    c.run("workspace install -cc core:default_profile=myprofile")
+    assert "conanfile.py (pkga/0.1): Generating!: FreeBSD-armv7!!!!" in c.out
+    # also the global.conf
+    c.save_home({"global.conf": "core:default_profile=myprofile"})
+    c.run("workspace install")
+    assert "conanfile.py (pkga/0.1): Generating!: FreeBSD-armv7!!!!" in c.out