diff --git a/.gitignore b/.gitignore index 634d6fc26c1..b4da296cf20 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ CMakeUserPresets.json # pixi environments .pixi *.egg-info +InsightData-*.tar.gz diff --git a/CMake/ITKModuleMacros.cmake b/CMake/ITKModuleMacros.cmake index 96ef854c4db..a5e8735dc96 100644 --- a/CMake/ITKModuleMacros.cmake +++ b/CMake/ITKModuleMacros.cmake @@ -69,12 +69,18 @@ macro(itk_module _name) set(ITK_MODULE_${itk-module}_DESCRIPTION "description") set(ITK_MODULE_${itk-module}_EXCLUDE_FROM_DEFAULT 0) set(ITK_MODULE_${itk-module}_ENABLE_SHARED 0) + set(ITK_MODULE_${itk-module}_SPDX_LICENSE "") + set(ITK_MODULE_${itk-module}_SPDX_VERSION "") + set(ITK_MODULE_${itk-module}_SPDX_DOWNLOAD_LOCATION "") + set(ITK_MODULE_${itk-module}_SPDX_COPYRIGHT "") + set(ITK_MODULE_${itk-module}_SPDX_CUSTOM_LICENSE_TEXT "") + set(ITK_MODULE_${itk-module}_SPDX_CUSTOM_LICENSE_NAME "") foreach(arg ${ARGN}) ### Parse itk_module named options if( "${arg}" MATCHES - "^((|COMPILE_|PRIVATE_|TEST_|)DEPENDS|DESCRIPTION|DEFAULT|FACTORY_NAMES)$" + "^((|COMPILE_|PRIVATE_|TEST_|)DEPENDS|DESCRIPTION|DEFAULT|FACTORY_NAMES|SPDX_LICENSE|SPDX_VERSION|SPDX_DOWNLOAD_LOCATION|SPDX_COPYRIGHT|SPDX_CUSTOM_LICENSE_TEXT|SPDX_CUSTOM_LICENSE_NAME)$" ) set(_doing "${arg}") elseif("${arg}" MATCHES "^EXCLUDE_FROM_DEFAULT$") @@ -104,6 +110,24 @@ macro(itk_module _name) elseif("${_doing}" MATCHES "^DESCRIPTION$") set(_doing "") set(ITK_MODULE_${itk-module}_DESCRIPTION "${arg}") + elseif("${_doing}" MATCHES "^SPDX_LICENSE$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_LICENSE "${arg}") + elseif("${_doing}" MATCHES "^SPDX_VERSION$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_VERSION "${arg}") + elseif("${_doing}" MATCHES "^SPDX_DOWNLOAD_LOCATION$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_DOWNLOAD_LOCATION "${arg}") + elseif("${_doing}" MATCHES "^SPDX_COPYRIGHT$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_COPYRIGHT "${arg}") + elseif("${_doing}" MATCHES "^SPDX_CUSTOM_LICENSE_TEXT$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_CUSTOM_LICENSE_TEXT "${arg}") + elseif("${_doing}" MATCHES "^SPDX_CUSTOM_LICENSE_NAME$") + set(_doing "") + set(ITK_MODULE_${itk-module}_SPDX_CUSTOM_LICENSE_NAME "${arg}") elseif("${_doing}" MATCHES "^DEFAULT") message(FATAL_ERROR "Invalid argument [DEFAULT]") else() diff --git a/CMake/ITKSBOMGeneration.cmake b/CMake/ITKSBOMGeneration.cmake new file mode 100644 index 00000000000..a26f4edb9ce --- /dev/null +++ b/CMake/ITKSBOMGeneration.cmake @@ -0,0 +1,421 @@ +#[=============================================================================[ + ITKSBOMGeneration.cmake - Generate SPDX Software Bill of Materials (SBOM) + + This module generates an SPDX 2.3 JSON SBOM document describing ITK and + its enabled third-party dependencies at build configuration time. The SBOM + includes component names, versions, licenses, and dependency relationships. + + Per-module SPDX metadata is declared in each module's itk-module.cmake via + the itk_module() macro arguments: + SPDX_LICENSE - SPDX license identifier (e.g. "Apache-2.0") + SPDX_VERSION - Version of the vendored dependency + SPDX_DOWNLOAD_LOCATION - URL for the upstream source + SPDX_COPYRIGHT - Copyright text + SPDX_CUSTOM_LICENSE_TEXT - Extracted text for custom LicenseRef-* IDs + SPDX_CUSTOM_LICENSE_NAME - Human-readable name for custom license refs + + Usage: + option(ITK_GENERATE_SBOM "Generate SPDX SBOM at configure time" ON) + include(ITKSBOMGeneration) + + The generated file is written to: + ${CMAKE_BINARY_DIR}/sbom.spdx.json +#]=============================================================================] + +if(NOT ITK_GENERATE_SBOM) + return() +endif() + +set( + ITK_SBOM_SPDX_LICENSE_LIST_VERSION + "3.25" + CACHE STRING + "SPDX license list version recorded in the generated SBOM" +) + +#----------------------------------------------------------------------------- +# Allow remote modules to register SBOM package metadata. +# +# Usage in a remote module's CMakeLists.txt: +# itk_sbom_register_package( +# NAME "MyRemoteModule" +# VERSION "1.0.0" +# SPDX_LICENSE "Apache-2.0" +# DOWNLOAD_LOCATION "https://github.com/example/MyRemoteModule" +# SUPPLIER "Organization: Example" +# COPYRIGHT "Copyright Example Inc." +# ) +# +define_property( + GLOBAL + PROPERTY ITK_SBOM_EXTRA_PACKAGES + BRIEF_DOCS + "Additional SBOM package entries registered by remote modules." + FULL_DOCS + "A list of JSON-formatted package entries for the SBOM." +) + +function(itk_sbom_register_package) + set(_options "") + set( + _one_value + NAME + VERSION + SPDX_LICENSE + DOWNLOAD_LOCATION + SUPPLIER + COPYRIGHT + ) + set(_multi_value "") + cmake_parse_arguments( + _pkg + "${_options}" + "${_one_value}" + "${_multi_value}" + ${ARGN} + ) + + if(NOT _pkg_NAME) + message(FATAL_ERROR "itk_sbom_register_package: NAME is required.") + endif() + if(NOT _pkg_SPDX_LICENSE) + set(_pkg_SPDX_LICENSE "NOASSERTION") + endif() + if(NOT _pkg_VERSION) + set(_pkg_VERSION "NOASSERTION") + endif() + if(NOT _pkg_DOWNLOAD_LOCATION) + set(_pkg_DOWNLOAD_LOCATION "NOASSERTION") + endif() + if(NOT _pkg_SUPPLIER) + set(_pkg_SUPPLIER "NOASSERTION") + endif() + if(NOT _pkg_COPYRIGHT) + set(_pkg_COPYRIGHT "NOASSERTION") + endif() + + # Sanitize the name for use as SPDX ID (only alphanumeric and -) + string(REGEX REPLACE "[^A-Za-z0-9-]" "-" _spdx_id "${_pkg_NAME}") + + # Escape all user-supplied fields for JSON safety + _itk_sbom_json_escape("${_pkg_NAME}" _pkg_NAME_escaped) + _itk_sbom_json_escape("${_pkg_VERSION}" _pkg_VERSION_escaped) + _itk_sbom_json_escape("${_pkg_DOWNLOAD_LOCATION}" _pkg_DOWNLOAD_escaped) + _itk_sbom_json_escape("${_pkg_SUPPLIER}" _pkg_SUPPLIER_escaped) + _itk_sbom_json_escape("${_pkg_SPDX_LICENSE}" _pkg_LICENSE_escaped) + _itk_sbom_json_escape("${_pkg_COPYRIGHT}" _pkg_COPYRIGHT_escaped) + + set(_entry "") + string(APPEND _entry " {\n") + string(APPEND _entry " \"SPDXID\": \"SPDXRef-${_spdx_id}\",\n") + string(APPEND _entry " \"name\": \"${_pkg_NAME_escaped}\",\n") + string(APPEND _entry " \"versionInfo\": \"${_pkg_VERSION_escaped}\",\n") + string( + APPEND + _entry + " \"downloadLocation\": \"${_pkg_DOWNLOAD_escaped}\",\n" + ) + string(APPEND _entry " \"supplier\": \"${_pkg_SUPPLIER_escaped}\",\n") + string( + APPEND + _entry + " \"licenseConcluded\": \"${_pkg_LICENSE_escaped}\",\n" + ) + string( + APPEND + _entry + " \"licenseDeclared\": \"${_pkg_LICENSE_escaped}\",\n" + ) + string( + APPEND + _entry + " \"copyrightText\": \"${_pkg_COPYRIGHT_escaped}\",\n" + ) + string(APPEND _entry " \"filesAnalyzed\": false\n") + string(APPEND _entry " }") + + set_property( + GLOBAL + APPEND + PROPERTY + ITK_SBOM_EXTRA_PACKAGES + "${_entry}" + ) + + # Also store the SPDX ID for relationship generation + set_property( + GLOBAL + APPEND + PROPERTY + ITK_SBOM_EXTRA_SPDX_IDS + "SPDXRef-${_spdx_id}" + ) +endfunction() + +#----------------------------------------------------------------------------- +# Internal: Escape a string for use in a JSON value. +# Handles backslashes, double quotes, and newlines. +# +function(_itk_sbom_json_escape input_string output_var) + set(_str "${input_string}") + # Escape backslashes first + string(REPLACE "\\" "\\\\" _str "${_str}") + # Escape double quotes + string(REPLACE "\"" "\\\"" _str "${_str}") + # Escape newlines (CMake uses \n for semicolons and actual newlines) + string(REPLACE "\n" "\\n" _str "${_str}") + # Escape tabs + string(REPLACE "\t" "\\t" _str "${_str}") + set(${output_var} "${_str}" PARENT_SCOPE) +endfunction() + +#----------------------------------------------------------------------------- +# Main function: Generate the SPDX 2.3 SBOM JSON document. +# +function(itk_generate_sbom) + string(TIMESTAMP _sbom_timestamp "%Y-%m-%dT%H:%M:%SZ" UTC) + string(TIMESTAMP _sbom_uid "%Y%m%d%H%M%S" UTC) + + set( + _sbom_namespace + "https://spdx.org/spdxdocs/ITK-${ITK_VERSION}-${_sbom_uid}" + ) + + # --- Begin JSON document --- + set(_json "") + string(APPEND _json "{\n") + string(APPEND _json " \"spdxVersion\": \"SPDX-2.3\",\n") + string(APPEND _json " \"dataLicense\": \"CC0-1.0\",\n") + string(APPEND _json " \"SPDXID\": \"SPDXRef-DOCUMENT\",\n") + string(APPEND _json " \"name\": \"ITK-${ITK_VERSION}-SBOM\",\n") + string(APPEND _json " \"documentNamespace\": \"${_sbom_namespace}\",\n") + + # --- creationInfo --- + string(APPEND _json " \"creationInfo\": {\n") + string(APPEND _json " \"created\": \"${_sbom_timestamp}\",\n") + string(APPEND _json " \"creators\": [\n") + string(APPEND _json " \"Tool: CMake-${CMAKE_VERSION}\",\n") + string(APPEND _json " \"Organization: NumFOCUS\"\n") + string(APPEND _json " ],\n") + string( + APPEND + _json + " \"licenseListVersion\": \"${ITK_SBOM_SPDX_LICENSE_LIST_VERSION}\"\n" + ) + string(APPEND _json " },\n") + + # --- packages array --- + string(APPEND _json " \"packages\": [\n") + + # ITK main package + string(APPEND _json " {\n") + string(APPEND _json " \"SPDXID\": \"SPDXRef-ITK\",\n") + string(APPEND _json " \"name\": \"ITK\",\n") + string(APPEND _json " \"versionInfo\": \"${ITK_VERSION}\",\n") + string( + APPEND + _json + " \"downloadLocation\": \"https://github.com/InsightSoftwareConsortium/ITK\",\n" + ) + string(APPEND _json " \"supplier\": \"Organization: NumFOCUS\",\n") + string(APPEND _json " \"licenseConcluded\": \"Apache-2.0\",\n") + string(APPEND _json " \"licenseDeclared\": \"Apache-2.0\",\n") + string( + APPEND + _json + " \"copyrightText\": \"Copyright 1999-2019 Insight Software Consortium, Copyright 2020-present NumFOCUS\",\n" + ) + string(APPEND _json " \"filesAnalyzed\": false\n") + string(APPEND _json " }") + + # Collect ThirdParty modules from enabled modules list + set(_thirdparty_spdx_ids "") + # Track custom license refs for hasExtractedLicensingInfo + set(_custom_license_ids "") + set(_custom_license_names "") + set(_custom_license_texts "") + foreach(_mod ${ITK_MODULES_ENABLED}) + if(${_mod}_IS_TEST) + continue() + endif() + + # Only include modules that have SPDX metadata declared + set(_pkg_license "${ITK_MODULE_${_mod}_SPDX_LICENSE}") + if(NOT _pkg_license) + if(${_mod}_THIRD_PARTY) + message( + AUTHOR_WARNING + "ThirdParty module ${_mod} has no SPDX_LICENSE in itk-module.cmake. " + "Please add SPDX metadata for SBOM compliance." + ) + endif() + continue() + endif() + + set(_pkg_version "${ITK_MODULE_${_mod}_SPDX_VERSION}") + set(_pkg_download "${ITK_MODULE_${_mod}_SPDX_DOWNLOAD_LOCATION}") + set(_pkg_copyright "${ITK_MODULE_${_mod}_SPDX_COPYRIGHT}") + if(NOT _pkg_version) + set(_pkg_version "NOASSERTION") + endif() + if(NOT _pkg_download) + set(_pkg_download "NOASSERTION") + endif() + if(NOT _pkg_copyright) + set(_pkg_copyright "NOASSERTION") + endif() + + # Escape all user-supplied fields for JSON safety + _itk_sbom_json_escape("${_pkg_version}" _pkg_version) + _itk_sbom_json_escape("${_pkg_download}" _pkg_download) + _itk_sbom_json_escape("${_pkg_license}" _pkg_license) + _itk_sbom_json_escape("${_pkg_copyright}" _pkg_copyright) + + set(_pkg_description "${ITK_MODULE_${_mod}_DESCRIPTION}") + if(_pkg_description) + _itk_sbom_json_escape("${_pkg_description}" _pkg_description) + endif() + + # Collect custom license references + set(_custom_text "${ITK_MODULE_${_mod}_SPDX_CUSTOM_LICENSE_TEXT}") + set(_custom_name "${ITK_MODULE_${_mod}_SPDX_CUSTOM_LICENSE_NAME}") + if(_custom_text AND _custom_name) + list(APPEND _custom_license_ids "${_pkg_license}") + list(APPEND _custom_license_names "${_custom_name}") + list(APPEND _custom_license_texts "${_custom_text}") + endif() + + # Sanitize module name for SPDX ID + string(REGEX REPLACE "[^A-Za-z0-9-]" "-" _spdx_id "${_mod}") + list(APPEND _thirdparty_spdx_ids "SPDXRef-${_spdx_id}") + + string(APPEND _json ",\n") + string(APPEND _json " {\n") + string(APPEND _json " \"SPDXID\": \"SPDXRef-${_spdx_id}\",\n") + string(APPEND _json " \"name\": \"${_mod}\",\n") + string(APPEND _json " \"versionInfo\": \"${_pkg_version}\",\n") + string(APPEND _json " \"downloadLocation\": \"${_pkg_download}\",\n") + string(APPEND _json " \"supplier\": \"NOASSERTION\",\n") + string(APPEND _json " \"licenseConcluded\": \"${_pkg_license}\",\n") + string(APPEND _json " \"licenseDeclared\": \"${_pkg_license}\",\n") + string(APPEND _json " \"copyrightText\": \"${_pkg_copyright}\",\n") + if(_pkg_description) + string(APPEND _json " \"description\": \"${_pkg_description}\",\n") + endif() + string(APPEND _json " \"filesAnalyzed\": false\n") + string(APPEND _json " }") + endforeach() + + # FFTW (not an ITK module, but an optional external dependency) + if(ITK_USE_FFTWD OR ITK_USE_FFTWF) + set(_fftw_license "GPL-2.0-or-later") + set(_fftw_version "NOASSERTION") + if(DEFINED _fftw_target_version) + set(_fftw_version "${_fftw_target_version}") + elseif(DEFINED FFTW_VERSION) + set(_fftw_version "${FFTW_VERSION}") + endif() + + string(APPEND _json ",\n") + string(APPEND _json " {\n") + string(APPEND _json " \"SPDXID\": \"SPDXRef-FFTW\",\n") + string(APPEND _json " \"name\": \"FFTW\",\n") + string(APPEND _json " \"versionInfo\": \"${_fftw_version}\",\n") + string( + APPEND + _json + " \"downloadLocation\": \"https://www.fftw.org\",\n" + ) + string(APPEND _json " \"supplier\": \"Organization: MIT\",\n") + string(APPEND _json " \"licenseConcluded\": \"${_fftw_license}\",\n") + string(APPEND _json " \"licenseDeclared\": \"${_fftw_license}\",\n") + string( + APPEND + _json + " \"copyrightText\": \"Copyright Matteo Frigo and Massachusetts Institute of Technology\",\n" + ) + string( + APPEND + _json + " \"description\": \"Fastest Fourier Transform in the West\",\n" + ) + string(APPEND _json " \"filesAnalyzed\": false\n") + string(APPEND _json " }") + list(APPEND _thirdparty_spdx_ids "SPDXRef-FFTW") + endif() + + # Append extra packages registered by remote modules + get_property(_extra_packages GLOBAL PROPERTY ITK_SBOM_EXTRA_PACKAGES) + foreach(_extra_pkg IN LISTS _extra_packages) + string(APPEND _json ",\n${_extra_pkg}") + endforeach() + + string(APPEND _json "\n ],\n") + + # --- relationships array --- + string(APPEND _json " \"relationships\": [\n") + + # DOCUMENT describes ITK + string(APPEND _json " {\n") + string(APPEND _json " \"spdxElementId\": \"SPDXRef-DOCUMENT\",\n") + string(APPEND _json " \"relationshipType\": \"DESCRIBES\",\n") + string(APPEND _json " \"relatedSpdxElement\": \"SPDXRef-ITK\"\n") + string(APPEND _json " }") + + # ITK DEPENDS_ON each ThirdParty module + foreach(_spdx_id ${_thirdparty_spdx_ids}) + string(APPEND _json ",\n") + string(APPEND _json " {\n") + string(APPEND _json " \"spdxElementId\": \"SPDXRef-ITK\",\n") + string(APPEND _json " \"relationshipType\": \"DEPENDS_ON\",\n") + string(APPEND _json " \"relatedSpdxElement\": \"${_spdx_id}\"\n") + string(APPEND _json " }") + endforeach() + + # Extra packages registered by remote modules + get_property(_extra_spdx_ids GLOBAL PROPERTY ITK_SBOM_EXTRA_SPDX_IDS) + foreach(_spdx_id IN LISTS _extra_spdx_ids) + string(APPEND _json ",\n") + string(APPEND _json " {\n") + string(APPEND _json " \"spdxElementId\": \"SPDXRef-ITK\",\n") + string(APPEND _json " \"relationshipType\": \"DEPENDS_ON\",\n") + string(APPEND _json " \"relatedSpdxElement\": \"${_spdx_id}\"\n") + string(APPEND _json " }") + endforeach() + + string(APPEND _json "\n ]") + + # --- hasExtractedLicensingInfo for custom LicenseRef identifiers --- + list(LENGTH _custom_license_ids _num_custom) + if(_num_custom GREATER 0) + string(APPEND _json ",\n") + string(APPEND _json " \"hasExtractedLicensingInfo\": [\n") + set(_first_custom TRUE) + math(EXPR _last_idx "${_num_custom} - 1") + foreach(_idx RANGE ${_last_idx}) + list(GET _custom_license_ids ${_idx} _lic_id) + list(GET _custom_license_names ${_idx} _lic_name) + list(GET _custom_license_texts ${_idx} _lic_text) + if(NOT _first_custom) + string(APPEND _json ",\n") + endif() + set(_first_custom FALSE) + _itk_sbom_json_escape("${_lic_name}" _lic_name_escaped) + _itk_sbom_json_escape("${_lic_text}" _lic_text_escaped) + string(APPEND _json " {\n") + string(APPEND _json " \"licenseId\": \"${_lic_id}\",\n") + string(APPEND _json " \"name\": \"${_lic_name_escaped}\",\n") + string(APPEND _json " \"extractedText\": \"${_lic_text_escaped}\"\n") + string(APPEND _json " }") + endforeach() + string(APPEND _json "\n ]") + endif() + + # --- Close JSON document --- + string(APPEND _json "\n}\n") + + # Write SBOM to build directory + set(_sbom_file "${CMAKE_BINARY_DIR}/sbom.spdx.json") + file(WRITE "${_sbom_file}" "${_json}") + message(STATUS "SBOM generated: ${_sbom_file}") +endfunction() diff --git a/CMake/ITKSBOMValidation.cmake b/CMake/ITKSBOMValidation.cmake new file mode 100644 index 00000000000..2787c9462cf --- /dev/null +++ b/CMake/ITKSBOMValidation.cmake @@ -0,0 +1,112 @@ +#[=============================================================================[ + ITKSBOMValidation.cmake - Validate the generated SPDX SBOM document + + Adds a CTest test that validates the SBOM JSON file produced by + ITKSBOMGeneration.cmake. Checks: + 1. Valid JSON syntax + 2. Required SPDX 2.3 top-level fields present + 3. At least one package (ITK itself) + 4. DESCRIBES relationship present +#]=============================================================================] + +if(NOT ITK_GENERATE_SBOM OR NOT BUILD_TESTING) + return() +endif() + +set(_sbom_file "${CMAKE_BINARY_DIR}/sbom.spdx.json") +if(NOT EXISTS "${_sbom_file}") + return() +endif() + +if(NOT Python3_EXECUTABLE) + message(WARNING "Python3 not found; skipping SBOM validation tests.") + return() +endif() + +add_test( + NAME ITKSBOMValidation + COMMAND + ${Python3_EXECUTABLE} -c + " +import json, sys + +with open(sys.argv[1]) as f: + doc = json.load(f) + +errors = [] + +# Required SPDX 2.3 fields +for field in ['spdxVersion', 'dataLicense', 'SPDXID', 'name', + 'documentNamespace', 'creationInfo', 'packages', + 'relationships']: + if field not in doc: + errors.append(f'Missing required field: {field}') + +if doc.get('spdxVersion') != 'SPDX-2.3': + errors.append(f'Expected spdxVersion SPDX-2.3, got {doc.get(\"spdxVersion\")}') + +if doc.get('dataLicense') != 'CC0-1.0': + errors.append(f'Expected dataLicense CC0-1.0, got {doc.get(\"dataLicense\")}') + +# Must have at least ITK package +packages = doc.get('packages', []) +if len(packages) < 1: + errors.append('No packages found') + +itk_pkg = next((p for p in packages if p.get('name') == 'ITK'), None) +if itk_pkg is None: + errors.append('ITK package not found') + +# Check DESCRIBES relationship exists +rels = doc.get('relationships', []) +describes = [r for r in rels if r.get('relationshipType') == 'DESCRIBES'] +if not describes: + errors.append('No DESCRIBES relationship found') + +# Validate package SPDX IDs are unique +spdx_ids = [p.get('SPDXID') for p in packages] +dupes = set(x for x in spdx_ids if spdx_ids.count(x) > 1) +if dupes: + errors.append(f'Duplicate SPDX IDs: {dupes}') + +# Check hasExtractedLicensingInfo references are valid +extracted = doc.get('hasExtractedLicensingInfo', []) +for entry in extracted: + if 'licenseId' not in entry: + errors.append(f'Extracted license missing licenseId') + if 'extractedText' not in entry: + errors.append(f'Extracted license missing extractedText') + +if errors: + for e in errors: + print(f'SBOM ERROR: {e}', file=sys.stderr) + sys.exit(1) + +print(f'SBOM valid: {len(packages)} packages, {len(rels)} relationships, ' + f'{len(extracted)} extracted licenses') +" + "${_sbom_file}" +) +set_tests_properties( + ITKSBOMValidation + PROPERTIES + LABELS + "SBOM" +) + +# Verify SPDX_VERSION entries match UpdateFromUpstream.sh tags. +# This catches the case where a vendored dependency is updated but +# the SPDX_VERSION in itk-module.cmake is not bumped. +add_test( + NAME ITKSBOMVersionConsistency + COMMAND + ${Python3_EXECUTABLE} + "${ITK_SOURCE_DIR}/Utilities/Maintenance/VerifySPDXVersions.py" + "${ITK_SOURCE_DIR}" +) +set_tests_properties( + ITKSBOMVersionConsistency + PROPERTIES + LABELS + "SBOM" +) diff --git a/CMakeLists.txt b/CMakeLists.txt index aba2ec14dd4..46a1b2c0ed7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,14 @@ If this is not set during the initial configuration, it will have no effect." ) mark_as_advanced(ITK_USE_SYSTEM_LIBRARIES) +#----------------------------------------------------------------------------- +# Generate a Software Bill of Materials (SBOM) in SPDX 2.3 JSON format. +option( + ITK_GENERATE_SBOM + "Generate SPDX Software Bill of Materials (SBOM) at configure time" + ON +) + #----------------------------------------------------------------------------- # Enable the download and use of BrainWeb datasets. # When this data is available, additional 3D tests are enabled. @@ -744,6 +752,13 @@ add_subdirectory(Modules/Remote) # Enable modules according to user inputs and the module dependency DAG. include(ITKModuleEnablement) +# Generate SPDX Software Bill of Materials (SBOM) if enabled. +include(ITKSBOMGeneration) +if(ITK_GENERATE_SBOM) + itk_generate_sbom() +endif() +include(ITKSBOMValidation) + # Setup clang-tidy for code best-practices enforcement for C++11 include(ITKClangTidySetup) #---------------------------------------------------------------------- @@ -855,6 +870,15 @@ install( COMPONENT Runtime ) +if(ITK_GENERATE_SBOM) + install( + FILES + "${CMAKE_BINARY_DIR}/sbom.spdx.json" + DESTINATION ${ITK_INSTALL_DOC_DIR} + COMPONENT Runtime + ) +endif() + if(BUILD_TESTING) # If building the testing, write the test costs (i.e. time to run) # analysis to disk to more easily review long-running test times diff --git a/Modules/ThirdParty/DCMTK/itk-module.cmake b/Modules/ThirdParty/DCMTK/itk-module.cmake index 83cbf13f46f..17bd2e9aade 100644 --- a/Modules/ThirdParty/DCMTK/itk-module.cmake +++ b/Modules/ThirdParty/DCMTK/itk-module.cmake @@ -6,7 +6,17 @@ library suite." ) if(ITK_USE_SYSTEM_DCMTK) - itk_module(ITKDCMTK DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT) + itk_module( + ITKDCMTK + DESCRIPTION "${DOCUMENTATION}" + EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://dicom.offis.de/dcmtk" + SPDX_COPYRIGHT + "Copyright OFFIS e.V." + ) else() itk_module( ITKDCMTK @@ -17,5 +27,11 @@ else() ITKPNG DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://dicom.offis.de/dcmtk" + SPDX_COPYRIGHT + "Copyright OFFIS e.V." ) endif() diff --git a/Modules/ThirdParty/DICOMParser/itk-module.cmake b/Modules/ThirdParty/DICOMParser/itk-module.cmake index d452bc733d3..d517f5d72b5 100644 --- a/Modules/ThirdParty/DICOMParser/itk-module.cmake +++ b/Modules/ThirdParty/DICOMParser/itk-module.cmake @@ -6,4 +6,13 @@ DICOMParser is a small, lightweight C++ toolkit for reading DICOM format medical image files." ) -itk_module(ITKDICOMParser DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKDICOMParser + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://github.com/InsightSoftwareConsortium/ITK" + SPDX_COPYRIGHT + "Copyright Kitware Inc." +) diff --git a/Modules/ThirdParty/DoubleConversion/itk-module.cmake b/Modules/ThirdParty/DoubleConversion/itk-module.cmake index 6fb81b78546..9c256d5c355 100644 --- a/Modules/ThirdParty/DoubleConversion/itk-module.cmake +++ b/Modules/ThirdParty/DoubleConversion/itk-module.cmake @@ -5,4 +5,13 @@ double-conversion library published by Google." ) -itk_module(ITKDoubleConversion DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKDoubleConversion + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://github.com/google/double-conversion" + SPDX_COPYRIGHT + "Copyright Google Inc." +) diff --git a/Modules/ThirdParty/Eigen3/itk-module.cmake b/Modules/ThirdParty/Eigen3/itk-module.cmake index c2783bfa2f2..21a646bd1cd 100644 --- a/Modules/ThirdParty/Eigen3/itk-module.cmake +++ b/Modules/ThirdParty/Eigen3/itk-module.cmake @@ -9,4 +9,12 @@ itk_module( DEPENDS DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "MPL-2.0 OR Apache-2.0" + SPDX_VERSION + "3.4.90" + SPDX_DOWNLOAD_LOCATION + "https://gitlab.com/libeigen/eigen" + SPDX_COPYRIGHT + "Copyright Eigen contributors" ) diff --git a/Modules/ThirdParty/Expat/itk-module.cmake b/Modules/ThirdParty/Expat/itk-module.cmake index c51714845eb..aba1f536b62 100644 --- a/Modules/ThirdParty/Expat/itk-module.cmake +++ b/Modules/ThirdParty/Expat/itk-module.cmake @@ -5,4 +5,15 @@ href=\"http://expat.sourceforge.net/\">Expat library. Expat is an XML parser library written in C." ) -itk_module(ITKExpat DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKExpat + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "MIT" + SPDX_VERSION + "2.7.4" + SPDX_DOWNLOAD_LOCATION + "https://libexpat.github.io" + SPDX_COPYRIGHT + "Copyright Expat development team" +) diff --git a/Modules/ThirdParty/GDCM/itk-module.cmake b/Modules/ThirdParty/GDCM/itk-module.cmake index c50bb710e5d..ac028a5c61b 100644 --- a/Modules/ThirdParty/GDCM/itk-module.cmake +++ b/Modules/ThirdParty/GDCM/itk-module.cmake @@ -6,7 +6,17 @@ Grassroots DiCoM is a C++ library for DICOM medical files." ) if(ITK_USE_SYSTEM_GDCM) - itk_module(ITKGDCM DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT) + itk_module( + ITKGDCM + DESCRIPTION "${DOCUMENTATION}" + EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://gdcm.sourceforge.net" + SPDX_COPYRIGHT + "Copyright GDCM contributors" + ) else() itk_module( ITKGDCM @@ -15,5 +25,11 @@ else() ITKExpat ITKOpenJPEG DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://gdcm.sourceforge.net" + SPDX_COPYRIGHT + "Copyright GDCM contributors" ) endif() diff --git a/Modules/ThirdParty/GIFTI/itk-module.cmake b/Modules/ThirdParty/GIFTI/itk-module.cmake index f13fc091ea5..84723025637 100644 --- a/Modules/ThirdParty/GIFTI/itk-module.cmake +++ b/Modules/ThirdParty/GIFTI/itk-module.cmake @@ -12,4 +12,14 @@ itk_module( ITKExpat ITKNIFTI DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "LicenseRef-NITRC-Public-Domain" + SPDX_DOWNLOAD_LOCATION + "https://www.nitrc.org/projects/gifti" + SPDX_COPYRIGHT + "NOASSERTION" + SPDX_CUSTOM_LICENSE_NAME + "NITRC GIFTI Public Domain License" + SPDX_CUSTOM_LICENSE_TEXT + "The GIFTI library is released into the public domain under the NITRC project." ) diff --git a/Modules/ThirdParty/GoogleTest/itk-module.cmake b/Modules/ThirdParty/GoogleTest/itk-module.cmake index 7473d474488..0077baa4eae 100644 --- a/Modules/ThirdParty/GoogleTest/itk-module.cmake +++ b/Modules/ThirdParty/GoogleTest/itk-module.cmake @@ -4,4 +4,16 @@ set( Google's C++ test framework. This module provides the GTest::gtest and GTest::gtest_main targets in the build directory only, and are not installed." ) -itk_module(ITKGoogleTest DEPENDS DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKGoogleTest + DEPENDS + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_VERSION + "1.17.0" + SPDX_DOWNLOAD_LOCATION + "https://github.com/google/googletest" + SPDX_COPYRIGHT + "Copyright Google Inc." +) diff --git a/Modules/ThirdParty/HDF5/itk-module.cmake b/Modules/ThirdParty/HDF5/itk-module.cmake index 4e446253184..ab10c5ded45 100644 --- a/Modules/ThirdParty/HDF5/itk-module.cmake +++ b/Modules/ThirdParty/HDF5/itk-module.cmake @@ -5,4 +5,17 @@ href=\"http://www.hdfgroup.org/HDF5/\">HDF5 library. HDF5 is a data model, library, and file format for storing and managing data." ) -itk_module(ITKHDF5 DEPENDS ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKHDF5 + DEPENDS + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_VERSION + "1.14.5" + SPDX_DOWNLOAD_LOCATION + "https://www.hdfgroup.org/solutions/hdf5" + SPDX_COPYRIGHT + "Copyright The HDF Group" +) diff --git a/Modules/ThirdParty/JPEG/itk-module.cmake b/Modules/ThirdParty/JPEG/itk-module.cmake index 4da1bfdcfca..c0d033f0f89 100644 --- a/Modules/ThirdParty/JPEG/itk-module.cmake +++ b/Modules/ThirdParty/JPEG/itk-module.cmake @@ -5,4 +5,15 @@ library published by the Independent JPEG Group and libjpeg-turbo." ) -itk_module(ITKJPEG DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKJPEG + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "IJG AND BSD-3-Clause AND Zlib" + SPDX_VERSION + "3.0.4" + SPDX_DOWNLOAD_LOCATION + "https://libjpeg-turbo.org" + SPDX_COPYRIGHT + "Copyright libjpeg-turbo contributors" +) diff --git a/Modules/ThirdParty/KWSys/itk-module.cmake b/Modules/ThirdParty/KWSys/itk-module.cmake index 36ebf9e0ae9..c1f6defbecb 100644 --- a/Modules/ThirdParty/KWSys/itk-module.cmake +++ b/Modules/ThirdParty/KWSys/itk-module.cmake @@ -7,4 +7,13 @@ library is intended to be shared among many projects. For more information, see Modules/ThirdParty/KWSys/src/README.kwsys." ) -itk_module(ITKKWSys DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKKWSys + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://gitlab.kitware.com/utils/kwsys" + SPDX_COPYRIGHT + "Copyright Kitware Inc." +) diff --git a/Modules/ThirdParty/MINC/itk-module.cmake b/Modules/ThirdParty/MINC/itk-module.cmake index 19eb4ba58b2..4774b78701b 100644 --- a/Modules/ThirdParty/MINC/itk-module.cmake +++ b/Modules/ThirdParty/MINC/itk-module.cmake @@ -6,7 +6,19 @@ image file format library." ) if(ITK_USE_SYSTEM_MINC) - itk_module(ITKMINC DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT) + itk_module( + ITKMINC + DESCRIPTION "${DOCUMENTATION}" + EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "LGPL-2.1-only" + SPDX_VERSION + "2.4.06" + SPDX_DOWNLOAD_LOCATION + "https://github.com/BIC-MNI/libminc" + SPDX_COPYRIGHT + "Copyright McConnell Brain Imaging Centre" + ) else() itk_module( ITKMINC @@ -16,5 +28,13 @@ else() ITKZLIB DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "LGPL-2.1-only" + SPDX_VERSION + "2.4.06" + SPDX_DOWNLOAD_LOCATION + "https://github.com/BIC-MNI/libminc" + SPDX_COPYRIGHT + "Copyright McConnell Brain Imaging Centre" ) endif() diff --git a/Modules/ThirdParty/MetaIO/itk-module.cmake b/Modules/ThirdParty/MetaIO/itk-module.cmake index 3d9c955a10c..6c633d1704f 100644 --- a/Modules/ThirdParty/MetaIO/itk-module.cmake +++ b/Modules/ThirdParty/MetaIO/itk-module.cmake @@ -8,4 +8,15 @@ vessels, needles, etc.), blobs (for arbitrary shaped objects), cubes, spheres, etc. The complete library is known as MetaIO." ) -itk_module(ITKMetaIO DEPENDS ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKMetaIO + DEPENDS + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "Apache-2.0" + SPDX_DOWNLOAD_LOCATION + "https://github.com/Kitware/MetaIO" + SPDX_COPYRIGHT + "Copyright Kitware Inc." +) diff --git a/Modules/ThirdParty/NIFTI/itk-module.cmake b/Modules/ThirdParty/NIFTI/itk-module.cmake index 43a6bde4e44..41ccb7dcf1b 100644 --- a/Modules/ThirdParty/NIFTI/itk-module.cmake +++ b/Modules/ThirdParty/NIFTI/itk-module.cmake @@ -6,4 +6,21 @@ Neuroimaging Informatics Technology Initiative provides an Analyze-style MRI file format." ) -itk_module(ITKNIFTI DEPENDS ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKNIFTI + DEPENDS + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "LicenseRef-NIFTI-Public-Domain" + SPDX_VERSION + "3.0.0" + SPDX_DOWNLOAD_LOCATION + "https://github.com/NIFTI-Imaging/nifti_clib" + SPDX_COPYRIGHT + "NOASSERTION" + SPDX_CUSTOM_LICENSE_NAME + "NIFTI Public Domain License" + SPDX_CUSTOM_LICENSE_TEXT + "This software is in the public domain. The NIFTI header and library are released into the public domain." +) diff --git a/Modules/ThirdParty/Netlib/itk-module.cmake b/Modules/ThirdParty/Netlib/itk-module.cmake index 411112d5b39..a626fd4e5d8 100644 --- a/Modules/ThirdParty/Netlib/itk-module.cmake +++ b/Modules/ThirdParty/Netlib/itk-module.cmake @@ -5,4 +5,19 @@ href=\"http://www.netlib.org/slatec/\">netlib slatec routines. They are used by the probability distributions in ITK." ) -itk_module(ITKNetlib DEPENDS ITKVNL DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKNetlib + DEPENDS + ITKVNL + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "LicenseRef-Netlib-SLATEC" + SPDX_DOWNLOAD_LOCATION + "https://www.netlib.org/slatec" + SPDX_COPYRIGHT + "NOASSERTION" + SPDX_CUSTOM_LICENSE_NAME + "Netlib SLATEC Public Domain License" + SPDX_CUSTOM_LICENSE_TEXT + "The SLATEC Common Mathematical Library is issued by the U.S. Government and is in the public domain." +) diff --git a/Modules/ThirdParty/NrrdIO/itk-module.cmake b/Modules/ThirdParty/NrrdIO/itk-module.cmake index 91123ea8cd8..9fbadb50c36 100644 --- a/Modules/ThirdParty/NrrdIO/itk-module.cmake +++ b/Modules/ThirdParty/NrrdIO/itk-module.cmake @@ -4,4 +4,15 @@ set( href=\"http://teem.sourceforge.net/nrrd/lib.html\">NRRD image file format." ) -itk_module(ITKNrrdIO DEPENDS ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKNrrdIO + DEPENDS + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "LGPL-2.1-only" + SPDX_DOWNLOAD_LOCATION + "https://teem.sourceforge.net/nrrd" + SPDX_COPYRIGHT + "Copyright Teem contributors" +) diff --git a/Modules/ThirdParty/OpenJPEG/itk-module.cmake b/Modules/ThirdParty/OpenJPEG/itk-module.cmake index 21129c30e8e..d0a16b0f17e 100644 --- a/Modules/ThirdParty/OpenJPEG/itk-module.cmake +++ b/Modules/ThirdParty/OpenJPEG/itk-module.cmake @@ -7,4 +7,16 @@ has been developed in order to promote the use of JPEG 2000, the new still-image compression standard from the Joint Photographic Experts Group (JPEG)." ) -itk_module(ITKOpenJPEG EXCLUDE_FROM_DEFAULT DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKOpenJPEG + EXCLUDE_FROM_DEFAULT + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-2-Clause" + SPDX_VERSION + "2.5.4" + SPDX_DOWNLOAD_LOCATION + "https://www.openjpeg.org" + SPDX_COPYRIGHT + "Copyright OpenJPEG contributors" +) diff --git a/Modules/ThirdParty/PNG/itk-module.cmake b/Modules/ThirdParty/PNG/itk-module.cmake index ad742c3b0f0..65085256156 100644 --- a/Modules/ThirdParty/PNG/itk-module.cmake +++ b/Modules/ThirdParty/PNG/itk-module.cmake @@ -5,4 +5,17 @@ href=\"http://www.libpng.org/pub/png/libpng.html/\">Portable Network Graphics (PNG) image file format library." ) -itk_module(ITKPNG DEPENDS ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKPNG + DEPENDS + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "Libpng" + SPDX_VERSION + "1.6.54" + SPDX_DOWNLOAD_LOCATION + "https://www.libpng.org/pub/png/libpng.html" + SPDX_COPYRIGHT + "Copyright libpng contributors" +) diff --git a/Modules/ThirdParty/TBB/itk-module.cmake b/Modules/ThirdParty/TBB/itk-module.cmake index 63ebc3ad4b3..305b388fdda 100644 --- a/Modules/ThirdParty/TBB/itk-module.cmake +++ b/Modules/ThirdParty/TBB/itk-module.cmake @@ -7,4 +7,14 @@ TBB is Intel TBB threading library." # ITKTBB module needs to be defined even if ITK_USE_TBB # is OFF, otherwise ITK cannot compile. -itk_module(ITKTBB DESCRIPTION "${DOCUMENTATION}" EXCLUDE_FROM_DEFAULT) +itk_module( + ITKTBB + DESCRIPTION "${DOCUMENTATION}" + EXCLUDE_FROM_DEFAULT + SPDX_LICENSE + "Apache-2.0" + SPDX_DOWNLOAD_LOCATION + "https://github.com/oneapi-src/oneTBB" + SPDX_COPYRIGHT + "Copyright Intel Corporation" +) diff --git a/Modules/ThirdParty/TIFF/itk-module.cmake b/Modules/ThirdParty/TIFF/itk-module.cmake index 5d005104093..adffb740178 100644 --- a/Modules/ThirdParty/TIFF/itk-module.cmake +++ b/Modules/ThirdParty/TIFF/itk-module.cmake @@ -11,4 +11,12 @@ itk_module( ITKZLIB ITKJPEG DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "libtiff" + SPDX_VERSION + "4.7.0" + SPDX_DOWNLOAD_LOCATION + "https://libtiff.maptools.org" + SPDX_COPYRIGHT + "Copyright libtiff contributors" ) diff --git a/Modules/ThirdParty/VNL/itk-module.cmake b/Modules/ThirdParty/VNL/itk-module.cmake index 957d4ccc0f4..ea9c4e5f07a 100644 --- a/Modules/ThirdParty/VNL/itk-module.cmake +++ b/Modules/ThirdParty/VNL/itk-module.cmake @@ -5,4 +5,13 @@ href=\"http://vxl.sourceforge.net\">VNL numeric library from the VXL vision library suite." ) -itk_module(ITKVNL DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKVNL + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "BSD-3-Clause" + SPDX_DOWNLOAD_LOCATION + "https://vxl.github.io" + SPDX_COPYRIGHT + "Copyright VXL contributors" +) diff --git a/Modules/ThirdParty/ZLIB/itk-module.cmake b/Modules/ThirdParty/ZLIB/itk-module.cmake index 547b75167d6..4ce178fd719 100644 --- a/Modules/ThirdParty/ZLIB/itk-module.cmake +++ b/Modules/ThirdParty/ZLIB/itk-module.cmake @@ -6,4 +6,15 @@ general purpose data compression library, designed as a drop-in replacement for ZLIB." ) -itk_module(ITKZLIB DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKZLIB + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "Zlib" + SPDX_VERSION + "2.2.5" + SPDX_DOWNLOAD_LOCATION + "https://github.com/zlib-ng/zlib-ng" + SPDX_COPYRIGHT + "Copyright zlib-ng contributors" +) diff --git a/Modules/ThirdParty/libLBFGS/itk-module.cmake b/Modules/ThirdParty/libLBFGS/itk-module.cmake index 3ef2ce59ed8..1ce757eaca5 100644 --- a/Modules/ThirdParty/libLBFGS/itk-module.cmake +++ b/Modules/ThirdParty/libLBFGS/itk-module.cmake @@ -5,4 +5,13 @@ href=\"https://github.com/chokkan/liblbfgs\">libLBFGS library, a C++ implementaiton of the LBFGS implementation in Netlib." ) -itk_module(ITKLIBLBFGS DESCRIPTION "${DOCUMENTATION}") +itk_module( + ITKLIBLBFGS + DESCRIPTION "${DOCUMENTATION}" + SPDX_LICENSE + "MIT" + SPDX_DOWNLOAD_LOCATION + "https://github.com/chokkan/liblbfgs" + SPDX_COPYRIGHT + "Copyright Naoaki Okazaki" +) diff --git a/Utilities/Maintenance/VerifySPDXVersions.py b/Utilities/Maintenance/VerifySPDXVersions.py new file mode 100755 index 00000000000..26ab562ae79 --- /dev/null +++ b/Utilities/Maintenance/VerifySPDXVersions.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Verify SPDX_VERSION in itk-module.cmake matches UpdateFromUpstream.sh tags. + +For each ThirdParty module that has both an UpdateFromUpstream.sh with a +parseable version tag and an SPDX_VERSION in itk-module.cmake, verify +they are consistent. + +Modules tracking 'master', commit SHAs, or custom ITK tags are skipped +since their version cannot be derived from the tag alone. + +Exit code 0 if all checked modules match, 1 if any mismatch is found. +""" + +import re +import sys +from pathlib import Path + + +def extract_tag_from_upstream_script(script_path: Path) -> str | None: + """Extract the 'tag' value from UpdateFromUpstream.sh.""" + text = script_path.read_text() + # Match: readonly tag="..." or tag="..." or readonly tag='...' + m = re.search(r"""(?:readonly\s+)?tag\s*=\s*['"]([^'"]+)['"]""", text) + return m.group(1) if m else None + + +def normalize_version_from_tag(tag: str) -> str | None: + """Attempt to extract a semver-like version from a git tag. + + Returns None if the tag is a SHA, 'master', or an unrecognizable format. + """ + # Skip SHAs (40-hex or short) + if re.fullmatch(r"[0-9a-f]{7,40}", tag): + return None + # Skip 'master' or 'main' + if tag in ("master", "main"): + return None + # Skip custom ITK tags like 'for/itk-20260305-4c99fca' + if tag.startswith("for/"): + return None + + # Try common patterns: + # v1.2.3 or V1.2.3 + m = re.fullmatch(r"[vV]?(\d+\.\d+(?:\.\d+)?)", tag) + if m: + return m.group(1) + + # hdf5_1.14.5 + m = re.fullmatch(r"[a-zA-Z0-9]+[_-](\d+\.\d+(?:\.\d+)?)", tag) + if m: + return m.group(1) + + # R_2_7_4 (Expat style) + m = re.fullmatch(r"R_(\d+)_(\d+)_(\d+)", tag) + if m: + return f"{m.group(1)}.{m.group(2)}.{m.group(3)}" + + # Bare version like 2.2.5 or 3.0.4 + m = re.fullmatch(r"(\d+\.\d+(?:\.\d+)?)", tag) + if m: + return m.group(1) + + return None + + +def extract_spdx_version(module_cmake: Path) -> str | None: + """Extract SPDX_VERSION value from itk-module.cmake.""" + text = module_cmake.read_text() + m = re.search(r'SPDX_VERSION\s+"([^"]+)"', text) + return m.group(1) if m else None + + +def main() -> int: + if len(sys.argv) > 1: + itk_source = Path(sys.argv[1]) + else: + # Default: assume running from ITK source root + itk_source = Path(__file__).resolve().parents[2] + + thirdparty_dir = itk_source / "Modules" / "ThirdParty" + if not thirdparty_dir.is_dir(): + print(f"ERROR: {thirdparty_dir} not found", file=sys.stderr) + return 1 + + errors = [] + checked = 0 + skipped = 0 + + for module_dir in sorted(thirdparty_dir.iterdir()): + if not module_dir.is_dir(): + continue + + upstream_script = module_dir / "UpdateFromUpstream.sh" + module_cmake = module_dir / "itk-module.cmake" + + if not upstream_script.exists() or not module_cmake.exists(): + continue + + tag = extract_tag_from_upstream_script(upstream_script) + if tag is None: + skipped += 1 + continue + + expected_version = normalize_version_from_tag(tag) + if expected_version is None: + skipped += 1 + continue + + declared_version = extract_spdx_version(module_cmake) + if declared_version is None: + errors.append( + f"{module_dir.name}: UpdateFromUpstream.sh tag='{tag}' " + f"implies version {expected_version}, but no SPDX_VERSION " + f"declared in itk-module.cmake" + ) + checked += 1 + continue + + if declared_version != expected_version: + errors.append( + f"{module_dir.name}: SPDX_VERSION='{declared_version}' " + f"does not match UpdateFromUpstream.sh tag='{tag}' " + f"(expected '{expected_version}')" + ) + + checked += 1 + + print(f"Checked {checked} modules, skipped {skipped} " f"(master/SHA/custom tags)") + + if errors: + print(f"\n{len(errors)} version mismatch(es):", file=sys.stderr) + for e in errors: + print(f" FAIL: {e}", file=sys.stderr) + return 1 + + print("All SPDX_VERSION entries match UpdateFromUpstream.sh tags.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())