Repository: conan-io/conan
Pull request #18334 (2025-05-21 05:52:57)

[workspace] Packages/products do not need to be within the WS folder
Changelog: Feature: Packages/products do not need to be within the `workspace` folder
Docs: https://github.com/conan-io/docs/pull/XXXX
Closes: https://github.com/conan-io/conan/issues/18271



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 621f5aa60..b0d3ede2d 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -2,11 +2,11 @@ import json
 import os
 import shutil
 import tarfile
-import tempfile
+from io import BytesIO
 
 from conan.api.model import PackagesList
 from conan.api.output import ConanOutput
-from conan.internal.api.uploader import compress_files
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.conan_reference_layout import EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, \
     METADATA, DOWNLOAD_EXPORT_FOLDER
@@ -17,7 +17,7 @@ from conan.errors import ConanException
 from conan.api.model import PkgReference
 from conan.api.model import RecipeReference
 from conan.internal.util.dates import revision_timestamp_now
-from conan.internal.util.files import rmdir, mkdir, remove, save
+from conan.internal.util.files import rmdir, mkdir, remove
 
 
 class CacheAPI:
@@ -131,49 +131,50 @@ class CacheAPI:
         cache_folder = cache.store  # Note, this is not the home, but the actual package cache
         out = ConanOutput()
         mkdir(os.path.dirname(tgz_path))
+        name = os.path.basename(tgz_path)
         compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int)
-        tar_files: dict[str,str] = {} # {path_in_tar: abs_path}
-
-        for ref, ref_bundle in package_list.refs().items():
-            ref_layout = cache.recipe_layout(ref)
-            recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder)
-            recipe_folder = recipe_folder.replace("\\", "/")  # make win paths portable
-            ref_bundle["recipe_folder"] = recipe_folder
-            out.info(f"Saving {ref}: {recipe_folder}")
-            # Package only selected folders, not DOWNLOAD one
-            for f in (EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER):
-                if f == SRC_FOLDER and no_source:
-                    continue
-                path = os.path.join(cache_folder, recipe_folder, f)
+        with open(tgz_path, "wb") as tgz_handle:
+            tgz = gzopen_without_timestamps(name, fileobj=tgz_handle,
+                                            compresslevel=compresslevel)
+            for ref, ref_bundle in package_list.refs().items():
+                ref_layout = cache.recipe_layout(ref)
+                recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder)
+                recipe_folder = recipe_folder.replace("\\", "/")  # make win paths portable
+                ref_bundle["recipe_folder"] = recipe_folder
+                out.info(f"Saving {ref}: {recipe_folder}")
+                # Package only selected folders, not DOWNLOAD one
+                for f in (EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER):
+                    if f == SRC_FOLDER and no_source:
+                        continue
+                    path = os.path.join(cache_folder, recipe_folder, f)
+                    if os.path.exists(path):
+                        tgz.add(path, f"{recipe_folder}/{f}", recursive=True)
+                path = os.path.join(cache_folder, recipe_folder, DOWNLOAD_EXPORT_FOLDER, METADATA)
                 if os.path.exists(path):
