Alex Fedotov.com 

Abstract

When distributing applications over Internet, you will definitely want to compress the installation package to reduce download time, especially for users with slow modem connections. The next thing you will want to do is to combine both the decompression utility and the intallation package into one program, so users will be able to download and install your program with just a single mouse click. This article explains how to implement such a self-installing package using Win32 API.

<< 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:

//-----------------------------------------------------------------------
// GetSizeOfImage
//
//  Determines size of the executeble from its headers. This will give us
//  an offset to cabinet file, which is simply appended to the end of the
//  executable.
//
//  Parameters:
//    pImageBase - base address of the current module
//
//  Returns:
//    size of the executable in bytes
//
ULONG __fastcall GetSizeOfImage(
    IN PVOID pImageBase
    )
{
    _ASSERTE(pImageBase != NULL);

    // get DOS header
    IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)pImageBase;

    // find an offset to the main PE header ...
    IMAGE_FILE_HEADER * pFileHeader = 
        (IMAGE_FILE_HEADER *)(((LPBYTE)pImageBase) + 
                      pDosHeader->e_lfanew + 
                      sizeof(IMAGE_NT_SIGNATURE));

    // ... and optional PE header
    IMAGE_OPTIONAL_HEADER * pOptHeader = 
        (IMAGE_OPTIONAL_HEADER *)(((LPBYTE)pFileHeader) + 
                      IMAGE_SIZEOF_FILE_HEADER);

    // calculate the size
    ULONG nSizeOfImage = pOptHeader->SizeOfHeaders;

    IMAGE_SECTION_HEADER * pSecHeader = 
        (IMAGE_SECTION_HEADER *)(((LPBYTE)pOptHeader) + 
                     pFileHeader->SizeOfOptionalHeader);

    // sum size of all image sections; this will result in the image
    // size
    for (int i = 0; i < pFileHeader->NumberOfSections; i++, pSecHeader++)
        nSizeOfImage += pSecHeader->SizeOfRawData;

    // return size of the executable
    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.

//-----------------------------------------------------------------------
// SplitBaggage
//
//  Stores cab-file, appended to the executable, into a temporary direc-
//  tory and returns the path to the file.
//
//  Parameters:
//    hInstance      - module instance handle
//    pszTempPath    - path to the Windows temporary directory
//    pszCabinetPath - pointer to a buffer that receives full path of
//                     the cabinet file; the buffer must be at least
//                     MAX_PATH characters long
//
//  Returns:
//    Win32 error code.
//
DWORD SplitBaggage(
    IN HINSTANCE hInstance, 
    IN PCTSTR pszTempPath, 
    OUT PTSTR pszCabinetPath
    )
{
    _ASSERTE(hInstance != NULL);
    _ASSERTE(pszTempPath != NULL);
    _ASSERTE(pszCabinetPath != NULL);
    _ASSERTE(*pszTempPath != 0);

    // determine size of the exe-file according to its headers; note that
    // in Win32, hInstance is a base address where a module was loaded
    ULONG nSizeOfImage = GetSizeOfImage((PVOID)hInstance);

    // when loading the executable into memory, the system loader has
    // ignored a huge cabinet appended to the file, therefore, we must to
    // map it into the memory manually

    // determine file path
    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;

    // open the file; don't forget it is already used by the system 
    // (hence FILE_SHARE_READ)
    hExeFile = CreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ, 
                          NULL, OPEN_EXISTING, 0, NULL);
    if (hExeFile == INVALID_HANDLE_VALUE)
        return GetLastError();    // strange...

    for (;;)
    {
        // determine real file size
        ULONG nFileSize = GetFileSize(hExeFile, NULL);
        _ASSERTE(nFileSize >= nSizeOfImage);

        if (nSizeOfImage == nFileSize)
        {
            // no cabinet - nothing to do
            dwError = ERROR_FILE_NOT_FOUND;
            break;
        }

        // create file mapping object
        hMapping = CreateFileMapping(hExeFile, NULL, PAGE_READONLY,
                                     0, 0, NULL);
        if (hMapping == NULL)
        {
            dwError = GetLastError();
            break;
        }

        // map the entire file (we can't map only necessary part, 
        // since 64K alignment is required but the cabinet is appended 
	// at a random offset)
        pImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
        if (pImageBase == NULL)
        {
            dwError = GetLastError();
            break;
        }

        // create a name for the cabinet file and open it for writing
        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;

        // check for debug information reference
        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)
        {
            // no cabinet - nothing to do
            dwError = ERROR_FILE_NOT_FOUND;
            break;
        }

        // write the cabinet file
        ULONG cbWritten;
        if (!WriteFile(hCabFile, (PUCHAR)pImageBase + nSizeOfImage,
                       nFileSize - nSizeOfImage, &cbWritten, NULL))
            dwError = GetLastError();

        break;
    }

    // cleanup
    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,            // name of the cabinet file
    DWORD Reserved,                // this parameter is not used
    PSP_FILE_CALLBACK MsgHandler,  // callback routine to use
    PVOID Context                  // callback routine 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.

