aboutsummaryrefslogtreecommitdiff
path: root/install.ps1
blob: b26923adad895d272524f6389fe7552af5b14c2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

#requires -Version 5.1
<#
.SYNOPSIS
Installs cpsc and its HTML manual for the current user.
.DESCRIPTION
Copies cpsc.exe and doc\cpsc.html, creates a Start Menu help shortcut, and
adds the installation directory to the user PATH. No administrator is needed
for the default location: %LOCALAPPDATA%\Programs\cpsc.
.EXAMPLE
.\install.ps1 -SourceDir .\build
.EXAMPLE
.\install.ps1 -Prefix 'D:\My Programs\cpsc'
#>
[CmdletBinding()]
param(
    [ValidateNotNullOrEmpty()]
    [string]$Prefix,
    [ValidateNotNullOrEmpty()]
    [string]$SourceDir = $PSScriptRoot,
    [switch]$Help
)

Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'

if ($Help) {
    Write-Output 'Usage: install.ps1 [-Prefix DIR] [-SourceDir DIR] [-Help]'
    Write-Output 'Default destination: %LOCALAPPDATA%\Programs\cpsc'
    Write-Output 'Source must contain cpsc.exe and doc\cpsc.html; default: script directory.'
    return
}

function Test-PathEntry([string]$PathValue, [string]$Directory) {
    $key = $Directory.Replace('/', '\').TrimEnd('\')
    foreach ($entry in ($PathValue -split ';')) {
        $expanded = [Environment]::ExpandEnvironmentVariables($entry.Trim().Trim('"'))
        if ($expanded.Replace('/', '\').TrimEnd('\') -ieq $key) {
            return $true
        }
    }
    return $false
}

try {
    if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
        throw 'This installer requires Windows.'
    }
    if (-not [string]::IsNullOrEmpty($env:DESTDIR)) {
        throw 'DESTDIR staging is unsupported on Windows; use -Prefix instead.'
    }
    if (-not $PSBoundParameters.ContainsKey('Prefix')) {
        $localAppData = [Environment]::GetFolderPath('LocalApplicationData')
        if ([string]::IsNullOrWhiteSpace($localAppData)) {
            throw 'Cannot locate the current user application directory; specify -Prefix.'
        }
        $Prefix = Join-Path $localAppData 'Programs\cpsc'
    }
    if ($Prefix -notmatch '^(?:[A-Za-z]:[\\/]|\\\\[^\\]+\\[^\\]+)' -or $Prefix.Contains(';')) {
        throw '-Prefix must be an absolute Windows path without a semicolon.'
    }
    $Prefix = [IO.Path]::GetFullPath($Prefix)
    if (-not (Test-Path -LiteralPath $SourceDir -PathType Container)) {
        throw "Cannot open source directory: $SourceDir"
    }
    $SourceDir = (Resolve-Path -LiteralPath $SourceDir).ProviderPath
    $sourceExe = Join-Path $SourceDir 'cpsc.exe'
    $sourceManual = Join-Path $SourceDir 'doc\cpsc.html'
    foreach ($source in @($sourceExe, $sourceManual)) {
        if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
            throw "Missing source file: $source; build cpsc and the doc target first."
        }
    }

    $docDir = Join-Path $Prefix 'doc'
    $targetExe = Join-Path $Prefix 'cpsc.exe'
    $targetManual = Join-Path $docDir 'cpsc.html'
    foreach ($directory in @($Prefix, $docDir)) {
        if ((Test-Path -LiteralPath $directory) -and
            -not (Test-Path -LiteralPath $directory -PathType Container)) {
            throw "Installation directory is occupied by a file: $directory"
        }
    }
    foreach ($file in @($targetExe, $targetManual)) {
        if (Test-Path -LiteralPath $file -PathType Container) {
            throw "Installation file is occupied by a directory: $file"
        }
    }
    $programs = [Environment]::GetFolderPath('Programs')
    if ([string]::IsNullOrWhiteSpace($programs)) {
        throw 'Cannot locate the current user Start Menu.'
    }
    $shortcutDir = Join-Path $programs 'cpsc'
    $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
    $shell = New-Object -ComObject WScript.Shell

    # Directory.CreateDirectory treats paths literally, including spaces and brackets.
    [IO.Directory]::CreateDirectory($docDir) | Out-Null
    if ($sourceExe -ine $targetExe) {
        Copy-Item -LiteralPath $sourceExe -Destination $targetExe -Force
    }
    if ($sourceManual -ine $targetManual) {
        Copy-Item -LiteralPath $sourceManual -Destination $targetManual -Force
    }
    [IO.Directory]::CreateDirectory($shortcutDir) | Out-Null
    $shortcut = $shell.CreateShortcut((Join-Path $shortcutDir 'cpsc Help.lnk'))
    $shortcut.TargetPath = $targetManual
    $shortcut.WorkingDirectory = $docDir
    $shortcut.Description = 'cpsc command-line manual'
    $shortcut.Save()

    # Read only the user PATH; never persist the combined process/system PATH.
    if (-not (Test-PathEntry $userPath $Prefix)) {
        $separator = if ([string]::IsNullOrEmpty($userPath) -or $userPath.EndsWith(';')) { '' } else { ';' }
        [Environment]::SetEnvironmentVariable('Path', "$userPath$separator$Prefix", 'User')
    }
    if (-not (Test-PathEntry $env:PATH $Prefix)) {
        $env:PATH = "$env:PATH;$Prefix"
    }
    Write-Output "Installed $targetExe"
    Write-Output "Installed $targetManual"
    Write-Output 'Open the manual from Start Menu > cpsc > cpsc Help.'
    Write-Output 'Open a new terminal to use cpsc by name (existing parent terminals keep their old PATH).'
}
catch {
    Write-Error "install.ps1: $($_.Exception.Message) Check the destination permissions and supplied paths." -ErrorAction Continue
    exit 1
}