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.
This commit is contained in:
Michael Herrmann 2019-02-21 10:52:40 +01:00
parent 150b0493de
commit 6e28eb11b5
5 changed files with 206 additions and 1 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
aminal
aminal.exe
*.syso
.idea
.idea
tmp/

View File

@ -1,6 +1,11 @@
SHELL := /bin/bash
BINARY := aminal
FONTPATH := ./gui/packed-fonts
TMPDIR := ./tmp
VERSION_MAJOR := 0
VERSION_MINOR := 9
VERSION_PATCH := 0
VERSION := ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
.PHONY: build
build:
@ -50,6 +55,22 @@ build-windows:
windres -o aminal.syso aminal.rc
go build -o ${BINARY}-windows-amd64.exe
.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
.PHONY: build-darwin-native-travis
build-darwin-native-travis:
mkdir -p bin/darwin

View File

@ -0,0 +1,123 @@
//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)
}
}

View File

@ -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"
}
}
}

View File

@ -0,0 +1,25 @@
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
}