./flixw pins one Flix compiler version in your repository and runs it — no Flix
installation, no fork, no patched build. A collaborator with nothing but a JDK clones and
compiles with the same bytes you did.
What the digest does and does not prove. It is recorded from whatever the first
pindownloaded, so it guarantees everyone runs the same bytes — not that those bytes are the ones the Flix project built. Flix publishes no signatures.docs/LIMITATIONS.mdstates the difference plainly and is worth reading before trusting this with a download. This is an experimental, third-party, opt-in tool, not affiliated with or endorsed by the Flix project.
Install the committed wrapper, pin one Flix compiler version, commit the lock, then use
./flixw where you would have used flix. The output below is from real runs; version
strings, digests and paths will differ on your machine.
wrapper what flixw writes into your repo — two shims, stage 0, and a
.gitignore· pin the act of choosing one exact compiler release · lock.flixw/lock.toml, which records that choice and its SHA-256 · cache a machine-wide directory outside your project where the compiler JAR and JDKs are kept
curl -fsSLO https://github.com/wstein/flixw/releases/download/v0.25.11/flixw-setup.javasha256sum flixw-setup.java # macOS: shasum -a 256 flixw-setup.javaIt must print exactly this, and if it does not, stop:
827409b8d53501b449910f5eae294820c08e34a998f82996ef6b80c0b61af430 flixw-setup.java
The digest comes from this page, not from the download. That is the whole point of the
step. Taking it from the release's own SHA256SUMS would prove only that the file arrived
intact — a tampered release would carry a matching SHA256SUMS to go with it. This page is
a different artifact with its own history, so it is a second opinion rather than an echo,
and tests/lint.sh fails if it ever stops matching what a release actually publishes.
sha256sum is coreutils and busybox; shasum -a 256 is what stock macOS has. Neither is on
every machine, which is why both are given.
The same as one command, for scripts and CI
Comparing by eye is fine for a one-off. A pipeline wants an exit status:
echo "827409b8d53501b449910f5eae294820c08e34a998f82996ef6b80c0b61af430 flixw-setup.java" \ | sha256sum -c - # macOS: shasum -a 256 -c -It prints flixw-setup.java: OK and exits non-zero on a mismatch.
Verifying against the release's own manifest instead is one line shorter and one guarantee weaker — it catches a corrupted or intercepted download, not a replaced release:
curl -fsSLO https://github.com/wstein/flixw/releases/download/v0.25.11/SHA256SUMSsha256sum --ignore-missing -c SHA256SUMS # macOS: shasum -a 256 --ignore-missing -c SHA256SUMSThe same, in cmd.exe
certutil is the built-in — cmd has no Get-FileHash, and curl.exe ships with
Windows 10 and later:
curl -fsSLO https://github.com/wstein/flixw/releases/download/v0.25.11/flixw-setup.java
certutil -hashfile flixw-setup.java SHA256
java .\flixw-setup.java
del flixw-setup.javacertutil prints the digest on a line of its own between two lines of chatter. Older
Windows builds space the bytes in pairs; compare it without the spaces.
Then read .\flixw.cmd wherever the five steps write ./flixw: .\flixw.cmd pin 0.75.3,
.\flixw.cmd check, .\flixw.cmd validate. setup writes both shims on every platform,
so the POSIX flixw is there too and is what Git Bash and WSL use.
The same, in PowerShell
Invoke-WebRequest-OutFile flixw-setup.java `
https://github.com/wstein/flixw/releases/download/v0.25.11/flixw-setup.java
(Get-FileHash-Algorithm SHA256 flixw-setup.java).Hash.ToLower()
# compare with the digest printed above before running the next line
java .\flixw-setup.java
Remove-Item flixw-setup.javaThen read .\flixw.cmd wherever the five steps write ./flixw: .\flixw.cmd pin 0.75.3,
.\flixw.cmd check, .\flixw.cmd validate. Git Bash and WSL run the POSIX shim, so
there the commands work as written.
Network happens in three places, and nowhere else.setup downloads the wrapper code
and verifies it; pin downloads the compiler and verifies it; the JDK and completion
helpers download only when you ask for them by name. An ordinary ./flixw check on a
pinned project touches the network not at all.
java ./flixw-setup.java # here, newest Flixjava ./flixw-setup.java --pin 0.75.3 # here, a version you choosejava ./flixw-setup.java myproject # a directory, creating it if neededrm flixw-setup.javaThat is the whole adoption: it installs the wrapper and pins a compiler, taking the
version --pin names or the newest Flix release if you name none. Steps 3 to 5 below
explain what it did and what to commit.
Afterwards the same thing is one word, because setup leaves a launcher on your machine:
flixw setup [<dir>] [--pin <version>]That is the only flixw command that works with no project around it — it is the one that
creates one. Everything else delegates to the ./flixw checked into the project you are
standing in.
In your project it writes the wrapper files and nothing else — it merges its block into an
existing .gitattributes rather than replacing it, and never touches flix.toml or
.flixw/lock.toml. It does reach the network once, to fetch and digest-verify this
release's wrapper code into a cache outside your project. It also writes a small launcher
to <cache>/bin/flixw (shown by ./flixw info): add that directory to your PATH if you
want flixw info to delegate to the current project's checked-in ./flixw. The launcher
never selects a compiler or supplies a lock, and reports an error outside a project tree.
Its last line tells you which of two situations you are in.
| already pinned | first adoption |
|---|---|
Nothing further to run. | The project has no lock yet. Continue with step 3. |
flixw: pinned Flix 0.75.3 from flix/flix (bf123cdb6494d6e0...)Run ./flixw pin <version> yourself to move to another compiler, or if step 2 could not
reach GitHub to find the newest one -- it says so and leaves the project usable rather than
failing the install.
This is the trust root: it fetches that exact release, hashes it, and records the digest in
.flixw/lock.toml. Every later run re-checks it. flix.toml is untouched — that file is
Flix's, and pin has no business editing it.
An empty directory needs ./flixw init after this, which is a compiler verb: Flix
scaffolds the project around the wrapper.
$ ./flixw check
$ ./flixw testPassed: 1, Failed: 0. Skipped: 0. Elapsed: 3.4ms.An ordinary verb goes straight to the pinned compiler. pin, info, doctor, validate
and help are flixw's own, because a project needs them before it can reach a compiler at
all.
git add flixw flixw.cmd .flixw .gitattributesWhat gets committed:flixw, flixw.cmd, .flixw/flixw.java, .flixw/.gitignore,
.flixw/lock.toml, and the flixw block merged into your .gitattributes.
Never.flixw/local/ — that is this machine's resolved JDK and belongs to no one else.
A collaborator with nothing but a JDK now gets the compiler you have.
Those releases upgrade by running flixw.java install, which no longer exists, so
./flixw wrapper --upgrade fails with FLIXW001 and changes nothing at all. The five
steps above are the way across, and they are the same steps: your lock.toml and compiler
pin survive, so step 2 will tell you the pin is untouched and there is nothing else to do.
docs/LIMITATIONS.md has the full failure text.
flixw-setup.java is a program you run directly, not a flixw verb — stage 0 has no
install at all. That is deliberate: install is a name Flix could claim for a project's
dependencies, so ./flixw install reaches the compiler, like every other word flixw does
not own.
The .flixw/flixw.java in your project is the documented source with its comments removed
— 3256 lines rather than 4651 — with a header pointing at
the docs and the source
for the reasoning behind every check. The strip is reproducible from the tagged source, so
the file you can read and the file you run can be compared rather than taken on trust.
| symptom | what it means | do this |
|---|---|---|
| the shim prints install instructions and stops | no java on PATH at all | install any JDK 16+; flixw cannot bootstrap itself |
FLIXW003: no Java in [21, …] | flixw runs, the compiler cannot | ./flixw wrapper --install-jdk, or install a JDK 21+ |
FLIXW002: no .flixw/lock.toml | the project has never pinned | ./flixw pin <version> |
FLIXW005: cannot reach … on a first pin | no network, and nothing cached yet | pin once online; afterwards the cache serves it |
FLIXW006: digest mismatch | the bytes are not what the lock pins | do not override it — re-pin, or find out why they differ |
Two versions of "you need a JDK" are true at once. The pinned compiler needs Java 21+;
stage 0 itself compiles on Java 16. So with anything from 16 up, flixw runs, tells you the
compiler will not, and ./flixw wrapper --install-jdk fetches a verified Temurin 21 into
its own cache rather than touching your system.
Below 16, and with no Java at all, that command cannot help: it is a Java program and there is nothing to run it. You get the shim's own message naming the install command for your platform instead. First contact needs a JDK you installed yourself.
From here an ordinary verb is the stock compiler, run by the wrapper — pin, info,
doctor, validate and help are flixw's, because a project needs them before it can
reach a compiler at all:
$ ./flixw testPassed: 1, Failed: 0. Skipped: 0. Elapsed: 3.4ms.
$ ./flixw validateok ./flixw matches flixw 0.25.3ok ./flixw.cmd matches flixw 0.25.3ok .flixw/flixw.java sha256=c41d7b3eec8f91ce...ok the lock satisfies flix.tomlok the compiler reports the version the lock pinsThe JDK can be pinned too, in the same file and for the same reason — a version, not a path, since a path is true on one machine only:
$ ./flixw pin --java 21flixw: pinned java 21Any vendor's JDK satisfies it, 21.0.12 pins harder than 21, and a machine without a
match is told so before anything is downloaded. Leave it out and flixw picks the newest
tested JDK it can find, which is what it has always done.
./flixw validate prints the digest of the stage 0 in your project, so it can be compared
against the release you meant to install.
Nine committed files and one that stays on your machine. flixw writes five of the nine and
shares a sixth; all of them are committed on purpose, so a clone needs no bootstrap step of its own and no flix on PATH. (init
also scaffolds a README.md, a LICENSE.md, a .gitignore and a CI workflow, which are
yours to keep or delete.)
hello/
├── flixw the wrapper you actually run; a POSIX sh shim
├── flixw.cmd the same, for cmd.exe and PowerShell
├── .flixw/
│ ├── flixw.java stage 0: the bootstrap, one dependency-free Java file
│ ├── .gitignore keeps local/ out of git
│ ├── lock.toml the pin — repository, version, URL, SHA-256
│ └── local/java the JDK this machine resolved to — not committed
├── .gitattributes line endings for the five above, as a block in your file
├── flix.toml your project: name, dependencies, the minimum Flix
├── src/Main.flix your code
└── test/TestMain.flix your tests
flixw, flixw.cmd, flixw.java and .gitignore are byte-identical in every project on
the same flixw release, so one published digest validates all four. lock.toml is yours,
.gitattributes is yours with a block of ours in it, and local/java belongs to this
machine alone. Not committed, and safe to delete at any time:
build/ lib/ artifact/ .flix-cache/ Flix's own output and dependency cache
<cache>/ verified compiler JARs and JDKs, shared
across every project on the machine
<cache> is FLIX_CACHE_HOME if you set it, and otherwise the place the platform keeps
caches:
| Linux, BSD | ${XDG_CACHE_HOME:-~/.cache}/flixw |
| macOS | ~/Library/Caches/flixw |
| Windows | %LOCALAPPDATA%\flixw |
./flixw info prints the one in use; ./flixw info --verbose (or -v) lists what is
actually cached there -- every compiler JAR and JDK, not just the ones this project has
pinned -- plus every JDK it can find on the machine without flixw having installed it
(Homebrew, scoop, sdkman, asdf, mise, jenv, and the usual OS install directories).
When space matters, ./flixw wrapper --purge [days] offers each flixw cache entry unused
for 14 days by default for deletion: compiler JARs, provisioned JDKs, plugins and old
companion assets. flixw records its own last-use date rather than trusting filesystem access
time. Answer each prompt, or add --yes for an intentional non-interactive purge. It retains
the default JDK, this release's assets and stage-0 cache; entries without a flixw use record
are retained conservatively.
A real project on this: flix-invaders, by
the same author, which type-checks, tests, formats and packages through ./flixw on
Linux, macOS and Windows, and keeps a
pin-lag log — one row
per Flix release — so the cost of pinning is a number rather than an argument. That is one
project; docs/LIMITATIONS.md says exactly what is and is not
established.
For when you would rather not run a downloaded program to install a program. Its digests come from the release's own SHA256SUMS, which proves the
download arrived intact and nothing more — unlike the setup asset's digest above, which is
published here, on a different path, and can be compared against something the release did
not serve:
base=https://github.com/wstein/flixw/releases/download/v0.25.11curl -fsSLO $base/flixw-0.25.3.tar.gzcurl -fsSL $base/SHA256SUMS | grep flixw-0.25.3.tar.gz | sha256sum -c -tar -xzf flixw-0.25.3.tar.gz # flixw, flixw.cmd, .flixw/flixw.javarm flixw-0.25.3.tar.gz./flixw pin <version> # writes the lock, fetches and verifies the compiler # 0.75.3 or v0.75.3 -- the release tag works too./flixw doctor --fix # merges the .gitattributes blockgit add flixw flixw.cmd .flixw .gitattributesThe digest line is a check you run, not a comparison you eyeball: it prints OK or fails.
On Windows the equivalents are Get-FileHash flixw-0.25.3.tar.gz and Expand-Archive in
PowerShell, or certutil -hashfile flixw-0.25.3.tar.gz SHA256 and tar -xf in cmd.exe —
tar ships with Windows 10 and later.
Pick <version> to satisfy the flix key your flix.toml already has. That key is Flix's
own field and flixw reads it as a minimum, so the same version or anything newer is
fine. Pinning something older is allowed but warned about, and every later command that
needs the compiler refuses until it is resolved, naming both numbers and the repair — pin
is deliberately still usable in that state, because lowering the floor in flix.toml may
be exactly what you meant. flixw never edits the manifest itself.
A .zip with the same contents is attached for machines without tar. The archives leave
.gitattributes alone rather than overwriting the one your project already has, which is
why doctor --fix — which merges the block — is a step of its own; install does that
merge itself. It comes after pin because it reports on the whole installation, and until
there is a lock the honest report is that one is missing.
Install from a release rather than from main: a tool that asks you to pin an exact
compiler should not ask you to fetch itself from a moving branch.
Once installed, ./flixw wrapper --upgrade moves the project to the newest release.
./flixw wrapper --help prints the routing table: which verbs go to the compiler, which to
the wrapper, and how to force either.
.flixw/lock.toml is generated, and its first line says what it is:
#:schema https://wstein.github.io/flixw/schema/lock-v1.schema.jsonThat is the directive taplo and the Even Better TOML
extension follow, so an editor validates the file — completing keys, flagging a mistyped
one, explaining what each holds — with nothing configured per project. ./flixw wrapper --schema
prints the same schema on stdout if you would rather validate offline; it is rendered from
the list stage 0 itself checks the lock against, so the two cannot disagree.
Nothing about the build depends on the line. A lock written by an older flixw has none, and
./flixw validate says so:
$ ./flixw pin --refreshflixw: rewrote .flixw/lock.toml in the shape flixw 0.25.3 writes; the pin is unchangedThat is offline and moves nothing — same repository, version, URL, digest and java pin —
which is why it is not pin <version>, which would fetch the compiler again to write one
comment. ./flixw doctor --fix performs the same rewrite as one repair among several.
The schema, the API docs for stage 0, and a short index of both live at https://wstein.github.io/flixw/, published from the same tag as the release.
./flixw completion bash >~/.local/share/bash-completion/completions/flixw
./flixw completion zsh >"${fpath[1]}/_flixw"
./flixw completion fish >~/.config/fish/completions/flixw.fish
./flixw completion pwsh >>$PROFILEThe script describes the compiler you have pinned, so run it again after ./flixw pin.
In exchange you get more than a verb list: each command carries the compiler's own
description, and its options complete too.
bash and zsh come from picocli's generator, fish and PowerShell from flixw walking the same
command tree, so all four agree with what ./flixw help shows.
The generator is fetched from the flixw release you're running and cached machine-wide, the
same way wrapper --upgrade fetches flixw.java itself — so the first completion call on
a machine, for a given flixw release, needs network once; every call after that is offline.
Generating a script does not need a project at all.
Past the options flixw knows about, the shell's own filename completion takes over.
cmd.exe is not supported and cannot be: it has no per-command completion mechanism.
PowerShell works against the flixw.cmd that already ships; nothing needs to move to a
.ps1.
Two ways to extend what ./flixw runs, both opt-in. pin, info, doctor, validate
and help stay built in permanently — they are what a fresh clone needs before anything
else can be trusted — but everything past them can be a task or a plugin.
Tasks are the lightweight one: .flixw/tasks.toml is a flat name = "shell command"
table you write and commit yourself, the same idea as npm's scripts. Nothing is fetched.
build = "./flixw build && ./flixw build-jar"./flixw task # lists the names you have defined./flixw task build # runs it./flixw task build --release # extra words are appended to the commandPlugins are the heavier one: third-party .jar, .java or .flix code, installed
once into a machine-wide, digest-verified cache and invoked by name.
./flixw plugin install metrics 1.2.0 https://example.com/metrics/plugin.jar./flixw plugin metrics --since 30d./flixw plugin list./flixw plugin remove metricsEvery invocation re-verifies the cached bytes against the digest install recorded, and says
on stderr that it is running unaudited third-party code — a digest proves the bytes have not
changed, not that they are safe. --sha256 <digest> at install time lets you pin the digest
you expect instead of trusting whatever the URL returns.
A plugin gets a small, versioned context — project root, cache location, the pinned
compiler's version and jar path, the JDK in use — as both environment variables
(FLIXW_PROJECT_ROOT, FLIXW_CACHE_HOME, FLIXW_COMPILER_VERSION, …) and a JSON file
named by FLIXW_CONTEXT, so it can extend what Flix does in this project without flixw
handing it a second, unverified compiler to run. A .flix plugin reads that same context
through Sys.Env.getVar even though it cannot receive command-line arguments — stock Flix
has no way to run a standalone file with args.
See docs/CONTRACT.md for exactly what is guaranteed.
If you build Flix yourself — a fork, or a patch you have not tagged yet — run it with:
FLIX_JAR=/path/to/flix.jar ./flixw runTwo caveats. The jar is not digest-verified, every such run says so on stderr, and
those runs are not evidence about the stock compiler. And a valid .flixw/lock.toml is
still required: the lock is read and drift is checked before the override is, so pin a
release first even if you intend to override it every time.
To set this and the other variables — FLIX_JAVA_HOME, FLIX_CACHE_HOME,
FLIX_DIST_URL, FLIX_JVM_OPTS — per project rather than per shell, write them into an
.envrc for direnv and run direnv allow. The full table is in
docs/CONTRACT.md. direnv needs its hook in your shell's startup file
first:
eval"$(direnv hook bash)"# ~/.bashrceval"$(direnv hook zsh)"# ~/.zshrc
direnv hook fish |source# ~/.config/fish/config.fishThe .envrc is bash whatever your own shell is — direnv evaluates it with bash and exports
the difference — so fish users still write export FOO=bar in it, not set -x FOO bar.
flixw itself never reads it; direnv sets the variables in your shell before flixw
starts, which is why it works in a terminal but not for an editor-spawned flixw lsp, and
why there is no cmd.exe equivalent. flixw ships no .envrc template — the variables are
listed above and in docs/CONTRACT.md, which is where they stay current. See docs/CONTRACT.md for the full
rules.
docs/CONTRACT.md— what is guaranteed, and the diagnostics tabledocs/BENCHMARKS.md— measured overhead, with the methoddocs/LIMITATIONS.md— what it cannot do, stated plainlydocs/Flix_Bootstrap_Wrapper_Paper.md— the design paper this grew from, kept as historical evidence
flixw/
├── src/
│ ├── flixw.java stage 0 — the whole bootstrap, one dependency-free Java file
│ ├── flixw-setup.java the bootstrap: fetches and verifies stage 0, writes a project
│ ├── flixw-jdk.java optional JDK provisioning, for `wrapper --install-jdk`
│ ├── flixw-inspect.java the cache inventory behind `info --verbose`
│ ├── flixw-help.java TAB-completion generator
│ │ — the four companion assets: published per release, fetched
│ │ on first use, digest-verified, never committed to a project
│ ├── flixw POSIX shim — finds a Java, prefers the compiled stage 0
│ └── flixw.cmd cmd.exe shim — the same, without a POSIX shell
├── tests/ regression suite, unit checks, and a corpus of 95 real
│ flix.toml files, checked against a TOML oracle
└── docs/ contract, benchmarks, limitations, design paper
sh tests/lint.sh # javac -Xlint:all -Werror, shellcheck, shim byte-paritysh tests/run.sh # regression suite (needs network on first run)Both are required before a commit. In CI, lint.sh runs once on Linux — javac and
shellcheck answer the same on every platform — while run.sh runs on Linux, macOS and
Windows, on Java 21 and again on the tested ceiling, and Windows additionally gets a
cmd.exe job that installs into a scratch project and drives flixw.cmd end to end.
sh tests/pack.sh <dir> builds the release archives locally, by the same script the release
workflow runs — so a published digest can be reproduced rather than trusted. It needs
java, zip, tar, and sha256sum or shasum; the byte-for-byte reproduction of a
published archive needs GNU tar, which is why the release job runs on Linux.
sh tests/fetch-corpus.sh refreshes the manifest corpus; it is a maintenance tool rather
than a test, and needs gh, curl and python3. See
tests/corpus/README.md.
MIT. These files are meant to be committed into other people's repositories; the license permits that without conditions beyond attribution.