Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 19 additions & 204 deletions .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ name: Build native
# They are committed rather than built during a normal build so that a plain
# `dotnet build src --configuration Release` produces a shippable package on any machine, and
# contributors never need a C++ toolchain. Run this whenever native/ changes.
#
# The work is PlatformHeads' build-native workflow, which this one was moved into so that
# BuildMonitor, built the same way, runs it too: the Linux library built in a manylinux_2_28
# container and checked for the glibc it needs and the libraries it records as needed, the
# universal dylib, and the pull request with the binaries. BuildMonitor had a copy of it, and
# three fixes made here were waiting to be found again there.
# The tag is the one native/CMakeLists.txt fetches and Directory.Packages.props names.
on:
push:
paths:
Expand All @@ -27,210 +34,18 @@ concurrency:
cancel-in-progress: true

jobs:
build:
name: ${{ matrix.rid }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
# No Windows entries. That head renders with WinForms and loads no native library.
- rid: linux-x64
os: ubuntu-24.04
- rid: linux-arm64
os: ubuntu-24.04-arm
# One universal binary covers both macOS RIDs; it is copied into each below.
- rid: osx
os: macos-14
steps:
- name: Checkout
uses: actions/checkout@v7

# In a manylinux_2_28 container rather than on the runner, for its glibc 2.28: the floor
# .NET 10 itself supports, which is RHEL 8. A library records the glibc symbol versions of
# the headers it was compiled against, and the loader refuses it anywhere older - built on
# the runner's own Ubuntu 24.04 it needed GLIBC_2.38, so the viewer could not start on
# Ubuntu 22.04, Debian 12, or RHEL 8 or 9, and every inline snapshot sent to it was lost.
- name: Build
if: runner.os == 'Linux'
run: docker run --rm -v "$PWD:/src" -w /src "quay.io/pypa/manylinux_2_28_$(uname -m)" bash native/build-linux.sh

# The floor the container is there to hold, checked rather than trusted, since nothing else
# would notice until a user on an older distribution did.
- name: Check glibc floor
if: runner.os == 'Linux'
env:
GLIBC_FLOOR: GLIBC_2.28
run: |
highest=$(objdump -T build/libdiffengine_viewer.so | grep -o 'GLIBC_[0-9][0-9.]*' | sort -uV | tail -n 1)
echo "Highest glibc symbol version required: $highest"
if [ "$(printf '%s\n' "$GLIBC_FLOOR" "$highest" | sort -V | tail -n 1)" != "$GLIBC_FLOOR" ]; then
echo "::error::libdiffengine_viewer.so requires $highest, above the $GLIBC_FLOOR floor"
exit 1
fi
# libGL.so.1 is on every machine with a GL driver; libOpenGL.so.0 is not
if objdump -p build/libdiffengine_viewer.so | grep -q 'NEEDED.*libOpenGL'; then
echo "::error::libdiffengine_viewer.so links libOpenGL.so.0 rather than libGL.so.1"
exit 1
fi

# Everything recorded as needed has to be on a machine for the library to load at all, so
# the list is held to what any machine that runs .NET and has a display already has. It grew
# once without anybody choosing it: the container's linker records every library it is
# handed, used or not, and raylib hands on libSM and libICE, which a stock Ubuntu lacks.
# The viewer could not start there, and nothing noticed until somebody ran it on one.
- name: Check needed libraries
if: runner.os == 'Linux'
env:
ALLOWED: >-
libc.so.6 libm.so.6 libdl.so.2 libpthread.so.0 librt.so.1 libstdc++.so.6 libgcc_s.so.1
libX11.so.6 libGL.so.1 ld-linux-x86-64.so.2 ld-linux-aarch64.so.1
run: |
needed=$(objdump -p build/libdiffengine_viewer.so | awk '$1 == "NEEDED" { print $2 }')
echo "Needed:" $needed
failed=0
for library in $needed; do
case " $ALLOWED " in
*" $library "*) ;;
*)
echo "::error::libdiffengine_viewer.so needs $library, which is not in the list of libraries it may need"
failed=1
;;
esac
done
exit $failed

