Skip to content

Merge branch 'main' of https://github.com/chemicallang/chemical #3361

Merge branch 'main' of https://github.com/chemicallang/chemical

Merge branch 'main' of https://github.com/chemicallang/chemical #3361

name: Build and Release
permissions:
contents: write
on:
push:
branches:
- main
workflow_dispatch:
inputs:
config:
description: >-
Build configuration. 'Release' = optimized binaries published to chemicallang/chemical
(default). 'RelWithDebInfo' (recommended for debugging: symbols + optimization) or
'Debug' = binaries with full debug symbols, published to chemicallang/chemical-debug
instead.
required: false
type: choice
default: Release
options:
- Release
- RelWithDebInfo
- Debug
publish_release:
description: >-
Publish the debug release to chemicallang/chemical-debug instead of drafting it.
When checked, the release is automatically tagged with the latest tag of
chemicallang/chemical and published immediately. When unchecked (default), the
release is drafted and tagged with the source commit SHA.
required: false
type: boolean
default: false
env:
LLVM_RELEASE_TAG: "v22.1.8"
# Node 24 actions (actions/github-script, softprops/action-gh-release, ...)
# intermittently fail with "HttpError: self-signed certificate; ... try
# running Node.js with --use-system-ca" on hosted runners: their api.github.com
# calls can route through GitHub's runner egress proxy, whose root CA lives only
# in the runner's OS trust store, not in Node's bundled store. --use-system-ca
# makes Node consult the OS store as well, fixing the flaky release-check steps
# (see run 31575861448, job 94047706398).
# Requires Node >= 22.10 (--use-system-ca). All JS actions now run on Node 24
# on hosted runners; if Node 20 is ever re-enabled
# (ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION) or on pre-migration self-hosted
# runners, remove this line.
NODE_OPTIONS: --use-system-ca
# Effective build config. On push (no dispatch inputs) this falls back to Release.
BUILD_CONFIG: ${{ inputs.config || 'Release' }}
jobs:
build-windows:
name: Build on Windows (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
# Only run when the commit message contains "releaseIt"
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: windows-2025
llvm_asset: windows-x64.zip
- arch: arm64
runner: windows-11-arm
llvm_asset: windows-arm64.zip
steps:
# Checkout chemical repository
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
# ––– Setup build environment –––
- name: Set up Visual Studio Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1
with:
arch: ${{ matrix.arch }}
# Download prebuilt LLVM and unzip it
- name: Download prebuilt LLVM
shell: pwsh
run: |
$url = "https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/${{ matrix.llvm_asset }}"
Write-Host "Downloading LLVM from $url"
curl.exe -L -o llvm.zip -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" $url
7z x llvm.zip -y -ochemical
# Configure the project
- name: Map workspace to X and configure
shell: pwsh
run: |
subst X: $PWD
cd X:\chemical
mkdir out\build
cmake -S . -B out\build -G Ninja -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} -DCMAKE_NINJA_FORCE_RESPONSE_FILE=ON
# Run the configure script to configure the project
- name: Run configure script (Windows)
working-directory: chemical
run: bash ./scripts/setup.sh --arch ${{ matrix.arch }}
# Building the llvm based, tiny cc based compilers and lsp
- name: Build Compiler Targets
working-directory: chemical
run: |
cmake --build out\build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out\build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
cmake --build out\build --config ${{ env.BUILD_CONFIG }} --target ChemicalLsp
# Run the release script to create ZIP files
- name: Run release packaging script (Windows)
working-directory: chemical
run: bash ./scripts/release.sh --windows
# rename windows release files to include the aarch
- name: Rename Windows release files
shell: bash
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/windows.zip chemical/out/release/windows-${{ matrix.arch }}${SUFFIX}.zip
mv chemical/out/release/windows-tcc.zip chemical/out/release/windows-${{ matrix.arch }}-tcc${SUFFIX}.zip
mv chemical/out/release/windows-lsp.zip chemical/out/release/windows-${{ matrix.arch }}-lsp${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-windows-${{ matrix.arch }}
path: |
chemical/out/release/windows-${{ matrix.arch }}-debug.zip
chemical/out/release/windows-${{ matrix.arch }}-tcc-debug.zip
chemical/out/release/windows-${{ matrix.arch }}-lsp-debug.zip
# ––– Get information about the latest release –––
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
# ––– we check whether it is already attached –––
- name: Check if windows-${{ matrix.arch }}.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_windows
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`windows-${{ matrix.arch }}.zip`,
`windows-${{ matrix.arch }}-tcc.zip`,
`windows-${{ matrix.arch }}-lsp.zip`
];
// Check if any of these files exist in the release assets
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
# Upload artifacts
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_windows.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/windows-${{ matrix.arch }}.zip
chemical/out/release/windows-${{ matrix.arch }}-tcc.zip
chemical/out/release/windows-${{ matrix.arch }}-lsp.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save windows-${{ matrix.arch }}.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}.zip
path: chemical/out/release/windows-${{ matrix.arch }}.zip
- name: Save windows-${{ matrix.arch }}-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}-tcc.zip
path: chemical/out/release/windows-${{ matrix.arch }}-tcc.zip
- name: Save windows-${{ matrix.arch }}-lsp.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-${{ matrix.arch }}-lsp.zip
path: chemical/out/release/windows-${{ matrix.arch }}-lsp.zip
build-windows-mingw:
name: Build on Windows MinGW (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: windows-2025
msystem: CLANG64
msys_env: clang-x86_64
llvm_asset: windows-mingw-ucrt-x64.zip
- arch: arm64
runner: windows-11-arm
msystem: CLANGARM64
msys_env: clang-aarch64
llvm_asset: windows-mingw-ucrt-arm64.zip
steps:
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
install: >-
base-devel
git
zip
unzip
mingw-w64-${{ matrix.msys_env }}-toolchain
mingw-w64-${{ matrix.msys_env }}-cmake
mingw-w64-${{ matrix.msys_env }}-ninja
- name: Download prebuilt LLVM (MinGW)
shell: msys2 {0}
run: |
url="https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/${{ matrix.llvm_asset }}"
echo "Downloading LLVM from $url"
curl -L -o llvm.zip -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$url"
unzip -o llvm.zip -d chemical
- name: Configure Chemical Project
shell: msys2 {0}
run: |
cd chemical
mkdir -p out/build
cmake -S . -B out/build -G Ninja -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} -DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON
- name: Run configure script
shell: msys2 {0}
run: |
cd chemical
./scripts/setup.sh --arch ${{ matrix.arch }} --mingw --ucrt
- name: Build Compiler Targets
shell: msys2 {0}
run: |
cd chemical
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target ChemicalLsp
- name: Run release packaging
shell: msys2 {0}
run: |
cd chemical
./scripts/release.sh --windows
- name: Rename Windows MinGW release files
shell: msys2 {0}
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/windows.zip chemical/out/release/windows-mingw-${{ matrix.arch }}${SUFFIX}.zip
mv chemical/out/release/windows-tcc.zip chemical/out/release/windows-mingw-${{ matrix.arch }}-tcc${SUFFIX}.zip
mv chemical/out/release/windows-lsp.zip chemical/out/release/windows-mingw-${{ matrix.arch }}-lsp${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-windows-mingw-${{ matrix.arch }}
path: |
chemical/out/release/windows-mingw-${{ matrix.arch }}-debug.zip
chemical/out/release/windows-mingw-${{ matrix.arch }}-tcc-debug.zip
chemical/out/release/windows-mingw-${{ matrix.arch }}-lsp-debug.zip
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
- name: Check if windows-mingw-${{ matrix.arch }}.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_windows_mingw
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`windows-mingw-${{ matrix.arch }}.zip`,
`windows-mingw-${{ matrix.arch }}-tcc.zip`,
`windows-mingw-${{ matrix.arch }}-lsp.zip`
];
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/windows-mingw-${{ matrix.arch }}.zip
chemical/out/release/windows-mingw-${{ matrix.arch }}-tcc.zip
chemical/out/release/windows-mingw-${{ matrix.arch }}-lsp.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save windows-mingw-${{ matrix.arch }}.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-${{ matrix.arch }}.zip
path: chemical/out/release/windows-mingw-${{ matrix.arch }}.zip
- name: Save windows-mingw-${{ matrix.arch }}-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-${{ matrix.arch }}-tcc.zip
path: chemical/out/release/windows-mingw-${{ matrix.arch }}-tcc.zip
- name: Save windows-mingw-${{ matrix.arch }}-lsp.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-${{ matrix.arch }}-lsp.zip
path: chemical/out/release/windows-mingw-${{ matrix.arch }}-lsp.zip
build-windows-mingw-msvcrt:
name: Build on Windows MinGW MSVCRT (x64)
runs-on: windows-2025
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
env:
LLVM_MINGW_VER: "20260224"
LLVM_MINGW_NAME: "llvm-mingw-20260224-msvcrt-x86_64"
steps:
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
- name: Set up llvm-mingw MSVCRT toolchain
shell: bash
run: |
LLVM_MINGW_NAME="llvm-mingw-${LLVM_MINGW_VER}-msvcrt-x86_64"
curl -L -o llvm-mingw.zip "https://github.com/mstorsjo/llvm-mingw/releases/download/${LLVM_MINGW_VER}/${LLVM_MINGW_NAME}.zip"
7z x llvm-mingw.zip -o"$HOME"
echo "$HOME/${LLVM_MINGW_NAME}/bin" >> $GITHUB_PATH
choco install ninja cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y
- name: Download prebuilt LLVM (MinGW MSVCRT)
shell: bash
run: |
# The logic assumes there might be a specific asset for msvcrt, but based on the user request
# "add a mingw msvc build too, these both are ucrt", I will assume we use the regular mingw asset or a specific one if available.
# The previous llvm workflow had separate steps for msvcrt.
# Assuming we use the same llvm asset logic but looking for msvcrt variant if it exists, or just reuse mingw asset.
# The user's provided LLVM workflow example doesn't explicitly show msvcrt assets being uploaded for LLVM itself in the 'Compress release assets' step
# (it shows 'windows-mingw-msvcrt-${{ matrix.arch }}.zip').
# Wait, the LLVM workflow DOES show:
# elif [ "${{ matrix.toolchain }}" == "mingw-msvcrt" ]; then
# cd chemical-bootstrap && 7z a -tzip ../windows-mingw-msvcrt-${{ matrix.arch }}.zip out\*
# So we should look for windows-mingw-msvcrt-x64.zip
url="https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/windows-mingw-msvcrt-x64.zip"
echo "Downloading LLVM from $url"
curl -L -o llvm.zip -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$url"
unzip -o llvm.zip -d chemical
- name: Configure Chemical Project
shell: bash
run: |
LLVM_MINGW_NAME="llvm-mingw-${LLVM_MINGW_VER}-msvcrt-x86_64"
export PATH="$HOME/${LLVM_MINGW_NAME}/bin:$PATH"
export CC=clang
export CXX=clang++
cd chemical
mkdir -p out/build
cmake -S . -B out/build -G Ninja -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON
- name: Run configure script
shell: bash
run: |
cd chemical
./scripts/setup.sh --arch x64 --mingw --msvcrt
- name: Build Compiler Targets
shell: bash
run: |
export PATH="$HOME/${LLVM_MINGW_NAME}/bin:$PATH"
export CC=clang
export CXX=clang++
cd chemical
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target ChemicalLsp
- name: Run release packaging
shell: bash
run: |
cd chemical
./scripts/release.sh --windows
- name: Rename Windows MinGW MSVCRT release files
shell: bash
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/windows.zip chemical/out/release/windows-mingw-msvcrt-x64${SUFFIX}.zip
mv chemical/out/release/windows-tcc.zip chemical/out/release/windows-mingw-msvcrt-x64-tcc${SUFFIX}.zip
mv chemical/out/release/windows-lsp.zip chemical/out/release/windows-mingw-msvcrt-x64-lsp${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-windows-mingw-msvcrt-x64
path: |
chemical/out/release/windows-mingw-msvcrt-x64-debug.zip
chemical/out/release/windows-mingw-msvcrt-x64-tcc-debug.zip
chemical/out/release/windows-mingw-msvcrt-x64-lsp-debug.zip
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
- name: Check if windows-mingw-msvcrt-x64.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_windows_mingw_msvcrt
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`windows-mingw-msvcrt-x64.zip`,
`windows-mingw-msvcrt-x64-tcc.zip`,
`windows-mingw-msvcrt-x64-lsp.zip`
];
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw_msvcrt.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/windows-mingw-msvcrt-x64.zip
chemical/out/release/windows-mingw-msvcrt-x64-tcc.zip
chemical/out/release/windows-mingw-msvcrt-x64-lsp.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save windows-mingw-msvcrt-x64.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw_msvcrt.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-msvcrt-x64.zip
path: chemical/out/release/windows-mingw-msvcrt-x64.zip
- name: Save windows-mingw-msvcrt-x64-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw_msvcrt.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-msvcrt-x64-tcc.zip
path: chemical/out/release/windows-mingw-msvcrt-x64-tcc.zip
- name: Save windows-mingw-msvcrt-x64-lsp.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_windows_mingw_msvcrt.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: windows-mingw-msvcrt-x64-lsp.zip
path: chemical/out/release/windows-mingw-msvcrt-x64-lsp.zip
build-linux:
name: Build on Linux (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-24.04
llvm_asset: linux-x64.tar.gz
- arch: arm64
runner: ubuntu-24.04-arm
llvm_asset: linux-arm64.tar.gz
steps:
# Checkout chemical repository
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
# ––– Install dependencies –––
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake git
- name: Download prebuilt LLVM
run: |
curl -L -o llvm.tar.gz \
-H "Authorization: token $GITHUB_TOKEN" \
"https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/${{ matrix.llvm_asset }}"
tar -xzf llvm.tar.gz -C chemical
# Configure chemical project
- name: Configure Chemical Project
working-directory: chemical
run: |
mkdir -p out/build
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} -DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON
# Configure project dependencies using configuration script
- name: Run configure script
working-directory: chemical
run: |
chmod +x ./scripts/setup.sh
./scripts/setup.sh --arch ${{ matrix.arch }}
- name: Build Compiler Targets
working-directory: chemical
run: |
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target ChemicalLsp
- name: Run release packaging
working-directory: chemical
run: |
chmod +x ./scripts/release.sh
./scripts/release.sh --linux
# rename linux release files to include the aarch
- name: Rename Linux release files
shell: bash
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/linux.zip chemical/out/release/linux-${{ matrix.arch }}${SUFFIX}.zip
mv chemical/out/release/linux-tcc.zip chemical/out/release/linux-${{ matrix.arch }}-tcc${SUFFIX}.zip
mv chemical/out/release/linux-lsp.zip chemical/out/release/linux-${{ matrix.arch }}-lsp${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-linux-${{ matrix.arch }}
path: |
chemical/out/release/linux-${{ matrix.arch }}-debug.zip
chemical/out/release/linux-${{ matrix.arch }}-tcc-debug.zip
chemical/out/release/linux-${{ matrix.arch }}-lsp-debug.zip
# ––– Get information about the latest release –––
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
# ––– Check and (conditionally) upload Linux archives –––
- name: Check if linux-${{ matrix.arch }}.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_linux
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`linux-${{ matrix.arch }}.zip`,
`linux-${{ matrix.arch }}-tcc.zip`,
`linux-${{ matrix.arch }}-lsp.zip`
];
// Check if any of these files exist in the release assets
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/linux-${{ matrix.arch }}.zip
chemical/out/release/linux-${{ matrix.arch }}-tcc.zip
chemical/out/release/linux-${{ matrix.arch }}-lsp.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save linux-${{ matrix.arch }}.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}.zip
path: chemical/out/release/linux-${{ matrix.arch }}.zip
- name: Save linux-${{ matrix.arch }}-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}-tcc.zip
path: chemical/out/release/linux-${{ matrix.arch }}-tcc.zip
- name: Save linux-${{ matrix.arch }}-lsp.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}-lsp.zip
path: chemical/out/release/linux-${{ matrix.arch }}-lsp.zip
build-alpine:
name: Build on Alpine Linux (${{ matrix.arch }})
container:
image: alpine:3.20
runs-on: ${{ matrix.runner }}
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-24.04
llvm_asset: linux-alpine-x64.tar.gz
- arch: arm64
runner: ubuntu-24.04-arm
llvm_asset: linux-alpine-arm64.tar.gz
steps:
- name: Enable JavaScript actions in Alpine containers
uses: laverdet/alpine-arm64@v1
# ––– Install dependencies –––
- name: Install dependencies
run: |
apk add --no-cache build-base cmake ninja git bash python3 curl musl-dev linux-headers zip unzip
# Checkout chemical repository
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
- name: Download prebuilt LLVM
run: |
curl -L -o llvm.tar.gz \
-H "Authorization: token $GITHUB_TOKEN" \
"https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/${{ matrix.llvm_asset }}"
tar -xzf llvm.tar.gz -C chemical
# Linux steps unchanged
- name: Configure Chemical Project
working-directory: chemical
run: |
mkdir -p out/build
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} -DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON -DCHEMICAL_MUSL=ON
# Configure project dependencies using configuration script
- name: Run configure script
working-directory: chemical
run: |
chmod +x ./scripts/setup.sh
./scripts/setup.sh --arch ${{ matrix.arch }}
- name: Build Compiler Targets
working-directory: chemical
run: |
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
- name: Run release packaging
working-directory: chemical
run: |
chmod +x ./scripts/release.sh
./scripts/release.sh --alpine
# rename linux release files to include the aarch
- name: Rename Linux Alpine release files
shell: bash
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/linux-alpine.zip chemical/out/release/linux-alpine-${{ matrix.arch }}${SUFFIX}.zip
mv chemical/out/release/linux-alpine-tcc.zip chemical/out/release/linux-alpine-${{ matrix.arch }}-tcc${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-linux-alpine-${{ matrix.arch }}
path: |
chemical/out/release/linux-alpine-${{ matrix.arch }}-debug.zip
chemical/out/release/linux-alpine-${{ matrix.arch }}-tcc-debug.zip
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
# ––– Check and (conditionally) upload Linux archives –––
- name: Check if linux-alpine-${{ matrix.arch }}.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_linux
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`linux-alpine-${{ matrix.arch }}.zip`,
`linux-alpine-${{ matrix.arch }}-tcc.zip`
];
// Check if any of these files exist in the release assets
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/linux-alpine-${{ matrix.arch }}.zip
chemical/out/release/linux-alpine-${{ matrix.arch }}-tcc.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Save linux-alpine-${{ matrix.arch }}.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: linux-alpine-${{ matrix.arch }}.zip
path: chemical/out/release/linux-alpine-${{ matrix.arch }}.zip
- name: Save linux-alpine-${{ matrix.arch }}-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_linux.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: linux-alpine-${{ matrix.arch }}-tcc.zip
path: chemical/out/release/linux-alpine-${{ matrix.arch }}-tcc.zip
build-macos:
name: Build on macOS (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
if: |
github.event_name == 'workflow_dispatch' ||
contains(github.event.head_commit.message, 'releaseIt')
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: macos-15-intel
llvm_asset: macos-x64.tar.gz
- arch: arm64
runner: macos-26
llvm_asset: macos-arm64.tar.gz
steps:
# ––– Install dependencies –––
- name: Install Dependencies
run: |
brew update
brew install cmake ninja git
- name: Checkout chemical repository
uses: actions/checkout@v5
with:
repository: chemicallang/chemical
token: ${{ secrets.GITHUB_TOKEN }}
path: chemical
submodules: recursive
- name: Download prebuilt LLVM
run: |
curl -L -o llvm.tar.gz \
-H "Authorization: token $GITHUB_TOKEN" \
"https://github.com/chemicallang/llvm-prebuilt/releases/download/${{ env.LLVM_RELEASE_TAG }}/${{ matrix.llvm_asset }}"
tar -xzf llvm.tar.gz -C chemical
# Configure chemical project
- name: Configure Chemical Project
working-directory: chemical
run: |
mkdir -p out/build
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=${{ env.BUILD_CONFIG }} -DCMAKE_CXX_STANDARD=20 -DCMAKE_CXX_STANDARD_REQUIRED=ON
# Run configure script
- name: Run configure script
working-directory: chemical
run: |
chmod +x ./scripts/setup.sh
./scripts/setup.sh --arch ${{ matrix.arch }}
# Build Compiler Targets
- name: Build Compiler Targets
working-directory: chemical
run: |
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target Compiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target TCCCompiler
cmake --build out/build --config ${{ env.BUILD_CONFIG }} --target ChemicalLsp
# Run release packaging
- name: Run release packaging
working-directory: chemical
run: |
chmod +x ./scripts/release.sh
./scripts/release.sh --macos
# Rename macOS release files to include the arch
- name: Rename macOS release files
shell: bash
run: |
# debug configs get a -debug suffix so the chemical-debug release
# (and the download scripts) can tell them apart from release zips
SUFFIX=""
if [ "${{ env.BUILD_CONFIG }}" != "Release" ]; then SUFFIX="-debug"; fi
mv chemical/out/release/macos.zip chemical/out/release/macos-${{ matrix.arch }}${SUFFIX}.zip
mv chemical/out/release/macos-tcc.zip chemical/out/release/macos-${{ matrix.arch }}-tcc${SUFFIX}.zip
mv chemical/out/release/macos-lsp.zip chemical/out/release/macos-${{ matrix.arch }}-lsp${SUFFIX}.zip
# ––– Debug config: upload zips as artifacts for the chemical-debug publish job –––
- name: Upload debug zips as artifact
if: env.BUILD_CONFIG != 'Release'
uses: actions/upload-artifact@v4
with:
name: debug-macos-${{ matrix.arch }}
path: |
chemical/out/release/macos-${{ matrix.arch }}-debug.zip
chemical/out/release/macos-${{ matrix.arch }}-tcc-debug.zip
chemical/out/release/macos-${{ matrix.arch }}-lsp-debug.zip
# Get latest release info
- name: Get latest release info
if: env.BUILD_CONFIG == 'Release'
id: get_latest_release
uses: actions/github-script@v9
with:
retries: 5
script: |
const releasesResponse = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
});
const validReleases = releasesResponse.data.filter(release => !release.draft);
if (validReleases.length === 0) {
throw new Error("No published or pre-releases found.");
}
const latestRelease = validReleases[0];
core.setOutput("tag", latestRelease.tag_name);
core.setOutput("id", latestRelease.id);
# Check and (conditionally) upload macOS archives
- name: Check if macos-${{ matrix.arch }}.zip exists in latest release
if: env.BUILD_CONFIG == 'Release'
id: check_macos
uses: actions/github-script@v9
with:
retries: 5
script: |
const tag = '${{ steps.get_latest_release.outputs.tag }}';
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
const filenames = [
`macos-${{ matrix.arch }}.zip`,
`macos-${{ matrix.arch }}-tcc.zip`,
`macos-${{ matrix.arch }}-lsp.zip`
];
const exists = release.assets.some(asset => filenames.includes(asset.name));
core.setOutput("exists", exists.toString());
# Upload artifacts
- name: Upload release artifacts
if: env.BUILD_CONFIG == 'Release' && steps.check_macos.outputs.exists == 'false'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.get_latest_release.outputs.tag }}
files: |
chemical/out/release/macos-${{ matrix.arch }}.zip
chemical/out/release/macos-${{ matrix.arch }}-tcc.zip
chemical/out/release/macos-${{ matrix.arch }}-lsp.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Save as artifact if already exists
- name: Save macos-${{ matrix.arch }}.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_macos.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}.zip
path: chemical/out/release/macos-${{ matrix.arch }}.zip
- name: Save macos-${{ matrix.arch }}-tcc.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_macos.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-tcc.zip
path: chemical/out/release/macos-${{ matrix.arch }}-tcc.zip
- name: Save macos-${{ matrix.arch }}-lsp.zip as artifact if already exists
if: env.BUILD_CONFIG == 'Release' && steps.check_macos.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: macos-${{ matrix.arch }}-lsp.zip
path: chemical/out/release/macos-${{ matrix.arch }}-lsp.zip
# ––– Deploy debug builds to chemicallang/chemical-debug –––
# When a debug config (RelWithDebInfo/Debug) is selected, the platform jobs
# upload their zips as artifacts; this job collects them, attaches the source
# commit metadata, and creates a release in the chemical-debug repo. By
# default the release is DRAFTED, tagged with the chemical commit SHA. If the
# publish_release input is checked, the release is instead tagged with the
# latest tag of chemicallang/chemical and PUBLISHED immediately. The
# Build Compiler Tests workflow (compile-tests.yml, debug mode) reads
# commit.json from that release to know which chemical commit the binaries
# were compiled from (and which test sources to use with them). Requires a
# PAT with write access to chemical-debug, stored as the
# CHEMICAL_DEBUG_TOKEN repository secret.
deploy-debug:
name: Deploy debug builds to chemical-debug
needs: [build-windows, build-windows-mingw, build-windows-mingw-msvcrt, build-linux, build-alpine, build-macos]
# NOTE: the `env` context is NOT available in job-level `if:` — use the
# inputs directly. Only a manual workflow_dispatch with a non-Release
# config deploys debug builds (push/releaseIt runs are always Release).
#
# `always()` is REQUIRED: GitHub Actions auto-skips any job whose `needs`
# contains a failed job. A single platform that fails to build (e.g. alpine
# arm64) must NOT prevent the debug builds that DID succeed from being
# published — each artifact is independent. If no debug artifacts exist at
# all (all builds failed), the download step below fails the job instead of
# publishing an empty release.
if: always() && github.event_name == 'workflow_dispatch' && inputs.config != 'Release'
runs-on: ubuntu-24.04
steps:
# Fail FAST with a clear message if the PAT for chemical-debug is missing
# — otherwise github-script dies with a cryptic "Input required and not
# supplied: github-token" AFTER all platforms already built (30+ min of
# work wasted, exactly what happened on 2026-08-11).
- name: Verify CHEMICAL_DEBUG_TOKEN is configured
run: |
if [ -z "${{ secrets.CHEMICAL_DEBUG_TOKEN }}" ]; then
echo "::error::CHEMICAL_DEBUG_TOKEN secret is not set. Create a PAT with 'repo' scope (or fine-grained 'Contents: Read and write' on chemicallang/chemical-debug) and add it as a repository secret named CHEMICAL_DEBUG_TOKEN in chemicallang/chemical."
exit 1
fi
- name: Download all debug artifacts
uses: actions/download-artifact@v4
with:
path: debug
# only the debug-* artifacts (the platform jobs also upload saved
# release zips like windows-x64.zip as artifacts — we must not
# re-upload those as duplicate assets on the debug release)
pattern: debug-*
merge-multiple: true
# if every platform build failed there is nothing to publish — fail
# loudly rather than creating an empty debug release
if-no-files-found: error
- name: Write commit metadata
uses: actions/github-script@v9
with:
retries: 5
script: |
const { data: commit } = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
});
const sha = context.sha;
require('fs').writeFileSync('commit.json', JSON.stringify({
sha: sha,
short: sha.slice(0, 12),
date: commit.commit.committer.date,
message: commit.commit.message.split('\n')[0],
url: commit.html_url
}, null, 2) + '\n');
console.log('commit.json written for ' + sha);
- name: Resolve release tag (latest chemical tag when publishing)
id: release_tag
uses: actions/github-script@v9
with:
retries: 5
script: |
const publish = !!(context.payload.inputs && context.payload.inputs.publish_release === 'true');
let tag = context.sha;
let name = 'Debug build ' + context.sha;
if (publish) {
const { data: tags } = await github.rest.repos.listTags({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
const dated = [];
for (const t of tags) {
try {
const { data: c } = await github.rest.repos.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
ref: t.commit.sha
});
dated.push({ name: t.name, date: c.commit.committer.date });
} catch (e) { /* skip tags that cannot be resolved */ }
}
dated.sort((a, b) => (a.date < b.date ? 1 : -1));
const latest = dated[0];
if (!latest) {
core.setFailed('No tags found in chemicallang/chemical. Cannot publish with the latest tag.');
return;
}
tag = latest.name;
name = 'Debug build ' + latest.name;
}
core.setOutput('tag', tag);
core.setOutput('name', name);
core.setOutput('publish', String(publish));
console.log('resolved release tag: ' + tag + ' (publish=' + publish + ')');
- name: Remove previous debug release for this tag (idempotent)
uses: actions/github-script@v9
with:
retries: 5
github-token: ${{ secrets.CHEMICAL_DEBUG_TOKEN }}
script: |
const owner = 'chemicallang';
const repo = 'chemical-debug';
const tag = '${{ steps.release_tag.outputs.tag }}';
try {
const rel = await github.rest.repos.getReleaseByTag({ owner, repo, tag });
await github.rest.repos.deleteRelease({ owner, repo, release_id: rel.data.id });
try {
await github.rest.git.deleteRef({ owner, repo, ref: 'tags/' + tag });
} catch (e) { /* tag may not exist */ }
console.log('removed previous debug release for ' + tag);
} catch (e) {
console.log('no previous debug release to remove');
}
- name: List published platforms
run: |
# surface which platforms actually made it into this (possibly
# partial) debug release — a failed platform build must not silently
# hide the ones that succeeded
echo "PUBLISHED_PLATFORMS=$(ls debug/ | sed -E 's/-debug\.zip$|-tcc-debug\.zip$//' | sort -u | paste -sd, -)" >> "$GITHUB_ENV"
- name: Publish debug builds to chemical-debug
uses: softprops/action-gh-release@v3
with:
repository: chemicallang/chemical-debug
token: ${{ secrets.CHEMICAL_DEBUG_TOKEN }}
tag_name: ${{ steps.release_tag.outputs.tag }}
name: ${{ steps.release_tag.outputs.name }}
# draft when the publish_release input is unchecked; publish otherwise
draft: ${{ steps.release_tag.outputs.publish != 'true' }}
body: |
Debug builds of [chemicallang/chemical](https://github.com/chemicallang/chemical) at commit `${{ github.sha }}`.
- Read `commit.json` for the exact source commit metadata.
- Binary names follow the release naming with a `-debug` suffix:
`<platform>-<arch>-debug.zip` (LLVM Compiler) and
`<platform>-<arch>-tcc-debug.zip` (TCCCompiler).
- Platforms published in this release: `${{ env.PUBLISHED_PLATFORMS }}`
(a platform missing from this list means that platform's build
failed — the rest are unaffected).
files: |
debug/*.zip
commit.json