<< Hide left pane
Leave feedback
Creating Self-Installing Packages
Self-installing packages usually consist of a small launcher
application and an archive that contains the main installer and
application components to be installed. The launcher extracts
the archive into a temporary directory, runs the main installer,
waits for its completion and then deletes temporary files. The
launcher must be small and it has to run on any version of
Windows.
This article focuses on creating of the self-installing
package launcher. It does not explain how to create the main
installer. You can use one of commertial packages such as
InstallShield for that purpose, or you can write your own
installer. This is entirely up to you.
Composing a Self-Installing Package
The first thing we have to decide is which compression algorithm
to use. Another question is how to combine the launcher and the
archive into a single piece. Remember, we want the launcher to be
as small as possible, so an ideal solution would be to use a
compression scheme which is already built into the operating
system. Windows cabinet files provide reasonably good compression
and can be easily expanded using the SetupIterateCabinet
function, which we will describe later in this article.
To create a cabinet file you can use the cabarc utility,
which is shipped with the Platform SDK. Using this utility is
pretty straightforward, just run it without arguments to see
the list of available options:
Microsoft (R) Cabinet Tool - Version 5.00.2134.1
Copyright (C) Microsoft Corp. 1981-1999.
Usage: CABARC [options] command cabfile [@list] [files] [dest_dir]
Commands:
L List contents of cabinet (e.g. cabarc l test.cab)
N Create new cabinet (e.g. cabarc n test.cab *.c app.mak *.h)
X Extract file(s) from cabinet (e.g. cabarc x test.cab foo*.c)
Options:
-c Confirm files to be operated on
-o When extracting, overwrite without asking for confirmation
-m Set compression type [LZX:<15..21>|MSZIP|NONE], (default is MSZIP)
-p Preserve path names (absolute paths not allowed)
-P Strip specified prefix from files when added
-r Recurse into subdirectories when adding files (see -p also)
-s Reserve space in cabinet for signing (e.g. -s 6144 reserves 6K
bytes)
-i Set cabinet set ID when creating cabinets (default is 0)
-d Set diskette size (default is no limit/single CAB)
Notes
-----
When creating a cabinet, the plus sign (+) may be used as a filename
to force a folder boundary; e.g. cabarc n test.cab *.c test.h + *.bmp
When extracting files to disk, the <dest_dir>, if provided, must end in
a backslash; e.g. cabarc x test.cab bar*.cpp *.h d:\test\
The -P (strip prefix) option can be used to strip out path information
e.g. cabarc -r -p -P myproj\ a test.cab myproj\balloon\*.*
The -P option can be used multiple times to strip out multiple paths
When creating cabinet sets using -d, the cabinet name should contain
a single '*' character where the cabinet number will be inserted.
There are several ways in which you can combine the launcher
and the cabinet file. First, you can include it as a resource into
the laucher executable. Though this method makes it easy to access
the cabinet from the launcher's code using Win32 API resource functions,
it is not convenient in creating installation packages, since you
have to recompile the launcher each time you are creating a new
installation package.
Another way is to simply append the cabinet to the end of the
launcher executable. This is considerably more convenient in creating
installation packages, because you can compile the launcher once and
then use simple copy command to create a self-installing package:
copy lauch.exe /B + myinst.cab /B myinst.exe /B
This is the way we will use in our self-installer, but it poses
us with a problem: when it comes to extract the cabinet file and launch
the main installer, we need to decide where laucher's code ends
and the appended cabinet starts in the resulting file. Once we
know the offset to the cabinet, accessing it is a piece of cake,
but how to find that offset?
Finding Cabinet File
Of course, we can hardcode the pure launcher's size in its own
code. This will make compiling the launcher a bit tricky: you will
need to compile it once, note the executable size, update a
constant in the code and then compile the code again, hoping
that this modification will not change the executable size.
Well, I'd prefer a more clever way of doing that, and, this way
really exists.
The solution comes from the Portable Executable file format,
which is used for EXE and DLL files in 32-bit Windows. PE-files
(which are also called PE-images, since they represent in-memory
data layout very closely) contain headers and a number of sections.
Size of every section is stored in the headers. Thus, if we
sum up size of the headers and all section in the image, we should
get the size of the executable. Because the headers are left intact
when we append a cabinet to the executable, the calculated value
can be used as an offset to the beginning of the archive data.
The following function can be used to calculate PE-image size
based on its headers:
ULONG __fastcall GetSizeOfImage(
IN PVOID pImageBase
)
{
_ASSERTE(pImageBase != NULL);
IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)pImageBase;
IMAGE_FILE_HEADER * pFileHeader =
(IMAGE_FILE_HEADER *)(((LPBYTE)pImageBase) +
pDosHeader->e_lfanew +
sizeof(IMAGE_NT_SIGNATURE));
IMAGE_OPTIONAL_HEADER * pOptHeader =
(IMAGE_OPTIONAL_HEADER *)(((LPBYTE)pFileHeader) +
IMAGE_SIZEOF_FILE_HEADER);
ULONG nSizeOfImage = pOptHeader->SizeOfHeaders;
IMAGE_SECTION_HEADER * pSecHeader =
(IMAGE_SECTION_HEADER *)(((LPBYTE)pOptHeader) +
pFileHeader->SizeOfOptionalHeader);
for (int i = 0; i < pFileHeader->NumberOfSections; i++, pSecHeader++)
nSizeOfImage += pSecHeader->SizeOfRawData;
return nSizeOfImage;
}
The only parameter of this function is the base address at which
the EXE file was loaded into memory. When an executable file, EXE or DLL,
is loaded, its headers are loaded too. Knowing the base address of the
executable file in memory, we can access the headers without explicitly
reading the file. How to find that address? In Win32, the module instance
handle, which is passed to WinMain and DllMain, is the base
address of the module, so we can just cast HINSTANCE to PVOID and pass it
to this function.
Now, when we know where to find the cabinet file in our executable,
we can write the code that detaches the cabinet and stores it separately.
Why we ever need to store the cabinet in a separate file, why we can't read
it directly from our file since we already know its offset? This is because
the SetupIterateCabinet function we are going to use can only deal
with cabinet files stored separately. It is possible to extract the cabinet
directly from our file using more powerful cabinet.dll services, let
this enhancement be your homework.
Below is the source code of the SplitBaggage function, which
stores the cabinet file into a temporary directory and returns its path.
DWORD SplitBaggage(
IN HINSTANCE hInstance,
IN PCTSTR pszTempPath,
OUT PTSTR pszCabinetPath
)
{
_ASSERTE(hInstance != NULL);
_ASSERTE(pszTempPath != NULL);
_ASSERTE(pszCabinetPath != NULL);
_ASSERTE(*pszTempPath != 0);
ULONG nSizeOfImage = GetSizeOfImage((PVOID)hInstance);
// map it into the memory manually
TCHAR szFilePath[MAX_PATH];
GetModuleFileName(NULL, szFilePath, MAX_PATH);
DWORD dwError = 0;
HANDLE hExeFile = INVALID_HANDLE_VALUE;
HANDLE hCabFile = INVALID_HANDLE_VALUE;
HANDLE hMapping = NULL;
PVOID pImageBase = NULL;
hExeFile = CreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hExeFile == INVALID_HANDLE_VALUE)
return GetLastError();
for (;;)
{
ULONG nFileSize = GetFileSize(hExeFile, NULL);
_ASSERTE(nFileSize >= nSizeOfImage);
if (nSizeOfImage == nFileSize)
{
dwError = ERROR_FILE_NOT_FOUND;
break;
}
hMapping = CreateFileMapping(hExeFile, NULL, PAGE_READONLY,
0, 0, NULL);
if (hMapping == NULL)
{
dwError = GetLastError();
break;
}
pImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (pImageBase == NULL)
{
dwError = GetLastError();
break;
}
GetTempFileName(pszTempPath, _T("~ca"), 0, pszCabinetPath);
hCabFile = CreateFile(pszCabinetPath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, 0, NULL);
if (hCabFile == INVALID_HANDLE_VALUE)
{
dwError = GetLastError();
break;
}
PUCHAR pEndOfImage = (PUCHAR)pImageBase + nSizeOfImage;
if (pEndOfImage[0] == 'N' &&
pEndOfImage[1] == 'B' &&
pEndOfImage[2] == '1' &&
pEndOfImage[3] == '0')
{
pEndOfImage += 16;
while (*pEndOfImage != 0)
pEndOfImage++;
nSizeOfImage = pEndOfImage - (PUCHAR)pImageBase + 1;
}
if (nSizeOfImage == nFileSize)
{
dwError = ERROR_FILE_NOT_FOUND;
break;
}
ULONG cbWritten;
if (!WriteFile(hCabFile, (PUCHAR)pImageBase + nSizeOfImage,
nFileSize - nSizeOfImage, &cbWritten, NULL))
dwError = GetLastError();
break;
}
if (hCabFile != INVALID_HANDLE_VALUE)
CloseHandle(hCabFile);
if (pImageBase != NULL)
UnmapViewOfFile(pImageBase);
if (hMapping != NULL)
CloseHandle(hMapping);
if (hExeFile !=INVALID_HANDLE_VALUE)
CloseHandle(hExeFile);
return dwError;
}
While the system loader maps image headers into memory when loading
an executable, it completely ignores anything what might be appended to
the end of the file (in fact, the loader looks at the same section table
we used to calculare image size). Because of this we have to map the
whole file into memory again and then use WriteFile to store
the cabinet separately.
One interesting moment in the SplitBaggage code is the check
for debug information reference. If you choose the debug information to
be stored in a separate .pdb file, the Visual C++ linker adds a
reference to this file to the end of the executable file using exactly
the same technique as we do with the cabinet file. This means, however,
that our size calculation is no longer accurate, since it doesn't take
into account this debug information reference. The SplitBaggage
function copes with it by checking for 'NB10' signature immediately
after the nominal end of the file, which indicates presence of debug
information stored in a .pdb file. If the signature is found, the
function tries to skip the reference and corrects the cabinet file
offset correspondingly.
Expanding Cabinet File
Now we have our cabinet stored separately somewhere in the file
system, but we still don't have access to the files inside the cabinet.
Our next goal is to expand the cabinet file so we can invoke the
main installer.
As I already mentioned, to expand the cabinet file we will use
the SetupIterateCabinet function which is exported from
setupapi.dll. An alternative approach is to use the more powerful
cabinet.dll services (you can find cabinet.dll header and library
files in
Microsoft
Cabinet SDK).
BOOL SetupIterateCabinet(
PCTSTR CabinetFile,
DWORD Reserved,
PSP_FILE_CALLBACK MsgHandler,
PVOID Context
);
When using SetupIterateCabinet we supply it a path to the cabinet file
and a pointer to our callback function. SetupIterateCabinet analyzes the
cabinet file and calls our function when something interesting occurs, for example,
when a new file is about to be created. The following are the ExpandCabinet
function, which expands the cabinet, and the CabinetCallback function,
which is used as a callback function for SetupIterateCabinet.
typedef struct tagCTX {
PCTSTR pszDestPath;
} CTX;
UINT CALLBACK CabinetCallback(
IN PVOID pCtx,
IN UINT uNotify,
IN UINT_PTR uParam1,
IN UINT_PTR uParam2
)
{
_UNUSED(uParam2);
switch (uNotify)
{
case SPFILENOTIFY_FILEINCABINET:
{
FILE_IN_CABINET_INFO * pInfo =
(FILE_IN_CABINET_INFO *)uParam1;
_ASSERTE(pInfo != NULL);
lstrcpy(pInfo->FullTargetName, ((CTX *)pCtx)->pszDestPath);
lstrcat(pInfo->FullTargetName, _T("\\"));
lstrcat(pInfo->FullTargetName, pInfo->NameInCabinet);
VerifyPathExists(pInfo->FullTargetName);
return FILEOP_DOIT;
}
case SPFILENOTIFY_FILEEXTRACTED:
{
FILEPATHS * pPaths = (FILEPATHS *)uParam1;
_ASSERTE(pPaths != NULL);
return pPaths->Win32Error;
}
case SPFILENOTIFY_NEEDNEWCABINET:
{
_ASSERTE(0);
return ERROR_INVALID_PARAMETER;
}
}
return ERROR_SUCCESS;
}
DWORD ExpandCabinet(
IN PCTSTR pszCabinetPath,
IN PCTSTR pszTempPath,
OUT PTSTR pszSetupPath
)
{
_ASSERTE(pszCabinetPath != NULL);
_ASSERTE(pszTempPath != NULL);
_ASSERTE(pszSetupPath != NULL);
_ASSERTE(*pszCabinetPath != 0);
_ASSERTE(*pszTempPath != 0);
DWORD dwError = 0;
CTX ctx;
GetTempFileName(pszTempPath, _T("~ca"), 0, pszSetupPath);
DeleteFile(pszSetupPath);
ctx.pszDestPath = pszSetupPath;
if (!CreateDirectory(pszSetupPath, NULL) ||
!SetupIterateCabinet(pszCabinetPath, 0, CabinetCallback, &ctx))
dwError = GetLastError();
DeleteFile(pszCabinetPath);
return dwError;
}
The ExpandCabinet function is pretty straightforward. It just
creates a temporary directory and initializes a context structure to hold
the path to that directory. Then it calls SetupIterateCabinet passing
it cabinet file path, a pointer to the CabinetCallback function,
and a pointer to the context structure.
The callback function ignores most of notifications sent by
SetupIterateCabinet. The only notification which has meaningful
processing is SPFILENOTIFY_FILEINCABINET notification. Using this
notification, SetupIterateCabinet informs the callback function
that a new file is about to be extracted from the cabinet. The callback
function should in turn provide a path where the file should be placed.
CabinetCallback just appends the file name stored in the cabinet
to the root path, which is passed in the context structure. After that
it calls VerifyPathExists to create any directories that might
appear on the path, because SetupIterateCabinet won't create any
directories for us.
DWORD VerifyPathExists(
IN PCTSTR pszPathName
)
{
_ASSERTE(pszPathName != NULL);
TCHAR szPath[MAX_PATH];
lstrcpy(szPath, pszPathName);
PTSTR psz = _tcschr(szPath, _T('\\'));
_ASSERTE(psz != NULL);
psz++;
for (;;)
{
psz = _tcschr(psz, _T('\\'));
if (psz == NULL)
return 0;
*psz = 0;
if (GetFileAttributes(szPath) == 0xFFFFFFFF)
{
if (!CreateDirectory(szPath, NULL))
return GetLastError();
}
*psz++ = _T('\\');
}
_ASSERTE(0);
__assume(0);
}
When ExpandCabinet exits, it deletes the original cabinet file
since we don't need it anymore. Now we have all cabinet files extracted
into a temporary directory and our next step is to launch the main
installer program.
Running Main Installer
The first question is which executable to run. I solved this simple:
the main installer executable must be called setup.exe and must
reside in the root directory of the cabinet.
Another point to consider is a command line to the installer. Some
installers allow to specify a command line, for example, to enable silent
operation when the installer doesn't show any UI or produce any user
messages (this is particulary useful to incorporate your packages into
larger products; in fact, many Microsoft's redistributable packages work
this way). Thus, our launcher should pass any its command-line parameters
to the main installer. To make running the main installer completely
transparent, we also should grab the exit code of the main installer
process and use it as our exit code. This way, running our self-extracting
package will not differ from running the main installer directly.
The LaunchSetup function parses the launcher's
command line and calls the main installer. It then waits until
the installer completes and returns its exit code.
DWORD LaunchSetup(
IN PCTSTR pszSetupPath,
OUT PINT pnExitCode
)
{
_ASSERTE(pszSetupPath != NULL);
_ASSERTE(pnExitCode != NULL);
_ASSERTE(*pszSetupPath != 0);
PTSTR pszCmdLine = GetCommandLine();
_ASSERTE(pszCmdLine != NULL);
TCHAR chTerm = (*pszCmdLine == _T('\"')) ? _T('\"') : _T(' ');
pszCmdLine = CharNext(pszCmdLine);
while (*pszCmdLine != 0 && *pszCmdLine != chTerm)
pszCmdLine = CharNext(pszCmdLine);
if (*pszCmdLine == _T('\"'))
pszCmdLine++;
STARTUPINFO si;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
TCHAR szCmdLine[MAX_PATH];
szCmdLine[0] = _T('\"');
lstrcpy(szCmdLine + 1, pszSetupPath);
lstrcat(szCmdLine, _T("\""));
lstrcat(szCmdLine, pszCmdLine);
if (!CreateProcess(pszSetupPath, pszCmdLine, NULL, NULL, FALSE, 0,
NULL, NULL, &si, &pi))
return GetLastError();
MSG msg;
while (MsgWaitForMultipleObjects(1, &pi.hProcess, FALSE, INFINITE,
QS_ALLINPUT) == WAIT_OBJECT_0 + 1)
{
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
DispatchMessage(&msg);
}
GetExitCodeProcess(pi.hProcess, (PULONG)pnExitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
Cleaning Up
When the main installer completes, regardless of whether it was
successful or not, we have to delete all temporary files we created
on the user's system. The cabinet file is already deleted in the
ExpandCabinet function, now we should delete the files
extracted from the cabinet.
Since all extracted files were placed into one temporary directory
we can blindly delete this directory and all files that happened to
be inside this directory. This is done by the DelTree function,
which source code is shown below.
VOID DelTree(
IN PTSTR pszPath
)
{
_ASSERTE(pszPath != 0);
int len = lstrlen(pszPath);
lstrcpy(pszPath + len, _T("\\*.*"));
WIN32_FIND_DATA data;
HANDLE hFind = FindFirstFile(pszPath, &data);
if (hFind == INVALID_HANDLE_VALUE)
{
pszPath[len] = 0;
return;
}
do
{
if (lstrcmp(data.cFileName, _T(".")) == 0 ||
lstrcmp(data.cFileName, _T("..")) == 0)
continue;
lstrcpy(pszPath + len + 1, data.cFileName);
if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
DelTree(pszPath);
else
DeleteFile(pszPath);
}
while (FindNextFile(hFind, &data));
FindClose(hFind);
pszPath[len] = 0;
RemoveDirectory(pszPath);
}
Where Is WinMain?
So far we have discussed all steps of creating a self-installing
package, but we are still missing something. The missing part is the
WinMain function – a glue for all previously discussed pieces
of code.
int WINAPI WinMain(
IN HINSTANCE hInstance,
IN HINSTANCE hPrevInstance,
IN PSTR pszCmdLine,
IN int nCmdShow
)
{
_UNUSED(hPrevInstance);
_UNUSED(pszCmdLine);
_UNUSED(nCmdShow);
_ASSERTE(hInstance != NULL);
TCHAR szTempPath[MAX_PATH];
TCHAR szCabinetPath[MAX_PATH];
TCHAR szSetupPath[MAX_PATH];
OSVERSIONINFO osvi;
osvi.dwOSVersionInfoSize = sizeof(osvi);
_VERIFY(GetVersionEx(&osvi));
if (osvi.dwMajorVersion < 4)
{
TCHAR szBuffer[2048];
_VERIFY(LoadString(hInstance, IDS_OSVERSION, szBuffer,
countof(szBuffer)));
MessageBox(NULL, szBuffer, NULL, MB_OK|MB_ICONSTOP);
return -1;
}
GetTempPath(MAX_PATH, szTempPath);
DWORD dwError;
int nExitCode = -1;
dwError = SplitBaggage(hInstance, szTempPath, szCabinetPath);
if (dwError == ERROR_SUCCESS)
{
dwError = ExpandCabinet(szCabinetPath, szTempPath, szSetupPath);
if (dwError == ERROR_SUCCESS)
{
int len = lstrlen(szSetupPath);
lstrcpy(szSetupPath + len, _T("\\setup.exe"));
dwError = LaunchSetup(szSetupPath, &nExitCode);
szSetupPath[len] = 0;
DelTree(szSetupPath);
}
}
if (dwError != ERROR_SUCCESS)
{
TCHAR szBuffer[2048];
if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwError,
0, szBuffer, 2048, NULL))
{
MessageBox(NULL, szBuffer, NULL, MB_OK|MB_ICONSTOP);
}
}
return nExitCode;
}
If you put all the pieces together and compile the resulting
program, you'll find that its size is about 30KB. Not so bad, but
remember, we still want as smallest executable as possible, isn't
there a way to reduce its size?
Let's look into the .map file, generated by the linker: most of
the program's code is the standard C run-time library (CRT) functions
and CRT initialization code. Stop. In the launcher code we didn't use
a single CRT function, neither we used static initializers. That means
that it is possible to completely bypass CRT initialization and throw
all this fatty stuff out of our executable.
By default, the entry point for Windows GUI application is
WinMainCRTStartup. This function invokes CRT initialization
code and then calls WinMain. When WinMain returns,
CRT cleanup is called and control is passed to ExitProcess
which never returns. If we define WinMainCRTStartup in such
a way so CRT initialization code is not called, the CRT code
will not be linked into the program and its size will be smaller.
#ifndef _DEBUG
EXTERN_C void __cdecl WinMainCRTStartup()
{
ExitProcess(WinMain(GetModuleHandle(NULL), NULL, NULL, 0));
}
#endif
WinMainCRTStartup code is very simple because we don't need any
CRT initialization, we don't even need a command line. Also note that
the entry point is only redefined for Release builds. In Debug builds,
we want _ASSERTE macros to work, so we need CRT initialization.
After implementing WinMainCRTStartup, launch.exe size drops to
6KB. Feel the difference!
Code Signing Considerations
When software is distributed over the Internet, users are more likely
to trust software which is digitally signed by the software publisher.
You can sign a self-insalling package as usual, just make sure to sign
the whole package after you combine the launcher and the cabinet
together. This will allow the browser to validate the signature if
your package is installed directly from the Internet as, for
example, from this link (this file
is signed with a test certificate which is normally not trusted by
the browser).
Sample Code
To illustrate the usage of the launcher, I made a tiny setup.exe
program, which doesn't install anything, but displays a message box
that shows the command line to the program. Looking at the command
line you can find the temporary files location used by the launcher
and verify that command-line parameters are propagated properly.
References
- Matt Pietrek, Peering
Inside the PE: A Tour of the Win32 Portable Executable File Format.
MSDN Library, March 1994.
- HOWTO:
Use the SetupAPI's SetupIterateCabinet() Function, Q189085,
Microsoft Knowledge Base.
- Matt Pietrek, Reduce
EXE and DLL Size with LIBCTINY.LIB, MSDN Magazine, January 2001.