diff --git a/cachi2/core/package_managers/yarn_classic/main.py b/cachi2/core/package_managers/yarn_classic/main.py index e19b8f826..4b6c160f4 100644 --- a/cachi2/core/package_managers/yarn_classic/main.py +++ b/cachi2/core/package_managers/yarn_classic/main.py @@ -6,6 +6,7 @@ from cachi2.core.errors import PackageManagerError, PackageRejected from cachi2.core.models.input import Request from cachi2.core.models.output import Component, EnvironmentVariable, RequestOutput +from cachi2.core.models.property_semantics import PropertySet from cachi2.core.package_managers.yarn.utils import ( VersionsRange, extract_yarn_version_from_env, @@ -61,14 +62,16 @@ def _ensure_mirror_dir_exists(output_dir: RootedPath) -> None: for package in request.yarn_packages: package_path = request.source_dir.join_within_root(package.path) _ensure_mirror_dir_exists(request.output_dir) - _resolve_yarn_project(Project.from_source_dir(package_path), request.output_dir) + components.extend( + _resolve_yarn_project(Project.from_source_dir(package_path), request.output_dir) + ) return RequestOutput.from_obj_list( components, _generate_build_environment_variables(), project_files=[] ) -def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> None: +def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> list[Component]: """Process a request for a single yarn source directory.""" log.info(f"Fetching the yarn dependencies at the subpath {project.source_dir}") @@ -79,6 +82,25 @@ def _resolve_yarn_project(project: Project, output_dir: RootedPath) -> None: packages = resolve_packages(project, output_dir.join_within_root(MIRROR_DIR)) _verify_no_offline_mirror_collisions(packages) + return _create_sbom_components(packages) + + +def _create_sbom_components(packages: Iterable[YarnClassicPackage]) -> list[Component]: + """Create SBOM components from the given yarn packages.""" + result = [] + for package in packages: + properties = PropertySet(npm_development=package.dev).to_properties() + result.append( + Component( + name=package.name, + purl=package.purl, + version=package.version, + properties=properties, + ) + ) + + return result + def _fetch_dependencies(source_dir: RootedPath, env: dict[str, str]) -> None: """Fetch dependencies using 'yarn install'. diff --git a/tests/unit/package_managers/yarn_classic/test_main.py b/tests/unit/package_managers/yarn_classic/test_main.py index 030b99217..cefdc0799 100644 --- a/tests/unit/package_managers/yarn_classic/test_main.py +++ b/tests/unit/package_managers/yarn_classic/test_main.py @@ -1,3 +1,4 @@ +import itertools import json from pathlib import Path from typing import Any, Iterable @@ -54,19 +55,48 @@ def test_generate_build_environment_variables( @pytest.mark.parametrize( - "input_request, components", - [ + "input_request, package_components", + ( pytest.param( [{"type": "yarn", "path": "."}], - [], + [ + [ + Component( + name="foo", + purl="pkg:npm/foo@1.0.0", + version="1.0.0", + ), + Component(name="bar", purl="pkg:npm/bar@2.0.0", version="2.0.0"), + ], + ], id="single_input_package", ), pytest.param( [{"type": "yarn", "path": "."}, {"type": "yarn", "path": "./path"}], - [], + [ + [ + Component( + name="foo", + purl="pkg:npm/foo@1.0.0", + version="1.0.0", + ), + ], + [ + Component( + name="bar", + purl="pkg:npm/bar@2.0.0", + version="2.0.0", + ), + Component( + name="baz", + purl="pkg:npm/baz@3.0.0", + version="3.0.0", + ), + ], + ], id="multiple_input_packages", ), - ], + ), indirect=["input_request"], ) @mock.patch("cachi2.core.package_managers.yarn_classic.main._resolve_yarn_project") @@ -75,26 +105,28 @@ def test_fetch_yarn_source( mock_create_project: mock.Mock, mock_resolve_yarn: mock.Mock, input_request: Request, + package_components: list[Component], yarn_classic_env_variables: list[EnvironmentVariable], - components: list[Component], ) -> None: - expected_output = RequestOutput( - components=components, - build_config=BuildConfig(environment_variables=yarn_classic_env_variables), - ) package_dirs = [ input_request.source_dir.join_within_root(p.path) for p in input_request.packages ] projects = [_prepare_project(path, {}) for path in package_dirs] + mock_create_project.side_effect = projects + mock_resolve_yarn.side_effect = package_components output = fetch_yarn_source(input_request) mock_create_project.assert_has_calls([mock.call(path) for path in package_dirs]) mock_resolve_yarn.assert_has_calls([mock.call(p, input_request.output_dir) for p in projects]) - assert input_request.output_dir.join_within_root("deps/yarn-classic").path.exists() + expected_output = RequestOutput( + components=list(itertools.chain.from_iterable(package_components)), + build_config=BuildConfig(environment_variables=yarn_classic_env_variables), + ) assert output == expected_output + assert input_request.output_dir.join_within_root(MIRROR_DIR).path.exists() @mock.patch("cachi2.core.package_managers.yarn_classic.main.resolve_packages")