# macOS draws with AppKit and Core Text rather than raylib and ImGui, so it is a Swift
# package rather than a CMake project. Both --arch flags in one invocation produce a
# universal binary, so there is no separate lipo step.
#
# Nothing of the Swift runtime is shipped: it has been part of macOS since 10.14.4, which is
# why this dylib is a fraction of the size of the one it replaced.
- name: Build
if: matrix.rid == 'osx'
run: swift build -c release --arch arm64 --arch x86_64 --package-path native/swift

- name: Collect
shell: bash
run: |
# Laid out as src/{head}/runtimes/{rid}/native, so the propose job below can merge every
# artifact straight into src and the binaries land in the head that loads them.
collect() {
mkdir -p "artifacts/$1/runtimes/$2/native"
cp "$3" "artifacts/$1/runtimes/$2/native/"
}
case "${{ matrix.rid }}" in
linux-*)
# Already stripped, inside the container that owns the build directory
collect DiffEngineViewer.Linux "${{ matrix.rid }}" build/libdiffengine_viewer.so
;;
osx)
# swift build leaves a per architecture dylib in more than one place, so taking the
# first one found gives a single slice binary that loads on half the Macs in the
# world. Every candidate is checked, and if none is already universal they are
# merged, so the only thing that can be collected is a fat binary.
# The dSYM contains a DWARF file of the same name which is also universal, so the
# arch check below would happily accept it and ship debug symbols as the library.
candidates=$(find native/swift/.build -name libdiffengine_viewer.dylib -not -path '*.dSYM/*')
if [ -z "$candidates" ]; then
echo "::error::swift build produced no libdiffengine_viewer.dylib"
exit 1
fi

echo "$candidates" | while read -r candidate; do
echo "$candidate: $(lipo -archs "$candidate" 2>/dev/null)"
done

dylib=""
for candidate in $candidates; do
archs=$(lipo -archs "$candidate" 2>/dev/null || echo "")
if [[ "$archs" == *x86_64* && "$archs" == *arm64* ]]; then
dylib="$candidate"
break
fi
done

if [ -z "$dylib" ]; then
dylib=universal/libdiffengine_viewer.dylib
mkdir -p universal
# shellcheck disable=SC2086
lipo -create $candidates -output "$dylib"
fi

archs=$(lipo -archs "$dylib")
echo "collecting $dylib: $archs"
for arch in x86_64 arm64; do
case " $archs " in
*" $arch "*) ;;
*) echo "::error::$dylib is missing the $arch slice"; exit 1 ;;
esac
done

strip -x "$dylib"
# The dylib is universal, so both macOS RIDs get the same file.
collect DiffEngineViewer.Mac osx-x64 "$dylib"
collect DiffEngineViewer.Mac osx-arm64 "$dylib"
;;
esac
ls -lhR artifacts

- name: Upload
uses: actions/upload-artifact@v7
with:
name: native-${{ matrix.rid }}
path: artifacts/
retention-days: 7

propose:
name: propose update
needs: build
# Also on push, not just manual dispatch. workflow_dispatch does not appear in the Actions UI
# until the workflow is on the default branch, so on a feature branch a push is the only way
# to trigger this, and leaving the binaries as bare artifacts to copy by hand is worse.
#
# Branches only. Never on pull_request, which would mean opening a PR from a PR, and never on
# a tag: a tag checkout is a detached HEAD with no base branch to target, and proposing a
# binaries PR against a release tag is meaningless anyway.
if: >-
github.event_name != 'pull_request' &&
startsWith(github.ref, 'refs/heads/') &&
(github.event_name != 'workflow_dispatch' || inputs.commit)
runs-on: ubuntu-latest
native:
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.1.0
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Download
uses: actions/download-artifact@v4
with:
pattern: native-*
merge-multiple: true
path: src

- name: Show what changed
run: |
ls -lhR src/DiffEngineViewer.Linux/runtimes src/DiffEngineViewer.Mac/runtimes
git status --short

