From 62d09442d89a390e66e1295a498c140109462ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Frauenschl=C3=A4ger?= Date: Thu, 22 Oct 2026 17:31:15 +0200 Subject: [PATCH] fix(cmake): call the SBOM helper from any directory, date tracked subdirs sbom.cmake kept its own directory and the python3 it found in ordinary variables, which are scoped to the directory that include()d it. A project that includes the fragment in one subdirectory and calls wolfssl_compliance_tooling_add_sbom() from another failed at configure time with "python3 was not found", and the driver and script paths there were empty as well. The fragment now also records both in global properties, and the helpers fall back to them only when the calling directory has no value of its own. A call from a directory that included a copy, or from one of its children, uses that copy, so a subproject vendoring a different pin no longer redirects the top project's SBOM to its driver. A call from an unrelated sibling uses the copy included last, as the function definition itself does. sbom-with-sde.cmake took the commit time only when SBOM_ROOT was the git toplevel, so a ROOT in a subdirectory of its own repository got gen-sbom's default timestamp instead. It now uses the commit time when SBOM_ROOT holds tracked files (git ls-files --error-unmatch -- .). A release tarball unpacked into an untracked directory of another checkout holds none, so it still does not take the outer commit time. A ROOT holding any file tracked by another repository, such as a committed copy or a wrapper CMakeLists.txt next to an unpacked tarball, takes that repository's commit time. sbom.am used any enclosing git repository; it now applies the same check to srcdir. test_sbom_cmake_install.py covers a call from a sibling directory (configure, sbom and install-sbom), a top project next to a subproject with its own copy, and a tracked subdirectory ROOT. --- share/build/sbom-with-sde.cmake | 36 ++++++------- share/build/sbom.cmake | 28 ++++++++-- share/sbom.am | 2 +- tests/test_sbom_cmake_install.py | 93 ++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 26 deletions(-) diff --git a/share/build/sbom-with-sde.cmake b/share/build/sbom-with-sde.cmake index cdd12cf..830670d 100644 --- a/share/build/sbom-with-sde.cmake +++ b/share/build/sbom-with-sde.cmake @@ -2,9 +2,9 @@ # build time. # # Keep a value that is already in the environment. When it is empty, use -# the last git commit time of SBOM_ROOT when that directory is the git -# toplevel. A tarball unpacked inside another checkout is not that -# toplevel, so the outer commit time is not used. A tree with no git +# the last commit time of the enclosing checkout when SBOM_ROOT holds +# files tracked there. A tarball unpacked into an untracked directory of +# another checkout does not take that checkout's time. A tree with no git # leaves the variable empty, and gen-sbom uses its own default. # # Arguments after "--" are the driver command. @@ -20,26 +20,20 @@ if(_sde STREQUAL "" AND NOT SBOM_ROOT STREQUAL "") set(_sde "") execute_process( COMMAND "${_wolfssl_compliance_tooling_git}" -C "${SBOM_ROOT}" - rev-parse --show-toplevel - OUTPUT_VARIABLE _git_top - OUTPUT_STRIP_TRAILING_WHITESPACE + ls-files --error-unmatch -- . + OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE _git_rc) - if(_git_rc EQUAL 0 AND NOT _git_top STREQUAL "") - # REALPATH is available on CMake 3.16. file(REAL_PATH) needs 3.19. - get_filename_component(_root_real "${SBOM_ROOT}" REALPATH) - get_filename_component(_top_real "${_git_top}" REALPATH) - if(_root_real STREQUAL _top_real) - execute_process( - COMMAND "${_wolfssl_compliance_tooling_git}" -C "${SBOM_ROOT}" - log -1 --format=%ct - OUTPUT_VARIABLE _sde - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET - RESULT_VARIABLE _git_rc) - if(NOT _git_rc EQUAL 0) - set(_sde "") - endif() + if(_git_rc EQUAL 0) + execute_process( + COMMAND "${_wolfssl_compliance_tooling_git}" -C "${SBOM_ROOT}" + log -1 --format=%ct + OUTPUT_VARIABLE _sde + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _git_rc) + if(NOT _git_rc EQUAL 0) + set(_sde "") endif() endif() if(NOT _sde STREQUAL "") diff --git a/share/build/sbom.cmake b/share/build/sbom.cmake index ae9b309..b0d208d 100644 --- a/share/build/sbom.cmake +++ b/share/build/sbom.cmake @@ -101,6 +101,12 @@ get_filename_component(_WOLFSSL_COMPLIANCE_TOOLING_DRIVER "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}/../sbom-driver.py" ABSOLUTE) find_package(Python3 COMPONENTS Interpreter) +# Fallbacks for a call from a directory that did not include() this file. +set_property(GLOBAL PROPERTY _WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR + "${CMAKE_CURRENT_LIST_DIR}") +set_property(GLOBAL PROPERTY _WOLFSSL_COMPLIANCE_TOOLING_PYTHON3 + "${Python3_EXECUTABLE}") + function(wolfssl_compliance_tooling_add_sbom) set(_opts NO_ARTIFACT_HASH SOURCE_ONLY NO_INSTALL) set(_one NAME TARGET_NAME VERSION VERSION_FILE VERSION_MACRO LICENSE @@ -166,14 +172,24 @@ function(wolfssl_compliance_tooling_add_sbom) endforeach() list(REMOVE_DUPLICATES _srcs) + if(NOT Python3_EXECUTABLE) + get_property(Python3_EXECUTABLE GLOBAL PROPERTY + _WOLFSSL_COMPLIANCE_TOOLING_PYTHON3) + endif() if(NOT Python3_EXECUTABLE) message(FATAL_ERROR "wolfssl_compliance_tooling_add_sbom: python3 was not found") endif() + set(_sbom_dir "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}") + if(NOT _sbom_dir) + get_property(_sbom_dir GLOBAL PROPERTY _WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR) + endif() + get_filename_component(_driver "${_sbom_dir}/../sbom-driver.py" ABSOLUTE) + set(_cmd ${CMAKE_COMMAND} -E env HOSTCC=${SB_HOSTCC} ${Python3_EXECUTABLE} - ${_WOLFSSL_COMPLIANCE_TOOLING_DRIVER} + ${_driver} --name ${SB_NAME} --root ${SB_ROOT} --license-file ${SB_LICENSE} @@ -305,7 +321,7 @@ function(wolfssl_compliance_tooling_add_sbom) add_custom_target(${SB_TARGET_NAME} COMMAND ${CMAKE_COMMAND} -DSBOM_ROOT=${SB_ROOT} - -P "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}/sbom-with-sde.cmake" + -P "${_sbom_dir}/sbom-with-sde.cmake" -- ${_cmd} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} @@ -371,6 +387,10 @@ function(wolfssl_compliance_tooling_add_sbom_install) if(NOT IS_ABSOLUTE "${_docdir}") set(_docdir "${CMAKE_INSTALL_PREFIX}/${_docdir}") endif() + set(_sbom_dir "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}") + if(NOT _sbom_dir) + get_property(_sbom_dir GLOBAL PROPERTY _WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR) + endif() add_custom_target(${SI_INSTALL_TARGET} COMMAND ${CMAKE_COMMAND} @@ -383,7 +403,7 @@ function(wolfssl_compliance_tooling_add_sbom_install) -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_VERSION_FILE=${SI_VERSION_FILE} -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_VERSION_MACRO=${SI_VERSION_MACRO} -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_BINDIR=${SI_BINDIR} - -P "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}/install-sbom.cmake" + -P "${_sbom_dir}/install-sbom.cmake" VERBATIM COMMENT "Installing ${SI_NAME} SBOM to ${_docdir}") add_dependencies(${SI_INSTALL_TARGET} ${SI_DEPENDS}) @@ -399,7 +419,7 @@ function(wolfssl_compliance_tooling_add_sbom_install) -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_VERSION_FILE=${SI_VERSION_FILE} -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_VERSION_MACRO=${SI_VERSION_MACRO} -DWOLFSSL_COMPLIANCE_TOOLING_SBOM_BINDIR=${SI_BINDIR} - -P "${_WOLFSSL_COMPLIANCE_TOOLING_SBOM_DIR}/uninstall-sbom.cmake" + -P "${_sbom_dir}/uninstall-sbom.cmake" VERBATIM COMMENT "Uninstalling ${SI_NAME} SBOM from ${_docdir}") endfunction() diff --git a/share/sbom.am b/share/sbom.am index c54906d..23fc9a0 100644 --- a/share/sbom.am +++ b/share/sbom.am @@ -213,7 +213,7 @@ sbom: -x c /dev/null > "$$_defines"; \ fi; \ if test -z "$${SOURCE_DATE_EPOCH:-}" && test -n "$(GIT)" && \ - $(GIT) -C "$(srcdir)" rev-parse --git-dir >/dev/null 2>&1; then \ + $(GIT) -C "$(srcdir)" ls-files --error-unmatch -- . >/dev/null 2>&1; then \ sde=`$(GIT) -C "$(srcdir)" log -1 --format=%ct 2>/dev/null`; \ if test -n "$$sde"; then SOURCE_DATE_EPOCH="$$sde"; export SOURCE_DATE_EPOCH; fi; \ fi; \ diff --git a/tests/test_sbom_cmake_install.py b/tests/test_sbom_cmake_install.py index 240a7dd..ab7d1a1 100644 --- a/tests/test_sbom_cmake_install.py +++ b/tests/test_sbom_cmake_install.py @@ -158,6 +158,25 @@ def test_empty_value_uses_git_commit_time(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), stamp.stdout.strip()) + def test_tracked_subdirectory_uses_the_commit_time(self): + git = shutil.which("git") + if not git: + self.skipTest("git is not installed") + run([git, "init"], cwd=self.dir) + run([git, "config", "user.email", "sbom@example.invalid"], cwd=self.dir) + run([git, "config", "user.name", "sbom"], cwd=self.dir) + sub = self.dir / "lib" + sub.mkdir() + (sub / "x.c").write_text("int x;\n") + run([git, "add", "lib/x.c"], cwd=self.dir) + run([git, "commit", "-m", "init"], cwd=self.dir) + stamp = run([git, "log", "-1", "--format=%ct"], cwd=self.dir) + env = os.environ.copy() + env.pop("SOURCE_DATE_EPOCH", None) + result = self._print_sde(sub, env) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), stamp.stdout.strip()) + def test_nested_directory_does_not_use_the_outer_commit(self): # A release tarball unpacked inside another checkout must not # take that checkout's commit time. @@ -303,6 +322,80 @@ def test_source_only_sbom_target_builds(self): self.assertTrue((build / "t-1.cdx.json").is_file()) self.assertTrue((build / "t-1.spdx.json").is_file()) + def test_call_from_a_sibling_directory_builds(self): + # sbom.cmake is included in one subdirectory and the helper is + # called from another, where its directory variables are unset. + src = self.dir / "src" + (src / "inc").mkdir(parents=True) + (src / "app").mkdir() + (src / "app" / "x.c").write_text("int x(void) { return 1; }\n") + (src / "LICENSE").write_text("demo\n") + (src / "CMakeLists.txt").write_text(""" +cmake_minimum_required(VERSION 3.16) +project(sbomtest C) +add_subdirectory(inc) +add_subdirectory(app) +""") + (src / "inc" / "CMakeLists.txt").write_text( + f'include("{BUILD / "sbom.cmake"}")\n') + (src / "app" / "CMakeLists.txt").write_text(""" +add_library(x STATIC x.c) +wolfssl_compliance_tooling_add_sbom( + NAME t + VERSION 1 + TARGETS x + SOURCE_ONLY + ROOT ${CMAKE_SOURCE_DIR} + LICENSE ${CMAKE_SOURCE_DIR}/LICENSE + INSTALL_DIR ${CMAKE_BINARY_DIR}/doc) +""") + build = self.dir / "build" + configured = run([ + CMAKE, "-S", str(src), "-B", str(build), "-G", "Unix Makefiles", + ]) + self.assertEqual(configured.returncode, 0, + configured.stderr + configured.stdout) + for target in ("sbom", "install-sbom"): + built = run([CMAKE, "--build", str(build), "--target", target]) + self.assertEqual(built.returncode, 0, built.stderr + built.stdout) + self.assertTrue((build / "app" / "t-1.cdx.json").is_file()) + self.assertTrue((build / "doc" / "t-1.cdx.json").is_file()) + + def test_call_uses_its_own_directory_copy(self): + # A subproject that vendors its own copy must not redirect the + # top project's SBOM to that copy's driver. + src = self.dir / "src" + (src / "dep").mkdir(parents=True) + shutil.copytree(BUILD.parent, src / "dep" / "sbom") + (src / "x.c").write_text("int x(void) { return 1; }\n") + (src / "LICENSE").write_text("demo\n") + (src / "dep" / "CMakeLists.txt").write_text( + 'include("${CMAKE_CURRENT_SOURCE_DIR}/sbom/build/sbom.cmake")\n') + (src / "CMakeLists.txt").write_text(f""" +cmake_minimum_required(VERSION 3.16) +project(sbomtest C) +include("{BUILD / "sbom.cmake"}") +add_subdirectory(dep) +add_library(x STATIC x.c) +wolfssl_compliance_tooling_add_sbom( + NAME t + VERSION 1 + TARGETS x + SOURCE_ONLY + ROOT ${{CMAKE_CURRENT_SOURCE_DIR}} + LICENSE ${{CMAKE_CURRENT_SOURCE_DIR}}/LICENSE) +""") + build = self.dir / "build" + configured = run([ + CMAKE, "-S", str(src), "-B", str(build), "-G", "Unix Makefiles", + ]) + self.assertEqual(configured.returncode, 0, + configured.stderr + configured.stdout) + text = (build / "CMakeFiles" / "sbom.dir" / "build.make").read_text() + self.assertIn(str(BUILD / "sbom-with-sde.cmake"), text) + self.assertIn(str(BUILD.parent / "sbom-driver.py"), text) + self.assertNotIn(str(src / "dep"), text) + def test_second_call_keeps_its_install_target_name(self): build = self._configure(f""" cmake_minimum_required(VERSION 3.16)