##// END OF EJS Templates
wix: autogenerate wxs file for library files...
Gregory Szorc -
r42123:131d0b7c default
parent child Browse files
Show More
@@ -1,239 +1,301 b''
1 # wix.py - WiX installer functionality
1 # wix.py - WiX installer functionality
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import os
10 import os
11 import pathlib
11 import pathlib
12 import re
12 import re
13 import subprocess
13 import subprocess
14 import tempfile
15 import xml.dom.minidom
14
16
15 from .downloads import (
17 from .downloads import (
16 download_entry,
18 download_entry,
17 )
19 )
18 from .py2exe import (
20 from .py2exe import (
19 build_py2exe,
21 build_py2exe,
20 )
22 )
21 from .util import (
23 from .util import (
22 extract_zip_to_directory,
24 extract_zip_to_directory,
23 sign_with_signtool,
25 sign_with_signtool,
24 )
26 )
25
27
26
28
27 SUPPORT_WXS = [
29 SUPPORT_WXS = [
28 ('contrib.wxs', r'contrib'),
30 ('contrib.wxs', r'contrib'),
29 ('dist.wxs', r'dist'),
31 ('dist.wxs', r'dist'),
30 ('doc.wxs', r'doc'),
32 ('doc.wxs', r'doc'),
31 ('help.wxs', r'mercurial\help'),
33 ('help.wxs', r'mercurial\help'),
32 ('i18n.wxs', r'i18n'),
34 ('i18n.wxs', r'i18n'),
33 ('locale.wxs', r'mercurial\locale'),
35 ('locale.wxs', r'mercurial\locale'),
34 ('templates.wxs', r'mercurial\templates'),
36 ('templates.wxs', r'mercurial\templates'),
35 ]
37 ]
36
38
37
39
38 EXTRA_PACKAGES = {
40 EXTRA_PACKAGES = {
39 'distutils',
41 'distutils',
40 'pygments',
42 'pygments',
41 }
43 }
42
44
43
45
44 def find_version(source_dir: pathlib.Path):
46 def find_version(source_dir: pathlib.Path):
45 version_py = source_dir / 'mercurial' / '__version__.py'
47 version_py = source_dir / 'mercurial' / '__version__.py'
46
48
47 with version_py.open('r', encoding='utf-8') as fh:
49 with version_py.open('r', encoding='utf-8') as fh:
48 source = fh.read().strip()
50 source = fh.read().strip()
49
51
50 m = re.search('version = b"(.*)"', source)
52 m = re.search('version = b"(.*)"', source)
51 return m.group(1)
53 return m.group(1)
52
54
53
55
54 def normalize_version(version):
56 def normalize_version(version):
55 """Normalize Mercurial version string so WiX accepts it.
57 """Normalize Mercurial version string so WiX accepts it.
56
58
57 Version strings have to be numeric X.Y.Z.
59 Version strings have to be numeric X.Y.Z.
58 """
60 """
59
61
60 if '+' in version:
62 if '+' in version:
61 version, extra = version.split('+', 1)
63 version, extra = version.split('+', 1)
62 else:
64 else:
63 extra = None
65 extra = None
64
66
65 # 4.9rc0
67 # 4.9rc0
66 if version[:-1].endswith('rc'):
68 if version[:-1].endswith('rc'):
67 version = version[:-3]
69 version = version[:-3]
68
70
69 versions = [int(v) for v in version.split('.')]
71 versions = [int(v) for v in version.split('.')]
70 while len(versions) < 3:
72 while len(versions) < 3:
71 versions.append(0)
73 versions.append(0)
72
74
73 major, minor, build = versions[:3]
75 major, minor, build = versions[:3]
74
76
75 if extra:
77 if extra:
76 # <commit count>-<hash>+<date>
78 # <commit count>-<hash>+<date>
77 build = int(extra.split('-')[0])
79 build = int(extra.split('-')[0])
78
80
79 return '.'.join('%d' % x for x in (major, minor, build))
81 return '.'.join('%d' % x for x in (major, minor, build))
80
82
81
83
82 def ensure_vc90_merge_modules(build_dir):
84 def ensure_vc90_merge_modules(build_dir):
83 x86 = (
85 x86 = (
84 download_entry('vc9-crt-x86-msm', build_dir,
86 download_entry('vc9-crt-x86-msm', build_dir,
85 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
87 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
86 download_entry('vc9-crt-x86-msm-policy', build_dir,
88 download_entry('vc9-crt-x86-msm-policy', build_dir,
87 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
89 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
88 )
90 )
89
91
90 x64 = (
92 x64 = (
91 download_entry('vc9-crt-x64-msm', build_dir,
93 download_entry('vc9-crt-x64-msm', build_dir,
92 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
94 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
93 download_entry('vc9-crt-x64-msm-policy', build_dir,
95 download_entry('vc9-crt-x64-msm-policy', build_dir,
94 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
96 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
95 )
97 )
96 return {
98 return {
97 'x86': x86,
99 'x86': x86,
98 'x64': x64,
100 'x64': x64,
99 }
101 }
100
102
101
103
102 def run_candle(wix, cwd, wxs, source_dir, defines=None):
104 def run_candle(wix, cwd, wxs, source_dir, defines=None):
103 args = [
105 args = [
104 str(wix / 'candle.exe'),
106 str(wix / 'candle.exe'),
105 '-nologo',
107 '-nologo',
106 str(wxs),
108 str(wxs),
107 '-dSourceDir=%s' % source_dir,
109 '-dSourceDir=%s' % source_dir,
108 ]
110 ]
109
111
110 if defines:
112 if defines:
111 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
113 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
112
114
113 subprocess.run(args, cwd=str(cwd), check=True)
115 subprocess.run(args, cwd=str(cwd), check=True)
114
116
115
117
116 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
118 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
117 cert_password=None, timestamp_url=None):
119 cert_password=None, timestamp_url=None):
118 """Create a callable that will use signtool to sign hg.exe."""
120 """Create a callable that will use signtool to sign hg.exe."""
119
121
120 def post_build_sign(source_dir, build_dir, dist_dir, version):
122 def post_build_sign(source_dir, build_dir, dist_dir, version):
121 description = '%s %s' % (name, version)
123 description = '%s %s' % (name, version)
122
124
123 sign_with_signtool(dist_dir / 'hg.exe', description,
125 sign_with_signtool(dist_dir / 'hg.exe', description,
124 subject_name=subject_name, cert_path=cert_path,
126 subject_name=subject_name, cert_path=cert_path,
125 cert_password=cert_password,
127 cert_password=cert_password,
126 timestamp_url=timestamp_url)
128 timestamp_url=timestamp_url)
127
129
128 return post_build_sign
130 return post_build_sign
129
131
130
132
133 LIBRARIES_XML = '''
134 <?xml version="1.0" encoding="utf-8"?>
135 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
136
137 <?include {wix_dir}/guids.wxi ?>
138 <?include {wix_dir}/defines.wxi ?>
139
140 <Fragment>
141 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
142 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
143 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
144 </Component>
145 </Directory>
146 </DirectoryRef>
147 </Fragment>
148 </Wix>
149 '''.lstrip()
150
151
152 def make_libraries_xml(wix_dir: pathlib.Path, dist_dir: pathlib.Path):
153 """Make XML data for library components WXS."""
154 # We can't use ElementTree because it doesn't handle the
155 # <?include ?> directives.
156 doc = xml.dom.minidom.parseString(
157 LIBRARIES_XML.format(wix_dir=str(wix_dir)))
158
159 component = doc.getElementsByTagName('Component')[0]
160
161 f = doc.createElement('File')
162 f.setAttribute('Name', 'library.zip')
163 f.setAttribute('KeyPath', 'yes')
164 component.appendChild(f)
165
166 lib_dir = dist_dir / 'lib'
167
168 for p in sorted(lib_dir.iterdir()):
169 if not p.name.endswith(('.dll', '.pyd')):
170 continue
171
172 f = doc.createElement('File')
173 f.setAttribute('Name', p.name)
174 component.appendChild(f)
175
176 return doc.toprettyxml()
177
178
131 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
179 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
132 msi_name='mercurial', version=None, post_build_fn=None):
180 msi_name='mercurial', version=None, post_build_fn=None):
133 """Build a WiX MSI installer.
181 """Build a WiX MSI installer.
134
182
135 ``source_dir`` is the path to the Mercurial source tree to use.
183 ``source_dir`` is the path to the Mercurial source tree to use.
136 ``arch`` is the target architecture. either ``x86`` or ``x64``.
184 ``arch`` is the target architecture. either ``x86`` or ``x64``.
137 ``python_exe`` is the path to the Python executable to use/bundle.
185 ``python_exe`` is the path to the Python executable to use/bundle.
138 ``version`` is the Mercurial version string. If not defined,
186 ``version`` is the Mercurial version string. If not defined,
139 ``mercurial/__version__.py`` will be consulted.
187 ``mercurial/__version__.py`` will be consulted.
140 ``post_build_fn`` is a callable that will be called after building
188 ``post_build_fn`` is a callable that will be called after building
141 Mercurial but before invoking WiX. It can be used to e.g. facilitate
189 Mercurial but before invoking WiX. It can be used to e.g. facilitate
142 signing. It is passed the paths to the Mercurial source, build, and
190 signing. It is passed the paths to the Mercurial source, build, and
143 dist directories and the resolved Mercurial version.
191 dist directories and the resolved Mercurial version.
144 """
192 """
145 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
193 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
146
194
147 hg_build_dir = source_dir / 'build'
195 hg_build_dir = source_dir / 'build'
148 dist_dir = source_dir / 'dist'
196 dist_dir = source_dir / 'dist'
149 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
197 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
150
198
151 requirements_txt = wix_dir / 'requirements.txt'
199 requirements_txt = wix_dir / 'requirements.txt'
152
200
153 build_py2exe(source_dir, hg_build_dir,
201 build_py2exe(source_dir, hg_build_dir,
154 python_exe, 'wix', requirements_txt,
202 python_exe, 'wix', requirements_txt,
155 extra_packages=EXTRA_PACKAGES)
203 extra_packages=EXTRA_PACKAGES)
156
204
157 version = version or normalize_version(find_version(source_dir))
205 version = version or normalize_version(find_version(source_dir))
158 print('using version string: %s' % version)
206 print('using version string: %s' % version)
159
207
160 if post_build_fn:
208 if post_build_fn:
161 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
209 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
162
210
163 build_dir = hg_build_dir / ('wix-%s' % arch)
211 build_dir = hg_build_dir / ('wix-%s' % arch)
164
212
165 build_dir.mkdir(exist_ok=True)
213 build_dir.mkdir(exist_ok=True)
166
214
167 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
215 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
168 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
216 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
169
217
170 if not wix_path.exists():
218 if not wix_path.exists():
171 extract_zip_to_directory(wix_pkg, wix_path)
219 extract_zip_to_directory(wix_pkg, wix_path)
172
220
173 ensure_vc90_merge_modules(hg_build_dir)
221 ensure_vc90_merge_modules(hg_build_dir)
174
222
175 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
223 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
176
224
177 defines = {'Platform': arch}
225 defines = {'Platform': arch}
178
226
179 for wxs, rel_path in SUPPORT_WXS:
227 for wxs, rel_path in SUPPORT_WXS:
180 wxs = wix_dir / wxs
228 wxs = wix_dir / wxs
181 wxs_source_dir = source_dir / rel_path
229 wxs_source_dir = source_dir / rel_path
182 run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
230 run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
183
231
232 # candle.exe doesn't like when we have an open handle on the file.
233 # So use TemporaryDirectory() instead of NamedTemporaryFile().
234 with tempfile.TemporaryDirectory() as td:
235 td = pathlib.Path(td)
236
237 tf = td / 'library.wxs'
238 with tf.open('w') as fh:
239 fh.write(make_libraries_xml(wix_dir, dist_dir))
240
241 run_candle(wix_path, build_dir, tf, dist_dir, defines=defines)
242
184 source = wix_dir / 'mercurial.wxs'
243 source = wix_dir / 'mercurial.wxs'
185 defines['Version'] = version
244 defines['Version'] = version
186 defines['Comments'] = 'Installs Mercurial version %s' % version
245 defines['Comments'] = 'Installs Mercurial version %s' % version
187 defines['VCRedistSrcDir'] = str(hg_build_dir)
246 defines['VCRedistSrcDir'] = str(hg_build_dir)
188
247
189 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
248 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
190
249
191 msi_path = source_dir / 'dist' / (
250 msi_path = source_dir / 'dist' / (
192 '%s-%s-%s.msi' % (msi_name, version, arch))
251 '%s-%s-%s.msi' % (msi_name, version, arch))
193
252
194 args = [
253 args = [
195 str(wix_path / 'light.exe'),
254 str(wix_path / 'light.exe'),
196 '-nologo',
255 '-nologo',
197 '-ext', 'WixUIExtension',
256 '-ext', 'WixUIExtension',
198 '-sw1076',
257 '-sw1076',
199 '-spdb',
258 '-spdb',
200 '-o', str(msi_path),
259 '-o', str(msi_path),
201 ]
260 ]
202
261
203 for source, rel_path in SUPPORT_WXS:
262 for source, rel_path in SUPPORT_WXS:
204 assert source.endswith('.wxs')
263 assert source.endswith('.wxs')
205 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
264 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
206
265
207 args.append(str(build_dir / 'mercurial.wixobj'))
266 args.extend([
267 str(build_dir / 'library.wixobj'),
268 str(build_dir / 'mercurial.wixobj'),
269 ])
208
270
209 subprocess.run(args, cwd=str(source_dir), check=True)
271 subprocess.run(args, cwd=str(source_dir), check=True)
210
272
211 print('%s created' % msi_path)
273 print('%s created' % msi_path)
212
274
213 return {
275 return {
214 'msi_path': msi_path,
276 'msi_path': msi_path,
215 }
277 }
216
278
217
279
218 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
280 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
219 name: str, version=None, subject_name=None,
281 name: str, version=None, subject_name=None,
220 cert_path=None, cert_password=None,
282 cert_path=None, cert_password=None,
221 timestamp_url=None):
283 timestamp_url=None):
222 """Build an installer with signed executables."""
284 """Build an installer with signed executables."""
223
285
224 post_build_fn = make_post_build_signing_fn(
286 post_build_fn = make_post_build_signing_fn(
225 name,
287 name,
226 subject_name=subject_name,
288 subject_name=subject_name,
227 cert_path=cert_path,
289 cert_path=cert_path,
228 cert_password=cert_password,
290 cert_password=cert_password,
229 timestamp_url=timestamp_url)
291 timestamp_url=timestamp_url)
230
292
231 info = build_installer(source_dir, python_exe=python_exe,
293 info = build_installer(source_dir, python_exe=python_exe,
232 msi_name=name.lower(), version=version,
294 msi_name=name.lower(), version=version,
233 post_build_fn=post_build_fn)
295 post_build_fn=post_build_fn)
234
296
235 description = '%s %s' % (name, version)
297 description = '%s %s' % (name, version)
236
298
237 sign_with_signtool(info['msi_path'], description,
299 sign_with_signtool(info['msi_path'], description,
238 subject_name=subject_name, cert_path=cert_path,
300 subject_name=subject_name, cert_path=cert_path,
239 cert_password=cert_password, timestamp_url=timestamp_url)
301 cert_password=cert_password, timestamp_url=timestamp_url)
@@ -1,44 +1,15 b''
1 <?xml version="1.0" encoding="utf-8"?>
1 <?xml version="1.0" encoding="utf-8"?>
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
2 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3
3
4 <?include guids.wxi ?>
4 <?include guids.wxi ?>
5 <?include defines.wxi ?>
5 <?include defines.wxi ?>
6
6
7 <Fragment>
7 <Fragment>
8 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
8 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
9 <Component Id="distOutput" Guid="$(var.dist.guid)" Win64='$(var.IsX64)'>
9 <Component Id="distOutput" Guid="$(var.dist.guid)" Win64='$(var.IsX64)'>
10 <File Name="python27.dll" KeyPath="yes" />
10 <File Name="python27.dll" KeyPath="yes" />
11 </Component>
11 </Component>
12 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
13 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
14 <File Name="library.zip" KeyPath="yes" />
15 <File Name="mercurial.cext.base85.pyd" />
16 <File Name="mercurial.cext.bdiff.pyd" />
17 <File Name="mercurial.cext.mpatch.pyd" />
18 <File Name="mercurial.cext.osutil.pyd" />
19 <File Name="mercurial.cext.parsers.pyd" />
20 <File Name="mercurial.thirdparty.zope.interface._zope_interface_coptimizations.pyd" />
21 <File Name="mercurial.zstd.pyd" />
22 <File Name="hgext.fsmonitor.pywatchman.bser.pyd" />
23 <File Name="pyexpat.pyd" />
24 <File Name="bz2.pyd" />
25 <File Name="select.pyd" />
26 <File Name="sqlite3.dll" />
27 <File Name="tcl85.dll" />
28 <File Name="tk85.dll" />
29 <File Name="unicodedata.pyd" />
30 <File Name="_ctypes.pyd" />
31 <File Name="_elementtree.pyd" />
32 <File Name="_testcapi.pyd" />
33 <File Name="_hashlib.pyd" />
34 <File Name="_multiprocessing.pyd" />
35 <File Name="_socket.pyd" />
36 <File Name="_sqlite3.pyd" />
37 <File Name="_ssl.pyd" />
38 <File Name="_tkinter.pyd" />
39 </Component>
40 </Directory>
41 </DirectoryRef>
12 </DirectoryRef>
42 </Fragment>
13 </Fragment>
43
14
44 </Wix>
15 </Wix>
General Comments 0
You need to be logged in to leave comments. Login now