wix.gradle.kts 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. import java.io.*
  2. import org.w3c.dom.Document
  3. import org.w3c.dom.Element
  4. import java.io.File
  5. import java.util.UUID
  6. import javax.xml.parsers.DocumentBuilderFactory
  7. import javax.xml.transform.TransformerFactory
  8. import javax.xml.transform.dom.DOMSource
  9. import javax.xml.transform.stream.StreamResult
  10. import javax.xml.transform.OutputKeys
  11. import java.nio.charset.StandardCharsets
  12. val appDir = project.layout.projectDirectory.dir("build/compose/binaries/main/app/")
  13. val iconPath = project.file("src/main/resources/logo/logo.ico").absolutePath
  14. val removeIconPath = project.file("RemoveConfig/src/main/resources/remove.ico").absolutePath
  15. val licensePath = project.file("license.rtf").absolutePath
  16. val manufacturer = "深圳市龙华区幕境网络工作室"
  17. val shortcutName = "幕境"
  18. val appName = "幕境"
  19. project.tasks.register("renameApp") {
  20. group = "compose wix"
  21. description = "rename the app-image"
  22. val createDistributable = tasks.named("createDistributable")
  23. dependsOn(createDistributable)
  24. doLast {
  25. val imageDir = project.layout.projectDirectory.dir("build/compose/binaries/main/app/幕境")
  26. val appDirFile = imageDir.getAsFile()
  27. val appDirParent = appDirFile.parentFile
  28. val appName = appDirFile.name
  29. val newAppName = "MuJing"
  30. val newAppDir = File(appDirParent, newAppName)
  31. // 幕境 -> MuJing
  32. appDirFile.renameTo(newAppDir)
  33. // 把 newAppDir 目录下的 幕境.exe 重命名为 MuJing.exe
  34. val appExe = File(newAppDir, "$appName.exe")
  35. val newAppExe = File(newAppDir, "$newAppName.exe")
  36. appExe.renameTo(newAppExe)
  37. // 把 newAppDir 目录下的 app 目录下的 幕境.cfg 重命名为 MuJing.cfg
  38. val appCfg = File(newAppDir, "app/$appName.cfg")
  39. val newAppCfg = File(newAppDir, "app/$newAppName.cfg")
  40. appCfg.renameTo(newAppCfg)
  41. }
  42. }
  43. tasks.register<Exec>("createRemoveConfigExe") {
  44. group = "compose wix"
  45. description = "Create RemoveConfig exe"
  46. workingDir(project.layout.projectDirectory.dir("RemoveConfig"))
  47. commandLine("gradlew.bat", "createDistributable")
  48. val renameApp = tasks.named("renameApp")
  49. dependsOn(renameApp)
  50. doLast{
  51. val removeConfigApp =project.layout.projectDirectory.dir("RemoveConfig/build/compose/binaries/main/app/RemoveConfig/app/").asFile
  52. val mujingApp = project.layout.projectDirectory.dir("build/compose/binaries/main/app/MuJing/app/").asFile
  53. val mujing = project.layout.projectDirectory.dir("build/compose/binaries/main/app/MuJing/").asFile
  54. val removeJar = removeConfigApp.listFiles { file -> file.name.startsWith("RemoveConfig") && file.extension == "jar" }?.first()
  55. val removecfg = project.layout.projectDirectory.dir("RemoveConfig/build/compose/binaries/main/app/RemoveConfig/app/RemoveConfig.cfg").asFile
  56. val removeExe = project.layout.projectDirectory.dir("RemoveConfig/build/compose/binaries/main/app/RemoveConfig/RemoveConfig.exe").asFile
  57. // 需要把 RemoveConfig.cfg 和 RemoveConfig.jar 复制到 mujingApp 里面, 把 RemoveConfig.exe 复制到 mujing 里面
  58. removeJar?.copyTo(File(mujingApp, removeJar?.name))
  59. removecfg.copyTo(File(mujingApp, removecfg.name))
  60. removeExe.copyTo(File(mujing, removeExe.name))
  61. }
  62. }
  63. project.tasks.register<Exec>("harvest") {
  64. group = "compose wix"
  65. description = "Generates Wxs authoring from application image"
  66. val removeConfig = tasks.named("createRemoveConfigExe")
  67. dependsOn(removeConfig)
  68. workingDir(appDir)
  69. val heat = project.layout.projectDirectory.file("build/wix311/heat.exe").asFile.absolutePath
  70. // heat dir "./MuJing" -cg DefaultFeature -gg -sfrag -sreg -template product -out MuJing.wxs -var var.SourceDir
  71. commandLine(
  72. heat,
  73. "dir",
  74. "./MuJing",
  75. "-nologo",
  76. "-cg",
  77. "DefaultFeature",
  78. "-gg",
  79. "-sfrag",
  80. "-sreg",
  81. "-template",
  82. "product",
  83. "-out",
  84. "MuJing.wxs",
  85. "-var",
  86. "var.SourceDir"
  87. )
  88. }
  89. project.tasks.register("editWxs") {
  90. group = "compose wix"
  91. description = "Edit the Wxs File"
  92. val harvest = tasks.named("harvest")
  93. dependsOn(harvest)
  94. doLast {
  95. editWixTask(
  96. shortcutName = shortcutName,
  97. iconPath = iconPath,
  98. removeIconPath = removeIconPath,
  99. licensePath = licensePath,
  100. manufacturer = manufacturer
  101. )
  102. }
  103. }
  104. project.tasks.register<Exec>("compileWxs") {
  105. group = "compose wix"
  106. description = "Compile Wxs to Wixobj"
  107. val editWxs = tasks.named("editWxs")
  108. dependsOn(editWxs)
  109. workingDir(appDir)
  110. val candle = project.layout.projectDirectory.file("build/wix311/candle.exe").asFile.absolutePath
  111. // candle main.wxs -dSourceDir=".\MuJing"
  112. commandLine(candle, "MuJing.wxs","-nologo", "-dSourceDir=.\\MuJing")
  113. }
  114. project.tasks.register<Exec>("light") {
  115. group = "compose wix"
  116. description = "Linking the .wixobj file and creating a MSI"
  117. val compileWxs = tasks.named("compileWxs")
  118. dependsOn(compileWxs)
  119. workingDir(appDir)
  120. val light = project.layout.projectDirectory.file("build/wix311/light.exe").asFile.absolutePath
  121. // light -ext WixUIExtension -cultures:zh-CN -spdb MuJing.wixobj -o MuJing.msi
  122. commandLine(light, "-ext", "WixUIExtension", "-cultures:zh-CN", "-spdb","-nologo", "MuJing.wixobj", "-o", "MuJing-${project.version}.msi")
  123. }
  124. private fun editWixTask(
  125. shortcutName: String,
  126. iconPath: String,
  127. removeIconPath: String,
  128. licensePath: String,
  129. manufacturer:String
  130. ) {
  131. val wixFile = project.layout.projectDirectory.dir("build/compose/binaries/main/app/MuJing.wxs").asFile
  132. val dbf = DocumentBuilderFactory.newInstance()
  133. val doc = dbf.newDocumentBuilder().parse(wixFile)
  134. doc.documentElement.normalize()
  135. println("Root Element :" + doc.documentElement.nodeName)
  136. val productElement = doc.documentElement.getElementsByTagName("Product").item(0) as Element
  137. println(productElement.nodeName)
  138. productElement.setAttribute("Manufacturer", manufacturer)
  139. productElement.setAttribute("Codepage", "936")
  140. // 这个 Name 属性会出现在安装引导界面
  141. // 控制面板-程序列表里也是这个名字
  142. productElement.setAttribute("Name", "幕境")
  143. productElement.setAttribute("Version", "${project.version}")
  144. // 设置升级码, 用于升级,大版本更新时,可能需要修改这个值
  145. // 如果要修改这个值,可能还需要修改安装位置,如果不修改安装位置,两个版本会安装在同一个位置
  146. // 这段代码和 MajorUpgrade 相关,如果 UpgradeCode 一直保持不变,安装新版的时候会自动卸载旧版本。
  147. val upgradeCode = createNameUUID("v2.0")
  148. productElement.setAttribute("UpgradeCode", upgradeCode)
  149. val packageElement = productElement.getElementsByTagName("Package").item(0) as Element
  150. println(packageElement.nodeName)
  151. packageElement.setAttribute("Comments", "幕境")
  152. packageElement.setAttribute("Compressed", "yes")
  153. packageElement.setAttribute("InstallerVersion", "200")
  154. packageElement.setAttribute("Languages", "1033")
  155. packageElement.setAttribute("Manufacturer", manufacturer)
  156. packageElement.setAttribute("Platform", "x64")
  157. // <CustomAction Id="RunRemoveConfigExe"
  158. // FileKey="RemoveConfig.exe"
  159. // ExeCommand=""
  160. // Execute="deferred"
  161. // Impersonate="yes"
  162. // Return="ignore" />
  163. // <InstallExecuteSequence>
  164. // <Custom Action="RunRemoveConfigExe" After="UnpublishFeatures">(REMOVE = "ALL") AND (NOT UPGRADINGPRODUCTCODE)</Custom>
  165. // </InstallExecuteSequence>
  166. val removeConfig = doc.createElement("CustomAction").apply {
  167. setAttributeNode(doc.createAttribute("Id").also { it.value = "RunRemoveConfigExe" })
  168. setAttributeNode(doc.createAttribute("FileKey").also { it.value = "RemoveConfig.exe" })
  169. setAttributeNode(doc.createAttribute("ExeCommand").also { it.value = "--uninstall" })
  170. setAttributeNode(doc.createAttribute("Execute").also { it.value = "deferred" })
  171. setAttributeNode(doc.createAttribute("Impersonate").also { it.value = "yes" })
  172. setAttributeNode(doc.createAttribute("Return").also { it.value = "ignore" })
  173. }
  174. productElement.appendChild(removeConfig)
  175. // <CustomAction Id="RunVlcCacheGen"
  176. // Execute="deferred"
  177. // Impersonate="no"
  178. // Directory="INSTALLDIR"
  179. // ExeCommand="[INSTALLDIR]app\\resources\\VLC\\vlc-cache-gen.exe [INSTALLDIR]app\\resources\\VLC\\plugins"
  180. // Return="check" />
  181. val runVlcCacheGen = doc.createElement("CustomAction").apply{
  182. setAttributeNode(doc.createAttribute("Id").also { it.value = "RunVlcCacheGen" })
  183. setAttributeNode(doc.createAttribute("Execute").also { it.value = "deferred" })
  184. setAttributeNode(doc.createAttribute("Impersonate").also { it.value = "no" })
  185. setAttributeNode(doc.createAttribute("Directory").also { it.value = "INSTALLDIR" })
  186. setAttributeNode(doc.createAttribute("ExeCommand").also { it.value = "[INSTALLDIR]app\\resources\\VLC\\vlc-cache-gen.exe [INSTALLDIR]app\\resources\\VLC\\plugins" })
  187. setAttributeNode(doc.createAttribute("Return").also { it.value = "check" })
  188. }
  189. productElement.appendChild(runVlcCacheGen)
  190. val installExecuteSequence = doc.createElement("InstallExecuteSequence")
  191. productElement.appendChild(installExecuteSequence)
  192. // <Custom Action="RunRemoveConfigExe" After="UnpublishFeatures">(REMOVE = "ALL") AND (NOT UPGRADINGPRODUCTCODE)</Custom>
  193. val customActionRef = doc.createElement("Custom").apply{
  194. setAttributeNode(doc.createAttribute("Action").also { it.value = "RunRemoveConfigExe" })
  195. setAttributeNode(doc.createAttribute("After").also { it.value = "UnpublishFeatures" })
  196. appendChild(doc.createTextNode("(REMOVE = \"ALL\") AND (NOT UPGRADINGPRODUCTCODE)"))
  197. }
  198. installExecuteSequence.appendChild(customActionRef)
  199. // <Custom Action="RunVlcCacheGen" After="InstallFiles">NOT Installed</Custom>
  200. val customActionGenCache = doc.createElement("Custom").apply{
  201. setAttributeNode(doc.createAttribute("Action").also { it.value = "RunVlcCacheGen" })
  202. setAttributeNode(doc.createAttribute("After").also { it.value = "InstallFiles" })
  203. appendChild(doc.createTextNode("NOT Installed"))
  204. }
  205. installExecuteSequence.appendChild(customActionGenCache)
  206. val targetDirectory = doc.documentElement.getElementsByTagName("Directory").item(0) as Element
  207. // 桌面文件夹
  208. // <Directory Id="DesktopFolder" Name="Desktop" />
  209. val desktopFolderElement = directoryBuilder(doc, id = "DesktopFolder").apply{
  210. setAttributeNode(doc.createAttribute("Name").also { it.value = "Desktop" })
  211. }
  212. val desktopGuid = createNameUUID("DesktopShortcutComponent")
  213. val desktopComponent = componentBuilder(doc, id = "DesktopShortcutComponent", guid = desktopGuid)
  214. val desktopReg = registryBuilder(doc, id = "DesktopShortcutReg", productCode = "[ProductCode]")
  215. // <Shortcut Advertise="no" Directory="DesktopFolder" Target = "[INSTALLDIR]MuJing.exe" Icon="icon.ico" IconIndex="0" Id="DesktopShortcut" Name="幕境" WorkingDirectory="INSTALLDIR"/>
  216. val desktopShortcut = shortcutBuilder(
  217. doc,
  218. id = "DesktopShortcut",
  219. directory = "DesktopFolder",
  220. workingDirectory = "INSTALLDIR",
  221. name = shortcutName,
  222. target = "[INSTALLDIR]MuJing.exe",
  223. icon="icon.ico"
  224. )
  225. // <RemoveFile Id="DesktopShortcut" On="uninstall" Name="幕境.lnk" Directory="DesktopFolder"/>
  226. val removeDesktopShortcut = doc.createElement("RemoveFile").apply{
  227. setAttributeNode(doc.createAttribute("Id").also { it.value = "DesktopShortcut" })
  228. setAttributeNode(doc.createAttribute("On").also { it.value = "uninstall" })
  229. setAttributeNode(doc.createAttribute("Name").also { it.value = "$shortcutName.lnk" })
  230. setAttributeNode(doc.createAttribute("Directory").also { it.value = "DesktopFolder" })
  231. }
  232. desktopComponent.appendChild(desktopShortcut)
  233. desktopComponent.appendChild(desktopReg)
  234. desktopComponent.appendChild(removeDesktopShortcut)
  235. desktopFolderElement.appendChild(desktopComponent)
  236. targetDirectory.appendChild(desktopFolderElement)
  237. // 开始菜单文件夹
  238. val programMenuFolderElement = directoryBuilder(doc, id = "ProgramMenuFolder", name = "Programs")
  239. val programeMenuDir = directoryBuilder(doc, id = "ProgramMenuDir", name = "幕境")
  240. val menuGuid = createNameUUID("programMenuDirComponent")
  241. val programMenuDirComponent = componentBuilder(doc, id = "programMenuDirComponent", guid = menuGuid)
  242. val startMenuShortcut = shortcutBuilder(
  243. doc,
  244. id = "startMenuShortcut",
  245. directory = "ProgramMenuDir",
  246. workingDirectory = "INSTALLDIR",
  247. name = shortcutName,
  248. target = "[INSTALLDIR]MuJing.exe"
  249. )
  250. val uninstallShortcut = shortcutBuilder(
  251. doc,
  252. id = "uninstallShortcut",
  253. name = "卸载幕境",
  254. directory = "ProgramMenuDir",
  255. target = "[System64Folder]msiexec.exe",
  256. arguments = "/x [ProductCode]",
  257. icon = "removeIcon.ico"
  258. )
  259. val removeFolder = removeFolderBuilder(doc, id = "CleanUpShortCut", directory = "ProgramMenuDir")
  260. val pRegistryValue = registryBuilder(doc, id = "ProgramMenuShortcutReg", productCode = "[ProductCode]")
  261. programMenuFolderElement.appendChild(programeMenuDir)
  262. programeMenuDir.appendChild(programMenuDirComponent)
  263. programMenuDirComponent.appendChild(startMenuShortcut)
  264. programMenuDirComponent.appendChild(uninstallShortcut)
  265. programMenuDirComponent.appendChild(removeFolder)
  266. programMenuDirComponent.appendChild(pRegistryValue)
  267. //<Component Guid="*" Id="RemoveShortcutComponent" Win64="yes">
  268. // <RemoveFile Id="RemoveMenuShortcut" On="uninstall" Name="幕境.lnk" Directory="ProgramMenuDir"/>
  269. // <RegistryValue Id="RemoveMenuShortcutReg" Key="Software\MuJing" KeyPath="yes" Name="ProductCode" Root="HKCU" Type="string" Value="[ProductCode]"/>
  270. //</Component>
  271. val removeShortcutComponent = componentBuilder(doc, id = "RemoveShortcutComponent", guid = createNameUUID("RemoveShortcutComponent"))
  272. val removeMenuShortcut = doc.createElement("RemoveFile").apply{
  273. setAttributeNode(doc.createAttribute("Id").also { it.value = "RemoveMenuShortcut" })
  274. setAttributeNode(doc.createAttribute("On").also { it.value = "uninstall" })
  275. setAttributeNode(doc.createAttribute("Name").also { it.value = "*.lnk" })
  276. setAttributeNode(doc.createAttribute("Directory").also { it.value = "ProgramMenuDir" })
  277. }
  278. val removeMenuShortcutReg = registryBuilder(doc, id = "RemoveMenuShortcutReg", productCode = "[ProductCode]")
  279. removeShortcutComponent.appendChild(removeMenuShortcut)
  280. removeShortcutComponent.appendChild(removeMenuShortcutReg)
  281. targetDirectory.appendChild(programMenuFolderElement)
  282. targetDirectory.appendChild(removeShortcutComponent)
  283. // 添加 ProgramFiles64Folder 节点
  284. val programFilesElement = doc.createElement("Directory")
  285. val idAttr = doc.createAttribute("Id")
  286. idAttr.value = "ProgramFiles64Folder"
  287. programFilesElement.setAttributeNode(idAttr)
  288. targetDirectory.appendChild(programFilesElement)
  289. val installDir = targetDirectory.getElementsByTagName("Directory").item(0)
  290. // 移除 installDir 节点
  291. val removedNode = targetDirectory.removeChild(installDir)
  292. // 将 installDir 节点添加到 programFilesElement 节点
  293. programFilesElement.appendChild(removedNode)
  294. // 设置安装目录的 Id 为 INSTALLDIR,快捷方式需要引用这个 Id
  295. val installDirElement = programFilesElement.getElementsByTagName("Directory").item(0) as Element
  296. installDirElement.setAttribute("Id", "INSTALLDIR")
  297. val fileComponents = installDirElement.getElementsByTagName("Component")
  298. for (i in 0 until fileComponents.length) {
  299. val component = fileComponents.item(i) as Element
  300. val files = component.getElementsByTagName("File")
  301. val file = files.item(0) as Element
  302. // 设置 RemoveConfig.exe文件的 Id 为 RemoveConfig.exe
  303. if(file.getAttribute("Source").endsWith("RemoveConfig.exe")){
  304. file.setAttribute("Id","RemoveConfig.exe")
  305. }
  306. }
  307. // <Component Guid="{GUID}" Id="installProduct">
  308. // <RegistryValue Root="HKLM" Key="Software\MuJing"
  309. // Name="InstallLocation" Type="string" Value="[INSTALLDIR]" KeyPath="yes"/>
  310. //</Component>
  311. val installGuid = createNameUUID("installProduct")
  312. val installComponent = componentBuilder(doc, id = "installProduct", guid = installGuid)
  313. val installRegistry = doc.createElement("RegistryValue").apply{
  314. setAttributeNode(doc.createAttribute("Root").also { it.value = "HKLM" })
  315. setAttributeNode(doc.createAttribute("Key").also { it.value = "Software\\MuJing" })
  316. setAttributeNode(doc.createAttribute("Name").also { it.value = "InstallLocation" })
  317. setAttributeNode(doc.createAttribute("Type").also { it.value = "string" })
  318. setAttributeNode(doc.createAttribute("Value").also { it.value = "[INSTALLDIR]" })
  319. setAttributeNode(doc.createAttribute("KeyPath").also { it.value = "yes" })
  320. }
  321. installComponent.appendChild(installRegistry)
  322. installDirElement.appendChild(installComponent)
  323. // 设置所有组件的架构为 64 位
  324. val components = doc.documentElement.getElementsByTagName("Component")
  325. for (i in 0 until components.length) {
  326. val component = components.item(i) as Element
  327. val win64 = doc.createAttribute("Win64")
  328. win64.value = "yes"
  329. component.setAttributeNode(win64)
  330. }
  331. // 设置 Feature 节点
  332. val featureElement = doc.getElementsByTagName("Feature").item(0) as Element
  333. featureElement.setAttribute("Id", "Complete")
  334. featureElement.setAttribute("Title", "幕境")
  335. // 设置 UI
  336. // 添加 <Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
  337. val installUI = doc.createElement("Property").apply{
  338. setAttributeNode(doc.createAttribute("Id").also { it.value = "WIXUI_INSTALLDIR" })
  339. setAttributeNode(doc.createAttribute("Value").also { it.value = "INSTALLDIR" })
  340. }
  341. productElement.appendChild(installUI)
  342. // 添加 <UIRef Id="WixUI_InstallDir" />
  343. val installDirUIRef = doc.createElement("UIRef").apply{
  344. setAttributeNode(doc.createAttribute("Id").also { it.value = "WixUI_InstallDir" })
  345. }
  346. productElement.appendChild(installDirUIRef)
  347. // 添加 <UIRef Id="WixUI_ErrorProgressText" />
  348. val errText = doc.createElement("UIRef").apply{
  349. setAttributeNode(doc.createAttribute("Id").also { it.value = "WixUI_ErrorProgressText" })
  350. }
  351. productElement.appendChild(errText)
  352. // 添加 Icon, 这个 Icon 会显示在控制面板的应用程序列表
  353. // <Icon Id="icon.ico" SourceFile="D:\MuJing\wix\MuJing\logo.ico"/>
  354. val iconElement = doc.createElement("Icon").apply{
  355. setAttributeNode(doc.createAttribute("Id").also { it.value = "icon.ico" })
  356. setAttributeNode(doc.createAttribute("SourceFile").also { it.value = iconPath })
  357. }
  358. productElement.appendChild(iconElement)
  359. // <Property Id="ARPPRODUCTICON" Value="icon.ico" />
  360. val iconProperty = doc.createElement("Property").apply{
  361. setAttributeNode(doc.createAttribute("Id").also { it.value = "ARPPRODUCTICON" })
  362. setAttributeNode(doc.createAttribute("Value").also { it.value = "icon.ico" })
  363. }
  364. productElement.appendChild(iconProperty)
  365. //<Icon Id="removeIcon.ico" SourceFile="removeIconPath"/>
  366. val removeIconElement = doc.createElement("Icon").apply{
  367. setAttributeNode(doc.createAttribute("Id").also { it.value = "removeIcon.ico" })
  368. setAttributeNode(doc.createAttribute("SourceFile").also { it.value = removeIconPath })
  369. }
  370. productElement.appendChild(removeIconElement)
  371. // 设置 license file
  372. // <WixVariable Id="WixUILicenseRtf" Value="license.rtf" />
  373. val wixVariable = doc.createElement("WixVariable").apply{
  374. setAttributeNode(doc.createAttribute("Id").also { it.value = "WixUILicenseRtf" })
  375. setAttributeNode(doc.createAttribute("Value").also { it.value = licensePath })
  376. }
  377. productElement.appendChild(wixVariable)
  378. // 安装新版时,自动卸载旧版本,已经安装新版,再安装旧版本,提示用户先卸载新版。
  379. // 这段逻辑要和 UpgradeCode 一起设置,如果 UpgradeCode 一直保持不变,安装新版的时候会自动卸载旧版本。
  380. // 如果 UpgradeCode 改变了,可能会安装两个版本
  381. // <MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="新版的[ProductName]已经安装,如果要安装旧版本,请先把新版本卸载。" />
  382. val majorUpgrade = doc.createElement("MajorUpgrade").apply{
  383. setAttributeNode(doc.createAttribute("AllowSameVersionUpgrades").also { it.value = "yes" })
  384. val message = "新版的[ProductName]已经安装,如果要安装旧版本,请先把新版本卸载。"
  385. setAttributeNode(doc.createAttribute("DowngradeErrorMessage").also { it.value = message })
  386. }
  387. productElement.appendChild(majorUpgrade)
  388. // <Property Id="INSTALLED">
  389. // <RegistrySearch Id="SearchOldVersion" Root="HKCU" Key="Software\深圳市龙华区幕境网络工作室\幕境\2.3.1" Name="ProductCode" Type="raw" Win64 = "yes" />
  390. // </Property>
  391. val installedProperty = doc.createElement("Property")
  392. val installedPropertyId = doc.createAttribute("Id")
  393. installedPropertyId.value = "INSTALLED"
  394. installedProperty.setAttributeNode(installedPropertyId)
  395. // 幕境 v2 的所有版本
  396. val oldVersionList = listOf("2.3.1","2.3.0","2.2.25","2.2.23","2.2.20","2.2.16","2.2.13","2.2.12","2.2.3","2.1.6","2.1.5","2.1.4","2.1.1","2.0.8","2.0.7","2.0.6","2.0.5","2.0.4","2.0.3","2.0.2")
  397. var id = 1
  398. oldVersionList.forEach{version ->
  399. val registrySearch = registrySearchBuilder(doc,version,id)
  400. id++
  401. installedProperty.appendChild(registrySearch)
  402. }
  403. productElement.appendChild(installedProperty)
  404. // Condition 的语法好变态,如果不看文档根本没法写,语法的详细解释在这里:
  405. // https://learn.microsoft.com/en-us/windows/win32/msi/conditional-statement-syntax#summary-of-conditional-statement-syntax
  406. //<!-- NOT INSTALLED -->
  407. //<!-- 如果 INSTALLED 为空或 null,那么 NOT INSTALLED 的结果将是 FALSE-->
  408. //<!-- 如果 INSTALLED 不为空或 null,那么 NOT INSTALLED 的结果将是 TRUE-->
  409. // <Condition Message="已经安装了幕境的另一个版本,无法继续安装此版本。可以使用”控制面板“中”添加/删除程序“来删除该版本">
  410. // <![CDATA[NOT INSTALLED]]>
  411. // </Condition>
  412. val installCondition = doc.createElement("Condition").apply{
  413. val message = "已经安装了幕境的另一个版本,无法继续安装此版本。可以使用”控制面板“中”添加/删除程序“来删除该版本"
  414. setAttributeNode(doc.createAttribute("Message").also { it.value = message })
  415. appendChild(doc.createCDATASection("NOT INSTALLED"))
  416. }
  417. productElement.appendChild(installCondition)
  418. // 设置 fragment 节点
  419. val fragmentElement = doc.getElementsByTagName("Fragment").item(0) as Element
  420. val componentGroup = fragmentElement.getElementsByTagName("ComponentGroup").item(0) as Element
  421. val desktopShortcuRef = componentRefBuilder(doc, "DesktopShortcutComponent")
  422. val programMenuDirRef = componentRefBuilder(doc, "programMenuDirComponent")
  423. val removeShortcutRef = componentRefBuilder(doc, "RemoveShortcutComponent")
  424. val installProductRef = componentRefBuilder(doc, "installProduct")
  425. componentGroup.appendChild(desktopShortcuRef)
  426. componentGroup.appendChild(programMenuDirRef)
  427. componentGroup.appendChild(installProductRef)
  428. componentGroup.appendChild(removeShortcutRef)
  429. generateXml(doc, wixFile)
  430. }
  431. private fun generateXml(doc: Document, file: File) {
  432. // Instantiate the Transformer
  433. val transformerFactory = TransformerFactory.newInstance()
  434. transformerFactory.setAttribute("indent-number", 4);
  435. val transformer = transformerFactory.newTransformer()
  436. // Enable indentation and set encoding
  437. transformer.setOutputProperty(OutputKeys.INDENT, "yes")
  438. transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
  439. transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
  440. val source = DOMSource(doc)
  441. val result = StreamResult(file)
  442. transformer.transform(source, result)
  443. }
  444. private fun directoryBuilder(doc: Document, id: String, name: String = ""): Element {
  445. val directory = doc.createElement("Directory").apply{
  446. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  447. if(name.isNotEmpty()){
  448. setAttributeNode(doc.createAttribute("Name").also { it.value = name })
  449. }
  450. }
  451. return directory
  452. }
  453. private fun componentBuilder(doc: Document, id: String, guid: String): Element {
  454. val component = doc.createElement("Component").apply{
  455. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  456. setAttributeNode(doc.createAttribute("Guid").also { it.value = guid })
  457. }
  458. return component
  459. }
  460. private fun registryBuilder(doc: Document, id: String, productCode: String): Element {
  461. val regComponentElement = doc.createElement("RegistryValue").apply{
  462. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  463. setAttributeNode(doc.createAttribute("Root").also { it.value = "HKCU" })
  464. setAttributeNode(doc.createAttribute("Key").also { it.value = "Software\\MuJing" })
  465. setAttributeNode(doc.createAttribute("Type").also { it.value = "string" })
  466. setAttributeNode(doc.createAttribute("Name").also { it.value = "ProductCode" })
  467. setAttributeNode(doc.createAttribute("Value").also { it.value = productCode })
  468. setAttributeNode(doc.createAttribute("KeyPath").also { it.value = "yes" })
  469. }
  470. return regComponentElement
  471. }
  472. private fun registrySearchBuilder(doc:Document,version:String,id:Int):Element{
  473. val registrySearch = doc.createElement("RegistrySearch").apply{
  474. setAttributeNode(doc.createAttribute("Id").also { it.value = "SearchOldVersion$id" })
  475. setAttributeNode(doc.createAttribute("Root").also { it.value = "HKCU" })
  476. setAttributeNode(doc.createAttribute("Key").also { it.value = "Software\\深圳市龙华区幕境网络工作室\\幕境\\$version" })
  477. setAttributeNode(doc.createAttribute("Name").also { it.value = "ProductCode" })
  478. setAttributeNode(doc.createAttribute("Type").also { it.value = "raw" })
  479. setAttributeNode(doc.createAttribute("Win64").also { it.value = "yes" })
  480. }
  481. return registrySearch
  482. }
  483. private fun shortcutBuilder(
  484. doc: Document,
  485. id: String,
  486. directory: String = "",
  487. workingDirectory: String = "",
  488. name: String,
  489. target: String,
  490. description: String = "",
  491. arguments: String = "",
  492. icon:String = ""
  493. ): Element {
  494. val shortcut = doc.createElement("Shortcut").apply{
  495. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  496. setAttributeNode(doc.createAttribute("Name").also { it.value = name })
  497. setAttributeNode(doc.createAttribute("Advertise").also { it.value = "no" })
  498. setAttributeNode(doc.createAttribute("Target").also { it.value = target })
  499. if(directory.isNotEmpty()){
  500. setAttributeNode(doc.createAttribute("Directory").also { it.value = directory })
  501. }
  502. if(workingDirectory.isNotEmpty()){
  503. setAttributeNode(doc.createAttribute("WorkingDirectory").also { it.value = workingDirectory })
  504. }
  505. if(description.isNotEmpty()){
  506. setAttributeNode(doc.createAttribute("Description").also { it.value = description })
  507. }
  508. if(arguments.isNotEmpty()){
  509. setAttributeNode(doc.createAttribute("Arguments").also { it.value = arguments })
  510. }
  511. if(icon.isNotEmpty()){
  512. setAttributeNode(doc.createAttribute("Icon").also { it.value = icon })
  513. }
  514. }
  515. return shortcut
  516. }
  517. private fun removeFolderBuilder(doc: Document, id: String,directory:String): Element {
  518. val removeFolder = doc.createElement("RemoveFolder").apply{
  519. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  520. setAttributeNode(doc.createAttribute("Directory").also { it.value = directory })
  521. setAttributeNode(doc.createAttribute("On").also { it.value = "uninstall" })
  522. }
  523. return removeFolder
  524. }
  525. private fun componentRefBuilder(doc: Document, id: String): Element {
  526. val componentRef = doc.createElement("ComponentRef").apply{
  527. setAttributeNode(doc.createAttribute("Id").also { it.value = id })
  528. }
  529. return componentRef
  530. }
  531. private fun createNameUUID(str: String): String {
  532. return "{" + UUID.nameUUIDFromBytes(str.toByteArray(StandardCharsets.UTF_8)).toString().uppercase() + "}"
  533. }