feat: Windows icons and MSI support

This commit is contained in:
jamesread 2026-06-26 21:19:19 +01:00
parent e61c8b036d
commit abf4fc2cf0
13 changed files with 359 additions and 2 deletions

View File

@ -13,6 +13,7 @@ on:
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
- 'var/windows/**'
workflow_dispatch:
push:
tags:
@ -31,6 +32,7 @@ on:
- 'integration-tests/**'
- 'proto/**'
- 'service/**'
- 'var/windows/**'
jobs:
build:
@ -112,6 +114,10 @@ jobs:
integration-tests
!integration-tests/node_modules
- name: Install msitools
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
run: sudo apt-get update && sudo apt-get install -y msitools
- name: Install goreleaser
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
uses: goreleaser/goreleaser-action@v6

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
service/OliveTin
service/OliveTin.armhf
service/OliveTin.exe
service/resource_windows_*.syso
service/reports
releases/
dist/

View File

@ -3,6 +3,7 @@ version: 2
before:
hooks:
- make service-prep
- make windows-resources VERSION={{ .Version }}
builds:
- env:

View File

@ -10,5 +10,7 @@ plugins:
- - "@semantic-release/exec"
- publishCmd: |
goreleaser release --clean --timeout 60m
VERSION=${nextRelease.version} ./var/windows/build-msi.sh
./var/windows/upload-msi-release.sh ${nextRelease.gitTag}
tagFormat: '${version}'

View File

@ -8,6 +8,12 @@ service:
service-prep:
$(MAKE) -wC service prep
windows-resources:
VERSION="$(VERSION)" ./var/windows/generate-resources.sh
windows-msi:
VERSION="$(VERSION)" ./var/windows/build-msi.sh
service-unittests:
$(MAKE) -wC service unittests
@ -59,6 +65,7 @@ clean:
$(call delete-files,OliveTin)
$(call delete-files,OliveTin.armhf)
$(call delete-files,OliveTin.exe)
rm -f service/resource_windows_*.syso
$(call delete-files,reports)
$(call delete-files,gen)

View File

@ -19,11 +19,14 @@ compile-x64-lin:
go build -o OliveTin
go env -u GOOS
compile-x64-win:
compile-x64-win: windows-resources
go env -w GOOS=windows GOARCH=amd64
go build -o OliveTin.exe
go env -u GOOS GOARCH
windows-resources:
$(MAKE) -wC .. windows-resources
compile: compile-armhf compile-x64-lin compile-x64-win
codestyle: go-tools

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="OliveTin.OliveTin"
type="win32" />
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*" />
</dependentAssembly>
</dependency>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

BIN
var/windows/OliveTin.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

46
var/windows/OliveTin.wxs Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product
Id="*"
Name="OliveTin $(var.Version)"
Language="1033"
Version="$(var.Version)"
Manufacturer="James Read"
UpgradeCode="8B5E3F2A-1C4D-4E6F-9A0B-2D3C4E5F6071">
<Package
InstallerVersion="500"
Compressed="yes"
InstallScope="perMachine"
Description="OliveTin web interface for running shell commands"
Comments="https://github.com/OliveTin/OliveTin" />
<MajorUpgrade
AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of OliveTin is already installed." />
<MediaTemplate />
<Feature Id="ProductFeature" Title="OliveTin" Level="1">
<ComponentGroupRef Id="CG.AppFiles" />
<ComponentRef Id="ConfigFile" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLDIR" Name="OliveTin" />
</Directory>
<Directory Id="CommonAppDataFolder">
<Directory Id="ConfigDir" Name="OliveTin" />
</Directory>
</Directory>
<DirectoryRef Id="ConfigDir">
<Component Id="ConfigFile" Guid="A1B2C3D4-E5F6-7890-ABCD-EF1234567890" Win64="yes">
<File
Id="ConfigYaml"
Source="$(var.ConfigSource)"
Name="config.yaml"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>

93
var/windows/build-msi.sh Executable file
View File

