#!/usr/bin/env python3 ############################################################################## # MIT License # # Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. ############################################################################## import argparse import os import subprocess import sys import tempfile from pathlib import Path def detect_repo_structure(): """Detect if in monorepo or standalone structure""" cwd = Path.cwd() if "projects/rocprofiler-compute" in str(cwd): parts = cwd.parts idx = parts.index("rocprofiler-compute") if idx > 0 and parts[idx - 1] == "projects": monorepo_root = Path(*parts[: idx - 1]) project_root = monorepo_root / "projects" / "rocprofiler-compute" return True, monorepo_root, project_root if (cwd / "CMakeLists.txt").exists(): with open(cwd / "CMakeLists.txt") as f: content = f.read() if ( "project(rocprofiler-compute" in content or "project(\n rocprofiler-compute" in content ): return False, None, cwd return False, None, cwd def generate_ctest_dashboard_script(args, source_dir, binary_dir): """ Generate a complete CTest dashboard script that handles: - Configuration - Build - Test - Coverage - Submit to CDash """ cache_entries = [ "CMAKE_BUILD_TYPE:STRING=Release", f"CMAKE_PREFIX_PATH:PATH={os.environ.get('ROCM_PATH', '/opt/rocm')}", "ENABLE_TESTS:BOOL=ON", "INSTALL_TESTS:BOOL=ON", "ENABLE_COVERAGE:BOOL=ON", f"PYTEST_NUMPROCS:STRING={args.pytest_numprocs}", ] test_args = "" if args.ctest_args: test_args = " ".join(args.ctest_args) # ruff: noqa script_content = f""" cmake_minimum_required(VERSION 3.19) ############################################################################## # CTest Dashboard Script for rocprofiler-compute # Auto-generated by run-ci.py ############################################################################## # Dashboard configuration set(CTEST_PROJECT_NAME "{args.project_name}") set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC") set(CTEST_DROP_SITE_CDASH TRUE) # CDash submission settings if(CMAKE_VERSION VERSION_GREATER 3.14) set(CTEST_SUBMIT_URL "{args.submit_url}") else() set(CTEST_DROP_METHOD "https") set(CTEST_DROP_SITE "{args.cdash_host}") set(CTEST_DROP_LOCATION "{args.submit_path}") endif() # Build identification set(CTEST_SITE "{args.site}") set(CTEST_BUILD_NAME "{args.build_name}") set(CTEST_SOURCE_DIRECTORY "{source_dir}") set(CTEST_BINARY_DIRECTORY "{binary_dir}") # Build config set(CTEST_CMAKE_GENERATOR "Unix Makefiles") set(CTEST_BUILD_CONFIGURATION "Release") # Config CMake command with all required options set(CTEST_CONFIGURE_COMMAND "cmake -B ${{CTEST_BINARY_DIRECTORY}} ${{CTEST_SOURCE_DIRECTORY}} -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH={os.environ.get("ROCM_PATH", "/opt/rocm")} -DENABLE_TESTS=ON -DINSTALL_TESTS=ON -DENABLE_COVERAGE=ON -DPYTEST_NUMPROCS={args.pytest_numprocs}") message(STATUS "CMake configure command: ${{CTEST_CONFIGURE_COMMAND}}") message(STATUS "Starting {args.mode} dashboard...") ctest_start({args.mode}) # Config step message(STATUS "Configuring project...") ctest_configure( RETURN_VALUE configure_result CAPTURE_CMAKE_ERROR configure_error ) if(configure_result OR configure_error) message(WARNING "Configuration had issues but continuing...") endif() # Build step message(STATUS "Building project...") set(CTEST_BUILD_FLAGS "-j{args.build_jobs}") ctest_build( TARGET {"install" if args.install else "all"} RETURN_VALUE build_result CAPTURE_CMAKE_ERROR build_error ) if(build_result OR build_error) message(WARNING "Build had issues but continuing...") endif() # Test step message(STATUS "Running tests...") ctest_test( {test_args} PARALLEL_LEVEL 1 RETURN_VALUE test_result CAPTURE_CMAKE_ERROR test_error ) if(test_result OR test_error) message(WARNING "Some tests failed but continuing with coverage and upload...") endif() # Coverage step (if coverage file exists) set(COVERAGE_FILE "${{CTEST_BINARY_DIRECTORY}}/coverage.xml") if(EXISTS "${{COVERAGE_FILE}}") message(STATUS "Processing coverage data...") file(MAKE_DIRECTORY "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo") file(COPY "${{COVERAGE_FILE}}" DESTINATION "${{CTEST_BINARY_DIRECTORY}}/Testing/CoverageInfo/") ctest_coverage( RETURN_VALUE coverage_result CAPTURE_CMAKE_ERROR coverage_error ) if(coverage_result OR coverage_error) message(WARNING "Coverage processing had issues but continuing...") endif() else() message(STATUS "No coverage file found at ${{COVERAGE_FILE}}") message(STATUS "Skipping coverage step") endif() message(STATUS "Submitting to CDash...") ctest_submit( RETRY_COUNT 3 RETRY_DELAY 5 RETURN_VALUE submit_result CAPTURE_CMAKE_ERROR submit_error ) if(submit_result OR submit_error) message(WARNING "CDash submission encountered issues") message(STATUS "Results available locally in: ${{CTEST_BINARY_DIRECTORY}}/Testing/") else() message(STATUS "Successfully submitted to CDash!") message(STATUS "View at: {args.submit_url.replace("/submit.php?project=", "/index.php?project=")}") message(STATUS "Build name: {args.build_name}") endif() message(STATUS "") message(STATUS "========================================") message(STATUS "Dashboard Summary") message(STATUS "========================================") if(test_result) message(STATUS "Tests: FAILED (some tests failed or timed out)") else() message(STATUS "Tests: PASSED") endif() if(submit_result OR submit_error) message(STATUS "CDash Upload: FAILED") else() message(STATUS "CDash Upload: SUCCESS") endif() message(STATUS "========================================") # For nightly/continuous, don't fail on test failures # Only warn in Experimental mode if("{args.mode}" STREQUAL "Experimental" AND test_result) message(WARNING "Tests failed in Experimental mode") endif() """ return script_content def main(): parser = argparse.ArgumentParser( description="CI script for rocprofiler-compute: build, test, and upload to CDash" ) parser.add_argument( "--build-name", "--name", dest="build_name", required=True, help="Build name for CDash", ) parser.add_argument( "--project-name", default="rocprofiler-compute", help="CDash project name", ) parser.add_argument( "--submit-url", default="https://my.cdash.org/submit.php?project=rocprofiler-compute", help="CDash submission URL", ) parser.add_argument( "--cdash-host", default="my.cdash.org", help="CDash host (for older CMake)", ) parser.add_argument( "--submit-path", default="/submit.php?project=rocprofiler-compute", help="CDash submission path (for older CMake)", ) parser.add_argument( "--site", default=None, help="Site name for CDash (auto-detected if not provided)", ) parser.add_argument( "--source-dir", "-S", default=None, help="Source directory path (auto-detected if not provided)", ) parser.add_argument( "--binary-dir", "-B", default=None, help="Binary directory path (defaults to source-dir/build)", ) parser.add_argument( "--mode", default="Experimental", choices=["Experimental", "Nightly", "Continuous"], help="CTest dashboard mode", ) parser.add_argument( "--build-jobs", "-j", type=int, default=8, help="Number of parallel build jobs", ) parser.add_argument( "--install", action="store_true", help="Build install target instead of all target", ) parser.add_argument( "--dry-run", action="store_true", help="Generate dashboard script but don't execute", ) parser.add_argument( "--pytest-numprocs", type=int, default=4, help="Number of parallel processes for pytest", ) args, unknown = parser.parse_known_args() args.cmake_args = [] args.ctest_args = [] is_monorepo, monorepo_root, project_root = detect_repo_structure() if not args.source_dir: args.source_dir = str(project_root) if not args.binary_dir: args.binary_dir = os.path.join(args.source_dir, "build") if not args.site: if is_monorepo: args.site = f"Monorepo-{os.uname().nodename}" else: args.site = os.uname().nodename source_dir = Path(args.source_dir).absolute() binary_dir = Path(args.binary_dir).absolute() print("=" * 80) print("rocprofiler-compute CI Dashboard") print("=" * 80) print(f"Repository type: {'Monorepo' if is_monorepo else 'Standalone'}") print(f"Source directory: {source_dir}") print(f"Binary directory: {binary_dir}") print(f"Build name: {args.build_name}") print(f"Dashboard mode: {args.mode}") print(f"CDash project: {args.project_name}") print(f"Site: {args.site}") print("=" * 80) script_content = generate_ctest_dashboard_script(args, source_dir, binary_dir) if args.dry_run: print("\nGenerated CTest Dashboard Script:") print("=" * 80) print(script_content) print("=" * 80) return 0 binary_dir.mkdir(parents=True, exist_ok=True) with tempfile.NamedTemporaryFile( mode="w", suffix=".cmake", delete=False, dir=binary_dir ) as f: f.write(script_content) script_path = f.name print(f"\nDashboard script: {script_path}") print("=" * 80) try: cmd = ["ctest", "-S", script_path, "-V"] print(f"Running: {' '.join(cmd)}") print("=" * 80) print() process = subprocess.Popen( cmd, cwd=source_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, ) for line in process.stdout: print(line, end="") returncode = process.wait() print() print("=" * 80) if returncode == 0: print("✅ Dashboard completed successfully!") else: print(f"⚠️ Dashboard completed with return code: {returncode}") print("=" * 80) return returncode except Exception as e: print(f"\n❌ Error executing dashboard script: {e}", file=sys.stderr) import traceback traceback.print_exc() return 1 finally: try: os.unlink(script_path) except Exception: pass if __name__ == "__main__": sys.exit(main())