mirror of https://github.com/liamg/aminal.git
Automatic updates on Windows (#247)
* Add Windows launcher The launcher looks at directory "Versions" next to its executable. It finds the latest version and runs the executable in that directory with the same name as itself. For instance: Aminal.exe <- the launcher Versions/ 1.0.0/ Aminal.exe 1.0.1/ Aminal.exe In this example, running the top-level Aminal.exe (the launcher) starts Versions/1.0.1/Aminal.exe. Having a launcher allows Aminal to be updated while it is running. For example, version 1.0.1 could be downloaded without disturbing running instances of Aminal 1.0.0. * 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. * Implement Uninstaller for Windows * Codesign Windows auto-update executables * Don't require Admin privileges for Win uninstaller * Remove references to fman * Explain automatic updates in windows.md * Limit installer go packages compilation only to Windows platform
This commit is contained in:
parent
f207ec0930
commit
ed8656930a
|
@ -1,4 +1,5 @@
|
|||
aminal
|
||||
aminal.exe
|
||||
*.syso
|
||||
.idea
|
||||
.idea
|
||||
generated-src/
|
49
Makefile
49
Makefile
|
@ -1,6 +1,11 @@
|
|||
SHELL := /bin/bash
|
||||
BINARY := aminal
|
||||
FONTPATH := ./gui/packed-fonts
|
||||
GEN_SRC_DIR := ./generated-src
|
||||
VERSION_MAJOR := 0
|
||||
VERSION_MINOR := 9
|
||||
VERSION_PATCH := 0
|
||||
VERSION := ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
|
@ -59,6 +64,50 @@ build-windows:
|
|||
windres -o aminal.syso aminal.rc
|
||||
go build -o ${BINARY}-windows-amd64.exe
|
||||
|
||||
.PHONY: launcher-windows
|
||||
launcher-windows: build-windows
|
||||
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\Aminal" rmdir /S /Q "bin\windows\Aminal"
|
||||
mkdir "bin\windows\Aminal\Versions\${VERSION}"
|
||||
go build -o "bin\windows\Aminal\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher"
|
||||
copy ${BINARY}-windows-amd64.exe "bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe" /Y
|
||||
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\${BINARY}.exe
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\${BINARY}.exe
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
|
||||
|
||||
.PHONY: uninstaller-windows
|
||||
uninstaller-windows: launcher-windows
|
||||
makensis "/XOutFile bin/windows/UninstallerSetup.exe" /NOCD windows\Uninstaller.nsi
|
||||
cmd /c "bin\windows\UninstallerSetup.exe /S /D=%cd%\bin\windows\Aminal"
|
||||
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\uninstall.exe
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\uninstall.exe
|
||||
|
||||
.PHONY: installer-windows
|
||||
installer-windows: uninstaller-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\Aminal" -o "${GEN_SRC_DIR}/installer/data/data.go" "./bin/windows/Aminal/..."
|
||||
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
|
||||
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\AminalSetup.exe
|
||||
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\AminalSetup.exe
|
||||
|
||||
.PHONY: build-darwin-native-travis
|
||||
build-darwin-native-travis:
|
||||
mkdir -p bin/darwin
|
||||
|
|
95
windows.md
95
windows.md
|
@ -12,8 +12,7 @@
|
|||
cd %YOUR_PROJECT_WORKING_DIR%
|
||||
mkdir go\src\github.com\liamg
|
||||
cd go\src\github.com\liamg
|
||||
git clone git@github.com:jumptrading/aminal-mirror.git
|
||||
move aminal-mirror aminal
|
||||
git clone https://github.com/liamg/aminal.git
|
||||
|
||||
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
|
||||
set GOBIN=%GOPATH%/bin
|
||||
|
@ -28,3 +27,95 @@ go install
|
|||
|
||||
Look for the aminal.exe built binary under your %GOBIN% path
|
||||
|
||||
### Building an installer for automatic updates:
|
||||
|
||||
In addition to the above commands:
|
||||
|
||||
```
|
||||
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
|
||||
go get golang.org/x/sys/windows
|
||||
go get github.com/jteeuwen/go-bindata/...
|
||||
```
|
||||
|
||||
Install NSIS and place it on the `PATH`:
|
||||
```
|
||||
choco install nsis
|
||||
set PATH=%PATH%;%ProgramFiles(x86)%\NSIS\Bin
|
||||
```
|
||||
|
||||
Ensure `signtool.exe` is on the `PATH`. For instance, on Windows 10:
|
||||
|
||||
```
|
||||
set PATH=%PATH%;%ProgramFiles(x86)%\Windows Kits\10\bin\10.0.17763.0\x64
|
||||
```
|
||||
|
||||
Copy your code signing certificate to `go\src\github.com\liamg\windows\codesigning_certificate.pfx`.
|
||||
|
||||
Set the `WINDOWS_CODESIGNING_CERT_PW` to the password of your code signing certificate:
|
||||
|
||||
```
|
||||
set WINDOWS_CODESIGNING_CERT_PW=PASSWORD
|
||||
```
|
||||
|
||||
Compile Aminal and build the installer:
|
||||
|
||||
```
|
||||
mingw32-make installer-windows
|
||||
```
|
||||
|
||||
This produces several files in `bin/windows`. Their purpose is explained below.
|
||||
|
||||
### How Aminal's automatic update mechanism works (on Windows)
|
||||
|
||||
Aminal uses a technology called [Google Omaha](https://github.com/google/omaha) for automatic updates on Windows. It's the same technology which Google use to auto-update Chrome. For a quick introduction, see [this Google Omaha tutorial](https://fman.io/blog/google-omaha-tutorial/).
|
||||
|
||||
Aminal has an online installer. This is a 1 MB executable, which was created using Google Omaha. Suppose it's called `OnlineInstaller.exe`. When a user runs it, the following things happen:
|
||||
|
||||
* `OnlineInstaller.exe` installs Aminal's version of Google Omaha on the user's system. In particular, this creates two `AminalUpdateTask...` tasks in the Windows Task Scheduler. They're set up to run once per day in the background and check for updates.
|
||||
* `OnlineInstaller.exe` contacts Aminal's update server and asks "what is the latest version?". The server responds with the URL to an _offline_ installer and some command line parameters. Say this offline installer is called `install-aminal-0.9.0.exe`.
|
||||
* `OnlineInstaller.exe` downloads `install-aminal-0.9.0.exe` and invokes it with the given command line arguments, typically `-install`.
|
||||
* `install-aminal-0.9.0.exe` performs the following steps:
|
||||
* It installs Aminal 0.9.0 into `%LOCALAPPDATA%\Aminal`.
|
||||
* It sets some registry keys in `HKCU\Software\Aminal`. This lets the `AminalUpdateTask...` tasks know which version of Aminal is installed.
|
||||
* It creates a Start menu shortcut for starting Aminal.
|
||||
|
||||
When the update tasks run, you will see `AminalUpdate.exe` in the Windows Task Manager. They use the registry to send the current Aminal version to the update server and ask "is there a new version?". If yes, the server again responds with the URL to an `.exe` and some command line parameters. In Aminal's current setup, this too is `install-aminal-0.9.0.exe` (if the user had, say, Aminal 0.8.9 installed). But this time the command line flag is `-update`. The `.exe` again installs the current version of Aminal and updates the registry.
|
||||
|
||||
The offline installer `install-aminal-0.9.0.exe` is actually what's produced by the `mingw32-make installer-windows` command in the previous section. It is placed at `bin/windows/AminalSetup.exe` and supports the command line flags `-install`, `-update` or none. In the last case (i.e. when invoked without arguments), it acts as a normal offline installer to be invoked by the user, and does not set Omaha's registry keys. The source code for this installer lies in `windows/installer/installer.go`.
|
||||
|
||||
Due to the asynchronous nature of the update tasks, it can happen that Aminal is running while a new version is being downloaded / installed. To prevent this from breaking Aminal's running instance, Aminal's install dir `%LOCALAPPDATA%\Aminal` contains the following hierarchy:
|
||||
|
||||
* `Aminal.exe`
|
||||
* `Versions/`
|
||||
* `0.9.0/`
|
||||
* `Aminal.exe`
|
||||
|
||||
The top-level `Aminal.exe` is a launcher that always invokes the latest version. When an update is downloaded / installed, it is placed in a new subfolder of `Versions/`. For instance:
|
||||
|
||||
* `Aminal.exe`
|
||||
* `Versions/`
|
||||
* `0.9.0/`
|
||||
* `0.9.1/`
|
||||
|
||||
The next time the top-level `Aminal.exe` is invoked, it runs `Versions/0.9.1/Aminal.exe`.
|
||||
|
||||
The code for this top-level launcher is in `windows/launcher/launcher.go`. Its binary (and the current version subdirectory) is produced in `bin/windows` when you do `mingw32-make installer-windows`.
|
||||
|
||||
#### Forcing updates
|
||||
|
||||
By default, Aminal's (/Omaha's) update tasks only run once per day. To force an immediate update, perform the following steps:
|
||||
|
||||
* Delete the registry key `HKCU\Software\Aminal\Updated\LastChecked`.
|
||||
* Run the task `AminalUpdateTask...UA` in the Windows Task Scheduler. Press `F5`. You'll see its result change to `0x41301`. This means it's currently running. You'll also see `AminalUpdate.exe` in the Task _Scheduler_. Keep refreshing with `F5` until both disappear.
|
||||
|
||||
#### Uninstalling Aminal
|
||||
|
||||
The installer above adds an uninstaller to the user's _Add or remove programs_ panel in Windows. When the user goes to the Control Panel and uninstalls Aminal this way, the install directory and start menu entries are removed. Further, the registry key `HKCU\Software\Aminal\Clients\{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}` is removed. What's not removed immediately is Omaha. (So you'll still see the update tasks in the Task Scheduler.) But! The next time the update tasks run, they realize by looking at the registry that Aminal is no longer installed and uninstall themselves (and Omaha).
|
||||
|
||||
To work around some potential permission issues, the uninstaller is not implemented in Go (like the installer and launcher above). But via NSIS. The source code is in `windows/Uninstaller.nsi`. It's an "installer" whose sole purpose is to generate `bin/windows/Aminal/uninstall.exe`.
|
||||
|
||||
#### Releasing a new version via automatic updates
|
||||
|
||||
To release a new version, update the `VERSION` fields in `Makefile`. Then, invoke `mingw32-make installer-windows`. Log into the Omaha update server, add a new Version for Aminal that mirrors the one you set in `Makefile`. You'll need to add a trailing `.0` to the version number on the server, because Omaha uses four-tuples for versions (`0.9.1.0` instead of `0.9.1`). As the "File" for the version, upload `bin/windows/AminalSetup.exe`. Add two "Action"s: One for Event _install_, one for event _update_. In both cases, instruct the server to _Run_ `AminalSetup.exe`. For the install Event, supply Arguments `-install`. For the update Event, supply Arguments `-update`.
|
||||
|
||||
Once you have done this, the background update tasks on your users' systems will (over the next 24 hours) download and install the new version of Aminal from the server.
|
|
@ -0,0 +1,70 @@
|
|||
!include MUI2.nsh
|
||||
|
||||
;--------------------------------
|
||||
!define MULTIUSER_EXECUTIONLEVEL Standard
|
||||
;Add support for command-line args that let uninstaller know whether to
|
||||
;uninstall machine- or user installation:
|
||||
!define MULTIUSER_INSTALLMODE_COMMANDLINE
|
||||
!include MultiUser.nsh
|
||||
!include LogicLib.nsh
|
||||
|
||||
Function .onInit
|
||||
!insertmacro MULTIUSER_INIT
|
||||
FunctionEnd
|
||||
|
||||
Function un.onInit
|
||||
!insertmacro MULTIUSER_UNINIT
|
||||
FunctionEnd
|
||||
|
||||
;--------------------------------
|
||||
;General
|
||||
|
||||
Name "Aminal"
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;--------------------------------
|
||||
;Languages
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
;--------------------------------
|
||||
;Installer Sections
|
||||
|
||||
Section
|
||||
SetOutPath "$InstDir"
|
||||
WriteUninstaller "$InstDir\uninstall.exe"
|
||||
SectionEnd
|
||||
|
||||
;--------------------------------
|
||||
;Uninstaller Section
|
||||
|
||||
!define UNINST_KEY \
|
||||
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Aminal"
|
||||
!define ROOT_KEY "Software\Aminal"
|
||||
!define UPDATE_KEY \
|
||||
"${ROOT_KEY}\Update\Clients\{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}"
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
RMDir /r "$InstDir\Versions"
|
||||
Delete "$InstDir\Aminal.exe"
|
||||
Delete "$InstDir\uninstall.exe"
|
||||
;Omaha leaves this directory behind. Delete if empty:
|
||||
RMDir "$InstDir\CrashReports"
|
||||
RMDir "$InstDir"
|
||||
Delete "$SMPROGRAMS\Aminal.lnk"
|
||||
DeleteRegKey SHCTX "${UNINST_KEY}"
|
||||
DeleteRegKey SHCTX "${UPDATE_KEY}"
|
||||
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}\Update\Clients"
|
||||
;Try to speed up uninstall of Omaha:
|
||||
DeleteRegValue SHCTX "${ROOT_KEY}\Update" "LastChecked"
|
||||
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}\Update"
|
||||
WriteRegStr SHCTX "${ROOT_KEY}" "" ""
|
||||
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}"
|
||||
|
||||
SectionEnd
|
|
@ -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,218 @@
|
|||
// +build windows
|
||||
|
||||
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)
|
||||
launchAminal(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)
|
||||
launchAminal(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 launchAminal(installDir string) {
|
||||
cmd := exec.Command(getAminalExePath(installDir))
|
||||
check(cmd.Start())
|
||||
}
|
||||
|
||||
func getAminalExePath(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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
// +build windows
|
||||
//go:generate goversioninfo -icon=aminal.ico
|
||||
|
||||
/*
|
||||
Looks at directory "Versions" next to this executable. Finds the latest version
|
||||
and runs the executable with the same name as this executable in that directory.
|
||||
Eg.:
|
||||
Aminal.exe (=launcher.exe)
|
||||
Versions/
|
||||
1.0.0/
|
||||
Aminal.exe
|
||||
1.0.1
|
||||
Aminal.exe
|
||||
-> Launches Versions/1.0.1/Aminal.exe.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/liamg/aminal/windows/winutil"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
number [3]int
|
||||
name string
|
||||
}
|
||||
type Versions []Version
|
||||
|
||||
func main() {
|
||||
executable, err := winutil.GetExecutablePath()
|
||||
check(err)
|
||||
executableDir, executableName := filepath.Split(executable)
|
||||
versionsDir := filepath.Join(executableDir, "Versions")
|
||||
latestVersion, err := getLatestVersion(versionsDir)
|
||||
check(err)
|
||||
target := filepath.Join(versionsDir, latestVersion, executableName)
|
||||
cmd := exec.Command(target, os.Args[1:]...)
|
||||
check(cmd.Start())
|
||||
}
|
||||
|
||||
func getLatestVersion(versionsDir string) (string, error) {
|
||||
potentialVersions, err := ioutil.ReadDir(versionsDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var versions Versions
|
||||
for _, file := range potentialVersions {
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
version, err := parseVersionString(file.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, version)
|
||||
}
|
||||
if len(versions) == 0 {
|
||||
errMsg := fmt.Sprintf("No valid version in %s.", versionsDir)
|
||||
return "", errors.New(errMsg)
|
||||
}
|
||||
sort.Sort(versions)
|
||||
return versions[len(versions)-1].String(), nil
|
||||
}
|
||||
|
||||
func parseVersionString(version string) (Version, error) {
|
||||
var result Version
|
||||
result.name = version
|
||||
err := error(nil)
|
||||
version = strings.TrimSuffix(version, "-SNAPSHOT")
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) != len(result.number) {
|
||||
err = errors.New("Wrong number of parts.")
|
||||
} else {
|
||||
for i, partStr := range parts {
|
||||
result.number[i], err = strconv.Atoi(partStr)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (arr Versions) Len() int {
|
||||
return len(arr)
|
||||
}
|
||||
|
||||
func (arr Versions) Less(i, j int) bool {
|
||||
for k, left := range arr[i].number {
|
||||
right := arr[j].number[k]
|
||||
if left > right {
|
||||
return false
|
||||
} else if left < right {
|
||||
return true
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s < %s\n", arr[i], arr[j])
|
||||
return true
|
||||
}
|
||||
|
||||
func (arr Versions) Swap(i, j int) {
|
||||
tmp := arr[j]
|
||||
arr[j] = arr[i]
|
||||
arr[i] = tmp
|
||||
}
|
||||
|
||||
func (version Version) String() string {
|
||||
return version.name
|
||||
}
|
||||
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"FixedFileInfo":
|
||||
{
|
||||
"FileVersion": {
|
||||
"Major": VERSION_MAJOR,
|
||||
"Minor": VERSION_MINOR,
|
||||
"Patch": VERSION_PATCH,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": VERSION_MAJOR,
|
||||
"Minor": VERSION_MINOR,
|
||||
"Patch": VERSION_PATCH,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags ": "00",
|
||||
"FileOS": "040004",
|
||||
"FileType": "01",
|
||||
"FileSubType": "00"
|
||||
},
|
||||
"StringFileInfo":
|
||||
{
|
||||
"ProductName": "Aminal",
|
||||
"ProductVersion": "VERSION",
|
||||
"LegalCopyright": "Copyright 2018-YEAR Liam Galvin"
|
||||
},
|
||||
"VarFileInfo":
|
||||
{
|
||||
"Translation": {
|
||||
"LangID": "0409",
|
||||
"CharsetID": "04B0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// +build windows
|
||||
|
||||
package winutil
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel = windows.MustLoadDLL("kernel32.dll")
|
||||
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
||||
)
|
||||
|
||||
func GetExecutablePath() (string, error) {
|
||||
var n uint32
|
||||
b := make([]uint16, windows.MAX_PATH)
|
||||
size := uint32(len(b))
|
||||
bPtr := uintptr(unsafe.Pointer(&b[0]))
|
||||
r0, _, e1 := getModuleFileNameProc.Call(0, bPtr, uintptr(size))
|
||||
n = uint32(r0)
|
||||
if n == 0 {
|
||||
return "", e1
|
||||
}
|
||||
return string(utf16.Decode(b[0:n])), nil
|
||||
}
|
Loading…
Reference in New Issue