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