Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

first sketch for workspace build #17538

Merged
merged 7 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion conan/api/subapi/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def get_builtin_template(template_name):
from conan.internal.api.new.autoools_exe import autotools_exe_files
from conan.internal.api.new.local_recipes_index import local_recipes_index_files
from conan.internal.api.new.qbs_lib import qbs_lib_files
from conan.internal.api.new.workspace import workspace_files
new_templates = {"basic": basic_file,
"cmake_lib": cmake_lib_files,
"cmake_exe": cmake_exe_files,
Expand All @@ -48,7 +49,8 @@ def get_builtin_template(template_name):
"autotools_exe": autotools_exe_files,
"alias": alias_file,
"local_recipes_index": local_recipes_index_files,
"qbs_lib": qbs_lib_files}
"qbs_lib": qbs_lib_files,
"workspace": workspace_files}
memsharded marked this conversation as resolved.
Show resolved Hide resolved
template_files = new_templates.get(template_name)
return template_files

Expand Down
36 changes: 31 additions & 5 deletions conan/api/subapi/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ def __init__(self, conan_api):
self._workspace = Workspace(conan_api)

def home_folder(self):
"""
@return: The custom defined Conan home/cache folder if defined, else None
"""
return self._workspace.home_folder()

def folder(self):
"""
@return: the current workspace folder where the conanws.yml or conanws.py is located
"""
return self._workspace.folder

def config_folder(self):
Expand All @@ -31,6 +37,10 @@ def config_folder(self):
def editable_packages(self):
return self._workspace.editables()

@property
def products(self):
return self._workspace.products

def open(self, require, remotes, cwd=None):
app = ConanApp(self._conan_api)
ref = RecipeReference.loads(require)
Expand Down Expand Up @@ -63,21 +73,37 @@ def open(self, require, remotes, cwd=None):
return dst_path

def add(self, path, name=None, version=None, user=None, channel=None, cwd=None,
output_folder=None, remotes=None):
path = self._conan_api.local.get_conanfile_path(path, cwd, py=True)
output_folder=None, remotes=None, product=False):
"""
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
@param name: (optional) The name of the package to be added if not defined in recipe
@param version:
@param user:
@param channel:
@param cwd:
@param output_folder:
@param remotes:
@param product:
@return: The reference of the added package
"""
full_path = self._conan_api.local.get_conanfile_path(path, cwd, py=True)
app = ConanApp(self._conan_api)
conanfile = app.loader.load_named(path, name, version, user, channel, remotes=remotes)
conanfile = app.loader.load_named(full_path, name, version, user, channel, remotes=remotes)
if conanfile.name is None or conanfile.version is None:
raise ConanException("Editable package recipe should declare its name and version")
ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel)
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._workspace.add(ref, path, output_folder=output_folder)
self._workspace.add(ref, full_path, output_folder=output_folder, product=product)
return ref

def remove(self, path):
def remove(self, path, cwd=None):
return self._workspace.remove(path)

def info(self):
return self._workspace.serialize()

def editable_from_path(self, path):
return self._workspace.editable_from_path(path)
2 changes: 1 addition & 1 deletion conan/cli/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def new(conan_api, parser, *args):
"either a predefined built-in or a user-provided one. "
"Available built-in templates: basic, cmake_lib, cmake_exe, "
"meson_lib, meson_exe, msbuild_lib, msbuild_exe, bazel_lib, bazel_exe, "
"autotools_lib, autotools_exe, local_recipes_index. "
"autotools_lib, autotools_exe, local_recipes_index, workspace. "
"E.g. 'conan new cmake_lib -d name=hello -d version=0.1'. "
"You can define your own templates too by inputting an absolute path "
"as your template, or a path relative to your conan home folder."
Expand Down
67 changes: 65 additions & 2 deletions conan/cli/commands/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from conan.api.conan_api import ConanAPI
from conan.api.output import ConanOutput, cli_out_write
from conan.cli import make_abs_path
from conan.cli.args import add_reference_args
from conan.cli.args import add_reference_args, add_common_install_arguments, add_lockfile_args
from conan.cli.command import conan_command, conan_subcommand
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


Expand Down Expand Up @@ -56,6 +58,7 @@ 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")
Expand All @@ -67,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)
cwd, args.output_folder, remotes=remotes, product=args.product)
ConanOutput().success("Reference '{}' added to workspace".format(ref))


Expand Down Expand Up @@ -101,6 +104,66 @@ def workspace_info(conan_api: ConanAPI, parser, subparser, *args):
return {"info": conan_api.workspace.info()}


