diff --git a/itk_common.cmake b/itk_common.cmake index c3e25c64434..0dffbc5a449 100644 --- a/itk_common.cmake +++ b/itk_common.cmake @@ -382,12 +382,114 @@ if(NOT DEFINED dashboard_loop) endif() endif() +# CI log section helpers — emit collapsible group markers for +# GitHub Actions, Azure DevOps, and GitLab CI. +# GitLab CI requires unique section IDs within a job log, so callers +# in the loop below include _dashboard_iteration in the ID. +string(ASCII 27 _CI_ESC) + +function(ci_section_start section_id title) + if(DEFINED ENV{GITLAB_CI}) + string(TIMESTAMP _epoch "%s" UTC) + message("${_CI_ESC}[0Ksection_start:${_epoch}:${section_id}[collapsed=true]\r${_CI_ESC}[0K${title}") + elseif(DEFINED ENV{GITHUB_ACTIONS}) + message("::group::${title}") + elseif(DEFINED ENV{TF_BUILD}) + message("##[group]${title}") + else() + message("--- ${title} ---") + endif() +endfunction() + +function(ci_section_end section_id) + if(DEFINED ENV{GITLAB_CI}) + string(TIMESTAMP _epoch "%s" UTC) + message("${_CI_ESC}[0Ksection_end:${_epoch}:${section_id}\r${_CI_ESC}[0K") + elseif(DEFINED ENV{GITHUB_ACTIONS}) + message("::endgroup::") + elseif(DEFINED ENV{TF_BUILD}) + message("##[endgroup]") + else() + message("--- end ${section_id} ---") + endif() +endfunction() + +# Extract and print build warnings/errors from Build.xml so they +# appear directly in the CI log. ctest_build() only exposes counts +# (NUMBER_WARNINGS / NUMBER_ERRORS) — the actual compiler messages +# live inside the XML that CTest writes for CDash submission. +function(ci_report_build_diagnostics binary_dir num_warnings num_errors) + if(num_warnings EQUAL 0 AND num_errors EQUAL 0) + return() + endif() + + # Locate the Build.xml via the TAG file that CTest maintains. + set(_tag_file "${binary_dir}/Testing/TAG") + if(NOT EXISTS "${_tag_file}") + return() + endif() + file(STRINGS "${_tag_file}" _tag_lines) + list(GET _tag_lines 0 _tag_dir) + set(_build_xml "${binary_dir}/Testing/${_tag_dir}/Build.xml") + if(NOT EXISTS "${_build_xml}") + return() + endif() + + # Read Build.xml — escape semicolons so CMake list operations + # don't mangle lines that happen to contain them. + file(READ "${_build_xml}" _xml) + string(REPLACE ";" "\\;" _xml "${_xml}") + string(REPLACE "\n" ";" _xml_lines "${_xml}") + + # Walk the XML line-by-line, tracking whether we are inside a + # or block, and collect the content. + set(_in_warning FALSE) + set(_in_error FALSE) + set(_warning_texts "") + set(_error_texts "") + foreach(_line IN LISTS _xml_lines) + if("${_line}" MATCHES "") + set(_in_warning TRUE) + elseif("${_line}" MATCHES "") + set(_in_warning FALSE) + elseif("${_line}" MATCHES "") + set(_in_error TRUE) + elseif("${_line}" MATCHES "") + set(_in_error FALSE) + endif() + if("${_line}" MATCHES "(.*)") + if(_in_warning) + list(APPEND _warning_texts "${CMAKE_MATCH_1}") + elseif(_in_error) + list(APPEND _error_texts "${CMAKE_MATCH_1}") + endif() + endif() + endforeach() + + # Print collected diagnostics so they are visible in CI output. + if(num_errors GREATER 0) + message("========== BUILD ERRORS (${num_errors}) ==========") + foreach(_t IN LISTS _error_texts) + message(" ${_t}") + endforeach() + endif() + if(num_warnings GREATER 0) + message("========== BUILD WARNINGS (${num_warnings}) ==========") + foreach(_t IN LISTS _warning_texts) + message(" ${_t}") + endforeach() + endif() + message("====================================================") +endfunction() + if(COMMAND dashboard_hook_init) dashboard_hook_init() endif() set(dashboard_done 0) +set(_dashboard_iteration 0) while(NOT dashboard_done) + math(EXPR _dashboard_iteration "${_dashboard_iteration} + 1") if(dashboard_loop) set(START_TIME ${CTEST_ELAPSED_TIME}) endif() @@ -423,39 +525,57 @@ while(NOT dashboard_done) message("Found ${count} changed files") if(dashboard_fresh OR NOT dashboard_continuous OR count GREATER 0) + ci_section_start("configure_${_dashboard_iteration}" "Configure") ctest_configure(RETURN_VALUE configure_return) ctest_read_custom_files(${CTEST_BINARY_DIRECTORY}) + ci_section_end("configure_${_dashboard_iteration}") + ci_section_start("build_${_dashboard_iteration}" "Build") if(COMMAND dashboard_hook_build) dashboard_hook_build() endif() ctest_build(RETURN_VALUE build_return NUMBER_ERRORS build_errors NUMBER_WARNINGS build_warnings) + ci_section_end("build_${_dashboard_iteration}") + + # Intentionally placed OUTSIDE the collapsible build section so + # that warnings and errors are always visible in the CI log + # without having to expand the build section. + ci_report_build_diagnostics( + "${CTEST_BINARY_DIRECTORY}" "${build_warnings}" "${build_errors}") + ci_section_start("test_${_dashboard_iteration}" "Test") if(COMMAND dashboard_hook_test) dashboard_hook_test() endif() ctest_test(${CTEST_TEST_ARGS} RETURN_VALUE test_return) + ci_section_end("test_${_dashboard_iteration}") if(dashboard_do_coverage) + ci_section_start("coverage_${_dashboard_iteration}" "Coverage") if(COMMAND dashboard_hook_coverage) dashboard_hook_coverage() endif() ctest_coverage(${CTEST_COVERAGE_ARGS}) + ci_section_end("coverage_${_dashboard_iteration}") endif() if(dashboard_do_memcheck) + ci_section_start("memcheck_${_dashboard_iteration}" "MemCheck") if(COMMAND dashboard_hook_memcheck) dashboard_hook_memcheck() endif() ctest_memcheck(${CTEST_MEMCHECK_ARGS}) + ci_section_end("memcheck_${_dashboard_iteration}") endif() + ci_section_start("submit_${_dashboard_iteration}" "Submit to CDash") if(COMMAND dashboard_hook_submit) dashboard_hook_submit() endif() if(NOT dashboard_no_submit) ctest_submit() endif() + ci_section_end("submit_${_dashboard_iteration}") if(COMMAND dashboard_hook_end) dashboard_hook_end() endif()