Skip to content

Commit 8f4aa9c

Browse files
committed
feat(cpp): a stack, and a fixture that compiles in seconds instead of an hour
Discovery has suggested "cpp" for a long time and the orchestrator had no branch for it, so the value selected a stack nobody routed: the job built nothing and reported success. "core" had the same hole. Both are wired now, and the selector fails loudly on an unroutable stack rather than letting the next one added repeat it. The stack itself follows the wails3 shape — the project's own build description is the unit, not a command this action reconstructs. There the Taskfile owns it; here the conanfile does. So it runs `conan install` then `conan build`, which calls the project's build() method, and CI takes the path a developer does. Where the binary lands is searched for rather than predicted. Visual Studio is multi-config and writes build/release/Release/name.exe; Makefiles and Ninja are single-config and write build/release/name. And a conanfile's layout() may put the build folder anywhere, so guessing is the wrong instinct. tdd/cpp-root was four lines of CMake pointing at a main.cpp that did not exist — enough for discovery to answer "cpp", not enough to configure. It is now a real Conan 2 project mirroring the production one: a generated toolchain, CMakeDeps, an msvc-aware layout, a CTest target. What it does not mirror is the dependency list. Lethean's blockchain pulls boost, openssl and oatpp and takes about an hour; a fixture doing the same would test the runner's patience, not this action. zlib is compiled with prebuilt ConanCenter binaries and nlohmann_json is header-only, so linking and includes are both exercised. Locally: 5.5s to resolve, 2.0s to build, and the binary prints {"fixture":"cpp-fixture","json":"3.11","zlib":"1.3.1"} — both dependencies genuinely reached. CI builds it on ubuntu, macos and windows and asserts that output, so a stack that compiled but failed to link would still fail the job. Two things the new job would have tripped over: setup/conan installed with `pip install --user` and printed "Add ~/.local/bin to PATH if needed", leaving conan unfindable by the next step. It now installs through uv, which removes the question rather than answering it: one shim location on every platform, an isolated environment that cannot collide with the runner's system Python, and caching via setup-uv. setup-conan-tests passed whether or not conan was on PATH — "ok for placeholder". Conan is not a placeholder any more, so it asserts. Also corrects the Go stack docs, written from the README rather than the code: build/core is a library quality pipeline, not a binary builder. It emits no artifact, and a page promising one was wrong. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent 9eab304 commit 8f4aa9c

14 files changed

Lines changed: 581 additions & 34 deletions

File tree

.github/workflows/ci.yml

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,14 @@ jobs:
174174
- uses: actions/checkout@v4
175175
- name: Setup Conan
176176
uses: ./actions/setup/conan
177-
- name: Assert conan version present
177+
- name: Assert conan is on PATH
178178
shell: bash
179179
run: |
180-
if command -v conan >/dev/null 2>&1; then conan --version; else echo "Conan not found in PATH (ok for placeholder)"; fi
180+
set -euo pipefail
181+
# This used to pass either way — "Conan not found in PATH (ok for
182+
# placeholder)". Conan is not a placeholder any more; the cpp stack
183+
# calls it, so not finding it is a failure.
184+
conan --version
181185
182186

