##// END OF EJS Templates
packaging: rename run_pyoxidizer()...
Gregory Szorc -
r47979:df1767fa default
parent child Browse files
Show More
@@ -1,242 +1,244 b''
1 1 # inno.py - Inno Setup 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 shutil
13 13 import subprocess
14 14
15 15 import jinja2
16 16
17 17 from .py2exe import (
18 18 build_py2exe,
19 19 stage_install,
20 20 )
21 from .pyoxidizer import run_pyoxidizer
21 from .pyoxidizer import create_pyoxidizer_install_layout
22 22 from .util import (
23 23 find_legacy_vc_runtime_files,
24 24 normalize_windows_version,
25 25 process_install_rules,
26 26 read_version_py,
27 27 )
28 28
29 29 EXTRA_PACKAGES = {
30 30 'dulwich',
31 31 'keyring',
32 32 'pygments',
33 33 'win32ctypes',
34 34 }
35 35
36 36 EXTRA_INCLUDES = {
37 37 '_curses',
38 38 '_curses_panel',
39 39 }
40 40
41 41 EXTRA_INSTALL_RULES = [
42 42 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
43 43 ]
44 44
45 45 PACKAGE_FILES_METADATA = {
46 46 'ReadMe.html': 'Flags: isreadme',
47 47 }
48 48
49 49
50 50 def build_with_py2exe(
51 51 source_dir: pathlib.Path,
52 52 build_dir: pathlib.Path,
53 53 python_exe: pathlib.Path,
54 54 iscc_exe: pathlib.Path,
55 55 version=None,
56 56 ):
57 57 """Build the Inno installer using py2exe.
58 58
59 59 Build files will be placed in ``build_dir``.
60 60
61 61 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
62 62 for finding the Python 2.7 toolchain. So, we require the environment
63 63 to already be configured with an active toolchain.
64 64 """
65 65 if not iscc_exe.exists():
66 66 raise Exception('%s does not exist' % iscc_exe)
67 67
68 68 vc_x64 = r'\x64' in os.environ.get('LIB', '')
69 69 arch = 'x64' if vc_x64 else 'x86'
70 70 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
71 71 staging_dir = inno_build_dir / 'stage'
72 72
73 73 requirements_txt = (
74 74 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
75 75 )
76 76
77 77 inno_build_dir.mkdir(parents=True, exist_ok=True)
78 78
79 79 build_py2exe(
80 80 source_dir,
81 81 build_dir,
82 82 python_exe,
83 83 'inno',
84 84 requirements_txt,
85 85 extra_packages=EXTRA_PACKAGES,
86 86 extra_includes=EXTRA_INCLUDES,
87 87 )
88 88
89 89 # Purge the staging directory for every build so packaging is
90 90 # pristine.
91 91 if staging_dir.exists():
92 92 print('purging %s' % staging_dir)
93 93 shutil.rmtree(staging_dir)
94 94
95 95 # Now assemble all the packaged files into the staging directory.
96 96 stage_install(source_dir, staging_dir)
97 97
98 98 # We also install some extra files.
99 99 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
100 100
101 101 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
102 102 for f in find_legacy_vc_runtime_files(vc_x64):
103 103 if f.name.endswith('.manifest'):
104 104 basename = 'Microsoft.VC90.CRT.manifest'
105 105 else:
106 106 basename = f.name
107 107
108 108 dest_path = staging_dir / basename
109 109
110 110 print('copying %s to %s' % (f, dest_path))
111 111 shutil.copyfile(f, dest_path)
112 112
113 113 build_installer(
114 114 source_dir,
115 115 inno_build_dir,
116 116 staging_dir,
117 117 iscc_exe,
118 118 version,
119 119 arch="x64" if vc_x64 else None,
120 120 suffix="-python2",
121 121 )
122 122
123 123
124 124 def build_with_pyoxidizer(
125 125 source_dir: pathlib.Path,
126 126 build_dir: pathlib.Path,
127 127 target_triple: str,
128 128 iscc_exe: pathlib.Path,
129 129 version=None,
130 130 ):
131 131 """Build the Inno installer using PyOxidizer."""
132 132 if not iscc_exe.exists():
133 133 raise Exception("%s does not exist" % iscc_exe)
134 134
135 135 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
136 136 staging_dir = inno_build_dir / "stage"
137 137
138 138 inno_build_dir.mkdir(parents=True, exist_ok=True)
139 run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
139 create_pyoxidizer_install_layout(
140 source_dir, inno_build_dir, staging_dir, target_triple
141 )
140 142
141 143 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
142 144
143 145 build_installer(
144 146 source_dir,
145 147 inno_build_dir,
146 148 staging_dir,
147 149 iscc_exe,
148 150 version,
149 151 arch="x64" if "x86_64" in target_triple else None,
150 152 )
151 153
152 154
153 155 def build_installer(
154 156 source_dir: pathlib.Path,
155 157 inno_build_dir: pathlib.Path,
156 158 staging_dir: pathlib.Path,
157 159 iscc_exe: pathlib.Path,
158 160 version,
159 161 arch=None,
160 162 suffix="",
161 163 ):
162 164 """Build an Inno installer from staged Mercurial files.
163 165
164 166 This function is agnostic about how to build Mercurial. It just
165 167 cares that Mercurial files are in ``staging_dir``.
166 168 """
167 169 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
168 170
169 171 # The final package layout is simply a mirror of the staging directory.
170 172 package_files = []
171 173 for root, dirs, files in os.walk(staging_dir):
172 174 dirs.sort()
173 175
174 176 root = pathlib.Path(root)
175 177
176 178 for f in sorted(files):
177 179 full = root / f
178 180 rel = full.relative_to(staging_dir)
179 181 if str(rel.parent) == '.':
180 182 dest_dir = '{app}'
181 183 else:
182 184 dest_dir = '{app}\\%s' % rel.parent
183 185
184 186 package_files.append(
185 187 {
186 188 'source': rel,
187 189 'dest_dir': dest_dir,
188 190 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
189 191 }
190 192 )
191 193
192 194 print('creating installer')
193 195
194 196 # Install Inno files by rendering a template.
195 197 jinja_env = jinja2.Environment(
196 198 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
197 199 # Need to change these to prevent conflict with Inno Setup.
198 200 comment_start_string='{##',
199 201 comment_end_string='##}',
200 202 )
201 203
202 204 try:
203 205 template = jinja_env.get_template('mercurial.iss')
204 206 except jinja2.TemplateSyntaxError as e:
205 207 raise Exception(
206 208 'template syntax error at %s:%d: %s'
207 209 % (
208 210 e.name,
209 211 e.lineno,
210 212 e.message,
211 213 )
212 214 )
213 215
214 216 content = template.render(package_files=package_files)
215 217
216 218 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
217 219 fh.write(content)
218 220
219 221 # Copy additional files used by Inno.
220 222 for p in ('mercurial.ico', 'postinstall.txt'):
221 223 shutil.copyfile(
222 224 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
223 225 )
224 226
225 227 args = [str(iscc_exe)]
226 228
227 229 if arch:
228 230 args.append('/dARCH=%s' % arch)
229 231 args.append('/dSUFFIX=-%s%s' % (arch, suffix))
230 232 else:
231 233 args.append('/dSUFFIX=-x86%s' % suffix)
232 234
233 235 if not version:
234 236 version = read_version_py(source_dir)
235 237
236 238 args.append('/dVERSION=%s' % version)
237 239 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
238 240
239 241 args.append('/Odist')
240 242 args.append(str(inno_build_dir / 'mercurial.iss'))
241 243
242 244 subprocess.run(args, cwd=str(source_dir), check=True)
@@ -1,152 +1,152 b''
1 1 # pyoxidizer.py - Packaging support for PyOxidizer
2 2 #
3 3 # Copyright 2020 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 shutil
13 13 import subprocess
14 14 import sys
15 15
16 16 from .downloads import download_entry
17 17 from .util import (
18 18 extract_zip_to_directory,
19 19 process_install_rules,
20 20 find_vc_runtime_dll,
21 21 )
22 22
23 23
24 24 STAGING_RULES_WINDOWS = [
25 25 ('contrib/bash_completion', 'contrib/'),
26 26 ('contrib/hgk', 'contrib/hgk.tcl'),
27 27 ('contrib/hgweb.fcgi', 'contrib/'),
28 28 ('contrib/hgweb.wsgi', 'contrib/'),
29 29 ('contrib/logo-droplets.svg', 'contrib/'),
30 30 ('contrib/mercurial.el', 'contrib/'),
31 31 ('contrib/mq.el', 'contrib/'),
32 32 ('contrib/tcsh_completion', 'contrib/'),
33 33 ('contrib/tcsh_completion_build.sh', 'contrib/'),
34 34 ('contrib/vim/*', 'contrib/vim/'),
35 35 ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
36 36 ('contrib/win32/ReadMe.html', 'ReadMe.html'),
37 37 ('contrib/xml.rnc', 'contrib/'),
38 38 ('contrib/zsh_completion', 'contrib/'),
39 39 ('doc/*.html', 'doc/'),
40 40 ('doc/style.css', 'doc/'),
41 41 ('COPYING', 'Copying.txt'),
42 42 ]
43 43
44 44 STAGING_RULES_APP = [
45 45 ('lib/mercurial/helptext/**/*.txt', 'helptext/'),
46 46 ('lib/mercurial/defaultrc/*.rc', 'defaultrc/'),
47 47 ('lib/mercurial/locale/**/*', 'locale/'),
48 48 ('lib/mercurial/templates/**/*', 'templates/'),
49 49 ]
50 50
51 51 STAGING_EXCLUDES_WINDOWS = [
52 52 "doc/hg-ssh.8.html",
53 53 ]
54 54
55 55
56 56 def build_docs_html(source_dir: pathlib.Path):
57 57 """Ensures HTML documentation is built.
58 58
59 59 This will fail if docutils isn't available.
60 60
61 61 (The HTML docs aren't built as part of `pip install` so we need to build them
62 62 out of band.)
63 63 """
64 64 subprocess.run(
65 65 [sys.executable, str(source_dir / "setup.py"), "build_doc", "--html"],
66 66 cwd=str(source_dir),
67 67 check=True,
68 68 )
69 69
70 70
71 def run_pyoxidizer(
71 def create_pyoxidizer_install_layout(
72 72 source_dir: pathlib.Path,
73 73 build_dir: pathlib.Path,
74 74 out_dir: pathlib.Path,
75 75 target_triple: str,
76 76 ):
77 77 """Build Mercurial with PyOxidizer and copy additional files into place.
78 78
79 79 After successful completion, ``out_dir`` contains files constituting a
80 80 Mercurial install.
81 81 """
82 82 # We need to make gettext binaries available for compiling i18n files.
83 83 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
84 84 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
85 85
86 86 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
87 87
88 88 if not gettext_root.exists():
89 89 extract_zip_to_directory(gettext_pkg, gettext_root)
90 90 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
91 91
92 92 env = dict(os.environ)
93 93 env["PATH"] = "%s%s%s" % (
94 94 env["PATH"],
95 95 os.pathsep,
96 96 str(gettext_root / "bin"),
97 97 )
98 98
99 99 args = [
100 100 "pyoxidizer",
101 101 "build",
102 102 "--path",
103 103 str(source_dir / "rust" / "hgcli"),
104 104 "--release",
105 105 "--target-triple",
106 106 target_triple,
107 107 ]
108 108
109 109 subprocess.run(args, env=env, check=True)
110 110
111 111 if "windows" in target_triple:
112 112 target = "app_windows"
113 113 else:
114 114 target = "app_posix"
115 115
116 116 build_dir = (
117 117 source_dir / "build" / "pyoxidizer" / target_triple / "release" / target
118 118 )
119 119
120 120 if out_dir.exists():
121 121 print("purging %s" % out_dir)
122 122 shutil.rmtree(out_dir)
123 123
124 124 # Now assemble all the files from PyOxidizer into the staging directory.
125 125 shutil.copytree(build_dir, out_dir)
126 126
127 127 # Move some of those files around. We can get rid of this once Mercurial
128 128 # is taught to use the importlib APIs for reading resources.
129 129 process_install_rules(STAGING_RULES_APP, build_dir, out_dir)
130 130
131 131 build_docs_html(source_dir)
132 132
133 133 if "windows" in target_triple:
134 134 process_install_rules(STAGING_RULES_WINDOWS, source_dir, out_dir)
135 135
136 136 # Write out a default editor.rc file to configure notepad as the
137 137 # default editor.
138 138 os.makedirs(out_dir / "defaultrc", exist_ok=True)
139 139 with (out_dir / "defaultrc" / "editor.rc").open(
140 140 "w", encoding="utf-8"
141 141 ) as fh:
142 142 fh.write("[ui]\neditor = notepad\n")
143 143
144 144 for f in STAGING_EXCLUDES_WINDOWS:
145 145 p = out_dir / f
146 146 if p.exists():
147 147 print("removing %s" % p)
148 148 p.unlink()
149 149
150 150 # Add vcruntimeXXX.dll next to executable.
151 151 vc_runtime_dll = find_vc_runtime_dll(x64="x86_64" in target_triple)
152 152 shutil.copy(vc_runtime_dll, out_dir / vc_runtime_dll.name)
@@ -1,547 +1,549 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 collections
11 11 import os
12 12 import pathlib
13 13 import re
14 14 import shutil
15 15 import subprocess
16 16 import typing
17 17 import uuid
18 18 import xml.dom.minidom
19 19
20 20 from .downloads import download_entry
21 21 from .py2exe import (
22 22 build_py2exe,
23 23 stage_install,
24 24 )
25 from .pyoxidizer import run_pyoxidizer
25 from .pyoxidizer import create_pyoxidizer_install_layout
26 26 from .util import (
27 27 extract_zip_to_directory,
28 28 normalize_windows_version,
29 29 process_install_rules,
30 30 sign_with_signtool,
31 31 )
32 32
33 33
34 34 EXTRA_PACKAGES = {
35 35 'dulwich',
36 36 'distutils',
37 37 'keyring',
38 38 'pygments',
39 39 'win32ctypes',
40 40 }
41 41
42 42 EXTRA_INCLUDES = {
43 43 '_curses',
44 44 '_curses_panel',
45 45 }
46 46
47 47 EXTRA_INSTALL_RULES = [
48 48 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
49 49 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
50 50 ]
51 51
52 52 STAGING_REMOVE_FILES = [
53 53 # We use the RTF variant.
54 54 'copying.txt',
55 55 ]
56 56
57 57 SHORTCUTS = {
58 58 # hg.1.html'
59 59 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
60 60 'Name': 'Mercurial Command Reference',
61 61 },
62 62 # hgignore.5.html
63 63 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
64 64 'Name': 'Mercurial Ignore Files',
65 65 },
66 66 # hgrc.5.html
67 67 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
68 68 'Name': 'Mercurial Configuration Files',
69 69 },
70 70 }
71 71
72 72
73 73 def find_version(source_dir: pathlib.Path):
74 74 version_py = source_dir / 'mercurial' / '__version__.py'
75 75
76 76 with version_py.open('r', encoding='utf-8') as fh:
77 77 source = fh.read().strip()
78 78
79 79 m = re.search('version = b"(.*)"', source)
80 80 return m.group(1)
81 81
82 82
83 83 def ensure_vc90_merge_modules(build_dir):
84 84 x86 = (
85 85 download_entry(
86 86 'vc9-crt-x86-msm',
87 87 build_dir,
88 88 local_name='microsoft.vcxx.crt.x86_msm.msm',
89 89 )[0],
90 90 download_entry(
91 91 'vc9-crt-x86-msm-policy',
92 92 build_dir,
93 93 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
94 94 )[0],
95 95 )
96 96
97 97 x64 = (
98 98 download_entry(
99 99 'vc9-crt-x64-msm',
100 100 build_dir,
101 101 local_name='microsoft.vcxx.crt.x64_msm.msm',
102 102 )[0],
103 103 download_entry(
104 104 'vc9-crt-x64-msm-policy',
105 105 build_dir,
106 106 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
107 107 )[0],
108 108 )
109 109 return {
110 110 'x86': x86,
111 111 'x64': x64,
112 112 }
113 113
114 114
115 115 def run_candle(wix, cwd, wxs, source_dir, defines=None):
116 116 args = [
117 117 str(wix / 'candle.exe'),
118 118 '-nologo',
119 119 str(wxs),
120 120 '-dSourceDir=%s' % source_dir,
121 121 ]
122 122
123 123 if defines:
124 124 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
125 125
126 126 subprocess.run(args, cwd=str(cwd), check=True)
127 127
128 128
129 129 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
130 130 """Create XML string listing every file to be installed."""
131 131
132 132 # We derive GUIDs from a deterministic file path identifier.
133 133 # We shoehorn the name into something that looks like a URL because
134 134 # the UUID namespaces are supposed to work that way (even though
135 135 # the input data probably is never validated).
136 136
137 137 doc = xml.dom.minidom.parseString(
138 138 '<?xml version="1.0" encoding="utf-8"?>'
139 139 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
140 140 '</Wix>'
141 141 )
142 142
143 143 # Assemble the install layout by directory. This makes it easier to
144 144 # emit XML, since each directory has separate entities.
145 145 manifest = collections.defaultdict(dict)
146 146
147 147 for root, dirs, files in os.walk(staging_dir):
148 148 dirs.sort()
149 149
150 150 root = pathlib.Path(root)
151 151 rel_dir = root.relative_to(staging_dir)
152 152
153 153 for i in range(len(rel_dir.parts)):
154 154 parent = '/'.join(rel_dir.parts[0 : i + 1])
155 155 manifest.setdefault(parent, {})
156 156
157 157 for f in sorted(files):
158 158 full = root / f
159 159 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
160 160
161 161 component_groups = collections.defaultdict(list)
162 162
163 163 # Now emit a <Fragment> for each directory.
164 164 # Each directory is composed of a <DirectoryRef> pointing to its parent
165 165 # and defines child <Directory>'s and a <Component> with all the files.
166 166 for dir_name, entries in sorted(manifest.items()):
167 167 # The directory id is derived from the path. But the root directory
168 168 # is special.
169 169 if dir_name == '.':
170 170 parent_directory_id = 'INSTALLDIR'
171 171 else:
172 172 parent_directory_id = 'hg.dir.%s' % dir_name.replace(
173 173 '/', '.'
174 174 ).replace('-', '_')
175 175
176 176 fragment = doc.createElement('Fragment')
177 177 directory_ref = doc.createElement('DirectoryRef')
178 178 directory_ref.setAttribute('Id', parent_directory_id)
179 179
180 180 # Add <Directory> entries for immediate children directories.
181 181 for possible_child in sorted(manifest.keys()):
182 182 if (
183 183 dir_name == '.'
184 184 and '/' not in possible_child
185 185 and possible_child != '.'
186 186 ):
187 187 child_directory_id = ('hg.dir.%s' % possible_child).replace(
188 188 '-', '_'
189 189 )
190 190 name = possible_child
191 191 else:
192 192 if not possible_child.startswith('%s/' % dir_name):
193 193 continue
194 194 name = possible_child[len(dir_name) + 1 :]
195 195 if '/' in name:
196 196 continue
197 197
198 198 child_directory_id = 'hg.dir.%s' % possible_child.replace(
199 199 '/', '.'
200 200 ).replace('-', '_')
201 201
202 202 directory = doc.createElement('Directory')
203 203 directory.setAttribute('Id', child_directory_id)
204 204 directory.setAttribute('Name', name)
205 205 directory_ref.appendChild(directory)
206 206
207 207 # Add <Component>s for files in this directory.
208 208 for rel, source_path in sorted(entries.items()):
209 209 if dir_name == '.':
210 210 full_rel = rel
211 211 else:
212 212 full_rel = '%s/%s' % (dir_name, rel)
213 213
214 214 component_unique_id = (
215 215 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
216 216 % full_rel
217 217 )
218 218 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
219 219 component_id = 'hg.component.%s' % str(component_guid).replace(
220 220 '-', '_'
221 221 )
222 222
223 223 component = doc.createElement('Component')
224 224
225 225 component.setAttribute('Id', component_id)
226 226 component.setAttribute('Guid', str(component_guid).upper())
227 227 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
228 228
229 229 # Assign this component to a top-level group.
230 230 if dir_name == '.':
231 231 component_groups['ROOT'].append(component_id)
232 232 elif '/' in dir_name:
233 233 component_groups[dir_name[0 : dir_name.index('/')]].append(
234 234 component_id
235 235 )
236 236 else:
237 237 component_groups[dir_name].append(component_id)
238 238
239 239 unique_id = (
240 240 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
241 241 )
242 242 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
243 243
244 244 # IDs have length limits. So use GUID to derive them.
245 245 file_guid_normalized = str(file_guid).replace('-', '_')
246 246 file_id = 'hg.file.%s' % file_guid_normalized
247 247
248 248 file_element = doc.createElement('File')
249 249 file_element.setAttribute('Id', file_id)
250 250 file_element.setAttribute('Source', str(source_path))
251 251 file_element.setAttribute('KeyPath', 'yes')
252 252 file_element.setAttribute('ReadOnly', 'yes')
253 253
254 254 component.appendChild(file_element)
255 255 directory_ref.appendChild(component)
256 256
257 257 fragment.appendChild(directory_ref)
258 258 doc.documentElement.appendChild(fragment)
259 259
260 260 for group, component_ids in sorted(component_groups.items()):
261 261 fragment = doc.createElement('Fragment')
262 262 component_group = doc.createElement('ComponentGroup')
263 263 component_group.setAttribute('Id', 'hg.group.%s' % group)
264 264
265 265 for component_id in component_ids:
266 266 component_ref = doc.createElement('ComponentRef')
267 267 component_ref.setAttribute('Id', component_id)
268 268 component_group.appendChild(component_ref)
269 269
270 270 fragment.appendChild(component_group)
271 271 doc.documentElement.appendChild(fragment)
272 272
273 273 # Add <Shortcut> to files that have it defined.
274 274 for file_id, metadata in sorted(SHORTCUTS.items()):
275 275 els = doc.getElementsByTagName('File')
276 276 els = [el for el in els if el.getAttribute('Id') == file_id]
277 277
278 278 if not els:
279 279 raise Exception('could not find File[Id=%s]' % file_id)
280 280
281 281 for el in els:
282 282 shortcut = doc.createElement('Shortcut')
283 283 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
284 284 shortcut.setAttribute('Directory', 'ProgramMenuDir')
285 285 shortcut.setAttribute('Icon', 'hgIcon.ico')
286 286 shortcut.setAttribute('IconIndex', '0')
287 287 shortcut.setAttribute('Advertise', 'yes')
288 288 for k, v in sorted(metadata.items()):
289 289 shortcut.setAttribute(k, v)
290 290
291 291 el.appendChild(shortcut)
292 292
293 293 return doc.toprettyxml()
294 294
295 295
296 296 def build_installer_py2exe(
297 297 source_dir: pathlib.Path,
298 298 python_exe: pathlib.Path,
299 299 msi_name='mercurial',
300 300 version=None,
301 301 extra_packages_script=None,
302 302 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
303 303 extra_features: typing.Optional[typing.List[str]] = None,
304 304 signing_info: typing.Optional[typing.Dict[str, str]] = None,
305 305 ):
306 306 """Build a WiX MSI installer using py2exe.
307 307
308 308 ``source_dir`` is the path to the Mercurial source tree to use.
309 309 ``arch`` is the target architecture. either ``x86`` or ``x64``.
310 310 ``python_exe`` is the path to the Python executable to use/bundle.
311 311 ``version`` is the Mercurial version string. If not defined,
312 312 ``mercurial/__version__.py`` will be consulted.
313 313 ``extra_packages_script`` is a command to be run to inject extra packages
314 314 into the py2exe binary. It should stage packages into the virtualenv and
315 315 print a null byte followed by a newline-separated list of packages that
316 316 should be included in the exe.
317 317 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
318 318 ``extra_features`` is a list of additional named Features to include in
319 319 the build. These must match Feature names in one of the wxs scripts.
320 320 """
321 321 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
322 322
323 323 hg_build_dir = source_dir / 'build'
324 324
325 325 requirements_txt = (
326 326 source_dir / 'contrib' / 'packaging' / 'requirements-windows-py2.txt'
327 327 )
328 328
329 329 build_py2exe(
330 330 source_dir,
331 331 hg_build_dir,
332 332 python_exe,
333 333 'wix',
334 334 requirements_txt,
335 335 extra_packages=EXTRA_PACKAGES,
336 336 extra_packages_script=extra_packages_script,
337 337 extra_includes=EXTRA_INCLUDES,
338 338 )
339 339
340 340 build_dir = hg_build_dir / ('wix-%s' % arch)
341 341 staging_dir = build_dir / 'stage'
342 342
343 343 build_dir.mkdir(exist_ok=True)
344 344
345 345 # Purge the staging directory for every build so packaging is pristine.
346 346 if staging_dir.exists():
347 347 print('purging %s' % staging_dir)
348 348 shutil.rmtree(staging_dir)
349 349
350 350 stage_install(source_dir, staging_dir, lower_case=True)
351 351
352 352 # We also install some extra files.
353 353 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
354 354
355 355 # And remove some files we don't want.
356 356 for f in STAGING_REMOVE_FILES:
357 357 p = staging_dir / f
358 358 if p.exists():
359 359 print('removing %s' % p)
360 360 p.unlink()
361 361
362 362 return run_wix_packaging(
363 363 source_dir,
364 364 build_dir,
365 365 staging_dir,
366 366 arch,
367 367 version=version,
368 368 python2=True,
369 369 msi_name=msi_name,
370 370 suffix="-python2",
371 371 extra_wxs=extra_wxs,
372 372 extra_features=extra_features,
373 373 signing_info=signing_info,
374 374 )
375 375
376 376
377 377 def build_installer_pyoxidizer(
378 378 source_dir: pathlib.Path,
379 379 target_triple: str,
380 380 msi_name='mercurial',
381 381 version=None,
382 382 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
383 383 extra_features: typing.Optional[typing.List[str]] = None,
384 384 signing_info: typing.Optional[typing.Dict[str, str]] = None,
385 385 ):
386 386 """Build a WiX MSI installer using PyOxidizer."""
387 387 hg_build_dir = source_dir / "build"
388 388 build_dir = hg_build_dir / ("wix-%s" % target_triple)
389 389 staging_dir = build_dir / "stage"
390 390
391 391 arch = "x64" if "x86_64" in target_triple else "x86"
392 392
393 393 build_dir.mkdir(parents=True, exist_ok=True)
394 run_pyoxidizer(source_dir, build_dir, staging_dir, target_triple)
394 create_pyoxidizer_install_layout(
395 source_dir, build_dir, staging_dir, target_triple
396 )
395 397
396 398 # We also install some extra files.
397 399 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
398 400
399 401 # And remove some files we don't want.
400 402 for f in STAGING_REMOVE_FILES:
401 403 p = staging_dir / f
402 404 if p.exists():
403 405 print('removing %s' % p)
404 406 p.unlink()
405 407
406 408 return run_wix_packaging(
407 409 source_dir,
408 410 build_dir,
409 411 staging_dir,
410 412 arch,
411 413 version,
412 414 python2=False,
413 415 msi_name=msi_name,
414 416 extra_wxs=extra_wxs,
415 417 extra_features=extra_features,
416 418 signing_info=signing_info,
417 419 )
418 420
419 421
420 422 def run_wix_packaging(
421 423 source_dir: pathlib.Path,
422 424 build_dir: pathlib.Path,
423 425 staging_dir: pathlib.Path,
424 426 arch: str,
425 427 version: str,
426 428 python2: bool,
427 429 msi_name: typing.Optional[str] = "mercurial",
428 430 suffix: str = "",
429 431 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
430 432 extra_features: typing.Optional[typing.List[str]] = None,
431 433 signing_info: typing.Optional[typing.Dict[str, str]] = None,
432 434 ):
433 435 """Invokes WiX to package up a built Mercurial.
434 436
435 437 ``signing_info`` is a dict defining properties to facilitate signing the
436 438 installer. Recognized keys include ``name``, ``subject_name``,
437 439 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
438 440 we will sign both the hg.exe and the .msi using the signing credentials
439 441 specified.
440 442 """
441 443
442 444 orig_version = version or find_version(source_dir)
443 445 version = normalize_windows_version(orig_version)
444 446 print('using version string: %s' % version)
445 447 if version != orig_version:
446 448 print('(normalized from: %s)' % orig_version)
447 449
448 450 if signing_info:
449 451 sign_with_signtool(
450 452 staging_dir / "hg.exe",
451 453 "%s %s" % (signing_info["name"], version),
452 454 subject_name=signing_info["subject_name"],
453 455 cert_path=signing_info["cert_path"],
454 456 cert_password=signing_info["cert_password"],
455 457 timestamp_url=signing_info["timestamp_url"],
456 458 )
457 459
458 460 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
459 461
460 462 wix_pkg, wix_entry = download_entry('wix', build_dir)
461 463 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
462 464
463 465 if not wix_path.exists():
464 466 extract_zip_to_directory(wix_pkg, wix_path)
465 467
466 468 if python2:
467 469 ensure_vc90_merge_modules(build_dir)
468 470
469 471 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
470 472
471 473 defines = {'Platform': arch}
472 474
473 475 # Derive a .wxs file with the staged files.
474 476 manifest_wxs = build_dir / 'stage.wxs'
475 477 with manifest_wxs.open('w', encoding='utf-8') as fh:
476 478 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
477 479
478 480 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
479 481
480 482 for source, rel_path in sorted((extra_wxs or {}).items()):
481 483 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
482 484
483 485 source = wix_dir / 'mercurial.wxs'
484 486 defines['Version'] = version
485 487 defines['Comments'] = 'Installs Mercurial version %s' % version
486 488
487 489 if python2:
488 490 defines["PythonVersion"] = "2"
489 491 defines['VCRedistSrcDir'] = str(build_dir)
490 492 else:
491 493 defines["PythonVersion"] = "3"
492 494
493 495 if (staging_dir / "lib").exists():
494 496 defines["MercurialHasLib"] = "1"
495 497
496 498 if extra_features:
497 499 assert all(';' not in f for f in extra_features)
498 500 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
499 501
500 502 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
501 503
502 504 msi_path = (
503 505 source_dir
504 506 / 'dist'
505 507 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
506 508 )
507 509
508 510 args = [
509 511 str(wix_path / 'light.exe'),
510 512 '-nologo',
511 513 '-ext',
512 514 'WixUIExtension',
513 515 '-sw1076',
514 516 '-spdb',
515 517 '-o',
516 518 str(msi_path),
517 519 ]
518 520
519 521 for source, rel_path in sorted((extra_wxs or {}).items()):
520 522 assert source.endswith('.wxs')
521 523 source = os.path.basename(source)
522 524 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
523 525
524 526 args.extend(
525 527 [
526 528 str(build_dir / 'stage.wixobj'),
527 529 str(build_dir / 'mercurial.wixobj'),
528 530 ]
529 531 )
530 532
531 533 subprocess.run(args, cwd=str(source_dir), check=True)
532 534
533 535 print('%s created' % msi_path)
534 536
535 537 if signing_info:
536 538 sign_with_signtool(
537 539 msi_path,
538 540 "%s %s" % (signing_info["name"], version),
539 541 subject_name=signing_info["subject_name"],
540 542 cert_path=signing_info["cert_path"],
541 543 cert_password=signing_info["cert_password"],
542 544 timestamp_url=signing_info["timestamp_url"],
543 545 )
544 546
545 547 return {
546 548 'msi_path': msi_path,
547 549 }
General Comments 0
You need to be logged in to leave comments. Login now