#!/usr/bin/env python3 """Set up CodSpeed's google_benchmark fork as a PlatformIO library. CodSpeed requires their codspeed-cpp fork for CPU simulation instrumentation. This script clones the repo and assembles a flat PlatformIO-compatible library by combining google_benchmark sources, codspeed core, and instrument-hooks. PlatformIO quirks addressed: - .cc files renamed to .cpp (PlatformIO ignores .cc) - All sources merged into one src/ dir (PlatformIO can't compile from multiple source directories in a single library) - library.json created with required CodSpeed preprocessor defines Usage: python script/setup_codspeed_lib.py [--output-dir DIR] Prints JSON to stdout with lib_path for cpp_benchmark.py. Git output goes to stderr. See https://codspeed.io/docs/benchmarks/cpp#custom-build-systems """ from __future__ import annotations import argparse import json from pathlib import Path import shutil import subprocess import sys # Pin to a specific release for reproducibility CODSPEED_CPP_REPO = "https://github.com/CodSpeedHQ/codspeed-cpp.git" CODSPEED_CPP_SHA = "e633aca00da3d0ad14e7bf424d9cb47165a29028" # v2.1.0 DEFAULT_OUTPUT_DIR = "/tmp/codspeed-cpp" # Well-known paths within the codspeed-cpp repository GOOGLE_BENCHMARK_SUBDIR = "google_benchmark" CORE_SUBDIR = "core" INSTRUMENT_HOOKS_SUBDIR = Path(CORE_SUBDIR) / "instrument-hooks" INSTRUMENT_HOOKS_INCLUDES = INSTRUMENT_HOOKS_SUBDIR / "includes" INSTRUMENT_HOOKS_DIST = INSTRUMENT_HOOKS_SUBDIR / "dist" / "core.c" CORE_CMAKE = Path(CORE_SUBDIR) / "CMakeLists.txt" def _git(args: list[str], **kwargs: object) -> None: """Run a git command, sending output to stderr.""" subprocess.run( ["git", *args], check=True, stdout=kwargs.pop("stdout", sys.stderr), stderr=kwargs.pop("stderr", sys.stderr), **kwargs, ) def _clone_repo(output_dir: Path) -> None: """Shallow-clone codspeed-cpp at the pinned SHA with submodules.""" output_dir.mkdir(parents=True, exist_ok=True) _git(["init", str(output_dir)]) _git(["-C", str(output_dir), "remote", "add", "origin", CODSPEED_CPP_REPO]) _git(["-C", str(output_dir), "fetch", "--depth", "1", "origin", CODSPEED_CPP_SHA]) _git( ["-C", str(output_dir), "checkout", "FETCH_HEAD"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) _git( [ "-C", str(output_dir), "submodule", "update", "--init", "--recursive", "--depth", "1", ] ) def _read_codspeed_version(cmake_path: Path) -> str: """Extract CODSPEED_VERSION from core/CMakeLists.txt.""" if not cmake_path.exists(): return "0.0.0" for line in cmake_path.read_text().splitlines(): if line.startswith("set(CODSPEED_VERSION"): return line.split()[1].rstrip(")") return "0.0.0" def _rename_cc_to_cpp(src_dir: Path) -> None: """Rename .cc files to .cpp so PlatformIO compiles them.""" for cc_file in src_dir.glob("*.cc"): cpp_file = cc_file.with_suffix(".cpp") if not cpp_file.exists(): cc_file.rename(cpp_file) def _copy_if_missing(src: Path, dest: Path) -> None: """Copy a file only if the destination doesn't already exist.""" if not dest.exists(): shutil.copy2(src, dest) def _merge_codspeed_core_into_lib(core_src: Path, lib_src: Path) -> None: """Copy codspeed core sources into the benchmark library src/. .cpp files get a ``codspeed_`` prefix to avoid name collisions with google_benchmark's own sources. .h files keep their original names since they're referenced by ``#include "walltime.h"`` etc. """ for src_file in core_src.iterdir(): if src_file.suffix == ".cpp": _copy_if_missing(src_file, lib_src / f"codspeed_{src_file.name}") elif src_file.suffix == ".h": _copy_if_missing(src_file, lib_src / src_file.name) def _write_library_json( benchmark_dir: Path, core_include: Path, hooks_include: Path, version: str, project_root: Path, ) -> None: """Write a PlatformIO library.json with CodSpeed build flags.""" library_json = { "name": "benchmark", "version": "0.0.0", "build": { "flags": [ f"-I{core_include}", f"-I{hooks_include}", # google benchmark build flags # -O2 is critical: without it, instrument_hooks_start_benchmark_inline # doesn't get inlined and shows up as overhead in profiles "-O2", "-DNDEBUG", "-DHAVE_STD_REGEX", "-DHAVE_STEADY_CLOCK", "-DBENCHMARK_STATIC_DEFINE", # CodSpeed instrumentation flags # https://codspeed.io/docs/benchmarks/cpp#custom-build-systems "-DCODSPEED_ENABLED", "-DCODSPEED_ANALYSIS", f'-DCODSPEED_VERSION=\\"{version}\\"', f'-DCODSPEED_ROOT_DIR=\\"{project_root}\\"', '-DCODSPEED_MODE_DISPLAY=\\"simulation\\"', ], "includeDir": "include", }, } (benchmark_dir / "library.json").write_text( json.dumps(library_json, indent=2) + "\n" ) def setup_codspeed_lib(output_dir: Path) -> None: """Clone codspeed-cpp and assemble a flat PlatformIO library. The resulting library at ``output_dir/google_benchmark/`` contains: - google_benchmark sources (.cc renamed to .cpp) - codspeed core sources (prefixed ``codspeed_``) - instrument-hooks C source (as ``instrument_hooks.c``) - library.json with all required CodSpeed defines Args: output_dir: Directory to clone the repository into """ if not (output_dir / ".git").exists(): _clone_repo(output_dir) else: # Verify the existing checkout matches the pinned SHA result = subprocess.run( ["git", "-C", str(output_dir), "rev-parse", "HEAD"], capture_output=True, text=True, check=False, ) if result.returncode != 0 or result.stdout.strip() != CODSPEED_CPP_SHA: print( f"Stale codspeed-cpp checkout, re-cloning at {CODSPEED_CPP_SHA}", file=sys.stderr, ) shutil.rmtree(output_dir) _clone_repo(output_dir) benchmark_dir = output_dir / GOOGLE_BENCHMARK_SUBDIR lib_src = benchmark_dir / "src" core_dir = output_dir / CORE_SUBDIR core_include = core_dir / "include" hooks_include = output_dir / INSTRUMENT_HOOKS_INCLUDES hooks_dist_c = output_dir / INSTRUMENT_HOOKS_DIST project_root = Path(__file__).resolve().parent.parent # 1. Rename .cc → .cpp (PlatformIO doesn't compile .cc) _rename_cc_to_cpp(lib_src) # 2. Merge codspeed core sources into the library _merge_codspeed_core_into_lib(core_dir / "src", lib_src) # 3. Copy instrument-hooks C source (provides instrument_hooks_* symbols) if hooks_dist_c.exists(): _copy_if_missing(hooks_dist_c, lib_src / "instrument_hooks.c") # 4. Write library.json version = _read_codspeed_version(output_dir / CORE_CMAKE) _write_library_json( benchmark_dir, core_include, hooks_include, version, project_root ) # Output JSON config for cpp_benchmark.py print(json.dumps({"lib_path": str(benchmark_dir)})) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--output-dir", type=Path, default=Path(DEFAULT_OUTPUT_DIR), help=f"Directory to clone codspeed-cpp into (default: {DEFAULT_OUTPUT_DIR})", ) args = parser.parse_args() setup_codspeed_lib(args.output_dir) if __name__ == "__main__": main()