feat(release): 发布 Windows Portable 与 MSI 双产物

- Windows AMD64 与 ARM64 同时生成 Portable EXE 和 MSI 安装包
- 统一 stable 与 dev 的 MSI 产品身份并支持通道切换升级
- 发布清单同时包含两类资产及对应 SHA256 校验信息
- 在 Windows workflow 中执行双模式更新与 WiX 契约测试
This commit is contained in:
Syngnat
2026-07-13 09:25:21 +08:00
parent 431f740476
commit 7bdce19d01
5 changed files with 354 additions and 18 deletions

View File

@@ -25,6 +25,11 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Test release asset contracts
run: |
python3 tools/generate-update-latest-manifest.test.py
python3 tools/windows-release-artifacts.test.py
- name: Setup Node
uses: actions/setup-node@v5
with:
@@ -391,6 +396,24 @@ jobs:
}
& $upxCmd --version
- name: Install WiX Toolset (Windows)
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
$wixVersion = "6.0.2"
$wixTools = Join-Path $env:RUNNER_TEMP "wix-tools"
dotnet tool install wix --tool-path $wixTools --version $wixVersion
if ($LASTEXITCODE -ne 0) {
throw "WiX Toolset 安装失败,退出码: $LASTEXITCODE"
}
$wixTools | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
$env:PATH = "$wixTools;$env:PATH"
wix extension add --global "WixToolset.UI.wixext/$wixVersion"
if ($LASTEXITCODE -ne 0) {
throw "WiX UI 扩展安装失败,退出码: $LASTEXITCODE"
}
wix --version
- name: Install Linux Dependencies
if: contains(matrix.platform, 'linux')
run: |
@@ -519,7 +542,7 @@ jobs:
- name: Test Windows Online Updater (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
shell: pwsh
run: go test ./internal/app -run '^TestWindowsPowerShellUpdater' -count=1 -timeout=3m
run: go test ./internal/app -run '^(TestWindowsPowerShellUpdater|TestExpectedAssetNameForExecutableUsesWindowsPortableSuffix|TestShouldWindowsUpdateLaunchDownloadedAssetDirectly|TestResolveUpdateInstallMode|TestExpectedAssetNameForWindowsInstallMode|TestResolveUpdateWorkspaceDirForPlatformSeparatesMSIFromInstallDirectory|TestValidateUpdatePackageForCurrentInstallMode|TestInstallUpdateAndRestartMSI|TestBuildWindowsMSIUpdatePowerShellScript|TestBuildWindowsMSILaunchCommand|TestResolveWindowsMSIExecPath)' -count=1 -timeout=3m
# ---- 生成 dev 版本号 ----
- name: Generate Dev Version
@@ -861,14 +884,16 @@ jobs:
mv "$DMG_NAME" "../../$FINAL_NAME"
# Windows Packaging
- name: Package Windows EXE
- name: Package Windows Portable EXE and MSI
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
Set-Location build/bin
$version = "${{ steps.version.outputs.version }}"
$target = "${{ matrix.build_name }}"
$finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe"
$archName = "${{ matrix.arch_name }}"
$portableExeName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Portable.exe"
$msiName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Installer.msi"
if (Test-Path "$target.exe") {
$finalExe = "$target.exe"
@@ -911,8 +936,55 @@ jobs:
}
}
Write-Host "📦 输出 Windows 可执行文件 $finalExeName..."
Copy-Item -LiteralPath $finalExe -Destination "..\\..\\$finalExeName" -Force
Write-Host "📦 输出 Windows Portable 可执行文件 $portableExeName..."
Copy-Item -LiteralPath $finalExe -Destination "..\\..\\$portableExeName" -Force
$runNumber = [int64]$env:GITHUB_RUN_NUMBER
if ($runNumber -le 0 -or $runNumber -gt 65535) {
throw "GitHub run number 无法映射为 MSI 版本号: $runNumber"
}
$productVersion = "255.0.$runNumber"
$wixArch = if ($archName -eq "Arm64") { "arm64" } else { "x64" }
$componentGuid = if ($archName -eq "Arm64") {
"7F4C7757-329F-42B0-B312-D4B8AD50415E"
} else {
"0BCEE70B-9CF2-449C-9ADE-190188493234"
}
$licenseText = Get-Content -LiteralPath "..\\..\\LICENSE" -Raw
$escapedLicense = $licenseText.Replace('\', '\\').Replace('{', '\{').Replace('}', '\}')
$escapedLicense = $escapedLicense -replace "`r?`n", "\par`r`n"
$licenseRtf = Join-Path $env:RUNNER_TEMP "GoNavi-License.rtf"
$rtfContent = "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}}\f0\fs18 $escapedLicense}"
Set-Content -LiteralPath $licenseRtf -Value $rtfContent -Encoding ascii
$sourceExe = (Resolve-Path -LiteralPath $finalExe).Path
$wxsPath = (Resolve-Path -LiteralPath "..\\windows\\installer.wxs").Path
$iconPath = (Resolve-Path -LiteralPath "..\\windows\\icon.ico").Path
$installMarkerPath = Join-Path $env:RUNNER_TEMP ".gonavi-msi-install"
Set-Content -LiteralPath $installMarkerPath -Value "MSI" -Encoding ascii -NoNewline
$installMarkerPath = (Resolve-Path -LiteralPath $installMarkerPath).Path
$wixIntermediateDir = Join-Path $env:RUNNER_TEMP "wixobj-$archName"
Write-Host "📦 生成 Windows MSI 安装程序 $msiName (version=$productVersion arch=$wixArch)..."
wix build $wxsPath `
-arch $wixArch `
-ext "WixToolset.UI.wixext/6.0.2" `
-d "ProductName=GoNavi" `
-d "ProductVersion=$productVersion" `
-d "UpgradeCode=CDD6BF2F-ED1E-4345-A0AB-DCDB7E15FB23" `
-d "ComponentGuid=$componentGuid" `
-d "InstallFolderName=GoNavi" `
-d "RegistryKeyName=GoNavi" `
-d "SourceExe=$sourceExe" `
-d "InstallMarker=$installMarkerPath" `
-d "IconPath=$iconPath" `
-d "LicenseRtf=$licenseRtf" `
-pdbtype none `
-intermediatefolder $wixIntermediateDir `
-out "..\\..\\$msiName"
if ($LASTEXITCODE -ne 0) {
throw "WiX MSI 构建失败,退出码: $LASTEXITCODE"
}
# Linux Packaging
- name: Package Linux
@@ -1003,6 +1075,7 @@ jobs:
path: |
GoNavi-*.dmg
GoNavi-*.exe
GoNavi-*.msi
GoNavi-*.tar.gz
GoNavi-*.AppImage
drivers/**

View File

@@ -21,6 +21,11 @@ jobs:
with:
go-version-file: 'go.mod'
- name: Test release asset contracts
run: |
python3 tools/generate-update-latest-manifest.test.py
python3 tools/windows-release-artifacts.test.py
- name: Setup Node
uses: actions/setup-node@v5
with:
@@ -343,6 +348,24 @@ jobs:
}
& $upxCmd --version
- name: Install WiX Toolset (Windows)
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
$wixVersion = "6.0.2"
$wixTools = Join-Path $env:RUNNER_TEMP "wix-tools"
dotnet tool install wix --tool-path $wixTools --version $wixVersion
if ($LASTEXITCODE -ne 0) {
throw "WiX Toolset 安装失败,退出码: $LASTEXITCODE"
}
$wixTools | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
$env:PATH = "$wixTools;$env:PATH"
wix extension add --global "WixToolset.UI.wixext/$wixVersion"
if ($LASTEXITCODE -ne 0) {
throw "WiX UI 扩展安装失败,退出码: $LASTEXITCODE"
}
wix --version
# Linux Dependencies (GTK3, WebKit2GTK required by Wails)
- name: Install Linux Dependencies
if: contains(matrix.platform, 'linux')
@@ -475,7 +498,7 @@ jobs:
- name: Test Windows Online Updater (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
shell: pwsh
run: go test ./internal/app -run '^TestWindowsPowerShellUpdater' -count=1 -timeout=3m
run: go test ./internal/app -run '^(TestWindowsPowerShellUpdater|TestExpectedAssetNameForExecutableUsesWindowsPortableSuffix|TestShouldWindowsUpdateLaunchDownloadedAssetDirectly|TestResolveUpdateInstallMode|TestExpectedAssetNameForWindowsInstallMode|TestResolveUpdateWorkspaceDirForPlatformSeparatesMSIFromInstallDirectory|TestValidateUpdatePackageForCurrentInstallMode|TestInstallUpdateAndRestartMSI|TestBuildWindowsMSIUpdatePowerShellScript|TestBuildWindowsMSILaunchCommand|TestResolveWindowsMSIExecPath)' -count=1 -timeout=3m
- name: Build
shell: bash
@@ -806,7 +829,7 @@ jobs:
mv "$DMG_NAME" "../../$FINAL_NAME"
# Windows Packaging
- name: Package Windows EXE
- name: Package Windows Portable EXE and MSI
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
@@ -816,7 +839,9 @@ jobs:
$version = $version.Substring(1)
}
$target = "${{ matrix.build_name }}"
$finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe"
$archName = "${{ matrix.arch_name }}"
$portableExeName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Portable.exe"
$msiName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Installer.msi"
if (Test-Path "$target.exe") {
$finalExe = "$target.exe"
@@ -859,8 +884,61 @@ jobs:
}
}
Write-Host "📦 输出 Windows 可执行文件 $finalExeName..."
Copy-Item -LiteralPath $finalExe -Destination "..\\..\\$finalExeName" -Force
Write-Host "📦 输出 Windows Portable 可执行文件 $portableExeName..."
Copy-Item -LiteralPath $finalExe -Destination "..\\..\\$portableExeName" -Force
if ($version -match '^(?<major>\d+)\.(?<minor>\d+)\.(?<build>\d+)(?:[-+].*)?$') {
$major = [int]$Matches.major
$minor = [int]$Matches.minor
$build = [int]$Matches.build
} else {
throw "Windows MSI 版本必须是 major.minor.patch 格式,当前为: $version"
}
if ($major -gt 255 -or $minor -gt 255 -or $build -gt 65535) {
throw "Windows MSI 版本超出范围 (major/minor <= 255, patch <= 65535): $version"
}
$productVersion = "$major.$minor.$build"
$wixArch = if ($archName -eq "Arm64") { "arm64" } else { "x64" }
$componentGuid = if ($archName -eq "Arm64") {
"7F4C7757-329F-42B0-B312-D4B8AD50415E"
} else {
"0BCEE70B-9CF2-449C-9ADE-190188493234"
}
$licenseText = Get-Content -LiteralPath "..\\..\\LICENSE" -Raw
$escapedLicense = $licenseText.Replace('\', '\\').Replace('{', '\{').Replace('}', '\}')
$escapedLicense = $escapedLicense -replace "`r?`n", "\par`r`n"
$licenseRtf = Join-Path $env:RUNNER_TEMP "GoNavi-License.rtf"
$rtfContent = "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}}\f0\fs18 $escapedLicense}"
Set-Content -LiteralPath $licenseRtf -Value $rtfContent -Encoding ascii
$sourceExe = (Resolve-Path -LiteralPath $finalExe).Path
$wxsPath = (Resolve-Path -LiteralPath "..\\windows\\installer.wxs").Path
$iconPath = (Resolve-Path -LiteralPath "..\\windows\\icon.ico").Path
$installMarkerPath = Join-Path $env:RUNNER_TEMP ".gonavi-msi-install"
Set-Content -LiteralPath $installMarkerPath -Value "MSI" -Encoding ascii -NoNewline
$installMarkerPath = (Resolve-Path -LiteralPath $installMarkerPath).Path
$wixIntermediateDir = Join-Path $env:RUNNER_TEMP "wixobj-$archName"
Write-Host "📦 生成 Windows MSI 安装程序 $msiName (version=$productVersion arch=$wixArch)..."
wix build $wxsPath `
-arch $wixArch `
-ext "WixToolset.UI.wixext/6.0.2" `
-d "ProductName=GoNavi" `
-d "ProductVersion=$productVersion" `
-d "UpgradeCode=CDD6BF2F-ED1E-4345-A0AB-DCDB7E15FB23" `
-d "ComponentGuid=$componentGuid" `
-d "InstallFolderName=GoNavi" `
-d "RegistryKeyName=GoNavi" `
-d "SourceExe=$sourceExe" `
-d "InstallMarker=$installMarkerPath" `
-d "IconPath=$iconPath" `
-d "LicenseRtf=$licenseRtf" `
-pdbtype none `
-intermediatefolder $wixIntermediateDir `
-out "..\\..\\$msiName"
if ($LASTEXITCODE -ne 0) {
throw "WiX MSI 构建失败,退出码: $LASTEXITCODE"
}
# Linux Packaging (tar.gz and AppImage)
- name: Package Linux
@@ -962,6 +1040,7 @@ jobs:
path: |
GoNavi-*.dmg
GoNavi-*.exe
GoNavi-*.msi
GoNavi-*.tar.gz
GoNavi-*.AppImage
drivers/**

View File

@@ -0,0 +1,76 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package Name="$(var.ProductName)"
Manufacturer="Syngnat"
Language="1033"
Version="$(var.ProductVersion)"
ProductCode="*"
UpgradeCode="$(var.UpgradeCode)"
Scope="perMachine"
InstallerVersion="500"
Compressed="yes">
<MajorUpgrade AllowDowngrades="yes"
Schedule="afterInstallInitialize" />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<Icon Id="GoNaviIcon" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="GoNaviIcon" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/Syngnat/GoNavi" />
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseRtf)" />
<Feature Id="MainFeature" Title="$(var.ProductName)" Level="1">
<ComponentRef Id="MainExecutable" />
</Feature>
<Component Id="MainExecutable"
Directory="INSTALLFOLDER"
Guid="$(var.ComponentGuid)"
Bitness="always64">
<File Id="GoNaviExe"
Source="$(var.SourceExe)"
Name="GoNavi.exe"
KeyPath="yes">
<Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder"
Name="$(var.ProductName)"
WorkingDirectory="INSTALLFOLDER"
Icon="GoNaviIcon"
Advertise="no" />
<Shortcut Id="DesktopShortcut"
Directory="DesktopFolder"
Name="$(var.ProductName)"
WorkingDirectory="INSTALLFOLDER"
Icon="GoNaviIcon"
Advertise="no" />
</File>
<File Id="GoNaviMsiInstallMarker"
Source="$(var.InstallMarker)"
Name=".gonavi-msi-install" />
<RegistryValue Root="HKLM"
Key="Software\Syngnat\$(var.RegistryKeyName)"
Name="InstallType"
Value="MSI"
Type="string"
KeyPath="no" />
<RegistryValue Root="HKLM"
Key="Software\Syngnat\$(var.RegistryKeyName)"
Name="InstallPath"
Value="[INSTALLFOLDER]GoNavi.exe"
Type="string"
KeyPath="no" />
</Component>
<ui:WixUI Id="WixUI_InstallDir"
InstallDirectory="INSTALLFOLDER"
ExtendedPathValidation="yes" />
</Package>
<Fragment>
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="$(var.InstallFolderName)" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<StandardDirectory Id="DesktopFolder" />
</Fragment>
</Wix>

View File

@@ -14,10 +14,13 @@ class GenerateUpdateLatestManifestTest(unittest.TestCase):
def test_generates_manifest_with_sha256(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
assets = Path(tmp)
exe = assets / "GoNavi-1.2.3-Windows-Amd64.exe"
exe = assets / "GoNavi-1.2.3-Windows-Amd64-Portable.exe"
msi = assets / "GoNavi-1.2.3-Windows-Amd64-Installer.msi"
exe.write_bytes(b"fake-binary")
msi.write_bytes(b"fake-installer")
(assets / "SHA256SUMS").write_text(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa GoNavi-1.2.3-Windows-Amd64.exe\n",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa GoNavi-1.2.3-Windows-Amd64-Portable.exe\n"
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb GoNavi-1.2.3-Windows-Amd64-Installer.msi\n",
encoding="utf-8",
)
out = assets / "latest.json"
@@ -43,14 +46,26 @@ class GenerateUpdateLatestManifestTest(unittest.TestCase):
self.assertEqual(data["channel"], "latest")
self.assertEqual(data["version"], "1.2.3")
self.assertEqual(data["tagName"], "v1.2.3")
self.assertEqual(len(data["assets"]), 1)
asset = data["assets"][0]
self.assertEqual(asset["name"], "GoNavi-1.2.3-Windows-Amd64.exe")
assets_by_name = {asset["name"]: asset for asset in data["assets"]}
self.assertEqual(
asset["url"],
"https://github.com/Syngnat/GoNavi/releases/download/v1.2.3/GoNavi-1.2.3-Windows-Amd64.exe",
set(assets_by_name),
{
"GoNavi-1.2.3-Windows-Amd64-Portable.exe",
"GoNavi-1.2.3-Windows-Amd64-Installer.msi",
},
)
self.assertEqual(asset["sha256"], "a" * 64)
portable_asset = assets_by_name["GoNavi-1.2.3-Windows-Amd64-Portable.exe"]
self.assertEqual(
portable_asset["url"],
"https://github.com/Syngnat/GoNavi/releases/download/v1.2.3/GoNavi-1.2.3-Windows-Amd64-Portable.exe",
)
self.assertEqual(portable_asset["sha256"], "a" * 64)
installer_asset = assets_by_name["GoNavi-1.2.3-Windows-Amd64-Installer.msi"]
self.assertEqual(
installer_asset["url"],
"https://github.com/Syngnat/GoNavi/releases/download/v1.2.3/GoNavi-1.2.3-Windows-Amd64-Installer.msi",
)
self.assertEqual(installer_asset["sha256"], "b" * 64)
self.assertNotIn("SHA256SUMS", [a["name"] for a in data["assets"]])

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WORKFLOWS = (
ROOT / ".github" / "workflows" / "release.yml",
ROOT / ".github" / "workflows" / "dev-build.yml",
)
INSTALLER = ROOT / "build" / "windows" / "installer.wxs"
WIX_NAMESPACE = "http://wixtoolset.org/schemas/v4/wxs"
UPGRADE_CODE = "CDD6BF2F-ED1E-4345-A0AB-DCDB7E15FB23"
AMD64_COMPONENT_GUID = "0BCEE70B-9CF2-449C-9ADE-190188493234"
ARM64_COMPONENT_GUID = "7F4C7757-329F-42B0-B312-D4B8AD50415E"
class WindowsReleaseArtifactsTest(unittest.TestCase):
def test_release_workflows_publish_portable_and_msi_assets(self) -> None:
for workflow in WORKFLOWS:
with self.subTest(workflow=workflow.name):
source = workflow.read_text(encoding="utf-8")
self.assertIn("Package Windows Portable EXE and MSI", source)
self.assertIn("-Portable.exe", source)
self.assertIn("-Installer.msi", source)
self.assertIn("GoNavi-*.msi", source)
self.assertIn("dotnet tool install wix --tool-path $wixTools --version $wixVersion", source)
self.assertIn('WixToolset.UI.wixext/$wixVersion', source)
self.assertIn('WixToolset.UI.wixext/6.0.2', source)
self.assertIn("-arch $wixArch", source)
self.assertIn("TestExpectedAssetNameForWindowsInstallMode", source)
self.assertIn("TestInstallUpdateAndRestartMSI", source)
self.assertIn("TestBuildWindowsMSIUpdatePowerShellScript", source)
self.assertIn('-d "ProductName=GoNavi"', source)
self.assertIn(f'-d "UpgradeCode={UPGRADE_CODE}"', source)
self.assertIn('-d "InstallFolderName=GoNavi"', source)
self.assertIn('-d "RegistryKeyName=GoNavi"', source)
self.assertIn(f'"{AMD64_COMPONENT_GUID}"', source)
self.assertIn(f'"{ARM64_COMPONENT_GUID}"', source)
self.assertIn('$installMarkerPath = Join-Path $env:RUNNER_TEMP ".gonavi-msi-install"', source)
self.assertIn('-d "InstallMarker=$installMarkerPath"', source)
self.assertNotIn('-d "ProductName=GoNavi Dev"', source)
self.assertNotIn('-d "InstallFolderName=GoNavi Dev"', source)
self.assertNotIn('-d "RegistryKeyName=GoNavi Dev"', source)
self.assertNotIn("DA3DACF2-1E4B-428C-8E6F-A37D3A05CAF7", source)
self.assertNotIn(
'$finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe"',
source,
)
dev_source = WORKFLOWS[1].read_text(encoding="utf-8")
self.assertIn('$productVersion = "255.0.$runNumber"', dev_source)
self.assertNotIn('$productVersion = "0.0.$runNumber"', dev_source)
def test_installer_declares_upgrade_shortcuts_and_uninstall_metadata(self) -> None:
root = ET.parse(INSTALLER).getroot()
ns = {"wix": WIX_NAMESPACE}
package = root.find("wix:Package", ns)
self.assertIsNotNone(package)
assert package is not None
self.assertEqual(package.attrib["Scope"], "perMachine")
self.assertEqual(package.attrib["InstallerVersion"], "500")
self.assertEqual(package.attrib["ProductCode"], "*")
major_upgrade = package.find("wix:MajorUpgrade", ns)
self.assertIsNotNone(major_upgrade)
assert major_upgrade is not None
self.assertEqual(major_upgrade.attrib["AllowDowngrades"], "yes")
self.assertEqual(major_upgrade.attrib["Schedule"], "afterInstallInitialize")
self.assertNotIn("AllowSameVersionUpgrades", major_upgrade.attrib)
self.assertNotIn("DowngradeErrorMessage", major_upgrade.attrib)
self.assertIsNotNone(package.find("wix:MediaTemplate", ns))
self.assertIsNotNone(package.find("wix:Property[@Id='ARPPRODUCTICON']", ns))
self.assertIsNotNone(package.find("wix:SetProperty[@Id='ARPINSTALLLOCATION']", ns))
self.assertIsNotNone(package.find(".//wix:Shortcut[@Id='StartMenuShortcut']", ns))
self.assertIsNotNone(package.find(".//wix:Shortcut[@Id='DesktopShortcut']", ns))
marker = package.find(".//wix:RegistryValue[@Name='InstallType']", ns)
self.assertIsNotNone(marker)
assert marker is not None
self.assertEqual(marker.attrib["Value"], "MSI")
install_path = package.find(".//wix:RegistryValue[@Name='InstallPath']", ns)
self.assertIsNotNone(install_path)
assert install_path is not None
self.assertEqual(install_path.attrib["Value"], "[INSTALLFOLDER]GoNavi.exe")
file_marker = package.find(".//wix:File[@Id='GoNaviMsiInstallMarker']", ns)
self.assertIsNotNone(file_marker)
assert file_marker is not None
self.assertEqual(file_marker.attrib["Source"], "$(var.InstallMarker)")
self.assertEqual(file_marker.attrib["Name"], ".gonavi-msi-install")
if __name__ == "__main__":
unittest.main()