Skip to content

Commit 5111421

Browse files
committed
Add CMake build workflow and documentation
## Motivation Swift projects that support CMake as a build system need automated validation to ensure their CMakeLists files stay in sync with the source tree. Without this, CMakeLists files can become stale as source files are added or removed, leading to build failures for users relying on the CMake build path. A reusable workflow helps enforce this validation consistently across Swiftlang projects. ## Modifications This change introduces a new reusable CMake build workflow that validates and builds Swift projects using CMake. The workflow includes two shell scripts: `cmake-build.sh` which builds projects using CMake and Ninja, and `cmake-update-cmake-lists.sh` which verifies that CMakeLists files accurately reflect the source tree based on a JSON configuration. The workflow runs in a Docker container and accepts inputs for the CMakeLists update configuration, the target directory to build, and the container image to use. ## Result Projects can now use the cmake_build workflow to automatically verify their CMake build system on every pull request. The workflow ensures CMakeLists files stay up-to-date and that the project builds successfully with CMake, preventing CMake build regressions from being merged.
1 parent 7650d81 commit 5111421

8 files changed

Lines changed: 301 additions & 0 deletions

File tree

.github/workflows/cmake_build.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: CMake build
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
update_cmake_lists_config:
10+
type: string
11+
description: "The configuration used when updating the CMake lists."
12+
required: true
13+
cmake_build_target_directory:
14+
type: string
15+
description: "The directory to pass to `cmake build`."
16+
default: "."
17+
image:
18+
type: string
19+
description: "The docker image to run the checks in."
20+
default: "swift:6.2-noble"
21+
22+
jobs:
23+
cmake-checks:
24+
name: CMake checks
25+
runs-on: ubuntu-latest
26+
container:
27+
image: ${{ inputs.image }}
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
with:
32+
persist-credentials: false
33+
submodules: true
34+
- name: Checkout swiftlang/github-workflows repository
35+
if: ${{ github.repository != 'swiftlang/github-workflows' }}
36+
uses: actions/checkout@v4
37+
with:
38+
repository: swiftlang/github-workflows
39+
path: github-workflows
40+
- name: Determine script-root path
41+
id: script_path
42+
run: |
43+
if [ "${{ github.repository }}" = "swiftlang/github-workflows" ]; then
44+
echo "root=$GITHUB_WORKSPACE" >> $GITHUB_OUTPUT
45+
else
46+
echo "root=$GITHUB_WORKSPACE/github-workflows" >> $GITHUB_OUTPUT
47+
fi
48+
- name: Check CMakeLists files
49+
run: |
50+
which curl jq || apt -q update
51+
which curl || apt -yq install curl
52+
which jq || apt -yq install jq
53+
${{ steps.script_path.outputs.root }}/.github/workflows/scripts/cmake-update-cmake-lists.sh | CONFIG_JSON='${{ inputs.update_cmake_lists_config }}' FAIL_ON_CHANGES=true bash
54+
- name: CMake build
55+
run: |
56+
which curl cmake ninja || apt -q update
57+
which curl || apt -yq install curl
58+
which cmake || apt -yq install cmake
59+
which ninja || apt -yq install ninja-build
60+
${{ steps.script_path.outputs.root }}/.github/workflows/scripts/cmake-build.sh | TARGET_DIRECTORY="${{ inputs.cmake_build_target_directory }}" bash

