Install the engine
Build the binary, put it where you want it, and take the packs beside it.
The wizard does this for you. This page is what it does, for when you would rather do it yourself.
The engine is one binary, nils. One command installs it for you; this page is what that command does, and what to do when you would rather do it yourself.
Take the release
v=v1.0.0-beta.1 # the newest tag on the releases page
os=linux; cpu=x86_64 # or macos, and arm64
base=https://github.com/kineuro/nils/releases/download/$v
curl -fsSLO $base/nils-$os-$cpu
curl -fsSL $base/SHA256SUMS | grep " nils-$os-$cpu$" | sha256sum -c -
sudo install -m 755 nils-$os-$cpu /usr/local/bin/nils
nils --version
Name the version rather than saying latest. GitHub’s latest skips a prerelease, and until 1.0.0 that is all there is, so a latest/download/... URL answers 404. The releases page has the newest tag.
The packs, the rule sets that decide what a scan is, come from the same release:
curl -fsSLO $base/packs.tar.gz
mkdir -p ~/.local/share/nils/packs
tar xzf packs.tar.gz -C ~/.local/share/nils/packs --strip-components=1
The engine looks there by itself, so --pack-dir is only needed for packs kept somewhere else. The wizard does this step for you.
Or build it
The repository pins the toolchain, so rustup fetches the right version by itself.
git clone https://github.com/kineuro/nils
cd nils/engine
cargo build --release -p nils
That leaves target/release/nils, about 140 MB unstripped and with no runtime dependency beyond the C library. Put it on the path:
sudo install -m 755 target/release/nils /usr/local/bin/nils
nils --version
The build takes a few minutes and about 4 GB of memory. A Linux build runs on any distribution with glibc 2.39 or newer; for anything older, use the container.
Where the packs live
A pack is the rule set that decides what a scan is: which axis values a stack carries, how the rules vote, what evidence they leave. Packs are data, not code, and they ship both in the release and in the repository under packs/.
Without a --pack-dir the engine looks in $NILS_PACK_DIR, then the registry’s own packs directory, then $XDG_DATA_HOME/nils/packs (else ~/.local/share/nils/packs), then /usr/local/share/nils/packs, then /usr/share/nils/packs, and last share/nils/packs beside the binary’s own prefix. It takes the first that holds a pack, and nils serve names the directory it took. Without any packs at all it still runs, digests and answers questions; it just cannot classify anything.
Where things will live
Nothing is written outside what you name. A typical layout, all owned by the user the engine runs as:
/srv/nils/registry the registry: nils.toml, the databases, the key store
/srv/nils/packs the rule sets
/srv/nils/backups where the backup job writes archives
The DICOM files themselves stay where they are. The engine reads them and records what it found; it never copies them in.
dcm2niix
A release to NIfTI or BIDS converts with dcm2niix. The wizard places the build this engine pins; to do it yourself:
nils tools install dcm2niix
nils tools list
The install downloads the pinned build, checks its sha256, unpacks it into the registry’s tools/ folder and asks its version. nils tools list says whether dcm2niix is here, where, which build, and how it was found.
Where the machine cannot reach GitHub, nils tools list names the archive to fetch, with its address and its sha256. Fetch it on a machine that can, and give it to the engine:
nils tools install dcm2niix --from dcm2niix_lnx.zip
The archive is checked against the pinned sha256 before anything is unpacked, and installed as the download would be; any other file is refused, with the archive it takes named. In the desk the same is Install from a file, beside Try again on Settings, Parts, Tools.
Keeping it current
nils update # the newest release, verified, in place
nils update --check # what it would do
The engine, the desk, the assistant and Kvasir each release on their own, under version numbers of their own, so the engine’s number says nothing about the desk’s. nils update --check lists every part beside its own newest release, and nils update --part <part> updates one part alone (engine, desk, assistant or kvasir); see keeping it current.
An engine update brings the packs of its release as well, into the directory the engine reads them from, and keeps the packs it replaces in <dir>.previous. Only packs that differ from the release’s move. A site’s own packs are never touched, and a first-party pack changed on the machine is kept and said. nils update --check shows each pack’s version beside the release’s. A new pack version applies to what is classified after it, so reclassify to bring older stacks under it.
Since 1.0.0-alpha.66 the engine reads what an enhanced MR object says of how it was acquired (its MR Pulse Sequence module) and which kind of object each file is. After updating to it, in this order:
nils fingerprint # every stack again: the SOP class and the phase contrast fill at once
nils digest <root> --reread <manufacturer> # per root and manufacturer that writes enhanced objects: the module's fields
nils fingerprint # the stacks of the series read again
nils classify --pack mri --name <label> # every stack under the new pack
A re-read reads every file of that manufacturer’s MR series under the root again, so it takes as long as digesting them did; --reread-exact Siemens takes Siemens’ XA scanners alone, which spell their make that way. A registry with no enhanced objects needs neither the re-read nor the second fingerprint.
Since 1.0.0-alpha.69 the engine also reads GE’s series plane, which MRI pack 0.21.0 reads to tell a three-plane localizer whatever its slice count, and the ImageType Philips writes per frame of an enhanced MR object, by whose Dixon part (water, fat, in phase, out of phase) such an object’s frames are now stacks of their own. The registry’s schema moves to 78 by itself when the new engine first opens it. After updating to it, in this order:
nils digest <root> --reread "GE MEDICAL SYSTEMS" # per root with GE MR series: the series plane
nils fingerprint # every stack again, at the fingerprint's new revision
nils classify --pack mri --name <label> # every stack under MRI pack 0.21.0
Use the manufacturer as your GE studies spell it, once for each spelling. A registry with no GE series needs no re-read. A Philips enhanced object digested before keeps the new field empty until its files are read again.
Since 1.0.0-alpha.70 the MRI pack is 0.22.0, with no change to the engine or the registry. After updating, classify again; no re-read is needed:
nils classify --pack mri --name <label> # every stack under MRI pack 0.22.0
Since 1.0.0-alpha.71 the MRI pack is 0.23.0, again with no change to the engine or the registry. After updating, classify again; no re-read is needed:
nils classify --pack mri --name <label> # every stack under MRI pack 0.23.0
Since 1.0.0-alpha.72 the slab door reads a slab’s tiles at once and keeps recent slabs in memory (256 MB), so pictures from pyramids on a network share come in a fraction of the time. Nothing to do after updating; the registry is unchanged.
Since 1.0.0-alpha.73 the MRI pack is 0.24.0, with no change to the registry. After updating, classify again; no re-read is needed. Classify takes about a quarter longer than under 0.23.0:
nils classify --pack mri --name <label> # every stack under MRI pack 0.24.0
Since 1.0.0-alpha.73 the engine keeps serving when it runs short of open files, and says so on stderr rather than losing its port. It raises its own soft limit on open files to the hard one, and the unit nils setup and nils update write sets LimitNOFILE=65536 and puts the engine under systemd’s watchdog (WatchdogSec=120, NotifyAccess=main): the engine feeds the watchdog only while it takes requests, so one that hangs is restarted. A unit you wrote yourself should say the same. GET /api/health answers with no token: whether the engine is live and ready, and its counts (handlers, open files, tile reads, caches, accept errors, panics), 200 when ready and 503 when not.
Since 1.0.0-alpha.80 the rules ship on their own. A first-party pack is released under a tag of its own (pack-mri-v<version>), and its pack.yml names the engines it works with; an engine outside that range refuses it and the update path never installs it. nils update --check lists each pack beside its newest release, and nils update --part rules takes the rules alone and restarts nothing, since the engine reads a pack again when its files change. The engine’s own release still carries its packs, and stays their floor.
The MRI pack of 1.0.0-alpha.80 is 1.2.0, which needs this engine or later. The registry’s schema moves to 98 by itself when the new engine first opens it. A source place without a whole, current declaration (what its PatientID holds and how its subjects are found) becomes undeclared, and an undeclared place is never read until its folder is explored again with nils place explore; no file is touched. After updating, explore the places and classify again; no re-read and no fingerprint are needed:
nils place explore # every source place's state read again
nils classify --pack mri --name <label> # every stack under MRI pack 1.2.0
Since 1.0.0-alpha.82 a release builds every model’s image (body part, post-contrast’s cascade and System 1) from its tag, and ships the descriptors that name them by the digests it pushed, beside the binaries. Take one by its name from the release this engine is:
nils pipeline add --release bodypart-infer-fusion
nils pipeline add --release postcontrast-cascade
nils pipeline add --release system1-score
Post-contrast’s fallback image (postcontrast-full) is too large for a hosted build: it is published beforehand to ghcr.io/kineuro/nils-postcontrast-full, and the release ships its descriptor pinned to the digest published there. nils pipeline add --release postcontrast-full takes it the same way. One image runs each model on the CPU and on a card.
Since 1.0.0-alpha.82 the registry’s schema moves to 102 by itself when the new engine first opens it. It brings the rules engine on Review (proposals with their effect shown before anyone decides), plain admin access, body part and post-contrast as layers of the Main scans map, an ID type’s identifying flag, and one lookup from an ID to its subject code. After updating, take the newest rules and sort again:
nils update --part rules
nils classify --pack mri --name <label> # every stack under the rules now in place
In the desk the same is Update on the rules row of Settings, Parts, and then Sort again.
The rules ship on their own tag: the MRI pack is released as pack-mri-v<version>, and an install runs the newest release its engine reads. nils update --pin mri@1.0.0 holds an install at one version, and it is for one purpose: reproducing what the paper reports, which was found with rules 1.0.0. A site does not pin otherwise, since a pinned install misses the fixes made since. nils update --unpin mri lifts it. Rules 1.0.0 and 1.0.1 are published as rules releases of their own for this.
The MRI rules 1.3.0 need engine 1.0.0-alpha.87 or later. They state the thresholds at which a dataset’s headers are flagged as damaged, and change no rule, so no scan needs sorting again.
Since 1.0.0-beta.1 the registry’s schema moves to 106 by itself when the new engine first opens it. On a large registry the new index takes a few minutes, once, while the engine starts. A sort no longer makes pictures itself: it ends once its scans are judged, and its pictures and 3D views are jobs of their own, made several scans at a time. Nothing to do after updating.
Next: make the registry.
Updated 17 October 2026