183187
# setup-deno-tests:
@@ -545,6 +549,43 @@ jobs:
545549
# package, so this exercises the real cgo and webview toolchain — on Linux
546550
# that is GTK4 and webkitgtk-6.0, which is precisely where the stack is
547551
# easiest to get wrong.
552+
# The application this stack exists for — Lethean's blockchain — pulls boost,
553+
# openssl and oatpp and takes about an hour to compile. Nothing about that
554+
# duration tests the action, so the fixture mirrors the shape (Conan 2 with a
555+
# generated toolchain, CMakeDeps, an msvc-aware layout, CTest) with
556+
# dependencies that resolve in seconds.
557+
build-cpp:
558+
name: Build C++ project (actual compilation) on ${{ matrix.os }}
559+
needs: [discovery-tests, discovery-fixture-tests]
560+
strategy:
561+
fail-fast: false
562+
matrix:
563+
os: [ubuntu-latest, macos-latest, windows-latest]
564+
runs-on: ${{ matrix.os }}
565+
steps:
566+
- uses: actions/checkout@v4
567+
- name: Build with the cpp stack
568+
id: cpp
569+
uses: ./actions/build/cpp
570+
with:
571+
build-name: cpp-fixture
572+
app-working-directory: tdd/cpp-root
573+
build: true
574+
package: false
575+
sign: false
576+
- name: Verify the binary runs
577+
shell: bash
578+
working-directory: tdd/cpp-root
579+
run: |
580+
set -euo pipefail
581+
out="$(./${{ steps.cpp.outputs.BINARY_PATH }})"
582+
echo "[DEBUG_LOG] fixture said: $out"
583+
# It links zlib and includes nlohmann_json, so seeing both in the
584+
# output proves the Conan half resolved rather than just that a
585+
# compiler ran.
586+
echo "$out" | grep -q '"zlib"'
587+
echo "$out" | grep -q '"json"'
588+
548589
build-wails3:
549590
name: Build Wails3 project (actual compilation) on ${{ matrix.os }}
550591
needs: [setup-go-tests, setup-npm-tests]

actions/action.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ runs:
6868
else
6969
echo "[DEBUG_LOG] (orchestrator) AUTO_STACK disabled; defaulting to wails2"
7070
fi
71+
# A stack nobody routes builds nothing and reports success, which is
72+
# worse than failing. Every value that reaches here must have a wrapper
73+
# below; this catches the gap at the point it is introduced.
74+
case "$sel_stack" in
75+
wails2|wails3|cpp|core) ;;
76+
*)
77+
echo "::error::no wrapper for stack '$sel_stack' — nothing would be built"
78+
exit 1
79+
;;
80+
esac
7181
echo "SELECTED_STACK=$sel_stack" >> "$GITHUB_OUTPUT"
7282
7383
- name: Call Wails v2 wrapper
@@ -93,6 +103,27 @@ runs:
93103
package: ${{ inputs.package }}
94104
build-name: ${{ inputs.build-name }}
95105
app-working-directory: ${{ inputs.app-working-directory }}
106+
107+
# cpp takes no build-platform for the same reason v3 does not: the
108+
# conanfile's profile and layout decide the target, and the runner decides
109+
# which one it can produce.
110+
- name: Call C++ wrapper
111+
if: steps.sel.outputs.SELECTED_STACK == 'cpp'
112+
uses: dAppCore/build/actions/build/cpp@v4
113+
with:
114+
build: ${{ inputs.build }}
115+
sign: ${{ inputs.sign }}
116+
package: ${{ inputs.package }}
117+
build-name: ${{ inputs.build-name }}
118+
app-working-directory: ${{ inputs.app-working-directory }}
119+
120+
# core is a library pipeline, not a builder — vet, test, lint, vulncheck.
121+
# It produces no artifact, so `build-name` and `package` do not reach it and
122+
# a `core` build uploads nothing. That is the stack behaving correctly, not
123+
# a packaging failure.
124+
- name: Call Core Go wrapper
125+
if: steps.sel.outputs.SELECTED_STACK == 'core'
126+
uses: dAppCore/build/actions/build/core@v4
96127
outputs:
97128
SELECTED_STACK:
98129
description: "Stack selected by the orchestrator"

