Files
kjol/c/installer/installer.c
2026-07-13 13:02:47 -04:00

605 lines
20 KiB
C

#define _CRT_SECURE_NO_WARNINGS
// installer.c — Self-contained Windows installer for codeMAX
//
// Uses Win32 PropertySheet wizard for the UI, embeds payload EXEs as
// resources, and handles PATH, Start Menu, and Add/Remove Programs.
//
// When invoked with /uninstall, runs the uninstaller instead.
#include <windows.h>
#include <commctrl.h>
#include <shlobj.h>
#include <objbase.h>
#include <shobjidl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(lib, "uuid.lib")
#pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
#include "installer.h"
// Use codebase types
typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;
typedef unsigned long long U64;
typedef int S32;
typedef int B32;
#define ArrayCount(a) (sizeof(a) / sizeof((a)[0]))
////////////////////////////////
// Globals
static wchar_t g_install_dir[MAX_PATH];
static B32 g_add_to_path = 1;
static HINSTANCE g_hinst;
////////////////////////////////
// Helpers
static B32 extract_resource(S32 resource_id, const wchar_t *dest_path) {
HRSRC res = FindResourceW(NULL, MAKEINTRESOURCEW(resource_id), (LPCWSTR)RT_RCDATA);
if (!res) return 0;
HGLOBAL h = LoadResource(NULL, res);
if (!h) return 0;
void *data = LockResource(h);
DWORD size = SizeofResource(NULL, res);
if (!data || size == 0) return 0;
HANDLE f = CreateFileW(dest_path, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (f == INVALID_HANDLE_VALUE) return 0;
DWORD written;
WriteFile(f, data, size, &written, NULL);
CloseHandle(f);
return written == size;
}
static B32 create_directory_recursive(const wchar_t *path) {
wchar_t tmp[MAX_PATH];
wcscpy_s(tmp, MAX_PATH, path);
for (wchar_t *p = tmp + 3; *p; p++) { // skip "C:\"
if (*p == L'\\' || *p == L'/') {
*p = 0;
CreateDirectoryW(tmp, NULL);
*p = L'\\';
}
}
return CreateDirectoryW(tmp, NULL) || GetLastError() == ERROR_ALREADY_EXISTS;
}
static B32 create_shortcut(const wchar_t *lnk_path, const wchar_t *target,
const wchar_t *work_dir, const wchar_t *description,
const wchar_t *icon_path) {
HRESULT hr;
IShellLinkW *sl = NULL;
hr = CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
&IID_IShellLinkW, (void **)&sl);
if (FAILED(hr)) return 0;
sl->lpVtbl->SetPath(sl, target);
sl->lpVtbl->SetWorkingDirectory(sl, work_dir);
if (description) sl->lpVtbl->SetDescription(sl, description);
if (icon_path) sl->lpVtbl->SetIconLocation(sl, icon_path, 0);
IPersistFile *pf = NULL;
hr = sl->lpVtbl->QueryInterface(sl, &IID_IPersistFile, (void **)&pf);
B32 ok = 0;
if (SUCCEEDED(hr)) {
ok = SUCCEEDED(pf->lpVtbl->Save(pf, lnk_path, TRUE));
pf->lpVtbl->Release(pf);
}
sl->lpVtbl->Release(sl);
return ok;
}
static B32 path_add(const wchar_t *dir) {
HKEY key;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return 0;
wchar_t buf[8192] = {0};
DWORD buf_size = sizeof(buf) - sizeof(wchar_t);
DWORD type = REG_EXPAND_SZ;
RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size);
// Check if already present
if (wcsstr(buf, dir)) {
RegCloseKey(key);
return 1;
}
// Append
S32 len = (S32)wcslen(buf);
if (len > 0 && buf[len - 1] != L';')
wcscat_s(buf, ArrayCount(buf), L";");
wcscat_s(buf, ArrayCount(buf), dir);
RegSetValueExW(key, L"Path", 0, type,
(BYTE *)buf, (DWORD)((wcslen(buf) + 1) * sizeof(wchar_t)));
RegCloseKey(key);
return 1;
}
static void path_remove(const wchar_t *dir) {
HKEY key;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment",
0, KEY_READ | KEY_WRITE, &key) != ERROR_SUCCESS) return;
wchar_t buf[8192] = {0};
DWORD buf_size = sizeof(buf) - sizeof(wchar_t);
DWORD type = REG_EXPAND_SZ;
RegQueryValueExW(key, L"Path", NULL, &type, (BYTE *)buf, &buf_size);
wchar_t result[8192] = {0};
wchar_t *ctx = NULL;
wchar_t copy[8192];
wcscpy_s(copy, ArrayCount(copy), buf);
wchar_t *tok = wcstok_s(copy, L";", &ctx);
B32 first = 1;
while (tok) {
if (_wcsicmp(tok, dir) != 0) {
if (!first) wcscat_s(result, ArrayCount(result), L";");
wcscat_s(result, ArrayCount(result), tok);
first = 0;
}
tok = wcstok_s(NULL, L";", &ctx);
}
RegSetValueExW(key, L"Path", 0, type,
(BYTE *)result, (DWORD)((wcslen(result) + 1) * sizeof(wchar_t)));
RegCloseKey(key);
}
static void broadcast_env_change(void) {
SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0,
(LPARAM)L"Environment", SMTO_ABORTIFHUNG, 5000, NULL);
}
////////////////////////////////
// In-memory dialog template builder
typedef struct {
U8 data[4096];
S32 len;
} DlgBuf;
static void dlg_align(DlgBuf *b, S32 align) {
while (b->len % align) b->data[b->len++] = 0;
}
static void dlg_write(DlgBuf *b, const void *src, S32 n) {
memcpy(b->data + b->len, src, n);
b->len += n;
}
static void dlg_write16(DlgBuf *b, U16 v) { dlg_write(b, &v, 2); }
static void dlg_write32(DlgBuf *b, U32 v) { dlg_write(b, &v, 4); }
static void dlg_write_wstr(DlgBuf *b, const wchar_t *s) {
S32 n = (S32)((wcslen(s) + 1) * sizeof(wchar_t));
dlg_write(b, s, n);
}
static DLGTEMPLATE *build_page_template(DlgBuf *b, S32 w, S32 h) {
b->len = 0;
// DLGTEMPLATE
DLGTEMPLATE *dt = (DLGTEMPLATE *)b->data;
dlg_write32(b, WS_CHILD | WS_VISIBLE | DS_SHELLFONT); // style
dlg_write32(b, 0); // dwExtendedStyle
dlg_write16(b, 0); // cdit (control count, filled later)
dlg_write16(b, 0); // x
dlg_write16(b, 0); // y
dlg_write16(b, (U16)w); // cx
dlg_write16(b, (U16)h); // cy
dlg_write16(b, 0); // menu (none)
dlg_write16(b, 0); // class (default)
dlg_write_wstr(b, L""); // title
// DS_SHELLFONT font
dlg_write16(b, 9); // point size
dlg_write_wstr(b, L"Segoe UI");
return dt;
}
static void add_control(DlgBuf *b, DLGTEMPLATE *dt, U32 style, S32 x, S32 y,
S32 w, S32 h, S32 id, const wchar_t *cls, const wchar_t *text) {
dlg_align(b, 4);
// DLGITEMTEMPLATE
dlg_write32(b, style | WS_CHILD | WS_VISIBLE); // style
dlg_write32(b, 0); // dwExtendedStyle
dlg_write16(b, (U16)x);
dlg_write16(b, (U16)y);
dlg_write16(b, (U16)w);
dlg_write16(b, (U16)h);
dlg_write16(b, (U16)id);
dlg_write_wstr(b, cls);
dlg_write_wstr(b, text);
dlg_write16(b, 0); // creation data
dt->cdit++;
}
////////////////////////////////
// Page IDs for controls
#define IDC_DIR_EDIT 1001
#define IDC_DIR_BROWSE 1002
#define IDC_PATH_CHK 1003
#define IDC_PROGRESS 1004
#define IDC_STATUS 1005
#define IDC_LAUNCH_CHK 1006
////////////////////////////////
// Wizard pages
static INT_PTR CALLBACK welcome_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT);
SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0);
}
return TRUE;
}
}
return FALSE;
}
static INT_PTR CALLBACK dir_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
SetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir);
CheckDlgButton(dlg, IDC_PATH_CHK, g_add_to_path ? BST_CHECKED : BST_UNCHECKED);
return TRUE;
case WM_COMMAND:
if (LOWORD(wp) == IDC_DIR_BROWSE) {
BROWSEINFOW bi = {0};
bi.hwndOwner = dlg;
bi.lpszTitle = L"Select installation directory:";
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
LPITEMIDLIST pidl = SHBrowseForFolderW(&bi);
if (pidl) {
wchar_t path[MAX_PATH];
SHGetPathFromIDListW(pidl, path);
wcscat_s(path, MAX_PATH, L"\\codeMAX");
SetDlgItemTextW(dlg, IDC_DIR_EDIT, path);
CoTaskMemFree(pidl);
}
}
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE)
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK | PSWIZB_NEXT);
if (nm->code == PSN_WIZNEXT) {
GetDlgItemTextW(dlg, IDC_DIR_EDIT, g_install_dir, MAX_PATH);
g_add_to_path = (IsDlgButtonChecked(dlg, IDC_PATH_CHK) == BST_CHECKED);
}
return TRUE;
}
}
return FALSE;
}
static B32 do_install(HWND dlg) {
HWND prog = GetDlgItem(dlg, IDC_PROGRESS);
HWND status = GetDlgItem(dlg, IDC_STATUS);
SendMessageW(prog, PBM_SETRANGE, 0, MAKELPARAM(0, 7));
SendMessageW(prog, PBM_SETSTEP, 1, 0);
// 1. Create install directory
SetWindowTextW(status, L"Creating install directory...");
create_directory_recursive(g_install_dir);
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 2. Extract terminal exe
SetWindowTextW(status, L"Installing codemax.exe...");
wchar_t path[MAX_PATH];
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
if (!extract_resource(IDR_EXE_GUI, path)) {
MessageBoxW(dlg, L"Failed to extract codemax.exe", L"Error", MB_ICONERROR);
return 0;
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 4. Copy self as uninstaller
SetWindowTextW(status, L"Creating uninstaller...");
wchar_t self_path[MAX_PATH];
GetModuleFileNameW(NULL, self_path, MAX_PATH);
_snwprintf(path, MAX_PATH, L"%s\\uninstall.exe", g_install_dir);
CopyFileW(self_path, path, FALSE);
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 5. Add to PATH (optional)
if (g_add_to_path) {
SetWindowTextW(status, L"Updating PATH...");
path_add(g_install_dir);
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 6. Create Start Menu shortcuts
SetWindowTextW(status, L"Creating shortcuts...");
CoInitialize(NULL);
{
wchar_t programs[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs);
wchar_t menu_dir[MAX_PATH];
_snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs);
CreateDirectoryW(menu_dir, NULL);
wchar_t exe_path[MAX_PATH];
_snwprintf(exe_path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
wchar_t lnk[MAX_PATH];
_snwprintf(lnk, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir);
create_shortcut(lnk, exe_path, g_install_dir,
L"codeMAX Text Editor", exe_path);
}
CoUninitialize();
SendMessageW(prog, PBM_STEPIT, 0, 0);
// 7. Register in Add/Remove Programs
SetWindowTextW(status, L"Registering application...");
{
HKEY key;
RegCreateKeyExA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY,
0, NULL, 0, KEY_WRITE, NULL, &key, NULL);
char dir_a[MAX_PATH], uninst[MAX_PATH * 2];
WideCharToMultiByte(CP_UTF8, 0, g_install_dir, -1, dir_a, MAX_PATH, NULL, NULL);
snprintf(uninst, sizeof(uninst), "\"%s\\uninstall.exe\" /uninstall", dir_a);
RegSetValueExA(key, "DisplayName", 0, REG_SZ,
(BYTE *)CODEMAX_DISPLAY_NAME, (DWORD)strlen(CODEMAX_DISPLAY_NAME) + 1);
RegSetValueExA(key, "DisplayVersion", 0, REG_SZ,
(BYTE *)CODEMAX_VERSION, (DWORD)strlen(CODEMAX_VERSION) + 1);
RegSetValueExA(key, "Publisher", 0, REG_SZ,
(BYTE *)CODEMAX_PUBLISHER, (DWORD)strlen(CODEMAX_PUBLISHER) + 1);
RegSetValueExA(key, "InstallLocation", 0, REG_SZ,
(BYTE *)dir_a, (DWORD)strlen(dir_a) + 1);
RegSetValueExA(key, "UninstallString", 0, REG_SZ,
(BYTE *)uninst, (DWORD)strlen(uninst) + 1);
char icon[MAX_PATH + 4];
snprintf(icon, sizeof(icon), "%s\\codemax.exe,0", dir_a);
RegSetValueExA(key, "DisplayIcon", 0, REG_SZ,
(BYTE *)icon, (DWORD)strlen(icon) + 1);
DWORD one = 1;
RegSetValueExA(key, "NoModify", 0, REG_DWORD, (BYTE *)&one, sizeof(one));
RegSetValueExA(key, "NoRepair", 0, REG_DWORD, (BYTE *)&one, sizeof(one));
RegCloseKey(key);
}
SendMessageW(prog, PBM_STEPIT, 0, 0);
broadcast_env_change();
SetWindowTextW(status, L"Installation complete.");
return 1;
}
static INT_PTR CALLBACK progress_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
// Disable all buttons during install
PropSheet_SetWizButtons(GetParent(dlg), 0);
// Run install
if (do_install(dlg)) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_NEXT);
} else {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_BACK);
}
SetWindowLongPtrW(dlg, DWLP_MSGRESULT, 0);
}
return TRUE;
}
}
return FALSE;
}
static INT_PTR CALLBACK finish_proc(HWND dlg, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_INITDIALOG:
CheckDlgButton(dlg, IDC_LAUNCH_CHK, BST_CHECKED);
return TRUE;
case WM_NOTIFY: {
NMHDR *nm = (NMHDR *)lp;
if (nm->code == PSN_SETACTIVE) {
PropSheet_SetWizButtons(GetParent(dlg), PSWIZB_FINISH);
}
if (nm->code == PSN_WIZFINISH) {
if (IsDlgButtonChecked(dlg, IDC_LAUNCH_CHK) == BST_CHECKED) {
wchar_t exe[MAX_PATH];
_snwprintf(exe, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
ShellExecuteW(NULL, L"open", exe, NULL, g_install_dir, SW_SHOWNORMAL);
}
}
return TRUE;
}
}
return FALSE;
}
////////////////////////////////
// Wizard launcher
static void run_installer(void) {
// Default install dir
wcscpy_s(g_install_dir, MAX_PATH, L"C:\\Program Files\\codeMAX");
InitCommonControls();
// Build page templates in memory
DlgBuf bufs[4] = {0};
S32 page_w = 317, page_h = 143;
// Page 0: Welcome
DLGTEMPLATE *dt0 = build_page_template(&bufs[0], page_w, page_h);
add_control(&bufs[0], dt0, SS_LEFT, 10, 10, 297, 40, -1,
L"Static", L"Welcome to the codeMAX installer.\n\n"
L"This will install codeMAX on your computer.");
add_control(&bufs[0], dt0, SS_LEFT, 10, 60, 297, 20, -1,
L"Static", L"Click Next to continue.");
// Page 1: Directory + options
DLGTEMPLATE *dt1 = build_page_template(&bufs[1], page_w, page_h);
add_control(&bufs[1], dt1, SS_LEFT, 10, 10, 297, 10, -1,
L"Static", L"Choose the installation directory:");
add_control(&bufs[1], dt1, ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP,
10, 28, 230, 14, IDC_DIR_EDIT, L"Edit", L"");
add_control(&bufs[1], dt1, BS_PUSHBUTTON | WS_TABSTOP,
248, 27, 60, 14, IDC_DIR_BROWSE, L"Button", L"Browse...");
add_control(&bufs[1], dt1, BS_AUTOCHECKBOX | WS_TABSTOP,
10, 52, 250, 14, IDC_PATH_CHK, L"Button", L"Add to system PATH");
// Page 2: Progress
DLGTEMPLATE *dt2 = build_page_template(&bufs[2], page_w, page_h);
add_control(&bufs[2], dt2, SS_LEFT, 10, 10, 297, 10, IDC_STATUS,
L"Static", L"Installing...");
add_control(&bufs[2], dt2, 0, 10, 30, 297, 14, IDC_PROGRESS,
PROGRESS_CLASSW, L"");
// Page 3: Finish
DLGTEMPLATE *dt3 = build_page_template(&bufs[3], page_w, page_h);
add_control(&bufs[3], dt3, SS_LEFT, 10, 10, 297, 20, -1,
L"Static", L"codeMAX has been installed successfully.");
add_control(&bufs[3], dt3, BS_AUTOCHECKBOX | WS_TABSTOP,
10, 45, 200, 14, IDC_LAUNCH_CHK, L"Button", L"Launch codeMAX GUI");
PROPSHEETPAGEW pages[4] = {0};
for (S32 i = 0; i < 4; i++) {
pages[i].dwSize = sizeof(PROPSHEETPAGEW);
pages[i].dwFlags = PSP_DLGINDIRECT;
pages[i].hInstance = g_hinst;
}
pages[0].pResource = dt0;
pages[0].pfnDlgProc = welcome_proc;
pages[1].pResource = dt1;
pages[1].pfnDlgProc = dir_proc;
pages[2].pResource = dt2;
pages[2].pfnDlgProc = progress_proc;
pages[3].pResource = dt3;
pages[3].pfnDlgProc = finish_proc;
PROPSHEETHEADERW psh = {0};
psh.dwSize = sizeof(PROPSHEETHEADERW);
psh.dwFlags = PSH_WIZARD | PSH_PROPSHEETPAGE | PSH_USEICONID;
psh.hwndParent = NULL;
psh.hInstance = g_hinst;
psh.pszIcon = MAKEINTRESOURCEW(IDI_CODEMAX);
psh.pszCaption = L"codeMAX Setup";
psh.nPages = 4;
psh.ppsp = pages;
PropertySheetW(&psh);
}
////////////////////////////////
// Uninstaller
static void run_uninstaller(void) {
// Get install dir from our own exe path
wchar_t self[MAX_PATH];
GetModuleFileNameW(NULL, self, MAX_PATH);
wcscpy_s(g_install_dir, MAX_PATH, self);
wchar_t *last_sep = wcsrchr(g_install_dir, L'\\');
if (last_sep) *last_sep = 0;
S32 result = MessageBoxW(NULL,
L"Are you sure you want to uninstall codeMAX?",
L"codeMAX Uninstall", MB_YESNO | MB_ICONQUESTION);
if (result != IDYES) return;
// Delete installed files
wchar_t path[MAX_PATH];
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
DeleteFileW(path);
_snwprintf(path, MAX_PATH, L"%s\\codemax.exe", g_install_dir);
DeleteFileW(path);
// Remove from PATH
path_remove(g_install_dir);
broadcast_env_change();
// Remove Start Menu shortcuts
{
wchar_t programs[MAX_PATH];
SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, programs);
wchar_t menu_dir[MAX_PATH];
_snwprintf(menu_dir, MAX_PATH, L"%s\\codeMAX", programs);
_snwprintf(path, MAX_PATH, L"%s\\codeMAX.lnk", menu_dir);
DeleteFileW(path);
_snwprintf(path, MAX_PATH, L"%s\\codeMAX GUI.lnk", menu_dir);
DeleteFileW(path);
RemoveDirectoryW(menu_dir);
}
// Remove registry entry
RegDeleteKeyA(HKEY_LOCAL_MACHINE, CODEMAX_UNINSTALL_KEY);
// Schedule self-deletion and remove install dir
// Use cmd.exe to wait for us to exit, then delete
wchar_t cmd[MAX_PATH * 3];
_snwprintf(cmd, ArrayCount(cmd),
L"cmd.exe /c timeout /t 2 /nobreak >nul & del \"%s\\uninstall.exe\" & rmdir \"%s\"",
g_install_dir, g_install_dir);
STARTUPINFOW si = {0};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi = {0};
CreateProcessW(NULL, cmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
MessageBoxW(NULL, L"codeMAX has been uninstalled.", L"codeMAX Uninstall",
MB_OK | MB_ICONINFORMATION);
}
////////////////////////////////
// Entry point
int main(int argc, char **argv) {
(void)argc;
g_hinst = GetModuleHandleW(NULL);
// Check for uninstall flag
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "/uninstall") == 0) {
run_uninstaller();
return 0;
}
}
// Also check via GetCommandLineW for when launched without CRT argc/argv
wchar_t *cmdline = GetCommandLineW();
if (wcsstr(cmdline, L"/uninstall")) {
run_uninstaller();
return 0;
}
run_installer();
return 0;
}