Skip to content
Closed
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
143 changes: 143 additions & 0 deletions .github/workflows/build-engines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# ─────────────────────────────────────────────────────────────────────────────
# Build the engine binaries — every language, every platform — as artifacts.
#
# Reusable (workflow_call) so publish-npm.yml can build then publish in one run, and
# dispatchable on its own to check that the rules still compile everywhere without
# publishing anything. Soufflé is a BUILD-time dependency only: `souffle -g` turns each
# language's rules into portable C++ once on Ubuntu (the pinned .deb), and every platform
# compiles that C++ with its own C++17 compiler against Soufflé's headers. The result is
# one self-contained executable per language per platform, linked against nothing but the
# C++ runtime. Same flags as the local compile (no OpenMP/zlib/sqlite), portable targets.
#
# Artifacts: engines-<platform>/ holding <lang>/axiomcode-engine-<lang>[.exe] + <lang>/ENGINE_ID
# Platforms are named the npm way (process.platform-process.arch): darwin-arm64, linux-x64,
# linux-arm64, win32-x64. macOS builds on a SELF-HOSTED Apple Silicon runner (hosted macOS
# minutes bill at 10x); pass macos=false to skip it while no runner is registered.
# ─────────────────────────────────────────────────────────────────────────────
name: build-engines

on:
workflow_call:
inputs:
macos:
description: build the darwin-arm64 engines on the self-hosted macOS runner
type: boolean
default: true
workflow_dispatch:
inputs:
macos:
description: build the darwin-arm64 engines on the self-hosted macOS runner
type: boolean
default: false

env:
LANGUAGES: java typescript python javascript

jobs:
generate:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install the pinned Soufflé
run: |
. graph/pipeline/engine.conf
deb="x86_64-ubuntu-2404-souffle-${SOUFFLE_VERSION}-Linux.deb"
curl -fsSL -o "/tmp/$deb" "https://github.com/souffle-lang/souffle/releases/download/${SOUFFLE_VERSION}/$deb"
sudo apt-get update -qq && sudo apt-get install -y -qq "/tmp/$deb"
souffle --version | head -2
test -f /usr/include/souffle/CompiledSouffle.h
- name: Generate portable C++ per language
run: |
set -e
mkdir -p gen
for lang in $LANGUAGES; do
id="$(bash graph/pipeline/run-souffle.sh --language "$lang" --print-engine-id)"
echo "$lang: $id"; printf '%s' "$id" > "gen/$lang.id"
bash graph/pipeline/run-souffle.sh --language "$lang" --emit-program "gen/$lang.dl"
souffle -I graph -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; }
awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "gen/$lang.gen.log"
done
cp -r /usr/include/souffle gen/souffle
- uses: actions/upload-artifact@v4
with: { name: generated, path: gen, retention-days: 3, if-no-files-found: error }

build:
needs: generate
strategy:
fail-fast: false
matrix:
target:
- { os: ubuntu-24.04, platform: linux-x64 }
- { os: ubuntu-24.04-arm, platform: linux-arm64 }
- { os: windows-2025, platform: win32-x64 }
runs-on: ${{ matrix.target.os }}
steps:
- uses: actions/download-artifact@v4
with: { name: generated, path: gen }
- name: Compile every language (Linux)
if: startsWith(matrix.target.platform, 'linux')
run: |
set -e
for lang in $LANGUAGES; do
mkdir -p "engines/$lang"
c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
cp "gen/$lang.id" "engines/$lang/ENGINE_ID"
done
ls -la engines/*; ldd engines/java/axiomcode-engine-java || true
- uses: ilammy/msvc-dev-cmd@v1
if: startsWith(matrix.target.platform, 'win32')
with: { arch: x64 }
- name: Compile every language (Windows, MSVC)
if: startsWith(matrix.target.platform, 'win32')
shell: cmd
run: |
for %%L in (java typescript python javascript) do (
mkdir engines\%%L
cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /I gen gen\%%L.cpp /Fe:engines\%%L\axiomcode-engine-%%L.exe
if errorlevel 1 exit /b 1
copy gen\%%L.id engines\%%L\ENGINE_ID
)
dir /s engines
- name: Smoke — every binary starts on empty inputs
shell: bash
run: |
set -e
for lang in $LANGUAGES; do
bin="$(ls engines/$lang/axiomcode-engine-$lang* )"; chmod +x "$bin" 2>/dev/null || true
mkdir -p "facts-$lang" "out-$lang"
sed -n 's/^\.input \([A-Za-z0-9_]*\)(.*/\1/p' "gen/$lang.dl" | while read -r r; do : > "facts-$lang/$r.facts"; done
"./$bin" -F "facts-$lang" -D "out-$lang"
echo "$lang: ok ($(ls out-$lang | wc -l) relations written)"
done
- uses: actions/upload-artifact@v4
with:
name: engines-${{ matrix.target.platform }}
path: engines
if-no-files-found: error