actions/build/cpp/action.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: "C++ (full pipeline)"
2+
description: >
3+
Discovery, Conan, CMake build, tests and packaging for a C/C++ project.
4+
5+
The unit of description here is the conanfile, in the same way the Taskfile
6+
is for Wails v3: it names the dependencies, generates the toolchain, and owns
7+
the build. This pipeline runs it rather than reconstructing a cmake command,
8+
so a project builds the same way in CI as on a desk.
9+
10+
inputs:
11+
build:
12+
description: "Whether to run the build"
13+
required: false
14+
default: "true"
15+
sign:
16+
description: "Sign the build (macOS/Windows)"
17+
required: false
18+
default: "false"
19+
package:
20+
description: "Upload artifacts and release on tag"
21+
required: false
22+
default: "true"
23+
build-name:
24+
description: "The name of the binary"
25+
required: true
26+
build-type:
27+
description: "CMake build type"
28+
required: false
29+
default: "Release"
30+
conan-version:
31+
description: "Conan version to install"
32+
required: false
33+
default: "latest"
34+
conan-profile:
35+
description: "Conan profile; empty means auto-detect"
36+
required: false
37+
default: ""
38+
test:
39+
description: "Run ctest after the build"
40+
required: false
41+
default: "true"
42+
app-working-directory:
43+
description: "Root of the project being built"
44+
required: false
45+
default: "."
46+
47+
outputs:
48+
SELECTED_STACK:
49+
description: "Always cpp; lets the orchestrator report what it chose"
50+
value: "cpp"
51+
BINARY_PATH:
52+
description: "Path to the built executable, relative to the working directory"
53+
value: ${{ steps.build.outputs.BINARY_PATH }}
54+
55+
runs:
56+
using: "composite"
57+
steps:
58+
- name: Discovery
59+
id: discovery
60+
uses: dAppCore/build/actions/discovery@v4
61+
with:
62+
working-directory: ${{ inputs.app-working-directory }}
63+
64+
- name: Setup Conan
65+
uses: dAppCore/build/actions/setup/conan@v4
66+
with:
67+
version: ${{ inputs.conan-version }}
68+
69+
- name: Build
70+
id: build
71+
uses: dAppCore/build/actions/build/cpp/build@v4
72+
with:
73+
build: ${{ inputs.build }}
74+
app-working-directory: ${{ inputs.app-working-directory }}
75+
build-name: ${{ inputs.build-name }}
76+
build-type: ${{ inputs.build-type }}
77+
conan-profile: ${{ inputs.conan-profile }}
78+
test: ${{ inputs.test }}
79+
80+
- name: Sign artifacts (OS-conditional)
81+
uses: dAppCore/build/actions/sign@v4
82+
with:
83+
sign: ${{ inputs.sign }}
84+
app-working-directory: ${{ inputs.app-working-directory }}
85+
build-name: ${{ inputs.build-name }}
86+
87+
- name: Package & release
88+
uses: dAppCore/build/actions/package@v4
89+
with:
90+
package: ${{ inputs.package }}
91+
build-name: ${{ inputs.build-name }}
92+
os: ${{ steps.discovery.outputs.OS }}
93+
arch: ${{ steps.discovery.outputs.ARCH }}
94+
tag: ${{ steps.discovery.outputs.TAG }}
95+
is-tag: ${{ steps.discovery.outputs.IS_TAG }}
96+
short-sha: ${{ steps.discovery.outputs.SHORT_SHA }}