// context for SetupIterateCabinet
typedef struct tagCTX {
    PCTSTR pszDestPath;    // destination path
} CTX;

//-----------------------------------------------------------------------
// CabinetCallback
//
//  Called periodically in the process of unpacking of cabinet file.
//
//  Parameters:
//    pCtx    - pointer to a CTX structure
//    uNotify - notification code
//    uParam1 - notification parameter
//    uParam2 - notification parameter
//
//  Returns:
//    a return code, which is specific to the notification.
//
UINT CALLBACK CabinetCallback(
    IN PVOID pCtx, 
    IN UINT uNotify, 
    IN UINT_PTR uParam1, 
    IN UINT_PTR uParam2
    )
{
    _UNUSED(uParam2);

    switch (uNotify)
    {
        // SetupIterateCabinet is going to unpack a file and we have
        // to tell it where it should place the file
        case SPFILENOTIFY_FILEINCABINET:
        {
            FILE_IN_CABINET_INFO * pInfo = 
                (FILE_IN_CABINET_INFO *)uParam1;
            _ASSERTE(pInfo != NULL);

            // make a full path name
            lstrcpy(pInfo->FullTargetName, ((CTX *)pCtx)->pszDestPath);
            lstrcat(pInfo->FullTargetName, _T("\\"));
            lstrcat(pInfo->FullTargetName, pInfo->NameInCabinet);

            // make sure the path exists
            VerifyPathExists(pInfo->FullTargetName);

            // enable to unpack the file
            return FILEOP_DOIT;
        }

        // a file was extracted
        case SPFILENOTIFY_FILEEXTRACTED:
        {
            FILEPATHS * pPaths = (FILEPATHS *)uParam1;
            _ASSERTE(pPaths != NULL);
            return pPaths->Win32Error;
        }

        // a new cabinet file is required; this notification should never
        // appear in our program, since all files have to be in a single
        // cabinet
        case SPFILENOTIFY_NEEDNEWCABINET:
        {
            _ASSERTE(0);
            return ERROR_INVALID_PARAMETER;
        }
    }

    return ERROR_SUCCESS;
}

//-----------------------------------------------------------------------
// ExpandCabinet
//
//  Expands the cab-file and returns the path of the directory where it
//  has been expanded. The original cabinet file is deleted regarless of
//  the function is succeeded or not.
//
//  Parameters:
//    pszCabinetPath - a path to the cabinet file
//    pszTempPath    - a path to the Windows temporary directory
//    pszSetupPath   - pointer to a buffer that receives the full path
//                     to the directory where the cabinet file was
//                     expanded; this buffer must be at least MAX_PATH
//                     characters long
//
//  Returns:
//    Win32 error code.
//
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;

    // create a name for the temporary directory where we will place
    // extracted files
    GetTempFileName(pszTempPath, _T("~ca"), 0, pszSetupPath);
    DeleteFile(pszSetupPath);

    ctx.pszDestPath = pszSetupPath;

    // create the directory and expand the cabinet
    if (!CreateDirectory(pszSetupPath, NULL) ||
        !SetupIterateCabinet(pszCabinetPath, 0, CabinetCallback, &ctx))
        dwError = GetLastError();

    // clean up
    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.