-                    tar_files[f"{recipe_folder}/{f}"] = path
-            path = os.path.join(cache_folder, recipe_folder, DOWNLOAD_EXPORT_FOLDER, METADATA)
-            if os.path.exists(path):
-                tar_files[f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}"] = path
-
-            for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
-                pref_layout = cache.pkg_layout(pref)
-                pkg_folder = pref_layout.package()
-                folder = os.path.relpath(pkg_folder, cache_folder)
-                folder = folder.replace("\\", "/")  # make win paths portable
-                pref_bundle["package_folder"] = folder
-                out.info(f"Saving {pref}: {folder}")
-                tar_files[folder] = os.path.join(cache_folder, folder)
-
-                if os.path.exists(pref_layout.metadata()):
-                    metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder)
-                    metadata_folder = metadata_folder.replace("\\", "/")  # make paths portable
-                    pref_bundle["metadata_folder"] = metadata_folder
-                    out.info(f"Saving {pref} metadata: {metadata_folder}")
-                    tar_files[metadata_folder] = os.path.join(cache_folder, metadata_folder)
-
-        # Create a temporary file in order to reuse compress_files functionality
-        serialized = json.dumps(package_list.serialize(), indent=2)
-        pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json")
-        save(pkglist_path, serialized)
-        tar_files["pkglist.json"] = pkglist_path
-        compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, recursive=True)
-        remove(pkglist_path)
+                    tgz.add(path, f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}",
+                            recursive=True)
+
+                for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
+                    pref_layout = cache.pkg_layout(pref)
+                    pkg_folder = pref_layout.package()
+                    folder = os.path.relpath(pkg_folder, cache_folder)
+                    folder = folder.replace("\\", "/")  # make win paths portable
+                    pref_bundle["package_folder"] = folder
+                    out.info(f"Saving {pref}: {folder}")
+                    tgz.add(os.path.join(cache_folder, folder), folder, recursive=True)
+                    if os.path.exists(pref_layout.metadata()):
+                        metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder)
+                        metadata_folder = metadata_folder.replace("\\", "/")  # make paths portable
+                        pref_bundle["metadata_folder"] = metadata_folder
+                        out.info(f"Saving {pref} metadata: {metadata_folder}")
+                        tgz.add(os.path.join(cache_folder, metadata_folder), metadata_folder,
+                                recursive=True)
+            serialized = json.dumps(package_list.serialize(), indent=2)
+            info = tarfile.TarInfo(name="pkglist.json")
+            data = serialized.encode('utf-8')
+            info.size = len(data)
+            tgz.addfile(tarinfo=info, fileobj=BytesIO(data))
+            tgz.close()
 
     def restore(self, path):
         if not os.path.isfile(path):
diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py
index 10d04f774..70b99f379 100644
--- a/conan/api/subapi/workspace.py
+++ b/conan/api/subapi/workspace.py
@@ -1,6 +1,7 @@
 import inspect
 import os
 import shutil
+import textwrap
 from pathlib import Path
 
 from conan import ConanFile
@@ -9,7 +10,7 @@ from conan.api.output import ConanOutput
 from conan.cli import make_abs_path
 from conan.errors import ConanException
 from conan.internal.conan_app import ConanApp
-from conan.internal.model.workspace import Workspace
+from conan.internal.model.workspace import Workspace, WORKSPACE_YML, WORKSPACE_PY
 from conan.tools.scm import Git
 from conan.internal.graph.graph import RECIPE_EDITABLE, DepsGraph, CONTEXT_HOST, RECIPE_VIRTUAL, Node, \
     RECIPE_CONSUMER
@@ -17,13 +18,13 @@ from conan.internal.graph.graph import TransitiveRequirement
 from conan.internal.graph.profile_node_definer import consumer_definer
 from conan.internal.loader import load_python_file
 from conan.internal.source import retrieve_exports_sources
-from conan.internal.util.files import merge_directories
+from conan.internal.util.files import merge_directories, save
 
 
 def _find_ws_folder():
     path = Path(os.getcwd())
     while path.is_dir() and len(path.parts) > 1:  # finish at '/'
-        if (path / "conanws.yml").is_file() or (path / "conanws.py").is_file():
+        if (path / WORKSPACE_YML).is_file() or (path / WORKSPACE_PY).is_file():
             return str(path)
         else:
             path = path.parent
@@ -32,10 +33,10 @@ def _find_ws_folder():
 def _load_workspace(ws_folder, conan_api):
     """ loads a conanfile basic object without evaluating anything, returns the module too
     """
-    wspy = os.path.join(ws_folder, "conanws.py")
+    wspy = os.path.join(ws_folder, WORKSPACE_PY)
     if not os.path.isfile(wspy):
-        ConanOutput().info(f"conanws.py doesn't exist in {ws_folder}, using default behavior")
-        assert os.path.exists(os.path.join(ws_folder, "conanws.yml"))
+        ConanOutput().info(f"{WORKSPACE_PY} doesn't exist in {ws_folder}, using default behavior")
+        assert os.path.exists(os.path.join(ws_folder, WORKSPACE_YML))
         ws = Workspace(ws_folder, conan_api)
     else:
         try:
@@ -43,7 +44,7 @@ def _load_workspace(ws_folder, conan_api):
             ws = _parse_module(module, module_id)
             ws = ws(ws_folder, conan_api)
         except ConanException as e:
-            raise ConanException(f"Error loading conanws.py at '{wspy}': {e}")
+            raise ConanException(f"Error loading {WORKSPACE_PY} at '{wspy}': {e}")
     return ws
 
 
@@ -170,8 +171,8 @@ class WorkspaceAPI:
 
     def _check_ws(self):
         if not self._folder:
-            raise ConanException("Workspace not defined, please create a "
-                                 "'conanws.py' or 'conanws.yml' file")
+            raise ConanException(f"Workspace not defined, please create a "
+                                 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):
@@ -201,6 +202,30 @@ class WorkspaceAPI:
         self._ws.add(ref, full_path, output_folder, product)
         return ref
 
+    @staticmethod
+    def init(path):
+        abs_path = make_abs_path(path)
+        os.makedirs(abs_path, exist_ok=True)
+        ws_yml_file = Path(abs_path, WORKSPACE_YML)
+        ws_py_file = Path(abs_path, WORKSPACE_PY)
+        if not ws_yml_file.exists():
+            ConanOutput().success(f"Created empty {WORKSPACE_YML} in {path}")
+            save(ws_yml_file, "")
+        if not ws_py_file.exists():
+            ConanOutput().success(f"Created minimal {WORKSPACE_PY} in {path}")
+            ws_name = os.path.basename(abs_path)
+            save(ws_py_file, textwrap.dedent(f'''\
+            from conan import Workspace
+
+            class MyWorkspace(Workspace):
+               """
+               Minimal Workspace class definition.
+               More info: https://docs.conan.io/2/incubating.html#workspaces
+               """
+               def name(self):
+                  return "{ws_name}"
+            '''))
+
     def remove(self, path):
         self._check_ws()
         return self._ws.remove(path)
@@ -223,11 +248,11 @@ class WorkspaceAPI:
                 return RecipeReference.loads(ref)
 
     def collapse_editables(self, deps_graph, profile_host, profile_build):
-        ConanOutput().title(f"Collapsing workspace editables")
+        ConanOutput().title("Collapsing workspace editables")
 
         root_class = self._ws.root_conanfile()
         if root_class is not None:
-            conanfile = root_class("conanws.py base project Conanfile")
+            conanfile = root_class(f"{WORKSPACE_PY} base project Conanfile")
             consumer_definer(conanfile, profile_host, profile_build)
             root = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER,
                         path=self._folder)  # path lets use the conanws.py folder
@@ -237,7 +262,7 @@ class WorkspaceAPI:
                 if getattr(conanfile, field, None):
                     raise ConanException(f"Conanfile in conanws.py shouldn't have '{field}'")
         else:
-            ConanOutput().info("Workspace conanfilews.py not found in the workspace folder, "
+            ConanOutput().info(f"Workspace {WORKSPACE_PY} not found in the workspace folder, "
                                "using default behavior")
             conanfile = ConanFile(display_name="cli")
             consumer_definer(conanfile, profile_host, profile_build)
diff --git a/conan/cli/commands/workspace.py b/conan/cli/commands/workspace.py
index 871ae2bcb..f1ea664e1 100644
--- a/conan/cli/commands/workspace.py
+++ b/conan/cli/commands/workspace.py
@@ -225,6 +225,18 @@ def workspace_clean(conan_api: ConanAPI, parser, subparser, *args):
     conan_api.workspace.clean()
 
 
+@conan_subcommand()
+def workspace_init(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Clean the temporary build folders when possible
+    """
+    subparser.add_argument("path", nargs="?", default=os.getcwd(),
+                        help="Path to a folder where the workspace will be initialized. "
+                             "If  does not exist")
+    args = parser.parse_args(*args)
+    conan_api.workspace.init(args.path)
+
+
 @conan_command(group="Consumer")
 def workspace(conan_api, parser, *args):  # noqa
     """
diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py
index 44681eebb..b075e4f07 100644
--- a/conan/internal/api/uploader.py
+++ b/conan/internal/api/uploader.py
@@ -271,17 +271,16 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None):
     return t
 
 
-def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False):
+def compress_files(files, name, dest_dir, compresslevel=None, ref=None):
     t1 = time.time()
     # FIXME, better write to disk sequentially and not keep tgz contents in memory
     tgz_path = os.path.join(dest_dir, name)
