From 6e28eb11b5315310e93b48321891e51786716552 Mon Sep 17 00:00:00 2001 From: Michael Herrmann Date: Thu, 21 Feb 2019 10:52:40 +0100 Subject: [PATCH] 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. --- .gitignore | 3 +- Makefile | 21 +++++ windows/launcher/launcher.go | 123 ++++++++++++++++++++++++++++++ windows/launcher/versioninfo.json | 35 +++++++++ windows/winutil/winutil.go | 25 ++++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 windows/launcher/launcher.go create mode 100644 windows/launcher/versioninfo.json create mode 100644 windows/winutil/winutil.go diff --git a/.gitignore b/.gitignore index 627ca78..3a53cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ aminal aminal.exe *.syso -.idea \ No newline at end of file +.idea +tmp/ \ No newline at end of file diff --git a/Makefile b/Makefile index 9749f29..349d864 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/windows/launcher/launcher.go b/windows/launcher/launcher.go new file mode 100644 index 0000000..c001126 --- /dev/null +++ b/windows/launcher/launcher.go @@ -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) + } +} \ No newline at end of file diff --git a/windows/launcher/versioninfo.json b/windows/launcher/versioninfo.json new file mode 100644 index 0000000..af8c654 --- /dev/null +++ b/windows/launcher/versioninfo.json @@ -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" + } + } +} \ No newline at end of file diff --git a/windows/winutil/winutil.go b/windows/winutil/winutil.go new file mode 100644 index 0000000..8b31fa4 --- /dev/null +++ b/windows/winutil/winutil.go @@ -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 +} \ No newline at end of file