@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}"
ARCH="${ARCH:-amd64}"
ZIP_NAME="OliveTin-windows-${ARCH}.zip"
ZIP_PATH="${DIST_DIR}/${ZIP_NAME}"
MSI_NAME="OliveTin-windows-${ARCH}.msi"
MSI_PATH="${DIST_DIR}/${MSI_NAME}"
if [[ ! -f "${ZIP_PATH}" ]]; then
echo "Windows archive not found: ${ZIP_PATH}" >&2
exit 1
fi
if ! command -v wixl >/dev/null || ! command -v wixl-heat >/dev/null; then
echo "wixl and wixl-heat are required (install the wixl/msitools package)" >&2
exit 1
fi
normalize_windows_version() {
local raw="${1#v}"
raw="${raw%%-*}"
if [[ ! "${raw}" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
echo "0.0.0.0"
return
fi
local -a parts=()
IFS='.' read -r -a parts <<<"${raw}"
local major="${parts[0]:-0}"
local minor="${parts[1]:-0}"
local patch="${parts[2]:-0}"
local build="${parts[3]:-0}"
printf '%s.%s.%s.%s' "${major}" "${minor}" "${patch}" "${build}"
}
VERSION="${VERSION:-}"
if [[ -z "${VERSION}" ]]; then
VERSION="$(git -C "${REPO_ROOT}" describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)"
fi
if [[ -z "${VERSION}" ]]; then
echo "Could not determine release version; set VERSION explicitly" >&2
exit 1
fi
WINDOWS_VERSION="$(normalize_windows_version "${VERSION}")"
STAGING="$(mktemp -d)"
APP_STAGING="$(mktemp -d)"
HEAT_WXS="$(mktemp)"
trap 'rm -rf "${STAGING}" "${APP_STAGING}" "${HEAT_WXS}"' EXIT
unzip -q "${ZIP_PATH}" -d "${STAGING}"
SOURCE_ROOT="${STAGING}/OliveTin-windows-${ARCH}"
if [[ ! -f "${SOURCE_ROOT}/OliveTin.exe" ]]; then
echo "OliveTin.exe not found in ${SOURCE_ROOT}" >&2
exit 1
fi
if [[ ! -f "${SOURCE_ROOT}/config.yaml" ]]; then
echo "config.yaml not found in ${SOURCE_ROOT}" >&2
exit 1
fi
mkdir -p "${APP_STAGING}/webui"
cp "${SOURCE_ROOT}/OliveTin.exe" "${APP_STAGING}/"
cp -a "${SOURCE_ROOT}/webui/." "${APP_STAGING}/webui/"
(
cd "${APP_STAGING}"
find . -type f | sed 's|^\./||'
) | wixl-heat \
-p "" \
--component-group CG.AppFiles \
--var var.SourceDir \
--directory-ref INSTALLDIR \
--win64 \
> "${HEAT_WXS}"
wixl \
-v \
-a x64 \
-D "Version=${WINDOWS_VERSION}" \
-D "Win64=yes" \
-D "SourceDir=${APP_STAGING}" \
-D "ConfigSource=${SOURCE_ROOT}/config.yaml" \
-o "${MSI_PATH}" \
"${SCRIPT_DIR}/OliveTin.wxs" \
"${HEAT_WXS}"
echo "Built ${MSI_PATH}"

View File

@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
SERVICE_DIR="${REPO_ROOT}/service"
VERSIONINFO_JSON="${SCRIPT_DIR}/versioninfo.json"
ICON_PATH="${SCRIPT_DIR}/OliveTin.ico"
MANIFEST_PATH="${SCRIPT_DIR}/OliveTin.exe.manifest"
GOVERSIONINFO_VERSION="${GOVERSIONINFO_VERSION:-v1.5.0}"
usage() {
cat <<EOF
Usage: $(basename "$0") [version]
Generate Windows resource (.syso) files for embedding in OliveTin.exe.
version Release version (e.g. 3.0.0 or v3.0.0). Defaults to VERSION env,
then the latest git tag, then 0.0.0.
EOF
}
normalize_windows_version() {
local raw="${1#v}"
raw="${raw%%-*}"
if [[ ! "${raw}" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
echo "0.0.0.0"
return
fi
local -a parts=()
IFS='.' read -r -a parts <<<"${raw}"
local major="${parts[0]:-0}"
local minor="${parts[1]:-0}"
local patch="${parts[2]:-0}"
local build="${parts[3]:-0}"
printf '%s.%s.%s.%s' "${major}" "${minor}" "${patch}" "${build}"
}
resolve_version() {
if [[ -n "${VERSION:-}" ]]; then
echo "${VERSION}"
return
fi
if [[ $# -gt 0 && -n "${1:-}" ]]; then
echo "${1}"
return
fi
if git -C "${REPO_ROOT}" describe --tags --abbrev=0 >/dev/null 2>&1; then
git -C "${REPO_ROOT}" describe --tags --abbrev=0
return
fi
echo "0.0.0"
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ ! -f "${VERSIONINFO_JSON}" ]]; then
echo "versioninfo.json not found: ${VERSIONINFO_JSON}" >&2
exit 1
fi
if [[ ! -f "${ICON_PATH}" ]]; then
echo "icon not found: ${ICON_PATH}" >&2
exit 1
fi
WINDOWS_VERSION="$(normalize_windows_version "$(resolve_version "${1:-}")")"
echo "Generating Windows resources for version ${WINDOWS_VERSION}"
go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}"
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "${WORK_DIR}"' EXIT
(
cd "${WORK_DIR}"
goversioninfo \
-64 \
-platform-specific \
-icon="${ICON_PATH}" \
-manifest="${MANIFEST_PATH}" \
-file-version="${WINDOWS_VERSION}" \
-product-version="${WINDOWS_VERSION}" \
"${VERSIONINFO_JSON}"
)
rm -f "${SERVICE_DIR}"/resource_windows_*.syso
mv "${WORK_DIR}"/resource_windows_*.syso "${SERVICE_DIR}/"
echo "Wrote Windows resource files to ${SERVICE_DIR}"

View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}"
ARCH="${ARCH:-amd64}"
MSI_NAME="OliveTin-windows-${ARCH}.msi"
MSI_PATH="${DIST_DIR}/${MSI_NAME}"
TAG="${1:-}"
if [[ -z "${TAG}" ]]; then
echo "Usage: $(basename "$0") <release-tag>" >&2
exit 1
fi
if [[ ! -f "${MSI_PATH}" ]]; then
echo "MSI not found: ${MSI_PATH}" >&2
exit 1
fi
if ! command -v gh >/dev/null; then
echo "gh is required to upload the MSI to GitHub releases" >&2
exit 1
fi
checksums_path="${DIST_DIR}/checksums.txt"
if [[ -f "${checksums_path}" ]] && ! grep -qF " ${MSI_NAME}" "${checksums_path}"; then
(
cd "${DIST_DIR}"
sha256sum "${MSI_NAME}"
) >> "${checksums_path}"
fi
gh release upload "${TAG}" "${MSI_PATH}" --clobber
if [[ -f "${checksums_path}" ]]; then
gh release upload "${TAG}" "${checksums_path}" --clobber
fi
echo "Uploaded ${MSI_NAME} to release ${TAG}"

View File

@ -0,0 +1,41 @@
{
"FixedFileInfo": {
"FileVersion": {
"Major": 0,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 0,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags ": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo": {
"Comments": "https://github.com/OliveTin/OliveTin",
"CompanyName": "James Read",
"FileDescription": "OliveTin web interface for running shell commands",
"FileVersion": "0.0.0.0",
"InternalName": "OliveTin",
"LegalCopyright": "Copyright (C) James Read. Licensed under AGPL-3.0.",
"LegalTrademarks": "",
"OriginalFilename": "OliveTin.exe",
"PrivateBuild": "",
"ProductName": "OliveTin",
"ProductVersion": "0.0.0.0",
"SpecialBuild": ""
},
"VarFileInfo": {
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
}
}