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