Skip to content

Not available

Everything with no Installua equivalent, and what to write instead. Nothing here is pending: these are decisions.

Installua has no preprocessor — the script is a program — and 33 of the 37 ! directives have no equivalent here. They are not all out for the same reason, though, so they are listed under the reason rather than in one heap. Only the first group is answered by “there is no preprocessor”; the rest are not preprocessing at all.

!addincludedir · !cd · !define · !else · !endif · !if · !ifdef · !ifmacrodef · !ifmacrondef · !ifndef · !include · !insertmacro · !macro · !macroend · !macroundef · !undef

These are not withheld, they are unnecessary. A top-level local X <const> = … is a !define — that is what it emits — and the rest have ordinary constructs:

NSIS Installua
!define APP "Example" local APP <const> = "Example"
!ifndef VERSION / !define / !endif local VERSION <const> = param("VERSION", "1.4.2")
!if ${ARCH} == "x64" … !else … !endif an if on a <const>, at the top level or in a body
!include "MyHelpers.nsh" (your own file) include("helpers.lua")
!include "FileFunc.nsh" (an NSIS header) local fileFunc = import "FileFunc"
!macro Banner text … !macroend func("banner", function(text) … end)
!insertmacro Banner "1.4.2" banner("1.4.2")

Two of those are worth more than a row. Conditional compilation is decided in the compiler rather than emitted into the script, and param does the !ifndef sandwich’s job while additionally rejecting a -D nobody declared.

!macro and !insertmacro are the ones that stay out, and they are the largest bucket by a distance — a scan of 984 real-world scripts finds 15094 !insertmacro sites. Most of that number is already answered: 77% are MUI_* and LANGFILE names, which are pages and page settings here, and another slice is stdlib macros that are ordinary calls. What is left expands to declarations rather than instructions — a !define that a later !insertmacro reads, a Var that has to precede its use — and admitting that is admitting a preprocessor with real ordering consequences. The fixed emission order is what makes define-before-insert, Var-before-use and section-index-before-.onInit stop being your problem, and text substitution is the one feature that cannot coexist with it. Functions plus import cover the rest.

If a particular stock header keeps coming up, the answer is to declare it so its macros are calls — the way FileFunc and WordFunc already are. One header at a time, on evidence: the same scan puts nsProcess.nsh in 25 files, FileAssociation.nsh in 22 and EnvVarUpdate.nsh in 20, and nsProcess is declared here because of it. A .toml in your own project does the same thing without waiting for anyone, and a header you only reach through raw needs no declaration at all.

!ifdef used to ask whether a name exists has no equivalent at all, and that is the one place this group really does lose something. Every name here is declared: an undeclared one is an error, and a name that may or may not be supplied is a parameter.

!appendfile · !appendmemfile · !delfile · !execute · !gettlbversion · !makensis · !searchparse · !searchreplace · !tempfile

These run a program or read a file while the installer is being built, and hand the answer back to the script as a !define. Installua cannot catch that answer as a <const>: a <const> is folded before makensis runs anything, and making it wait would mean running things at build time, which is what order-free resolution costs. !system and !getdllversion are the exception in part: MAKENSIS.system and MAKENSIS.getDllVersion catch the answer as an install-time value, which a build-time if cannot read.

So the work moves to whatever runs the build, and the value arrives as a -D:

!system 'git rev-parse --short HEAD > rev.txt'
!searchparse /file rev.txt "" REV
DetailPrint "build ${REV}"
local REV <const> = param("REV") -- no default: the build stops without it
installer {
section("Core", function()
detailPrint("build " .. REV)
end),
}
Terminal window
$ installua build install.lua -D REV=$(git rev-parse --short HEAD)

A parameter with no default is what makes this complete rather than hopeful: a wrapper that forgets to pass the value fails the build instead of shipping a default nobody chose.

If you only want the side effect and never the value, MAKENSIS.system runs it where it stands, and raw.head runs it above everything:

raw.head [[ !system 'echo building' ]]

!finalize · !packhdr · !uninstfinalize

These register a command makensis runs once the installer exists. No value flows anywhere and position is the whole of it, which is what an anchor is for:

raw.tail [[ !finalize '"sign.exe" "%1"' ]]

There is no Installua equivalent because one would add nothing — the NSIS line already says exactly what it does, and raw.tail is the position it needs.

!pragma · !verbose

Nothing, and deliberately. installua build runs makensis -WX with no warning allowed and rewrites every message back onto the Lua line it came from. A script turning verbosity down or disabling a warning would be switching off the check that mapping exists to serve. This is a decision about who owns the makensis invocation, not about staging.

!assert · !error · !warning

The common one is the guard that stops a build when a value was not supplied:

!ifndef SIGNING_CERT
!error "pass -DSIGNING_CERT"
!endif
local SIGNING_CERT <const> = param("SIGNING_CERT")

That refuses the build the same way and does it from the declaration, so there is no guard to forget and nowhere to paste it wrongly.

