From 7bdce19d01f3aa0299bf92432362a88bb75886c6 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 13 Jul 2026 09:25:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(release):=20=E5=8F=91=E5=B8=83?= =?UTF-8?q?=20Windows=20Portable=20=E4=B8=8E=20MSI=20=E5=8F=8C=E4=BA=A7?= =?UTF-8?q?=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Windows AMD64 与 ARM64 同时生成 Portable EXE 和 MSI 安装包 - 统一 stable 与 dev 的 MSI 产品身份并支持通道切换升级 - 发布清单同时包含两类资产及对应 SHA256 校验信息 - 在 Windows workflow 中执行双模式更新与 WiX 契约测试 --- .github/workflows/dev-build.yml | 83 ++++++++++++++++- .github/workflows/release.yml | 89 +++++++++++++++++- build/windows/installer.wxs | 76 +++++++++++++++ tools/generate-update-latest-manifest.test.py | 31 +++++-- tools/windows-release-artifacts.test.py | 93 +++++++++++++++++++ 5 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 build/windows/installer.wxs create mode 100644 tools/windows-release-artifacts.test.py diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 40d7e409..386c16b7 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -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/** diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1ec4e8a..da35d75f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 '^(?\d+)\.(?\d+)\.(?\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/** diff --git a/build/windows/installer.wxs b/build/windows/installer.wxs new file mode 100644 index 00000000..e7988b48 --- /dev/null +++ b/build/windows/installer.wxs @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/generate-update-latest-manifest.test.py b/tools/generate-update-latest-manifest.test.py index bf2f6e8b..92e4d1a4 100644 --- a/tools/generate-update-latest-manifest.test.py +++ b/tools/generate-update-latest-manifest.test.py @@ -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"]]) diff --git a/tools/windows-release-artifacts.test.py b/tools/windows-release-artifacts.test.py new file mode 100644 index 00000000..0938bdc0 --- /dev/null +++ b/tools/windows-release-artifacts.test.py @@ -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()