build-macos:
needs: generate
if: inputs.macos
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/download-artifact@v4
with: { name: generated, path: gen }
- name: Compile every language (macOS arm64)
run: |
set -e
for lang in $LANGUAGES; do
mkdir -p "engines/$lang"
c++ -std=c++17 -O3 -w -arch arm64 -mmacosx-version-min=12.0 -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
cp "gen/$lang.id" "engines/$lang/ENGINE_ID"
done
otool -L engines/java/axiomcode-engine-java
- name: Smoke — every binary starts on empty inputs
run: |
set -e
for lang in $LANGUAGES; do
mkdir -p "facts-$lang" "out-$lang"
sed -n 's/^\.input \([A-Za-z0-9_]*\)(.*/\1/p' "gen/$lang.dl" | while read -r r; do : > "facts-$lang/$r.facts"; done
"./engines/$lang/axiomcode-engine-$lang" -F "facts-$lang" -D "out-$lang"
done
- uses: actions/upload-artifact@v4
with: { name: engines-darwin-arm64, path: engines, if-no-files-found: error }
76 changes: 76 additions & 0 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ─────────────────────────────────────────────────────────────────────────────
# Publish the engine packages to npm: @axiomcode/engine-<os>-<cpu>, one per platform, each
# holding every language's engine for that platform. This package lists them as optional
# dependencies, so `npm install` fetches exactly the one for the machine — no Soufflé, no
# compiler, no download of ours.
#
# Runs ONLY when asked:
# • manually (Actions → publish-npm → Run workflow): version, dry_run (default TRUE — packs
# and prints exactly what would be published, uploads nothing), macos.
# • on a version tag `v1.2.3`: a real publish of that version.
# Needs the NPM_TOKEN secret (an npmjs automation token with publish rights on @axiomcode)
# for a real publish; a dry run needs nothing.
# ─────────────────────────────────────────────────────────────────────────────
name: publish-npm

on:
workflow_dispatch:
inputs:
version:
description: version to publish (e.g. 0.1.0)
required: true
type: string
dry_run:
description: pack and print, publish nothing
type: boolean
default: true
macos:
description: include darwin-arm64 (needs the self-hosted macOS runner)
type: boolean
default: true
push:
tags: ['v*']

permissions:
contents: read
id-token: write

jobs:
engines:
uses: ./.github/workflows/build-engines.yml
with:
macos: ${{ github.event_name == 'push' || inputs.macos }}

publish:
needs: engines
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- uses: actions/download-artifact@v4
with: { pattern: engines-*, path: artifacts }
- name: Assemble one package per platform
run: |
set -e
if [ "${{ github.event_name }}" = push ]; then version="${GITHUB_REF_NAME#v}"; else version="${{ inputs.version }}"; fi
echo "version=$version" >> "$GITHUB_ENV"
for d in artifacts/engines-*; do
platform="${d#artifacts/engines-}"
bash packaging/assemble-engine-package.sh "$platform" "$version" "$d" "packages/engine-$platform"
cat "packages/engine-$platform/package.json"
done
- name: Publish (or dry-run)
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
DRY: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
run: |
set -e
flag=""; [ "$DRY" = true ] && flag="--dry-run"
for p in packages/engine-*; do
echo "══ $p $flag"
( cd "$p" && npm publish --access public $flag )
done
[ "$DRY" = true ] && echo "DRY RUN — nothing was uploaded. Re-run with dry_run=false, or push tag v$version, to publish." || echo "published version $version"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

# ── Dependencies ─────────────────────────────────────────────────────────────
node_modules/
# and the bare name: `node_modules/` matches a directory only, so a symlink named node_modules slips past it
node_modules
jspm_packages/
web_modules/
.pnp
Expand Down
3 changes: 2 additions & 1 deletion bin/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
# bin/axiomcode engine --language L --client-ir <ir-dir>/<lang> --out <dir> [options]
# bin/axiomcode test [java|typescript|python|javascript|parser|all] [suite options]
#
# Requires Node ≥ 22.5; no Soufflé or compiler (binaries/, or a local souffle if present).
# Requires Node ≥ 22.5; no Soufflé or compiler (the engine comes from npm as @axiomcode/engine-<os>-<cpu>,
# or is compiled locally when souffle is present).
# ─────────────────────────────────────────────────────────────────────────────
set -eu
ROOT="$(d="$(cd "$(dirname "$0")" && pwd)"; while [ "$d" != / ] && { [ ! -f "$d/package.json" ] || [ ! -d "$d/graph" ]; }; do d="$(dirname "$d")"; done; echo "$d")"
Expand Down
15 changes: 15 additions & 0 deletions graph/pipeline/engine.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ─────────────────────────────────────────────────────────────────────────────
# The prebuilt-engine contract. Sourced by run-souffle.sh and by the CI workflows.
#
# SOUFFLE_VERSION is PINNED here, not read from a `souffle --version`, because the machine
# that runs a prebuilt binary has no souffle to ask — and the engine id it computes must be
# the id CI computed. Bumping it changes every language's id, which is what a new code
# generator should do. CI installs exactly this version.
#
# ENGINE_PACKAGE_SCOPE is the npm scope the engine packages are published under:
# <scope>/engine-<os>-<cpu> (darwin-arm64, linux-x64, linux-arm64, win32-x64), each holding
# every language's engine for that platform under <lang>/ with its ENGINE_ID. This package
# lists them as optionalDependencies, so `npm install` fetches the one for the machine.
# ─────────────────────────────────────────────────────────────────────────────
SOUFFLE_VERSION="2.5"
ENGINE_PACKAGE_SCOPE="@axiomcode"
10 changes: 10 additions & 0 deletions graph/pipeline/portable-stat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,13 @@ sha1_stdin(){
[ -n "$_SHA1_CMD" ] || { echo "neither shasum nor sha1sum is on PATH" >&2; return 1; }
"$_SHA1_CMD" | cut -d' ' -f1
}

# sha256 of stdin, for the engine id. Same three spellings as sha1 above: `shasum -a 256`
# (macOS, perl shasum in Git Bash) or `sha256sum` (coreutils).
if command -v sha256sum >/dev/null 2>&1; then _SHA256_CMD="sha256sum"
elif command -v shasum >/dev/null 2>&1; then _SHA256_CMD="shasum -a 256"
else _SHA256_CMD=""; fi
sha256_stdin(){
[ -n "$_SHA256_CMD" ] || { echo "neither sha256sum nor shasum is on PATH" >&2; return 1; }
$_SHA256_CMD | cut -d' ' -f1
}
Loading