##// END OF EJS Templates
packaging: rename hgrc.d to defaultrc for Windows config files next to the exe...
Matt Harbison -
r44614:e4344e46 5.3rc1 stable
parent child Browse files
Show More
@@ -1,245 +1,245 b''
1 # py2exe.py - Functionality for performing py2exe builds.
1 # py2exe.py - Functionality for performing py2exe builds.
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import os
10 import os
11 import pathlib
11 import pathlib
12 import subprocess
12 import subprocess
13
13
14 from .downloads import download_entry
14 from .downloads import download_entry
15 from .util import (
15 from .util import (
16 extract_tar_to_directory,
16 extract_tar_to_directory,
17 extract_zip_to_directory,
17 extract_zip_to_directory,
18 process_install_rules,
18 process_install_rules,
19 python_exe_info,
19 python_exe_info,
20 )
20 )
21
21
22
22
23 STAGING_RULES = [
23 STAGING_RULES = [
24 ('contrib/bash_completion', 'Contrib/'),
24 ('contrib/bash_completion', 'Contrib/'),
25 ('contrib/hgk', 'Contrib/hgk.tcl'),
25 ('contrib/hgk', 'Contrib/hgk.tcl'),
26 ('contrib/hgweb.fcgi', 'Contrib/'),
26 ('contrib/hgweb.fcgi', 'Contrib/'),
27 ('contrib/hgweb.wsgi', 'Contrib/'),
27 ('contrib/hgweb.wsgi', 'Contrib/'),
28 ('contrib/logo-droplets.svg', 'Contrib/'),
28 ('contrib/logo-droplets.svg', 'Contrib/'),
29 ('contrib/mercurial.el', 'Contrib/'),
29 ('contrib/mercurial.el', 'Contrib/'),
30 ('contrib/mq.el', 'Contrib/'),
30 ('contrib/mq.el', 'Contrib/'),
31 ('contrib/tcsh_completion', 'Contrib/'),
31 ('contrib/tcsh_completion', 'Contrib/'),
32 ('contrib/tcsh_completion_build.sh', 'Contrib/'),
32 ('contrib/tcsh_completion_build.sh', 'Contrib/'),
33 ('contrib/vim/*', 'Contrib/Vim/'),
33 ('contrib/vim/*', 'Contrib/Vim/'),
34 ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
34 ('contrib/win32/postinstall.txt', 'ReleaseNotes.txt'),
35 ('contrib/win32/ReadMe.html', 'ReadMe.html'),
35 ('contrib/win32/ReadMe.html', 'ReadMe.html'),
36 ('contrib/xml.rnc', 'Contrib/'),
36 ('contrib/xml.rnc', 'Contrib/'),
37 ('contrib/zsh_completion', 'Contrib/'),
37 ('contrib/zsh_completion', 'Contrib/'),
38 ('dist/hg.exe', './'),
38 ('dist/hg.exe', './'),
39 ('dist/lib/*.dll', 'lib/'),
39 ('dist/lib/*.dll', 'lib/'),
40 ('dist/lib/*.pyd', 'lib/'),
40 ('dist/lib/*.pyd', 'lib/'),
41 ('dist/lib/library.zip', 'lib/'),
41 ('dist/lib/library.zip', 'lib/'),
42 ('dist/Microsoft.VC*.CRT.manifest', './'),
42 ('dist/Microsoft.VC*.CRT.manifest', './'),
43 ('dist/msvc*.dll', './'),
43 ('dist/msvc*.dll', './'),
44 ('dist/python*.dll', './'),
44 ('dist/python*.dll', './'),
45 ('doc/*.html', 'doc/'),
45 ('doc/*.html', 'doc/'),
46 ('doc/style.css', 'doc/'),
46 ('doc/style.css', 'doc/'),
47 ('mercurial/helptext/**/*.txt', 'helptext/'),
47 ('mercurial/helptext/**/*.txt', 'helptext/'),
48 ('mercurial/defaultrc/*.rc', 'hgrc.d/'),
48 ('mercurial/defaultrc/*.rc', 'defaultrc/'),
49 ('mercurial/locale/**/*', 'locale/'),
49 ('mercurial/locale/**/*', 'locale/'),
50 ('mercurial/templates/**/*', 'Templates/'),
50 ('mercurial/templates/**/*', 'Templates/'),
51 ('COPYING', 'Copying.txt'),
51 ('COPYING', 'Copying.txt'),
52 ]
52 ]
53
53
54 # List of paths to exclude from the staging area.
54 # List of paths to exclude from the staging area.
55 STAGING_EXCLUDES = [
55 STAGING_EXCLUDES = [
56 'doc/hg-ssh.8.html',
56 'doc/hg-ssh.8.html',
57 ]
57 ]
58
58
59
59
60 def build_py2exe(
60 def build_py2exe(
61 source_dir: pathlib.Path,
61 source_dir: pathlib.Path,
62 build_dir: pathlib.Path,
62 build_dir: pathlib.Path,
63 python_exe: pathlib.Path,
63 python_exe: pathlib.Path,
64 build_name: str,
64 build_name: str,
65 venv_requirements_txt: pathlib.Path,
65 venv_requirements_txt: pathlib.Path,
66 extra_packages=None,
66 extra_packages=None,
67 extra_excludes=None,
67 extra_excludes=None,
68 extra_dll_excludes=None,
68 extra_dll_excludes=None,
69 extra_packages_script=None,
69 extra_packages_script=None,
70 ):
70 ):
71 """Build Mercurial with py2exe.
71 """Build Mercurial with py2exe.
72
72
73 Build files will be placed in ``build_dir``.
73 Build files will be placed in ``build_dir``.
74
74
75 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
75 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
76 for finding the Python 2.7 toolchain. So, we require the environment
76 for finding the Python 2.7 toolchain. So, we require the environment
77 to already be configured with an active toolchain.
77 to already be configured with an active toolchain.
78 """
78 """
79 if 'VCINSTALLDIR' not in os.environ:
79 if 'VCINSTALLDIR' not in os.environ:
80 raise Exception(
80 raise Exception(
81 'not running from a Visual C++ build environment; '
81 'not running from a Visual C++ build environment; '
82 'execute the "Visual C++ <version> Command Prompt" '
82 'execute the "Visual C++ <version> Command Prompt" '
83 'application shortcut or a vcsvarsall.bat file'
83 'application shortcut or a vcsvarsall.bat file'
84 )
84 )
85
85
86 # Identity x86/x64 and validate the environment matches the Python
86 # Identity x86/x64 and validate the environment matches the Python
87 # architecture.
87 # architecture.
88 vc_x64 = r'\x64' in os.environ['LIB']
88 vc_x64 = r'\x64' in os.environ['LIB']
89
89
90 py_info = python_exe_info(python_exe)
90 py_info = python_exe_info(python_exe)
91
91
92 if vc_x64:
92 if vc_x64:
93 if py_info['arch'] != '64bit':
93 if py_info['arch'] != '64bit':
94 raise Exception(
94 raise Exception(
95 'architecture mismatch: Visual C++ environment '
95 'architecture mismatch: Visual C++ environment '
96 'is configured for 64-bit but Python is 32-bit'
96 'is configured for 64-bit but Python is 32-bit'
97 )
97 )
98 else:
98 else:
99 if py_info['arch'] != '32bit':
99 if py_info['arch'] != '32bit':
100 raise Exception(
100 raise Exception(
101 'architecture mismatch: Visual C++ environment '
101 'architecture mismatch: Visual C++ environment '
102 'is configured for 32-bit but Python is 64-bit'
102 'is configured for 32-bit but Python is 64-bit'
103 )
103 )
104
104
105 if py_info['py3']:
105 if py_info['py3']:
106 raise Exception('Only Python 2 is currently supported')
106 raise Exception('Only Python 2 is currently supported')
107
107
108 build_dir.mkdir(exist_ok=True)
108 build_dir.mkdir(exist_ok=True)
109
109
110 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
110 gettext_pkg, gettext_entry = download_entry('gettext', build_dir)
111 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
111 gettext_dep_pkg = download_entry('gettext-dep', build_dir)[0]
112 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
112 virtualenv_pkg, virtualenv_entry = download_entry('virtualenv', build_dir)
113 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
113 py2exe_pkg, py2exe_entry = download_entry('py2exe', build_dir)
114
114
115 venv_path = build_dir / (
115 venv_path = build_dir / (
116 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
116 'venv-%s-%s' % (build_name, 'x64' if vc_x64 else 'x86')
117 )
117 )
118
118
119 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
119 gettext_root = build_dir / ('gettext-win-%s' % gettext_entry['version'])
120
120
121 if not gettext_root.exists():
121 if not gettext_root.exists():
122 extract_zip_to_directory(gettext_pkg, gettext_root)
122 extract_zip_to_directory(gettext_pkg, gettext_root)
123 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
123 extract_zip_to_directory(gettext_dep_pkg, gettext_root)
124
124
125 # This assumes Python 2. We don't need virtualenv on Python 3.
125 # This assumes Python 2. We don't need virtualenv on Python 3.
126 virtualenv_src_path = build_dir / (
126 virtualenv_src_path = build_dir / (
127 'virtualenv-%s' % virtualenv_entry['version']
127 'virtualenv-%s' % virtualenv_entry['version']
128 )
128 )
129 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
129 virtualenv_py = virtualenv_src_path / 'virtualenv.py'
130
130
131 if not virtualenv_src_path.exists():
131 if not virtualenv_src_path.exists():
132 extract_tar_to_directory(virtualenv_pkg, build_dir)
132 extract_tar_to_directory(virtualenv_pkg, build_dir)
133
133
134 py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version'])
134 py2exe_source_path = build_dir / ('py2exe-%s' % py2exe_entry['version'])
135
135
136 if not py2exe_source_path.exists():
136 if not py2exe_source_path.exists():
137 extract_zip_to_directory(py2exe_pkg, build_dir)
137 extract_zip_to_directory(py2exe_pkg, build_dir)
138
138
139 if not venv_path.exists():
139 if not venv_path.exists():
140 print('creating virtualenv with dependencies')
140 print('creating virtualenv with dependencies')
141 subprocess.run(
141 subprocess.run(
142 [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
142 [str(python_exe), str(virtualenv_py), str(venv_path)], check=True
143 )
143 )
144
144
145 venv_python = venv_path / 'Scripts' / 'python.exe'
145 venv_python = venv_path / 'Scripts' / 'python.exe'
146 venv_pip = venv_path / 'Scripts' / 'pip.exe'
146 venv_pip = venv_path / 'Scripts' / 'pip.exe'
147
147
148 subprocess.run(
148 subprocess.run(
149 [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
149 [str(venv_pip), 'install', '-r', str(venv_requirements_txt)], check=True
150 )
150 )
151
151
152 # Force distutils to use VC++ settings from environment, which was
152 # Force distutils to use VC++ settings from environment, which was
153 # validated above.
153 # validated above.
154 env = dict(os.environ)
154 env = dict(os.environ)
155 env['DISTUTILS_USE_SDK'] = '1'
155 env['DISTUTILS_USE_SDK'] = '1'
156 env['MSSdk'] = '1'
156 env['MSSdk'] = '1'
157
157
158 if extra_packages_script:
158 if extra_packages_script:
159 more_packages = set(
159 more_packages = set(
160 subprocess.check_output(extra_packages_script, cwd=build_dir)
160 subprocess.check_output(extra_packages_script, cwd=build_dir)
161 .split(b'\0')[-1]
161 .split(b'\0')[-1]
162 .strip()
162 .strip()
163 .decode('utf-8')
163 .decode('utf-8')
164 .splitlines()
164 .splitlines()
165 )
165 )
166 if more_packages:
166 if more_packages:
167 if not extra_packages:
167 if not extra_packages:
168 extra_packages = more_packages
168 extra_packages = more_packages
169 else:
169 else:
170 extra_packages |= more_packages
170 extra_packages |= more_packages
171
171
172 if extra_packages:
172 if extra_packages:
173 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
173 env['HG_PY2EXE_EXTRA_PACKAGES'] = ' '.join(sorted(extra_packages))
174 hgext3rd_extras = sorted(
174 hgext3rd_extras = sorted(
175 e for e in extra_packages if e.startswith('hgext3rd.')
175 e for e in extra_packages if e.startswith('hgext3rd.')
176 )
176 )
177 if hgext3rd_extras:
177 if hgext3rd_extras:
178 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
178 env['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'] = ' '.join(hgext3rd_extras)
179 if extra_excludes:
179 if extra_excludes:
180 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
180 env['HG_PY2EXE_EXTRA_EXCLUDES'] = ' '.join(sorted(extra_excludes))
181 if extra_dll_excludes:
181 if extra_dll_excludes:
182 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
182 env['HG_PY2EXE_EXTRA_DLL_EXCLUDES'] = ' '.join(
183 sorted(extra_dll_excludes)
183 sorted(extra_dll_excludes)
184 )
184 )
185
185
186 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
186 py2exe_py_path = venv_path / 'Lib' / 'site-packages' / 'py2exe'
187 if not py2exe_py_path.exists():
187 if not py2exe_py_path.exists():
188 print('building py2exe')
188 print('building py2exe')
189 subprocess.run(
189 subprocess.run(
190 [str(venv_python), 'setup.py', 'install'],
190 [str(venv_python), 'setup.py', 'install'],
191 cwd=py2exe_source_path,
191 cwd=py2exe_source_path,
192 env=env,
192 env=env,
193 check=True,
193 check=True,
194 )
194 )
195
195
196 # Register location of msgfmt and other binaries.
196 # Register location of msgfmt and other binaries.
197 env['PATH'] = '%s%s%s' % (
197 env['PATH'] = '%s%s%s' % (
198 env['PATH'],
198 env['PATH'],
199 os.pathsep,
199 os.pathsep,
200 str(gettext_root / 'bin'),
200 str(gettext_root / 'bin'),
201 )
201 )
202
202
203 print('building Mercurial')
203 print('building Mercurial')
204 subprocess.run(
204 subprocess.run(
205 [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
205 [str(venv_python), 'setup.py', 'py2exe', 'build_doc', '--html'],
206 cwd=str(source_dir),
206 cwd=str(source_dir),
207 env=env,
207 env=env,
208 check=True,
208 check=True,
209 )
209 )
210
210
211
211
212 def stage_install(
212 def stage_install(
213 source_dir: pathlib.Path, staging_dir: pathlib.Path, lower_case=False
213 source_dir: pathlib.Path, staging_dir: pathlib.Path, lower_case=False
214 ):
214 ):
215 """Copy all files to be installed to a directory.
215 """Copy all files to be installed to a directory.
216
216
217 This allows packaging to simply walk a directory tree to find source
217 This allows packaging to simply walk a directory tree to find source
218 files.
218 files.
219 """
219 """
220 if lower_case:
220 if lower_case:
221 rules = []
221 rules = []
222 for source, dest in STAGING_RULES:
222 for source, dest in STAGING_RULES:
223 # Only lower directory names.
223 # Only lower directory names.
224 if '/' in dest:
224 if '/' in dest:
225 parent, leaf = dest.rsplit('/', 1)
225 parent, leaf = dest.rsplit('/', 1)
226 dest = '%s/%s' % (parent.lower(), leaf)
226 dest = '%s/%s' % (parent.lower(), leaf)
227 rules.append((source, dest))
227 rules.append((source, dest))
228 else:
228 else:
229 rules = STAGING_RULES
229 rules = STAGING_RULES
230
230
231 process_install_rules(rules, source_dir, staging_dir)
231 process_install_rules(rules, source_dir, staging_dir)
232
232
233 # Write out a default editor.rc file to configure notepad as the
233 # Write out a default editor.rc file to configure notepad as the
234 # default editor.
234 # default editor.
235 with (staging_dir / 'hgrc.d' / 'editor.rc').open(
235 with (staging_dir / 'defaultrc' / 'editor.rc').open(
236 'w', encoding='utf-8'
236 'w', encoding='utf-8'
237 ) as fh:
237 ) as fh:
238 fh.write('[ui]\neditor = notepad\n')
238 fh.write('[ui]\neditor = notepad\n')
239
239
240 # Purge any files we don't want to be there.
240 # Purge any files we don't want to be there.
241 for f in STAGING_EXCLUDES:
241 for f in STAGING_EXCLUDES:
242 p = staging_dir / f
242 p = staging_dir / f
243 if p.exists():
243 if p.exists():
244 print('removing %s' % p)
244 print('removing %s' % p)
245 p.unlink()
245 p.unlink()
@@ -1,518 +1,518 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 process_install_rules,
27 process_install_rules,
28 sign_with_signtool,
28 sign_with_signtool,
29 )
29 )
30
30
31
31
32 EXTRA_PACKAGES = {
32 EXTRA_PACKAGES = {
33 'distutils',
33 'distutils',
34 'pygments',
34 'pygments',
35 }
35 }
36
36
37
37
38 EXTRA_INSTALL_RULES = [
38 EXTRA_INSTALL_RULES = [
39 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
39 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
40 ('contrib/win32/mercurial.ini', 'hgrc.d/mercurial.rc'),
40 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
41 ]
41 ]
42
42
43 STAGING_REMOVE_FILES = [
43 STAGING_REMOVE_FILES = [
44 # We use the RTF variant.
44 # We use the RTF variant.
45 'copying.txt',
45 'copying.txt',
46 ]
46 ]
47
47
48 SHORTCUTS = {
48 SHORTCUTS = {
49 # hg.1.html'
49 # hg.1.html'
50 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
50 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
51 'Name': 'Mercurial Command Reference',
51 'Name': 'Mercurial Command Reference',
52 },
52 },
53 # hgignore.5.html
53 # hgignore.5.html
54 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
54 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
55 'Name': 'Mercurial Ignore Files',
55 'Name': 'Mercurial Ignore Files',
56 },
56 },
57 # hgrc.5.html
57 # hgrc.5.html
58 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
58 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
59 'Name': 'Mercurial Configuration Files',
59 'Name': 'Mercurial Configuration Files',
60 },
60 },
61 }
61 }
62
62
63
63
64 def find_version(source_dir: pathlib.Path):
64 def find_version(source_dir: pathlib.Path):
65 version_py = source_dir / 'mercurial' / '__version__.py'
65 version_py = source_dir / 'mercurial' / '__version__.py'
66
66
67 with version_py.open('r', encoding='utf-8') as fh:
67 with version_py.open('r', encoding='utf-8') as fh:
68 source = fh.read().strip()
68 source = fh.read().strip()
69
69
70 m = re.search('version = b"(.*)"', source)
70 m = re.search('version = b"(.*)"', source)
71 return m.group(1)
71 return m.group(1)
72
72
73
73
74 def normalize_version(version):
74 def normalize_version(version):
75 """Normalize Mercurial version string so WiX accepts it.
75 """Normalize Mercurial version string so WiX accepts it.
76
76
77 Version strings have to be numeric X.Y.Z.
77 Version strings have to be numeric X.Y.Z.
78 """
78 """
79
79
80 if '+' in version:
80 if '+' in version:
81 version, extra = version.split('+', 1)
81 version, extra = version.split('+', 1)
82 else:
82 else:
83 extra = None
83 extra = None
84
84
85 # 4.9rc0
85 # 4.9rc0
86 if version[:-1].endswith('rc'):
86 if version[:-1].endswith('rc'):
87 version = version[:-3]
87 version = version[:-3]
88
88
89 versions = [int(v) for v in version.split('.')]
89 versions = [int(v) for v in version.split('.')]
90 while len(versions) < 3:
90 while len(versions) < 3:
91 versions.append(0)
91 versions.append(0)
92
92
93 major, minor, build = versions[:3]
93 major, minor, build = versions[:3]
94
94
95 if extra:
95 if extra:
96 # <commit count>-<hash>+<date>
96 # <commit count>-<hash>+<date>
97 build = int(extra.split('-')[0])
97 build = int(extra.split('-')[0])
98
98
99 return '.'.join('%d' % x for x in (major, minor, build))
99 return '.'.join('%d' % x for x in (major, minor, build))
100
100
101
101
102 def ensure_vc90_merge_modules(build_dir):
102 def ensure_vc90_merge_modules(build_dir):
103 x86 = (
103 x86 = (
104 download_entry(
104 download_entry(
105 'vc9-crt-x86-msm',
105 'vc9-crt-x86-msm',
106 build_dir,
106 build_dir,
107 local_name='microsoft.vcxx.crt.x86_msm.msm',
107 local_name='microsoft.vcxx.crt.x86_msm.msm',
108 )[0],
108 )[0],
109 download_entry(
109 download_entry(
110 'vc9-crt-x86-msm-policy',
110 'vc9-crt-x86-msm-policy',
111 build_dir,
111 build_dir,
112 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
112 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
113 )[0],
113 )[0],
114 )
114 )
115
115
116 x64 = (
116 x64 = (
117 download_entry(
117 download_entry(
118 'vc9-crt-x64-msm',
118 'vc9-crt-x64-msm',
119 build_dir,
119 build_dir,
120 local_name='microsoft.vcxx.crt.x64_msm.msm',
120 local_name='microsoft.vcxx.crt.x64_msm.msm',
121 )[0],
121 )[0],
122 download_entry(
122 download_entry(
123 'vc9-crt-x64-msm-policy',
123 'vc9-crt-x64-msm-policy',
124 build_dir,
124 build_dir,
125 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
125 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
126 )[0],
126 )[0],
127 )
127 )
128 return {
128 return {
129 'x86': x86,
129 'x86': x86,
130 'x64': x64,
130 'x64': x64,
131 }
131 }
132
132
133
133
134 def run_candle(wix, cwd, wxs, source_dir, defines=None):
134 def run_candle(wix, cwd, wxs, source_dir, defines=None):
135 args = [
135 args = [
136 str(wix / 'candle.exe'),
136 str(wix / 'candle.exe'),
137 '-nologo',
137 '-nologo',
138 str(wxs),
138 str(wxs),
139 '-dSourceDir=%s' % source_dir,
139 '-dSourceDir=%s' % source_dir,
140 ]
140 ]
141
141
142 if defines:
142 if defines:
143 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
143 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
144
144
145 subprocess.run(args, cwd=str(cwd), check=True)
145 subprocess.run(args, cwd=str(cwd), check=True)
146
146
147
147
148 def make_post_build_signing_fn(
148 def make_post_build_signing_fn(
149 name,
149 name,
150 subject_name=None,
150 subject_name=None,
151 cert_path=None,
151 cert_path=None,
152 cert_password=None,
152 cert_password=None,
153 timestamp_url=None,
153 timestamp_url=None,
154 ):
154 ):
155 """Create a callable that will use signtool to sign hg.exe."""
155 """Create a callable that will use signtool to sign hg.exe."""
156
156
157 def post_build_sign(source_dir, build_dir, dist_dir, version):
157 def post_build_sign(source_dir, build_dir, dist_dir, version):
158 description = '%s %s' % (name, version)
158 description = '%s %s' % (name, version)
159
159
160 sign_with_signtool(
160 sign_with_signtool(
161 dist_dir / 'hg.exe',
161 dist_dir / 'hg.exe',
162 description,
162 description,
163 subject_name=subject_name,
163 subject_name=subject_name,
164 cert_path=cert_path,
164 cert_path=cert_path,
165 cert_password=cert_password,
165 cert_password=cert_password,
166 timestamp_url=timestamp_url,
166 timestamp_url=timestamp_url,
167 )
167 )
168
168
169 return post_build_sign
169 return post_build_sign
170
170
171
171
172 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
172 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
173 """Create XML string listing every file to be installed."""
173 """Create XML string listing every file to be installed."""
174
174
175 # We derive GUIDs from a deterministic file path identifier.
175 # We derive GUIDs from a deterministic file path identifier.
176 # We shoehorn the name into something that looks like a URL because
176 # We shoehorn the name into something that looks like a URL because
177 # the UUID namespaces are supposed to work that way (even though
177 # the UUID namespaces are supposed to work that way (even though
178 # the input data probably is never validated).
178 # the input data probably is never validated).
179
179
180 doc = xml.dom.minidom.parseString(
180 doc = xml.dom.minidom.parseString(
181 '<?xml version="1.0" encoding="utf-8"?>'
181 '<?xml version="1.0" encoding="utf-8"?>'
182 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
182 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
183 '</Wix>'
183 '</Wix>'
184 )
184 )
185
185
186 # Assemble the install layout by directory. This makes it easier to
186 # Assemble the install layout by directory. This makes it easier to
187 # emit XML, since each directory has separate entities.
187 # emit XML, since each directory has separate entities.
188 manifest = collections.defaultdict(dict)
188 manifest = collections.defaultdict(dict)
189
189
190 for root, dirs, files in os.walk(staging_dir):
190 for root, dirs, files in os.walk(staging_dir):
191 dirs.sort()
191 dirs.sort()
192
192
193 root = pathlib.Path(root)
193 root = pathlib.Path(root)
194 rel_dir = root.relative_to(staging_dir)
194 rel_dir = root.relative_to(staging_dir)
195
195
196 for i in range(len(rel_dir.parts)):
196 for i in range(len(rel_dir.parts)):
197 parent = '/'.join(rel_dir.parts[0 : i + 1])
197 parent = '/'.join(rel_dir.parts[0 : i + 1])
198 manifest.setdefault(parent, {})
198 manifest.setdefault(parent, {})
199
199
200 for f in sorted(files):
200 for f in sorted(files):
201 full = root / f
201 full = root / f
202 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
202 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
203
203
204 component_groups = collections.defaultdict(list)
204 component_groups = collections.defaultdict(list)
205
205
206 # Now emit a <Fragment> for each directory.
206 # Now emit a <Fragment> for each directory.
207 # Each directory is composed of a <DirectoryRef> pointing to its parent
207 # Each directory is composed of a <DirectoryRef> pointing to its parent
208 # and defines child <Directory>'s and a <Component> with all the files.
208 # and defines child <Directory>'s and a <Component> with all the files.
209 for dir_name, entries in sorted(manifest.items()):
209 for dir_name, entries in sorted(manifest.items()):
210 # The directory id is derived from the path. But the root directory
210 # The directory id is derived from the path. But the root directory
211 # is special.
211 # is special.
212 if dir_name == '.':
212 if dir_name == '.':
213 parent_directory_id = 'INSTALLDIR'
213 parent_directory_id = 'INSTALLDIR'
214 else:
214 else:
215 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
215 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
216
216
217 fragment = doc.createElement('Fragment')
217 fragment = doc.createElement('Fragment')
218 directory_ref = doc.createElement('DirectoryRef')
218 directory_ref = doc.createElement('DirectoryRef')
219 directory_ref.setAttribute('Id', parent_directory_id)
219 directory_ref.setAttribute('Id', parent_directory_id)
220
220
221 # Add <Directory> entries for immediate children directories.
221 # Add <Directory> entries for immediate children directories.
222 for possible_child in sorted(manifest.keys()):
222 for possible_child in sorted(manifest.keys()):
223 if (
223 if (
224 dir_name == '.'
224 dir_name == '.'
225 and '/' not in possible_child
225 and '/' not in possible_child
226 and possible_child != '.'
226 and possible_child != '.'
227 ):
227 ):
228 child_directory_id = 'hg.dir.%s' % possible_child
228 child_directory_id = 'hg.dir.%s' % possible_child
229 name = possible_child
229 name = possible_child
230 else:
230 else:
231 if not possible_child.startswith('%s/' % dir_name):
231 if not possible_child.startswith('%s/' % dir_name):
232 continue
232 continue
233 name = possible_child[len(dir_name) + 1 :]
233 name = possible_child[len(dir_name) + 1 :]
234 if '/' in name:
234 if '/' in name:
235 continue
235 continue
236
236
237 child_directory_id = 'hg.dir.%s' % possible_child.replace(
237 child_directory_id = 'hg.dir.%s' % possible_child.replace(
238 '/', '.'
238 '/', '.'
239 )
239 )
240
240
241 directory = doc.createElement('Directory')
241 directory = doc.createElement('Directory')
242 directory.setAttribute('Id', child_directory_id)
242 directory.setAttribute('Id', child_directory_id)
243 directory.setAttribute('Name', name)
243 directory.setAttribute('Name', name)
244 directory_ref.appendChild(directory)
244 directory_ref.appendChild(directory)
245
245
246 # Add <Component>s for files in this directory.
246 # Add <Component>s for files in this directory.
247 for rel, source_path in sorted(entries.items()):
247 for rel, source_path in sorted(entries.items()):
248 if dir_name == '.':
248 if dir_name == '.':
249 full_rel = rel
249 full_rel = rel
250 else:
250 else:
251 full_rel = '%s/%s' % (dir_name, rel)
251 full_rel = '%s/%s' % (dir_name, rel)
252
252
253 component_unique_id = (
253 component_unique_id = (
254 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
254 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
255 % full_rel
255 % full_rel
256 )
256 )
257 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
257 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
258 component_id = 'hg.component.%s' % str(component_guid).replace(
258 component_id = 'hg.component.%s' % str(component_guid).replace(
259 '-', '_'
259 '-', '_'
260 )
260 )
261
261
262 component = doc.createElement('Component')
262 component = doc.createElement('Component')
263
263
264 component.setAttribute('Id', component_id)
264 component.setAttribute('Id', component_id)
265 component.setAttribute('Guid', str(component_guid).upper())
265 component.setAttribute('Guid', str(component_guid).upper())
266 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
266 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
267
267
268 # Assign this component to a top-level group.
268 # Assign this component to a top-level group.
269 if dir_name == '.':
269 if dir_name == '.':
270 component_groups['ROOT'].append(component_id)
270 component_groups['ROOT'].append(component_id)
271 elif '/' in dir_name:
271 elif '/' in dir_name:
272 component_groups[dir_name[0 : dir_name.index('/')]].append(
272 component_groups[dir_name[0 : dir_name.index('/')]].append(
273 component_id
273 component_id
274 )
274 )
275 else:
275 else:
276 component_groups[dir_name].append(component_id)
276 component_groups[dir_name].append(component_id)
277
277
278 unique_id = (
278 unique_id = (
279 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
279 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
280 )
280 )
281 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
281 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
282
282
283 # IDs have length limits. So use GUID to derive them.
283 # IDs have length limits. So use GUID to derive them.
284 file_guid_normalized = str(file_guid).replace('-', '_')
284 file_guid_normalized = str(file_guid).replace('-', '_')
285 file_id = 'hg.file.%s' % file_guid_normalized
285 file_id = 'hg.file.%s' % file_guid_normalized
286
286
287 file_element = doc.createElement('File')
287 file_element = doc.createElement('File')
288 file_element.setAttribute('Id', file_id)
288 file_element.setAttribute('Id', file_id)
289 file_element.setAttribute('Source', str(source_path))
289 file_element.setAttribute('Source', str(source_path))
290 file_element.setAttribute('KeyPath', 'yes')
290 file_element.setAttribute('KeyPath', 'yes')
291 file_element.setAttribute('ReadOnly', 'yes')
291 file_element.setAttribute('ReadOnly', 'yes')
292
292
293 component.appendChild(file_element)
293 component.appendChild(file_element)
294 directory_ref.appendChild(component)
294 directory_ref.appendChild(component)
295
295
296 fragment.appendChild(directory_ref)
296 fragment.appendChild(directory_ref)
297 doc.documentElement.appendChild(fragment)
297 doc.documentElement.appendChild(fragment)
298
298
299 for group, component_ids in sorted(component_groups.items()):
299 for group, component_ids in sorted(component_groups.items()):
300 fragment = doc.createElement('Fragment')
300 fragment = doc.createElement('Fragment')
301 component_group = doc.createElement('ComponentGroup')
301 component_group = doc.createElement('ComponentGroup')
302 component_group.setAttribute('Id', 'hg.group.%s' % group)
302 component_group.setAttribute('Id', 'hg.group.%s' % group)
303
303
304 for component_id in component_ids:
304 for component_id in component_ids:
305 component_ref = doc.createElement('ComponentRef')
305 component_ref = doc.createElement('ComponentRef')
306 component_ref.setAttribute('Id', component_id)
306 component_ref.setAttribute('Id', component_id)
307 component_group.appendChild(component_ref)
307 component_group.appendChild(component_ref)
308
308
309 fragment.appendChild(component_group)
309 fragment.appendChild(component_group)
310 doc.documentElement.appendChild(fragment)
310 doc.documentElement.appendChild(fragment)
311
311
312 # Add <Shortcut> to files that have it defined.
312 # Add <Shortcut> to files that have it defined.
313 for file_id, metadata in sorted(SHORTCUTS.items()):
313 for file_id, metadata in sorted(SHORTCUTS.items()):
314 els = doc.getElementsByTagName('File')
314 els = doc.getElementsByTagName('File')
315 els = [el for el in els if el.getAttribute('Id') == file_id]
315 els = [el for el in els if el.getAttribute('Id') == file_id]
316
316
317 if not els:
317 if not els:
318 raise Exception('could not find File[Id=%s]' % file_id)
318 raise Exception('could not find File[Id=%s]' % file_id)
319
319
320 for el in els:
320 for el in els:
321 shortcut = doc.createElement('Shortcut')
321 shortcut = doc.createElement('Shortcut')
322 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
322 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
323 shortcut.setAttribute('Directory', 'ProgramMenuDir')
323 shortcut.setAttribute('Directory', 'ProgramMenuDir')
324 shortcut.setAttribute('Icon', 'hgIcon.ico')
324 shortcut.setAttribute('Icon', 'hgIcon.ico')
325 shortcut.setAttribute('IconIndex', '0')
325 shortcut.setAttribute('IconIndex', '0')
326 shortcut.setAttribute('Advertise', 'yes')
326 shortcut.setAttribute('Advertise', 'yes')
327 for k, v in sorted(metadata.items()):
327 for k, v in sorted(metadata.items()):
328 shortcut.setAttribute(k, v)
328 shortcut.setAttribute(k, v)
329
329
330 el.appendChild(shortcut)
330 el.appendChild(shortcut)
331
331
332 return doc.toprettyxml()
332 return doc.toprettyxml()
333
333
334
334
335 def build_installer(
335 def build_installer(
336 source_dir: pathlib.Path,
336 source_dir: pathlib.Path,
337 python_exe: pathlib.Path,
337 python_exe: pathlib.Path,
338 msi_name='mercurial',
338 msi_name='mercurial',
339 version=None,
339 version=None,
340 post_build_fn=None,
340 post_build_fn=None,
341 extra_packages_script=None,
341 extra_packages_script=None,
342 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
342 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
343 extra_features: typing.Optional[typing.List[str]] = None,
343 extra_features: typing.Optional[typing.List[str]] = None,
344 ):
344 ):
345 """Build a WiX MSI installer.
345 """Build a WiX MSI installer.
346
346
347 ``source_dir`` is the path to the Mercurial source tree to use.
347 ``source_dir`` is the path to the Mercurial source tree to use.
348 ``arch`` is the target architecture. either ``x86`` or ``x64``.
348 ``arch`` is the target architecture. either ``x86`` or ``x64``.
349 ``python_exe`` is the path to the Python executable to use/bundle.
349 ``python_exe`` is the path to the Python executable to use/bundle.
350 ``version`` is the Mercurial version string. If not defined,
350 ``version`` is the Mercurial version string. If not defined,
351 ``mercurial/__version__.py`` will be consulted.
351 ``mercurial/__version__.py`` will be consulted.
352 ``post_build_fn`` is a callable that will be called after building
352 ``post_build_fn`` is a callable that will be called after building
353 Mercurial but before invoking WiX. It can be used to e.g. facilitate
353 Mercurial but before invoking WiX. It can be used to e.g. facilitate
354 signing. It is passed the paths to the Mercurial source, build, and
354 signing. It is passed the paths to the Mercurial source, build, and
355 dist directories and the resolved Mercurial version.
355 dist directories and the resolved Mercurial version.
356 ``extra_packages_script`` is a command to be run to inject extra packages
356 ``extra_packages_script`` is a command to be run to inject extra packages
357 into the py2exe binary. It should stage packages into the virtualenv and
357 into the py2exe binary. It should stage packages into the virtualenv and
358 print a null byte followed by a newline-separated list of packages that
358 print a null byte followed by a newline-separated list of packages that
359 should be included in the exe.
359 should be included in the exe.
360 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
360 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
361 ``extra_features`` is a list of additional named Features to include in
361 ``extra_features`` is a list of additional named Features to include in
362 the build. These must match Feature names in one of the wxs scripts.
362 the build. These must match Feature names in one of the wxs scripts.
363 """
363 """
364 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
364 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
365
365
366 hg_build_dir = source_dir / 'build'
366 hg_build_dir = source_dir / 'build'
367 dist_dir = source_dir / 'dist'
367 dist_dir = source_dir / 'dist'
368 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
368 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
369
369
370 requirements_txt = wix_dir / 'requirements.txt'
370 requirements_txt = wix_dir / 'requirements.txt'
371
371
372 build_py2exe(
372 build_py2exe(
373 source_dir,
373 source_dir,
374 hg_build_dir,
374 hg_build_dir,
375 python_exe,
375 python_exe,
376 'wix',
376 'wix',
377 requirements_txt,
377 requirements_txt,
378 extra_packages=EXTRA_PACKAGES,
378 extra_packages=EXTRA_PACKAGES,
379 extra_packages_script=extra_packages_script,
379 extra_packages_script=extra_packages_script,
380 )
380 )
381
381
382 version = version or normalize_version(find_version(source_dir))
382 version = version or normalize_version(find_version(source_dir))
383 print('using version string: %s' % version)
383 print('using version string: %s' % version)
384
384
385 if post_build_fn:
385 if post_build_fn:
386 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
386 post_build_fn(source_dir, hg_build_dir, dist_dir, version)
387
387
388 build_dir = hg_build_dir / ('wix-%s' % arch)
388 build_dir = hg_build_dir / ('wix-%s' % arch)
389 staging_dir = build_dir / 'stage'
389 staging_dir = build_dir / 'stage'
390
390
391 build_dir.mkdir(exist_ok=True)
391 build_dir.mkdir(exist_ok=True)
392
392
393 # Purge the staging directory for every build so packaging is pristine.
393 # Purge the staging directory for every build so packaging is pristine.
394 if staging_dir.exists():
394 if staging_dir.exists():
395 print('purging %s' % staging_dir)
395 print('purging %s' % staging_dir)
396 shutil.rmtree(staging_dir)
396 shutil.rmtree(staging_dir)
397
397
398 stage_install(source_dir, staging_dir, lower_case=True)
398 stage_install(source_dir, staging_dir, lower_case=True)
399
399
400 # We also install some extra files.
400 # We also install some extra files.
401 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
401 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
402
402
403 # And remove some files we don't want.
403 # And remove some files we don't want.
404 for f in STAGING_REMOVE_FILES:
404 for f in STAGING_REMOVE_FILES:
405 p = staging_dir / f
405 p = staging_dir / f
406 if p.exists():
406 if p.exists():
407 print('removing %s' % p)
407 print('removing %s' % p)
408 p.unlink()
408 p.unlink()
409
409
410 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
410 wix_pkg, wix_entry = download_entry('wix', hg_build_dir)
411 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
411 wix_path = hg_build_dir / ('wix-%s' % wix_entry['version'])
412
412
413 if not wix_path.exists():
413 if not wix_path.exists():
414 extract_zip_to_directory(wix_pkg, wix_path)
414 extract_zip_to_directory(wix_pkg, wix_path)
415
415
416 ensure_vc90_merge_modules(hg_build_dir)
416 ensure_vc90_merge_modules(hg_build_dir)
417
417
418 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
418 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
419
419
420 defines = {'Platform': arch}
420 defines = {'Platform': arch}
421
421
422 # Derive a .wxs file with the staged files.
422 # Derive a .wxs file with the staged files.
423 manifest_wxs = build_dir / 'stage.wxs'
423 manifest_wxs = build_dir / 'stage.wxs'
424 with manifest_wxs.open('w', encoding='utf-8') as fh:
424 with manifest_wxs.open('w', encoding='utf-8') as fh:
425 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
425 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
426
426
427 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
427 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
428
428
429 for source, rel_path in sorted((extra_wxs or {}).items()):
429 for source, rel_path in sorted((extra_wxs or {}).items()):
430 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
430 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
431
431
432 source = wix_dir / 'mercurial.wxs'
432 source = wix_dir / 'mercurial.wxs'
433 defines['Version'] = version
433 defines['Version'] = version
434 defines['Comments'] = 'Installs Mercurial version %s' % version
434 defines['Comments'] = 'Installs Mercurial version %s' % version
435 defines['VCRedistSrcDir'] = str(hg_build_dir)
435 defines['VCRedistSrcDir'] = str(hg_build_dir)
436 if extra_features:
436 if extra_features:
437 assert all(';' not in f for f in extra_features)
437 assert all(';' not in f for f in extra_features)
438 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
438 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
439
439
440 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
440 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
441
441
442 msi_path = (
442 msi_path = (
443 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, version, arch))
443 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, version, arch))
444 )
444 )
445
445
446 args = [
446 args = [
447 str(wix_path / 'light.exe'),
447 str(wix_path / 'light.exe'),
448 '-nologo',
448 '-nologo',
449 '-ext',
449 '-ext',
450 'WixUIExtension',
450 'WixUIExtension',
451 '-sw1076',
451 '-sw1076',
452 '-spdb',
452 '-spdb',
453 '-o',
453 '-o',
454 str(msi_path),
454 str(msi_path),
455 ]
455 ]
456
456
457 for source, rel_path in sorted((extra_wxs or {}).items()):
457 for source, rel_path in sorted((extra_wxs or {}).items()):
458 assert source.endswith('.wxs')
458 assert source.endswith('.wxs')
459 source = os.path.basename(source)
459 source = os.path.basename(source)
460 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
460 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
461
461
462 args.extend(
462 args.extend(
463 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
463 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
464 )
464 )
465
465
466 subprocess.run(args, cwd=str(source_dir), check=True)
466 subprocess.run(args, cwd=str(source_dir), check=True)
467
467
468 print('%s created' % msi_path)
468 print('%s created' % msi_path)
469
469
470 return {
470 return {
471 'msi_path': msi_path,
471 'msi_path': msi_path,
472 }
472 }
473
473
474
474
475 def build_signed_installer(
475 def build_signed_installer(
476 source_dir: pathlib.Path,
476 source_dir: pathlib.Path,
477 python_exe: pathlib.Path,
477 python_exe: pathlib.Path,
478 name: str,
478 name: str,
479 version=None,
479 version=None,
480 subject_name=None,
480 subject_name=None,
481 cert_path=None,
481 cert_path=None,
482 cert_password=None,
482 cert_password=None,
483 timestamp_url=None,
483 timestamp_url=None,
484 extra_packages_script=None,
484 extra_packages_script=None,
485 extra_wxs=None,
485 extra_wxs=None,
486 extra_features=None,
486 extra_features=None,
487 ):
487 ):
488 """Build an installer with signed executables."""
488 """Build an installer with signed executables."""
489
489
490 post_build_fn = make_post_build_signing_fn(
490 post_build_fn = make_post_build_signing_fn(
491 name,
491 name,
492 subject_name=subject_name,
492 subject_name=subject_name,
493 cert_path=cert_path,
493 cert_path=cert_path,
494 cert_password=cert_password,
494 cert_password=cert_password,
495 timestamp_url=timestamp_url,
495 timestamp_url=timestamp_url,
496 )
496 )
497
497
498 info = build_installer(
498 info = build_installer(
499 source_dir,
499 source_dir,
500 python_exe=python_exe,
500 python_exe=python_exe,
501 msi_name=name.lower(),
501 msi_name=name.lower(),
502 version=version,
502 version=version,
503 post_build_fn=post_build_fn,
503 post_build_fn=post_build_fn,
504 extra_packages_script=extra_packages_script,
504 extra_packages_script=extra_packages_script,
505 extra_wxs=extra_wxs,
505 extra_wxs=extra_wxs,
506 extra_features=extra_features,
506 extra_features=extra_features,
507 )
507 )
508
508
509 description = '%s %s' % (name, version)
509 description = '%s %s' % (name, version)
510
510
511 sign_with_signtool(
511 sign_with_signtool(
512 info['msi_path'],
512 info['msi_path'],
513 description,
513 description,
514 subject_name=subject_name,
514 subject_name=subject_name,
515 cert_path=cert_path,
515 cert_path=cert_path,
516 cert_password=cert_password,
516 cert_password=cert_password,
517 timestamp_url=timestamp_url,
517 timestamp_url=timestamp_url,
518 )
518 )
@@ -1,82 +1,82 b''
1 ; Script generated by the Inno Setup Script Wizard.
1 ; Script generated by the Inno Setup Script Wizard.
2 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
2 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3
3
4 #ifndef ARCH
4 #ifndef ARCH
5 #define ARCH = "x86"
5 #define ARCH = "x86"
6 #endif
6 #endif
7
7
8 [Setup]
8 [Setup]
9 AppCopyright=Copyright 2005-2019 Matt Mackall and others
9 AppCopyright=Copyright 2005-2019 Matt Mackall and others
10 AppName=Mercurial
10 AppName=Mercurial
11 AppVersion={#VERSION}
11 AppVersion={#VERSION}
12 #if ARCH == "x64"
12 #if ARCH == "x64"
13 AppVerName=Mercurial {#VERSION} (64-bit)
13 AppVerName=Mercurial {#VERSION} (64-bit)
14 OutputBaseFilename=Mercurial-{#VERSION}-x64
14 OutputBaseFilename=Mercurial-{#VERSION}-x64
15 ArchitecturesAllowed=x64
15 ArchitecturesAllowed=x64
16 ArchitecturesInstallIn64BitMode=x64
16 ArchitecturesInstallIn64BitMode=x64
17 #else
17 #else
18 AppVerName=Mercurial {#VERSION}
18 AppVerName=Mercurial {#VERSION}
19 OutputBaseFilename=Mercurial-{#VERSION}
19 OutputBaseFilename=Mercurial-{#VERSION}
20 #endif
20 #endif
21 InfoAfterFile=../postinstall.txt
21 InfoAfterFile=../postinstall.txt
22 LicenseFile=Copying.txt
22 LicenseFile=Copying.txt
23 ShowLanguageDialog=yes
23 ShowLanguageDialog=yes
24 AppPublisher=Matt Mackall and others
24 AppPublisher=Matt Mackall and others
25 AppPublisherURL=https://mercurial-scm.org/
25 AppPublisherURL=https://mercurial-scm.org/
26 AppSupportURL=https://mercurial-scm.org/
26 AppSupportURL=https://mercurial-scm.org/
27 AppUpdatesURL=https://mercurial-scm.org/
27 AppUpdatesURL=https://mercurial-scm.org/
28 {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }}
28 {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }}
29 AppContact=mercurial@mercurial-scm.org
29 AppContact=mercurial@mercurial-scm.org
30 DefaultDirName={pf}\Mercurial
30 DefaultDirName={pf}\Mercurial
31 SourceDir=stage
31 SourceDir=stage
32 VersionInfoDescription=Mercurial distributed SCM (version {#VERSION})
32 VersionInfoDescription=Mercurial distributed SCM (version {#VERSION})
33 VersionInfoCopyright=Copyright 2005-2019 Matt Mackall and others
33 VersionInfoCopyright=Copyright 2005-2019 Matt Mackall and others
34 VersionInfoCompany=Matt Mackall and others
34 VersionInfoCompany=Matt Mackall and others
35 InternalCompressLevel=max
35 InternalCompressLevel=max
36 SolidCompression=true
36 SolidCompression=true
37 SetupIconFile=../mercurial.ico
37 SetupIconFile=../mercurial.ico
38 AllowNoIcons=true
38 AllowNoIcons=true
39 DefaultGroupName=Mercurial
39 DefaultGroupName=Mercurial
40 PrivilegesRequired=none
40 PrivilegesRequired=none
41 ChangesEnvironment=true
41 ChangesEnvironment=true
42
42
43 [Files]
43 [Files]
44 {% for entry in package_files -%}
44 {% for entry in package_files -%}
45 Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }}
45 Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }}
46 {%- if entry.metadata %}; {{ entry.metadata }}{% endif %}
46 {%- if entry.metadata %}; {{ entry.metadata }}{% endif %}
47 {% endfor %}
47 {% endfor %}
48
48
49 [INI]
49 [INI]
50 Filename: {app}\Mercurial.url; Section: InternetShortcut; Key: URL; String: https://mercurial-scm.org/
50 Filename: {app}\Mercurial.url; Section: InternetShortcut; Key: URL; String: https://mercurial-scm.org/
51
51
52 [UninstallDelete]
52 [UninstallDelete]
53 Type: files; Name: {app}\Mercurial.url
53 Type: files; Name: {app}\Mercurial.url
54 Type: filesandordirs; Name: {app}\hgrc.d
54 Type: filesandordirs; Name: {app}\defaultrc
55
55
56 [Icons]
56 [Icons]
57 Name: {group}\Uninstall Mercurial; Filename: {uninstallexe}
57 Name: {group}\Uninstall Mercurial; Filename: {uninstallexe}
58 Name: {group}\Mercurial Command Reference; Filename: {app}\Docs\hg.1.html
58 Name: {group}\Mercurial Command Reference; Filename: {app}\Docs\hg.1.html
59 Name: {group}\Mercurial Configuration Files; Filename: {app}\Docs\hgrc.5.html
59 Name: {group}\Mercurial Configuration Files; Filename: {app}\Docs\hgrc.5.html
60 Name: {group}\Mercurial Ignore Files; Filename: {app}\Docs\hgignore.5.html
60 Name: {group}\Mercurial Ignore Files; Filename: {app}\Docs\hgignore.5.html
61 Name: {group}\Mercurial Web Site; Filename: {app}\Mercurial.url
61 Name: {group}\Mercurial Web Site; Filename: {app}\Mercurial.url
62
62
63 [Tasks]
63 [Tasks]
64 Name: modifypath; Description: Add the installation path to the search path; Flags: unchecked
64 Name: modifypath; Description: Add the installation path to the search path; Flags: unchecked
65
65
66 [Code]
66 [Code]
67 procedure Touch(fn: String);
67 procedure Touch(fn: String);
68 begin
68 begin
69 SaveStringToFile(ExpandConstant(fn), '', False);
69 SaveStringToFile(ExpandConstant(fn), '', False);
70 end;
70 end;
71
71
72 const
72 const
73 ModPathName = 'modifypath';
73 ModPathName = 'modifypath';
74 ModPathType = 'user';
74 ModPathType = 'user';
75
75
76 function ModPathDir(): TArrayOfString;
76 function ModPathDir(): TArrayOfString;
77 begin
77 begin
78 setArrayLength(Result, 1)
78 setArrayLength(Result, 1)
79 Result[0] := ExpandConstant('{app}');
79 Result[0] := ExpandConstant('{app}');
80 end;
80 end;
81
81
82 {% include 'modpath.iss' %}
82 {% include 'modpath.iss' %}
@@ -1,143 +1,143 b''
1 <?xml version='1.0' encoding='windows-1252'?>
1 <?xml version='1.0' encoding='windows-1252'?>
2 <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
2 <Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
3
3
4 <!-- Copyright 2010 Steve Borho <steve@borho.org>
4 <!-- Copyright 2010 Steve Borho <steve@borho.org>
5
5
6 This software may be used and distributed according to the terms of the
6 This software may be used and distributed according to the terms of the
7 GNU General Public License version 2 or any later version. -->
7 GNU General Public License version 2 or any later version. -->
8
8
9 <?include guids.wxi ?>
9 <?include guids.wxi ?>
10 <?include defines.wxi ?>
10 <?include defines.wxi ?>
11
11
12 <?if $(var.Platform) = "x64" ?>
12 <?if $(var.Platform) = "x64" ?>
13 <?define PFolder = ProgramFiles64Folder ?>
13 <?define PFolder = ProgramFiles64Folder ?>
14 <?else?>
14 <?else?>
15 <?define PFolder = ProgramFilesFolder ?>
15 <?define PFolder = ProgramFilesFolder ?>
16 <?endif?>
16 <?endif?>
17
17
18 <Product Id='*'
18 <Product Id='*'
19 Name='Mercurial $(var.Version) ($(var.Platform))'
19 Name='Mercurial $(var.Version) ($(var.Platform))'
20 UpgradeCode='$(var.ProductUpgradeCode)'
20 UpgradeCode='$(var.ProductUpgradeCode)'
21 Language='1033' Codepage='1252' Version='$(var.Version)'
21 Language='1033' Codepage='1252' Version='$(var.Version)'
22 Manufacturer='Matt Mackall and others'>
22 Manufacturer='Matt Mackall and others'>
23
23
24 <Package Id='*'
24 <Package Id='*'
25 Keywords='Installer'
25 Keywords='Installer'
26 Description="Mercurial distributed SCM (version $(var.Version))"
26 Description="Mercurial distributed SCM (version $(var.Version))"
27 Comments='$(var.Comments)'
27 Comments='$(var.Comments)'
28 Platform='$(var.Platform)'
28 Platform='$(var.Platform)'
29 Manufacturer='Matt Mackall and others'
29 Manufacturer='Matt Mackall and others'
30 InstallerVersion='300' Languages='1033' Compressed='yes' SummaryCodepage='1252' />
30 InstallerVersion='300' Languages='1033' Compressed='yes' SummaryCodepage='1252' />
31
31
32 <Media Id='1' Cabinet='mercurial.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'
32 <Media Id='1' Cabinet='mercurial.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'
33 CompressionLevel='high' />
33 CompressionLevel='high' />
34 <Property Id='DiskPrompt' Value="Mercurial $(var.Version) Installation [1]" />
34 <Property Id='DiskPrompt' Value="Mercurial $(var.Version) Installation [1]" />
35
35
36 <Condition Message='Mercurial MSI installers require Windows XP or higher'>
36 <Condition Message='Mercurial MSI installers require Windows XP or higher'>
37 VersionNT >= 501
37 VersionNT >= 501
38 </Condition>
38 </Condition>
39
39
40 <Property Id="INSTALLDIR">
40 <Property Id="INSTALLDIR">
41 <ComponentSearch Id='SearchForMainExecutableComponent'
41 <ComponentSearch Id='SearchForMainExecutableComponent'
42 Guid='$(var.ComponentMainExecutableGUID)' />
42 Guid='$(var.ComponentMainExecutableGUID)' />
43 </Property>
43 </Property>
44
44
45 <!--Property Id='ARPCOMMENTS'>any comments</Property-->
45 <!--Property Id='ARPCOMMENTS'>any comments</Property-->
46 <Property Id='ARPCONTACT'>mercurial@mercurial-scm.org</Property>
46 <Property Id='ARPCONTACT'>mercurial@mercurial-scm.org</Property>
47 <Property Id='ARPHELPLINK'>https://mercurial-scm.org/wiki/</Property>
47 <Property Id='ARPHELPLINK'>https://mercurial-scm.org/wiki/</Property>
48 <Property Id='ARPURLINFOABOUT'>https://mercurial-scm.org/about/</Property>
48 <Property Id='ARPURLINFOABOUT'>https://mercurial-scm.org/about/</Property>
49 <Property Id='ARPURLUPDATEINFO'>https://mercurial-scm.org/downloads/</Property>
49 <Property Id='ARPURLUPDATEINFO'>https://mercurial-scm.org/downloads/</Property>
50 <Property Id='ARPHELPTELEPHONE'>https://mercurial-scm.org/wiki/Support</Property>
50 <Property Id='ARPHELPTELEPHONE'>https://mercurial-scm.org/wiki/Support</Property>
51 <Property Id='ARPPRODUCTICON'>hgIcon.ico</Property>
51 <Property Id='ARPPRODUCTICON'>hgIcon.ico</Property>
52
52
53 <Property Id='INSTALLEDMERCURIALPRODUCTS' Secure='yes'></Property>
53 <Property Id='INSTALLEDMERCURIALPRODUCTS' Secure='yes'></Property>
54 <Property Id='REINSTALLMODE'>amus</Property>
54 <Property Id='REINSTALLMODE'>amus</Property>
55
55
56 <!--Auto-accept the license page-->
56 <!--Auto-accept the license page-->
57 <Property Id='LicenseAccepted'>1</Property>
57 <Property Id='LicenseAccepted'>1</Property>
58
58
59 <Directory Id='TARGETDIR' Name='SourceDir'>
59 <Directory Id='TARGETDIR' Name='SourceDir'>
60 <Directory Id='$(var.PFolder)' Name='PFiles'>
60 <Directory Id='$(var.PFolder)' Name='PFiles'>
61 <Directory Id='INSTALLDIR' Name='Mercurial'>
61 <Directory Id='INSTALLDIR' Name='Mercurial'>
62 <Component Id='MainExecutable' Guid='$(var.ComponentMainExecutableGUID)' Win64='$(var.IsX64)'>
62 <Component Id='MainExecutable' Guid='$(var.ComponentMainExecutableGUID)' Win64='$(var.IsX64)'>
63 <CreateFolder />
63 <CreateFolder />
64 <Environment Id="Environment" Name="PATH" Part="last" System="yes"
64 <Environment Id="Environment" Name="PATH" Part="last" System="yes"
65 Permanent="no" Value="[INSTALLDIR]" Action="set" />
65 Permanent="no" Value="[INSTALLDIR]" Action="set" />
66 </Component>
66 </Component>
67 </Directory>
67 </Directory>
68 </Directory>
68 </Directory>
69
69
70 <Directory Id="ProgramMenuFolder" Name="Programs">
70 <Directory Id="ProgramMenuFolder" Name="Programs">
71 <Directory Id="ProgramMenuDir" Name="Mercurial $(var.Version)">
71 <Directory Id="ProgramMenuDir" Name="Mercurial $(var.Version)">
72 <Component Id="ProgramMenuDir" Guid="$(var.ProgramMenuDir.guid)" Win64='$(var.IsX64)'>
72 <Component Id="ProgramMenuDir" Guid="$(var.ProgramMenuDir.guid)" Win64='$(var.IsX64)'>
73 <RemoveFolder Id='ProgramMenuDir' On='uninstall' />
73 <RemoveFolder Id='ProgramMenuDir' On='uninstall' />
74 <RegistryValue Root='HKCU' Key='Software\Mercurial\InstallDir' Type='string'
74 <RegistryValue Root='HKCU' Key='Software\Mercurial\InstallDir' Type='string'
75 Value='[INSTALLDIR]' KeyPath='yes' />
75 Value='[INSTALLDIR]' KeyPath='yes' />
76 <Shortcut Id='UrlShortcut' Directory='ProgramMenuDir' Name='Mercurial Web Site'
76 <Shortcut Id='UrlShortcut' Directory='ProgramMenuDir' Name='Mercurial Web Site'
77 Target='[ARPHELPLINK]' Icon="hgIcon.ico" IconIndex='0' />
77 Target='[ARPHELPLINK]' Icon="hgIcon.ico" IconIndex='0' />
78 </Component>
78 </Component>
79 </Directory>
79 </Directory>
80 </Directory>
80 </Directory>
81
81
82 <?if $(var.Platform) = "x86" ?>
82 <?if $(var.Platform) = "x86" ?>
83 <Merge Id='VCRuntime' DiskId='1' Language='1033'
83 <Merge Id='VCRuntime' DiskId='1' Language='1033'
84 SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x86_msm.msm' />
84 SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x86_msm.msm' />
85 <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
85 <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
86 SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x86_msm.msm' />
86 SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x86_msm.msm' />
87 <?else?>
87 <?else?>
88 <Merge Id='VCRuntime' DiskId='1' Language='1033'
88 <Merge Id='VCRuntime' DiskId='1' Language='1033'
89 SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x64_msm.msm' />
89 SourceFile='$(var.VCRedistSrcDir)\microsoft.vcxx.crt.x64_msm.msm' />
90 <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
90 <Merge Id='VCRuntimePolicy' DiskId='1' Language='1033'
91 SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x64_msm.msm' />
91 SourceFile='$(var.VCRedistSrcDir)\policy.x.xx.microsoft.vcxx.crt.x64_msm.msm' />
92 <?endif?>
92 <?endif?>
93 </Directory>
93 </Directory>
94
94
95 <Feature Id='Complete' Title='Mercurial' Description='The complete package'
95 <Feature Id='Complete' Title='Mercurial' Description='The complete package'
96 Display='expand' Level='1' ConfigurableDirectory='INSTALLDIR' >
96 Display='expand' Level='1' ConfigurableDirectory='INSTALLDIR' >
97 <Feature Id='MainProgram' Title='Program' Description='Mercurial command line app'
97 <Feature Id='MainProgram' Title='Program' Description='Mercurial command line app'
98 Level='1' Absent='disallow' >
98 Level='1' Absent='disallow' >
99 <ComponentRef Id='MainExecutable' />
99 <ComponentRef Id='MainExecutable' />
100 <ComponentRef Id='ProgramMenuDir' />
100 <ComponentRef Id='ProgramMenuDir' />
101 <ComponentGroupRef Id="hg.group.ROOT" />
101 <ComponentGroupRef Id="hg.group.ROOT" />
102 <ComponentGroupRef Id="hg.group.hgrc.d" />
102 <ComponentGroupRef Id="hg.group.defaultrc" />
103 <ComponentGroupRef Id="hg.group.helptext" />
103 <ComponentGroupRef Id="hg.group.helptext" />
104 <ComponentGroupRef Id="hg.group.lib" />
104 <ComponentGroupRef Id="hg.group.lib" />
105 <ComponentGroupRef Id="hg.group.templates" />
105 <ComponentGroupRef Id="hg.group.templates" />
106 <MergeRef Id='VCRuntime' />
106 <MergeRef Id='VCRuntime' />
107 <MergeRef Id='VCRuntimePolicy' />
107 <MergeRef Id='VCRuntimePolicy' />
108 </Feature>
108 </Feature>
109 <?ifdef MercurialExtraFeatures?>
109 <?ifdef MercurialExtraFeatures?>
110 <?foreach EXTRAFEAT in $(var.MercurialExtraFeatures)?>
110 <?foreach EXTRAFEAT in $(var.MercurialExtraFeatures)?>
111 <FeatureRef Id="$(var.EXTRAFEAT)" />
111 <FeatureRef Id="$(var.EXTRAFEAT)" />
112 <?endforeach?>
112 <?endforeach?>
113 <?endif?>
113 <?endif?>
114 <Feature Id='Locales' Title='Translations' Description='Translations' Level='1'>
114 <Feature Id='Locales' Title='Translations' Description='Translations' Level='1'>
115 <ComponentGroupRef Id="hg.group.locale" />
115 <ComponentGroupRef Id="hg.group.locale" />
116 </Feature>
116 </Feature>
117 <Feature Id='Documentation' Title='Documentation' Description='HTML man pages' Level='1'>
117 <Feature Id='Documentation' Title='Documentation' Description='HTML man pages' Level='1'>
118 <ComponentGroupRef Id="hg.group.doc" />
118 <ComponentGroupRef Id="hg.group.doc" />
119 </Feature>
119 </Feature>
120 <Feature Id='Misc' Title='Miscellaneous' Description='Contributed scripts' Level='1'>
120 <Feature Id='Misc' Title='Miscellaneous' Description='Contributed scripts' Level='1'>
121 <ComponentGroupRef Id="hg.group.contrib" />
121 <ComponentGroupRef Id="hg.group.contrib" />
122 </Feature>
122 </Feature>
123 </Feature>
123 </Feature>
124
124
125 <UIRef Id="WixUI_FeatureTree" />
125 <UIRef Id="WixUI_FeatureTree" />
126 <UIRef Id="WixUI_ErrorProgressText" />
126 <UIRef Id="WixUI_ErrorProgressText" />
127
127
128 <WixVariable Id="WixUILicenseRtf" Value="contrib\packaging\wix\COPYING.rtf" />
128 <WixVariable Id="WixUILicenseRtf" Value="contrib\packaging\wix\COPYING.rtf" />
129
129
130 <Icon Id="hgIcon.ico" SourceFile="contrib/win32/mercurial.ico" />
130 <Icon Id="hgIcon.ico" SourceFile="contrib/win32/mercurial.ico" />
131
131
132 <Upgrade Id='$(var.ProductUpgradeCode)'>
132 <Upgrade Id='$(var.ProductUpgradeCode)'>
133 <UpgradeVersion
133 <UpgradeVersion
134 IncludeMinimum='yes' Minimum='0.0.0' IncludeMaximum='no' OnlyDetect='no'
134 IncludeMinimum='yes' Minimum='0.0.0' IncludeMaximum='no' OnlyDetect='no'
135 Property='INSTALLEDMERCURIALPRODUCTS' />
135 Property='INSTALLEDMERCURIALPRODUCTS' />
136 </Upgrade>
136 </Upgrade>
137
137
138 <InstallExecuteSequence>
138 <InstallExecuteSequence>
139 <RemoveExistingProducts After='InstallInitialize'/>
139 <RemoveExistingProducts After='InstallInitialize'/>
140 </InstallExecuteSequence>
140 </InstallExecuteSequence>
141
141
142 </Product>
142 </Product>
143 </Wix>
143 </Wix>
General Comments 0
You need to be logged in to leave comments. Login now