- UFO:Alien Invasion Plugin (mattn2)
[xonotic/netradiant.git] / setup / win32 / installer.py
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
20 import os.path
21 import xml.dom
22 import os
23 import stat
24 import string
25
26 from xml.dom.minidom import parse
27
28 import msi
29
30 cwd = os.getcwd()
31 print("cwd=" + cwd)
32
33
34 def format_guid(guid):
35   return "{" + guid.upper() + "}"
36
37 def 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   
45 def 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
57 class 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
68 class FeatureComponent:
69   def __init__(self, feature, component):
70     self.feature = feature
71     self.component = component
72     
73 class Directory:
74   def __init__(self, directory, parent, default):
75     self.directory = directory
76     self.parent = parent
77     self.default = default
78     
79 class 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     
86 class 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     
94 class 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
102 class ComponentFiles:
103   def __init__(self, name, files, directory):
104     self.name = name
105     self.files = files
106     self.directory = directory
107
108 class 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