diff --git a/.gitattributes b/.gitattributes index da0b45d5a2..29660f68a5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ *.png binary *.jpg binary +*.ico binary config/* diff config/applications.json diff diff --git a/.gitignore b/.gitignore index a97fe97dec..548864410a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Microsoft.PowerShell.ConsoleHost.dll winutil.exe.config winutil.ps1 binary/ +installer/ # general software/os specific desktop.ini diff --git a/build-installer.ps1 b/build-installer.ps1 new file mode 100644 index 0000000000..4a2abafadb --- /dev/null +++ b/build-installer.ps1 @@ -0,0 +1,96 @@ +<# +.SYNOPSIS + Compiles winutil.ps1 and builds a Windows installer (WinUtil-Setup-.exe). + +.DESCRIPTION + 1. Runs Compile.ps1 to bundle all functions + configs into winutil.ps1. + 2. Locates Inno Setup 6 (ISCC.exe) — installs it via winget if missing. + 3. Invokes ISCC.exe with winutil.iss to produce the installer under .\installer\. + +.EXAMPLE + .\build-installer.ps1 + .\build-installer.ps1 -SkipCompile # reuse an existing winutil.ps1 + .\build-installer.ps1 -Version "26.06.05" +#> + +param ( + [switch]$SkipCompile, + [string]$Version = (Get-Date -Format 'yy.MM.dd') +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$projectRoot = $PSScriptRoot + +function Find-ISCC { + $candidates = @( + 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe', + 'C:\Program Files\Inno Setup 6\ISCC.exe', + "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe", + (Get-Command iscc -ErrorAction SilentlyContinue)?.Source + ) | Where-Object { $_ -and (Test-Path $_) } + return $candidates | Select-Object -First 1 +} + +# ── Step 1: Compile ────────────────────────────────────────────────────────── +if (-not $SkipCompile) { + Write-Host "Compiling winutil.ps1..." -ForegroundColor Cyan + & "$projectRoot\Compile.ps1" + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { + throw "Compile.ps1 failed (exit $LASTEXITCODE)." + } + Write-Host "Compile succeeded." -ForegroundColor Green +} else { + Write-Host "Skipping compile (-SkipCompile)." -ForegroundColor Yellow +} + +$winutilPs1 = Join-Path $projectRoot 'winutil.ps1' +if (-not (Test-Path $winutilPs1)) { + throw "winutil.ps1 not found at '$winutilPs1'. Run without -SkipCompile first." +} + +# ── Step 2: Locate / install Inno Setup ────────────────────────────────────── +$iscc = Find-ISCC +if (-not $iscc) { + Write-Host "Inno Setup 6 not found. Attempting install via winget..." -ForegroundColor Yellow + winget install --id JRSoftware.InnoSetup --silent --accept-source-agreements --accept-package-agreements + $iscc = Find-ISCC +} + +if (-not $iscc) { + Write-Error @" +Inno Setup 6 (ISCC.exe) was not found and could not be installed automatically. + +Please install it manually from: + https://jrsoftware.org/isdl.php + +Then re-run this script. +"@ + exit 1 +} + +Write-Host "Using ISCC: $iscc" -ForegroundColor Cyan + +# ── Step 3: Create output directory ───────────────────────────────────────── +$installerDir = Join-Path $projectRoot 'installer' +if (-not (Test-Path $installerDir)) { + New-Item -ItemType Directory -Path $installerDir | Out-Null +} + +# ── Step 4: Build installer ────────────────────────────────────────────────── +$issFile = Join-Path $projectRoot 'winutil.iss' +Write-Host "Building installer (version $Version)..." -ForegroundColor Cyan + +& $iscc "/DMyAppVersion=$Version" $issFile + +if ($LASTEXITCODE -ne 0) { + throw "ISCC.exe failed (exit $LASTEXITCODE)." +} + +$outputExe = Join-Path $installerDir "WinUtil-Setup-$Version.exe" +if (Test-Path $outputExe) { + Write-Host "`nInstaller ready: $outputExe" -ForegroundColor Green +} else { + Write-Warning "Build reported success but '$outputExe' was not found. Check ISCC output above." +} diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico index f95955c6de..ff0cee0a81 100644 Binary files a/docs/static/favicon.ico and b/docs/static/favicon.ico differ diff --git a/winutil.iss b/winutil.iss new file mode 100644 index 0000000000..d0a8072c4e --- /dev/null +++ b/winutil.iss @@ -0,0 +1,120 @@ +; Inno Setup Script for WinUtil +; Chris Titus Tech's Windows Utility +; Build with: ISCC.exe winutil.iss +; Or run: .\build-installer.ps1 + +#define MyAppName "WinUtil" +#define MyAppPublisher "Chris Titus Tech" +#define MyAppURL "https://christitus.com" +#define MyAppDescription "Chris Titus Tech Windows Utility" + +[Setup] +AppId={{8B7A5E3C-4F2D-4E8A-B1C0-9D6F8E2A3B7F} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +AllowNoIcons=no +OutputDir=installer +OutputBaseFilename=WinUtil-Setup-{#MyAppVersion} +SetupIconFile=docs\static\favicon.ico +Compression=lzma2 +SolidCompression=yes +WizardStyle=modern +; Installer itself requires admin; the app also self-elevates on launch +PrivilegesRequired=admin +UninstallDisplayIcon={app}\favicon.ico +UninstallDisplayName={#MyAppName} +VersionInfoVersion=1.0.0.0 +VersionInfoCompany={#MyAppPublisher} +VersionInfoDescription={#MyAppDescription} +; Prevent downgrade installs silently overwriting a newer version +VersionInfoProductVersion={#MyAppVersion} + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce + +[Files] +Source: "winutil.ps1"; DestDir: "{app}"; Flags: ignoreversion +Source: "docs\static\favicon.ico"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +; Start Menu shortcut — targets powershell.exe; winutil.ps1 self-elevates to admin via UAC +Name: "{group}\{#MyAppName}"; \ + Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; \ + Parameters: "-ExecutionPolicy Bypass -NoProfile -File ""{app}\winutil.ps1"""; \ + WorkingDir: "{app}"; \ + IconFilename: "{app}\favicon.ico"; \ + Comment: "Launch Chris Titus Tech's Windows Utility" + +Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" + +; Optional desktop shortcut (checked by default once) +Name: "{commondesktop}\{#MyAppName}"; \ + Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; \ + Parameters: "-ExecutionPolicy Bypass -NoProfile -File ""{app}\winutil.ps1"""; \ + WorkingDir: "{app}"; \ + IconFilename: "{app}\favicon.ico"; \ + Comment: "Launch Chris Titus Tech's Windows Utility"; \ + Tasks: desktopicon + +[Run] +; Offer to launch WinUtil immediately after installation +Filename: "{sys}\WindowsPowerShell\v1.0\powershell.exe"; \ + Parameters: "-ExecutionPolicy Bypass -NoProfile -File ""{app}\winutil.ps1"""; \ + WorkingDir: "{app}"; \ + Description: "Launch {#MyAppName} now"; \ + Flags: nowait postinstall skipifsilent shellexec + +[Code] +// Set the "Run as administrator" flag on both shortcuts after creation. +// The .lnk format stores this in byte 21 (0-indexed), bit 5 (0x20). +procedure SetShortcutRunAsAdmin(LinkPath: String); +var + FileStream: TFileStream; + FlagByte: Byte; + Buffer: AnsiString; +begin + if not FileExists(LinkPath) then Exit; + try + FileStream := TFileStream.Create(LinkPath, fmOpenReadWrite); + try + FileStream.Seek(21, soFromBeginning); + SetLength(Buffer, 1); + FileStream.ReadBuffer(Buffer[1], 1); + FlagByte := Ord(Buffer[1]) or $20; + Buffer[1] := Chr(FlagByte); + FileStream.Seek(21, soFromBeginning); + FileStream.WriteBuffer(Buffer[1], 1); + finally + FileStream.Free; + end; + except + // Non-fatal: shortcuts still work without the admin flag; + // winutil.ps1 self-elevates via UAC on first run anyway. + end; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + StartMenuLink: String; + DesktopLink: String; +begin + if CurStep = ssPostInstall then begin + StartMenuLink := ExpandConstant('{group}\WinUtil.lnk'); + SetShortcutRunAsAdmin(StartMenuLink); + + if WizardIsTaskSelected('desktopicon') then begin + DesktopLink := ExpandConstant('{commondesktop}\WinUtil.lnk'); + SetShortcutRunAsAdmin(DesktopLink); + end; + end; +end;