Skip to content

build: engines ship on npm as @axiomcode/engine-<os>-<cpu>; publish is a dispatch-only workflow (dry run by default) - #478

Merged
swapnilpaliwal-sd merged 6 commits into
mainfrom
npm-engines
Sep 18, 2026
Merged

swapnilpaliwal-sd merged 6 commits into
mainfrom
npm-engines

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #454. Supersedes #455 (binaries committed to the repository), engines ship on npm instead; nothing is committed.

How it works

  • build-engines.yml (reusable + dispatchable): souffle -g once on Ubuntu with the pinned Soufflé → portable C++ per language → compiled on linux-x64, linux-arm64, win32-x64 (MSVC) and a self-hosted darwin-arm64, smoke-run on empty inputs, uploaded as engines-<platform> artifacts. Platform names are npm's (process.platform-process.arch).
  • publish-npm.yml, runs only on demand (version, dry_run = true by default, macos) or on a v* tag. Assembles @axiomcode/engine-<os>-<cpu> per platform (every language's engine under <lang>/ with its ENGINE_ID, license, readme, os/cpu fields) and runs npm publish --access public, with --dry-run it packs and prints without uploading, so the pipeline can be exercised before any token exists.
  • package.json lists the four as optionalDependencies: npm installs exactly the one whose os/cpu match the machine, skips the rest. Verified npm install succeeds today with them unpublished (optional).
  • run-souffle.sh resolves the engine: (1) node_modules/@axiomcode/engine-<platform>/<lang>/ if its ENGINE_ID equals the checkout's rule hash; (2) local compile when souffle is present; else an error naming both ways out. The program the id hashes is a pure function of the repository (--print-engine-id, --emit-program); the Soufflé pin is graph/pipeline/engine.conf.

To publish (one-time setup, then one the CLI-argument library)

  1. npmjs: create the axiomcode org; a granular token with publish on @axiomcode/* → repo secret NPM_TOKEN.
  2. gh workflow run publish-npm.yml -f version=0.1.0 -f dry_run=true -f macos=false → inspect; then dry_run=false (or git tag v0.1.0 && git push --tags).
  3. After a rule change that should ship: publish a new version and bump the four pins in package.json.

Verified

  • Java 39/39, TypeScript 53/53, Python 15/15 on this branch (from a fresh npm install).
  • Preflights (Java suite, no souffle/network): engine-id-test.sh (same id from two paths; changed by rule/map/manifest/pin) and engine-package-test.sh (a hand-made engine package is used when its id matches, refused with both ways out when not, absence explained).
  • bin/axiomcode <mixed Java+TS+Python tree> <out> --library … → three graphs, Java library targets named.
  • Package assembly + npm publish --dry-run locally: correct tarball contents.
  • The four-platform compile itself was validated by build: prebuilt engine binaries, CI builds Linux/macOS/Windows on every merge and commits them under engine/binaries/ #455's CI runs (same steps).

…s a dispatch-only workflow (dry run by default)

Running the engine needed Soufflé and a C++ toolchain. The compiled engine is one
self-contained executable per language and platform, so CI builds them (build-engines.yml:
generate the portable C++ once on Ubuntu with the pinned Soufflé, compile on linux-x64,
linux-arm64, win32-x64 and a self-hosted darwin-arm64, smoke-run each) and publish-npm.yml
assembles one package per platform — @axiomcode/engine-<os>-<cpu>, every language's engine
under <lang>/ with its ENGINE_ID — and publishes them. The workflow runs only on demand
(version, dry_run=true by default, macos) or on a v* tag; a dry run packs and prints
without uploading.

This package lists the four as optionalDependencies, so `npm install` fetches exactly the
one npm's os/cpu filter matches; nothing is committed to git. run-souffle.sh resolves the
engine in order: the installed package when its ENGINE_ID equals the checkout's rule hash
(edited rules never run a stale binary), else a local compile when souffle is present, else
an error naming both ways out. The program the id hashes is a pure function of the
repository (relative includes, inputs derived from the maps), exposed as --print-engine-id
and --emit-program; the pinned Soufflé version lives in graph/pipeline/engine.conf.

Guards, without souffle or network, as Java-suite preflights: engine-id-test.sh (same id
from two paths; changed by a rule, a map, the manifest, the pin) and engine-package-test.sh
(a hand-made engine package: used when its id matches, refused with both ways out when not,
absence explained).
swapnilpaliwal-sd and others added 2 commits September 14, 2026 02:03
…e — LANGUAGES gains javascript in the generate, Linux, Windows and smoke steps, and in the engine-id preflight

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Merged into engine-prebuilt (#455) as its base, at the request of the repository owner: the npm mechanism replaces the committed-binaries workflow, the release fetch test and the binaries .gitattributes there (all superseded by build-engines.yml, publish-npm.yml and engine-package-test.sh). javascript is in the build matrix (36d0725d). Retargeted to engine-prebuilt; nothing here merges to main before the C# engine.

swapnilpaliwal-sd added a commit that referenced this pull request Sep 14, 2026
…he committed-binaries workflow, the release fetch test and the binaries .gitattributes are superseded by build-engines.yml, publish-npm.yml and engine-package-test.sh
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Closing without merge: this branch is fully contained in engine-prebuilt (#455, merge aac70d76), which is the base for the CI gate (#418) and lands after the C# engine. Nothing from here goes to main on its own.

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Reopened: the engine build and npm publish stays its own open pull request until it merges to main. The same commits were also merged into #418's branch; whichever merges first carries the work, the other rebases.

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Not mergeable as it stands: package.json conflicts with main at 45fa172. Rebase and it can be re-checked. I have not touched the branch.

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Cross-checking this branch against a real install, on GCP rather than Actions. Results and two gaps.

Verified working. Built all engines from this branch for five platforms and ran them end to end. The engine id is path independent (same tree at two paths gives the same id), the generated C++ carries no build-machine paths, and every platform's ENGINE_ID matches the generate step. Smoke counts agree across three operating systems and two architectures: java 22, typescript 45, python 85, javascript 34 relations. engine-id-test.sh and engine-package-test.sh both pass, and a staged node_modules/@axiomcode/engine-darwin-arm64 resolves and runs with souffle absent, in 1s against 849s for a local compile. A corrupted ENGINE_ID is refused, as intended.

Gap 1: darwin-x64 is not built. The matrix covers darwin-arm64, linux-x64, linux-arm64 and win32-x64, so an Intel Mac gets no engine and falls back to needing souffle and a compiler. A missing optional dependency is not an install error, so it fails silently. This needs no Intel hardware and no extra runner: the Apple Silicon runner cross compiles with -arch x86_64 and Rosetta 2 runs the result, so the same job can build and smoke both. Measured here: the x86_64 java engine writes the same 22 relations and links only /usr/lib.

Gap 2: the build depends on Actions minutes. With none available this branch cannot produce a binary at all, which blocks a release for a reason unrelated to the tool. Worth a documented path that does not need runners. The shape of this design makes that cheap, since souffle -g runs once and every platform then only needs a C++17 compiler: the build VMs never install souffle, and Windows never needs a souffle that does not exist for it natively.

Notes from doing it:

  • GCP cannot build macOS at all. Compute Engine has no macOS machine type or image, and Apple's licence permits macOS virtualisation only on Apple hardware. Both Mac engines have to come from a Mac.
  • Ubuntu 20.04 is no longer published by ubuntu-os-cloud. 22.04 is the oldest available, so glibc 2.35 is the floor.
  • On Windows Server, Invoke-WebRequest without -UseBasicParsing throws, because the IE engine has never completed first launch configuration. It cost a completed build its upload once, after all four engines had compiled and smoke tested.

#887 is required before this ships. It fixes four defects in front of the engine: the built parser is not in the tarball, there is no bin entry, the bin root walk fails through the node_modules/.bin symlink, and no runtime dependencies are declared. With those in place the engine resolves correctly from the package and the run still fails, because the parser half is not installable. On a clean VM with both changes, all four languages complete against real projects with nothing compiled.

# Conflicts:
#	graph/pipeline/run-souffle.sh
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Resolved the conflict on this branch locally and did not merge, because the merge result
regresses the suite. Two separate findings.

1. The conflict hid a real drop. graph/pipeline/run-souffle.sh conflicted because
this branch extracts the program generation into write_program() while main added a
phase folder inside that same block. Taking either side wholesale is wrong:

write_program(): for d in projections containment resolution config-resolution \
                        expression-resolution call-edge-generation
main:            for d in ... call-edge-generation framework-behavior

framework-behavior/ is where persistence.dl and destinations.dl live. Resolving in
this branch's favour, which is what a careful merge looks like since the refactor is the
point of the PR, silently stops including them: no error, no conflict, the rules simply
never reach the program. Worth adding framework-behavior to write_program() on this
branch now so the next person to merge cannot get it wrong.

2. With that resolved and built, the suite regresses. On the merge result the bundle
stage fails for every language before any case runs:

✗ java: bundle failed:   SyntaxError: Unexpected token '{'
    at Loader.moduleStrategy (internal/modules/esm/translators.js:141:18)
✗ typescript: bundle failed:  (same)
engine-id: 8 failure(s)
aborting: the engine id is not a function of the rules alone

internal/modules/esm/translators.js is a Node 12/14 path; this machine has v14.15.0 at
/usr/local/bin/node and v22.23.2 on PATH, so something in this branch is reaching the
older interpreter where main does not.

Control: current main, same shell, same PATH, same Node v22.23.2, same JDK: 56
passed, 0 failed
. So this is not the environment and not a pre-existing failure.

Also worth noting for validation: all four optionalDependencies resolve to
UNMET OPTIONAL DEPENDENCY @axiomcode/engine-*@0.1.0, so the packaging path this PR
exists to establish cannot actually be exercised until those are published. That is not
itself a blocker for optional deps, but it does mean the interesting half is untested.

I have not pushed anything to this branch. The conflict resolution above is the part I am
confident about; the interpreter question belongs to whoever owns the packaging design.

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Correcting my comment above and clearing this for merge. Pushed 2a7a31b7.

I was wrong about the bundle. I reported the bundle stage failing for every language
with a Node 12/14 ESM syntax error. That was my environment: /usr/local/bin/node is
v14.15.0 on this machine and had won the PATH race in that particular shell. Re-run with
v22.23.2 confirmed, the bundle is ok (3 languages, ...). Nothing in this PR touches it.
I should have run the control before posting, and did not.

The engine-id and engine-package failures were real, and are fixed here. Both
sandboxes rebuilt PATH from a whitelist of symlinked tools. That cannot work on macOS:
/usr/bin/shasum is a perl script and the system perl dispatches on the script's
CANONICAL path, so a symlink to it is refused, and so is a copy:

$ echo hi | shasum -a 256                 # works
98ea6e4f...
$ echo hi | PATH=$TMP/bin shasum -a 256   # symlink or copy, either way
perl version 5.30.3 can't run /var/folders/.../bin/shasum.  Try the alternative(s):
(Error: no alternatives found)

There is no sha256sum on a macOS box without coreutils, so the sandbox had no digest
tool at all. engine-id reported 8 failures and engine-package 4, none about the
engine. Both now hide souffle by dropping the one PATH entry that holds it, and assert
souffle is really gone; every other tool stays where the system expects it.

Verified with souffle present, which is the case that matters until the packages are
published.

java suite 56 passed, 0 failed (engine-id ok, engine-package ok)
cold run, empty engine cache compiling souffle program (cache miss), 3m19s, 486 call edges
warm run, same engine id 3.0s, 486 call edges, identical
engine id stable across both, rules + souffle 2.5

The cache lands souffle-engine-java-<engine-id>, so the compile happens once and the id
is a function of the rules and the souffle version as intended.

I also carried framework-behavior into write_program(). That was the conflict's real
content: this branch moved the phase list into the function while main added the folder
to it, so resolving in this branch's favour without noticing would have silently stopped
including persistence.dl and destinations.dl.

Understood that the UNMET OPTIONAL DEPENDENCY lines are expected until the engine
packages are published, so I am no longer treating that as a blocker.

@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit f6ba1c2 into main Sep 18, 2026
Both engine sandboxes rebuilt PATH from a whitelist of symlinked tools. That cannot
work on macOS: /usr/bin/shasum is a perl script and the system perl dispatches on
the script's canonical path, so a symlink to it, or a copy of it, is refused with
'perl version 5.30.3 cannot run <path>'. With no sha256sum on a machine without
coreutils, the sandbox had no digest tool at all: engine-id reported 8 failures and
engine-package 4, none of them about the engine.

Dropping the one directory that holds souffle hides it and leaves every other tool
where the system expects to find it. The sandbox asserts souffle is really gone.
@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor Author

Blocker for this branch, filed as #895 with the fix in #897 against main.

The engine id is a function of the shell locale as well as the rules. The #include lines are built from globs, and bash orders a glob by LC_COLLATE: under a UTF-8 collation call-site.dl and callee-resolution.dl swap places against their byte order, so the program text and the id change with the environment.

Measured with engines built from this branch, installed as a real package:

language macOS Windows (MSYS2, en_US.UTF-8)
java 16a17ec0 d79f318c
typescript e8df2d7c e8df2d7c
python 934aa018 1302327180
javascript c135763f c135763f

End to end on Windows with the correct engine installed, java and python fail with engine-win32-x64 holds java at 16a17ec0..., these rules are d79f318c... - not using it, while typescript and javascript pass. The refusal is this branch's id check working correctly on wrong inputs. TypeScript and JavaScript agree only because no pair of their rule filenames collides.

CI runs under a C-ish locale and users commonly do not, so published engines would be refused for java and python on a large share of machines, with an error naming the rules rather than the locale. #897 pins the collation in the executor; write_program here needs the same guard, and engine-id-test.sh should assert invariance under at least two collations. Note it is invisible on macOS, whose collation matches C either way.

Separately, linux-arm64 cannot install at all, unrelated to the engine: the 0.21.x tree-sitter generation ships no linux-arm64 prebuilds, so npm falls back to node-gyp. tree-sitter-groovy also pins tree-sitter-java@0.23.4 exactly against the parser's ^0.21.0, forcing a nested second copy whose gyp config miscounts its relative path and fails even with a full toolchain. Prebuild coverage:

package linux-arm64
tree-sitter@0.21.1 no
tree-sitter@0.22.4 yes
tree-sitter-java@0.21.0 no
tree-sitter-java@0.23.4 yes
tree-sitter-python@0.21.0 no
tree-sitter-python@0.23.6 yes

That one is a parser dependency upgrade and needs the suites run against the newer grammars, so it is not folded into either PR.

swapnilpaliwal-sd added a commit that referenced this pull request Sep 18, 2026
…478) (#903)

Reverts f6ba1c2. The engine binaries work is not ready to ship, so it goes back
to a pull request and #454 reopens with it.

Not a plain revert, because two changes landed on top of it and both have to keep
working:

- #897 pinned the collation inside write_program, which #478 introduced. Reverting
  removes that function and would take the guard with it, and the restored executor
  has the same defect: its #include lines come from unguarded globs, so the cache
  key follows the user's locale. The guard is re-applied to the restored program
  generation, and engine-id-locale-test.sh now accepts either shape of the
  executor so it keeps measuring rather than failing for the wrong reason. Its
  --emit-program checks skip here, since that flag belongs to #478, and they report
  the skip rather than passing silently.

- #887 added bin, files and dependencies, which are independent of the engine work
  and stay. optionalDependencies goes with the feature: nothing resolves a packaged
  engine any more, so those entries would name packages the executor never looks for.

What comes back: the executor compiles with a local souffle and caches the binary,
exactly as before #478. engine.conf, packaging/ and the two engine tests go with it.
swapnilpaliwal-sd added a commit that referenced this pull request Sep 24, 2026
Graphify was labelled "Slug-graph" in the figure alt text and in the comparison
tables — the same label the two SVGs carried. Every tool now reads the way the
other figures read it: AxiomCode, GitNexus, CodeGraph, Graphify,
Code-Review-Graph. Package and marketplace ids are untouched: axiomcodegraph,
@axiomcode/code-graph and @colbymchenry/codegraph are ids, not names.

The requirements line led with Soufflé and cited #478 as the reason, but #478 is
merged — the machinery it added is a dispatch-only publish workflow that has not
been run for real, so a reader was pointed at finished work as though it were the
blocker. It now says the intended thing first: npm install fetches
@axiomcode/engine-<os>-<cpu> for the platform and nothing else is needed to run.
Soufflé and a C++ compiler are the temporary fallback while those packages are
absent from npm, which they still are.

The test-selection alt text also still described tests-selected-per-bug; that
panel reports F1 now.
swapnilpaliwal-sd added a commit that referenced this pull request Sep 24, 2026
#1258 dropped both when it restructured the header, leaving Build, License and
Node. They are status, not decoration: engines-not-yet-published is the one line
that tells a reader why the Requirements paragraph still asks for Soufflé, and
nightly-not-yet-enabled says the scheduled run is not there rather than passing.

Restored as they were, static shields rather than workflow badges — which is
deliberate, per the commit that made the nightly badge render by not asking for a
run that has not happened.

Worth a look before release: the engines badge points at #478, which is merged.
The machinery landed; the publish has not run. #418 is where the CI arrives, and
is the honest target for both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build, packaging and developer setup enhancement New feature or request platform OS / toolchain portability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prebuilt engine binaries: build in CI for Linux/macOS/Windows on merge, fetch from the script, no Soufflé or compiler needed to run

1 participant