##// END OF EJS Templates
packaging: integrate signing into run_wix_packaging()...
Gregory Szorc -
r45272:a3998409 stable
parent child Browse files
Show More
@@ -1,166 +1,166 b''
1 # cli.py - Command line interface for automation
1 # cli.py - Command line interface for automation
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 argparse
10 import argparse
11 import os
11 import os
12 import pathlib
12 import pathlib
13
13
14 from . import (
14 from . import (
15 inno,
15 inno,
16 wix,
16 wix,
17 )
17 )
18
18
19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
20 SOURCE_DIR = HERE.parent.parent.parent
20 SOURCE_DIR = HERE.parent.parent.parent
21
21
22
22
23 def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
23 def build_inno(pyoxidizer_target=None, python=None, iscc=None, version=None):
24 if not pyoxidizer_target and not python:
24 if not pyoxidizer_target and not python:
25 raise Exception("--python required unless building with PyOxidizer")
25 raise Exception("--python required unless building with PyOxidizer")
26
26
27 if python and not os.path.isabs(python):
27 if python and not os.path.isabs(python):
28 raise Exception("--python arg must be an absolute path")
28 raise Exception("--python arg must be an absolute path")
29
29
30 if iscc:
30 if iscc:
31 iscc = pathlib.Path(iscc)
31 iscc = pathlib.Path(iscc)
32 else:
32 else:
33 iscc = (
33 iscc = (
34 pathlib.Path(os.environ["ProgramFiles(x86)"])
34 pathlib.Path(os.environ["ProgramFiles(x86)"])
35 / "Inno Setup 5"
35 / "Inno Setup 5"
36 / "ISCC.exe"
36 / "ISCC.exe"
37 )
37 )
38
38
39 build_dir = SOURCE_DIR / "build"
39 build_dir = SOURCE_DIR / "build"
40
40
41 if pyoxidizer_target:
41 if pyoxidizer_target:
42 inno.build_with_pyoxidizer(
42 inno.build_with_pyoxidizer(
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
43 SOURCE_DIR, build_dir, pyoxidizer_target, iscc, version=version
44 )
44 )
45 else:
45 else:
46 inno.build_with_py2exe(
46 inno.build_with_py2exe(
47 SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
47 SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
48 )
48 )
49
49
50
50
51 def build_wix(
51 def build_wix(
52 name=None,
52 name=None,
53 python=None,
53 python=None,
54 version=None,
54 version=None,
55 sign_sn=None,
55 sign_sn=None,
56 sign_cert=None,
56 sign_cert=None,
57 sign_password=None,
57 sign_password=None,
58 sign_timestamp_url=None,
58 sign_timestamp_url=None,
59 extra_packages_script=None,
59 extra_packages_script=None,
60 extra_wxs=None,
60 extra_wxs=None,
61 extra_features=None,
61 extra_features=None,
62 ):
62 ):
63 fn = wix.build_installer
64 kwargs = {
63 kwargs = {
65 "source_dir": SOURCE_DIR,
64 "source_dir": SOURCE_DIR,
66 "python_exe": pathlib.Path(python),
65 "python_exe": pathlib.Path(python),
67 "version": version,
66 "version": version,
68 }
67 }
69
68
70 if not os.path.isabs(python):
69 if not os.path.isabs(python):
71 raise Exception("--python arg must be an absolute path")
70 raise Exception("--python arg must be an absolute path")
72
71
73 if extra_packages_script:
72 if extra_packages_script:
74 kwargs["extra_packages_script"] = extra_packages_script
73 kwargs["extra_packages_script"] = extra_packages_script
75 if extra_wxs:
74 if extra_wxs:
76 kwargs["extra_wxs"] = dict(
75 kwargs["extra_wxs"] = dict(
77 thing.split("=") for thing in extra_wxs.split(",")
76 thing.split("=") for thing in extra_wxs.split(",")
78 )
77 )
79 if extra_features:
78 if extra_features:
80 kwargs["extra_features"] = extra_features.split(",")
79 kwargs["extra_features"] = extra_features.split(",")
81
80
82 if sign_sn or sign_cert:
81 if sign_sn or sign_cert:
83 fn = wix.build_signed_installer
82 kwargs["signing_info"] = {
84 kwargs["name"] = name
83 "name": name,
85 kwargs["subject_name"] = sign_sn
84 "subject_name": sign_sn,
86 kwargs["cert_path"] = sign_cert
85 "cert_path": sign_cert,
87 kwargs["cert_password"] = sign_password
86 "cert_password": sign_password,
88 kwargs["timestamp_url"] = sign_timestamp_url
87 "timestamp_url": sign_timestamp_url,
88 }
89
89
90 fn(**kwargs)
90 wix.build_installer(**kwargs)
91
91
92
92
93 def get_parser():
93 def get_parser():
94 parser = argparse.ArgumentParser()
94 parser = argparse.ArgumentParser()
95
95
96 subparsers = parser.add_subparsers()
96 subparsers = parser.add_subparsers()
97
97
98 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
98 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
99 sp.add_argument(
99 sp.add_argument(
100 "--pyoxidizer-target",
100 "--pyoxidizer-target",
101 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
101 choices={"i686-pc-windows-msvc", "x86_64-pc-windows-msvc"},
102 help="Build with PyOxidizer targeting this host triple",
102 help="Build with PyOxidizer targeting this host triple",
103 )
103 )
104 sp.add_argument("--python", help="path to python.exe to use")
104 sp.add_argument("--python", help="path to python.exe to use")
105 sp.add_argument("--iscc", help="path to iscc.exe to use")
105 sp.add_argument("--iscc", help="path to iscc.exe to use")
106 sp.add_argument(
106 sp.add_argument(
107 "--version",
107 "--version",
108 help="Mercurial version string to use "
108 help="Mercurial version string to use "
109 "(detected from __version__.py if not defined",
109 "(detected from __version__.py if not defined",
110 )
110 )
111 sp.set_defaults(func=build_inno)
111 sp.set_defaults(func=build_inno)
112
112
113 sp = subparsers.add_parser(
113 sp = subparsers.add_parser(
114 "wix", help="Build Windows installer with WiX Toolset"
114 "wix", help="Build Windows installer with WiX Toolset"
115 )
115 )
116 sp.add_argument("--name", help="Application name", default="Mercurial")
116 sp.add_argument("--name", help="Application name", default="Mercurial")
117 sp.add_argument(
117 sp.add_argument(
118 "--python", help="Path to Python executable to use", required=True
118 "--python", help="Path to Python executable to use", required=True
119 )
119 )
120 sp.add_argument(
120 sp.add_argument(
121 "--sign-sn",
121 "--sign-sn",
122 help="Subject name (or fragment thereof) of certificate "
122 help="Subject name (or fragment thereof) of certificate "
123 "to use for signing",
123 "to use for signing",
124 )
124 )
125 sp.add_argument(
125 sp.add_argument(
126 "--sign-cert", help="Path to certificate to use for signing"
126 "--sign-cert", help="Path to certificate to use for signing"
127 )
127 )
128 sp.add_argument("--sign-password", help="Password for signing certificate")
128 sp.add_argument("--sign-password", help="Password for signing certificate")
129 sp.add_argument(
129 sp.add_argument(
130 "--sign-timestamp-url",
130 "--sign-timestamp-url",
131 help="URL of timestamp server to use for signing",
131 help="URL of timestamp server to use for signing",
132 )
132 )
133 sp.add_argument("--version", help="Version string to use")
133 sp.add_argument("--version", help="Version string to use")
134 sp.add_argument(
134 sp.add_argument(
135 "--extra-packages-script",
135 "--extra-packages-script",
136 help=(
136 help=(
137 "Script to execute to include extra packages in " "py2exe binary."
137 "Script to execute to include extra packages in " "py2exe binary."
138 ),
138 ),
139 )
139 )
140 sp.add_argument(
140 sp.add_argument(
141 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
141 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
142 )
142 )
143 sp.add_argument(
143 sp.add_argument(
144 "--extra-features",
144 "--extra-features",
145 help=(
145 help=(
146 "CSV of extra feature names to include "
146 "CSV of extra feature names to include "
147 "in the installer from the extra wxs files"
147 "in the installer from the extra wxs files"
148 ),
148 ),
149 )
149 )
150 sp.set_defaults(func=build_wix)
150 sp.set_defaults(func=build_wix)
151
151
152 return parser
152 return parser
153
153
154
154
155 def main():
155 def main():
156 parser = get_parser()
156 parser = get_parser()
157 args = parser.parse_args()
157 args = parser.parse_args()
158
158
159 if not hasattr(args, "func"):
159 if not hasattr(args, "func"):
160 parser.print_help()
160 parser.print_help()
161 return
161 return
162
162
163 kwargs = dict(vars(args))
163 kwargs = dict(vars(args))
164 del kwargs["func"]
164 del kwargs["func"]
165
165
166 args.func(**kwargs)
166 args.func(**kwargs)
@@ -1,526 +1,476 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 collections
10 import collections
11 import os
11 import os
12 import pathlib
12 import pathlib
13 import re
13 import re
14 import shutil
14 import shutil
15 import subprocess
15 import subprocess
16 import typing
16 import typing
17 import uuid
17 import uuid
18 import xml.dom.minidom
18 import xml.dom.minidom
19
19
20 from .downloads import download_entry
20 from .downloads import download_entry
21 from .py2exe import (
21 from .py2exe import (
22 build_py2exe,
22 build_py2exe,
23 stage_install,
23 stage_install,
24 )
24 )
25 from .util import (
25 from .util import (
26 extract_zip_to_directory,
26 extract_zip_to_directory,
27 normalize_windows_version,
27 normalize_windows_version,
28 process_install_rules,
28 process_install_rules,
29 sign_with_signtool,
29 sign_with_signtool,
30 )
30 )
31
31
32
32
33 EXTRA_PACKAGES = {
33 EXTRA_PACKAGES = {
34 'dulwich',
34 'dulwich',
35 'distutils',
35 'distutils',
36 'keyring',
36 'keyring',
37 'pygments',
37 'pygments',
38 'win32ctypes',
38 'win32ctypes',
39 }
39 }
40
40
41
41
42 EXTRA_INSTALL_RULES = [
42 EXTRA_INSTALL_RULES = [
43 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
43 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
44 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
44 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
45 ]
45 ]
46
46
47 STAGING_REMOVE_FILES = [
47 STAGING_REMOVE_FILES = [
48 # We use the RTF variant.
48 # We use the RTF variant.
49 'copying.txt',
49 'copying.txt',
50 ]
50 ]
51
51
52 SHORTCUTS = {
52 SHORTCUTS = {
53 # hg.1.html'
53 # hg.1.html'
54 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
54 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
55 'Name': 'Mercurial Command Reference',
55 'Name': 'Mercurial Command Reference',
56 },
56 },
57 # hgignore.5.html
57 # hgignore.5.html
58 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
58 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
59 'Name': 'Mercurial Ignore Files',
59 'Name': 'Mercurial Ignore Files',
60 },
60 },
61 # hgrc.5.html
61 # hgrc.5.html
62 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
62 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
63 'Name': 'Mercurial Configuration Files',
63 'Name': 'Mercurial Configuration Files',
64 },
64 },
65 }
65 }
66
66
67
67
68 def find_version(source_dir: pathlib.Path):
68 def find_version(source_dir: pathlib.Path):
69 version_py = source_dir / 'mercurial' / '__version__.py'
69 version_py = source_dir / 'mercurial' / '__version__.py'
70
70
71 with version_py.open('r', encoding='utf-8') as fh:
71 with version_py.open('r', encoding='utf-8') as fh:
72 source = fh.read().strip()
72 source = fh.read().strip()
73
73
74 m = re.search('version = b"(.*)"', source)
74 m = re.search('version = b"(.*)"', source)
75 return m.group(1)
75 return m.group(1)
76
76
77
77
78 def ensure_vc90_merge_modules(build_dir):
78 def ensure_vc90_merge_modules(build_dir):
79 x86 = (
79 x86 = (
80 download_entry(
80 download_entry(
81 'vc9-crt-x86-msm',
81 'vc9-crt-x86-msm',
82 build_dir,
82 build_dir,
83 local_name='microsoft.vcxx.crt.x86_msm.msm',
83 local_name='microsoft.vcxx.crt.x86_msm.msm',
84 )[0],
84 )[0],
85 download_entry(
85 download_entry(
86 'vc9-crt-x86-msm-policy',
86 'vc9-crt-x86-msm-policy',
87 build_dir,
87 build_dir,
88 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
88 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
89 )[0],
89 )[0],
90 )
90 )
91
91
92 x64 = (
92 x64 = (
93 download_entry(
93 download_entry(
94 'vc9-crt-x64-msm',
94 'vc9-crt-x64-msm',
95 build_dir,
95 build_dir,
96 local_name='microsoft.vcxx.crt.x64_msm.msm',
96 local_name='microsoft.vcxx.crt.x64_msm.msm',
97 )[0],
97 )[0],
98 download_entry(
98 download_entry(
99 'vc9-crt-x64-msm-policy',
99 'vc9-crt-x64-msm-policy',
100 build_dir,
100 build_dir,
101 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
101 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
102 )[0],
102 )[0],
103 )
103 )
104 return {
104 return {
105 'x86': x86,
105 'x86': x86,
106 'x64': x64,
106 'x64': x64,
107 }
107 }
108
108
109
109
110 def run_candle(wix, cwd, wxs, source_dir, defines=None):
110 def run_candle(wix, cwd, wxs, source_dir, defines=None):
111 args = [
111 args = [
112 str(wix / 'candle.exe'),
112 str(wix / 'candle.exe'),
113 '-nologo',
113 '-nologo',
114 str(wxs),
114 str(wxs),
115 '-dSourceDir=%s' % source_dir,
115 '-dSourceDir=%s' % source_dir,
116 ]
116 ]
117
117
118 if defines:
118 if defines:
119 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
119 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
120
120
121 subprocess.run(args, cwd=str(cwd), check=True)
121 subprocess.run(args, cwd=str(cwd), check=True)
122
122
123
123
124 def make_post_build_signing_fn(
125 name,
126 subject_name=None,
127 cert_path=None,
128 cert_password=None,
129 timestamp_url=None,
130 ):
131 """Create a callable that will use signtool to sign hg.exe."""
132
133 def post_build_sign(source_dir, build_dir, dist_dir, version):
134 description = '%s %s' % (name, version)
135
136 sign_with_signtool(
137 dist_dir / 'hg.exe',
138 description,
139 subject_name=subject_name,
140 cert_path=cert_path,
141 cert_password=cert_password,
142 timestamp_url=timestamp_url,
143 )
144
145 return post_build_sign
146
147
148 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
124 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
149 """Create XML string listing every file to be installed."""
125 """Create XML string listing every file to be installed."""
150
126
151 # We derive GUIDs from a deterministic file path identifier.
127 # We derive GUIDs from a deterministic file path identifier.
152 # We shoehorn the name into something that looks like a URL because
128 # We shoehorn the name into something that looks like a URL because
153 # the UUID namespaces are supposed to work that way (even though
129 # the UUID namespaces are supposed to work that way (even though
154 # the input data probably is never validated).
130 # the input data probably is never validated).
155
131
156 doc = xml.dom.minidom.parseString(
132 doc = xml.dom.minidom.parseString(
157 '<?xml version="1.0" encoding="utf-8"?>'
133 '<?xml version="1.0" encoding="utf-8"?>'
158 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
134 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
159 '</Wix>'
135 '</Wix>'
160 )
136 )
161
137
162 # Assemble the install layout by directory. This makes it easier to
138 # Assemble the install layout by directory. This makes it easier to
163 # emit XML, since each directory has separate entities.
139 # emit XML, since each directory has separate entities.
164 manifest = collections.defaultdict(dict)
140 manifest = collections.defaultdict(dict)
165
141
166 for root, dirs, files in os.walk(staging_dir):
142 for root, dirs, files in os.walk(staging_dir):
167 dirs.sort()
143 dirs.sort()
168
144
169 root = pathlib.Path(root)
145 root = pathlib.Path(root)
170 rel_dir = root.relative_to(staging_dir)
146 rel_dir = root.relative_to(staging_dir)
171
147
172 for i in range(len(rel_dir.parts)):
148 for i in range(len(rel_dir.parts)):
173 parent = '/'.join(rel_dir.parts[0 : i + 1])
149 parent = '/'.join(rel_dir.parts[0 : i + 1])
174 manifest.setdefault(parent, {})
150 manifest.setdefault(parent, {})
175
151
176 for f in sorted(files):
152 for f in sorted(files):
177 full = root / f
153 full = root / f
178 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
154 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
179
155
180 component_groups = collections.defaultdict(list)
156 component_groups = collections.defaultdict(list)
181
157
182 # Now emit a <Fragment> for each directory.
158 # Now emit a <Fragment> for each directory.
183 # Each directory is composed of a <DirectoryRef> pointing to its parent
159 # Each directory is composed of a <DirectoryRef> pointing to its parent
184 # and defines child <Directory>'s and a <Component> with all the files.
160 # and defines child <Directory>'s and a <Component> with all the files.
185 for dir_name, entries in sorted(manifest.items()):
161 for dir_name, entries in sorted(manifest.items()):
186 # The directory id is derived from the path. But the root directory
162 # The directory id is derived from the path. But the root directory
187 # is special.
163 # is special.
188 if dir_name == '.':
164 if dir_name == '.':
189 parent_directory_id = 'INSTALLDIR'
165 parent_directory_id = 'INSTALLDIR'
190 else:
166 else:
191 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
167 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
192
168
193 fragment = doc.createElement('Fragment')
169 fragment = doc.createElement('Fragment')
194 directory_ref = doc.createElement('DirectoryRef')
170 directory_ref = doc.createElement('DirectoryRef')
195 directory_ref.setAttribute('Id', parent_directory_id)
171 directory_ref.setAttribute('Id', parent_directory_id)
196
172
197 # Add <Directory> entries for immediate children directories.
173 # Add <Directory> entries for immediate children directories.
198 for possible_child in sorted(manifest.keys()):
174 for possible_child in sorted(manifest.keys()):
199 if (
175 if (
200 dir_name == '.'
176 dir_name == '.'
201 and '/' not in possible_child
177 and '/' not in possible_child
202 and possible_child != '.'
178 and possible_child != '.'
203 ):
179 ):
204 child_directory_id = 'hg.dir.%s' % possible_child
180 child_directory_id = 'hg.dir.%s' % possible_child
205 name = possible_child
181 name = possible_child
206 else:
182 else:
207 if not possible_child.startswith('%s/' % dir_name):
183 if not possible_child.startswith('%s/' % dir_name):
208 continue
184 continue
209 name = possible_child[len(dir_name) + 1 :]
185 name = possible_child[len(dir_name) + 1 :]
210 if '/' in name:
186 if '/' in name:
211 continue
187 continue
212
188
213 child_directory_id = 'hg.dir.%s' % possible_child.replace(
189 child_directory_id = 'hg.dir.%s' % possible_child.replace(
214 '/', '.'
190 '/', '.'
215 )
191 )
216
192
217 directory = doc.createElement('Directory')
193 directory = doc.createElement('Directory')
218 directory.setAttribute('Id', child_directory_id)
194 directory.setAttribute('Id', child_directory_id)
219 directory.setAttribute('Name', name)
195 directory.setAttribute('Name', name)
220 directory_ref.appendChild(directory)
196 directory_ref.appendChild(directory)
221
197
222 # Add <Component>s for files in this directory.
198 # Add <Component>s for files in this directory.
223 for rel, source_path in sorted(entries.items()):
199 for rel, source_path in sorted(entries.items()):
224 if dir_name == '.':
200 if dir_name == '.':
225 full_rel = rel
201 full_rel = rel
226 else:
202 else:
227 full_rel = '%s/%s' % (dir_name, rel)
203 full_rel = '%s/%s' % (dir_name, rel)
228
204
229 component_unique_id = (
205 component_unique_id = (
230 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
206 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
231 % full_rel
207 % full_rel
232 )
208 )
233 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
209 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
234 component_id = 'hg.component.%s' % str(component_guid).replace(
210 component_id = 'hg.component.%s' % str(component_guid).replace(
235 '-', '_'
211 '-', '_'
236 )
212 )
237
213
238 component = doc.createElement('Component')
214 component = doc.createElement('Component')
239
215
240 component.setAttribute('Id', component_id)
216 component.setAttribute('Id', component_id)
241 component.setAttribute('Guid', str(component_guid).upper())
217 component.setAttribute('Guid', str(component_guid).upper())
242 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
218 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
243
219
244 # Assign this component to a top-level group.
220 # Assign this component to a top-level group.
245 if dir_name == '.':
221 if dir_name == '.':
246 component_groups['ROOT'].append(component_id)
222 component_groups['ROOT'].append(component_id)
247 elif '/' in dir_name:
223 elif '/' in dir_name:
248 component_groups[dir_name[0 : dir_name.index('/')]].append(
224 component_groups[dir_name[0 : dir_name.index('/')]].append(
249 component_id
225 component_id
250 )
226 )
251 else:
227 else:
252 component_groups[dir_name].append(component_id)
228 component_groups[dir_name].append(component_id)
253
229
254 unique_id = (
230 unique_id = (
255 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
231 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
256 )
232 )
257 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
233 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
258
234
259 # IDs have length limits. So use GUID to derive them.
235 # IDs have length limits. So use GUID to derive them.
260 file_guid_normalized = str(file_guid).replace('-', '_')
236 file_guid_normalized = str(file_guid).replace('-', '_')
261 file_id = 'hg.file.%s' % file_guid_normalized
237 file_id = 'hg.file.%s' % file_guid_normalized
262
238
263 file_element = doc.createElement('File')
239 file_element = doc.createElement('File')
264 file_element.setAttribute('Id', file_id)
240 file_element.setAttribute('Id', file_id)
265 file_element.setAttribute('Source', str(source_path))
241 file_element.setAttribute('Source', str(source_path))
266 file_element.setAttribute('KeyPath', 'yes')
242 file_element.setAttribute('KeyPath', 'yes')
267 file_element.setAttribute('ReadOnly', 'yes')
243 file_element.setAttribute('ReadOnly', 'yes')
268
244
269 component.appendChild(file_element)
245 component.appendChild(file_element)
270 directory_ref.appendChild(component)
246 directory_ref.appendChild(component)
271
247
272 fragment.appendChild(directory_ref)
248 fragment.appendChild(directory_ref)
273 doc.documentElement.appendChild(fragment)
249 doc.documentElement.appendChild(fragment)
274
250
275 for group, component_ids in sorted(component_groups.items()):
251 for group, component_ids in sorted(component_groups.items()):
276 fragment = doc.createElement('Fragment')
252 fragment = doc.createElement('Fragment')
277 component_group = doc.createElement('ComponentGroup')
253 component_group = doc.createElement('ComponentGroup')
278 component_group.setAttribute('Id', 'hg.group.%s' % group)
254 component_group.setAttribute('Id', 'hg.group.%s' % group)
279
255
280 for component_id in component_ids:
256 for component_id in component_ids:
281 component_ref = doc.createElement('ComponentRef')
257 component_ref = doc.createElement('ComponentRef')
282 component_ref.setAttribute('Id', component_id)
258 component_ref.setAttribute('Id', component_id)
283 component_group.appendChild(component_ref)
259 component_group.appendChild(component_ref)
284
260
285 fragment.appendChild(component_group)
261 fragment.appendChild(component_group)
286 doc.documentElement.appendChild(fragment)
262 doc.documentElement.appendChild(fragment)
287
263
288 # Add <Shortcut> to files that have it defined.
264 # Add <Shortcut> to files that have it defined.
289 for file_id, metadata in sorted(SHORTCUTS.items()):
265 for file_id, metadata in sorted(SHORTCUTS.items()):
290 els = doc.getElementsByTagName('File')
266 els = doc.getElementsByTagName('File')
291 els = [el for el in els if el.getAttribute('Id') == file_id]
267 els = [el for el in els if el.getAttribute('Id') == file_id]
292
268
293 if not els:
269 if not els:
294 raise Exception('could not find File[Id=%s]' % file_id)
270 raise Exception('could not find File[Id=%s]' % file_id)
295
271
296 for el in els:
272 for el in els:
297 shortcut = doc.createElement('Shortcut')
273 shortcut = doc.createElement('Shortcut')
298 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
274 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
299 shortcut.setAttribute('Directory', 'ProgramMenuDir')
275 shortcut.setAttribute('Directory', 'ProgramMenuDir')
300 shortcut.setAttribute('Icon', 'hgIcon.ico')
276 shortcut.setAttribute('Icon', 'hgIcon.ico')
301 shortcut.setAttribute('IconIndex', '0')
277 shortcut.setAttribute('IconIndex', '0')
302 shortcut.setAttribute('Advertise', 'yes')
278 shortcut.setAttribute('Advertise', 'yes')
303 for k, v in sorted(metadata.items()):
279 for k, v in sorted(metadata.items()):
304 shortcut.setAttribute(k, v)
280 shortcut.setAttribute(k, v)
305
281
306 el.appendChild(shortcut)
282 el.appendChild(shortcut)
307
283
308 return doc.toprettyxml()
284 return doc.toprettyxml()
309
285
310
286
311 def build_installer(
287 def build_installer(
312 source_dir: pathlib.Path,
288 source_dir: pathlib.Path,
313 python_exe: pathlib.Path,
289 python_exe: pathlib.Path,
314 msi_name='mercurial',
290 msi_name='mercurial',
315 version=None,
291 version=None,
316 post_build_fn=None,
317 extra_packages_script=None,
292 extra_packages_script=None,
318 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
293 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
319 extra_features: typing.Optional[typing.List[str]] = None,
294 extra_features: typing.Optional[typing.List[str]] = None,
295 signing_info: typing.Optional[typing.Dict[str, str]] = None,
320 ):
296 ):
321 """Build a WiX MSI installer.
297 """Build a WiX MSI installer.
322
298
323 ``source_dir`` is the path to the Mercurial source tree to use.
299 ``source_dir`` is the path to the Mercurial source tree to use.
324 ``arch`` is the target architecture. either ``x86`` or ``x64``.
300 ``arch`` is the target architecture. either ``x86`` or ``x64``.
325 ``python_exe`` is the path to the Python executable to use/bundle.
301 ``python_exe`` is the path to the Python executable to use/bundle.
326 ``version`` is the Mercurial version string. If not defined,
302 ``version`` is the Mercurial version string. If not defined,
327 ``mercurial/__version__.py`` will be consulted.
303 ``mercurial/__version__.py`` will be consulted.
328 ``post_build_fn`` is a callable that will be called after building
329 Mercurial but before invoking WiX. It can be used to e.g. facilitate
330 signing. It is passed the paths to the Mercurial source, build, and
331 dist directories and the resolved Mercurial version.
332 ``extra_packages_script`` is a command to be run to inject extra packages
304 ``extra_packages_script`` is a command to be run to inject extra packages
333 into the py2exe binary. It should stage packages into the virtualenv and
305 into the py2exe binary. It should stage packages into the virtualenv and
334 print a null byte followed by a newline-separated list of packages that
306 print a null byte followed by a newline-separated list of packages that
335 should be included in the exe.
307 should be included in the exe.
336 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
308 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
337 ``extra_features`` is a list of additional named Features to include in
309 ``extra_features`` is a list of additional named Features to include in
338 the build. These must match Feature names in one of the wxs scripts.
310 the build. These must match Feature names in one of the wxs scripts.
339 """
311 """
340 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
312 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
341
313
342 hg_build_dir = source_dir / 'build'
314 hg_build_dir = source_dir / 'build'
343 dist_dir = source_dir / 'dist'
344
315
345 requirements_txt = (
316 requirements_txt = (
346 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
317 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
347 )
318 )
348
319
349 build_py2exe(
320 build_py2exe(
350 source_dir,
321 source_dir,
351 hg_build_dir,
322 hg_build_dir,
352 python_exe,
323 python_exe,
353 'wix',
324 'wix',
354 requirements_txt,
325 requirements_txt,
355 extra_packages=EXTRA_PACKAGES,
326 extra_packages=EXTRA_PACKAGES,
356 extra_packages_script=extra_packages_script,
327 extra_packages_script=extra_packages_script,
357 )
328 )
358
329
359 orig_version = version or find_version(source_dir)
330 orig_version = version or find_version(source_dir)
360 version = normalize_windows_version(orig_version)
331 version = normalize_windows_version(orig_version)
361 print('using version string: %s' % version)
332 print('using version string: %s' % version)
362 if version != orig_version:
333 if version != orig_version:
363 print('(normalized from: %s)' % orig_version)
334 print('(normalized from: %s)' % orig_version)
364
335
365 if post_build_fn:
366 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
367
368 build_dir = hg_build_dir / ('wix-%s' % arch)
336 build_dir = hg_build_dir / ('wix-%s' % arch)
369 staging_dir = build_dir / 'stage'
337 staging_dir = build_dir / 'stage'
370
338
371 build_dir.mkdir(exist_ok=True)
339 build_dir.mkdir(exist_ok=True)
372
340
373 # Purge the staging directory for every build so packaging is pristine.
341 # Purge the staging directory for every build so packaging is pristine.
374 if staging_dir.exists():
342 if staging_dir.exists():
375 print('purging %s' % staging_dir)
343 print('purging %s' % staging_dir)
376 shutil.rmtree(staging_dir)
344 shutil.rmtree(staging_dir)
377
345
378 stage_install(source_dir, staging_dir, lower_case=True)
346 stage_install(source_dir, staging_dir, lower_case=True)
379
347
380 # We also install some extra files.
348 # We also install some extra files.
381 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
349 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
382
350
383 # And remove some files we don't want.
351 # And remove some files we don't want.
384 for f in STAGING_REMOVE_FILES:
352 for f in STAGING_REMOVE_FILES:
385 p = staging_dir / f
353 p = staging_dir / f
386 if p.exists():
354 if p.exists():
387 print('removing %s' % p)
355 print('removing %s' % p)
388 p.unlink()
356 p.unlink()
389
357
390 return run_wix_packaging(
358 return run_wix_packaging(
391 source_dir,
359 source_dir,
392 build_dir,
360 build_dir,
393 staging_dir,
361 staging_dir,
394 arch,
362 arch,
395 version=version,
363 version=version,
396 orig_version=orig_version,
364 orig_version=orig_version,
397 msi_name=msi_name,
365 msi_name=msi_name,
398 extra_wxs=extra_wxs,
366 extra_wxs=extra_wxs,
399 extra_features=extra_features,
367 extra_features=extra_features,
368 signing_info=signing_info,
400 )
369 )
401
370
402
371
403 def run_wix_packaging(
372 def run_wix_packaging(
404 source_dir: pathlib.Path,
373 source_dir: pathlib.Path,
405 build_dir: pathlib.Path,
374 build_dir: pathlib.Path,
406 staging_dir: pathlib.Path,
375 staging_dir: pathlib.Path,
407 arch: str,
376 arch: str,
408 version: str,
377 version: str,
409 orig_version: str,
378 orig_version: str,
410 msi_name: typing.Optional[str] = "mercurial",
379 msi_name: typing.Optional[str] = "mercurial",
411 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
380 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
412 extra_features: typing.Optional[typing.List[str]] = None,
381 extra_features: typing.Optional[typing.List[str]] = None,
382 signing_info: typing.Optional[typing.Dict[str, str]] = None,
413 ):
383 ):
414 """Invokes WiX to package up a built Mercurial."""
384 """Invokes WiX to package up a built Mercurial.
385
386 ``signing_info`` is a dict defining properties to facilitate signing the
387 installer. Recognized keys include ``name``, ``subject_name``,
388 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
389 we will sign both the hg.exe and the .msi using the signing credentials
390 specified.
391 """
392 if signing_info:
393 sign_with_signtool(
394 staging_dir / "hg.exe",
395 "%s %s" % (signing_info["name"], version),
396 subject_name=signing_info["subject_name"],
397 cert_path=signing_info["cert_path"],
398 cert_password=signing_info["cert_password"],
399 timestamp_url=signing_info["timestamp_url"],
400 )
415
401
416 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
402 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
417
403
418 wix_pkg, wix_entry = download_entry('wix', build_dir)
404 wix_pkg, wix_entry = download_entry('wix', build_dir)
419 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
405 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
420
406
421 if not wix_path.exists():
407 if not wix_path.exists():
422 extract_zip_to_directory(wix_pkg, wix_path)
408 extract_zip_to_directory(wix_pkg, wix_path)
423
409
424 ensure_vc90_merge_modules(build_dir)
410 ensure_vc90_merge_modules(build_dir)
425
411
426 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
412 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
427
413
428 defines = {'Platform': arch}
414 defines = {'Platform': arch}
429
415
430 # Derive a .wxs file with the staged files.
416 # Derive a .wxs file with the staged files.
431 manifest_wxs = build_dir / 'stage.wxs'
417 manifest_wxs = build_dir / 'stage.wxs'
432 with manifest_wxs.open('w', encoding='utf-8') as fh:
418 with manifest_wxs.open('w', encoding='utf-8') as fh:
433 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
419 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
434
420
435 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
421 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
436
422
437 for source, rel_path in sorted((extra_wxs or {}).items()):
423 for source, rel_path in sorted((extra_wxs or {}).items()):
438 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
424 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
439
425
440 source = wix_dir / 'mercurial.wxs'
426 source = wix_dir / 'mercurial.wxs'
441 defines['Version'] = version
427 defines['Version'] = version
442 defines['Comments'] = 'Installs Mercurial version %s' % version
428 defines['Comments'] = 'Installs Mercurial version %s' % version
443 defines['VCRedistSrcDir'] = str(build_dir)
429 defines['VCRedistSrcDir'] = str(build_dir)
444 if extra_features:
430 if extra_features:
445 assert all(';' not in f for f in extra_features)
431 assert all(';' not in f for f in extra_features)
446 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
432 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
447
433
448 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
434 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
449
435
450 msi_path = (
436 msi_path = (
451 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
437 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
452 )
438 )
453
439
454 args = [
440 args = [
455 str(wix_path / 'light.exe'),
441 str(wix_path / 'light.exe'),
456 '-nologo',
442 '-nologo',
457 '-ext',
443 '-ext',
458 'WixUIExtension',
444 'WixUIExtension',
459 '-sw1076',
445 '-sw1076',
460 '-spdb',
446 '-spdb',
461 '-o',
447 '-o',
462 str(msi_path),
448 str(msi_path),
463 ]
449 ]
464
450
465 for source, rel_path in sorted((extra_wxs or {}).items()):
451 for source, rel_path in sorted((extra_wxs or {}).items()):
466 assert source.endswith('.wxs')
452 assert source.endswith('.wxs')
467 source = os.path.basename(source)
453 source = os.path.basename(source)
468 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
454 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
469
455
470 args.extend(
456 args.extend(
471 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
457 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
472 )
458 )
473
459
474 subprocess.run(args, cwd=str(source_dir), check=True)
460 subprocess.run(args, cwd=str(source_dir), check=True)
475
461
476 print('%s created' % msi_path)
462 print('%s created' % msi_path)
477
463
464 if signing_info:
465 sign_with_signtool(
466 msi_path,
467 "%s %s" % (signing_info["name"], version),
468 subject_name=signing_info["subject_name"],
469 cert_path=signing_info["cert_path"],
470 cert_password=signing_info["cert_password"],
471 timestamp_url=signing_info["timestamp_url"],
472 )
473
478 return {
474 return {
479 'msi_path': msi_path,
475 'msi_path': msi_path,
480 }
476 }
481
482
483 def build_signed_installer(
484 source_dir: pathlib.Path,
485 python_exe: pathlib.Path,
486 name: str,
487 version=None,
488 subject_name=None,
489 cert_path=None,
490 cert_password=None,
491 timestamp_url=None,
492 extra_packages_script=None,
493 extra_wxs=None,
494 extra_features=None,
495 ):
496 """Build an installer with signed executables."""
497
498 post_build_fn = make_post_build_signing_fn(
499 name,
500 subject_name=subject_name,
501 cert_path=cert_path,
502 cert_password=cert_password,
503 timestamp_url=timestamp_url,
504 )
505
506 info = build_installer(
507 source_dir,
508 python_exe=python_exe,
509 msi_name=name.lower(),
510 version=version,
511 post_build_fn=post_build_fn,
512 extra_packages_script=extra_packages_script,
513 extra_wxs=extra_wxs,
514 extra_features=extra_features,
515 )
516
517 description = '%s %s' % (name, version)
518
519 sign_with_signtool(
520 info['msi_path'],
521 description,
522 subject_name=subject_name,
523 cert_path=cert_path,
524 cert_password=cert_password,
525 timestamp_url=timestamp_url,
526 )
General Comments 0
You need to be logged in to leave comments. Login now