##// END OF EJS Templates
wix: add a hook for a prebuild script to inject extra libraries...
Augie Fackler -
r42214:715d3220 default
parent child Browse files
Show More
@@ -1,135 +1,146 b''
1 1 # py2exe.py - Functionality for performing py2exe builds.
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import os
11 11 import pathlib
12 12 import subprocess
13 13
14 14 from .downloads import (
15 15 download_entry,
16 16 )
17 17 from .util import (
18 18 extract_tar_to_directory,
19 19 extract_zip_to_directory,
20 20 python_exe_info,
21 21 )
22 22
23 23
24 24 def build_py2exe(source_dir: pathlib.Path, build_dir: pathlib.Path,
25 25 python_exe: pathlib.Path, build_name: str,
26 26 venv_requirements_txt: pathlib.Path,
27 27 extra_packages=None, extra_excludes=None,
28 extra_dll_excludes=None):
28 extra_dll_excludes=None,
29 extra_packages_script=None):
29 30 """Build Mercurial with py2exe.
30 31
31 32 Build files will be placed in ``build_dir``.
32 33
33 34 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
34 35 for finding the Python 2.7 toolchain. So, we require the environment
35 36 to already be configured with an active toolchain.
36 37 """
37 38 if 'VCINSTALLDIR' not in os.environ:
38 39 raise Exception('not running from a Visual C++ build environment; '
39 40 'execute the "Visual C++ <version> Command Prompt" '
40 41 'application shortcut or a vcsvarsall.bat file')
41 42
42 43 # Identity x86/x64 and validate the environment matches the Python
43 44 # architecture.
44 45 vc_x64 = r'\x64' in os.environ['LIB']
45 46
46 47 py_info = python_exe_info(python_exe)
47 48
48 49 if vc_x64:
49 50 if py_info['arch'] != '64bit':
50 51 raise Exception('architecture mismatch: Visual C++ environment '
51 52 'is configured for 64-bit but Python is 32-bit')
52 53 else:
53 54 if py_info['arch'] != '32bit':
54 55 raise Exception('architecture mismatch: Visual C++ environment '
55 56 'is configured for 32-bit but Python is 64-bit')
56 57
57 58 if py_info['py3']:
58 59 raise Exception('Only Python 2 is currently supported')
59 60
60 61 build_dir.mkdir(exist_ok=True)
61 62
62 63 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
63 64 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
64 65 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
65 66 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
66 67
67 68 venv_path = build_dir / ('venv-%s-%s' % (build_name,
68 69 'x64' if vc_x64 else 'x86'))
69 70
70 71 gettext_root = build_dir / (
71 72 'gettext-win-%s' % gettext_entry['version'])
72 73
73 74 if not gettext_root.exists():
74 75 extract_zip_to_directory(gettext_pkg, gettext_root)
75 76 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
76 77
77 78 # This assumes Python 2. We don't need virtualenv on Python 3.
78 79 virtualenv_src_path = build_dir / (
79 80 'virtualenv-%s' % virtualenv_entry['version'])
80 81 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
81 82
82 83 if not virtualenv_src_path.exists():
83 84 extract_tar_to_directory(virtualenv_pkg, build_dir)
84 85
85 86 py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version'])
86 87
87 88 if not py2exe_source_path.exists():
88 89 extract_zip_to_directory(py2exe_pkg, build_dir)
89 90
90 91 if not venv_path.exists():
91 92 print('creating virtualenv with dependencies')
92 93 subprocess.run(
93 94 [str(python_exe), str(virtualenv_py), str(venv_path)],
94 95 check=True)
95 96
96 97 venv_python = venv_path / 'Scripts' / 'python.exe'
97 98 venv_pip = venv_path / 'Scripts' / 'pip.exe'
98 99
99 100 subprocess.run([str(venv_pip), 'install', '-r', str(venv_requirements_txt)],
100 101 check=True)
101 102
102 103 # Force distutils to use VC++ settings from environment, which was
103 104 # validated above.
104 105 env = dict(os.environ)
105 106 env['DISTUTILS_USE_SDK'] = '1'
106 107 env['MSSdk'] = '1'
107 108
109 if extra_packages_script:
110 more_packages = set(subprocess.check_output(
111 extra_packages_script,
112 cwd=build_dir).split(b'\0')[-1].strip().decode('utf-8').splitlines())
113 if more_packages:
114 if not extra_packages:
115 extra_packages = more_packages
116 else:
117 extra_packages |= more_packages
118
108 119 if extra_packages:
109 120 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
110 121 if extra_excludes:
111 122 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
112 123 if extra_dll_excludes:
113 124 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
114 125 sorted(extra_dll_excludes))
115 126
116 127 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
117 128 if not py2exe_py_path.exists():
118 129 print('building py2exe')
119 130 subprocess.run([str(venv_python), 'setup.py', 'install'],
120 131 cwd=py2exe_source_path,
121 132 env=env,
122 133 check=True)
123 134
124 135 # Register location of msgfmt and other binaries.
125 136 env['PATH'] = '%s%s%s' % (
126 137 env['PATH'], os.pathsep, str(gettext_root / 'bin'))
127 138
128 139 print('building Mercurial')
129 140 subprocess.run(
130 141 [str(venv_python), 'setup.py',
131 142 'py2exe',
132 143 'build_doc', '--html'],
133 144 cwd=str(source_dir),
134 145 env=env,
135 146 check=True)
@@ -1,301 +1,308 b''
1 1 # wix.py - WiX installer functionality
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import os
11 11 import pathlib
12 12 import re
13 13 import subprocess
14 14 import tempfile
15 15 import xml.dom.minidom
16 16
17 17 from .downloads import (
18 18 download_entry,
19 19 )
20 20 from .py2exe import (
21 21 build_py2exe,
22 22 )
23 23 from .util import (
24 24 extract_zip_to_directory,
25 25 sign_with_signtool,
26 26 )
27 27
28 28
29 29 SUPPORT_WXS = [
30 30 ('contrib.wxs', r'contrib'),
31 31 ('dist.wxs', r'dist'),
32 32 ('doc.wxs', r'doc'),
33 33 ('help.wxs', r'mercurial\help'),
34 34 ('i18n.wxs', r'i18n'),
35 35 ('locale.wxs', r'mercurial\locale'),
36 36 ('templates.wxs', r'mercurial\templates'),
37 37 ]
38 38
39 39
40 40 EXTRA_PACKAGES = {
41 41 'distutils',
42 42 'pygments',
43 43 }
44 44
45 45
46 46 def find_version(source_dir: pathlib.Path):
47 47 version_py = source_dir / 'mercurial' / '__version__.py'
48 48
49 49 with version_py.open('r', encoding='utf-8') as fh:
50 50 source = fh.read().strip()
51 51
52 52 m = re.search('version = b"(.*)"', source)
53 53 return m.group(1)
54 54
55 55
56 56 def normalize_version(version):
57 57 """Normalize Mercurial version string so WiX accepts it.
58 58
59 59 Version strings have to be numeric X.Y.Z.
60 60 """
61 61
62 62 if '+' in version:
63 63 version, extra = version.split('+', 1)
64 64 else:
65 65 extra = None
66 66
67 67 # 4.9rc0
68 68 if version[:-1].endswith('rc'):
69 69 version = version[:-3]
70 70
71 71 versions = [int(v) for v in version.split('.')]
72 72 while len(versions) < 3:
73 73 versions.append(0)
74 74
75 75 major, minor, build = versions[:3]
76 76
77 77 if extra:
78 78 # <commit count>-<hash>+<date>
79 79 build = int(extra.split('-')[0])
80 80
81 81 return '.'.join('%d' % x for x in (major, minor, build))
82 82
83 83
84 84 def ensure_vc90_merge_modules(build_dir):
85 85 x86 = (
86 86 download_entry('vc9-crt-x86-msm', build_dir,
87 87 local_name='microsoft.vcxx.crt.x86_msm.msm')[0],
88 88 download_entry('vc9-crt-x86-msm-policy', build_dir,
89 89 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm')[0]
90 90 )
91 91
92 92 x64 = (
93 93 download_entry('vc9-crt-x64-msm', build_dir,
94 94 local_name='microsoft.vcxx.crt.x64_msm.msm')[0],
95 95 download_entry('vc9-crt-x64-msm-policy', build_dir,
96 96 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm')[0]
97 97 )
98 98 return {
99 99 'x86': x86,
100 100 'x64': x64,
101 101 }
102 102
103 103
104 104 def run_candle(wix, cwd, wxs, source_dir, defines=None):
105 105 args = [
106 106 str(wix / 'candle.exe'),
107 107 '-nologo',
108 108 str(wxs),
109 109 '-dSourceDir=%s' % source_dir,
110 110 ]
111 111
112 112 if defines:
113 113 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
114 114
115 115 subprocess.run(args, cwd=str(cwd), check=True)
116 116
117 117
118 118 def make_post_build_signing_fn(name, subject_name=None, cert_path=None,
119 119 cert_password=None, timestamp_url=None):
120 120 """Create a callable that will use signtool to sign hg.exe."""
121 121
122 122 def post_build_sign(source_dir, build_dir, dist_dir, version):
123 123 description = '%s %s' % (name, version)
124 124
125 125 sign_with_signtool(dist_dir / 'hg.exe', description,
126 126 subject_name=subject_name, cert_path=cert_path,
127 127 cert_password=cert_password,
128 128 timestamp_url=timestamp_url)
129 129
130 130 return post_build_sign
131 131
132 132
133 133 LIBRARIES_XML = '''
134 134 <?xml version="1.0" encoding="utf-8"?>
135 135 <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
136 136
137 137 <?include {wix_dir}/guids.wxi ?>
138 138 <?include {wix_dir}/defines.wxi ?>
139 139
140 140 <Fragment>
141 141 <DirectoryRef Id="INSTALLDIR" FileSource="$(var.SourceDir)">
142 142 <Directory Id="libdir" Name="lib" FileSource="$(var.SourceDir)/lib">
143 143 <Component Id="libOutput" Guid="$(var.lib.guid)" Win64='$(var.IsX64)'>
144 144 </Component>
145 145 </Directory>
146 146 </DirectoryRef>
147 147 </Fragment>
148 148 </Wix>
149 149 '''.lstrip()
150 150
151 151
152 152 def make_libraries_xml(wix_dir: pathlib.Path, dist_dir: pathlib.Path):
153 153 """Make XML data for library components WXS."""
154 154 # We can't use ElementTree because it doesn't handle the
155 155 # <?include ?> directives.
156 156 doc = xml.dom.minidom.parseString(
157 157 LIBRARIES_XML.format(wix_dir=str(wix_dir)))
158 158
159 159 component = doc.getElementsByTagName('Component')[0]
160 160
161 161 f = doc.createElement('File')
162 162 f.setAttribute('Name', 'library.zip')
163 163 f.setAttribute('KeyPath', 'yes')
164 164 component.appendChild(f)
165 165
166 166 lib_dir = dist_dir / 'lib'
167 167
168 168 for p in sorted(lib_dir.iterdir()):
169 169 if not p.name.endswith(('.dll', '.pyd')):
170 170 continue
171 171
172 172 f = doc.createElement('File')
173 173 f.setAttribute('Name', p.name)
174 174 component.appendChild(f)
175 175
176 176 return doc.toprettyxml()
177 177
178 178
179 179 def build_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
180 msi_name='mercurial', version=None, post_build_fn=None):
180 msi_name='mercurial', version=None, post_build_fn=None,
181 extra_packages_script=None):
181 182 """Build a WiX MSI installer.
182 183
183 184 ``source_dir`` is the path to the Mercurial source tree to use.
184 185 ``arch`` is the target architecture. either ``x86`` or ``x64``.
185 186 ``python_exe`` is the path to the Python executable to use/bundle.
186 187 ``version`` is the Mercurial version string. If not defined,
187 188 ``mercurial/__version__.py`` will be consulted.
188 189 ``post_build_fn`` is a callable that will be called after building
189 190 Mercurial but before invoking WiX. It can be used to e.g. facilitate
190 191 signing. It is passed the paths to the Mercurial source, build, and
191 192 dist directories and the resolved Mercurial version.
193 ``extra_packages_script`` is a command to be run to inject extra packages
194 into the py2exe binary. It should stage packages into the virtualenv and
195 print a null byte followed by a newline-separated list of packages that
196 should be included in the exe.
192 197 """
193 198 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
194 199
195 200 hg_build_dir = source_dir / 'build'
196 201 dist_dir = source_dir / 'dist'
197 202 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
198 203
199 204 requirements_txt = wix_dir / 'requirements.txt'
200 205
201 206 build_py2exe(source_dir, hg_build_dir,
202 207 python_exe, 'wix', requirements_txt,
203 extra_packages=EXTRA_PACKAGES)
208 extra_packages=EXTRA_PACKAGES,
209 extra_packages_script=extra_packages_script)
204 210
205 211 version = version or normalize_version(find_version(source_dir))
206 212 print('using version string: %s' % version)
207 213
208 214 if post_build_fn:
209 215 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
210 216
211 217 build_dir = hg_build_dir / ('wix-%s' % arch)
212 218
213 219 build_dir.mkdir(exist_ok=True)
214 220
215 221 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
216 222 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
217 223
218 224 if not wix_path.exists():
219 225 extract_zip_to_directory(wix_pkg, wix_path)
220 226
221 227 ensure_vc90_merge_modules(hg_build_dir)
222 228
223 229 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
224 230
225 231 defines = {'Platform': arch}
226 232
227 233 for wxs, rel_path in SUPPORT_WXS:
228 234 wxs = wix_dir / wxs
229 235 wxs_source_dir = source_dir / rel_path
230 236 run_candle(wix_path, build_dir, wxs, wxs_source_dir, defines=defines)
231 237
232 238 # candle.exe doesn't like when we have an open handle on the file.
233 239 # So use TemporaryDirectory() instead of NamedTemporaryFile().
234 240 with tempfile.TemporaryDirectory() as td:
235 241 td = pathlib.Path(td)
236 242
237 243 tf = td / 'library.wxs'
238 244 with tf.open('w') as fh:
239 245 fh.write(make_libraries_xml(wix_dir, dist_dir))
240 246
241 247 run_candle(wix_path, build_dir, tf, dist_dir, defines=defines)
242 248
243 249 source = wix_dir / 'mercurial.wxs'
244 250 defines['Version'] = version
245 251 defines['Comments'] = 'Installs Mercurial version %s' % version
246 252 defines['VCRedistSrcDir'] = str(hg_build_dir)
247 253
248 254 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
249 255
250 256 msi_path = source_dir / 'dist' / (
251 257 '%s-%s-%s.msi' % (msi_name, version, arch))
252 258
253 259 args = [
254 260 str(wix_path / 'light.exe'),
255 261 '-nologo',
256 262 '-ext', 'WixUIExtension',
257 263 '-sw1076',
258 264 '-spdb',
259 265 '-o', str(msi_path),
260 266 ]
261 267
262 268 for source, rel_path in SUPPORT_WXS:
263 269 assert source.endswith('.wxs')
264 270 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
265 271
266 272 args.extend([
267 273 str(build_dir / 'library.wixobj'),
268 274 str(build_dir / 'mercurial.wixobj'),
269 275 ])
270 276
271 277 subprocess.run(args, cwd=str(source_dir), check=True)
272 278
273 279 print('%s created' % msi_path)
274 280
275 281 return {
276 282 'msi_path': msi_path,
277 283 }
278 284
279 285
280 286 def build_signed_installer(source_dir: pathlib.Path, python_exe: pathlib.Path,
281 287 name: str, version=None, subject_name=None,
282 288 cert_path=None, cert_password=None,
283 timestamp_url=None):
289 timestamp_url=None, extra_packages_script=None):
284 290 """Build an installer with signed executables."""
285 291
286 292 post_build_fn = make_post_build_signing_fn(
287 293 name,
288 294 subject_name=subject_name,
289 295 cert_path=cert_path,
290 296 cert_password=cert_password,
291 297 timestamp_url=timestamp_url)
292 298
293 299 info = build_installer(source_dir, python_exe=python_exe,
294 300 msi_name=name.lower(), version=version,
295 post_build_fn=post_build_fn)
301 post_build_fn=post_build_fn,
302 extra_packages_script=extra_packages_script)
296 303
297 304 description = '%s %s' % (name, version)
298 305
299 306 sign_with_signtool(info['msi_path'], description,
300 307 subject_name=subject_name, cert_path=cert_path,
301 308 cert_password=cert_password, timestamp_url=timestamp_url)
@@ -1,65 +1,71 b''
1 1 #!/usr/bin/env python3
2 2 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 # no-check-code because Python 3 native.
8 8
9 9 """Code to build Mercurial WiX installer."""
10 10
11 11 import argparse
12 12 import os
13 13 import pathlib
14 14 import sys
15 15
16 16
17 17 if __name__ == '__main__':
18 18 parser = argparse.ArgumentParser()
19 19
20 20 parser.add_argument('--name',
21 21 help='Application name',
22 22 default='Mercurial')
23 23 parser.add_argument('--python',
24 24 help='Path to Python executable to use',
25 25 required=True)
26 26 parser.add_argument('--sign-sn',
27 27 help='Subject name (or fragment thereof) of certificate '
28 28 'to use for signing')
29 29 parser.add_argument('--sign-cert',
30 30 help='Path to certificate to use for signing')
31 31 parser.add_argument('--sign-password',
32 32 help='Password for signing certificate')
33 33 parser.add_argument('--sign-timestamp-url',
34 34 help='URL of timestamp server to use for signing')
35 35 parser.add_argument('--version',
36 36 help='Version string to use')
37 parser.add_argument('--extra-packages-script',
38 help=('Script to execute to include extra packages in '
39 'py2exe binary.'))
37 40
38 41 args = parser.parse_args()
39 42
40 43 here = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
41 44 source_dir = here.parent.parent.parent
42 45
43 46 sys.path.insert(0, str(source_dir / 'contrib' / 'packaging'))
44 47
45 48 from hgpackaging.wix import (
46 49 build_installer,
47 50 build_signed_installer,
48 51 )
49 52
50 53 fn = build_installer
51 54 kwargs = {
52 55 'source_dir': source_dir,
53 56 'python_exe': pathlib.Path(args.python),
54 57 'version': args.version,
55 58 }
56 59
60 if args.extra_packages_script:
61 kwargs['extra_packages_script'] = args.extra_packages_script
62
57 63 if args.sign_sn or args.sign_cert:
58 64 fn = build_signed_installer
59 65 kwargs['name'] = args.name
60 66 kwargs['subject_name'] = args.sign_sn
61 67 kwargs['cert_path'] = args.sign_cert
62 68 kwargs['cert_password'] = args.sign_password
63 69 kwargs['timestamp_url'] = args.sign_timestamp_url
64 70
65 71 fn(**kwargs)
General Comments 0
You need to be logged in to leave comments. Login now