//-----------------------------------------------------------------------
// VerifyPathExists
//
//  Given a file path, creates all directories in the path, to make sure
//  the path exists.
//
//  Parameters:
//    pszPathName - full file path
//
//  Returns:
//    Win32 error code.
//
DWORD VerifyPathExists(
    IN PCTSTR pszPathName
    )
{
    _ASSERTE(pszPathName != NULL);

    TCHAR szPath[MAX_PATH];
    lstrcpy(szPath, pszPathName);

    // find and skip the first entry of '\\' character, that separates
    // drive letter from the rest of the path
    PTSTR psz = _tcschr(szPath, _T('\\'));
    _ASSERTE(psz != NULL);
    psz++;

    // sequentially increase length of the path, while checking it
    // for existance
    for (;;)
    {
        // find the name of a directory
        psz = _tcschr(psz, _T('\\'));
        if (psz == NULL) 
            return 0;

        // temporarily substitute '\\' character with end 
        // of line character
        *psz = 0;

        // creater the directory if it does not exist
        if (GetFileAttributes(szPath) == 0xFFFFFFFF)
        {
            if (!CreateDirectory(szPath, NULL))
                return GetLastError();
        }

        // restore '\\' character and move ahead
        *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.

//-----------------------------------------------------------------------
// LaunchSetup
//
//  Launches setup.exe with command line parameters, picked from our
//  command line, and waits for its completion.
//
//  Parameters:
//    pszSetupPath - path to the setup.exe file
//    pnExitCode   - pointer to a variable that receives setup exit code
//
//  Returns:
//    Win32 error code.
//
DWORD LaunchSetup(
    IN PCTSTR pszSetupPath,
    OUT PINT pnExitCode
    )
{
    _ASSERTE(pszSetupPath != NULL);
    _ASSERTE(pnExitCode != NULL);
    _ASSERTE(*pszSetupPath != 0);

    // get the command line
    PTSTR pszCmdLine = GetCommandLine();
    _ASSERTE(pszCmdLine != NULL);

    // determine a process name delimiter character
    TCHAR chTerm = (*pszCmdLine == _T('\"')) ? _T('\"') : _T(' ');
    pszCmdLine = CharNext(pszCmdLine);

    // skip the name of the process
    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);

    // create setup.exe process
    if (!CreateProcess(pszSetupPath, pszCmdLine, NULL, NULL, FALSE, 0, 
                       NULL, NULL, &si, &pi))
        return GetLastError();

    // wait until the process completes
    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);
    
    // close handles
    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.

//-----------------------------------------------------------------------
// DelTree
//
//  Removes a directory along with all files and subdirectories
//
//  Parameters:
//    pszPath - path to the directory; the function uses this buffer 
//              both for reading and writing and its size must be at 
//              least MAX_PATH characters, although on exit the buffer
//              will contain the original string
//
//  Returns:
//    no return value.
//
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.

//-----------------------------------------------------------------------
// WinMain
//
//  Program entry point.
//
//  Parameters:
//    hInstance     - module instance handle
//    hPrevInstance - handle of the previous instance
//    pszCmdLine    - command line
//    nCmdShow	    - defines the way to show the main window
//
//  Returns:
//    an exit code for the process.
//
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);

    // determine Windows version and demand at least 4.0
    _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;
    }

    // get Windows temporary directory path
    GetTempPath(MAX_PATH, szTempPath);

    DWORD dwError;
    int nExitCode = -1;

    // store the cabinet file separate from the executable file
    dwError = SplitBaggage(hInstance, szTempPath, szCabinetPath);
    if (dwError == ERROR_SUCCESS)
    {
        // expand the cabinet file
        dwError = ExpandCabinet(szCabinetPath, szTempPath, szSetupPath);
        if (dwError == ERROR_SUCCESS)
        {
            // launch the main setup application
            int len = lstrlen(szSetupPath);
            lstrcpy(szSetupPath + len, _T("\\setup.exe"));
            dwError = LaunchSetup(szSetupPath, &nExitCode);

            // delete all temporary files and directories
            szSetupPath[len] = 0;
            DelTree(szSetupPath);
        }
    }

    if (dwError != ERROR_SUCCESS)
    {
        // tell the user about the error
        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

//-----------------------------------------------------------------------
// WinMainCRTStartup
//
//  This is the real process entry point.
//
//  Parameters:
//    none.
//
//  Returns:
//    this function does not return.
//
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

  1. Matt Pietrek, Peering Inside the PE: A Tour of the Win32 Portable Executable File Format. MSDN Library, March 1994.
  2. HOWTO: Use the SetupAPI's SetupIterateCabinet() Function, Q189085, Microsoft Knowledge Base.
  3. Matt Pietrek, Reduce EXE and DLL Size with LIBCTINY.LIB, MSDN Magazine, January 2001.

Leave your comment

From:
Subject:
Comment:
 
 
Best viewed with Microsoft Internet Explorer 4.0+Send feedback to: me@alexfedotov.com
Last modified on Fri, Feb 10 2006

HotLog