Просмотр исходного кода

feat: Windows icons and MSI support

jamesread 1 неделя назад
Родитель
Сommit
abf4fc2cf0

+ 6 - 0
.github/workflows/build-and-release.yml

@@ -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 - 0
.gitignore

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

+ 1 - 0
.goreleaser.yml

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

+ 2 - 0
.releaserc.yaml

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

+ 7 - 0
Makefile

@@ -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)
 

+ 5 - 2
service/Makefile

@@ -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
@@ -33,7 +36,7 @@ codestyle: go-tools
 	gocritic check ./...
 
 test: unittests
-	
+
 tests: unittests
 
 unittests:

+ 25 - 0
var/windows/OliveTin.exe.manifest

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


+ 46 - 0
var/windows/OliveTin.wxs

@@ -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 - 0
var/windows/build-msi.sh

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

+ 92 - 0
var/windows/generate-resources.sh

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

+ 40 - 0
var/windows/upload-msi-release.sh

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

+ 41 - 0
var/windows/versioninfo.json

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