-    if ref:
-        ConanOutput(scope=str(ref) if ref else None).info(f"Compressing {name}")
+    ConanOutput(scope=str(ref)).info(f"Compressing {name}")
     with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle:
         tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel)
         for filename, abs_path in sorted(files.items()):
-            # recursive is False by default in case it is a symlink to a folder
-            tgz.add(abs_path, filename, recursive=recursive)
+            # recursive is False in case it is a symlink to a folder
+            tgz.add(abs_path, filename, recursive=False)
         tgz.close()
 
     duration = time.time() - t1
diff --git a/conan/internal/model/workspace.py b/conan/internal/model/workspace.py
index ec3b096bc..f41ca8145 100644
--- a/conan/internal/model/workspace.py
+++ b/conan/internal/model/workspace.py
@@ -7,6 +7,10 @@ from conan.api.output import ConanOutput
 from conan.errors import ConanException
 from conan.internal.util.files import load, save
 
+# Related files
+WORKSPACE_YML = "conanws.yml"
+WORKSPACE_PY = "conanws.py"
+
 
 class Workspace:
     """
@@ -23,13 +27,13 @@ class Workspace:
         return self.conan_data.get("name") or os.path.basename(self.folder)
 
     def _conan_load_data(self):
-        data_path = os.path.join(self.folder, "conanws.yml")
+        data_path = os.path.join(self.folder, WORKSPACE_YML)
         if not os.path.exists(data_path):
             return {}
         try:
             data = yaml.safe_load(load(data_path))
         except Exception as e:
-            raise ConanException("Invalid yml format at {}: {}".format("conanws.yml", e))
+            raise ConanException("Invalid yml format at {}: {}".format(WORKSPACE_YML, e))
         return data or {}
 
     def home_folder(self):
@@ -44,7 +48,7 @@ class Workspace:
         self.conan_data.setdefault("editables", {})[str(ref)] = editable
         if product:
             self.conan_data.setdefault("products", []).append(path)
-        save(os.path.join(self.folder, "conanws.yml"), yaml.dump(self.conan_data))
+        save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data))
 
     def remove(self, path):
         found_ref = None
@@ -58,7 +62,7 @@ class Workspace:
         self.conan_data["editables"].pop(found_ref)
         if path in self.conan_data.get("products", []):
             self.conan_data["products"].remove(path)
-        save(os.path.join(self.folder, "conanws.yml"), yaml.dump(self.conan_data))
+        save(os.path.join(self.folder, WORKSPACE_YML), yaml.dump(self.conan_data))
         return found_ref
 
     def clean(self):
@@ -80,9 +84,6 @@ class Workspace:
         if not os.path.isabs(path):
             raise ConanException(f"Editable path must be absolute: {path}")
         path = os.path.relpath(path, self.folder)
-        if path.startswith(".."):
-            raise ConanException(f"Editable path must be inside the workspace folder: "
-                                 f"{self.folder}")
         return path.replace("\\", "/")  # Normalize to unix path
 
     def editables(self):
diff --git a/conan/tools/build/__init__.py b/conan/tools/build/__init__.py
index f709bb2c8..0ae8bc5f3 100644
--- a/conan/tools/build/__init__.py
+++ b/conan/tools/build/__init__.py
@@ -7,7 +7,7 @@ from conan.tools.build.flags import cppstd_flag
 from conan.tools.build.cppstd import check_max_cppstd, check_min_cppstd, \
     valid_max_cppstd, valid_min_cppstd, default_cppstd, supported_cppstd
 from conan.tools.build.cstd import check_max_cstd, check_min_cstd, \
-    valid_max_cstd, valid_min_cstd, default_cstd, supported_cstd
+    valid_max_cstd, valid_min_cstd, supported_cstd
 from conan.tools.build.cpu import build_jobs
 from conan.tools.build.cross_building import cross_building, can_run
 from conan.tools.build.stdcpp_library import stdcpp_library
diff --git a/conan/tools/build/cstd.py b/conan/tools/build/cstd.py
index 2a762d775..abc594ee1 100644
--- a/conan/tools/build/cstd.py
+++ b/conan/tools/build/cstd.py
@@ -1,7 +1,6 @@
 import operator
 
 from conan.errors import ConanInvalidConfiguration, ConanException
-from conan.internal.api.detect.detect_api import default_cstd as default_cstd_
 from conan.internal.model.version import Version
 
 
@@ -74,21 +73,6 @@ def valid_max_cstd(conanfile, cstd, gnu_extensions=False):
         return False
     return True
 
-def default_cstd(conanfile, compiler=None, compiler_version=None):
-    """
-    Get the default ``compiler.cstd`` for the "conanfile.settings.compiler" and "conanfile
-    settings.compiler_version" or for the parameters "compiler" and "compiler_version" if specified.
-
-    :param conanfile: The current recipe object. Always use ``self``.
-    :param compiler: Name of the compiler e.g. gcc
-    :param compiler_version: Version of the compiler e.g. 12
-    :return: The default ``compiler.cstd`` for the specified compiler
-    """
-    compiler = compiler or conanfile.settings.get_safe("compiler")
-    compiler_version = compiler_version or conanfile.settings.get_safe("compiler.version")
-    if not compiler or not compiler_version:
-        raise ConanException("Called default_cppstd with no compiler or no compiler.version")
-    return default_cstd_(compiler, Version(compiler_version))
 
 def supported_cstd(conanfile, compiler=None, compiler_version=None):
     """
diff --git a/conan/tools/scm/git.py b/conan/tools/scm/git.py
index 48ad9fe5d..cd7e44c98 100644
--- a/conan/tools/scm/git.py
+++ b/conan/tools/scm/git.py
@@ -24,7 +24,7 @@ class Git:
         self._conanfile = conanfile
         self.folder = folder
         self._excluded = excluded
-        global_conf = conanfile._conan_helpers.global_conf  # noqa _conan_helpers
+        global_conf = conanfile._conan_helpers.global_conf
         conf_excluded = global_conf.get("core.scm:excluded", check_type=list)
         if conf_excluded:
             if excluded:
@@ -114,9 +114,9 @@ class Git:
 
         try:
             # This will raise if commit not present.
-            self.run(f"fetch {remote} --refetch --dry-run {commit}")
+            self.run("fetch {} --dry-run --depth=1 {}".format(remote, commit))
             return True
-        except (Exception,):
+        except Exception:
             # Don't raise an error because the fetch could fail for many more reasons than the branch.
             return False
 


Test patch:
diff --git a/test/functional/tools/scm/test_git.py b/test/functional/tools/scm/test_git.py
index 7e0a46d09..c5582d366 100644
--- a/test/functional/tools/scm/test_git.py
+++ b/test/functional/tools/scm/test_git.py
@@ -753,38 +753,6 @@ class TestGitBasicSCMFlow:
         assert "pkg/0.1: MYCMAKE: mycmake" in c2.out
         assert "pkg/0.1: MYFILE: myheader!" in c2.out
 
-    def test_grafted_commit(self):
-        # https://github.com/conan-io/conan/issues/18295
-        folder = os.path.join(temp_folder(), "myrepo")
-        conanfile = textwrap.dedent("""\
-           from conan import ConanFile
-           from conan.tools.scm import Git
-
-           class ConanRecipe(ConanFile):
-               name = "conan-grafted"
-               version = "1.2.3"
-
-               def export(self):
-                   git = Git(self, self.recipe_folder)
-                   git.coordinates_to_conandata(repository=True)
-           """)
-        url, commit = create_local_git_repo(files={"conanfile.py": conanfile}, folder=folder)
-
-        c = TestClient()
-        c.save({"README.txt": "my readme!"}, path=folder)
-        c.run_command("git checkout -b mybranch", cwd=folder)
-        orphan_commit = git_add_changes_commit(folder)
-        c.run_command(f"git checkout {orphan_commit}", cwd=folder)
-        c.run_command("git branch -D mybranch", cwd=folder)
-
-        # Now the client does the clone and export
-        c.run_command(f'git clone "file://{url}" . ')
-        c.run_command("git --no-pager log --decorate")
-        assert "grafted" not in c.out
-        c.run("export .")
-        c.run_command("git --no-pager log --decorate")
-        assert "grafted" not in c.out
-
 
 @pytest.mark.tool("git")
 class TestGitBasicSCMFlowSubfolder:
diff --git a/test/functional/workspace/test_workspace.py b/test/functional/workspace/test_workspace.py
index 78978ebaf..4fc8eb580 100644
--- a/test/functional/workspace/test_workspace.py
+++ b/test/functional/workspace/test_workspace.py
@@ -1,12 +1,13 @@
 import os
 import platform
+import textwrap
 
 import pytest
 
 from conan.api.subapi.workspace import WorkspaceAPI
+from conan.internal.util.files import chdir
 from conan.test.utils.tools import TestClient
 
-
 WorkspaceAPI.TEST_ENABLED = "will_break_next"
 
 
@@ -47,3 +48,49 @@ def test_metabuild():
     assert "Adding project app1" in c.out
     c.run_command("cmake --build --preset conan-release")
     # it doesn't fail
+
+
+@pytest.mark.tool("cmake")
+def test_relative_paths():
+    # This is using the meta-project
+    c = TestClient()
+    c.run("new cmake_lib -d name=liba --output=liba")
+    c.run("new cmake_exe -d name=app1 -d requires=liba/0.1 --output=app1")
+    c.run("new cmake_lib -d name=libb --output=other/libb")
+    c.run("new cmake_exe -d name=app2 -d requires=libb/0.1 --output=other/app2")
+    c.run("workspace init mywks")
+    c.run("workspace init otherwks")
+    # cd mywks
+    with c.chdir("mywks"):
+        c.run("workspace add ../liba")
+        c.run("workspace add ../app1 --product")
+        c.run("workspace build")
+        conanws_yml = textwrap.dedent("""\
+        editables:
+          app1/0.1:
+            path: ../app1
+          liba/0.1:
+            path: ../liba
+        products:
+        - ../app1
+        """)
+        assert conanws_yml == c.load("conanws.yml")
+    # cd otherwks
+    with c.chdir("otherwks"):
+        c.run("workspace add ../other/libb")
+        c.run("workspace add ../other/app2 --product")
+        c.run("workspace build")
+        conanws_yml = textwrap.dedent("""\
+        editables:
+          app2/0.1:
+            path: ../other/app2
+          libb/0.1:
+            path: ../other/libb
+        products:
+        - ../other/app2
+        """)
+        assert conanws_yml == c.load("conanws.yml")
+
+    ext = ".exe" if platform.system() == "Windows" else ""
+    assert os.path.exists(os.path.join(c.current_folder, "app1", "build", "Release", f"app1{ext}"))
+    assert os.path.exists(os.path.join(c.current_folder, "other", "app2", "build", "Release", f"app2{ext}"))
diff --git a/test/integration/conanfile/test_finalize_method.py b/test/integration/conanfile/test_finalize_method.py
index fe3f0363f..6a1679558 100644
--- a/test/integration/conanfile/test_finalize_method.py
+++ b/test/integration/conanfile/test_finalize_method.py
@@ -1,6 +1,5 @@
 import json
 import os
-from pathlib import Path
 import textwrap
 
 import pytest
@@ -128,12 +127,8 @@ class TestBasicLocalFlows:
     def test_save_restore_cache(self, client):
         # Not created in the cache, just exported, nothing breaks because there is not even a package there
         client.run("cache save *:*")
-        # Check pkglist.json has not been created inside conan cache folder
-        assert not any(Path(client.cache_folder).rglob("pgklist.json"))
         client.run("remove * -c")
         client.run("cache restore conan_cache_save.tgz")
-        # Check the extracted pkglist does not persist in the cache after restore
-        assert not any(Path(client.cache_folder).rglob("pgklist.json"))
 
         # Now create the package and then save/restore
         client.run("create dep")
diff --git a/test/integration/workspace/test_workspace.py b/test/integration/workspace/test_workspace.py
index 19095daa7..d364fc5ef 100644
--- a/test/integration/workspace/test_workspace.py
+++ b/test/integration/workspace/test_workspace.py
@@ -484,7 +484,7 @@ class TestMeta:
         c.run("workspace add liba")
         c.run("workspace add libb")
         c.run("workspace install -g CMakeDeps -g CMakeToolchain -of=build --envs-generation=false")
-        assert "Workspace conanfilews.py not found in the workspace folder, using default" in c.out
+        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
         assert "dep1-config.cmake" in files