createmsi.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #!/usr/bin/env python3
  2. # Copyright 2017 The Meson development team
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import sys, os, subprocess, shutil, uuid
  16. from glob import glob
  17. import platform
  18. import xml.etree.ElementTree as ET
  19. sys.path.append(os.getcwd())
  20. from mesonbuild import coredata
  21. def gen_guid():
  22. return str(uuid.uuid4()).upper()
  23. class Node:
  24. def __init__(self, dirs, files):
  25. assert(isinstance(dirs, list))
  26. assert(isinstance(files, list))
  27. self.dirs = dirs
  28. self.files = files
  29. class PackageGenerator:
  30. def __init__(self):
  31. self.product_name = 'Meson Build System'
  32. self.manufacturer = 'The Meson Development Team'
  33. self.version = coredata.version.replace('dev', '')
  34. self.guid = '*'
  35. self.update_guid = '141527EE-E28A-4D14-97A4-92E6075D28B2'
  36. self.main_xml = 'meson.wxs'
  37. self.main_o = 'meson.wixobj'
  38. self.bytesize = 32 if '32' in platform.architecture()[0] else 64
  39. # rely on the environment variable since python architecture may not be the same as system architecture
  40. if 'PROGRAMFILES(X86)' in os.environ:
  41. self.bytesize = 64
  42. self.final_output = 'meson-%s-%d.msi' % (self.version, self.bytesize)
  43. self.staging_dirs = ['dist', 'dist2']
  44. if self.bytesize == 64:
  45. self.progfile_dir = 'ProgramFiles64Folder'
  46. redist_glob = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC141_CRT_x64.msm'
  47. else:
  48. self.progfile_dir = 'ProgramFilesFolder'
  49. redist_glob = 'C:\\Program Files\\Microsoft Visual Studio\\2017\\Community\\VC\\Redist\\MSVC\\*\\MergeModules\\Microsoft_VC141_CRT_x86.msm'
  50. trials = glob(redist_glob)
  51. if len(trials) != 1:
  52. sys.exit('There are more than one potential redist dirs.')
  53. self.redist_path = trials[0]
  54. self.component_num = 0
  55. self.feature_properties = {
  56. self.staging_dirs[0]: {
  57. 'Id': 'MainProgram',
  58. 'Title': 'Meson',
  59. 'Description': 'Meson executables',
  60. 'Level': '1',
  61. 'Absent': 'disallow',
  62. },
  63. self.staging_dirs[1]: {
  64. 'Id': 'NinjaProgram',
  65. 'Title': 'Ninja',
  66. 'Description': 'Ninja build tool',
  67. 'Level': '1',
  68. }
  69. }
  70. self.feature_components = {}
  71. for sd in self.staging_dirs:
  72. self.feature_components[sd] = []
  73. def build_dist(self):
  74. for sdir in self.staging_dirs:
  75. if os.path.exists(sdir):
  76. shutil.rmtree(sdir)
  77. main_stage, ninja_stage = self.staging_dirs
  78. modules = [os.path.splitext(os.path.split(x)[1])[0] for x in glob(os.path.join('mesonbuild/modules/*'))]
  79. modules = ['mesonbuild.modules.' + x for x in modules if not x.startswith('_')]
  80. modules += ['distutils.version']
  81. modulestr = ','.join(modules)
  82. python = shutil.which('python')
  83. cxfreeze = os.path.join(os.path.dirname(python), "Scripts", "cxfreeze")
  84. if not os.path.isfile(cxfreeze):
  85. print("ERROR: This script requires cx_freeze module")
  86. sys.exit(1)
  87. subprocess.check_call([python,
  88. cxfreeze,
  89. '--target-dir',
  90. main_stage,
  91. '--include-modules',
  92. modulestr,
  93. 'meson.py'])
  94. if not os.path.exists(os.path.join(main_stage, 'meson.exe')):
  95. sys.exit('Meson exe missing from staging dir.')
  96. os.mkdir(ninja_stage)
  97. shutil.copy(shutil.which('ninja'), ninja_stage)
  98. if not os.path.exists(os.path.join(ninja_stage, 'ninja.exe')):
  99. sys.exit('Ninja exe missing from staging dir.')
  100. def generate_files(self):
  101. self.root = ET.Element('Wix', {'xmlns': 'http://schemas.microsoft.com/wix/2006/wi'})
  102. product = ET.SubElement(self.root, 'Product', {
  103. 'Name': self.product_name,
  104. 'Manufacturer': 'The Meson Development Team',
  105. 'Id': self.guid,
  106. 'UpgradeCode': self.update_guid,
  107. 'Language': '1033',
  108. 'Codepage': '1252',
  109. 'Version': self.version,
  110. })
  111. package = ET.SubElement(product, 'Package', {
  112. 'Id': '*',
  113. 'Keywords': 'Installer',
  114. 'Description': 'Meson %s installer' % self.version,
  115. 'Comments': 'Meson is a high performance build system',
  116. 'Manufacturer': 'The Meson Development Team',
  117. 'InstallerVersion': '500',
  118. 'Languages': '1033',
  119. 'Compressed': 'yes',
  120. 'SummaryCodepage': '1252',
  121. })
  122. ET.SubElement(product, 'MajorUpgrade',
  123. {'DowngradeErrorMessage': 'A newer version of Meson is already installed.'})
  124. if self.bytesize == 64:
  125. package.set('Platform', 'x64')
  126. ET.SubElement(product, 'Media', {
  127. 'Id': '1',
  128. 'Cabinet': 'meson.cab',
  129. 'EmbedCab': 'yes',
  130. })
  131. targetdir = ET.SubElement(product, 'Directory', {
  132. 'Id': 'TARGETDIR',
  133. 'Name': 'SourceDir',
  134. })
  135. progfiledir = ET.SubElement(targetdir, 'Directory', {
  136. 'Id': self.progfile_dir,
  137. })
  138. installdir = ET.SubElement(progfiledir, 'Directory', {
  139. 'Id': 'INSTALLDIR',
  140. 'Name': 'Meson',
  141. })
  142. ET.SubElement(installdir, 'Merge', {
  143. 'Id': 'VCRedist',
  144. 'SourceFile': self.redist_path,
  145. 'DiskId': '1',
  146. 'Language': '0',
  147. })
  148. ET.SubElement(product, 'Property', {
  149. 'Id': 'WIXUI_INSTALLDIR',
  150. 'Value': 'INSTALLDIR',
  151. })
  152. ET.SubElement(product, 'UIRef', {
  153. 'Id': 'WixUI_FeatureTree',
  154. })
  155. for sd in self.staging_dirs:
  156. assert(os.path.isdir(sd))
  157. top_feature = ET.SubElement(product, 'Feature', {
  158. 'Id': 'Complete',
  159. 'Title': 'Meson ' + self.version,
  160. 'Description': 'The complete package',
  161. 'Display': 'expand',
  162. 'Level': '1',
  163. 'ConfigurableDirectory': 'INSTALLDIR',
  164. })
  165. for sd in self.staging_dirs:
  166. nodes = {}
  167. for root, dirs, files in os.walk(sd):
  168. cur_node = Node(dirs, files)
  169. nodes[root] = cur_node
  170. self.create_xml(nodes, sd, installdir, sd)
  171. self.build_features(nodes, top_feature, sd)
  172. vcredist_feature = ET.SubElement(top_feature, 'Feature', {
  173. 'Id': 'VCRedist',
  174. 'Title': 'Visual C++ runtime',
  175. 'AllowAdvertise': 'no',
  176. 'Display': 'hidden',
  177. 'Level': '1',
  178. })
  179. ET.SubElement(vcredist_feature, 'MergeRef', {'Id': 'VCRedist'})
  180. ET.ElementTree(self.root).write(self.main_xml, encoding='utf-8', xml_declaration=True)
  181. # ElementTree can not do prettyprinting so do it manually
  182. import xml.dom.minidom
  183. doc = xml.dom.minidom.parse(self.main_xml)
  184. with open(self.main_xml, 'w') as of:
  185. of.write(doc.toprettyxml())
  186. def build_features(self, nodes, top_feature, staging_dir):
  187. feature = ET.SubElement(top_feature, 'Feature', self.feature_properties[staging_dir])
  188. for component_id in self.feature_components[staging_dir]:
  189. ET.SubElement(feature, 'ComponentRef', {
  190. 'Id': component_id,
  191. })
  192. def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir):
  193. cur_node = nodes[current_dir]
  194. if cur_node.files:
  195. component_id = 'ApplicationFiles%d' % self.component_num
  196. comp_xml_node = ET.SubElement(parent_xml_node, 'Component', {
  197. 'Id': component_id,
  198. 'Guid': gen_guid(),
  199. })
  200. self.feature_components[staging_dir].append(component_id)
  201. if self.bytesize == 64:
  202. comp_xml_node.set('Win64', 'yes')
  203. if self.component_num == 0:
  204. ET.SubElement(comp_xml_node, 'Environment', {
  205. 'Id': 'Environment',
  206. 'Name': 'PATH',
  207. 'Part': 'last',
  208. 'System': 'yes',
  209. 'Action': 'set',
  210. 'Value': '[INSTALLDIR]',
  211. })
  212. self.component_num += 1
  213. for f in cur_node.files:
  214. file_id = os.path.join(current_dir, f).replace('\\', '_').replace('#', '_').replace('-', '_')
  215. ET.SubElement(comp_xml_node, 'File', {
  216. 'Id': file_id,
  217. 'Name': f,
  218. 'Source': os.path.join(current_dir, f),
  219. })
  220. for dirname in cur_node.dirs:
  221. dir_id = os.path.join(current_dir, dirname).replace('\\', '_').replace('/', '_')
  222. dir_node = ET.SubElement(parent_xml_node, 'Directory', {
  223. 'Id': dir_id,
  224. 'Name': dirname,
  225. })
  226. self.create_xml(nodes, os.path.join(current_dir, dirname), dir_node, staging_dir)
  227. def build_package(self):
  228. wixdir = 'c:\\Program Files\\Wix Toolset v3.11\\bin'
  229. if not os.path.isdir(wixdir):
  230. wixdir = 'c:\\Program Files (x86)\\Wix Toolset v3.11\\bin'
  231. if not os.path.isdir(wixdir):
  232. print("ERROR: This script requires WIX")
  233. sys.exit(1)
  234. subprocess.check_call([os.path.join(wixdir, 'candle'), self.main_xml])
  235. subprocess.check_call([os.path.join(wixdir, 'light'),
  236. '-ext', 'WixUIExtension',
  237. '-cultures:en-us',
  238. '-dWixUILicenseRtf=msi\\License.rtf',
  239. '-out', self.final_output,
  240. self.main_o])
  241. if __name__ == '__main__':
  242. if not os.path.exists('meson.py'):
  243. sys.exit(print('Run me in the top level source dir.'))
  244. subprocess.check_call(['pip', 'install', '--upgrade', 'cx_freeze'])
  245. p = PackageGenerator()
  246. p.build_dist()
  247. p.generate_files()
  248. p.build_package()