diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index b9ba5a0..5fc1d7c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.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 diff --git a/.gitignore b/.gitignore index 48e8d33..469fbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ service/OliveTin service/OliveTin.armhf service/OliveTin.exe +service/resource_windows_*.syso service/reports releases/ dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml index a6ba475..32c6a76 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,7 @@ version: 2 before: hooks: - make service-prep + - make windows-resources VERSION={{ .Version }} builds: - env: diff --git a/.releaserc.yaml b/.releaserc.yaml index c3ddbdc..fc571cf 100644 --- a/.releaserc.yaml +++ b/.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}' diff --git a/Makefile b/Makefile index 23062c2..7ce7955 100644 --- a/Makefile +++ b/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) diff --git a/service/Makefile b/service/Makefile index dee7cc9..5d3044a 100644 --- a/service/Makefile +++ b/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: diff --git a/var/windows/OliveTin.exe.manifest b/var/windows/OliveTin.exe.manifest new file mode 100644 index 0000000..90faaa7 --- /dev/null +++ b/var/windows/OliveTin.exe.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + true + PerMonitorV2 + + + diff --git a/var/windows/OliveTin.ico b/var/windows/OliveTin.ico new file mode 100644 index 0000000..446dd82 Binary files /dev/null and b/var/windows/OliveTin.ico differ diff --git a/var/windows/OliveTin.wxs b/var/windows/OliveTin.wxs new file mode 100644 index 0000000..807dd5b --- /dev/null +++ b/var/windows/OliveTin.wxs @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/var/windows/build-msi.sh b/var/windows/build-msi.sh new file mode 100755 index 0000000..42aeb07 --- /dev/null +++ b/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}" diff --git a/var/windows/generate-resources.sh b/var/windows/generate-resources.sh new file mode 100755 index 0000000..50e562c --- /dev/null +++ b/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 </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}" diff --git a/var/windows/upload-msi-release.sh b/var/windows/upload-msi-release.sh new file mode 100755 index 0000000..3773c28 --- /dev/null +++ b/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") " >&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}" diff --git a/var/windows/versioninfo.json b/var/windows/versioninfo.json new file mode 100644 index 0000000..7822648 --- /dev/null +++ b/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" + } + } +}