| #!/usr/bin/env python |
| """Produces an LCOV file from a directory of LLVM .profraw files. |
| |
| Finds all .profraw files in a source directory specified as {--base}/ and merges |
| them into a single .profdata file using llvm-profdata. An optional objects list |
| specifying absolute paths for .a, .o, or executable binaries will be used to |
| find the coverage metadata for the collected profiles. |
| |
| If available a list of CMake targets and their corresponding object file will be |
| used to try to infer the objects based on the filenames of the .profraw files as |
| `cmake_target[.optional-extra-discriminators].profraw`. Objects can also be |
| explicitly specified for any profiles not matching a target. |
| The `--targets=` file is a ; delimited dictionary of target=path items. |
| e.g. `cmake_target_a=/bin/a;cmake_target_b=/bin/b` |
| |
| The paths for llvm-profdata and llvm-cov can be explicitly specified as flags |
| and otherwise must be available on PATH. |
| |
| Usage: |
| ./build_tools/scripts/export_profdata_lcov.py \\ |
| --base=../build/coverage/runtime \\ |
| --targets=../build/coverage/runtime.target.list \\ |
| --output=../build/coverage/runtime.lcov.info |
| """ |
| |
| import argparse |
| from pathlib import Path |
| import subprocess |
| import sys |
| import os |
| |
| |
| def parse_arguments(argv): |
| parser = argparse.ArgumentParser( |
| prog="export_profdata_lcov", |
| usage=__doc__, |
| ) |
| parser.add_argument( |
| "--base", |
| required=True, |
| type=Path, |
| help="Root source directory containing .profraw files.", |
| ) |
| parser.add_argument( |
| "--objects", |
| type=str, |
| help="Optional list of absolute paths for .a, .o, or executable binaries used to find coverage metadata for collected profiles.", |
| ) |
| parser.add_argument( |
| "--targets", |
| type=str, |
| help="Optional semicolon (;) delimited list of [CMake Target]=[path] items, e.g. `cmake_target_a=/bin/a;cmake_target_b=/bin/b`.", |
| ) |
| parser.add_argument( |
| "--output", |
| required=True, |
| type=Path, |
| help="Output file (e.g. ../build/covarge/runtime.lcov.info).", |
| ) |
| parser.add_argument( |
| "--llvm-profdata", |
| type=Path, |
| default="llvm-profdata", |
| help="Path to the llvm-profdata tool (may be relative if on PATH).", |
| ) |
| parser.add_argument( |
| "--llvm-cov", |
| type=Path, |
| default="llvm-cov", |
| help="Path to the llvm-cov tool (may be relative if on PATH).", |
| ) |
| args = parser.parse_args(argv) |
| return args |
| |
| |
| def main(args): |
| # Erase existing artifacts, if any. |
| output_path = args.output |
| if output_path.exists(): |
| print(f"Removing existing {output_path} file") |
| output_path.unlink() |
| profdata_path = args.base.with_suffix(".profdata") |
| if profdata_path.exists(): |
| print(f"Removing existing {profdata_path} file") |
| profdata_path.unlink() |
| |
| # Find all .profraw files in the source directory. |
| source_dir = Path(f"{args.base}{os.path.sep}") |
| if not source_dir.is_dir(): |
| print(f"WARNING: source directory {source_dir} not found, skipping run") |
| sys.exit(0) |
| source_files = list(source_dir.glob("*.profraw")) |
| if not source_files: |
| print(f"No sources profraw files found in {source_dir}, skipping run") |
| sys.exit(0) |
| |
| # Merge all source profraw files together into a single profdata file. |
| print(f"Merging from source files in {source_dir} to {profdata_path}:") |
| for source_file in source_files: |
| print(f"- {source_file}") |
| subprocess.check_call( |
| [ |
| args.llvm_profdata, |
| "merge", |
| "--sparse", |
| f"--output={profdata_path}", |
| ] |
| + source_files |
| ) |
| |
| # Add any explicitly specified objects first. |
| objects = [] |
| if args.objects: |
| objects.append([object for object in args.objects.split(";")]) |
| |
| # Read target map, if specified. |
| # This may not be needed if objects are also specified and is allowed to |
| # fail. The file is a ; delimited dictionary of target=path items. |
| # e.g. cmake_target_a=/bin/a;cmake_target_b=/bin/b |
| target_map = {} |
| if args.targets: |
| with open(args.targets, "r") as targets_file: |
| for target, object in [ |
| item.split("=", 1) for item in targets_file.read().split(";") |
| ]: |
| target_map[target] = object |
| |
| # Try to automatically add target objects from the source files. |
| for source_file in source_files: |
| target_name = source_file.stem |
| target_object = target_map[target_name] |
| if target_object: |
| objects.append(target_object) |
| |
| if not objects: |
| print(f"WARNING: no objects specified/discovered, skipping export") |
| print(f"Merged profiling data available in {profdata_path}") |
| sys.exit(0) |
| |
| # Export the lcov file. |
| print(f"Exporting {profdata_path} to {output_path} using objects:") |
| for object in objects: |
| print(f"- {object}") |
| with open(output_path, "wb") as output_file: |
| subprocess.check_call( |
| [ |
| args.llvm_cov, |
| "export", |
| "-format=lcov", |
| f"-instr-profile={profdata_path}", |
| ] |
| + [f"-object={object}" for object in objects], |
| stdout=output_file, |
| text=False, |
| ) |
| |
| print(f"LCOV file written: {output_path}") |
| |
| sys.exit(0) |
| |
| |
| if __name__ == "__main__": |
| main(parse_arguments(sys.argv[1:])) |