diff --git a/.github/actions/setup-llvm/action.yml b/.github/actions/setup-llvm/action.yml
new file mode 100644
index 00000000..4f8caf40
--- /dev/null
+++ b/.github/actions/setup-llvm/action.yml
@@ -0,0 +1,189 @@
+# Shared toolchain setup for jobs that compile cx: installs LLVM 23 and the
+# link-time dependencies, and exports LLVMDIR/EXTRA_CMAKE_* for the configure
+# step. Used by both build.yml and release.yml so releases can never drift
+# onto a different toolchain than CI tested.
+name: Set up LLVM 23 and link dependencies
+description: Install LLVM 23 plus zlib/zstd/DIA SDK and export CMake paths
+
+runs:
+ using: composite
+ steps:
+ # Cache downloaded .debs (LLVM, SDL build deps, glfw) in a
+ # user-owned dir; the default archive dir is root-owned and the
+ # cache action could not restore into it. Stale entries are safe:
+ # apt re-downloads anything not matching the package index.
+ # The lock file and partial/ dir stay excluded: apt recreates them
+ # as root, and tar cannot read them back as the runner user.
+ - name: Cache apt archives (Ubuntu)
+ if: runner.os == 'Linux'
+ uses: actions/cache@v5
+ with:
+ path: |
+ /tmp/apt-archives
+ !/tmp/apt-archives/lock
+ !/tmp/apt-archives/partial
+ key: apt-archives-llvm-23-sdl-glfw
+
+ - name: Redirect apt archives (Ubuntu)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ mkdir -p /tmp/apt-archives/partial
+ echo 'Dir::Cache::Archives "/tmp/apt-archives";' | sudo tee /etc/apt/apt.conf.d/99-ci-cache
+
+ - name: Download LLVM (Ubuntu)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ wget https://apt.llvm.org/llvm.sh
+ chmod +x llvm.sh
+ sudo ./llvm.sh 23 all
+ echo "/usr/lib/llvm-23/bin" >> $GITHUB_PATH
+
+ # Bottle downloads are content-addressed, so a static key is safe.
+ # Only the fetch is cached; install/link still runs (cheap, and the
+ # full cellar layout is too version-sensitive to restore blindly).
+ - name: Cache Homebrew downloads (macOS)
+ if: runner.os == 'macOS'
+ uses: actions/cache@v5
+ with:
+ path: ~/Library/Caches/Homebrew
+ key: homebrew-downloads-llvm-23-sdl3-glfw
+
+ - name: Download LLVM (macOS)
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ # Refresh Homebrew first: a stale snapshot installs an older LLVM
+ # bottle (e.g. 23.1.0, whose libclangDriver.a ships an LLVM-bitcode
+ # object that the system linker can't parse).
+ brew update
+ brew install llvm@23
+ echo LLVMDIR=$(brew --prefix llvm@23) >> $GITHUB_ENV
+
+ # Cache the extracted LLVM tree: re-downloading and
+ # double-extracting the tarball costs minutes on every Windows leg.
+ # Bump the key when the URL below changes.
+ - name: Cache LLVM (Windows)
+ if: runner.os == 'Windows'
+ uses: actions/cache@v5
+ with:
+ path: clang+llvm-23.1.1-x86_64-pc-windows-msvc
+ key: llvm-windows-23.1.1
+
+ - name: Download LLVM (Windows)
+ if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ # Reuse the cached tree only when it is complete; a cache entry
+ # saved from a failed extract must trigger a re-download, not a
+ # build against a corrupt tree.
+ if [ ! -f "clang+llvm-23.1.1-x86_64-pc-windows-msvc/bin/clang.exe" ]; then
+ rm -rf clang+llvm-23.1.1-x86_64-pc-windows-msvc
+ curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.1/clang+llvm-23.1.1-x86_64-pc-windows-msvc.tar.xz" --output llvm.tar.xz && 7z x llvm.tar.xz && 7z x llvm.tar
+ fi
+ # The sed below converts the path to a Windows-style path
+ echo LLVMDIR="$(echo "$PWD/clang+llvm-23.1.1-x86_64-pc-windows-msvc" | sed -e 's/^\///' -e 's/\//\\/g' -e 's/^./\0:/')" >> $GITHUB_ENV
+
+ # LLVM 22+ locates the DIA SDK via $VSINSTALLDIR (see llvm/cmake/modules/FindDIASDK.cmake),
+ # which is only set inside a Visual Studio developer environment.
+ # vs-shell is composite (no Node runtime); ilammy/msvc-dev-cmd is abandoned on node20.
+ - name: Set up MSVC environment (Windows)
+ if: runner.os == 'Windows'
+ uses: egor-tensin/vs-shell@v2
+
+ - name: Provide DIA SDK (Windows)
+ if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ # LLVM's LLVMDebugInfoPDB target needs diaguids.lib. Its FindDIASDK
+ # module only looks at $VSINSTALLDIR, which may point at an install
+ # without it, so search wider and pass an explicit path if needed.
+ DIA_CMAKE_DIR=""
+ if [ -f "$VSINSTALLDIR/DIA SDK/lib/amd64/diaguids.lib" ]; then
+ echo "DIA SDK found via VSINSTALLDIR"
+ else
+ VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
+ if [ -f "$VSWHERE" ]; then
+ mapfile -t VS_INSTALLS < <("$VSWHERE" -products '*' -property installationPath)
+ for INSTALL_PATH in "${VS_INSTALLS[@]}"; do
+ INSTALL_PATH_UNIX=$(echo "$INSTALL_PATH" | tr -d '\r' | sed -e 's/\\/\//g')
+ if [ -f "$INSTALL_PATH_UNIX/DIA SDK/lib/amd64/diaguids.lib" ]; then
+ DIA_CMAKE_DIR="$INSTALL_PATH_UNIX/DIA SDK"
+ break
+ fi
+ done
+ fi
+ if [ -z "$DIA_CMAKE_DIR" ]; then
+ # Fall back to the DIA files shipped in the Windows SDK, staged
+ # into the layout FindDIASDK expects.
+ DIA_LIB=$(ls -d "/c/Program Files (x86)/Windows Kits/10/Lib/"*"/um/x64/diaguids.lib" 2>/dev/null | sort -r | head -n 1 || true)
+ DIA_INC=$(echo "$DIA_LIB" | sed -e 's#/Lib/\(.*\)/um/x64/diaguids.lib#/Include/\1/um/dia2.h#')
+ if [ -n "$DIA_LIB" ] && [ -f "$DIA_INC" ]; then
+ DIA_STAGE="$RUNNER_TEMP/dia-sdk"
+ mkdir -p "$DIA_STAGE/include" "$DIA_STAGE/lib/amd64"
+ cp "$DIA_LIB" "$DIA_STAGE/lib/amd64/diaguids.lib"
+ cp "$DIA_INC" "$DIA_STAGE/include/dia2.h"
+ DIA_CMAKE_DIR=$(echo "$DIA_STAGE" | sed -e 's/\\/\//g')
+ fi
+ fi
+ if [ -z "$DIA_CMAKE_DIR" ]; then
+ echo "ERROR: DIA SDK with lib/amd64/diaguids.lib not found in VSINSTALLDIR, any VS instance, or the Windows Kits"
+ exit 1
+ fi
+ echo "DIA SDK found at: $DIA_CMAKE_DIR"
+ echo "DIA_CMAKE_DIR=$DIA_CMAKE_DIR" >> $GITHUB_ENV
+ fi
+
+ # LLVM 22+ links LLVMSupport against the ZLIB::ZLIB and zstd::libzstd_static
+ # imported targets, so findable ZLIB and zstd are required at configure
+ # time. Use the static triplets to match the /MT runtime used with the
+ # prebuilt LLVM.
+ # Note: vcpkg names the static libs zs/zsd, which FindZLIB only
+ # searches for with ZLIB_USE_STATIC_LIBS=ON. Similarly, LLVM's Findzstd
+ # module only defines zstd::libzstd_static for a library literally named
+ # zstd_static.lib, which vcpkg doesn't ship (only zstd.lib), so copy it.
+ # The vcpkg baseline moves with the runner image, so resolve it to
+ # a commit SHA for the cache key. A static key would freeze on the
+ # first binaries saved (actions/cache never re-saves on hit) and
+ # rebuild from source forever after any update. Falls back to a
+ # static key if the install is not a git checkout.
+ - name: Get vcpkg baseline (Windows)
+ if: runner.os == 'Windows'
+ id: vcpkg-baseline
+ shell: bash
+ run: echo "sha=$(git -C \"$VCPKG_INSTALLATION_ROOT\" rev-parse HEAD 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT"
+
+ # vcpkg restores the built ports from this binary cache instead of
+ # compiling them; each entry carries an ABI hash, so stale hits are
+ # impossible.
+ - name: Cache vcpkg binaries (Windows)
+ if: runner.os == 'Windows'
+ uses: actions/cache@v5
+ with:
+ path: ${{ runner.temp }}/vcpkg-binary-cache
+ key: vcpkg-${{ steps.vcpkg-baseline.outputs.sha }}-zlib-zstd-static
+
+ - name: Install zlib and zstd (Windows)
+ if: runner.os == 'Windows'
+ shell: bash
+ env:
+ VCPKG_BINARY_SOURCES: "clear;files,${{ runner.temp }}/vcpkg-binary-cache,readwrite"
+ run: |
+ vcpkg install zlib:x64-windows-static zstd:x64-windows-static
+ cp "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd.lib" "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd_static.lib"
+ ls "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib"
+ echo "EXTRA_CMAKE_PREFIX=$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static" >> $GITHUB_ENV
+ echo "EXTRA_CMAKE_ARGS=-DZLIB_USE_STATIC_LIBS=ON" >> $GITHUB_ENV
+
+ # The lit/snippet/LSP suites spawn thousands of compiler and linker
+ # processes, each writing temporary objects and executables. Real-time
+ # Defender scanning of every one is a large Windows-only slowdown, so
+ # exclude the build tree, the repo, and the temp directories. The
+ # runner is ephemeral, so narrowing scan coverage here costs nothing.
+ - name: Exclude build and temp paths from Defender (Windows)
+ if: runner.os == 'Windows'
+ shell: powershell
+ continue-on-error: true
+ run: |
+ Add-MpPreference -ExclusionPath "${{ runner.workspace }}", "$env:GITHUB_WORKSPACE", "$env:RUNNER_TEMP", "$env:TEMP"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index ae14b7dc..def4242d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -20,182 +20,8 @@ jobs:
with:
python-version: 3.12
- # Cache downloaded .debs (LLVM, SDL build deps, glfw) in a
- # user-owned dir; the default archive dir is root-owned and the
- # cache action could not restore into it. Stale entries are safe:
- # apt re-downloads anything not matching the package index.
- # The lock file and partial/ dir stay excluded: apt recreates them
- # as root, and tar cannot read them back as the runner user.
- - name: Cache apt archives (Ubuntu)
- if: matrix.os == 'ubuntu-latest'
- uses: actions/cache@v5
- with:
- path: |
- /tmp/apt-archives
- !/tmp/apt-archives/lock
- !/tmp/apt-archives/partial
- key: apt-archives-llvm-23-sdl-glfw
-
- - name: Redirect apt archives (Ubuntu)
- if: matrix.os == 'ubuntu-latest'
- run: |
- mkdir -p /tmp/apt-archives/partial
- echo 'Dir::Cache::Archives "/tmp/apt-archives";' | sudo tee /etc/apt/apt.conf.d/99-ci-cache
-
- - name: Download LLVM (Ubuntu)
- if: matrix.os == 'ubuntu-latest'
- run: |
- wget https://apt.llvm.org/llvm.sh
- chmod +x llvm.sh
- sudo ./llvm.sh 23 all
- echo "/usr/lib/llvm-23/bin" >> $GITHUB_PATH
-
- # Bottle downloads are content-addressed, so a static key is safe.
- # Only the fetch is cached; install/link still runs (cheap, and the
- # full cellar layout is too version-sensitive to restore blindly).
- - name: Cache Homebrew downloads (macOS)
- if: matrix.os == 'macos-latest'
- uses: actions/cache@v5
- with:
- path: ~/Library/Caches/Homebrew
- key: homebrew-downloads-llvm-23-sdl3-glfw
-
- - name: Download LLVM (macOS)
- if: matrix.os == 'macos-latest'
- run: |
- # Refresh Homebrew first: a stale snapshot installs an older LLVM
- # bottle (e.g. 23.1.0, whose libclangDriver.a ships an LLVM-bitcode
- # object that the system linker can't parse).
- brew update
- brew install llvm@23
- echo LLVMDIR=$(brew --prefix llvm@23) >> $GITHUB_ENV
-
- # Cache the extracted LLVM tree: re-downloading and
- # double-extracting the tarball costs minutes on every Windows leg.
- # Bump the key when the URL below changes.
- - name: Cache LLVM (Windows)
- if: matrix.os == 'windows-latest'
- uses: actions/cache@v5
- with:
- path: clang+llvm-23.1.1-x86_64-pc-windows-msvc
- key: llvm-windows-23.1.1
-
- - name: Download LLVM (Windows)
- if: matrix.os == 'windows-latest'
- shell: bash
- run: |
- # Reuse the cached tree only when it is complete; a cache entry
- # saved from a failed extract must trigger a re-download, not a
- # build against a corrupt tree.
- if [ ! -f "clang+llvm-23.1.1-x86_64-pc-windows-msvc/bin/clang.exe" ]; then
- rm -rf clang+llvm-23.1.1-x86_64-pc-windows-msvc
- curl -L "https://github.com/llvm/llvm-project/releases/download/llvmorg-23.1.1/clang+llvm-23.1.1-x86_64-pc-windows-msvc.tar.xz" --output llvm.tar.xz && 7z x llvm.tar.xz && 7z x llvm.tar
- fi
- # The sed below converts the path to a Windows-style path
- echo LLVMDIR="$(echo "$PWD/clang+llvm-23.1.1-x86_64-pc-windows-msvc" | sed -e 's/^\///' -e 's/\//\\/g' -e 's/^./\0:/')" >> $GITHUB_ENV
-
- # LLVM 22+ locates the DIA SDK via $VSINSTALLDIR (see llvm/cmake/modules/FindDIASDK.cmake),
- # which is only set inside a Visual Studio developer environment.
- # vs-shell is composite (no Node runtime); ilammy/msvc-dev-cmd is abandoned on node20.
- - name: Set up MSVC environment (Windows)
- if: matrix.os == 'windows-latest'
- uses: egor-tensin/vs-shell@v2
-
- - name: Provide DIA SDK (Windows)
- if: matrix.os == 'windows-latest'
- shell: bash
- run: |
- # LLVM's LLVMDebugInfoPDB target needs diaguids.lib. Its FindDIASDK
- # module only looks at $VSINSTALLDIR, which may point at an install
- # without it, so search wider and pass an explicit path if needed.
- DIA_CMAKE_DIR=""
- if [ -f "$VSINSTALLDIR/DIA SDK/lib/amd64/diaguids.lib" ]; then
- echo "DIA SDK found via VSINSTALLDIR"
- else
- VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
- if [ -f "$VSWHERE" ]; then
- mapfile -t VS_INSTALLS < <("$VSWHERE" -products '*' -property installationPath)
- for INSTALL_PATH in "${VS_INSTALLS[@]}"; do
- INSTALL_PATH_UNIX=$(echo "$INSTALL_PATH" | tr -d '\r' | sed -e 's/\\/\//g')
- if [ -f "$INSTALL_PATH_UNIX/DIA SDK/lib/amd64/diaguids.lib" ]; then
- DIA_CMAKE_DIR="$INSTALL_PATH_UNIX/DIA SDK"
- break
- fi
- done
- fi
- if [ -z "$DIA_CMAKE_DIR" ]; then
- # Fall back to the DIA files shipped in the Windows SDK, staged
- # into the layout FindDIASDK expects.
- DIA_LIB=$(ls -d "/c/Program Files (x86)/Windows Kits/10/Lib/"*"/um/x64/diaguids.lib" 2>/dev/null | sort -r | head -n 1 || true)
- DIA_INC=$(echo "$DIA_LIB" | sed -e 's#/Lib/\(.*\)/um/x64/diaguids.lib#/Include/\1/um/dia2.h#')
- if [ -n "$DIA_LIB" ] && [ -f "$DIA_INC" ]; then
- DIA_STAGE="$RUNNER_TEMP/dia-sdk"
- mkdir -p "$DIA_STAGE/include" "$DIA_STAGE/lib/amd64"
- cp "$DIA_LIB" "$DIA_STAGE/lib/amd64/diaguids.lib"
- cp "$DIA_INC" "$DIA_STAGE/include/dia2.h"
- DIA_CMAKE_DIR=$(echo "$DIA_STAGE" | sed -e 's/\\/\//g')
- fi
- fi
- if [ -z "$DIA_CMAKE_DIR" ]; then
- echo "ERROR: DIA SDK with lib/amd64/diaguids.lib not found in VSINSTALLDIR, any VS instance, or the Windows Kits"
- exit 1
- fi
- echo "DIA SDK found at: $DIA_CMAKE_DIR"
- echo "DIA_CMAKE_DIR=$DIA_CMAKE_DIR" >> $GITHUB_ENV
- fi
-
- # LLVM 22+ links LLVMSupport against the ZLIB::ZLIB and zstd::libzstd_static
- # imported targets, so findable ZLIB and zstd are required at configure
- # time. Use the static triplets to match the /MT runtime used with the
- # prebuilt LLVM.
- # Note: vcpkg names the static libs zs/zsd, which FindZLIB only
- # searches for with ZLIB_USE_STATIC_LIBS=ON. Similarly, LLVM's Findzstd
- # module only defines zstd::libzstd_static for a library literally named
- # zstd_static.lib, which vcpkg doesn't ship (only zstd.lib), so copy it.
- # The vcpkg baseline moves with the runner image, so resolve it to
- # a commit SHA for the cache key. A static key would freeze on the
- # first binaries saved (actions/cache never re-saves on hit) and
- # rebuild from source forever after any update. Falls back to a
- # static key if the install is not a git checkout.
- - name: Get vcpkg baseline (Windows)
- if: matrix.os == 'windows-latest'
- id: vcpkg-baseline
- shell: bash
- run: echo "sha=$(git -C \"$VCPKG_INSTALLATION_ROOT\" rev-parse HEAD 2>/dev/null || echo unknown)" >> "$GITHUB_OUTPUT"
-
- # vcpkg restores the built ports from this binary cache instead of
- # compiling them; each entry carries an ABI hash, so stale hits are
- # impossible.
- - name: Cache vcpkg binaries (Windows)
- if: matrix.os == 'windows-latest'
- uses: actions/cache@v5
- with:
- path: ${{ runner.temp }}/vcpkg-binary-cache
- key: vcpkg-${{ steps.vcpkg-baseline.outputs.sha }}-zlib-zstd-static
-
- - name: Install zlib and zstd (Windows)
- if: matrix.os == 'windows-latest'
- shell: bash
- env:
- VCPKG_BINARY_SOURCES: "clear;files,${{ runner.temp }}/vcpkg-binary-cache,readwrite"
- run: |
- vcpkg install zlib:x64-windows-static zstd:x64-windows-static
- cp "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd.lib" "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib/zstd_static.lib"
- ls "$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static/lib"
- echo "EXTRA_CMAKE_PREFIX=$VCPKG_INSTALLATION_ROOT/installed/x64-windows-static" >> $GITHUB_ENV
- echo "EXTRA_CMAKE_ARGS=-DZLIB_USE_STATIC_LIBS=ON" >> $GITHUB_ENV
-
- # The lit/snippet/LSP suites spawn thousands of compiler and linker
- # processes, each writing temporary objects and executables. Real-time
- # Defender scanning of every one is a large Windows-only slowdown, so
- # exclude the build tree, the repo, and the temp directories. The
- # runner is ephemeral, so narrowing scan coverage here costs nothing.
- - name: Exclude build and temp paths from Defender (Windows)
- if: matrix.os == 'windows-latest'
- shell: powershell
- continue-on-error: true
- run: |
- Add-MpPreference -ExclusionPath "${{ runner.workspace }}", "$env:GITHUB_WORKSPACE", "$env:RUNNER_TEMP", "$env:TEMP"
+ - name: Set up LLVM 23 and link dependencies
+ uses: ./.github/actions/setup-llvm
- name: Create Build Environment
run: cmake -E make_directory ${{runner.workspace}}/build
@@ -654,6 +480,7 @@ jobs:
run: |
node website/test-bench.mjs
node website/test-cx-mode.mjs
+ node website/test-install.mjs
node wasm/test-editor.mjs
node wasm/test-playground.mjs
node wasm/test-playground-worker.mjs
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..6f67ed6b
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,235 @@
+name: Release
+
+# Prebuilt binaries behind the front-page download links. Pushing a v* tag
+# publishes archives to GitHub Releases; pull requests touching this workflow
+# or the shared LLVM setup still build, package, and smoke-test the archives
+# (uploaded as artifacts) without publishing anything.
+#
+# Linux and Windows link LLVM statically, so their archives are standalone.
+# macOS archives bundle the Homebrew LLVM dylibs next to the binaries
+# (Homebrew ships shared-only libraries), with install names rewritten to
+# @executable_path/@loader_path. The otool assert fails the release if any
+# Homebrew reference survives.
+on:
+ push:
+ tags: ["v[0-9]*"]
+ pull_request:
+ paths:
+ - .github/workflows/release.yml
+ - .github/actions/setup-llvm/**
+ workflow_dispatch:
+
+jobs:
+ package:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Older runner than CI on purpose: binaries must run on older
+ # distros than the build machine (glibc is forward-compatible only).
+ - os: ubuntu-22.04
+ platform: linux-x64
+ - os: macos-latest
+ platform: macos-arm64
+ - os: windows-latest
+ platform: windows-x64
+ env:
+ STAGE_DIR: stage/cx-${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Set up LLVM 23 and link dependencies
+ uses: ./.github/actions/setup-llvm
+
+ - name: Create Build Environment
+ run: cmake -E make_directory ${{runner.workspace}}/build
+
+ - name: Configure CMake
+ shell: bash
+ working-directory: ${{runner.workspace}}/build
+ run: |
+ extra_args=()
+ if [ "$RUNNER_OS" = "Linux" ]; then
+ # Static libstdc++/libgcc: the archive must run on older distros
+ # than the build runner. Static LLVM/Clang: apt.llvm.org builds
+ # the LLVM dylib, which would otherwise link shared.
+ extra_args+=("-DCMAKE_EXE_LINKER_FLAGS=-static-libstdc++ -static-libgcc")
+ extra_args+=("-DCX_STATIC_LLVM=ON")
+ fi
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ # Static CRT so users need no Visual C++ redistributable. The
+ # dumpbin assert below proves no CRT DLL dependency; if /MT ever
+ # stops applying, the release fails there instead of shipping a
+ # binary that refuses to start.
+ extra_args+=("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded")
+ fi
+ cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$LLVMDIR${EXTRA_CMAKE_PREFIX:+;$EXTRA_CMAKE_PREFIX}" -DCMAKE_UNITY_BUILD=ON $EXTRA_CMAKE_ARGS ${DIA_CMAKE_DIR:+-DMSVC_DIA_SDK_DIR="$DIA_CMAKE_DIR"} "${extra_args[@]}"
+
+ - name: Build cx and cx-lsp
+ working-directory: ${{runner.workspace}}/build
+ shell: bash
+ run: cmake --build . --config Release --target cx cx-lsp
+
+ # The compiler finds its standard library next to the executable
+ # (see getCxRootDir), so the archive is just the two binaries plus
+ # a copy of std/.
+ - name: Stage archive contents
+ shell: bash
+ run: |
+ mkdir -p "$STAGE_DIR"
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ cp "${{runner.workspace}}/build/Release/cx.exe" "${{runner.workspace}}/build/Release/cx-lsp.exe" "$STAGE_DIR/"
+ # lld-link powers native builds on machines with no C compiler on
+ # PATH (the driver prefers it next to cx.exe over the build-time
+ # LLVM tools dir, which doesn't exist on user machines).
+ cp "${LLVMDIR}\\bin\\lld-link.exe" "$STAGE_DIR/"
+ else
+ cp "${{runner.workspace}}/build/cx" "${{runner.workspace}}/build/cx-lsp" "$STAGE_DIR/"
+ strip "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp"
+ fi
+ cp -r "$GITHUB_WORKSPACE/std" "$STAGE_DIR/std"
+ cp "$GITHUB_WORKSPACE/LICENSE.txt" "$STAGE_DIR/"
+
+ - name: Bundle macOS libraries
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ lib="$STAGE_DIR/lib"
+ mkdir -p "$lib"
+ # Copy every Homebrew library referenced by the binaries or by
+ # the bundled libraries themselves. Iterates to a fixpoint to
+ # catch transitive deps (z3, zstd, ...).
+ prev="unseeded"
+ while [ "$(ls "$lib" | tr '\n' ' ')" != "$prev" ]; do
+ prev="$(ls "$lib" | tr '\n' ' ')"
+ for f in "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$lib"/*.dylib; do
+ [ -f "$f" ] || continue
+ otool -L "$f" | tail -n +2 | awk '{print $1}' | while read -r dep; do
+ case "$dep" in
+ # Copied once: cx and cx-lsp share dependencies, and -n
+ # would exit 1 on the second copy. A real cp failure must
+ # still fail the release, so guard with a test instead.
+ /opt/homebrew/* | /usr/local/*)
+ [ -e "$lib/${dep##*/}" ] || cp "$dep" "$lib/"
+ ;;
+ esac
+ done
+ done
+ done
+ rewire() {
+ otool -L "$1" | tail -n +2 | awk '{print $1}' | while read -r dep; do
+ case "$dep" in
+ /opt/homebrew/* | /usr/local/* | @rpath/*)
+ install_name_tool -change "$dep" "$2/$(basename "$dep")" "$1"
+ ;;
+ esac
+ done
+ }
+ for f in "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp; do
+ rewire "$f" "@executable_path/lib"
+ # Drop Homebrew rpaths: an absolute fallback could mask a
+ # missed fixup, passing smoke on this machine only.
+ otool -l "$f" | awk '$1 == "path" {print $2}' | while read -r rpath; do
+ case "$rpath" in
+ /opt/homebrew/* | /usr/local/*) install_name_tool -delete_rpath "$rpath" "$f" || true ;;
+ esac
+ done
+ done
+ for d in "$lib"/*.dylib; do
+ rewire "$d" "@loader_path"
+ done
+ codesign --force -s - "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$lib"/*.dylib
+
+ # Hard fail before anything publishes: an archive that needs LLVM,
+ # libstdc++, Homebrew, or the MSVC redist installed is not a
+ # prebuilt binary.
+ - name: Assert standalone linking
+ shell: bash
+ run: |
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ # Dash, not slash: git-bash would rewrite /dependents to a
+ # Windows path (C:\Program Files\Git\dependents).
+ deps=$(dumpbin -dependents "$STAGE_DIR/cx.exe" "$STAGE_DIR/cx-lsp.exe" "$STAGE_DIR/lld-link.exe")
+ echo "$deps"
+ if echo "$deps" | grep -Ei 'vcruntime|msvcp|msvcr'; then
+ echo "ERROR: binaries need the Visual C++ redistributable (/MT did not apply)"
+ exit 1
+ fi
+ elif [ "$RUNNER_OS" = "macOS" ]; then
+ # The asset name promises arm64; fail loudly on runner drift.
+ [ "$(uname -m)" = "arm64" ] || { echo "ERROR: expected arm64 runner for macos-arm64 asset"; exit 1; }
+ otool -L "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$STAGE_DIR"/lib/*.dylib
+ # Skip each file's own install-ID line (the line after each
+ # "file:" header): IDs are inert once nothing references them.
+ if otool -L "$STAGE_DIR"/cx "$STAGE_DIR"/cx-lsp "$STAGE_DIR"/lib/*.dylib | awk '/:$/{skip=1; next} skip==1{skip=0; next} {print}' | grep -E '/opt/homebrew|/usr/local|@rpath'; then
+ echo "ERROR: macOS binaries still reference Homebrew or @rpath"
+ exit 1
+ fi
+ else
+ ldd "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp"
+ if ldd "$STAGE_DIR/cx" "$STAGE_DIR/cx-lsp" | grep -Ei 'llvm|clang|libstdc\+\+|libgcc'; then
+ echo "ERROR: binaries link LLVM, libstdc++, or libgcc dynamically"
+ exit 1
+ fi
+ fi
+
+ - name: Smoke-test the packaged binaries
+ shell: bash
+ run: |
+ # Run from a different directory than the binaries: this proves the
+ # packaged compiler finds its stdlib by executable path, not cwd.
+ printf 'void main() {\n println("smoke ok");\n}\n' > "$RUNNER_TEMP/hello.cx"
+ cd "$RUNNER_TEMP"
+ pkg="$GITHUB_WORKSPACE/$STAGE_DIR"
+ "$pkg/cx" run hello.cx
+ # Also exercise the native compile+link path (uses the system linker).
+ "$pkg/cx" hello.cx -o hello-native
+ ./hello-native
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ # Bare-machine simulation: with no compiler on PATH, native builds
+ # must still link through the bundled lld-link.
+ PATH="/usr/bin:/bin" "$pkg/cx" hello.cx -o hello-embedded
+ ./hello-embedded
+ fi
+ "$pkg/cx-lsp" --help > /dev/null
+
+ - name: Create archive
+ shell: bash
+ run: |
+ name="cx-${{ matrix.platform }}"
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ (cd stage && 7z a "../$name.zip" "$name")
+ else
+ tar -czf "$name.tar.gz" -C stage "$name"
+ fi
+
+ - name: Upload archive artifact
+ uses: actions/upload-artifact@v6
+ with:
+ name: binaries-${{ matrix.platform }}
+ path: cx-${{ matrix.platform }}.*
+
+ publish:
+ if: startsWith(github.ref, 'refs/tags/v')
+ needs: [package]
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/download-artifact@v7
+ with:
+ pattern: binaries-*
+ merge-multiple: true
+ path: dist
+
+ - uses: softprops/action-gh-release@v2
+ with:
+ files: dist/*
+ generate_release_notes: true
+ body: |
+ Prebuilt cx binaries for Linux (x64), macOS (Apple Silicon), and Windows (x64). Each archive holds `cx`, `cx-lsp`, and the standard library (Windows also bundles the lld linker). No installer, no dependencies: unpack and run. Native compilation needs a C toolchain: `cc` on PATH on Linux/macOS, Visual Studio Build Tools installed on Windows.
+
+ Fastest install on Linux/macOS: `curl https://cx-language.github.io/cx/install.sh | bash`
+
+ macOS notes: the archive bundles the LLVM libraries (~200MB unpacked), and the binaries are ad-hoc signed. The installer strips the download quarantine for you; if you unpack manually and macOS refuses to run them, right-click the binary and choose Open once.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index bfdc0463..c8071df6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -73,6 +73,11 @@ if(CX_SANITIZE_LEAK)
add_link_options(-fsanitize=leak)
endif()
+# Prebuilt binaries link LLVM and Clang statically so the archives run
+# without an LLVM install. Without this, distributions that build the LLVM
+# dylib (Debian/Ubuntu, Homebrew) link libLLVM/libclang-cpp shared.
+option(CX_STATIC_LLVM "Link the LLVM/Clang static archives instead of the shared dylib" OFF)
+
# Instrumented build for the ASan gate (test/asan/check_asan.py), which runs
# the compiler and repeated LSP analyses under AddressSanitizer. Needs a Clang
# with -fsanitize=address (LLVM Clang or Apple Clang).
@@ -131,7 +136,7 @@ endif()
# --- zstd (real, required via LLVMSupport) ---
find_package(zstd QUIET)
-if(NOT TARGET zstd::libzstd_static)
+if(NOT TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared)
message(STATUS "zstd not found, fetching zstd from source")
FetchContent_Declare(zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
@@ -156,11 +161,24 @@ if(NOT TARGET zstd::libzstd_static)
if(NOT TARGET zstd::libzstd_static)
add_library(zstd::libzstd_static ALIAS libzstd_static)
endif()
+ if(NOT TARGET zstd::libzstd_shared)
+ add_library(zstd::libzstd_shared ALIAS libzstd_static)
+ endif()
# As above: keep our unity-build default out of the zstd subbuild.
set_target_properties(libzstd_static PROPERTIES UNITY_BUILD OFF)
set(CMAKE_DISABLE_FIND_PACKAGE_zstd TRUE)
endif()
+# LLVM distributions disagree on the name: Debian/Ubuntu exports reference
+# zstd::libzstd_shared, others zstd::libzstd_static. A system zstd that only
+# provides one spelling aliases the other; both link the same library.
+if(TARGET zstd::libzstd_static AND NOT TARGET zstd::libzstd_shared)
+ add_library(zstd::libzstd_shared ALIAS zstd::libzstd_static)
+endif()
+if(TARGET zstd::libzstd_shared AND NOT TARGET zstd::libzstd_static)
+ add_library(zstd::libzstd_static ALIAS zstd::libzstd_shared)
+endif()
+
# --- LibXml2 (dummy suffices; LLVMWindowsManifest is not linked) ---
find_package(LibXml2 QUIET)
if(NOT TARGET LibXml2::LibXml2)
@@ -281,10 +299,15 @@ endif()
add_executable(cx-lsp src/lsp/main.cpp)
target_link_libraries(cx-lsp libcx)
-if(LLVM_LINK_LLVM_DYLIB)
+# plugins is only referenced by the Clang archives (PassPlugin::Load in
+# clangFrontend): non-dylib builds pull it through the Clang link interfaces,
+# so it must be named here for the dylib-edge rewrite below to reproduce that
+# closure on dylib-building distributions.
+llvm_map_components_to_libnames(CX_LLVM_COMPONENT_LIBS core native linker orcjit passes support plugins)
+if(LLVM_LINK_LLVM_DYLIB AND NOT CX_STATIC_LLVM)
set(LLVM_LIBS LLVM)
else()
- llvm_map_components_to_libnames(LLVM_LIBS core native linker orcjit passes support)
+ set(LLVM_LIBS ${CX_LLVM_COMPONENT_LIBS})
endif()
# The LibXml2/DIASDK dummy targets above only satisfy LLVMExports.cmake; they
# carry no library. Fail fast if a linked component ever needs the real one.
@@ -295,7 +318,7 @@ if(CX_DIASDK_DUMMY AND "LLVMDebugInfoPDB" IN_LIST LLVM_LIBS)
message(FATAL_ERROR "LLVMDebugInfoPDB requires the real DIA SDK, but the dummy target was used. Install the DIA SDK (Visual Studio) or point MSVC_DIA_SDK_DIR at it.")
endif()
-if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp)
+if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp AND NOT CX_STATIC_LLVM)
# Shared LLVM build: use only the shared libclang-cpp for the Clang C++
# API. It contains everything the static archives below have, so linking
# both only slows the link and bloats the binary. (Homebrew's static
@@ -305,6 +328,54 @@ if(LLVM_LINK_LLVM_DYLIB AND TARGET clang-cpp)
else()
list(APPEND LLVM_LIBS clangAST clangBasic clangFrontend clangLex clangParse clangSema clangDriver)
endif()
+# Dylib-building distributions (Debian/Ubuntu, Homebrew) export every Clang
+# archive's link interface with a trailing `LLVM` edge onto the shared dylib,
+# which would reintroduce the dylib even though LLVM_LIBS above names the
+# static components. Static builds rewrite that edge to the components,
+# keeping the other edges (transitive Clang archives) intact. The component
+# set already covers every LLVM symbol the Clang archives need: non-dylib
+# builds (Windows) link this exact combination.
+if(CX_STATIC_LLVM AND LLVM_LINK_LLVM_DYLIB)
+ # Walk the Clang link closure so transitive Clang archives (which carry
+ # the same `LLVM` edge) are covered on any LLVM version.
+ set(CX_CLANG_LIBS clangAST clangBasic clangFrontend clangLex clangParse clangSema clangDriver)
+ set(CX_CLANG_SEEN "")
+ set(CX_CLANG_QUEUE ${CX_CLANG_LIBS})
+ while(CX_CLANG_QUEUE)
+ list(POP_FRONT CX_CLANG_QUEUE CX_LIB)
+ if(CX_LIB IN_LIST CX_CLANG_SEEN OR NOT TARGET ${CX_LIB})
+ continue()
+ endif()
+ list(APPEND CX_CLANG_SEEN ${CX_LIB})
+ get_target_property(CX_IFACE ${CX_LIB} INTERFACE_LINK_LIBRARIES)
+ if(CX_IFACE)
+ foreach(dep IN LISTS CX_IFACE)
+ if(dep MATCHES "^clang")
+ list(APPEND CX_CLANG_QUEUE ${dep})
+ elseif(dep MATCHES "^\\$cx is a fast language that gets out of your way
simple C-like syntax, and type-safe constructs as efficient as hand-written C.
Prebuilt binaries for Linux (x64), macOS (Apple Silicon), and Windows (x64). To build from source, visit GitHub.
+ ##OSTABS## +$ curl https://cx-language.github.io/cx/install.sh | bash
+ Installs into ~/.cx, adds itself to your PATH, and smoke-tests the result.
void main() {
+ println("Hello world");
+}
+ $ export PATH="$HOME/.cx:$PATH"
+$ cx run hello.cx
+Hello world
+ New shells get the PATH automatically; this shell needs one export first. Then cx run JIT-compiles and executes in one step. Point your editor at the bundled cx-lsp - see Editor setup.
Download cx-windows-x64.zip from the latest release and unpack it.
+> Expand-Archive cx-windows-x64.zip
+> cd cx-windows-x64
+ void main() {
+ println("Hello world");
+}
+ > .\cx.exe run hello.cx
+Hello world
+ cx run builds and executes in one step. Point your editor at the bundled cx-lsp.exe - see Editor setup.