diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py index 61c0d7b85c1..2ac8efb5d9e 100644 --- a/conan/internal/api/install/generators.py +++ b/conan/internal/api/install/generators.py @@ -3,6 +3,7 @@ import traceback import importlib +from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conans.client.subsystems import deduce_subsystem, subsystem_path from conan.internal.errors import conanfile_exception_formatter @@ -141,6 +142,8 @@ def write_generators(conanfile, hook_manager, home_folder, envs_generation=None) _generate_aggregated_env(conanfile) + generate_graph_manifests(conanfile, home_folder) + hook_manager.execute("post_generate", conanfile=conanfile) @@ -157,6 +160,25 @@ def _receive_conf(conanfile): conanfile.conf.compose_conf(build_require.conf_info) +def generate_graph_manifests(conanfile, home_folder): + from conans.client.loader import load_python_file + mkdir(conanfile.package_metadata_folder) + sub_graph = conanfile.subgraph + sbom_plugin_path = HomePaths(home_folder).sbom_manifest_plugin_path + if os.path.exists(sbom_plugin_path): + mod, _ = load_python_file(sbom_plugin_path) + + if not hasattr(mod, "generate_sbom"): + raise ConanException( + f"SBOM manifest plugin does not have a 'generate_sbom' method") + if not callable(mod.generate_sbom): + raise ConanException( + f"SBOM manifest plugin 'generate_sbom' is not a function") + + conanfile.output.info(f"generating sbom") + # TODO think if this is conanfile or conanfile._conan_node + return mod.generate_sbom(sub_graph) + def _receive_generators(conanfile): """ Collect generators_info from the immediate build_requires""" for build_req in conanfile.dependencies.direct_build.values(): diff --git a/conan/internal/cache/home_paths.py b/conan/internal/cache/home_paths.py index 30e79002b34..bef60942465 100644 --- a/conan/internal/cache/home_paths.py +++ b/conan/internal/cache/home_paths.py @@ -67,6 +67,10 @@ def auth_source_plugin_path(self): def sign_plugin_path(self): return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "sign", "sign.py") + @property + def sbom_manifest_plugin_path(self): + return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "sbom.py") + @property def remotes_path(self): return os.path.join(self._home, "remotes.json") diff --git a/conan/internal/methods.py b/conan/internal/methods.py index 17050dbef04..4a6749db4b9 100644 --- a/conan/internal/methods.py +++ b/conan/internal/methods.py @@ -9,6 +9,7 @@ from conans.model.pkg_type import PackageType from conans.model.requires import BuildRequirements, TestRequirements, ToolRequirements from conans.util.files import mkdir, chdir, save +from conan.internal.api.install.generators import generate_graph_manifests def run_source_method(conanfile, hook_manager): @@ -63,6 +64,7 @@ def run_package_method(conanfile, package_id, hook_manager, ref): with conanfile_remove_attr(conanfile, ['info'], "package"): conanfile.package() hook_manager.execute("post_package", conanfile=conanfile) + generate_graph_manifests(conanfile, conanfile._conan_helpers.home_folder) save(os.path.join(conanfile.package_folder, CONANINFO), conanfile.info.dumps()) manifest = FileTreeManifest.create(conanfile.package_folder) diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py index 0444d80ce0f..eada5dfcdf2 100644 --- a/conans/client/graph/graph.py +++ b/conans/client/graph/graph.py @@ -70,6 +70,23 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None, test=False): self.replaced_requires = {} # To track the replaced requires for self.dependencies[old-ref] self.skipped_build_requires = False + def subgraph(self): + nodes = [self] + opened = [self] + while opened: + new_opened = [] + for o in opened: + for n in o.neighbors(): + if n not in nodes: + nodes.append(n) + if n not in opened: + new_opened.append(n) + opened = new_opened + + graph = DepsGraph() + graph.nodes = nodes + return graph + def __lt__(self, other): """ @type other: Node diff --git a/conans/client/graph/sbom.py b/conans/client/graph/sbom.py new file mode 100644 index 00000000000..afacdac92f1 --- /dev/null +++ b/conans/client/graph/sbom.py @@ -0,0 +1,7 @@ +from conans.client.graph.spdx import spdx_json_generator +from conan.internal.cache.home_paths import HomePaths + +def migrate_sbom_file(cache_folder): + from conans.client.migrations import update_file + sbom_path = HomePaths(cache_folder).sbom_manifest_plugin_path + update_file(sbom_path, spdx_json_generator) diff --git a/conans/client/graph/spdx.py b/conans/client/graph/spdx.py new file mode 100644 index 00000000000..5ca3e3b51de --- /dev/null +++ b/conans/client/graph/spdx.py @@ -0,0 +1,239 @@ +# TODO RENAME THIS FILE + +spdx_json_generator = """ + +def generate_sbom(graph, **kwargs): + cyclonedx_1_4(graph, **kwargs) + #spdx_sbom(graph, **kwargs) + +def cyclonedx_1_4(graph, **kwargs): + import json + import os + import uuid + import time + from datetime import datetime, timezone + from conan import conan_version + from conan.errors import ConanException + from conan.api.subapi.graph import CONTEXT_BUILD + from conan.api.output import ConanOutput + + has_special_root_node = not (getattr(graph.root.ref, "name", False) and getattr(graph.root.ref, "version", False) and getattr(graph.root.ref, "revision", False)) + special_id = str(uuid.uuid4()) + special_name = graph.root.conanfile.display_name.replace(".", "-").replace(" ", "_").replace("/", "-").upper() + + components = [node for node in graph.nodes] + if has_special_root_node: + components = components[1:] + + dependencies = [] + if has_special_root_node: + deps = {"ref": special_id} + deps["dependsOn"] = [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}" for d in graph.root.dependencies] + dependencies.append(deps) + for c in components: + deps = {"ref": f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}"} + dependsOn = [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}" for d in c.dependencies] + if dependsOn: + deps["dependsOn"] = dependsOn + dependencies.append(deps) + + def _calculate_licenses(component): + if isinstance(component.conanfile.license, str): # Just one license + return [{"license": { + "id": component.conanfile.license + }}] + return [{"license": { + "id": license + }} for license in c.conanfile.license] + + sbom_cyclonedx_1_4 = { + **({"components": [{ + "author": "Conan", + "bom-ref": special_id if has_special_root_node else f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}", + "description": c.conanfile.description, + **({"externalReferences": [{ + "type": "website", + "url": c.conanfile.homepage + }]} if c.conanfile.homepage else {}), + **({"licenses": _calculate_licenses(c)} if c.conanfile.license else {}), + "name": c.name, + "fpurl": f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}", + "type": "library", + "version": str(c.ref.version), + } for c in components]} if components else {}), + **({"dependencies": dependencies} if dependencies else {}), + "metadata": { + "component": { + "author": "Conan", + "bom-ref": special_id if has_special_root_node else f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}", + "name": graph.root.conanfile.display_name, + "type": "library" + }, + "timestamp": f"{datetime.fromtimestamp(time.time(), tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}", + "tools": [{ + "externalReferences": [{ + "type": "website", + "url": "https://github.com/conan-io/conan" + }], + "name": "Conan-io" + }], + }, + "serialNumber": f"urn:uuid:{uuid.uuid4()}", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + } + try: + metadata_folder = graph.root.conanfile.package_metadata_folder + file_name = f"{special_name}-cyclonedx.json" if has_special_root_node else f"{graph.root.name}-{graph.root.ref.version}-cyclonedx.json" + with open(os.path.join(metadata_folder, file_name), 'w') as f: + json.dump(sbom_cyclonedx_1_4, f, indent=4) + ConanOutput().success(f"CYCLONEDX CREATED - {graph.root.conanfile.package_metadata_folder}") + except Exception as e: + ConanException("error generating CYCLONEDX file") + +def spdx_sbom(graph, **kwargs): + import os + import time + import json + import pathlib + from glob import glob + from datetime import datetime, timezone + from conan import conan_version + from conan.errors import ConanException + from conan.api.output import ConanOutput + + name = graph.root.name if graph.root.name else "CLI" + version = "SPDX-2.2" + date = datetime.fromtimestamp(time.time(), tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + packages = [] + files = [] + relationships = [] + + # --- Root node --- + if graph.root.recipe != "Cli": + conan_data = graph.root.conanfile.conan_data + url_location = conan_data.get("sources", {}).get(graph.root.conanfile.version, {}).get("url", {}) if conan_data else None + checksum = conan_data.get("sources", {}).get(graph.root.conanfile.version, {}).get("sha256", {}) if conan_data else None + packages.extend([ + { + "name": graph.root.ref.name, + "SPDXID": f"SPDXRef-{graph.root.ref}", + "version": str(graph.root.ref.version), + "downloadLocation": graph.root.conanfile.url or "NOASSERTION", + "homePage": graph.root.conanfile.homepage or "NOASSERTION", + "licenseConcluded": graph.root.conanfile.license or "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "description": graph.root.conanfile.description or "NOASSERTION", + "comment": f"This is the {graph.root.ref.name} package in the remote" # TODO It could be a local package + + }, + { + "name": f"{graph.root.pref} binary", + "SPDXID": f"SPDXRef-binary-{graph.root.ref}", + "downloadLocation": graph.root.remote.url if graph.root.remote else "NONE", + "licenseConcluded": graph.root.conanfile.license or "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "comment": f"This is the {graph.root.ref} binary generated by conan" + }, + { + "name": f"{graph.root.ref.name} upstream", + "SPDXID": f"SPDXRef-resource-{graph.root.ref}", + "downloadLocation": url_location or "NONE", + **({"checksum": { + "algorithm": "SHA256", + "checksumValue": checksum + }} if checksum else {}), + "licenseConcluded": graph.root.conanfile.license or "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "comment": f"This is the {graph.root.ref.name} release file" + }]) + + relationships.extend([{ + "spdxElementId": f"SPDXRef-binary-{graph.root.ref}", + "relationshipType": "DEPENDS_ON", + "relatedSpdxElement": f"SPDXRef-binary-{d.dst.ref}", + }for d in graph.root.dependencies]) + + relationships.append({ + "spdxElementId": f"SPDXRef-{graph.root.ref}", + "relationshipType": "GENERATES", + "relatedSpdxElement": f"SPDXRef-binary-{graph.root.ref}", + }) + + exported_path = graph.root.conanfile.recipe_folder # /e folder + external_files = [f for f in glob(os.path.join(exported_path, "**", "*"), recursive=True) if not f.endswith('/')] if exported_path else [] + + try: + with open(os.path.join(graph.root.conanfile.recipe_folder, "conanmanifest.txt")) as conanmanifest: + external_files.extend([os.path.join(exported_path[:-1], *line.split(" ")[0][:-1].split("/")) for line in conanmanifest.readlines()[2:]]) + except Exception: + pass + + for i, file_name in enumerate(external_files): + checksum = None + files.append( + { + "fileName": file_name, + "SPDXID": f"SPDXRef-file-{graph.root.ref}-{i}", + **({"checksums":{ + "algorithm": "SHA256", + "checksumValues": checksum, + }} if checksum else {}), + "licenseConcluded": "NOASSERTION", + "copyrightText": "NOASSERTION" + } + ) + relationships.append({ + "spdxElementId": f"SPDXRef-{graph.root.ref}", + "relationshipType": "CONTAINS", + "relatedSpdxElement": f"SPDXRef-file-{graph.root.ref}-{i}", + }) + + # --- Just the binaries for dependencies --- + for node in graph.nodes[1:]: + conan_data = node.conanfile.conan_data + url_location = conan_data.get("sources", {}).get(node.conanfile.version, {}).get("url", {}) if conan_data else None + checksum = conan_data.get("sources", {}).get(node.conanfile.version, {}).get("sha256", {}) if conan_data else None + packages.extend([ + { + "name": f"{node.pref} binary", + "SPDXID": f"SPDXRef-binary-{node.ref}", + "downloadLocation": node.remote.url if node.remote else "NONE", + "licenseConcluded": node.conanfile.license or "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "comment": f"This is the {node.ref} binary generated by conan" + }]) + + relationships.extend([{ + "spdxElementId": f"SPDXRef-binary-{node.ref}", + "relationshipType": "DEPENDS_ON", + "relatedSpdxElement": f"SPDXRef-binary-{d.dst.ref}", + }for d in node.dependencies]) + + + # https://spdx.github.io/spdx-spec/v2.2.2/package-information/ + data = { + "SPDXVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "documentName": f"{name}-{version}", + "documentNamespace": f"http://spdx.org/spdxdocs/{name}-{version}-{date}", # the date or hash to make it unique + "creator": f"Tool: Conan-{conan_version}", + "created": date, #YYYY-MM-DDThh:mm:ssZ + "packages": packages, + **({"files": files} if files else {}), + **({"relationships": relationships} if relationships else {}), + } + try: + metadata_folder = graph.root.conanfile.package_metadata_folder + with open(os.path.join(metadata_folder, f"{name}-{graph.root.ref.version if graph.root.ref else 'local'}.spdx.json"), 'w') as f: + json.dump(data, f, indent=4) + ConanOutput().success(f"SPDX CREATED - {graph.root.conanfile.package_metadata_folder}") + except Exception as e: + ConanException("error generating spdx file") +""" diff --git a/conans/client/migrations.py b/conans/client/migrations.py index 82997ba4e9e..03487d423ef 100644 --- a/conans/client/migrations.py +++ b/conans/client/migrations.py @@ -52,6 +52,9 @@ def _apply_migrations(self, old_version): # Update profile plugin from conan.internal.api.profile.profile_loader import migrate_profile_plugin migrate_profile_plugin(self.cache_folder) + # Update sbom manifest plugins + from conans.client.graph.sbom import migrate_sbom_file + migrate_sbom_file(self.cache_folder) if old_version and old_version < "2.0.14-": _migrate_pkg_db_lru(self.cache_folder, old_version) diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py index 27001f20fd1..73d4890ba30 100644 --- a/conans/model/conan_file.py +++ b/conans/model/conan_file.py @@ -187,6 +187,10 @@ def output(self): def context(self): return self._conan_node.context + @property + def subgraph(self): + return self._conan_node.subgraph() + @property def dependencies(self): # Caching it, this object is requested many times diff --git a/test/functional/sbom/__init__.py b/test/functional/sbom/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/functional/sbom/test_sbom.py b/test/functional/sbom/test_sbom.py new file mode 100644 index 00000000000..b888f5a819c --- /dev/null +++ b/test/functional/sbom/test_sbom.py @@ -0,0 +1,83 @@ +import textwrap + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient +import os + + +def test_sbom_generation_create(): + tc = TestClient() + tc.run("new cmake_lib -d name=dep -d version=1.0") + tc.run("export .") + tc.run("new cmake_lib -d name=foo -d version=1.0 -d requires=dep/1.0 -f") + tc.run("export .") + tc.run("new cmake_lib -d name=bar -d version=1.0 -d requires=foo/1.0 -f") + # bar -> foo -> dep + tc.run("create . --build=missing") + bar_layout = tc.created_layout() + assert os.path.exists(os.path.join(bar_layout.build(),"..", "d", "metadata", "bar-1.0-cyclonedx.json")) + +def test_sbom_generation_install_requires(): + tc = TestClient() + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "conanfile.py": GenConanfile("foo", "1.0").with_requires("dep/1.0")}) + tc.run("export dep") + tc.run("create . --build=missing") + + #cli -> foo -> dep + tc.run("install --requires=foo/1.0") + assert os.path.exists(os.path.join(tc.current_folder, "CLI-cyclonedx.json")) + +def test_sbom_generation_install_path(): + tc = TestClient() + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "conanfile.py": GenConanfile("foo", "1.0").with_requires("dep/1.0")}) + tc.run("create dep") + + #foo -> dep + tc.run("install .") + assert os.path.exists(os.path.join(tc.current_folder, "CONANFILE-PY_(FOO-1-0)-cyclonedx.json")) + +def test_sbom_generation_install_path_consumer(): + # There is not .../d/metadata/... + tc = TestClient() + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "conanfile.py": GenConanfile().with_requires("dep/1.0")}) + tc.run("create dep") + + #conanfile.py -> dep + tc.run("install .") + assert os.path.exists(os.path.join(tc.current_folder, "CONANFILE-PY-cyclonedx.json")) + +def test_sbom_generation_install_path_txt(): + # There is not .../d/metadata/... + tc = TestClient() + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "conanfile.txt": textwrap.dedent( + """ + [requires] + dep/1.0 + """ + )}) + tc.run("create dep") + + #foo -> dep + tc.run("install .") + assert os.path.exists(os.path.join(tc.current_folder, "CONANFILE-TXT-cyclonedx.json")) + +def test_sbom_generation_skipped_dependencies(): + tc = TestClient() + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "app/conanfile.py": GenConanfile("app", "1.0") + .with_package_type("application") + .with_requires("dep/1.0"), + "conanfile.py": GenConanfile("foo", "1.0").with_tool_requires("app/1.0")}) + tc.run("create dep") + tc.run("create app") + tc.run("create .") + create_layout = tc.created_layout() + + cyclone_path = os.path.join(create_layout.build(), "..", "d", "metadata", "foo-1.0-cyclonedx.json") + content = tc.load(cyclone_path) + # A skipped dependency also shows up in the sbom + assert "pkg:conan/dep@1.0?rref=6a99f55e933fb6feeb96df134c33af44" in content diff --git a/test/integration/command/upload/upload_complete_test.py b/test/integration/command/upload/upload_complete_test.py index 24d6c837fb3..e3c384179b4 100644 --- a/test/integration/command/upload/upload_complete_test.py +++ b/test/integration/command/upload/upload_complete_test.py @@ -165,7 +165,7 @@ def test_upload_error(self): client.run("install --requires=hello0/1.2.1@frodo/stable --build='*' -r default") self._set_global_conf(client, retry=3, retry_wait=0) client.run("upload hello* --confirm -r default") - self.assertEqual(str(client.out).count("WARN: network: Pair file, error!"), 5) + self.assertEqual(str(client.out).count("WARN: network: Pair file, error!"), 6) def _set_global_conf(self, client, retry=None, retry_wait=None): lines = [] @@ -222,7 +222,7 @@ def test_upload_error_with_config(self): client.run("install --requires=hello0/1.2.1@frodo/stable --build='*'") self._set_global_conf(client, retry=3, retry_wait=0) client.run("upload hello* --confirm -r default") - self.assertEqual(str(client.out).count("WARN: network: Pair file, error!"), 5) + self.assertEqual(str(client.out).count("WARN: network: Pair file, error!"), 6) def test_upload_same_package_dont_compress(self): client = self._get_client() diff --git a/test/integration/graph/test_subgraph_reports.py b/test/integration/graph/test_subgraph_reports.py new file mode 100644 index 00000000000..95b425b3594 --- /dev/null +++ b/test/integration/graph/test_subgraph_reports.py @@ -0,0 +1,44 @@ +import json +import os +import textwrap + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient +from conans.util.files import load + + +def test_subgraph_reports(): + c = TestClient() + subgraph_hook = textwrap.dedent("""\ + import os, json + from conan.tools.files import save + from conans.model.graph_lock import Lockfile + def post_package(conanfile): + subgraph = conanfile.subgraph + save(conanfile, os.path.join(conanfile.package_folder, "..", "..", f"{conanfile.name}-conangraph.json"), + json.dumps(subgraph.serialize(), indent=2)) + save(conanfile, os.path.join(conanfile.package_folder, "..", "..", f"{conanfile.name}-conan.lock"), + Lockfile(subgraph).dumps()) + """) + + c.save_home({"extensions/hooks/subgraph_hook/hook_subgraph.py": subgraph_hook}) + c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), + "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requirement("dep/0.1"), + "app/conanfile.py": GenConanfile("app", "0.1").with_requirement("pkg/0.1")}) + c.run("export dep") + c.run("export pkg") + # app -> pkg -> dep + c.run("create app --build=missing --format=json") + + app_graph = json.loads(load(os.path.join(c.cache.builds_folder, "app-conangraph.json"))) + pkg_graph = json.loads(load(os.path.join(c.cache.builds_folder, "pkg-conangraph.json"))) + dep_graph = json.loads(load(os.path.join(c.cache.builds_folder, "dep-conangraph.json"))) + + app_lock = json.loads(load(os.path.join(c.cache.builds_folder, "app-conan.lock"))) + pkg_lock = json.loads(load(os.path.join(c.cache.builds_folder, "pkg-conan.lock"))) + dep_lock = json.loads(load(os.path.join(c.cache.builds_folder, "dep-conan.lock"))) + + assert len(app_graph["nodes"]) == len(app_lock["requires"]) + assert len(pkg_graph["nodes"]) == len(pkg_lock["requires"]) + assert len(dep_graph["nodes"]) == len(dep_lock["requires"]) + diff --git a/test/integration/metadata/test_metadata_commands.py b/test/integration/metadata/test_metadata_commands.py index 097ac4c4247..481b03b77f7 100644 --- a/test/integration/metadata/test_metadata_commands.py +++ b/test/integration/metadata/test_metadata_commands.py @@ -33,7 +33,7 @@ def test_upload(self, create_conan_pkg): # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out - assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out + assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out # Add new files to the metadata self.save_metadata_file(c, "pkg/0.1", "mylogs2.txt") @@ -42,7 +42,7 @@ def test_upload(self, create_conan_pkg): # adding the new metadata logs files c.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 2 files" in c.out - assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out + assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 3 files" in c.out c.run("remove * -c") c.run("install --requires=pkg/0.1") # wont install metadata by default @@ -116,7 +116,7 @@ def test_direct_download_redownload(self, create_conan_pkg): # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out - assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out + assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out c.run("remove * -c") @@ -146,7 +146,7 @@ def test_no_download_cached(self, create_conan_pkg): # Now upload everything c.run("upload * -c -r=default") assert "pkg/0.1: Recipe metadata: 1 files" in c.out - assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out + assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out c2 = TestClient(servers=c.servers) tmp_folder = temp_folder() @@ -169,7 +169,7 @@ def test_no_download_cached(self, create_conan_pkg): save(mypkgfile, "mybuildlogs2!!!!") c.run("upload * -c -r=default --metadata=*") assert "pkg/0.1: Recipe metadata: 1 files" in c.out - assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 1 files" in c.out + assert "pkg/0.1:da39a3ee5e6b4b0d3255bfef95601890afd80709: Package metadata: 2 files" in c.out # re-download of metadata in c2 c2.run("remove * -c") # to make sure the download cache works diff --git a/test/integration/metadata/test_metadata_logs.py b/test/integration/metadata/test_metadata_logs.py index 6de26128cb2..4fc7a9b1230 100644 --- a/test/integration/metadata/test_metadata_logs.py +++ b/test/integration/metadata/test_metadata_logs.py @@ -52,7 +52,7 @@ def test_metadata_logs(self): pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) - assert os.listdir(pref_layout.metadata()) == ["logs"] + assert os.listdir(pref_layout.metadata()) == ["logs", "pkg-0.1-cyclonedx.json"] assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" @@ -82,7 +82,7 @@ def test_download_pkg_list_from_graph(self): ref = RecipeReference.loads("pkg/0.1") pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) - assert os.listdir(pref_layout.metadata()) == ["logs"] + assert os.listdir(pref_layout.metadata()) == ["logs", "pkg-0.1-cyclonedx.json"] assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" @@ -180,7 +180,7 @@ def test_metadata_logs_hook(self, _client): pref = c.get_latest_package_reference(ref) pref_layout = c.get_latest_pkg_layout(pref) - assert os.listdir(pref_layout.metadata()) == ["logs"] + assert "logs" in os.listdir(pref_layout.metadata()) assert os.listdir(os.path.join(pref_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pref_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!" @@ -218,6 +218,6 @@ def package(self): c.run("export-pkg .") # Test local cache looks good pkg_layout = c.created_layout() - assert os.listdir(pkg_layout.metadata()) == ["logs"] + assert os.listdir(pkg_layout.metadata()) == ["logs", "pkg-0.1-cyclonedx.json"] assert os.listdir(os.path.join(pkg_layout.metadata(), "logs")) == ["mylogs.txt"] assert load(os.path.join(pkg_layout.metadata(), "logs", "mylogs.txt")) == "some logs!!!"