Jekyll2023-02-02T19:44:32+00:00https://mistake.computer/feed.xmlMistakes Were Madeexploring the depths of the collective mistake that is the computermolly howellPrivateer’s Hidden Text Adventure2020-05-12T00:00:00+00:002020-05-12T00:00:00+00:00https://mistake.computer/2020/05/12/privateer-batch-adventure<p>Or: How to Make a Text Adventure Game Out of DOS Batch Files</p>
<h2 id="background">Background</h2>
<p><a href="https://www.mobygames.com/game/dos/wing-commander-privateer">Wing Commander: Privateer</a> (often just called Privateer) is a space simulator game for PC, released by Origin Systems in 1993. It’s largely of the <a href="https://www.mobygames.com/game/elite">Elite</a> lineage, but set in the Wing Commander universe and with a bit more focus on fast action and dogfighting than on simulation detail, a design concept also inherited from Wing Commander. I highly recommend trying the game if that sort of thing sounds interesting to you. If you don’t have a copy, you can buy one from <a href="https://www.gog.com/game/wing_commander_privateer">GOG.com</a>; what you’ll get from there the CD-ROM re-release which includes the expansion pack Righteous Fire, the speech pack, and a few other added features.</p>
<p>That’s all I’m going to say about Privateer. We’re here to talk about something else.</p>
<h2 id="poking-around">Poking Around</h2>
<blockquote class="sidenote">
If you want to follow along with this article, you'll find all the files I'm talking about in the install directory of either the original or the GOG version. If you don't have or want a copy of Privateer, you can find just the relevant files nicely packaged and ready to run on <a href="https://archive.org/details/wcadvent">the Internet Archive</a>.
</blockquote>
<p>I had a copy of Privateer as a kid, and I was also constantly curious about what was going on with my computer, so obviously one of the first things I did was have a look in the game’s installation directory.</p>
<p>What I found there was all the usual DOS game stuff: a setup utility where you can tell the game about your sound card(s), some configuration files, lots of data files, and the game executables. But there’s another file too that didn’t look like it fit any of those categories. It’s about 10KB and it’s called <code class="highlighter-rouge">TABTNE.VDA</code>. So naturally I needed to have a look in there.</p>
<p>Since I didn’t have any actual tools at the time, I opened the file in <a href="https://en.wikipedia.org/wiki/MS-DOS_Editor"><code class="highlighter-rouge">EDIT.COM</code></a>. And it was… text? I didn’t expect readable text. Okay, maybe readable is a stretch, but it is ASCII text. In fact it looked like a <a href="https://en.wikipedia.org/wiki/Batch_file">batch file</a>, the old DOS version of what today we would think of as a shell script. The first line is <code class="highlighter-rouge">@echo off</code>, which is a telltale sign; <code class="highlighter-rouge">echo off</code> is a special form of the normal <code class="highlighter-rouge">echo</code> command that disables the DOS shell’s normal behavior of automatically printing every command in a batch file to the console, and the <code class="highlighter-rouge">@</code> at the front disables echoing just that one command. So tons of DOS/Windows batch files start with <code class="highlighter-rouge">@echo off</code>, and nothing else really does. I knew I was looking at a batch file.</p>
<h2 id="lets-run-a-questionable-script">Let’s Run a Questionable Script</h2>
<p>At the time I had no concept of computer security (in my defense, hardly anybody did), so my obvious next move is to run this thing and see what happens. In hindsight, I think there must have been instructions somewhere on how to get it to work, but I didn’t have any, so let’s go through it the hard way.</p>
<p>I’ve got Privateer installed into an MS-DOS 6 virtual machine, so I’ll be showing you some transcripts from its command line as we go through this. The obvious first step is to rename the file with a <code class="highlighter-rouge">.BAT</code> extension and run it:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\PRIVATER>ren tabtne.vba tabtne.bat
C:\PRIVATER>tabtne.bat
First you have to give this file its rightful name.
C:\PRIVATER>
</code></pre></div></div>
<blockquote class="sidenote">
Yes, the name of the directory is missing an "e". That's because this is DOS, and file names have to follow <a href="https://en.wikipedia.org/wiki/8.3_filename">the old 8.3 format</a>, so that's how the game's install program handles the title being too long.
</blockquote>
<p>… Okay. Well. Let’s look at the file again. At the top there’s a bunch of setup and command line handling and such. Line 21 says <code class="highlighter-rouge">if not exist advent.bat goto c0</code> <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote">1</a></sup>. Scrolling a few lines later, we find the label called <code class="highlighter-rouge">:c0</code>, which is what prints that message. So <code class="highlighter-rouge">advent.bat</code> must be the file’s “rightful” name. We’ll rename it to that and run it again:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\PRIVATER>ren tabtne.bat advent.bat
C:\PRIVATER>advent
Please place this file in an empty directory.
Then type 'advent setup'.
C:\PRIVATER>
</code></pre></div></div>
<p>So, it needs to be in an empty directory, huh. That might be a clue to how this thing works. But for now we’ll just do as it asks and give it a directory and run the setup.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\PRIVATER>mkdir C:\advent
C:\PRIVATER>copy advent.bat c:\advent
1 file(s) copied
C:\PRIVATER>cd ..\advent
C:\ADVENT>advent setup
Type 'advent' to start.
C:\ADVENT>
</code></pre></div></div>
<p>Okay! Progress! We must finally be done settings things up.</p>
<p>But wait a sec. What did that <code class="highlighter-rouge">setup</code> command actually do? What setup could a batch file possibly need?</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\ADVENT>dir
Volume in drive C is MS-DOS_6
Volume Serial Number is 4F1F-8F60
Directory of C:\ADVENT
. <DIR> 05-03-20 7:38p
.. <DIR> 05-03-20 7:38p
ADVENT BAT 9,843 08-10-92 8:04p
D BAT 15 05-03-20 7:39p
DROP BAT 18 05-03-20 7:39p
E BAT 15 05-03-20 7:39p
GET BAT 18 05-03-20 7:39p
GO BAT 16 05-03-20 7:39p
INV BAT 17 05-03-20 7:39p
N BAT 15 05-03-20 7:39p
QUIT BAT 15 05-03-20 7:39p
S BAT 15 05-03-20 7:39p
L BAT 18 05-03-20 7:39p
TAKE BAT 18 05-03-20 7:39p
U BAT 15 05-03-20 7:39p
W BAT 15 05-03-20 7:39p
LOOK BAT 18 05-03-20 7:39p
SAY BAT 17 05-03-20 7:39p
USE BAT 17 05-03-20 7:39p
I BAT 17 05-03-20 7:39p
EXAMINE BAT 18 05-03-20 7:39p
21 file(s) 10,140 bytes
</code></pre></div></div>
<p>… it made a bunch more tiny batch files. Which all have names that look suspiciously like verbs you would type in to a text adventure game parser (<code class="highlighter-rouge">N</code>, <code class="highlighter-rouge">S</code>, <code class="highlighter-rouge">E</code>, and <code class="highlighter-rouge">W</code> to move to the north, south, east, or west, for example). And it wanted us to call it “advent.” Surely this thing is not an entire text adventure game, hidden away inside of a batch file. Surely.</p>
<p>Let’s see what’s in those tiny verb batch files:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\ADVENT>type N.BAT
@advent go n
</code></pre></div></div>
<p>So these all just call the “main” batch file to pass along commands (again we see the <code class="highlighter-rouge">@</code> to disable echo).</p>
<p>If you’re curious about how creating batch files from within a batch file works, here’s a few snippets of the setup code, with comments added (<code class="highlighter-rouge">REM</code> for “remark” starts a comment line in batch files):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>REM There's one of these blocks for each verb batch file.
REM Check if the file we're trying to create already exists.
if exist d.bat goto e1
REM If it doesn't, echo the correct command string into it.
echo @advent go d > d.bat
REM Here's another example that writes a different verb.
e1:
if exist drop.bat goto e2
REM This time the verb has a subject, so this verb's batch file
REM needs to pass its first argument through to advent.bat.
echo @advent drop %%1% > drop.bat
REM Skipping over more of those blocks here.
REM Print the instruction message.
echo Type 'advent' to start.
</code></pre></div></div>
<h2 id="actually-running-the-script"><em>Actually</em> Running the Script</h2>
<p>Now, finally, for real, let’s start it up:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\ADVENT>advent
[ ... the screen clears ... ]
--- Art D's First Batch Adventure ---
A few notes before you begin:
1. Do not let your commands stray beyond two words.
2. Type 'quit' to quit.
3. Don't type DOS commands while playing (except 'dir' for a verb list).
You are walking along a sunny north/south path near a small stream.
A very recent landslide prevents your return to the south.
There is a sharp knife here.
>
</code></pre></div></div>
<p>Yeah, this certainly looks like the first screen of a normal text adventure game. We’re playing a text adventure game implemented entirely as a batch file, and hidden as an easter egg within another, commercially released game.</p>
<blockquote class="sidenote">
"Art D", by the way, I think must be <a href="https://www.mobygames.com/developer/sheet/view/developerId,3789/">Arthur DiBianca</a>, who is credited as a programmer on the original Privateer and as project lead and sole programmer (among other things) for both its expansion pack Righteous Fire and the CD-ROM re-release that combined both (the original versions were sold on floppy disks).
</blockquote>
<p>But what does it mean about not typing any DOS commands? Aren’t we inside a game? It took our DOS prompt away, how can any DOS commands even work? Let’s try the one it does suggest and see what happens.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>dir
Volume in drive C is MS-DOS_6
Volume Serial Number is 4F1F-8F60
Directory of C:\ADVENT
. <DIR> 05-03-20 7:38p
.. <DIR> 05-03-20 7:38p
ADVENT BAT 9,843 08-10-92 8:04p
D BAT 15 05-03-20 7:39p
DROP BAT 18 05-03-20 7:39p
E BAT 15 05-03-20 7:39p
GET BAT 18 05-03-20 7:39p
GO BAT 16 05-03-20 7:39p
INV BAT 17 05-03-20 7:39p
N BAT 15 05-03-20 7:39p
QUIT BAT 15 05-03-20 7:39p
S BAT 15 05-03-20 7:39p
L BAT 18 05-03-20 7:39p
TAKE BAT 18 05-03-20 7:39p
U BAT 15 05-03-20 7:39p
W BAT 15 05-03-20 7:39p
LOOK BAT 18 05-03-20 7:39p
SAY BAT 17 05-03-20 7:39p
USE BAT 17 05-03-20 7:39p
I BAT 17 05-03-20 7:39p
EXAMINE BAT 18 05-03-20 7:39p
21 file(s) 10,140 bytes
</code></pre></div></div>
<p>Wait, that’s… that’s DOS’s <code class="highlighter-rouge">dir</code> output. From the same directory we were already in. It’s identical to what we saw before. This thing has definitely not reimplemented <code class="highlighter-rouge">dir</code>. It must have just dropped us right back into the DOS shell. Plus it’s confirmed our guess from before: the batch files that the setup command created really are our verbs for playing the game. But it’s lying to us a bit; if we try other DOS commands, they do still work:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>more < n.bat
@advent go n
>
</code></pre></div></div>
<p>So yeah. All it’s done is mess with our prompt to make it look like a custom thing and not a normal DOS prompt, which is totally what it still is. We’re going to play a text adventure game inside of <code class="highlighter-rouge">COMMAND.COM</code>.</p>
<h2 id="were-playing-a-game-now-apparently">We’re Playing a Game Now, Apparently</h2>
<p>Since we’re in a text adventure, let’s do the natural thing and pick up the inventory item it told us is here:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>take knife
Taken.
>i
You are carrying a knife.
>
</code></pre></div></div>
<p>But wait! Wait, I say! We know now that all we just did was run a couple of files called <code class="highlighter-rouge">TAKE.BAT</code> and <code class="highlighter-rouge">I.BAT</code>, which each just exited and left us back at the shell. So how does the game keep track of an inventory? The location the player is currently in is another kind of important thing for the game to know, so it must be storing that somewhere too.</p>
<p>The possibilities for something like this in DOS are rather limited, especially when you’re running entirely out of batch files; all the really sneaky things you could do would require running your own native code, which isn’t an option here.</p>
<p>We know the game can create files because that’s what its setup routine does. So let’s check for any new files it might have just made.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>dir
[ ... everything is the same as before ... ]
>
</code></pre></div></div>
<p>There’s no new files, and none of the sizes or times are different either. It’s not using any aspect of the file system to store its state.</p>
<p>That pretty much leaves environment variables as our only other option. Since normal DOS commands work fine while we’re in the game, we have the tools we need to check and see if it’s made any changes to the environment. What that actually means here in unmodified MS-DOS 6 is really only that we can dump the entire environment, but let’s try that.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>>set
COMSPEC=C:\DOS\COMMAND.COM
PATH=C:\WINDOWS;C:\DOS
TEMP=C:\DOS
_R=$p$g
_L=path
_BOX=road
_LAMP=nowh
_SHOES=gymn
PROMPT=$g
_KNIFE=poss
_U=echo You are carrying
_T=X
>
</code></pre></div></div>
<p>It sure has made some environment variables; everything that starts with an <code class="highlighter-rouge">_</code> was created by the game. It’s also edited the <code class="highlighter-rouge">PROMPT</code> to remove the current directory; the original value of <code class="highlighter-rouge">PROMPT</code> is being kept around in <code class="highlighter-rouge">_R</code> so that it can be restored when we quit the game. In fact, I bet all that the <code class="highlighter-rouge">quit</code> command does is restore the prompt and delete all the <code class="highlighter-rouge">_</code> variables. Let’s look at its code:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:cquit
prompt %_R%
set _R=
set _L=
set _T=
set _D=
set _BOX=
set _KNIFE=
set _LAMP=
set _SHOES=
REM "echo." prints a blank line.
echo.
echo Your environment is clear. You may proceed.
echo.
REM Exit the script.
goto e
</code></pre></div></div>
<p>Yep.</p>
<p>So let’s take stock. We can see four variables with names that look like inventory items, <code class="highlighter-rouge">_BOX</code>, <code class="highlighter-rouge">_LAMP</code>, <code class="highlighter-rouge">_SHOES</code>, and <code class="highlighter-rouge">_KNIFE</code>. Doesn’t seem like very many, but the whole game is one 10 KB batch file, what do you want. <code class="highlighter-rouge">_KNIFE</code> is set to <code class="highlighter-rouge">poss</code>, and since we picked up the knife I’m assuming that’s short for “possessed”. The other three have values that look like names of locations, so that seems like it’s storing where those items currently are at. <code class="highlighter-rouge">_L</code> also looks like the name of a place, so that must be where the game stores the player’s current location. We don’t know yet what <code class="highlighter-rouge">_U</code> and <code class="highlighter-rouge">_T</code> are for.</p>
<p>Unfortunately the way this system of keeping track of game state works means that our progress isn’t saved at all, unless you manually copy out all of those environment variables somewhere yourself. But this game doesn’t seem long enough for that to become a problem. Again: 10 KB batch file. The Markdown file I am writing this blog post in is almost twice that long. There’s also the risk of name collisions with existing environment variables, the game doesn’t do anything to handle that, but the leading underscores and the particular names chosen make that seem unlikely.</p>
<p>From here, the rest of how things work is not complicated. Each of the tiny verb batch files calls into <code class="highlighter-rouge">ADVENT.BAT</code> with certain arguments for the verb it wants to execute, and there’s a simple parser in there that just <code class="highlighter-rouge">GOTO</code>’s the right label for whatever command you ran. Those labels then have those own if/goto chains that do different things based on the other parameters and on those environment variables. And that’s it, that’s how it works, there really isn’t any more to it. I’ll show you one example. One of the available commands is called <code class="highlighter-rouge">drop</code>; it takes an item out of your inventory and leaves it in whatever location you’re currently at. Here’s is the entire implementation of <code class="highlighter-rouge">drop</code>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>REM Near the beginning of the file:
if '%1'=='drop' goto cdrop
REM Later on:
:cdrop
if '%2'=='' goto dwhat
if '%2'=='box' set _T=%_BOX%
if '%2'=='knife' set _T=%_KNIFE%
if '%2'=='lamp' set _T=%_LAMP%
if '%2'=='shoes' set _T=%_SHOES%
if '%_T%'=='poss' goto dg
if not '%_T%'=='' goto db
echo Drop the %2? I don't think I quite understand you.
goto e
:dwhat
echo 'Take' ain't the only transitive verb, either.
goto e
:db
echo You aren't carrying the %2.
goto e
:dg
set _%2=%_L%
echo Dropped the %2.
goto e
</code></pre></div></div>
<p>Even though this isn’t complicated, there are a few things to explain.</p>
<p>The <code class="highlighter-rouge">e</code> label is at the end of the file and just exits, so going there leaves you back at the modified DOS prompt.</p>
<p><code class="highlighter-rouge">%1</code> and <code class="highlighter-rouge">%2</code> and so on are special variables containing the command-line arguments. If we don’t pass the name of an object to drop, the game prints a message complaining at us, if we pass a name that it doesn’t know, we get a different message, and there’s a third message for if we try to drop an item we don’t have.</p>
<p><code class="highlighter-rouge">set</code> is the DOS command that sets environment variables, so we can see now that, indeed, those are where all the game state is. We even see how that <code class="highlighter-rouge">_T</code> one that we didn’t understand earlier is used; it’s just temporary storage. And if the parameter validation passes, all the game does to implement the command is set the environment variable with the same name as the object (but with an underscore prepended) to the value of the current location, which is stored in the variable <code class="highlighter-rouge">_L</code>.</p>
<p>In DOS (and still today in some places in Windows), surrounding the name of an environment variable in <code class="highlighter-rouge">%</code> is how you retrieve its value, so the <code class="highlighter-rouge">set _T</code> commands are setting <code class="highlighter-rouge">_T</code> to the value of one of the four inventory object variables (which we’ve already seen hold the name of the location where that object currently is).</p>
<p>And that’s how the whole game works. The other commands are all the same, with different checks and different messages based on whatever the relevant environment variables are. That’s all you need to have for a complete text adventure game.</p>
<h2 id="but-wait">But Wait</h2>
<p>The game is short, and I don’t want to spoil anything here, so I won’t show any more of it. But if you do play through to the end, you’ll receive this message:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>As you emerge from the cellar, its entrance collapses behind you! You find
yourself in a sunny clearing with a statue of a smiling crowned man.
On its base is an inscription: 'You have passed the first test. Your
reward is a single word: <redacted>. Remember it well, for it will aid you
on the dark road that lies ahead. Play Art D's Next Batch Adventure and
fulfill your destiny!'
</code></pre></div></div>
<p>I even redacted the secret world, that’s how serious I am about not spoiling anything.</p>
<p>But yes! There is an Art D’s Next Batch Adventure! Privateer was successful enough to get an expansion pack, called Righteous Fire, and so was Art D’s First Batch Adventure deemed worthy of a sequel to include with it. You’ll find the batch file for this one in the same place, but the file name you’re looking for now is <code class="highlighter-rouge">TABTXE.NDA</code>. There’s even a third Batch Adventure, included in a World War I flight simulator called <a href="https://www.mobygames.com/game/dos/wings-of-glory">Wings of Glory</a>, which Art D isn’t actually credited on (it appears he had left Origin to do <a href="https://www.mobygames.com/game/ascendancy">his own thing</a> by then).</p>
<p>And that’s it! I’m out of things to talk about here. I don’t have any conclusion to draw or point I was trying to make, I just hope you enjoyed going along on this nostalgia trip with me to see this crazy, awesome, ridiculous thing.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>Code snippets that I quote here will not be identical to what’s in the original file, because I’ve undone a bit of what looks like intentional obfuscation. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>molly howellOr: How to Make a Text Adventure Game Out of DOS Batch FilesWrapping a .exe installer in an MSI using WiX2020-04-05T00:00:00+00:002020-04-05T00:00:00+00:00https://mistake.computer/2020/04/05/msi-exe-wrapper<p>For Firefox on Windows, we have an <a href="https://firefox-source-docs.mozilla.org/browser/installer/windows/installer/MSI.html">MSI-based installer</a>, but as its docs explain, it isn’t a “real” MSI; it doesn’t make any effort to use the MSI framework to run installer logic. Instead it’s a thin wrapper over the <a href="https://firefox-source-docs.mozilla.org/browser/installer/windows/installer/FullInstaller.html">full installer</a>. The MSI framework isn’t really supposed to let you do that, and neither is the <a href="https://wixtoolset.org/">WiX toolset</a> that we use to generate the packages. So, how does it work? There’s a few aspects to it, and we’ll go through each one. Feel free to follow along in our <a href="https://searchfox.org/mozilla-central/source/browser/installer/windows/msi/installer.wxs">WiX source file</a>.</p>
<p>I’ll be assuming basic familiarity with MSI and WiX, so a quick look at <a href="https://www.firegiant.com/wix/tutorial/">its tutorial</a> might be in order first.</p>
<h1 id="embed-the-executable-in-the-msi-package">Embed the executable in the MSI package</h1>
<p>The first step is to get the executable installer itself bundled into the MSI package. Fortunately that’s easy; you only need one XML element and there’s nothing tricky or non-obvious about it:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- This can go anywhere inside your Product element. --></span>
<span class="nt"><Binary</span> <span class="na">Id=</span><span class="s">"WrappedExe"</span> <span class="na">SourceFile=</span><span class="s">"$(var.ExeSourcePath)"</span> <span class="nt">/></span></code></pre></figure>
<p>That’s it. Now our MSI contains a copy of the installer we want to run (inside its Binary table).</p>
<h1 id="run-the-executable">Run the executable</h1>
<p>To get that installer to run, we’ll need to create a custom action and then add it to the install action sequence. The custom action we need to create looks something like this:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- This can also go anywhere inside your Product element. --></span>
<span class="nt"><CustomAction</span> <span class="na">Id=</span><span class="s">"RunInstaller"</span> <span class="na">Return=</span><span class="s">"check"</span> <span class="na">Execute=</span><span class="s">"deferred"</span>
<span class="na">HideTarget=</span><span class="s">"no"</span> <span class="na">Impersonate=</span><span class="s">"no"</span> <span class="na">BinaryKey=</span><span class="s">"WrappedExe"</span>
<span class="na">ExeCommand=</span><span class="s">"/S"</span> <span class="nt">/></span></code></pre></figure>
<p>Note that the <code class="highlighter-rouge">BinaryKey</code> attribute is set to the same string as the <code class="highlighter-rouge">Id</code> of the <code class="highlighter-rouge">Binary</code> element. And if you have any command-line parameters your installer needs (like a switch to make it run silently, as I’ve included here), add those in the <code class="highlighter-rouge">ExeCommand</code> attribute.</p>
<p>Now that we have an action defined, we need to get it to actually run by adding it to the sequence:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><InstallExecuteSequence></span>
<span class="nt"><Custom</span> <span class="na">Action=</span><span class="s">"RunInstaller"</span> <span class="na">After=</span><span class="s">"ProcessComponents"</span> <span class="nt">/></span>
<span class="nt"></InstallExecuteSequence></span></code></pre></figure>
<p>Again here, the <code class="highlighter-rouge">Action</code> attribute’s value needs to match the <code class="highlighter-rouge">Id</code> of the <code class="highlighter-rouge">CustomAction</code>.</p>
<h1 id="add-a-few-required-boilerplate-elements">Add a few required boilerplate elements</h1>
<p>MSI requires that we have at least one Cabinet, Component, and Feature, or it will refuse to load our file. So we’ll add in the WiX elements that create all those things:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- The cabinet file doesn't actually exist, it's just a placeholder. --></span>
<span class="nt"><Media</span> <span class="na">Id=</span><span class="s">"1"</span> <span class="na">Cabinet=</span><span class="s">"setup.cab"</span> <span class="na">EmbedCab=</span><span class="s">"yes"</span> <span class="nt">/></span>
<span class="nt"><Directory</span> <span class="na">Id=</span><span class="s">"TARGETDIR"</span> <span class="na">Name=</span><span class="s">"SourceDir"</span><span class="nt">></span>
<span class="nt"><Directory</span> <span class="na">Id=</span><span class="s">"TempFolder"</span><span class="nt">></span>
<span class="c"><!-- Generate your own GUID for the component. --></span>
<span class="nt"><Component</span> <span class="na">Id=</span><span class="s">"EmptyComponent"</span> <span class="na">Guid=</span><span class="s">"c98a7cdd-1cba-4220-8d99-4ef977aeecff"</span><span class="nt">></span>
<span class="nt"><CreateFolder</span> <span class="nt">/></span>
<span class="nt"></Component></span>
<span class="nt"></Directory></span>
<span class="nt"></Directory></span>
<span class="c"><!-- Setting the feature to level 0 marks it hidden, so it can't be installed.
That prevents getting this MSI registered as an installed product,
because it has no features of its own to install. --></span>
<span class="nt"><Feature</span> <span class="na">Id=</span><span class="s">"EmptyFeature"</span> <span class="na">Level=</span><span class="s">"0"</span><span class="nt">></span>
<span class="nt"><ComponentRef</span> <span class="na">Id=</span><span class="s">"EmptyComponent"</span> <span class="nt">/></span>
<span class="nt"></Feature></span></code></pre></figure>
<p>As the comment says, having a single non-installable feature prevents any installed products, components, or features from being registered with MSI, which means installing our MSI won’t create an uninstall entry. Since the bundled installer is going to create its own uninstall entry, this trick prevents us from having two entries registered for the same product.</p>
<h1 id="handle-msi-properties">Handle MSI properties</h1>
<p>The Firefox full installer can take a number of <a href="https://firefox-source-docs.mozilla.org/browser/installer/windows/installer/FullConfig.html">command-line arguments</a>. We would like for the MSI to provide access to those, so anyone deploying the MSI is still able to customize their deployment, and we’d like for that access to be something typical to how other MSI installer work. So, we support customization in the form of MSI properties. For every possible command-line option, we implement a corresponding property and pass its value through to the installer command line.</p>
<p>To make that happen, first we’ll create a WiX <code class="highlighter-rouge">Property</code> for each thing we want to allow customizing. Here’s one of the several that are implemented for Firefox:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="nt"><Property</span> <span class="na">Id=</span><span class="s">"EXTRACT_DIR"</span> <span class="na">Value=</span><span class="s">"__DEFAULT__"</span> <span class="nt">/></span></code></pre></figure>
<p>Now, we need our custom action to implement these properties, that is, to insert them into the command line it invokes the installer with. For Firefox, we have an extra complication here: some of the command line arguments are mutually exclusive. That means we need some logic to determine which arguments we should include and which we should leave out, based on the ones that the administrator has configured with non-default values (that’s why we use a loud, conspicuous default string for the values that aren’t just a boolean flag).</p>
<p>What all that means is that we need a) more than one <code class="highlighter-rouge">CustomAction</code>, with the only difference between them being which set of mutually-exclusive arguments they include in their <code class="highlighter-rouge">ExeCommand</code> attribute, and b) for each <code class="highlighter-rouge">CustomAction</code>, one corresponding <code class="highlighter-rouge">Custom</code> element in the <code class="highlighter-rouge">InstallExecuteSequence</code> that includes the necessary logic to select it and only it to run when the right properties are set. These get pretty verbose in Firefox, so I’ll just show one pair of those elements as an example:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"> <span class="nt"><CustomAction</span> <span class="na">Id=</span><span class="s">"RunExtractOnly"</span> <span class="na">Return=</span><span class="s">"check"</span> <span class="na">Execute=</span><span class="s">"deferred"</span>
<span class="na">HideTarget=</span><span class="s">"no"</span> <span class="na">Impersonate=</span><span class="s">"no"</span> <span class="na">BinaryKey=</span><span class="s">"WrappedExe"</span>
<span class="na">ExeCommand=</span><span class="s">"/ExtractDir=[EXTRACT_DIR]"</span> <span class="nt">/></span>
<span class="nt"><InstallExecuteSequence></span>
<span class="c"><!-- Other Custom elements would be here. --></span>
<span class="nt"><Custom</span> <span class="na">Action=</span><span class="s">"RunExtractOnly"</span> <span class="na">After=</span><span class="s">"ProcessComponents"</span><span class="nt">></span>
<span class="c"><!-- Only run this action when the EXTRACT_DIR property is set. --></span>
<span class="cp"><![CDATA[EXTRACT_DIR <> "__DEFAULT__"]]></span>
<span class="nt"></Custom></span>
<span class="nt"></InstallExecuteSequence></span></code></pre></figure>
<p>And that’s everything, that’s pretty much how the Firefox MSI works.</p>molly howellFor Firefox on Windows, we have an MSI-based installer, but as its docs explain, it isn’t a “real” MSI; it doesn’t make any effort to use the MSI framework to run installer logic. Instead it’s a thin wrapper over the full installer. The MSI framework isn’t really supposed to let you do that, and neither is the WiX toolset that we use to generate the packages. So, how does it work? There’s a few aspects to it, and we’ll go through each one. Feel free to follow along in our WiX source file. I’ll be assuming basic familiarity with MSI and WiX, so a quick look at its tutorial might be in order first. Embed the executable in the MSI package The first step is to get the executable installer itself bundled into the MSI package. Fortunately that’s easy; you only need one XML element and there’s nothing tricky or non-obvious about it: <!-- This can go anywhere inside your Product element. --> <Binary Id="WrappedExe" SourceFile="$(var.ExeSourcePath)" /> That’s it. Now our MSI contains a copy of the installer we want to run (inside its Binary table). Run the executable To get that installer to run, we’ll need to create a custom action and then add it to the install action sequence. The custom action we need to create looks something like this: <!-- This can also go anywhere inside your Product element. --> <CustomAction Id="RunInstaller" Return="check" Execute="deferred" HideTarget="no" Impersonate="no" BinaryKey="WrappedExe" ExeCommand="/S" /> Note that the BinaryKey attribute is set to the same string as the Id of the Binary element. And if you have any command-line parameters your installer needs (like a switch to make it run silently, as I’ve included here), add those in the ExeCommand attribute. Now that we have an action defined, we need to get it to actually run by adding it to the sequence: <InstallExecuteSequence> <Custom Action="RunInstaller" After="ProcessComponents" /> </InstallExecuteSequence> Again here, the Action attribute’s value needs to match the Id of the CustomAction. Add a few required boilerplate elements MSI requires that we have at least one Cabinet, Component, and Feature, or it will refuse to load our file. So we’ll add in the WiX elements that create all those things: <!-- The cabinet file doesn't actually exist, it's just a placeholder. --> <Media Id="1" Cabinet="setup.cab" EmbedCab="yes" /> <Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="TempFolder"> <!-- Generate your own GUID for the component. --> <Component Id="EmptyComponent" Guid="c98a7cdd-1cba-4220-8d99-4ef977aeecff"> <CreateFolder /> </Component> </Directory> </Directory> <!-- Setting the feature to level 0 marks it hidden, so it can't be installed. That prevents getting this MSI registered as an installed product, because it has no features of its own to install. --> <Feature Id="EmptyFeature" Level="0"> <ComponentRef Id="EmptyComponent" /> </Feature> As the comment says, having a single non-installable feature prevents any installed products, components, or features from being registered with MSI, which means installing our MSI won’t create an uninstall entry. Since the bundled installer is going to create its own uninstall entry, this trick prevents us from having two entries registered for the same product. Handle MSI properties The Firefox full installer can take a number of command-line arguments. We would like for the MSI to provide access to those, so anyone deploying the MSI is still able to customize their deployment, and we’d like for that access to be something typical to how other MSI installer work. So, we support customization in the form of MSI properties. For every possible command-line option, we implement a corresponding property and pass its value through to the installer command line. To make that happen, first we’ll create a WiX Property for each thing we want to allow customizing. Here’s one of the several that are implemented for Firefox: <Property Id="EXTRACT_DIR" Value="__DEFAULT__" /> Now, we need our custom action to implement these properties, that is, to insert them into the command line it invokes the installer with. For Firefox, we have an extra complication here: some of the command line arguments are mutually exclusive. That means we need some logic to determine which arguments we should include and which we should leave out, based on the ones that the administrator has configured with non-default values (that’s why we use a loud, conspicuous default string for the values that aren’t just a boolean flag). What all that means is that we need a) more than one CustomAction, with the only difference between them being which set of mutually-exclusive arguments they include in their ExeCommand attribute, and b) for each CustomAction, one corresponding Custom element in the InstallExecuteSequence that includes the necessary logic to select it and only it to run when the right properties are set. These get pretty verbose in Firefox, so I’ll just show one pair of those elements as an example: <CustomAction Id="RunExtractOnly" Return="check" Execute="deferred" HideTarget="no" Impersonate="no" BinaryKey="WrappedExe" ExeCommand="/ExtractDir=[EXTRACT_DIR]" /> <InstallExecuteSequence> <!-- Other Custom elements would be here. --> <Custom Action="RunExtractOnly" After="ProcessComponents"> <!-- Only run this action when the EXTRACT_DIR property is set. --> <![CDATA[EXTRACT_DIR <> "__DEFAULT__"]]> </Custom> </InstallExecuteSequence> And that’s everything, that’s pretty much how the Firefox MSI works.