The general form — “this combination of settings makes no sense, stop” — has no equivalent yet. It would be a top-level error(…) inside a build-time if, and it waits for a real script that wants one.

!addplugindir is written by the compiler rather than by you: it has exactly one legal position, and a dir on a [[plugin]] declaration is how you ask for it.

These are real NSIS lines in the output — you just do not write them yourself. Listed here so a search for the NSIS name lands somewhere.

NSIS What writes it
!define local X <const> = …, param
Goto if, while, for, break, continue(), return
Call a call: f(x)
Push / Pop / Exch the calling convention
StrCpy assignment
StrCmp / StrCmpS ==
IntOp / IntPtrOp the arithmetic operators
IntCmp / IntCmpU / Int64Cmp / Int64CmpU / IntPtrCmp / IntPtrCmpU comparison
IntFmt / Int64Fmt string.format
LangString languages { locales = { … } }
LicenseLangString page.license { file = { … } }
InitPluginsDir a body that names PLUGINSDIR
ReserveFile /plugin a plugin .onInit can reach
!addplugindir a plugin declared with a dir
EnableWindow / ShowWindow a control’s enabled / visible
SetCtlColors / CreateFont a control’s colors / font
LoadAndSetImage a bitmap’s image
GetFunctionAddress onClick / onChange

Sixteen plugins ship with NSIS. Thirteen are declared or reached through a construct; these three are not, and each one is a decision rather than a gap. All three remain callable through raw, which is where a script that needs one goes.

Plugin Why not, and what to write instead
Math Its script string reads and writes $0–$R9 by name, and the compiler owns the registers — a declaration would describe one string in and nothing out while the call quietly overwrote whatever the allocator had put there. Use the arithmetic operators.
BgImage Every method returns a value only after SetReturn on, which makes the arity a mode rather than a signature — and a .toml cannot describe a mode.
LangDLL Reached through languages { ask = … }, which emits MUI_LANGDLL_DISPLAY. A second way to write one dialog is worse than none.

InstallOptions and nsDialogs are not in that list because they are not absences: the first is superseded by page.custom and the second is what page.custom emits.

The same decision, made against a scan of 984 real-world scripts. Each of these is common enough to have been considered and turned down for a stated reason — and every reason is about the declaration format rather than about the plugin, which is what makes the list worth keeping. All remain callable through raw, and any of them can be declared by a project in five lines of its own .toml.

Plugin Scripts Why not, and what to write instead
Registry 40 Every scanned use is ${registry::…}, the Registry.nsh macro form, not a raw plugin call — and that form needs a trailing ${registry::Unload}, which is behaviour rather than arity. readReg, writeReg and deleteRegKey already cover 38 of the 40.
SimpleSC.getErrorMessage 61 Takes its argument by Push rather than inline: Push $code / SimpleSC::GetErrorMessage / Pop $msg. params become the arguments written after Plugin::Method, so a .toml has no way to describe it. Three lines of raw.
LockedList 0 It is a custom page, not a call. Declaring only the Add* setup calls would ship half a feature.
Crypto 0 No scanned script uses it.
Nsis7z.extractWithCallback 5 Not the register protocol the six FileFunc/TextFunc macros use — it pushes its two values on the stack. It stays out a step earlier than that: its second argument is the address of a function, params has no type for one, and GetFunctionAddress has no Lua equivalent. See the plugin reference.
nsJSON 3 Its node path is a variable number of positional strings — one to four across 25 call sites — and params is a fixed list, so a declaration would have to pick a depth and miscount the Pops at every other one. The repeated /index run its readme advertises turns out to appear in no script at all. See the plugin reference.
Inetc.post 2 Its POST body is popped before the flag loop (inetc.cpp:1369), so it has to be written ahead of every switch — and a params entry is emitted after the flags. get, head and put ship declared; only this entry point has an argument in front.
NSIS Write instead
CallInstDLL plugin.method(…)
ChangeUI — MUI2 owns the dialog resources
DirShow — NSIS reports this one as not working
FindFirst / FindNext / FindClose for path in glob("…") do
GetCurrentAddress / GetLabelAddress — there are no labels to address
LangStringUP languages { locales = { … } }
LoadLanguageFile languages { locales = { … } }
LogSet / LogText detailPrint — logging needs a custom makensis build
Nop write nothing
PageEx / PageExEnd / PageCallbacks page.*
SectionInstType a section’s installTypes
SetPluginUnload — NSIS retired it
SubSection / SubSectionEnd group(…)
Target cpu — the charset is always Unicode
Unicode — always true, written first
UninstallExeName writeUninstaller(…)
UnsafeStrCpy =
XPStyle — MUI2 emits it itself

Installua is Lua-shaped, not Lua (see Lua-shaped, not Lua): no floats, no closures as values, no metatables, no pairs, no require as a runtime load, # rejected on strings, and truthiness only for booleans.