actions/build/cpp/build/action.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: "Build C++ project (Conan + CMake)"
2+
description: >
3+
Resolves dependencies with Conan, builds through CMake, and reports where the
4+
binary landed.
5+
6+
The project's conanfile owns the build. `conan build` runs its build() method,
7+
which configures and builds CMake with the generated toolchain — so CI takes
8+
the same path a developer does locally, and a project that builds on a desk
9+
is not built a second, different way here.
10+
11+
inputs:
12+
build:
13+
description: "Whether to run the build"
14+
required: false
15+
default: "true"
16+
app-working-directory:
17+
description: "Root of the project — where conanfile.py lives"
18+
required: false
19+
default: "."
20+
build-name:
21+
description: "Binary name, used to locate the output"
22+
required: true
23+
build-type:
24+
description: "CMake build type"
25+
required: false
26+
default: "Release"
27+
conan-profile:
28+
description: >
29+
Conan profile to build with. Empty means the auto-detected default, which
30+
is what a fresh runner has.
31+
required: false
32+
default: ""
33+
test:
34+
description: "Run ctest after the build"
35+
required: false
36+
default: "true"
37+
38+
outputs:
39+
BINARY_PATH:
40+
description: "Path to the built executable, relative to the working directory"
41+
value: ${{ steps.locate.outputs.BINARY_PATH }}
42+
43+
runs:
44+
using: "composite"
45+
steps:
46+
- name: Detect a Conan profile
47+
if: inputs.build == 'true'
48+
shell: bash
49+
working-directory: ${{ inputs.app-working-directory }}
50+
run: |
51+
set -euo pipefail
52+
# A fresh runner has no profile at all, and Conan errors rather than
53+
# guessing. --exist-ok makes this safe to run when one is already there.
54+
conan profile detect --exist-ok
55+
echo "[DEBUG_LOG] (cpp) conan $(conan --version | awk '{print $3}')"
56+
57+
- name: Resolve dependencies
58+
if: inputs.build == 'true'
59+
shell: bash
60+
working-directory: ${{ inputs.app-working-directory }}
61+
env:
62+
BUILD_TYPE: ${{ inputs.build-type }}
63+
PROFILE: ${{ inputs.conan-profile }}
64+
run: |
65+
set -euo pipefail
66+
args=(install . --build=missing -s "build_type=$BUILD_TYPE")
67+
if [ -n "$PROFILE" ]; then args+=(-pr "$PROFILE"); fi
68+
echo "[DEBUG_LOG] (cpp) conan ${args[*]}"
69+
conan "${args[@]}"
70+
71+
- name: Build
72+
if: inputs.build == 'true'
73+
shell: bash
74+
working-directory: ${{ inputs.app-working-directory }}
75+
env:
76+
BUILD_TYPE: ${{ inputs.build-type }}
77+
PROFILE: ${{ inputs.conan-profile }}
78+
run: |
79+
set -euo pipefail
80+
args=(build . -s "build_type=$BUILD_TYPE")
81+
if [ -n "$PROFILE" ]; then args+=(-pr "$PROFILE"); fi
82+
conan "${args[@]}"
83+
84+
- name: Locate the binary
85+
id: locate
86+
if: inputs.build == 'true'
87+
shell: bash
88+
working-directory: ${{ inputs.app-working-directory }}
89+
env:
90+
NAME: ${{ inputs.build-name }}
91+
run: |
92+
set -euo pipefail
93+
# Where the binary lands is decided by the generator, not by us. Visual
94+
# Studio is multi-config and writes build/release/Release/name.exe;
95+
# Makefiles and Ninja are single-config and write build/release/name.
96+
# Searching beats predicting, because the conanfile's layout() is free
97+
# to put the build folder wherever it likes.
98+
exe="$NAME"
99+
if [ "${RUNNER_OS}" = "Windows" ]; then exe="$NAME.exe"; fi
100+
101+
found="$(find build -type f -name "$exe" 2>/dev/null | head -1 || true)"
102+
if [ -z "$found" ]; then
103+
echo "::error::built no file named '$exe' under build/"
104+
find build -maxdepth 3 -type f 2>/dev/null | head -40 || true
105+
exit 1
106+
fi
107+
echo "[DEBUG_LOG] (cpp) binary=$found"
108+
echo "BINARY_PATH=$found" >> "$GITHUB_OUTPUT"
109+
110+
- name: Normalise the executable bit
111+
if: inputs.build == 'true' && runner.os != 'Windows'
112+
shell: bash
113+
working-directory: ${{ inputs.app-working-directory }}
114+
run: chmod +x "${{ steps.locate.outputs.BINARY_PATH }}"
115+
116+
- name: Test
117+
if: inputs.build == 'true' && inputs.test == 'true'
118+
shell: bash
119+
working-directory: ${{ inputs.app-working-directory }}
120+
env:
121+
BUILD_TYPE: ${{ inputs.build-type }}
122+
run: |
123+
set -euo pipefail
124+
# ctest lives beside the CMake cache, which is the binary's directory
125+
# on single-config generators and its parent on multi-config ones.
126+
dir="$(dirname "${{ steps.locate.outputs.BINARY_PATH }}")"
127+
if [ ! -f "$dir/CTestTestfile.cmake" ] && [ -f "$(dirname "$dir")/CTestTestfile.cmake" ]; then
128+
dir="$(dirname "$dir")"
129+
fi
130+
if [ ! -f "$dir/CTestTestfile.cmake" ]; then
131+
echo "[DEBUG_LOG] (cpp) no CTest configuration — skipping tests"
132+
exit 0
133+
fi
134+
cd "$dir" && ctest --output-on-failure -C "$BUILD_TYPE"

0 commit comments

Comments
 (0)