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