Not available
Everything with no Installua equivalent, and what to write instead. Nothing here is pending: these are decisions.
The ! directives
Section titled “The ! directives”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.
Replaced by the language
Section titled “Replaced by the language”!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.
Computed while building
Section titled “Computed while building”!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 "" REVDetailPrint "build ${REV}"local REV <const> = param("REV") -- no default: the build stops without it
installer { section("Core", function() detailPrint("build " .. REV) end),}$ 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' ]]Run when the build ends
Section titled “Run when the build ends”!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.
makensis’s own output
Section titled “makensis’s own output”!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.
Build-time messages
Section titled “Build-time messages”!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"!endiflocal 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.
The one that is not out
Section titled “The one that is not out”!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.
Written by the compiler, never by you
Section titled “Written by the compiler, never by you”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 |
Plugins with no declaration
Section titled “Plugins with no declaration”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.
Third-party plugins that stay out
Section titled “Third-party plugins that stay out”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. |
Rejected NSIS commands
Section titled “Rejected NSIS commands”| 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 |
Lua that is not here
Section titled “Lua that is not here”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.