# A PR rather than a direct push: these are binaries, so the diff is not reviewable and the
# change deserves an explicit approval.
- name: Open pull request
uses: peter-evans/create-pull-request@v7
with:
# Scoped to the triggering branch, which is also the base, so a rebuild on a feature
# branch does not collide with one on main.
branch: native-binaries-${{ github.ref_name }}
title: 'Rebuild native renderer binaries'
commit-message: 'Rebuild native renderer binaries'
body: |
Rebuilt `diffengine_viewer` from `native/` for the four RIDs that load one.
Windows is not among them: that head renders with WinForms.

Produced by the `build-native` workflow from ${{ github.sha }}.
add-paths: |
src/DiffEngineViewer.Linux/runtimes
src/DiffEngineViewer.Mac/runtimes
with:
library: diffengine_viewer
linux-runtimes: src/DiffEngineViewer.Linux/runtimes
swift-package: native/swift
mac-runtimes: src/DiffEngineViewer.Mac/runtimes
# Also on push, not just manual dispatch. workflow_dispatch does not appear in the Actions
# UI until the workflow is on the default branch, so on a feature branch a push is the only
# way to trigger this, and leaving the binaries as bare artifacts to copy by hand is worse.
propose: ${{ github.event_name != 'workflow_dispatch' || inputs.commit }}
pull-request-title: Rebuild native renderer binaries
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
if-no-files-found: ignore
retention-days: 14

# DE0001 warns when a RID has no native renderer. Shipping a package that cannot run on a
# PH0001 warns when a RID has no native renderer. Shipping a package that cannot run on a
# platform it claims to support is worse than failing the release.
- name: Verify every RID has a native renderer
if: startsWith(github.ref, 'refs/tags/')
Expand Down
19 changes: 17 additions & 2 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,23 @@ the comment there about not caching "nothing staged" asks for.
gate's continuations would need.

**Native shim (`native/`), used by the Mac and Linux heads only:**
- `raylib` and `imgui` are fetched by CMake (`FetchContent`), pinned by tag in
`native/CMakeLists.txt`. Deliberately not submodules: nothing in a normal `dotnet build` touches
- The build and packaging of the heads is PlatformHeads' (`D:\Code\PlatformHeads`,
github.com/SimonCropp/PlatformHeads), which BuildMonitor shares: it was worked out here and
copied there, and fixes made here did not reach the copy. Three pins name one tag of it and move
together: the `uses:` of `.github/workflows/build-native.yml`, the `GIT_TAG` in
`native/CMakeLists.txt`, and `PlatformHeads` in `Directory.Packages.props`. The workflow is its
reusable `build-native`. Its CMake module pins raylib and Dear ImGui, carries the raylib 6.0
option workarounds and links the library, and `native/CMakeLists.txt` names only the picture
formats, the source file and the two libraries the viewer links beyond raylib. Its package's
targets publish and bundle the `PlatformHead` items of `DiffEngine.csproj` (`PH0001` for a
native head with no library, `PH0002` for an apphost that was not staged), staged in
`obj\viewer`, where `DiffEngineTray.csproj` takes the two Windows heads from by calling
`PlatformHeadsPublish`. A fix to any of that goes there, not here. To build the Linux library by
hand, from the repository root: `docker run --rm -v "$PWD:/src" -v
"D:/Code/PlatformHeads/native:/ph:ro" -w /src -e LIBRARY=diffengine_viewer
"quay.io/pypa/manylinux_2_28_$(uname -m)" bash /ph/build-linux.sh`.
- `raylib` and `imgui` are fetched by CMake (`FetchContent`), pinned by tag in PlatformHeads'
module. Deliberately not submodules: nothing in a normal `dotnet build` touches
this folder, so a recursive clone on every checkout would serve a path almost nobody takes.
- Building it needs CMake 3.24+, a C++17 compiler and network access. Contributors do not need
any of that, because the binaries are committed.
Expand Down
Loading
Loading