1# Copyright (C) 2001-2006 William Joseph. 2# 3# This file is part of GtkRadiant. 4# 5# GtkRadiant is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# GtkRadiant is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with GtkRadiant; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 19 20import os.path 21import xml.dom 22import os 23import stat 24import string 25 26from xml.dom.minidom import parse 27 28import msi 29 30cwd = os.getcwd() 31print("cwd=" + cwd) 32 33 34def format_guid(guid): 35 return "{" + guid.upper() + "}" 36 37def generate_guid(): 38 os.system("uuidgen > tmp_uuid.txt") 39 uuidFile = file("tmp_uuid.txt", "rt") 40 guid = format_guid(uuidFile.read(36)) 41 uuidFile.close() 42 os.system("del tmp_uuid.txt") 43 return guid 44 45def path_components(path): 46 directories = [] 47 remaining = path 48 while(remaining != ""): 49 splitPath = os.path.split(remaining) 50 remaining = splitPath[0] 51 directories.append(splitPath[1]) 52 directories.reverse() 53 return directories 54 55 56 57class Feature: 58 def __init__(self, feature, parent, title, desc, display, level, directory, attributes): 59 self.feature = feature 60 self.parent = parent 61 self.title = title 62 self.desc = desc 63 self.display = display 64 self.level = level 65 self.directory = directory 66 self.attributes = attributes 67 68class FeatureComponent: 69 def __init__(self, feature, component): 70 self.feature = feature 71 self.component = component 72 73class Directory: 74 def __init__(self, directory, parent, default): 75 self.directory = directory 76 self.parent = parent 77 self.default = default 78 79class Component: 80 def __init__(self, name, keypath, directory, attributes): 81 self.name = name 82 self.keypath = keypath 83 self.directory = directory 84 self.attributes = attributes 85 86class File: 87 def __init__(self, file, component, filename, filesize, sequence): 88 self.file = file 89 self.component = component 90 self.filename = filename 91 self.filesize = filesize 92 self.sequence = sequence 93 94class Shortcut: 95 def __init__(self, name, directory, component, feature, icon): 96 self.name = name 97 self.directory = directory 98 self.component = component 99 self.feature = feature 100 self.icon = icon 101 102class ComponentFiles: 103 def __init__(self, name, files, directory): 104 self.name = name 105 self.files = files 106 self.directory = directory 107 108class MSIPackage: 109 def __init__(self, packageFile): 110 self.code = "" 111 self.name = "" 112 self.version = "" 113 self.target = "" 114 self.license = "" 115 self.cabList = [] 116 self.featureCount = 0 117 self.featureTable = [] 118 self.featurecomponentsTable = [] 119 self.componentCache = {} 120 self.componentCount = 0 121 self.componentTable = {} 122 self.directoryTree = {} 123 self.directoryCount = 0 124 self.directoryTable = [] 125 self.fileCount = 0 126 self.fileTable = [] 127 self.shortcutCount = 0 128 self.shortcutTable = [] 129 self.createPackage(packageFile) 130 131 def addDirectory(self, directoryName, parentKey, directory): 132 if(not directory.has_key(directoryName)): 133 directoryKey = "d" + str(self.directoryCount) 134 self.directoryCount = self.directoryCount + 1 135 print("adding msi directory " + directoryKey + " parent=" + parentKey + " name=" + directoryName) 136 self.directoryTable.append(Directory(directoryKey, parentKey, directoryKey + "|" + directoryName)) 137 directory[directoryName] = (directoryKey, {}) 138 else: 139 print("ignored duplicate directory " + directoryName) 140 return directory[directoryName] 141 142 def parseComponentTree(self, treeElement, parent, directory, directoryPath, component): 143 files = [] 144 for childElement in treeElement.childNodes: 145 if (childElement.nodeName == "file"): 146 fileName = childElement.getAttribute("name") 147 filePath = os.path.join(directoryPath, fileName) 148 if(fileName != "" and os.path.exists(filePath)): 149 print("found file " + filePath) 150 file = (fileName, os.path.getsize(filePath), filePath) 151 files.append(file) 152 else: 153 raise Exception("file not found " + filePath) 154 155 if (childElement.nodeName == "dir"): 156 directoryName = childElement.getAttribute("name") 157 print("found directory " + directoryName) 158 directoryPair = self.addDirectory(directoryName, parent, directory) 159 self.parseComponentTree(childElement, directoryPair[0], directoryPair[1], os.path.join(directoryPath, directoryName), component) 160 161 count = len(files) 162 if(count != 0): 163 componentKey = "c" + str(self.componentCount) 164 self.componentCount = self.componentCount + 1 165 msiComponent = ComponentFiles(componentKey, files, parent); 166 print("adding msi component " + msiComponent.name + " with " + str(count) + " file(s)") 167 component.append(msiComponent) 168 169 def parseComponent(self, componentElement, rootPath): 170 shortcut = componentElement.getAttribute("shortcut") 171 icon = componentElement.getAttribute("icon") 172 component = [] 173 subDirectory = componentElement.getAttribute("subdirectory") 174 directoryPair = ("TARGETDIR", self.directoryTree) 175 for directoryName in path_components(subDirectory): 176 directoryPair = self.addDirectory(directoryName, directoryPair[0], directoryPair[1]) 177 self.parseComponentTree(componentElement, directoryPair[0], directoryPair[1], rootPath, component) 178 component.reverse() 179 print("component requires " + str(len(component)) + " msi component(s)") 180 return (component, shortcut, icon) 181 182 def parseComponentXML(self, filename, rootPath): 183 componentDocument = parse(filename) 184 print("parsing component file " + filename) 185 componentElement = componentDocument.documentElement 186 return self.parseComponent(componentElement, rootPath) 187 188 def componentForName(self, name, rootPath): 189 if(self.componentCache.has_key(name)): 190 return self.componentCache[name] 191 else: 192 component = self.parseComponentXML(name, rootPath) 193 self.componentCache[name] = component 194 return component 195 196 def parseFeature(self, featureElement, parent, index): 197 featureName = "ft" + str(self.featureCount) 198 self.featureCount = self.featureCount + 1 199 title = featureElement.getAttribute("name") 200 desc = featureElement.getAttribute("desc") 201 print("adding msi feature " + featureName + " title=" + title) 202 feature = Feature(featureName, parent, title, desc, index, 1, "TARGETDIR", 8) 203 self.featureTable.append(feature) 204 featureComponents = {} 205 indexChild = 2 206 for childElement in featureElement.childNodes: 207 if (childElement.nodeName == "feature"): 208 self.parseFeature(childElement, featureName, indexChild) 209 indexChild = indexChild + 2 210 elif (childElement.nodeName == "component"): 211 componentName = os.path.normpath(os.path.join(cwd, childElement.getAttribute("name"))) 212 if(featureComponents.has_key(componentName)): 213 raise Exception("feature \"" + title + "\" contains more than one reference to \"" + componentName + "\"") 214 featureComponents[componentName] = "" 215 componentSource = os.path.normpath(childElement.getAttribute("root")) 216 print("found component reference " + componentName) 217 componentPair = self.componentForName(componentName, componentSource) 218 component = componentPair[0] 219 for msiComponent in component: 220 print("adding msi featurecomponent " + featureName + " name=" + msiComponent.name) 221 self.featurecomponentsTable.append(FeatureComponent(featureName, msiComponent.name)) 222 223 if(not self.componentTable.has_key(msiComponent.name)): 224 keyPath = "" 225 for fileTuple in msiComponent.files: 226 fileKey = "f" + str(self.fileCount) 227 self.fileCount = self.fileCount + 1 228 if(keyPath == ""): 229 keyPath = fileKey 230 print("component " + msiComponent.name + " keypath=" + keyPath) 231 print("adding msi file " + fileKey + " name=" + fileTuple[0] + " size=" + str(fileTuple[1])) 232 self.fileTable.append(File(fileKey, msiComponent.name, fileKey + "|" + fileTuple[0], fileTuple[1], self.fileCount)) 233 self.cabList.append("\"" + fileTuple[2] + "\" " + fileKey + "\n") 234 self.componentTable[msiComponent.name] = Component(msiComponent.name, keyPath, msiComponent.directory, 0) 235 236 shortcut = componentPair[1] 237 if(shortcut != ""): 238 shortcutName = "sc" + str(self.shortcutCount) 239 self.shortcutCount = self.shortcutCount + 1 240 self.shortcutTable.append(Shortcut(shortcutName + "|" + shortcut, "ProductShortcutFolder", component[0].name, featureName, componentPair[2])) 241 print("adding msi shortcut " + shortcut) 242 243 def parsePackage(self, packageElement): 244 index = 2 245 self.code = packageElement.getAttribute("code") 246 if(self.code == ""): 247 raise Exception("invalid package code") 248 self.version = packageElement.getAttribute("version") 249 if(self.version == ""): 250 raise Exception("invalid package version") 251 self.name = packageElement.getAttribute("name") 252 if(self.name == ""): 253 raise Exception("invalid package name") 254 self.target = packageElement.getAttribute("target") 255 if(self.target == ""): 256 raise Exception("invalid target directory") 257 self.license = packageElement.getAttribute("license") 258 if(self.license == ""): 259 raise Exception("invalid package license agreement") 260 for childElement in packageElement.childNodes: 261 if (childElement.nodeName == "feature"): 262 self.parseFeature(childElement, "", index) 263 index = index + 2 264 265 def parsePackageXML(self, filename): 266 document = parse(filename) 267 print("parsing package file " + filename) 268 self.parsePackage(document.documentElement) 269 270 def createPackage(self, packageFile): 271 self.directoryTable.append(Directory("TARGETDIR", "", "SourceDir")) 272 self.directoryTable.append(Directory("ProgramMenuFolder", "TARGETDIR", ".")) 273 self.directoryTable.append(Directory("SystemFolder", "TARGETDIR", ".")) 274 self.parsePackageXML(packageFile) 275 if(self.shortcutCount != 0): 276 self.directoryTable.append(Directory("ProductShortcutFolder", "ProgramMenuFolder", "s0|" + self.name)) 277 278 def writeFileTable(self, name): 279 tableFile = file(name, "wt") 280 tableFile.write("File\tComponent_\tFileName\tFileSize\tVersion\tLanguage\tAttributes\tSequence\ns72\ts72\tl255\ti4\tS72\tS20\tI2\ti2\nFile\tFile\n") 281 for row in self.fileTable: 282 tableFile.write(row.file + "\t" + row.component + "\t" + row.filename + "\t" + str(row.filesize) + "\t" + "" + "\t" + "" + "\t" + "0" + "\t" + str(row.sequence) + "\n") 283 284 def writeComponentTable(self, name): 285 tableFile = file(name, "wt") 286 tableFile.write("Component\tComponentId\tDirectory_\tAttributes\tCondition\tKeyPath\ns72\tS38\ts72\ti2\tS255\tS72\nComponent\tComponent\n") 287 for k, row in self.componentTable.iteritems(): 288 tableFile.write(row.name + "\t" + generate_guid() + "\t" + row.directory + "\t" + str(row.attributes) + "\t" + "" + "\t" + row.keypath + "\n") 289 290 def writeFeatureComponentsTable(self, name): 291 tableFile = file(name, "wt") 292 tableFile.write("Feature_\tComponent_\ns38\ts72\nFeatureComponents\tFeature_\tComponent_\n") 293 for row in self.featurecomponentsTable: 294 tableFile.write(row.feature + "\t" + row.component + "\n") 295 296 def writeDirectoryTable(self, name): 297 tableFile = file(name, "wt") 298 tableFile.write("Directory\tDirectory_Parent\tDefaultDir\ns72\tS72\tl255\nDirectory\tDirectory\n") 299 for row in self.directoryTable: 300 tableFile.write(row.directory + "\t" + row.parent + "\t" + row.default + "\n") 301 302 def writeFeatureTable(self, name): 303 tableFile = file(name, "wt") 304 tableFile.write("Feature\tFeature_Parent\tTitle\tDescription\tDisplay\tLevel\tDirectory_\tAttributes\ns38\tS38\tL64\tL255\tI2\ti2\tS72\ti2\nFeature\tFeature\n") 305 for row in self.featureTable: 306 tableFile.write(row.feature + "\t" + row.parent + "\t" + row.title + "\t" + row.desc + "\t" + str(row.display) + "\t" + str(row.level) + "\t" + row.directory + "\t" + str(row.attributes) + "\n") 307 308 def writeMediaTable(self, name): 309 tableFile = file(name, "wt") 310 tableFile.write("DiskId\tLastSequence\tDiskPrompt\tCabinet\tVolumeLabel\tSource\ni2\ti2\tL64\tS255\tS32\tS72\nMedia\tDiskId\n") 311 tableFile.write("1" + "\t" + str(self.fileCount) + "\t" + "" + "\t" + "#archive.cab" + "\t" + "" + "\t" + "" + "\n") 312 313 def writeShortcutTable(self, name): 314 tableFile = file(name, "wt") 315 tableFile.write("Shortcut\tDirectory_\tName\tComponent_\tTarget\tArguments\tDescription\tHotkey\tIcon_\tIconIndex\tShowCmd\tWkDir\ns72\ts72\tl128\ts72\ts72\tS255\tL255\tI2\tS72\tI2\tI2\tS72\nShortcut\tShortcut\n") 316 for row in self.shortcutTable: 317 tableFile.write(row.component + "\t" + row.directory + "\t" + row.name + "\t" + row.component + "\t" + row.feature + "\t" + "" + "\t" + "" + "\t" + "" + "\t" + row.icon + "\t" + "" + "\t" + "" + "\t" + "" + "\n") 318 319 def writeRemoveFileTable(self, name): 320 tableFile = file(name, "wt") 321 tableFile.write("FileKey\tComponent_\tFileName\tDirProperty\tInstallMode\ns72\ts72\tL255\ts72\ti2\nRemoveFile\tFileKey\n") 322 count = 0 323 for row in self.shortcutTable: 324 tableFile.write("rf" + str(count) + "\t" + row.component + "\t" + "" + "\t" + row.directory + "\t" + "2" + "\n") 325 count = count + 1 326 327 def writeCustomActionTable(self, name): 328 tableFile = file(name, "wt") 329 tableFile.write("Action\tType\tSource\tTarget\ns72\ti2\tS72\tS255\nCustomAction\tAction\n") 330 tableFile.write("caSetTargetDir\t51\tTARGETDIR\t" + self.target) 331 332 def writeUpgradeTable(self, name): 333 tableFile = file(name, "wt") 334 tableFile.write("UpgradeCode\tVersionMin\tVersionMax\tLanguage\tAttributes\tRemove\tActionProperty\ns38\tS20\tS20\tS255\ti4\tS255\ts72\nUpgrade\tUpgradeCode\tVersionMin\tVersionMax\tLanguage\tAttributes\n") 335 tableFile.write(format_guid(self.code) + "\t\t" + self.version + "\t1033\t1\t\tRELATEDPRODUCTS") 336 337 def writeMSILicense(self, msiName, licenseName): 338 if(not os.path.exists(licenseName)): 339 raise Exception("file not found: " + licenseName) 340 print("license=\"" + licenseName + "\"") 341 licenseFile = file(licenseName, "rt") 342 text = licenseFile.read(1024) 343 rtfString = "" 344 while(text != ""): 345 rtfString += text 346 text = licenseFile.read(1024) 347 msiDB = msi.Database(msiName) 348 msiDB.setlicense(rtfString[:-1]) 349 msiDB.commit() 350 351 def writeMSIProperties(self, msiName): 352 msiDB = msi.Database(msiName) 353 print("ProductCode=" + format_guid(self.code)) 354 msiDB.setproperty("ProductCode", format_guid(self.code)) 355 print("UpgradeCode=" + format_guid(self.code)) 356 msiDB.setproperty("UpgradeCode", format_guid(self.code)) 357 print("ProductName=" + self.name) 358 msiDB.setproperty("ProductName", self.name) 359 print("ProductVersion=" + self.version) 360 msiDB.setproperty("ProductVersion", self.version) 361 msiDB.setproperty("RELATEDPRODUCTS", "") 362 msiDB.setproperty("SecureCustomProperties", "RELATEDPRODUCTS") 363 msiDB.commit() 364 365 def writeMSI(self, msiTemplate, msiName): 366 msiWorkName = "working.msi" 367 if(os.system("copy " + msiTemplate + " " + msiWorkName) != 0): 368 raise Exception("copy failed") 369 os.system("msiinfo " + msiWorkName + " /w 2 /v " + generate_guid() + " /a \"Radiant Community\" /j \"" + self.name + "\" /o \"This installation database contains the logic and data needed to install " + self.name + "\"") 370 371 self.writeMSIProperties(msiWorkName) 372 self.writeMSILicense(msiWorkName, self.license) 373 374 self.writeFileTable("File.idt") 375 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" File.idt") 376 os.system("del File.idt") 377 self.writeComponentTable("Component.idt") 378 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Component.idt") 379 os.system("del Component.idt") 380 self.writeFeatureComponentsTable("FeatureComponents.idt") 381 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" FeatureComponents.idt") 382 os.system("del FeatureComponents.idt") 383 self.writeDirectoryTable("Directory.idt") 384 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Directory.idt") 385 os.system("del Directory.idt") 386 self.writeFeatureTable("Feature.idt") 387 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Feature.idt") 388 os.system("del Feature.idt") 389 self.writeMediaTable("Media.idt") 390 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Media.idt") 391 os.system("del Media.idt") 392 self.writeShortcutTable("Shortcut.idt") 393 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Shortcut.idt") 394 os.system("del Shortcut.idt") 395 self.writeRemoveFileTable("RemoveFile.idt") 396 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" RemoveFile.idt") 397 os.system("del RemoveFile.idt") 398 self.writeCustomActionTable("CustomAction.idt") 399 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" CustomAction.idt") 400 os.system("del CustomAction.idt") 401 self.writeUpgradeTable("Upgrade.idt") 402 os.system("msidb -d " + msiWorkName + " -i -f \"" + cwd + "\" Upgrade.idt") 403 os.system("del Upgrade.idt") 404 405 cabText = file("archive_files.txt", "wt") 406 for cabDirective in self.cabList: 407 cabText.write(cabDirective) 408 cabText.close() 409 if(os.system("cabarc -m LZX:21 n archive.cab @archive_files.txt") != 0): 410 raise Exception("cabarc returned error") 411 os.system("del archive_files.txt") 412 os.system("msidb -d " + msiWorkName + " -a archive.cab") 413 os.system("del archive.cab") 414 415 print("running standard MSI validators ...") 416 if(os.system("msival2 " + msiWorkName + " darice.cub > darice.txt") != 0): 417 raise Exception("MSI VALIDATION ERROR: see darice.txt") 418 print("running Logo Program validators ...") 419 if(os.system("msival2 " + msiWorkName + " logo.cub > logo.txt") != 0): 420 raise Exception("MSI VALIDATION ERROR: see logo.txt") 421 print("running XP Logo Program validators ...") 422 if(os.system("msival2 " + msiWorkName + " XPlogo.cub > XPlogo.txt") != 0): 423 raise Exception("MSI VALIDATION ERROR: see XPlogo.txt") 424 425 msiNameQuoted = "\"" + msiName + "\"" 426 if(os.path.exists(os.path.join(".\\", msiName)) and os.system("del " + msiNameQuoted) != 0): 427 raise Exception("failed to delete old target") 428 if(os.system("rename " + msiWorkName + " " + msiNameQuoted) != 0): 429 raise Exception("failed to rename new target") 430 431