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)