build-msi.sh 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  4. REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
  5. DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}"
  6. ARCH="${ARCH:-amd64}"
  7. ZIP_NAME="OliveTin-windows-${ARCH}.zip"
  8. ZIP_PATH="${DIST_DIR}/${ZIP_NAME}"
  9. MSI_NAME="OliveTin-windows-${ARCH}.msi"
  10. MSI_PATH="${DIST_DIR}/${MSI_NAME}"
  11. if [[ ! -f "${ZIP_PATH}" ]]; then
  12. echo "Windows archive not found: ${ZIP_PATH}" >&2
  13. exit 1
  14. fi
  15. if ! command -v wixl >/dev/null || ! command -v wixl-heat >/dev/null; then
  16. echo "wixl and wixl-heat are required (install the wixl/msitools package)" >&2
  17. exit 1
  18. fi
  19. if [[ ! -f "${SCRIPT_DIR}/License.rtf" ]]; then
  20. echo "License.rtf not found: ${SCRIPT_DIR}/License.rtf" >&2
  21. exit 1
  22. fi
  23. INSTALLER_BANNER="${SCRIPT_DIR}/bitmaps/installer-banner.bmp"
  24. INSTALLER_DIALOG="${SCRIPT_DIR}/bitmaps/installer-dialog.bmp"
  25. if [[ ! -f "${INSTALLER_BANNER}" ]] || [[ ! -f "${INSTALLER_DIALOG}" ]]; then
  26. echo "Installer bitmaps not found: ${INSTALLER_BANNER} and ${INSTALLER_DIALOG}" >&2
  27. exit 1
  28. fi
  29. normalize_msi_version() {
  30. local raw="${1#v}"
  31. raw="${raw%%-*}"
  32. if [[ ! "${raw}" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then
  33. echo "Invalid MSI version (expected major[.minor[.patch[.build]]]): ${1}" >&2
  34. return 1
  35. fi
  36. local -a parts=()
  37. IFS='.' read -r -a parts <<<"${raw}"
  38. local major="${parts[0]:-0}"
  39. local minor="${parts[1]:-0}"
  40. local patch="${parts[2]:-0}"
  41. printf '%s.%s.%s' "${major}" "${minor}" "${patch}"
  42. }
  43. VERSION="${VERSION:-}"
  44. if [[ -z "${VERSION}" ]]; then
  45. VERSION="$(git -C "${REPO_ROOT}" describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)"
  46. fi
  47. if [[ -z "${VERSION}" ]]; then
  48. echo "Could not determine release version; set VERSION explicitly" >&2
  49. exit 1
  50. fi
  51. MSI_VERSION="$(normalize_msi_version "${VERSION}")" || exit 1
  52. STAGING="$(mktemp -d)"
  53. APP_STAGING="$(mktemp -d)"
  54. HEAT_WXS="$(mktemp)"
  55. WIXL_EXT_STAGING=""
  56. trap 'rm -rf "${STAGING}" "${APP_STAGING}" "${HEAT_WXS}" "${WIXL_EXT_STAGING}"' EXIT
  57. unzip -q "${ZIP_PATH}" -d "${STAGING}"
  58. SOURCE_ROOT="${STAGING}/OliveTin-windows-${ARCH}"
  59. if [[ ! -f "${SOURCE_ROOT}/OliveTin.exe" ]]; then
  60. echo "OliveTin.exe not found in ${SOURCE_ROOT}" >&2
  61. exit 1
  62. fi
  63. if [[ ! -f "${SOURCE_ROOT}/config.yaml" ]]; then
  64. echo "config.yaml not found in ${SOURCE_ROOT}" >&2
  65. exit 1
  66. fi
  67. if ! command -v objdump >/dev/null; then
  68. echo "objdump is required to verify OliveTin.exe Windows resources" >&2
  69. exit 1
  70. fi
  71. if ! objdump -h "${SOURCE_ROOT}/OliveTin.exe" | grep -q '[[:space:]]\.rsrc[[:space:]]'; then
  72. echo "OliveTin.exe is missing embedded Windows resources (.rsrc); use main: . in .goreleaser.yml" >&2
  73. exit 1
  74. fi
  75. mkdir -p "${APP_STAGING}/webui"
  76. cp "${SOURCE_ROOT}/OliveTin.exe" "${APP_STAGING}/"
  77. cp -a "${SOURCE_ROOT}/webui/." "${APP_STAGING}/webui/"
  78. (
  79. cd "${APP_STAGING}"
  80. find . -type f | sed 's|^\./||'
  81. ) | wixl-heat \
  82. -p "" \
  83. --component-group CG.AppFiles \
  84. --var var.SourceDir \
  85. --directory-ref INSTALLDIR \
  86. --win64 \
  87. > "${HEAT_WXS}"
  88. WIXL_EXT_DIR=""
  89. for candidate in /usr/share/wixl*/ext; do
  90. if [[ -d "${candidate}/ui/bitmaps" ]]; then
  91. WIXL_EXT_DIR="${candidate}"
  92. break
  93. fi
  94. done
  95. if [[ -z "${WIXL_EXT_DIR}" ]]; then
  96. echo "wixl UI extension not found under /usr/share/wixl*/ext" >&2
  97. exit 1
  98. fi
  99. WIXL_EXT_STAGING="$(mktemp -d)"
  100. cp -a "${WIXL_EXT_DIR}/." "${WIXL_EXT_STAGING}/"
  101. cp "${INSTALLER_BANNER}" "${WIXL_EXT_STAGING}/ui/bitmaps/bannrbmp.bmp"
  102. cp "${INSTALLER_DIALOG}" "${WIXL_EXT_STAGING}/ui/bitmaps/dlgbmp.bmp"
  103. (
  104. cd "${SCRIPT_DIR}"
  105. wixl \
  106. -v \
  107. -a x64 \
  108. --ext ui \
  109. --extdir "${WIXL_EXT_STAGING}" \
  110. -D "Version=${MSI_VERSION}" \
  111. -D "Win64=yes" \
  112. -D "SourceDir=${APP_STAGING}" \
  113. -D "ConfigSource=${SOURCE_ROOT}/config.yaml" \
  114. -o "${MSI_PATH}" \
  115. OliveTin.wxs \
  116. "${HEAT_WXS}"
  117. )
  118. if ! msiinfo export "${MSI_PATH}" Media 2>/dev/null | grep -q '#cab1.cab'; then
  119. echo "MSI cabinet is not embedded (expected #cab1.cab in Media table); check EmbedCab in OliveTin.wxs" >&2
  120. exit 1
  121. fi
  122. echo "Built ${MSI_PATH}"