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