Implement a command-line installer for Windows

It can be generated with the Make target installer-windows.
It requires that you ran Make target launcher-windows before.
This commit is contained in:
Michael Herrmann 2019-03-04 16:34:32 +01:00
parent 6e28eb11b5
commit 8fd76c876b
4 changed files with 273 additions and 15 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@ aminal
aminal.exe
*.syso
.idea
tmp/
generated-src/

View File

@ -1,7 +1,7 @@
SHELL := /bin/bash
BINARY := aminal
FONTPATH := ./gui/packed-fonts
TMPDIR := ./tmp
GEN_SRC_DIR := ./generated-src
VERSION_MAJOR := 0
VERSION_MINOR := 9
VERSION_PATCH := 0
@ -57,19 +57,31 @@ build-windows:
.PHONY: launcher-windows
launcher-windows: build-windows
if exist "${TMPDIR}\launcher-src" rmdir /S /Q "${TMPDIR}\launcher-src"
xcopy "windows\launcher\*.*" "${TMPDIR}\launcher-src" /K /H /Y /Q /I
powershell -Command "(gc ${TMPDIR}\launcher-src\versioninfo.json) -replace 'VERSION_MAJOR', '${VERSION_MAJOR}' | Out-File -Encoding default ${TMPDIR}\launcher-src\versioninfo.json"
powershell -Command "(gc ${TMPDIR}\launcher-src\versioninfo.json) -replace 'VERSION_MINOR', '${VERSION_MINOR}' | Out-File -Encoding default ${TMPDIR}\launcher-src\versioninfo.json"
powershell -Command "(gc ${TMPDIR}\launcher-src\versioninfo.json) -replace 'VERSION_PATCH', '${VERSION_PATCH}' | Out-File -Encoding default ${TMPDIR}\launcher-src\versioninfo.json"
powershell -Command "(gc ${TMPDIR}\launcher-src\versioninfo.json) -replace 'VERSION', '${VERSION}' | Out-File -Encoding default ${TMPDIR}\launcher-src\versioninfo.json"
powershell -Command "(gc ${TMPDIR}\launcher-src\versioninfo.json) -replace 'YEAR', (Get-Date -UFormat '%Y') | Out-File -Encoding default ${TMPDIR}\launcher-src\versioninfo.json"
copy aminal.ico "${TMPDIR}\launcher-src" /Y
go generate "${TMPDIR}\launcher-src"
if exist "${TMPDIR}\launcher" rmdir /S /Q "${TMPDIR}\launcher"
mkdir "${TMPDIR}\launcher\Versions\${VERSION}"
go build -o "${TMPDIR}\launcher\${BINARY}.exe" -ldflags "-H windowsgui" "${TMPDIR}\launcher-src"
copy ${BINARY}-windows-amd64.exe "${TMPDIR}\launcher\Versions\${VERSION}\${BINARY}.exe" /Y
if exist "${GEN_SRC_DIR}\launcher" rmdir /S /Q "${GEN_SRC_DIR}\launcher"
xcopy "windows\launcher\*.*" "${GEN_SRC_DIR}\launcher" /K /H /Y /Q /I
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MAJOR', '${VERSION_MAJOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MINOR', '${VERSION_MINOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_PATCH', '${VERSION_PATCH}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'YEAR', (Get-Date -UFormat '%Y') | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
copy aminal.ico "${GEN_SRC_DIR}\launcher" /Y
go generate "${GEN_SRC_DIR}\launcher"
if exist "bin\windows\launcher" rmdir /S /Q "bin\windows\launcher"
mkdir "bin\windows\launcher\Versions\${VERSION}"
go build -o "bin\windows\launcher\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher"
copy ${BINARY}-windows-amd64.exe "bin\windows\launcher\Versions\${VERSION}\${BINARY}.exe" /Y
.PHONY: installer-windows
installer-windows:
if exist "${GEN_SRC_DIR}\installer" rmdir /S /Q "${GEN_SRC_DIR}\installer"
xcopy "windows\installer\*.*" "${GEN_SRC_DIR}\installer" /K /H /Y /Q /I
powershell -Command "(gc ${GEN_SRC_DIR}\installer\installer.go) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\installer.go"
go-bindata -prefix "bin\windows\launcher" -o "${GEN_SRC_DIR}/installer/data/data.go" "./bin/windows/launcher/..."
powershell -Command "(gc ${GEN_SRC_DIR}\installer\data\data.go) -creplace 'package main', 'package data' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\data\data.go"
go build -o bin/windows/AminalSetup.exe -ldflags "-H windowsgui" "${GEN_SRC_DIR}/installer/installer.go"
rem If an .exe name contains "installer", "setup" etc., then at least Windows 10 automatically
rem opens a UAC prompt upon opening it. To avoid this, we add a compatibility manifest to the .exe.
mt -manifest windows\installer\AminalSetup.exe.manifest -outputresource:bin\windows\AminalSetup.exe;1
.PHONY: build-darwin-native-travis
build-darwin-native-travis:

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>True/PM</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,216 @@
package main
import (
"bufio"
"errors"
"golang.org/x/sys/windows/registry"
"os"
"os/user"
"strings"
"path/filepath"
"github.com/liamg/aminal/windows/winutil"
"github.com/liamg/aminal/generated-src/installer/data"
"text/template"
"io/ioutil"
"os/exec"
"syscall"
"flag"
)
const Version = "VERSION"
const ProductId = `{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}`
func main() {
doInstallPtr := flag.Bool("install", false, "Install Aminal")
doUpdatePtr := flag.Bool("update", false, "Update Aminal")
flag.Parse()
var installDir string
isUserInstall := strings.HasPrefix(os.Args[0], os.Getenv("LOCALAPPDATA"))
if *doInstallPtr {
installDir = getInstallDirWhenManagedByOmaha()
extractAssets(installDir)
createRegistryKeysForUninstaller(installDir, isUserInstall)
updateVersionInRegistry(isUserInstall)
createStartMenuShortcut(installDir, isUserInstall)
launchFman(installDir)
} else if *doUpdatePtr {
installDir = getInstallDirWhenManagedByOmaha()
extractAssets(installDir)
updateVersionInRegistry(isUserInstall)
removeOldVersions(installDir)
} else {
// Offline installer.
// We don't know whether we're being executed with Admin privileges.
// It's also not easy to determine. So perform user install:
isUserInstall = true
installDir = getDefaultInstallDir()
extractAssets(installDir)
createRegistryKeysForUninstaller(installDir, isUserInstall)
createStartMenuShortcut(installDir, isUserInstall)
launchFman(installDir)
}
}
func getInstallDirWhenManagedByOmaha() string {
executablePath, err := winutil.GetExecutablePath()
check(err)
result := executablePath
prevResult := ""
for filepath.Base(result) != "Aminal" {
prevResult = result
result = filepath.Dir(result)
if result == prevResult {
break
}
}
if result == prevResult {
msg := "Could not find parent directory 'Aminal' above " + executablePath
check(errors.New(msg))
}
return result
}
func getDefaultInstallDir() string {
localAppData := os.Getenv("LOCALAPPDATA")
if localAppData == "" {
panic("Environment variable LOCALAPPDATA is not set.")
}
return filepath.Join(localAppData, "Aminal")
}
func extractAssets(installDir string) {
for _, relPath := range data.AssetNames() {
bytes, err := data.Asset(relPath)
check(err)
absPath := filepath.Join(installDir, relPath)
check(os.MkdirAll(filepath.Dir(absPath), 0755))
f, err := os.OpenFile(absPath, os.O_CREATE, 0755)
check(err)
defer f.Close()
w := bufio.NewWriter(f)
_, err = w.Write(bytes)
check(err)
w.Flush()
}
}
func createRegistryKeysForUninstaller(installDir string, isUserInstall bool) {
regRoot := getRegistryRoot(isUserInstall)
uninstKey := `Software\Microsoft\Windows\CurrentVersion\Uninstall\Aminal`
writeRegStr(regRoot, uninstKey, "", installDir)
writeRegStr(regRoot, uninstKey, "DisplayName", "Aminal")
writeRegStr(regRoot, uninstKey, "Publisher", "Liam Galvin")
uninstaller := filepath.Join(installDir, "uninstall.exe")
uninstString := `"` + uninstaller + `"`
if isUserInstall {
uninstString += " /CurrentUser"
} else {
uninstString += " /AllUsers"
}
writeRegStr(regRoot, uninstKey, "UninstallString", uninstString)
}
func updateVersionInRegistry(isUserInstall bool) {
regRoot := getRegistryRoot(isUserInstall)
updateKey := `Software\Aminal\Update\Clients\` + ProductId
writeRegStr(regRoot, updateKey, "pv", Version + ".0")
writeRegStr(regRoot, updateKey, "name", "Aminal")
}
func getRegistryRoot(isUserInstall bool) registry.Key {
if isUserInstall {
return registry.CURRENT_USER
}
return registry.LOCAL_MACHINE
}
func writeRegStr(regRoot registry.Key, keyPath string, valueName string, value string) {
const mode = registry.WRITE|registry.WOW64_32KEY
key, _, err := registry.CreateKey(regRoot, keyPath, mode)
check(err)
defer key.Close()
check(key.SetStringValue(valueName, value))
}
func createStartMenuShortcut(installDir string, isUserInstall bool) {
startMenuDir := getStartMenuDir(isUserInstall)
linkPath := filepath.Join(startMenuDir, "Programs", "Aminal.lnk")
targetPath := filepath.Join(installDir, "Aminal.exe")
createShortcut(linkPath, targetPath)
}
func getStartMenuDir(isUserInstall bool) string {
if isUserInstall {
usr, err := user.Current()
check(err)
return usr.HomeDir + `\AppData\Roaming\Microsoft\Windows\Start Menu`
} else {
return os.Getenv("ProgramData") + `\Microsoft\Windows\Start Menu`
}
}
func createShortcut(linkPath, targetPath string) {
type Shortcut struct {
LinkPath string
TargetPath string
}
tmpl := template.New("createLnk.vbs")
tmpl, err := tmpl.Parse(`Set oWS = WScript.CreateObject("WScript.Shell")
sLinkFile = "{{.LinkPath}}"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = "{{.TargetPath}}"
oLink.Save
WScript.Quit 0`)
check(err)
tmpDir, err := ioutil.TempDir("", "Aminal")
check(err)
createLnk := filepath.Join(tmpDir, "createLnk.vbs")
defer os.RemoveAll(tmpDir)
f, err := os.Create(createLnk)
check(err)
defer f.Close()
w := bufio.NewWriter(f)
shortcut := Shortcut{linkPath, targetPath}
check(tmpl.Execute(w, shortcut))
w.Flush()
f.Close()
cmd := exec.Command("cscript", f.Name())
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
check(cmd.Run())
}
func launchFman(installDir string) {
cmd := exec.Command(getFmanExePath(installDir))
check(cmd.Start())
}
func getFmanExePath(installDir string) string {
return filepath.Join(installDir, "Aminal.exe")
}
func removeOldVersions(installDir string) {
versionsDir := filepath.Join(installDir, "Versions")
versions, err := ioutil.ReadDir(versionsDir)
check(err)
for _, version := range versions {
if version.Name() == Version {
continue
}
versionPath := filepath.Join(versionsDir, version.Name())
// Try deleting the main executable first. We do this to prevent a
// version that is still running from being deleted.
mainExecutable := filepath.Join(versionPath, "Aminal.exe")
err = os.Remove(mainExecutable)
if err == nil {
// Remove the rest:
check(os.RemoveAll(versionPath))
}
}
}
func check(e error) {
if e != nil {
panic(e)
}
}