[libcxx-commits] [libcxx] [llvm] [libc++] Extract building libc++ into a composite action (PR #224670)
Louis Dionne via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Sep 18 09:50:22 PDT 2026
https://github.com/ldionne updated https://github.com/llvm/llvm-project/pull/224670
>From 78ff0c34c1bf15937bdebbafbe924086e43b4dda Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Fri, 18 Sep 2026 07:33:36 -0700
Subject: [PATCH 1/4] [libc++] Extract building libc++ into a composite action
This allows reusing the same build script for the PR benchmark and
the historical benchmarking jobs. This also opens the door to adding
new configurations where the library isn't rebuilt (e.g. where the
artifacts are pulled from a pre-built location, or even testing against
non-libc++.
Assisted by Claude
---
.github/workflows/libcxx-benchmark-commit.yml | 20 +++++++-
.github/workflows/libcxx-pr-benchmark.yml | 26 ++++++++---
.../libcxx/build-at-commit/action.yml | 46 +++++++++++++++++++
libcxx/utils/ci/lnt/README.md | 7 +--
libcxx/utils/ci/lnt/run-benchmarks | 38 +++++----------
5 files changed, 99 insertions(+), 38 deletions(-)
create mode 100644 .github/workflows/libcxx/build-at-commit/action.yml
diff --git a/.github/workflows/libcxx-benchmark-commit.yml b/.github/workflows/libcxx-benchmark-commit.yml
index 040c0a7536dc4..9d10f31445e5e 100644
--- a/.github/workflows/libcxx-benchmark-commit.yml
+++ b/.github/workflows/libcxx-benchmark-commit.yml
@@ -107,6 +107,9 @@ jobs:
runs-on: ${{ matrix.runner }}
env:
COMPILER: ${{ matrix.cxx }}
+ # Where we install the library. This lives inside the workspace so that actions/checkout
+ # cleans it up between runs on self-hosted runners.
+ INSTALL_DIR: ${{ github.workspace }}/install
steps:
- name: Checkout the LLVM monorepo
uses: actions/checkout at df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -140,6 +143,19 @@ jobs:
"${COMPILER}" --version
python3 --version
+ # Build the library.
+ #
+ # A build failure is tolerated on purpose, since the library doesn't build at every historical commit.
+ # We still submit an empty run for such commits.
+ - name: Build libc++ at ${{ inputs.commit }}
+ continue-on-error: true
+ uses: ./.github/workflows/libcxx/build-at-commit
+ with:
+ commit: ${{ inputs.commit }}
+ install-dir: ${{ env.INSTALL_DIR }}
+ compiler: ${{ matrix.cxx }}
+ cmake-cache: ${{ matrix.cmake-cache }}
+
- name: Setup virtual environment
run: |
python3 -m venv .venv
@@ -149,7 +165,6 @@ jobs:
- name: Run the benchmarks
env:
BENCHMARK_SUITE_VERSION: ${{ matrix.benchmark-suite-version }}
- CMAKE_CACHE: ${{ matrix.cmake-cache }}
COMMIT: ${{ inputs.commit }}
FILTER: ${{ inputs.filter }}
LNT_MACHINE: ${{ matrix.lnt-machine }}
@@ -159,12 +174,13 @@ jobs:
if [ -n "${FILTER}" ]; then
filter_arg=(--filter "${FILTER}")
fi
+
libcxx/utils/ci/lnt/run-benchmarks \
--test-suite-commit "${BENCHMARK_SUITE_VERSION}" \
--machine "${LNT_MACHINE}" \
--compiler "${COMPILER}" \
--benchmark-commit "${COMMIT}" \
- --cmake-cache "${CMAKE_CACHE}" \
+ --libcxx-installation "${INSTALL_DIR}" \
"${filter_arg[@]}" \
--output "${COMMIT}.json"
diff --git a/.github/workflows/libcxx-pr-benchmark.yml b/.github/workflows/libcxx-pr-benchmark.yml
index 9c31be387b4d1..b9e540c986d17 100644
--- a/.github/workflows/libcxx-pr-benchmark.yml
+++ b/.github/workflows/libcxx-pr-benchmark.yml
@@ -110,7 +110,6 @@ jobs:
- extract-info
env:
BENCHMARKS: ${{ needs.extract-info.outputs.benchmarks }}
- CMAKE_CACHE: ${{ matrix.cmake-cache }}
COMPILER: ${{ matrix.cxx }}
LNT_MACHINE: ${{ matrix.lnt-machine }}
PR_HEAD: ${{ needs.extract-info.outputs.pr_head }}
@@ -153,12 +152,25 @@ jobs:
source .venv/bin/activate
pip install -r libcxx/utils/requirements.txt
- - name: Build the baseline and the candidate
- run: |
- source .venv/bin/activate
- baseline_commit=$(git merge-base $PR_BASE $PR_HEAD)
- ./libcxx/utils/build-at-commit --commit ${baseline_commit} --install-dir install/baseline -- -DCMAKE_CXX_COMPILER="${COMPILER}" -C "${CMAKE_CACHE}"
- ./libcxx/utils/build-at-commit --commit $PR_HEAD --install-dir install/candidate -- -DCMAKE_CXX_COMPILER="${COMPILER}" -C "${CMAKE_CACHE}"
+ - name: Determine the baseline commit
+ id: baseline
+ run: echo "commit=$(git merge-base "${PR_BASE}" "${PR_HEAD}")" >> "${GITHUB_OUTPUT}"
+
+ - name: Build the baseline
+ uses: ./.github/workflows/libcxx/build-at-commit
+ with:
+ commit: ${{ steps.baseline.outputs.commit }}
+ install-dir: ${{ github.workspace }}/install/baseline
+ compiler: ${{ matrix.cxx }}
+ cmake-cache: ${{ matrix.cmake-cache }}
+
+ - name: Build the candidate
+ uses: ./.github/workflows/libcxx/build-at-commit
+ with:
+ commit: ${{ needs.extract-info.outputs.pr_head }}
+ install-dir: ${{ github.workspace }}/install/candidate
+ compiler: ${{ matrix.cxx }}
+ cmake-cache: ${{ matrix.cmake-cache }}
- name: Run baseline and candidate interleaved
run: |
diff --git a/.github/workflows/libcxx/build-at-commit/action.yml b/.github/workflows/libcxx/build-at-commit/action.yml
new file mode 100644
index 0000000000000..33f9c35df16c3
--- /dev/null
+++ b/.github/workflows/libcxx/build-at-commit/action.yml
@@ -0,0 +1,46 @@
+# This composite action builds libc++ as of an arbitrary commit and installs it into the
+# specified directory.
+#
+# This allows other workflows to reuse this job for various purposes.
+#
+# This action assumes that the monorepo has been checked out with its full history (arbitrary
+# historical commits must be reachable) and that CMake, Ninja and the compiler are available.
+
+name: 'Build libc++ at a commit'
+description: 'Build and install libc++ as of the given commit.'
+
+inputs:
+ commit:
+ description: 'The libc++ commit to build.'
+ required: true
+ install-dir:
+ description: 'Directory to install the library into.'
+ required: true
+ compiler:
+ description: 'The C++ compiler to build the library with.'
+ required: true
+ cmake-cache:
+ description: >-
+ CMake cache describing how to build the library, as a path relative to the root of the
+ monorepo (e.g. libcxx/utils/ci/lnt/cmake/generic.cmake).
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Build libc++ at ${{ inputs.commit }}
+ shell: bash
+ env:
+ CMAKE_CACHE: ${{ inputs.cmake-cache }}
+ COMMIT: ${{ inputs.commit }}
+ COMPILER: ${{ inputs.compiler }}
+ INSTALL_DIR: ${{ inputs.install-dir }}
+ run: |
+ # Let build-at-commit use (and clean up) its own temporary source and build directories:
+ # only the installation is of interest to the caller.
+ libcxx/utils/build-at-commit \
+ --commit "${COMMIT}" \
+ --install-dir "${INSTALL_DIR}" \
+ -- \
+ -DCMAKE_CXX_COMPILER="${COMPILER}" \
+ -C "${GITHUB_WORKSPACE}/${CMAKE_CACHE}"
diff --git a/libcxx/utils/ci/lnt/README.md b/libcxx/utils/ci/lnt/README.md
index b82908f077e42..1b40d562d9391 100644
--- a/libcxx/utils/ci/lnt/README.md
+++ b/libcxx/utils/ci/lnt/README.md
@@ -76,12 +76,13 @@ which can be used to benchmark locally:
```
run-benchmarks --test-suite-commit <SHA1> --machine <MACHINE> \
--compiler clang++ --benchmark-commit <SHA2> \
+ --libcxx-installation <PATH> \
--output result.json
```
-This will run the benchmarks (using the test suite at the specified `SHA1`) against libc++
-as-of the specified `SHA2`, and produce a LNT-ready JSON report. The results can then be
-submitted to a LNT instance if desired.
+This will run the benchmarks (using the test suite at the specified `SHA1`) against the installation
+of libc++ at `PATH` (which is assumed to be libc++ as-of `SHA2`), and produce a LNT-ready JSON report.
+The results can then be submitted to a LNT instance if desired.
## Setting up a local LNT instance
diff --git a/libcxx/utils/ci/lnt/run-benchmarks b/libcxx/utils/ci/lnt/run-benchmarks
index 6365e2465892e..8ac6e8d4dfae6 100755
--- a/libcxx/utils/ci/lnt/run-benchmarks
+++ b/libcxx/utils/ci/lnt/run-benchmarks
@@ -109,6 +109,8 @@ def main(argv):
prog='run-benchmarks',
description='Benchmark libc++ at the given commit and produce a LNT JSON report.',
epilog='This script depends on the modules listed in `libcxx/utils/ci/lnt/requirements.txt`.')
+ parser.add_argument('--libcxx-installation', type=pathlib.Path, required=True,
+ help='Directory where libc++ is installed.')
parser.add_argument('--benchmark-commit', type=str, required=True,
help='The SHA representing the version of the library to benchmark.')
parser.add_argument('--test-suite-commit', type=str, required=True,
@@ -125,11 +127,7 @@ def main(argv):
"running only a subset of the benchmarks.")
parser.add_argument('--spec-dir', type=pathlib.Path, required=False,
help='Optional path to a SPEC installation to use for benchmarking.')
- parser.add_argument('--cmake-cache', type=pathlib.Path, required=False, default=pathlib.Path(__file__).resolve().parent / 'cmake' / 'generic.cmake',
- help='Optional path to a CMake cache to use when building the library. Only used when building '
- 'the library, not when running the test suite. Defaults to '
- 'libcxx/utils/ci/lnt/cmake/generic.cmake, which builds the library with optimizations enabled.')
- parser.add_argument('--build-dir', type=pathlib.Path, required=False,
+ parser.add_argument('--artifacts-dir', type=pathlib.Path, required=False,
help='Optional directory in which to keep build artifacts. By default, a temporary directory '
'is used and the build artifacts are discarded after the run. It is an error to specify '
'a build directory that already exists.')
@@ -175,39 +173,27 @@ def main(argv):
# check dependencies.
if args.output.exists():
sys.exit(f'error: output report {args.output} already exists; not overwriting it')
- if args.build_dir is not None and args.build_dir.exists():
- sys.exit(f'error: build directory {args.build_dir} already exists; not overwriting it')
- if args.cmake_cache is not None and not args.cmake_cache.is_file():
- sys.exit(f'error: CMake cache {args.cmake_cache} does not exist')
+ if not args.libcxx_installation.exists():
+ sys.exit(f'error: libc++ installation directory {args.libcxx_installation} does not exist')
+ if args.artifacts_dir is not None and args.artifacts_dir.exists():
+ sys.exit(f'error: build directory {args.artifacts_dir} already exists; not overwriting it')
if shutil.which('lnt') is None:
sys.exit('error: cannot find `lnt`; install libcxx/utils/ci/lnt/requirements.txt')
with contextlib.ExitStack() as stack:
- # The build artifacts are kept in --build-dir when it is given, and stored in a temporary directory
+ # The build artifacts are kept in --artifacts-dir when it is given, and stored in a temporary directory
# otherwise.
- if args.build_dir is not None:
- artifacts = args.build_dir
+ if args.artifacts_dir is not None:
+ artifacts = args.artifacts_dir
else:
artifacts = pathlib.Path(stack.enter_context(tempfile.TemporaryDirectory()))
if not args.dry_run:
artifacts.mkdir(parents=True, exist_ok=True)
logging.info(f'Storing build artifacts in {artifacts}')
- logging.info(f'Building libc++ at commit {args.benchmark_commit}')
- cmake_args = [f'-DCMAKE_CXX_COMPILER={args.compiler}']
- if args.cmake_cache is not None:
- cmake_args += ['-C', args.cmake_cache.resolve()]
- build_cmd = [args.git_repo / 'libcxx/utils/build-at-commit',
- '--git-repo', args.git_repo,
- '--install-dir', artifacts / 'libcxx-install',
- '--tmp-src-dir', artifacts / 'libcxx-src',
- '--tmp-build-dir', artifacts / 'libcxx-build',
- '--commit', args.benchmark_commit, '--', *cmake_args]
- run(build_cmd, enforce_success=False) # if the build fails, carry on: we'll fail later and submit empty LNT results
-
- logging.info(f'Running benchmarks from {args.test_suite_commit} against libc++ {args.benchmark_commit}')
+ logging.info(f'Running benchmarks from {args.test_suite_commit} against libc++ at {args.libcxx_installation}')
lit_params = ['--param', f'compiler={args.compiler}',
- '--param', f'libcxx_installation={artifacts / "libcxx-install"}',
+ '--param', f'libcxx_installation={args.libcxx_installation}',
'--param', 'optimization=speed',
'--param', 'std=c++26',
'--param', 'enable_werror=False'] # older versions of the library trigger new warnings, don't fail
>From d1ad5ebfe899fb5a5e523703ec5db7a318cc34a0 Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Fri, 18 Sep 2026 09:40:30 -0700
Subject: [PATCH 2/4] Update test-tools
---
libcxx/utils/ci/run-buildbot | 1 +
1 file changed, 1 insertion(+)
diff --git a/libcxx/utils/ci/run-buildbot b/libcxx/utils/ci/run-buildbot
index f2c3d1f67916e..02f9a87d01a07 100755
--- a/libcxx/utils/ci/run-buildbot
+++ b/libcxx/utils/ci/run-buildbot
@@ -337,6 +337,7 @@ EOF
"${MONOREPO_ROOT}/libcxx/utils/ci/lnt/run-benchmarks" \
--dry-run -vv \
--git-repo "${MONOREPO_ROOT}" \
+ --libcxx-installation "${INSTALL_DIR}" \
--benchmark-commit HEAD \
--test-suite-commit HEAD \
--machine test-tools \
>From c837f3e74a75d084442a560e779e44260303bb0b Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Fri, 18 Sep 2026 09:41:48 -0700
Subject: [PATCH 3/4] Reword artifacts directory
---
libcxx/utils/ci/lnt/run-benchmarks | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/libcxx/utils/ci/lnt/run-benchmarks b/libcxx/utils/ci/lnt/run-benchmarks
index 8ac6e8d4dfae6..8ecc653c8fb79 100755
--- a/libcxx/utils/ci/lnt/run-benchmarks
+++ b/libcxx/utils/ci/lnt/run-benchmarks
@@ -128,9 +128,9 @@ def main(argv):
parser.add_argument('--spec-dir', type=pathlib.Path, required=False,
help='Optional path to a SPEC installation to use for benchmarking.')
parser.add_argument('--artifacts-dir', type=pathlib.Path, required=False,
- help='Optional directory in which to keep build artifacts. By default, a temporary directory '
- 'is used and the build artifacts are discarded after the run. It is an error to specify '
- 'a build directory that already exists.')
+ help='Optional directory in which to keep the artifacts produced by the run. By default, a '
+ 'temporary directory is used and the artifacts are discarded after the run. It is an '
+ 'error to specify an artifacts directory that already exists.')
parser.add_argument('--git-repo', type=directory_path, default=os.getcwd(),
help='Optional path to the Git repository to use. By default, the current working directory is used.')
parser.add_argument('--dry-run', action='store_true',
@@ -176,20 +176,19 @@ def main(argv):
if not args.libcxx_installation.exists():
sys.exit(f'error: libc++ installation directory {args.libcxx_installation} does not exist')
if args.artifacts_dir is not None and args.artifacts_dir.exists():
- sys.exit(f'error: build directory {args.artifacts_dir} already exists; not overwriting it')
+ sys.exit(f'error: artifacts directory {args.artifacts_dir} already exists; not overwriting it')
if shutil.which('lnt') is None:
sys.exit('error: cannot find `lnt`; install libcxx/utils/ci/lnt/requirements.txt')
with contextlib.ExitStack() as stack:
- # The build artifacts are kept in --artifacts-dir when it is given, and stored in a temporary directory
- # otherwise.
+ # The artifacts are kept in --artifacts-dir when it is given, and stored in a temporary directory otherwise.
if args.artifacts_dir is not None:
artifacts = args.artifacts_dir
else:
artifacts = pathlib.Path(stack.enter_context(tempfile.TemporaryDirectory()))
if not args.dry_run:
artifacts.mkdir(parents=True, exist_ok=True)
- logging.info(f'Storing build artifacts in {artifacts}')
+ logging.info(f'Storing artifacts in {artifacts}')
logging.info(f'Running benchmarks from {args.test_suite_commit} against libc++ at {args.libcxx_installation}')
lit_params = ['--param', f'compiler={args.compiler}',
>From 3de4a249ac6f22ede07de45a7aded83920286c9b Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Fri, 18 Sep 2026 09:50:02 -0700
Subject: [PATCH 4/4] Restore behavior when the libc++ build fails
---
.github/workflows/libcxx-benchmark-commit.yml | 17 +++++++++++------
.../workflows/libcxx/build-at-commit/action.yml | 3 +++
2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/libcxx-benchmark-commit.yml b/.github/workflows/libcxx-benchmark-commit.yml
index 9d10f31445e5e..f1f8965760fdb 100644
--- a/.github/workflows/libcxx-benchmark-commit.yml
+++ b/.github/workflows/libcxx-benchmark-commit.yml
@@ -143,6 +143,12 @@ jobs:
"${COMPILER}" --version
python3 --version
+ - name: Setup virtual environment
+ run: |
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install -r libcxx/utils/ci/lnt/requirements.txt
+
# Build the library.
#
# A build failure is tolerated on purpose, since the library doesn't build at every historical commit.
@@ -156,12 +162,6 @@ jobs:
compiler: ${{ matrix.cxx }}
cmake-cache: ${{ matrix.cmake-cache }}
- - name: Setup virtual environment
- run: |
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -r libcxx/utils/ci/lnt/requirements.txt
-
- name: Run the benchmarks
env:
BENCHMARK_SUITE_VERSION: ${{ matrix.benchmark-suite-version }}
@@ -175,6 +175,11 @@ jobs:
filter_arg=(--filter "${FILTER}")
fi
+ # If the library build failed, still create an install directory so we
+ # run the benchmarks. The benchmarks will fail, and we'll submit an empty
+ # LNT report.
+ mkdir -p "${INSTALL_DIR}"
+
libcxx/utils/ci/lnt/run-benchmarks \
--test-suite-commit "${BENCHMARK_SUITE_VERSION}" \
--machine "${LNT_MACHINE}" \
diff --git a/.github/workflows/libcxx/build-at-commit/action.yml b/.github/workflows/libcxx/build-at-commit/action.yml
index 33f9c35df16c3..0528a8e37c5fe 100644
--- a/.github/workflows/libcxx/build-at-commit/action.yml
+++ b/.github/workflows/libcxx/build-at-commit/action.yml
@@ -36,6 +36,9 @@ runs:
COMPILER: ${{ inputs.compiler }}
INSTALL_DIR: ${{ inputs.install-dir }}
run: |
+ # Clean up any stale install directory
+ rm -rf "${INSTALL_DIR}"
+
# Let build-at-commit use (and clean up) its own temporary source and build directories:
# only the installation is of interest to the caller.
libcxx/utils/build-at-commit \
More information about the libcxx-commits
mailing list