.github/workflows/pull_request.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,10 @@ jobs:
9090
with:
9191
api_breakage_check_enabled: false
9292
license_header_check_project_name: "Swift.org"
93+
94+
cmake_build:
95+
name: CMake build
96+
uses: ./.github/workflows/cmake_build.yml
97+
with:
98+
update_cmake_lists_config: '{"targets":[{"name":"Target1","type":"source","exceptions":[]},{"name":"Target2","type":"source","exceptions":[]}]}'
99+
cmake_build_target_directory: "tests/TestPackage"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the Swift.org open source project
5+
##
6+
## Copyright (c) 2026 Apple Inc. and the Swift project authors
7+
## Licensed under Apache License v2.0 with Runtime Library Exception
8+
##
9+
## See https://swift.org/LICENSE.txt for license information
10+
## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11+
##
12+
##===----------------------------------------------------------------------===##
13+
14+
set -uo pipefail
15+
16+
log() { printf -- "** %s\n" "$*" >&2; }
17+
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
18+
fatal() { error "$@"; exit 1; }
19+
20+
target_dir="${TARGET_DIRECTORY:=""}"
21+
22+
if [ -z "$target_dir" ]; then
23+
fatal "Target directory must be specified."
24+
fi
25+
26+
CURL_BIN="${CURL_BIN:-$(which curl)}" || fatal "CURL_BIN unset and no curl on PATH"
27+
TAR_BIN="${TAR_BIN:-$(which tar)}" || fatal "TAR_BIN unset and no tar on PATH"
28+
CMAKE_BIN="${CMAKE_BIN:-$(which cmake)}" || fatal "CMAKE_BIN unset and no cmake on PATH"
29+
NINJA_BIN="${NINJA_BIN:-$(which ninja)}" || fatal "NINJA_BIN unset and no ninja on PATH"
30+
ASSEMBLY_COMPILER_BIN="${ASSEMBLY_COMPILER_BIN:-$(which clang)}" || fatal "ASSEMBLY_COMPILER_BIN unset and no clang on PATH"
31+
32+
log "Building Ninja build files for target"
33+
build_dir="${target_dir}/build"
34+
mkdir -p "$build_dir"
35+
cd "${build_dir}" || fatal "Could not 'cd' to ${build_dir}"
36+
ASM="$ASSEMBLY_COMPILER_BIN" "$CMAKE_BIN" build -G Ninja -S ..
37+
38+
log "Building target"
39+
"$NINJA_BIN"
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/bin/bash
2+
##===----------------------------------------------------------------------===##
3+
##
4+
## This source file is part of the Swift.org open source project
5+
##
6+
## Copyright (c) 2026 Apple Inc. and the Swift project authors
7+
## Licensed under Apache License v2.0 with Runtime Library Exception
8+
##
9+
## See https://swift.org/LICENSE.txt for license information
10+
## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
11+
##
12+
##===----------------------------------------------------------------------===##
13+
14+
set -eu
15+
16+
log() { printf -- "** %s\n" "$*" >&2; }
17+
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
18+
fatal() { error "$@"; exit 1; }
19+
20+
config="${CONFIG_JSON:=""}"
21+
fail_on_changes="${FAIL_ON_CHANGES:="false"}"
22+
23+
if [ -z "$config" ]; then
24+
fatal "Configuration must be provided."
25+
fi
26+
27+
here=$(pwd)
28+
29+
case "$(uname -s)" in
30+
Darwin)
31+
find=gfind # brew install findutils
32+
;;
33+
*)
34+
find='find'
35+
;;
36+
esac
37+
38+
function update_cmakelists_source() {
39+
src_root="$here/Sources/$1"
40+
41+
src_exts=("*.c" "*.swift" "*.cc")
42+
num_exts=${#src_exts[@]}
43+
log "Finding source files (" "${src_exts[@]}" ") and platform independent assembly files under $src_root"
44+
45+
# Build file extensions argument for `find`
46+
declare -a exts_arg
47+
exts_arg+=(-name "${src_exts[0]}")
48+
for (( i=1; i<num_exts; i++ ));
49+
do
50+
exts_arg+=(-o -name "${src_exts[$i]}")
51+
done
52+
53+
# Build an array with the rest of the arguments
54+
shift
55+
exceptions=("$@")
56+
# Add path exceptions for `find`
57+
if (( ${#exceptions[@]} )); then
58+
log "Excluding source paths (" "${exceptions[@]}" ") under $src_root"
59+
num_exceptions=${#exceptions[@]}
60+
for (( i=0; i<num_exceptions; i++ ));
61+
do
62+
exts_arg+=(! -path "${exceptions[$i]}")
63+
done
64+
fi
65+
66+
# Wrap quotes around each filename since it might contain spaces
67+
srcs=$($find -L "${src_root}" -type f \( "${exts_arg[@]}" \) -printf ' "%P"\n' | LC_ALL=POSIX sort)
68+
asm_srcs=$($find -L "${src_root}" -type f \( \( -name "*.S" -a ! -name "*x86_64*" -a ! -name "*arm*" -a ! -name "*apple*" -a ! -name "*linux*" \) \) -printf ' "$<$<NOT:$<PLATFORM_ID:Windows>>:%P>"\n' | LC_ALL=POSIX sort)
69+
70+
srcs="$srcs"$'\n'"$asm_srcs"
71+
log "$srcs"
72+
73+
# Update list of source files in CMakeLists.txt
74+
# The first part in `BEGIN` (i.e., `undef $/;`) is for working with multi-line;
75+
# the second is so that we can pass in a variable to replace with.
76+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/add_library\(([^\n]+)\n([^\)]+)/add_library\($1\n$replace/' "$srcs" "$src_root/CMakeLists.txt"
77+
log "Updated $src_root/CMakeLists.txt"
78+
}
79+
80+
function update_cmakelists_assembly() {
81+
src_root="$here/Sources/$1"
82+
log "Finding assembly files (.S) under $src_root"
83+
84+
mac_x86_64_asms=$($find "${src_root}" -type f \( -name "*x86_64*" -or -name "*avx2*" \) -name "*apple*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
85+
linux_x86_64_asms=$($find "${src_root}" -type f \( -name "*x86_64*" -or -name "*avx2*" \) -name "*linux*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
86+
win_x86_64_asms=$($find "${src_root}" -type f \( -name "*x86_64*" -or -name "*avx2*" \) -name "*win*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
87+
mac_aarch64_asms=$($find "${src_root}" -type f -name "*armv8*" -name "*apple*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
88+
linux_aarch64_asms=$($find "${src_root}" -type f -name "*armv8*" -name "*linux*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
89+
win_aarch64_asms=$($find "${src_root}" -type f -name "*armv8*" -name "*win*" -name "*.S" -printf ' %P\n' | LC_ALL=POSIX sort)
90+
log "$mac_x86_64_asms"
91+
log "$linux_x86_64_asms"
92+
log "$win_x86_64_asms"
93+
log "$mac_aarch64_asms"
94+
log "$linux_aarch64_asms"
95+
log "$win_aarch64_asms"
96+
97+
# Update list of assembly files in CMakeLists.txt
98+
# The first part in `BEGIN` (i.e., `undef $/;`) is for working with multi-line;
99+
# the second is so that we can pass in a variable to replace with.
100+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Darwin([^\)]+)x86_64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Darwin$1x86_64"\)\n target_sources\($2\n$replace/' "$mac_x86_64_asms" "$src_root/CMakeLists.txt"
101+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Linux([^\)]+)x86_64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Linux$1x86_64"\)\n target_sources\($2\n$replace/' "$linux_x86_64_asms" "$src_root/CMakeLists.txt"
102+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Windows([^\)]+)x86_64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Windows$1x86_64"\)\n target_sources\($2\n$replace/' "$win_x86_64_asms" "$src_root/CMakeLists.txt"
103+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Darwin([^\)]+)aarch64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Darwin$1aarch64"\)\n target_sources\($2\n$replace/' "$mac_aarch64_asms" "$src_root/CMakeLists.txt"
104+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Linux([^\)]+)aarch64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Linux$1aarch64"\)\n target_sources\($2\n$replace/' "$linux_aarch64_asms" "$src_root/CMakeLists.txt"
105+
perl -pi -e 'BEGIN { undef $/; $replace = shift } s/Windows([^\)]+)aarch64"\)\n target_sources\(([^\n]+)\n([^\)]+)/Windows$1aarch64"\)\n target_sources\($2\n$replace/' "$win_aarch64_asms" "$src_root/CMakeLists.txt"
106+
log "Updated $src_root/CMakeLists.txt"
107+
}
108+
109+
echo "$config" | jq -c '.targets[]' | while read -r target; do
110+
name="$(echo "$target" | jq -r .name)"
111+
type="$(echo "$target" | jq -r .type)"
112+
exceptions=("$(echo "$target" | jq -r .exceptions | jq -r @sh)")
113+
log "Updating cmake list for ${name}"
114+
115+
case "$type" in
116+
source)
117+
update_cmakelists_source "$name" "${exceptions[@]}"
118+
;;
119+
assembly)
120+
update_cmakelists_assembly "$name"
121+
;;
122+
*)
123+
fatal "Unknown target type: $type"
124+
;;
125+
esac
126+
done
127+
128+
if [[ "${fail_on_changes}" == true ]]; then
129+
if [ -n "$(git status --untracked-files=no --porcelain)" ]; then
130+
fatal "Changes in the cmake files detected. Please update. -- $(git diff)"
131+
else
132+
log "✅ CMake files are up-to-date."
133+
fi
134+
fi

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,48 @@ Linked PR: swiftlang/swift-syntax#2859
9494

9595
Enabling cross-PR testing will add about 10s to PR testing time.
9696

97+
### CMake build
98+
99+
For Swift projects that support CMake as their build system, the CMake build
100+
workflow provides automated checks to ensure CMakeLists files are up-to-date and
101+
that the project builds successfully with CMake.
102+
103+
A recommended workflow looks like this:
104+
105+
```yaml
106+
name: Pull request
107+
108+
on:
109+
pull_request:
110+
types: [opened, reopened, synchronize]
111+
112+
jobs:
113+
cmake-build:
114+
name: CMake build
115+
uses: swiftlang/github-workflows/.github/workflows/cmake_build.yml@0.0.1
116+
with:
117+
update_cmake_lists_config: '{"exclude": ["Tests/**"]}'
118+
```
119+
120+
The workflow accepts the following inputs:
121+
122+
- `update_cmake_lists_config` (required): JSON configuration for updating
123+
CMakeLists files. This is passed to the `cmake-update-cmake-lists.sh` script
124+
to verify that CMakeLists files are in sync with the source tree.
125+
- `cmake_build_target_directory` (optional, default: `"."`): The directory to
126+
pass to `cmake build`.
127+
- `cmake_version` (optional): Specific version of CMake to install. If not
128+
provided, uses the version available in the container image.
129+
- `image` (optional, default: `"swift:6.2-noble"`): The Docker image to run the
130+
checks in.
131+
132+
The workflow performs two main checks:
133+
134+
1. **CMakeLists validation**: Verifies that CMakeLists files are up-to-date with
135+
the source tree using the provided configuration.
136+
2. **CMake build**: Builds the project using CMake and Ninja to ensure the CMake
137+
build system is working correctly.
138+
97139
## Running workflows locally
98140

99141
You can run the Github Actions workflows locally using

tests/TestPackage/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
cmake_minimum_required(VERSION 3.24)
2+
3+
project(TestPackage
4+
LANGUAGES Swift)
5+
6+
add_subdirectory(Sources/Target1)
7+
add_subdirectory(Sources/Target2)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
add_library(Target1
2+
"Target1.swift"
3+
)
4+
target_include_directories(Target1 PUBLIC
5+
${CMAKE_CURRENT_SOURCE_DIR})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
add_library(Target2
2+
"Target2.swift"
3+
)
4+
target_include_directories(Target2 PUBLIC
5+
${CMAKE_CURRENT_SOURCE_DIR})
6+
target_link_libraries(Target2 PUBLIC
7+
Target1)

0 commit comments

Comments
 (0)