mirror of https://github.com/liamg/aminal.git
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:
parent
6e28eb11b5
commit
8fd76c876b
|
@ -2,4 +2,4 @@ aminal
|
||||||
aminal.exe
|
aminal.exe
|
||||||
*.syso
|
*.syso
|
||||||
.idea
|
.idea
|
||||||
tmp/
|
generated-src/
|
40
Makefile
40
Makefile
|
@ -1,7 +1,7 @@
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
BINARY := aminal
|
BINARY := aminal
|
||||||
FONTPATH := ./gui/packed-fonts
|
FONTPATH := ./gui/packed-fonts
|
||||||
TMPDIR := ./tmp
|
GEN_SRC_DIR := ./generated-src
|
||||||
VERSION_MAJOR := 0
|
VERSION_MAJOR := 0
|
||||||
VERSION_MINOR := 9
|
VERSION_MINOR := 9
|
||||||
VERSION_PATCH := 0
|
VERSION_PATCH := 0
|
||||||
|
@ -57,19 +57,31 @@ build-windows:
|
||||||
|
|
||||||
.PHONY: launcher-windows
|
.PHONY: launcher-windows
|
||||||
launcher-windows: build-windows
|
launcher-windows: build-windows
|
||||||
if exist "${TMPDIR}\launcher-src" rmdir /S /Q "${TMPDIR}\launcher-src"
|
if exist "${GEN_SRC_DIR}\launcher" rmdir /S /Q "${GEN_SRC_DIR}\launcher"
|
||||||
xcopy "windows\launcher\*.*" "${TMPDIR}\launcher-src" /K /H /Y /Q /I
|
xcopy "windows\launcher\*.*" "${GEN_SRC_DIR}\launcher" /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 ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MAJOR', '${VERSION_MAJOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\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 ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MINOR', '${VERSION_MINOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\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 ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_PATCH', '${VERSION_PATCH}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\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 ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\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"
|
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 "${TMPDIR}\launcher-src" /Y
|
copy aminal.ico "${GEN_SRC_DIR}\launcher" /Y
|
||||||
go generate "${TMPDIR}\launcher-src"
|
go generate "${GEN_SRC_DIR}\launcher"
|
||||||
if exist "${TMPDIR}\launcher" rmdir /S /Q "${TMPDIR}\launcher"
|
if exist "bin\windows\launcher" rmdir /S /Q "bin\windows\launcher"
|
||||||
mkdir "${TMPDIR}\launcher\Versions\${VERSION}"
|
mkdir "bin\windows\launcher\Versions\${VERSION}"
|
||||||
go build -o "${TMPDIR}\launcher\${BINARY}.exe" -ldflags "-H windowsgui" "${TMPDIR}\launcher-src"
|
go build -o "bin\windows\launcher\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher"
|
||||||
copy ${BINARY}-windows-amd64.exe "${TMPDIR}\launcher\Versions\${VERSION}\${BINARY}.exe" /Y
|
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
|
.PHONY: build-darwin-native-travis
|
||||||
build-darwin-native-travis:
|
build-darwin-native-travis:
|
||||||
|
|
|
@ -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>
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue