Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
memsharded committed Dec 30, 2024
1 parent 91b81b1 commit 48f203f
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 23 deletions.
11 changes: 7 additions & 4 deletions conan/api/subapi/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,23 @@ def add(self, path, name=None, version=None, user=None, channel=None, cwd=None,
@param product:
@return: The reference of the added package
"""
path = self._conan_api.local.get_conanfile_path(path, cwd, py=True)
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, product=product)
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)
20 changes: 13 additions & 7 deletions conan/cli/commands/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ 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)
Expand All @@ -123,21 +125,25 @@ def workspace_build(conan_api: ConanAPI, parser, subparser, *args):
profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)
print_profiles(profile_host, profile_build)

products = conan_api.workspace.products
ConanOutput().title(f"Building workspace products {products}")

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
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}")
editable = editables.get(product)
if editable is None:
raise ConanException(f"Product {product} not defined in the workspace as editable")
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,
Expand All @@ -150,7 +156,7 @@ def workspace_build(conan_api: ConanAPI, parser, subparser, *args):
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}")
ConanOutput().title(f"Calling build() for the product {product_ref}")
conanfile = deps_graph.root.conanfile
conan_api.local.build(conanfile)

Expand Down
21 changes: 16 additions & 5 deletions conan/internal/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def name(self):

@property
def products(self):
return [RecipeReference.loads(p) for p in self._attr("products") or []]
return self._attr("products")

@property
def folder(self):
Expand Down Expand Up @@ -113,12 +113,14 @@ def add(self, ref, path, output_folder, product=False):
"""
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(str(ref))
self._yml.setdefault("products", []).append(path)
save(self._yml_file, yaml.dump(self._yml))

def _rel_path(self, path):
Expand All @@ -132,13 +134,19 @@ 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:
Expand All @@ -148,13 +156,16 @@ def remove(self, path):
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 Down
54 changes: 47 additions & 7 deletions test/integration/workspace/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,7 @@ def editables():
name = open(os.path.join(workspace_folder, f, "name.txt")).read().strip()
version = open(os.path.join(workspace_folder, f,
"version.txt")).read().strip()
p = os.path.join(f, "conanfile.py").replace("\\\\", "/")
result[f"{name}/{version}"] = {"path": p}
result[f"{name}/{version}"] = {"path": f}
return result
""")
else:
Expand All @@ -166,8 +165,7 @@ def editables(*args, **kwargs):
result = {}
for f in os.listdir(workspace_api.folder):
if os.path.isdir(os.path.join(workspace_api.folder, f)):
f = os.path.join(f, "conanfile.py").replace("\\\\", "/")
conanfile = workspace_api.load(f)
conanfile = workspace_api.load(os.path.join(f, "conanfile.py"))
result[f"{conanfile.name}/{conanfile.version}"] = {"path": f}
return result
""")
Expand All @@ -178,12 +176,12 @@ def editables(*args, **kwargs):
"dep1/version.txt": "2.1"})
c.run("workspace info --format=json")
info = json.loads(c.stdout)
assert info["editables"] == {"pkg/2.1": {"path": "dep1/conanfile.py"}}
assert info["editables"] == {"pkg/2.1": {"path": "dep1"}}
c.save({"dep1/name.txt": "other",
"dep1/version.txt": "14.5"})
c.run("workspace info --format=json")
info = json.loads(c.stdout)
assert info["editables"] == {"other/14.5": {"path": "dep1/conanfile.py"}}
assert info["editables"] == {"other/14.5": {"path": "dep1"}}
c.run("install --requires=other/14.5")
# Doesn't fail
assert "other/14.5 - Editable" in c.out
Expand Down Expand Up @@ -322,6 +320,33 @@ def test_workspace_build_editables(self):


class TestWorkspaceBuild:
def test_dynamic_products(self):
c = TestClient(light=True)

workspace = textwrap.dedent("""\
import os
name = "myws"
workspace_folder = os.path.dirname(os.path.abspath(__file__))
def products():
result = []
for f in os.listdir(workspace_folder):
if os.path.isdir(os.path.join(workspace_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)
c.save({"conanws.yml": ""})
Expand All @@ -332,9 +357,24 @@ def test_build(self):
c.run("workspace add pkga")
c.run("workspace add pkgb --product")
c.run("workspace info --format=json")
assert json.loads(c.stdout)["products"] == ["pkgb/0.1"]
assert json.loads(c.stdout)["products"] == ["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 (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.assert_listed_binary({"pkga/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709",
"EditableBuild")})
assert "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")
assert "conanfile.py (pkga/0.1): Calling build()" in c.out
assert "conanfile.py (pkga/0.1): WARN: BUILD PKGA!" in c.out

0 comments on commit 48f203f

Please sign in to comment.