Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

*.png binary
*.jpg binary
*.ico binary

config/* diff
config/applications.json diff
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Microsoft.PowerShell.ConsoleHost.dll
winutil.exe.config
winutil.ps1
binary/
installer/

# general software/os specific
desktop.ini
Expand Down
96 changes: 96 additions & 0 deletions build-installer.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<#
.SYNOPSIS
Compiles winutil.ps1 and builds a Windows installer (WinUtil-Setup-<version>.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."
}
Binary file modified docs/static/favicon.ico
Binary file not shown.
120 changes: 120 additions & 0 deletions winutil.iss
Original file line number Diff line number Diff line change
@@ -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;
Loading