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
|
||||
*.syso
|
||||
.idea
|
||||
tmp/
|
||||
generated-src/
|
40
Makefile
40
Makefile
|
@ -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:
|
||||
|
|
|
@ -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