From cf602860d62c243decd888ff6b7b57a4b3cb0361 Mon Sep 17 00:00:00 2001 From: memsharded Date: Wed, 11 Oct 2023 01:09:45 +0200 Subject: [PATCH 1/5] cache get-put --- conan/api/subapi/cache.py | 72 ++++++++++++++++++- conan/cli/commands/cache.py | 23 ++++++ .../command_v2/test_cache_get_put.py | 35 +++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 conans/test/integration/command_v2/test_cache_get_put.py diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 3afb0010616..2042287fec3 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -1,11 +1,17 @@ +import json import os +import tarfile +from conan.api.model import MultiPackagesList +from conan.api.output import ConanOutput from conan.internal.conan_app import ConanApp from conan.internal.integrity_check import IntegrityChecker +from conans.client.cache.cache import ClientCache from conans.errors import ConanException from conans.model.package_ref import PkgReference from conans.model.recipe_ref import RecipeReference -from conans.util.files import rmdir +from conans.util.dates import revision_timestamp_now +from conans.util.files import rmdir, gzopen_without_timestamps, save, load class CacheAPI: @@ -101,6 +107,70 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True) if download: rmdir(pref_layout.download_package()) + def get(self, package_list): + multi_package_list = MultiPackagesList.load(package_list) + package_list = multi_package_list["Local Cache"] + app = ConanApp(self.conan_api.cache_folder) + ref_folders = {} + for ref, ref_bundle in package_list.refs().items(): + ref_layout = app.cache.recipe_layout(ref) + base_folder = ref_layout.base_folder + ref_folders[ref] = base_folder + for pref, _ in package_list.prefs(ref, ref_bundle).items(): + pref_layout = app.cache.pkg_layout(pref) + base_folder = pref_layout.package() + # FIXME: Missing metadata + ref_folders[pref] = base_folder + + tgz_path = os.path.abspath("cache.tgz") + _compress_cache(tgz_path, ref_folders, self.conan_api.cache_folder) + + def put(self, path): + with open(path, mode='rb') as file_handler: + the_tar = tarfile.open(fileobj=file_handler) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.list(verbose=True) + the_tar.extractall(path=self.conan_api.cache_folder) + the_tar.close() + + manifest_path = os.path.join(self.conan_api.cache_folder, "cache_manifest.json") + manifest = load(manifest_path) + os.remove(manifest_path) # remove file from the cache + manifest = json.loads(manifest) + print(manifest) + cache = ClientCache(self.conan_api.cache_folder) + for ref, folder in manifest.items(): + print(repr(ref), folder) + if ":" in ref: # Package + pref = PkgReference.loads(ref) + else: + ref = RecipeReference.loads(ref) + ref.timestamp = revision_timestamp_now() + recipe_layout = cache.get_or_create_ref_layout(ref) + rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder) + assert rel_path == folder, f"{rel_path}!={folder}" + + + +def _compress_cache(tgz_path, ref_folders, cache_folder): + name = os.path.basename(tgz_path) + out = ConanOutput() + serialized = {ref.repr_notime(): os.path.relpath(f, cache_folder) + for ref, f in ref_folders.items()} + serialized = json.dumps(serialized, indent=2) + manifest_path = os.path.join(cache_folder, "cache_manifest.json") + save(manifest_path, serialized) + with open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle) + tgz.add(manifest_path, "cache_manifest.json") + for ref, folder in ref_folders.items(): + rel_path = os.path.relpath(folder, cache_folder) + out.info(f"Compressing {ref}: {rel_path}") + tgz.add(folder, rel_path, recursive=True) + tgz.close() + def _resolve_latest_ref(app, ref): if ref.revision is None or ref.revision == "latest": diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index 4b3c868563d..11d8859939a 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -1,6 +1,7 @@ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern from conan.api.output import cli_out_write +from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.errors import ConanException from conans.model.package_ref import PkgReference @@ -99,3 +100,25 @@ def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) conan_api.cache.check_integrity(package_list) + + +@conan_subcommand() +def cache_get(conan_api: ConanAPI, parser, subparser, *args): + """ + Get the artifacts from a package list and archive them + """ + subparser.add_argument("package_list", help="Package list to be zipped") + args = parser.parse_args(*args) + package_list = make_abs_path(args.package_list) + conan_api.cache.get(package_list) + + +@conan_subcommand() +def cache_put(conan_api: ConanAPI, parser, subparser, *args): + """ + Put the artifacts from a an archive into the cache + """ + subparser.add_argument("path", help="Path to archive to put in the cache") + args = parser.parse_args(*args) + path = make_abs_path(args.path) + conan_api.cache.put(path) diff --git a/conans/test/integration/command_v2/test_cache_get_put.py b/conans/test/integration/command_v2/test_cache_get_put.py new file mode 100644 index 00000000000..e39c2c5d8ce --- /dev/null +++ b/conans/test/integration/command_v2/test_cache_get_put.py @@ -0,0 +1,35 @@ +import os +import shutil + +from conans.test.assets.genconanfile import GenConanfile +from conans.test.utils.tools import TestClient + + +def test_cache_path(): + c = TestClient() + c.save({"conanfile.py": GenConanfile()}) + c.run("create . --name=pkg --version=1.0") + c.run("create . --name=pkg --version=1.1") + c.run("create . --name=other --version=2.0") + # TODO: It needs the #latest to get a valid package-revision + c.run("list pkg/*:*#latest --format=json", redirect_stdout="pkglist.json") + c.run("cache get pkglist.json") + print(c.out) + print(c.current_folder) + cache_path = os.path.join(c.current_folder, "cache.tgz") + assert os.path.exists(cache_path) + + c2 = TestClient() + # Create a package in the cache to check put doesn't interact badly + c2.save({"conanfile.py": GenConanfile()}) + c2.run("create . --name=pkg2 --version=3.0") + shutil.copy2(cache_path, c2.current_folder) + c2.run("cache put cache.tgz") + print(c2.out) + print(c2.cache_folder) + c2.run("list *:*") + print(c2.out) + assert "pkg2/3.0" in c2.out + assert "pkg/1.0" in c2.out + assert "pkg/1.1" in c2.out + assert "other/2.0" not in c2.out From e587cb832c82ae22168fe07511b951751a474e67 Mon Sep 17 00:00:00 2001 From: memsharded Date: Fri, 13 Oct 2023 00:57:05 +0200 Subject: [PATCH 2/5] wip --- conan/api/subapi/cache.py | 99 +++++++++---------- conan/cli/commands/cache.py | 8 +- .../command_v2/test_cache_get_put.py | 18 ++-- 3 files changed, 60 insertions(+), 65 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 2042287fec3..dd3b696b028 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -1,8 +1,9 @@ import json import os +import shutil import tarfile -from conan.api.model import MultiPackagesList +from conan.api.model import PackagesList from conan.api.output import ConanOutput from conan.internal.conan_app import ConanApp from conan.internal.integrity_check import IntegrityChecker @@ -108,68 +109,60 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True) rmdir(pref_layout.download_package()) def get(self, package_list): - multi_package_list = MultiPackagesList.load(package_list) - package_list = multi_package_list["Local Cache"] - app = ConanApp(self.conan_api.cache_folder) - ref_folders = {} - for ref, ref_bundle in package_list.refs().items(): - ref_layout = app.cache.recipe_layout(ref) - base_folder = ref_layout.base_folder - ref_folders[ref] = base_folder - for pref, _ in package_list.prefs(ref, ref_bundle).items(): - pref_layout = app.cache.pkg_layout(pref) - base_folder = pref_layout.package() - # FIXME: Missing metadata - ref_folders[pref] = base_folder - + cache_folder = self.conan_api.cache_folder + app = ConanApp(cache_folder) + out = ConanOutput() tgz_path = os.path.abspath("cache.tgz") - _compress_cache(tgz_path, ref_folders, self.conan_api.cache_folder) + name = os.path.basename(tgz_path) + with open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle) + for ref, ref_bundle in package_list.refs().items(): + ref_layout = app.cache.recipe_layout(ref) + base_folder = ref_layout.base_folder + folder = os.path.relpath(base_folder, cache_folder) + ref_bundle["folder"] = folder + out.info(f"Compressing {ref}: {folder}") + tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) + for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): + pref_layout = app.cache.pkg_layout(pref) + pkg_folder = pref_layout.package() + # FIXME: Missing metadata + folder = os.path.relpath(pkg_folder, cache_folder) + pref_bundle["package_folder"] = folder + out.info(f"Compressing {pref}: {folder}") + tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) + serialized = json.dumps(package_list.serialize(), indent=2) + manifest_path = os.path.join(cache_folder, "pkglist.json") + save(manifest_path, serialized) + tgz.add(manifest_path, "pkglist.json") + os.remove(manifest_path) # remove file from the cache + tgz.close() def put(self, path): with open(path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.list(verbose=True) the_tar.extractall(path=self.conan_api.cache_folder) the_tar.close() - manifest_path = os.path.join(self.conan_api.cache_folder, "cache_manifest.json") - manifest = load(manifest_path) + out = ConanOutput() + manifest_path = os.path.join(self.conan_api.cache_folder, "pkglist.json") + package_list = PackagesList.deserialize(json.loads(load(manifest_path))) os.remove(manifest_path) # remove file from the cache - manifest = json.loads(manifest) - print(manifest) cache = ClientCache(self.conan_api.cache_folder) - for ref, folder in manifest.items(): - print(repr(ref), folder) - if ":" in ref: # Package - pref = PkgReference.loads(ref) - else: - ref = RecipeReference.loads(ref) - ref.timestamp = revision_timestamp_now() - recipe_layout = cache.get_or_create_ref_layout(ref) - rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder) - assert rel_path == folder, f"{rel_path}!={folder}" - - - -def _compress_cache(tgz_path, ref_folders, cache_folder): - name = os.path.basename(tgz_path) - out = ConanOutput() - serialized = {ref.repr_notime(): os.path.relpath(f, cache_folder) - for ref, f in ref_folders.items()} - serialized = json.dumps(serialized, indent=2) - manifest_path = os.path.join(cache_folder, "cache_manifest.json") - save(manifest_path, serialized) - with open(tgz_path, "wb") as tgz_handle: - tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle) - tgz.add(manifest_path, "cache_manifest.json") - for ref, folder in ref_folders.items(): - rel_path = os.path.relpath(folder, cache_folder) - out.info(f"Compressing {ref}: {rel_path}") - tgz.add(folder, rel_path, recursive=True) - tgz.close() + for ref, ref_bundle in package_list.refs().items(): + ref.timestamp = revision_timestamp_now() + recipe_layout = cache.get_or_create_ref_layout(ref) + folder = ref_bundle["folder"] + rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder) + assert rel_path == folder, f"{rel_path}!={folder}" + out.info(f"Put: {ref} in {folder}") + for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): + pref.timestamp = revision_timestamp_now() + pkg_layout = cache.get_or_create_pkg_layout(pref) + folder = pref_bundle["package_folder"] + out.info(f"Put: {pref} in {folder}") + shutil.move(os.path.join(cache.cache_folder, folder), + os.path.join(cache.cache_folder, pkg_layout.package())) def _resolve_latest_ref(app, ref): diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index 11d8859939a..d3641ee80be 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -107,9 +107,13 @@ def cache_get(conan_api: ConanAPI, parser, subparser, *args): """ Get the artifacts from a package list and archive them """ - subparser.add_argument("package_list", help="Package list to be zipped") + parser.add_argument('pattern', + help="A pattern in the form 'pkg/version#revision:package_id#revision', " + "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. " + "If revision is not specified, it is assumed latest one.") args = parser.parse_args(*args) - package_list = make_abs_path(args.package_list) + ref_pattern = ListPattern(args.pattern) + package_list = conan_api.list.select(ref_pattern) conan_api.cache.get(package_list) diff --git a/conans/test/integration/command_v2/test_cache_get_put.py b/conans/test/integration/command_v2/test_cache_get_put.py index e39c2c5d8ce..7fda7185ccd 100644 --- a/conans/test/integration/command_v2/test_cache_get_put.py +++ b/conans/test/integration/command_v2/test_cache_get_put.py @@ -7,13 +7,11 @@ def test_cache_path(): c = TestClient() - c.save({"conanfile.py": GenConanfile()}) - c.run("create . --name=pkg --version=1.0") - c.run("create . --name=pkg --version=1.1") - c.run("create . --name=other --version=2.0") - # TODO: It needs the #latest to get a valid package-revision - c.run("list pkg/*:*#latest --format=json", redirect_stdout="pkglist.json") - c.run("cache get pkglist.json") + c.save({"conanfile.py": GenConanfile().with_settings("os")}) + c.run("create . --name=pkg --version=1.0 -s os=Linux") + c.run("create . --name=pkg --version=1.1 -s os=Linux") + c.run("create . --name=other --version=2.0 -s os=Linux") + c.run("cache get pkg/*:*") print(c.out) print(c.current_folder) cache_path = os.path.join(c.current_folder, "cache.tgz") @@ -21,13 +19,13 @@ def test_cache_path(): c2 = TestClient() # Create a package in the cache to check put doesn't interact badly - c2.save({"conanfile.py": GenConanfile()}) - c2.run("create . --name=pkg2 --version=3.0") + c2.save({"conanfile.py": GenConanfile().with_settings("os")}) + c2.run("create . --name=pkg2 --version=3.0 -s os=Windows") shutil.copy2(cache_path, c2.current_folder) c2.run("cache put cache.tgz") print(c2.out) print(c2.cache_folder) - c2.run("list *:*") + c2.run("list *:*#*") print(c2.out) assert "pkg2/3.0" in c2.out assert "pkg/1.0" in c2.out From 902f189082b978fbdda117684e98540e558abc97 Mon Sep 17 00:00:00 2001 From: memsharded Date: Sat, 14 Oct 2023 00:59:57 +0200 Subject: [PATCH 3/5] wip --- conan/api/subapi/cache.py | 12 +++---- conan/cli/commands/cache.py | 31 ++++++++++++------- ..._get_put.py => test_cache_save_restore.py} | 11 ++----- 3 files changed, 28 insertions(+), 26 deletions(-) rename conans/test/integration/command_v2/{test_cache_get_put.py => test_cache_save_restore.py} (82%) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index dd3b696b028..7c0ff74ae83 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -108,11 +108,10 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True) if download: rmdir(pref_layout.download_package()) - def get(self, package_list): + def save(self, package_list, tgz_path): cache_folder = self.conan_api.cache_folder app = ConanApp(cache_folder) out = ConanOutput() - tgz_path = os.path.abspath("cache.tgz") name = os.path.basename(tgz_path) with open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle) @@ -121,7 +120,7 @@ def get(self, package_list): base_folder = ref_layout.base_folder folder = os.path.relpath(base_folder, cache_folder) ref_bundle["folder"] = folder - out.info(f"Compressing {ref}: {folder}") + out.info(f"Saving {ref}: {folder}") tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): pref_layout = app.cache.pkg_layout(pref) @@ -129,7 +128,7 @@ def get(self, package_list): # FIXME: Missing metadata folder = os.path.relpath(pkg_folder, cache_folder) pref_bundle["package_folder"] = folder - out.info(f"Compressing {pref}: {folder}") + out.info(f"Saving {pref}: {folder}") tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) serialized = json.dumps(package_list.serialize(), indent=2) manifest_path = os.path.join(cache_folder, "pkglist.json") @@ -138,7 +137,7 @@ def get(self, package_list): os.remove(manifest_path) # remove file from the cache tgz.close() - def put(self, path): + def restore(self, path): with open(path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) the_tar.extractall(path=self.conan_api.cache_folder) @@ -160,9 +159,10 @@ def put(self, path): pref.timestamp = revision_timestamp_now() pkg_layout = cache.get_or_create_pkg_layout(pref) folder = pref_bundle["package_folder"] - out.info(f"Put: {pref} in {folder}") + out.info(f"Restore: {pref} in {folder}") shutil.move(os.path.join(cache.cache_folder, folder), os.path.join(cache.cache_folder, pkg_layout.package())) + return package_list def _resolve_latest_ref(app, ref): diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index d3641ee80be..88c9897233c 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -3,6 +3,7 @@ from conan.api.output import cli_out_write from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument +from conan.cli.commands.list import print_list_text, print_list_json from conan.errors import ConanException from conans.model.package_ref import PkgReference from conans.model.recipe_ref import RecipeReference @@ -102,27 +103,33 @@ def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): conan_api.cache.check_integrity(package_list) -@conan_subcommand() -def cache_get(conan_api: ConanAPI, parser, subparser, *args): +@conan_subcommand(formatters={"text": print_list_text, + "json": print_list_json}) +def cache_save(conan_api: ConanAPI, parser, subparser, *args): """ Get the artifacts from a package list and archive them """ - parser.add_argument('pattern', - help="A pattern in the form 'pkg/version#revision:package_id#revision', " - "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. " - "If revision is not specified, it is assumed latest one.") + subparser.add_argument('file', help="Save to this file") + subparser.add_argument('pattern', + help="A pattern in the form 'pkg/version#revision:package_id#revision', " + "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. " + "If revision is not specified, it is assumed latest one.") args = parser.parse_args(*args) ref_pattern = ListPattern(args.pattern) package_list = conan_api.list.select(ref_pattern) - conan_api.cache.get(package_list) + tgz_path = make_abs_path(args.file) + conan_api.cache.save(package_list, tgz_path) + return {"results": {"Local Cache": package_list.serialize()}} -@conan_subcommand() -def cache_put(conan_api: ConanAPI, parser, subparser, *args): +@conan_subcommand(formatters={"text": print_list_text, + "json": print_list_json}) +def cache_restore(conan_api: ConanAPI, parser, subparser, *args): """ Put the artifacts from a an archive into the cache """ - subparser.add_argument("path", help="Path to archive to put in the cache") + subparser.add_argument("file", help="Path to archive to restore") args = parser.parse_args(*args) - path = make_abs_path(args.path) - conan_api.cache.put(path) + path = make_abs_path(args.file) + package_list = conan_api.cache.restore(path) + return {"results": {"Local Cache": package_list.serialize()}} diff --git a/conans/test/integration/command_v2/test_cache_get_put.py b/conans/test/integration/command_v2/test_cache_save_restore.py similarity index 82% rename from conans/test/integration/command_v2/test_cache_get_put.py rename to conans/test/integration/command_v2/test_cache_save_restore.py index 7fda7185ccd..d459cb1b47f 100644 --- a/conans/test/integration/command_v2/test_cache_get_put.py +++ b/conans/test/integration/command_v2/test_cache_save_restore.py @@ -5,15 +5,13 @@ from conans.test.utils.tools import TestClient -def test_cache_path(): +def test_cache_save_restore(): c = TestClient() c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=1.0 -s os=Linux") c.run("create . --name=pkg --version=1.1 -s os=Linux") c.run("create . --name=other --version=2.0 -s os=Linux") - c.run("cache get pkg/*:*") - print(c.out) - print(c.current_folder) + c.run("cache save cache.tgz pkg/*:* ") cache_path = os.path.join(c.current_folder, "cache.tgz") assert os.path.exists(cache_path) @@ -22,11 +20,8 @@ def test_cache_path(): c2.save({"conanfile.py": GenConanfile().with_settings("os")}) c2.run("create . --name=pkg2 --version=3.0 -s os=Windows") shutil.copy2(cache_path, c2.current_folder) - c2.run("cache put cache.tgz") - print(c2.out) - print(c2.cache_folder) + c2.run("cache restore cache.tgz") c2.run("list *:*#*") - print(c2.out) assert "pkg2/3.0" in c2.out assert "pkg/1.0" in c2.out assert "pkg/1.1" in c2.out From ff664b388526570a2c98e53e429ca16e31a40f1b Mon Sep 17 00:00:00 2001 From: memsharded Date: Sun, 15 Oct 2023 00:49:21 +0200 Subject: [PATCH 4/5] wip --- conan/api/subapi/cache.py | 53 +++++++++------ .../command_v2/test_cache_save_restore.py | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+), 21 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 7c0ff74ae83..14d4e9f27aa 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -2,6 +2,7 @@ import os import shutil import tarfile +from io import BytesIO from conan.api.model import PackagesList from conan.api.output import ConanOutput @@ -12,7 +13,7 @@ from conans.model.package_ref import PkgReference from conans.model.recipe_ref import RecipeReference from conans.util.dates import revision_timestamp_now -from conans.util.files import rmdir, gzopen_without_timestamps, save, load +from conans.util.files import rmdir, gzopen_without_timestamps class CacheAPI: @@ -117,51 +118,61 @@ def save(self, package_list, tgz_path): tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle) for ref, ref_bundle in package_list.refs().items(): ref_layout = app.cache.recipe_layout(ref) - base_folder = ref_layout.base_folder - folder = os.path.relpath(base_folder, cache_folder) - ref_bundle["folder"] = folder - out.info(f"Saving {ref}: {folder}") - tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) + recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) + ref_bundle["recipe_folder"] = recipe_folder + out.info(f"Saving {ref}: {recipe_folder}") + tgz.add(os.path.join(cache_folder, recipe_folder), recipe_folder, recursive=True) for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): pref_layout = app.cache.pkg_layout(pref) pkg_folder = pref_layout.package() - # FIXME: Missing metadata folder = os.path.relpath(pkg_folder, cache_folder) 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) + pref_bundle["metadata_folder"] = metadata_folder + out.info(f"Saving {pref} metadata: {folder}") + tgz.add(os.path.join(cache_folder, metadata_folder), metadata_folder, + recursive=True) serialized = json.dumps(package_list.serialize(), indent=2) - manifest_path = os.path.join(cache_folder, "pkglist.json") - save(manifest_path, serialized) - tgz.add(manifest_path, "pkglist.json") - os.remove(manifest_path) # remove file from the cache + 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): with open(path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) + fileobj = the_tar.extractfile("pkglist.json") + pkglist = fileobj.read() the_tar.extractall(path=self.conan_api.cache_folder) the_tar.close() out = ConanOutput() - manifest_path = os.path.join(self.conan_api.cache_folder, "pkglist.json") - package_list = PackagesList.deserialize(json.loads(load(manifest_path))) - os.remove(manifest_path) # remove file from the cache + package_list = PackagesList.deserialize(json.loads(pkglist)) cache = ClientCache(self.conan_api.cache_folder) for ref, ref_bundle in package_list.refs().items(): ref.timestamp = revision_timestamp_now() recipe_layout = cache.get_or_create_ref_layout(ref) - folder = ref_bundle["folder"] + recipe_folder = ref_bundle["recipe_folder"] rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder) - assert rel_path == folder, f"{rel_path}!={folder}" - out.info(f"Put: {ref} in {folder}") + assert rel_path == recipe_folder, f"{rel_path}!={recipe_folder}" + out.info(f"Put: {ref} in {recipe_folder}") for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): pref.timestamp = revision_timestamp_now() pkg_layout = cache.get_or_create_pkg_layout(pref) - folder = pref_bundle["package_folder"] - out.info(f"Restore: {pref} in {folder}") - shutil.move(os.path.join(cache.cache_folder, folder), - os.path.join(cache.cache_folder, pkg_layout.package())) + pkg_folder = pref_bundle["package_folder"] + out.info(f"Restore: {pref} in {pkg_folder}") + # We need to put the package in the final location in the cache + shutil.move(os.path.join(cache.cache_folder, pkg_folder), pkg_layout.package()) + metadata_folder = pref_bundle.get("metadata_folder") + if metadata_folder: + out.info(f"Restore: {pref} metadata in {metadata_folder}") + # We need to put the package in the final location in the cache + shutil.move(os.path.join(cache.cache_folder, metadata_folder), + pkg_layout.metadata()) return package_list diff --git a/conans/test/integration/command_v2/test_cache_save_restore.py b/conans/test/integration/command_v2/test_cache_save_restore.py index d459cb1b47f..c52daa485f7 100644 --- a/conans/test/integration/command_v2/test_cache_save_restore.py +++ b/conans/test/integration/command_v2/test_cache_save_restore.py @@ -3,6 +3,7 @@ from conans.test.assets.genconanfile import GenConanfile from conans.test.utils.tools import TestClient +from conans.util.files import save, load def test_cache_save_restore(): @@ -14,7 +15,29 @@ def test_cache_save_restore(): c.run("cache save cache.tgz pkg/*:* ") cache_path = os.path.join(c.current_folder, "cache.tgz") assert os.path.exists(cache_path) + _validate_restore(cache_path) + +def test_cache_save_downloaded_restore(): + """ what happens if we save packages downloaded from server, not + created + """ + c = TestClient(default_server_user=True) + c.save({"conanfile.py": GenConanfile().with_settings("os")}) + c.run("create . --name=pkg --version=1.0 -s os=Linux") + c.run("create . --name=pkg --version=1.1 -s os=Linux") + c.run("create . --name=other --version=2.0 -s os=Linux") + c.run("upload * -r=default -c") + c.run("remove * -c") + c.run("download *:* -r=default --metadata=*") + c.run("cache save cache.tgz pkg/*:* ") + cache_path = os.path.join(c.current_folder, "cache.tgz") + assert os.path.exists(cache_path) + + _validate_restore(cache_path) + + +def _validate_restore(cache_path): c2 = TestClient() # Create a package in the cache to check put doesn't interact badly c2.save({"conanfile.py": GenConanfile().with_settings("os")}) @@ -26,3 +49,44 @@ def test_cache_save_restore(): assert "pkg/1.0" in c2.out assert "pkg/1.1" in c2.out assert "other/2.0" not in c2.out + + # Restore again, just in case + c2.run("cache restore cache.tgz") + c2.run("list *:*#*") + assert "pkg2/3.0" in c2.out + assert "pkg/1.0" in c2.out + assert "pkg/1.1" in c2.out + assert "other/2.0" not in c2.out + + +def test_cache_save_restore_metadata(): + c = TestClient() + c.save({"conanfile.py": GenConanfile().with_settings("os")}) + c.run("create . --name=pkg --version=1.0 -s os=Linux") + pid = c.created_package_id("pkg/1.0") + # Add some metadata + c.run("cache path pkg/1.0 --folder=metadata") + metadata_path = str(c.stdout).strip() + myfile = os.path.join(metadata_path, "logs", "mylogs.txt") + save(myfile, "mylogs!!!!") + c.run(f"cache path pkg/1.0:{pid} --folder=metadata") + pkg_metadata_path = str(c.stdout).strip() + myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt") + save(myfile, "mybuildlogs!!!!") + + c.run("cache save cache.tgz pkg/*:* ") + cache_path = os.path.join(c.current_folder, "cache.tgz") + assert os.path.exists(cache_path) + + # restore and check + c2 = TestClient() + shutil.copy2(cache_path, c2.current_folder) + c2.run("cache restore cache.tgz") + c2.run("cache path pkg/1.0 --folder=metadata") + metadata_path = str(c2.stdout).strip() + myfile = os.path.join(metadata_path, "logs", "mylogs.txt") + assert load(myfile) == "mylogs!!!!" + c2.run(f"cache path pkg/1.0:{pid} --folder=metadata") + pkg_metadata_path = str(c2.stdout).strip() + myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt") + assert load(myfile) == "mybuildlogs!!!!" From 1aec968a735d15953d545f5a933f1e1cd1b5e061 Mon Sep 17 00:00:00 2001 From: memsharded Date: Mon, 16 Oct 2023 01:32:08 +0200 Subject: [PATCH 5/5] new test order revisions --- conan/api/subapi/cache.py | 3 ++ .../command_v2/test_cache_save_restore.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 14d4e9f27aa..5dc71bd694f 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -155,6 +155,7 @@ def restore(self, path): cache = ClientCache(self.conan_api.cache_folder) for ref, ref_bundle in package_list.refs().items(): ref.timestamp = revision_timestamp_now() + ref_bundle["timestamp"] = ref.timestamp recipe_layout = cache.get_or_create_ref_layout(ref) recipe_folder = ref_bundle["recipe_folder"] rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder) @@ -162,6 +163,7 @@ def restore(self, path): out.info(f"Put: {ref} in {recipe_folder}") for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): pref.timestamp = revision_timestamp_now() + pref_bundle["timestamp"] = pref.timestamp pkg_layout = cache.get_or_create_pkg_layout(pref) pkg_folder = pref_bundle["package_folder"] out.info(f"Restore: {pref} in {pkg_folder}") @@ -173,6 +175,7 @@ def restore(self, path): # We need to put the package in the final location in the cache shutil.move(os.path.join(cache.cache_folder, metadata_folder), pkg_layout.metadata()) + return package_list diff --git a/conans/test/integration/command_v2/test_cache_save_restore.py b/conans/test/integration/command_v2/test_cache_save_restore.py index c52daa485f7..107929c8d39 100644 --- a/conans/test/integration/command_v2/test_cache_save_restore.py +++ b/conans/test/integration/command_v2/test_cache_save_restore.py @@ -1,3 +1,4 @@ +import json import os import shutil @@ -90,3 +91,33 @@ def test_cache_save_restore_metadata(): pkg_metadata_path = str(c2.stdout).strip() myfile = os.path.join(pkg_metadata_path, "logs", "mybuildlogs.txt") assert load(myfile) == "mybuildlogs!!!!" + + +def test_cache_save_restore_multiple_revisions(): + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) + c.run("create .") + rrev1 = c.exported_recipe_revision() + c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("var=42")}) + c.run("create .") + rrev2 = c.exported_recipe_revision() + c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("var=123")}) + c.run("create .") + rrev3 = c.exported_recipe_revision() + + def check_ordered_revisions(client): + client.run("list *#* --format=json") + revisions = json.loads(client.stdout)["Local Cache"]["pkg/0.1"]["revisions"] + assert revisions[rrev1]["timestamp"] < revisions[rrev2]["timestamp"] + assert revisions[rrev2]["timestamp"] < revisions[rrev3]["timestamp"] + + check_ordered_revisions(c) + + c.run("cache save cache.tgz pkg/*#*:* ") + cache_path = os.path.join(c.current_folder, "cache.tgz") + + # restore and check + c2 = TestClient() + shutil.copy2(cache_path, c2.current_folder) + c2.run("cache restore cache.tgz") + check_ordered_revisions(c2)