@conan_subcommand()
def workspace_build(conan_api: ConanAPI, parser, subparser, *args):
"""
Build the current workspace, starting from the "products"
"""
subparser.add_argument("path", nargs="?",
help='Path to a package folder in the user workspace')
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 or []
if "editable" not in build_mode:
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
memsharded marked this conversation as resolved.
Show resolved Hide resolved
if products is None:
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)


@conan_command(group="Consumer")
def workspace(conan_api, parser, *args):
"""
Expand Down
33 changes: 33 additions & 0 deletions conan/internal/api/new/workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from conan.api.subapi.new import NewAPI
from conan.internal.api.new.cmake_exe import cmake_exe_files
from conan.internal.api.new.cmake_lib import cmake_lib_files


conanws_yml = """\
editables:
liba/0.1:
path: liba
libb/0.1:
path: libb
app1/0.1:
path: app1
products:
- app1
"""

workspace_files = {"conanws.yml": conanws_yml}

for lib in ("liba", "libb"):
definitions = {"name": lib}
if lib == "libb":
definitions["requires"] = ["liba/0.1"]
elif lib == "libc":
definitions["requires"] = ["libb/0.1"]
files = NewAPI.render(cmake_lib_files, definitions)
files = {f"{lib}/{k}": v for k, v in files.items()}
workspace_files.update(files)


files = NewAPI.render(cmake_exe_files, definitions={"name": "app1", "requires": ["libb/0.1"]})
files = {f"app1/{k}": v for k, v in files.items()}
workspace_files.update(files)
28 changes: 24 additions & 4 deletions conan/internal/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ def __init__(self, conan_api):
def name(self):
return self._attr("name") or os.path.basename(self._folder)

@property
def products(self):
return self._attr("products")

@property
def folder(self):
return self._folder
Expand Down Expand Up @@ -102,17 +106,21 @@ def _check_ws(self):
raise ConanException("Workspace not defined, please create a "
"'conanws.py' or 'conanws.yml' file")

def add(self, ref, path, output_folder):
def add(self, ref, path, output_folder, product=False):
"""
Add a new editable to the current workspace 'conanws.yml' file.
If existing, the 'conanws.py' must use this via 'conanws_data' attribute
"""
self._check_ws()
self._yml = self._yml or {}
editable = {"path": self._rel_path(path)}
assert os.path.isfile(path)
path = self._rel_path(os.path.dirname(path))
editable = {"path": path}
if output_folder:
editable["output_folder"] = self._rel_path(output_folder)
self._yml.setdefault("editables", {})[str(ref)] = editable
if product:
self._yml.setdefault("products", []).append(path)
save(self._yml_file, yaml.dump(self._yml))

def _rel_path(self, path):
Expand All @@ -126,29 +134,40 @@ def _rel_path(self, path):
f"{self._folder}")
return path.replace("\\", "/") # Normalize to unix path

def editable_from_path(self, path):
editables = self._attr("editables")
for ref, info in editables.items():
if info["path"].replace("\\", "/") == path:
return RecipeReference.loads(ref)

def remove(self, path):
self._check_ws()
self._yml = self._yml or {}
found_ref = None
path = self._rel_path(path)
for ref, info in self._yml.get("editables", {}).items():
if os.path.dirname(info["path"]).replace("\\", "/") == path:
if info["path"].replace("\\", "/") == path:
found_ref = ref
break
if not found_ref:
raise ConanException(f"No editable package to remove from this path: {path}")
self._yml["editables"].pop(found_ref)
if path in self._yml.get("products", []):
self._yml["products"].remove(path)
save(self._yml_file, yaml.dump(self._yml))
return found_ref

def editables(self):
"""
@return: Returns {RecipeReference: {"path": full abs-path, "output_folder": abs-path}}
"""
if not self._folder:
return
editables = self._attr("editables")
if editables:
editables = {RecipeReference.loads(r): v.copy() for r, v in editables.items()}
for v in editables.values():
v["path"] = os.path.normpath(os.path.join(self._folder, v["path"]))
v["path"] = os.path.normpath(os.path.join(self._folder, v["path"], "conanfile.py"))
if v.get("output_folder"):
v["output_folder"] = os.path.normpath(os.path.join(self._folder,
v["output_folder"]))
Expand All @@ -158,4 +177,5 @@ def serialize(self):
self._check_ws()
return {"name": self.name,
"folder": self._folder,
"products": self._attr("products"),
"editables": self._attr("editables")}
Loading
Loading