Skip to content

full

full #86

Workflow file for this run

name: full
# The whole application, Slint included — **the only CI**, and **where a release is built** since
# 2026-10-03, when the user had every build and test that ran twice taken out: core.yml (its tests
# were a subset of this workflow's windows-msvc job, run again on every push) and release.yml
# (the windows-msvc build over again on the same commit, ~40 minutes, only so the exe could name
# a tag that did not exist yet when this ran). What went with core.yml: the quicker first answer,
# and its check that the engine builds without Slint ever being fetched.
#
# **No schedule**, by the user on 2026-10-05. A weekly run (Mondays, 06:00 UTC) dated from when
# this ran only on demand; once every push that touches the code ran it, the weekly one only
# tested main again — and GitHub, starting it seven hours late, put it in this concurrency group on
# top of 1.3.0's release run and cancelled that.
#
# **A release:** push the bump commit and its annotated tag together —
# `git push --atomic origin main vX.Y.Z` — so the tag is there when this checks out, and every
# binary built here names it. When every build job passes on a commit a `v*` tag points at, the
# windows-msvc job's files are attached to that tag's GitHub Release (the `publish` job below),
# with the tag's message as the notes. A tag pushed after its commit's run: rerun that run
# (`gh run rerun <id>`); its checkout then has the tag. A push to main while a release run is
# building cancels it (the concurrency group below): rerun it the same way.
#
# **Slint is cached** (2026-10-03, the same request): it was 26 of the windows-msvc job's 42
# minutes of building, every run, and it changes only when the pins below do. See "Slint, from
# the cache".
#
# This workflow is ENABLED, by the user on 2026-09-14. It had been `disabled_manually` since
# 2026-09-04, which left a UI change covered by nothing. That was noticed while releasing 0.9.6
# — a release whose every changed file was under src/ui/ — and the answer was to turn this back
# on rather than to keep dispatching it by hand and forgetting to.
#
# **macOS and Linux are built again, by the user on 2026-10-08**, with the repo public: GitHub's
# standard runners cost a public repo nothing, macOS included. They had been left out since
# 2026-09-06, when a private repo paid 10× for every macOS minute. macOS is built twice, on an
# Apple Silicon runner and on an Intel one, because takt4 has to run on both; `linux` is the
# release build, on Ubuntu 22.04 so the binary runs on any glibc from 2.35 up, where
# `linux-tsan` (24.04) is the sanitizer.
#
# **Plus the sanitizers, on every push the path filter below lets through (every push that
# touches the code), by the user on 2026-09-23** (the audit's T4, which
# found that neither had ever run in CI and that `linux-tsan` had never run anywhere):
# AddressSanitizer over the whole tree, the UI included, on Windows; and ThreadSanitizer on
# Linux, because there is no ThreadSanitizer for Windows at all. takt4's threads — the audio
# callback, the model, the tracker, the output thread, the UI, the OSC and MIDI listeners
# and Link's own — are the reason. The cost was put to the user and accepted.
#
# The path filter covers everything that can change the app (the audit found src/core,
# tests/ui, tests/CMakeLists.txt, assets and tools/embed_asset.py all missing from it, so a
# change there was not built when it was pushed; and the audit of 2026-09-25, B4, the Rust
# pin, which is the compiler Slint is built with, and LICENSE and THIRD-PARTY-NOTICES.txt,
# which are embedded in takt4.exe). Documentation alone does not trigger it.
#
# Note the concurrency group below cancels in-progress runs for the same ref: a second push
# to main while this is building kills the first run rather than queueing behind it.
on:
push:
branches: [main]
paths:
- 'src/**'
- 'tests/**'
- 'assets/**'
- 'third_party/**'
- 'cmake/**'
- 'CMakeLists.txt'
- 'CMakePresets.json'
- 'tools/embed_asset.py'
- 'rust-toolchain.toml'
- 'LICENSE'
- 'THIRD-PARTY-NOTICES.txt'
- '.github/workflows/full.yml'
pull_request:
paths:
- 'src/**'
- 'tests/**'
- 'assets/**'
- 'third_party/**'
- 'cmake/**'
- 'CMakeLists.txt'
- 'CMakePresets.json'
- 'tools/embed_asset.py'
- 'rust-toolchain.toml'
- 'LICENSE'
- 'THIRD-PARTY-NOTICES.txt'
- '.github/workflows/full.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
build:
name: ${{ matrix.name || matrix.preset }}
runs-on: ${{ matrix.os }}
# The ASan job builds the whole tree instrumented and runs every test under it.
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
include:
# The hosted Windows images (windows-latest and windows-2025 alike, as of the
# 2026-08-24 image) ship Visual Studio 2026 only. The preset names no
# generator, so CMake picks whatever Visual Studio is installed.
- os: windows-latest
preset: windows-msvc
app: build/windows-msvc/bin/Release/takt4.exe
cli: build/windows-msvc/bin/Release/takt4-cli.exe
# Where Corrosion has cargo build Slint: `<build>/<platform>/<config>/cargo` under
# Visual Studio, `<build>/cargo` under Ninja (Corrosion.cmake, `cargo_target_dir`).
cargo: build/windows-msvc/x64/Release/cargo
# AddressSanitizer over everything, UI included. cmake/sanitizers.cmake copies the
# ASan runtime DLL beside the binaries, so they start without Visual Studio's PATH.
- os: windows-latest
preset: windows-asan
app: build/windows-asan/bin/Release/takt4.exe
cli: build/windows-asan/bin/Release/takt4-cli.exe
cargo: build/windows-asan/x64/Release/cargo
# ThreadSanitizer: Linux is where it exists. TSAN_OPTIONS makes a report fail the
# test that produced it rather than scroll past in a passing run.
- os: ubuntu-24.04
preset: linux-tsan
app: build/linux-tsan/bin/takt4
cli: build/linux-tsan/bin/takt4-cli
cargo: build/linux-tsan/cargo
# macOS on both kinds of Mac, from the same preset. `name` tells the two jobs, and their
# Slint caches, apart: the runner image's name is the same on both.
- os: macos-26
name: macos-arm64
preset: macos
app: build/macos/bin/takt4.app/Contents/MacOS/takt4
cli: build/macos/bin/takt4-cli
cargo: build/macos/cargo
- os: macos-26-intel
name: macos-x64
preset: macos
app: build/macos/bin/takt4.app/Contents/MacOS/takt4
cli: build/macos/bin/takt4-cli
cargo: build/macos/cargo
# The Linux release: the oldest supported Ubuntu, since a binary needs at least the
# glibc it was built against. Its own GCC 11 is older than anything else here builds
# with, so GCC 12, which 22.04 also ships.
- os: ubuntu-22.04
preset: linux
cc: gcc-12
cxx: g++-12
app: build/linux/bin/takt4
cli: build/linux/bin/takt4-cli
cargo: build/linux/cargo
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
# Every commit and tag, so `git describe` — what `--version` prints — names a release's
# tag rather than a hash. The whole history is ~33 MB.
fetch-depth: 0
# **Bounded, and tried again.** On 2026-10-07 the runners' own mirror stopped answering and
# apt, which has no timeout of its own, waited on archive.ubuntu.com for over an hour in two
# runs in a row, until each was cancelled. Each try now has a time limit, over IPv4, three times.
- name: Install Linux packages
if: runner.os == 'Linux'
timeout-minutes: 20
run: |
apt=(-o Acquire::ForceIPv4=true -o Acquire::Retries=3 -o Acquire::http::Timeout=30
-o Acquire::https::Timeout=30)
for try in 1 2 3; do
if sudo timeout 240 apt-get "${apt[@]}" update &&
sudo timeout 300 apt-get "${apt[@]}" install -y --no-install-recommends \
pkg-config libasound2-dev libjack-jackd2-dev libfontconfig-dev libfreetype-dev ${{ matrix.cxx }}; then
exit 0
fi
echo "apt did not finish (try $try of 3)"
sudo dpkg --configure -a || true # an install cut short leaves dpkg half done
sleep 20
done
exit 1
# A job that names its compiler has every later step use it: CMake reads CC and CXX once, at
# configure, but the C++ inside Slint's cargo build is compiled by the cc crate, which reads
# them when cargo runs.
- name: The compiler
if: matrix.cxx
run: |
echo "CC=${{ matrix.cc }}" >> "$GITHUB_ENV"
echo "CXX=${{ matrix.cxx }}" >> "$GITHUB_ENV"
# The macOS presets build with Ninja, which not every macOS image ships.
- name: Ninja
if: runner.os == 'macOS'
run: command -v ninja || brew install ninja
# The hosted images ship CMake and a Rust toolchain that satisfy the requirements in
# README.md. Record what was actually used.
- name: Toolchain versions
run: |
cmake --version
ninja --version || true
rustc --version || true
cargo --version || true
${CXX:-c++} --version || true
# **Slint, from the cache**: its source and everything cargo built from it, so a run builds
# only takt4 itself. Both are needed — cargo judges Slint's own crates stale by their
# sources' file times, and a fresh clone is newer than anything it built — so the source
# lives at one fixed place, handed to CMake (FETCHCONTENT_SOURCE_DIR_SLINT, below) and
# cached with its times, and is never cloned over. **And Slint's CMake build directory**
# (`_deps/slint-build`, ~1 MB): its build script writes the C++ headers there
# (`generated_include/private/slint_internal.h` …), outside cargo's own output, and a cargo
# that finds its output fresh does not run the script again — the first cached run, on
# 2026-10-03, restored everything else and failed on that header.
#
# The key is everything that decides what was built: the build type, the runner image
# (the C++ inside Slint's cargo build — Skia's bindings, ICU, harfbuzz — is compiled with
# the image's compiler), the Rust pin, and cmake/deps.cmake, which holds Slint's tag, its
# commit and its features. No fallback key: a near miss would mix two compilers' objects.
# `v2`: what is cached changed (the build directory above); a v1 cache lacks the headers.
# The image is read in a step: ImageOS and ImageVersion are the runner's environment, which
# the `env` context of an expression does not see.
- name: The runner image
id: image
run: echo "image=${ImageOS:?}-${ImageVersion:?}" >> "$GITHUB_OUTPUT"
- name: Slint, from the cache
id: slint
uses: actions/cache/restore@v6
with:
key: slint-v2-${{ matrix.name || matrix.preset }}-${{ steps.image.outputs.image }}-${{ hashFiles('rust-toolchain.toml', 'cmake/deps.cmake') }}
path: |
build/slint-src
build/${{ matrix.preset }}/_deps/slint-build
${{ matrix.cargo }}
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
# The tag cmake/deps.cmake fetches, cloned as FetchContent would; deps.cmake still checks
# the commit it checked out against the one it records.
- name: Slint source
if: steps.slint.outputs.cache-hit != 'true'
run: |
tag=$(grep -A1 'GIT_REPOSITORY https://github.com/slint-ui/slint.git' cmake/deps.cmake \
| sed -n 's/^ *GIT_TAG \(v[0-9.]*\)$/\1/p')
test -n "$tag"
rm -rf build/slint-src
git clone --depth 1 --branch "$tag" https://github.com/slint-ui/slint.git build/slint-src
# A path CMake reads on both systems: Git Bash's `pwd -W` is D:/a/..., Linux has no -W.
- name: Configure
run: |
src="$(pwd -W 2>/dev/null || pwd)/build/slint-src"
cmake --preset ${{ matrix.preset }} -DFETCHCONTENT_SOURCE_DIR_SLINT="$src"
# **Slint on its own first**, so it reaches the cache below even when takt4's own code then
# fails to compile — which, on a system nothing has built for a while, is most runs: without
# this, every one of those runs built Slint again from cold.
- name: Build Slint
if: steps.slint.outputs.cache-hit != 'true'
run: cmake --build --preset ${{ matrix.preset }} --parallel --target cargo-build_slint_cpp cargo-build_slint-compiler
# Saved as soon as Slint is built, not at the end: a test that fails must not cost the next
# run the build. Two runs missing at once both try; the second is refused, harmlessly.
- name: Slint, into the cache
if: steps.slint.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v6
with:
key: ${{ steps.slint.outputs.cache-primary-key }}
path: |
build/slint-src
build/${{ matrix.preset }}/_deps/slint-build
${{ matrix.cargo }}
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
# Ninja goes on past the first error (-k 0), so a run reports every file GCC or clang
# refuses rather than one per run: each warns about things MSVC does not, and this tree
# builds with warnings as errors.
- name: Build
run: cmake --build --preset ${{ matrix.preset }} --parallel ${{ runner.os != 'Windows' && '-- -k 0' || '' }}
# `-all`: a runner has no show on it to disturb, so it runs the [network] and [hardware]
# tests the default presets leave out on a rig (the audit's T2).
- name: Test
env:
TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1
run: ctest --preset ${{ matrix.preset }}-all
- name: Version banner
run: ${{ matrix.app }} --version
# The bundle macOS reads: an Info.plist it can parse, naming the executable that is there, and
# the two sentences it shows when it asks for the microphone and the local network.
- name: The app bundle
if: runner.os == 'macOS'
run: |
plist=build/macos/bin/takt4.app/Contents/Info.plist
plutil -lint "$plist"
plutil -p "$plist"
test "$(plutil -extract CFBundleExecutable raw "$plist")" = takt4
plutil -extract NSMicrophoneUsageDescription raw "$plist"
plutil -extract NSLocalNetworkUsageDescription raw "$plist"
codesign -dv build/macos/bin/takt4.app 2>&1 || true
- name: Console tool
run: |
${{ matrix.cli }} --version
${{ matrix.cli }} devices
# **A release's file**, on a commit a `v*` tag points at, pushed or dispatched — never a pull
# request. The tag must be the version in CMakeLists.txt, and takt4.exe must say it is exactly
# that tag: not "-dirty", not a commit after it. takt4.exe alone: takt4-cli is the development
# console, built and tested here but not released. Handed to `publish`, which attaches it
# once every job here has passed.
- name: Release files
id: release
if: matrix.preset == 'windows-msvc' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
run: |
tag=$(git tag --points-at HEAD 'v*' | head -1)
if [ -z "$tag" ]; then
echo "No v* tag on $GITHUB_SHA: not a release."
exit 0
fi
version=$(sed -n 's/^ VERSION \([0-9.]*\)$/\1/p' CMakeLists.txt | head -1)
echo "CMakeLists.txt says $version; the tag is $tag"
test "v$version" = "$tag"
bin=build/windows-msvc/bin/Release
"$bin/takt4.exe" --version | tee app.txt
grep -qx " commit: $tag" app.txt
mkdir dist
cp "$bin/takt4.exe" dist/
ls -l dist
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Keep the release files
if: steps.release.outputs.tag != ''
uses: actions/upload-artifact@v7
with:
name: release
path: dist/
retention-days: 3
# Attaches the windows-msvc job's files to the tag's Release — created from the tag's message if
# there is none yet, the files replaced if one was made by hand — once all three jobs passed.
# Nothing to do on a commit no `v*` tag points at.
publish:
needs: build
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# This token can write releases; it is handed to `gh` below and not left in .git.
persist-credentials: false
- name: Which release
id: tag
run: |
tag=$(git tag --points-at HEAD 'v*' | head -1)
echo "tag=$tag" >> "$GITHUB_OUTPUT"
if [ -z "$tag" ]; then echo "No v* tag on $GITHUB_SHA: nothing to publish."; fi
- name: The release files
if: steps.tag.outputs.tag != ''
uses: actions/download-artifact@v8
with:
name: release
path: dist
- name: Attach to the release
if: steps.tag.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
ls -l dist
if gh release view "$TAG" > /dev/null 2>&1; then
gh release upload "$TAG" dist/* --clobber
else
gh release create "$TAG" dist/* --verify-tag --notes-from-tag --title "takt4 ${TAG#v}"
fi