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())