##// END OF EJS Templates
packaging: add -python2 to Windows installer filenames...
Gregory Szorc -
r45276:9ade217b stable
parent child Browse files
Show More
@@ -1,533 +1,533 b''
1 # windows.py - Automation specific to Windows
1 # windows.py - Automation specific to Windows
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 datetime
10 import datetime
11 import os
11 import os
12 import paramiko
12 import paramiko
13 import pathlib
13 import pathlib
14 import re
14 import re
15 import subprocess
15 import subprocess
16 import tempfile
16 import tempfile
17
17
18 from .pypi import upload as pypi_upload
18 from .pypi import upload as pypi_upload
19 from .winrm import run_powershell
19 from .winrm import run_powershell
20
20
21
21
22 # PowerShell commands to activate a Visual Studio 2008 environment.
22 # PowerShell commands to activate a Visual Studio 2008 environment.
23 # This is essentially a port of vcvarsall.bat to PowerShell.
23 # This is essentially a port of vcvarsall.bat to PowerShell.
24 ACTIVATE_VC9_AMD64 = r'''
24 ACTIVATE_VC9_AMD64 = r'''
25 Write-Output "activating Visual Studio 2008 environment for AMD64"
25 Write-Output "activating Visual Studio 2008 environment for AMD64"
26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
27 $Env:VCINSTALLDIR = "${root}\VC\"
27 $Env:VCINSTALLDIR = "${root}\VC\"
28 $Env:WindowsSdkDir = "${root}\WinSDK\"
28 $Env:WindowsSdkDir = "${root}\WinSDK\"
29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
33 '''.lstrip()
33 '''.lstrip()
34
34
35 ACTIVATE_VC9_X86 = r'''
35 ACTIVATE_VC9_X86 = r'''
36 Write-Output "activating Visual Studio 2008 environment for x86"
36 Write-Output "activating Visual Studio 2008 environment for x86"
37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
38 $Env:VCINSTALLDIR = "${root}\VC\"
38 $Env:VCINSTALLDIR = "${root}\VC\"
39 $Env:WindowsSdkDir = "${root}\WinSDK\"
39 $Env:WindowsSdkDir = "${root}\WinSDK\"
40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
44 '''.lstrip()
44 '''.lstrip()
45
45
46 HG_PURGE = r'''
46 HG_PURGE = r'''
47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
48 Set-Location C:\hgdev\src
48 Set-Location C:\hgdev\src
49 hg.exe --config extensions.purge= purge --all
49 hg.exe --config extensions.purge= purge --all
50 if ($LASTEXITCODE -ne 0) {
50 if ($LASTEXITCODE -ne 0) {
51 throw "process exited non-0: $LASTEXITCODE"
51 throw "process exited non-0: $LASTEXITCODE"
52 }
52 }
53 Write-Output "purged Mercurial repo"
53 Write-Output "purged Mercurial repo"
54 '''
54 '''
55
55
56 HG_UPDATE_CLEAN = r'''
56 HG_UPDATE_CLEAN = r'''
57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
58 Set-Location C:\hgdev\src
58 Set-Location C:\hgdev\src
59 hg.exe --config extensions.purge= purge --all
59 hg.exe --config extensions.purge= purge --all
60 if ($LASTEXITCODE -ne 0) {{
60 if ($LASTEXITCODE -ne 0) {{
61 throw "process exited non-0: $LASTEXITCODE"
61 throw "process exited non-0: $LASTEXITCODE"
62 }}
62 }}
63 hg.exe update -C {revision}
63 hg.exe update -C {revision}
64 if ($LASTEXITCODE -ne 0) {{
64 if ($LASTEXITCODE -ne 0) {{
65 throw "process exited non-0: $LASTEXITCODE"
65 throw "process exited non-0: $LASTEXITCODE"
66 }}
66 }}
67 hg.exe log -r .
67 hg.exe log -r .
68 Write-Output "updated Mercurial working directory to {revision}"
68 Write-Output "updated Mercurial working directory to {revision}"
69 '''.lstrip()
69 '''.lstrip()
70
70
71 BUILD_INNO = r'''
71 BUILD_INNO = r'''
72 Set-Location C:\hgdev\src
72 Set-Location C:\hgdev\src
73 $python = "C:\hgdev\python27-{arch}\python.exe"
73 $python = "C:\hgdev\python27-{arch}\python.exe"
74 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python
74 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python
75 if ($LASTEXITCODE -ne 0) {{
75 if ($LASTEXITCODE -ne 0) {{
76 throw "process exited non-0: $LASTEXITCODE"
76 throw "process exited non-0: $LASTEXITCODE"
77 }}
77 }}
78 '''.lstrip()
78 '''.lstrip()
79
79
80 BUILD_WHEEL = r'''
80 BUILD_WHEEL = r'''
81 Set-Location C:\hgdev\src
81 Set-Location C:\hgdev\src
82 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
82 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
83 if ($LASTEXITCODE -ne 0) {{
83 if ($LASTEXITCODE -ne 0) {{
84 throw "process exited non-0: $LASTEXITCODE"
84 throw "process exited non-0: $LASTEXITCODE"
85 }}
85 }}
86 '''
86 '''
87
87
88 BUILD_WIX = r'''
88 BUILD_WIX = r'''
89 Set-Location C:\hgdev\src
89 Set-Location C:\hgdev\src
90 $python = "C:\hgdev\python27-{arch}\python.exe"
90 $python = "C:\hgdev\python27-{arch}\python.exe"
91 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
91 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
92 if ($LASTEXITCODE -ne 0) {{
92 if ($LASTEXITCODE -ne 0) {{
93 throw "process exited non-0: $LASTEXITCODE"
93 throw "process exited non-0: $LASTEXITCODE"
94 }}
94 }}
95 '''
95 '''
96
96
97 RUN_TESTS = r'''
97 RUN_TESTS = r'''
98 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
98 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
99 if ($LASTEXITCODE -ne 0) {{
99 if ($LASTEXITCODE -ne 0) {{
100 throw "process exited non-0: $LASTEXITCODE"
100 throw "process exited non-0: $LASTEXITCODE"
101 }}
101 }}
102 '''
102 '''
103
103
104 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
104 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
105 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
106 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
106 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
107 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
107 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
108 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
108 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
109 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
109 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
110
110
111 X86_EXE_FILENAME = 'Mercurial-{version}.exe'
111 X86_EXE_FILENAME = 'Mercurial-{version}-x86-python2.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64-python2.exe'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
114 X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
114 X64_MSI_FILENAME = 'mercurial-{version}-x64-python2.msi'
115
115
116 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
116 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
117
117
118 X86_USER_AGENT_PATTERN = '.*Windows.*'
118 X86_USER_AGENT_PATTERN = '.*Windows.*'
119 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
119 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
120
120
121 X86_EXE_DESCRIPTION = (
121 X86_EXE_DESCRIPTION = (
122 'Mercurial {version} Inno Setup installer - x86 Windows '
122 'Mercurial {version} Inno Setup installer - x86 Windows '
123 '- does not require admin rights'
123 '- does not require admin rights'
124 )
124 )
125 X64_EXE_DESCRIPTION = (
125 X64_EXE_DESCRIPTION = (
126 'Mercurial {version} Inno Setup installer - x64 Windows '
126 'Mercurial {version} Inno Setup installer - x64 Windows '
127 '- does not require admin rights'
127 '- does not require admin rights'
128 )
128 )
129 X86_MSI_DESCRIPTION = (
129 X86_MSI_DESCRIPTION = (
130 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
130 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
131 )
131 )
132 X64_MSI_DESCRIPTION = (
132 X64_MSI_DESCRIPTION = (
133 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
133 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
134 )
134 )
135
135
136
136
137 def get_vc_prefix(arch):
137 def get_vc_prefix(arch):
138 if arch == 'x86':
138 if arch == 'x86':
139 return ACTIVATE_VC9_X86
139 return ACTIVATE_VC9_X86
140 elif arch == 'x64':
140 elif arch == 'x64':
141 return ACTIVATE_VC9_AMD64
141 return ACTIVATE_VC9_AMD64
142 else:
142 else:
143 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
143 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
144
144
145
145
146 def fix_authorized_keys_permissions(winrm_client, path):
146 def fix_authorized_keys_permissions(winrm_client, path):
147 commands = [
147 commands = [
148 '$ErrorActionPreference = "Stop"',
148 '$ErrorActionPreference = "Stop"',
149 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
149 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
150 r'icacls %s /remove:g "NT Service\sshd"' % path,
150 r'icacls %s /remove:g "NT Service\sshd"' % path,
151 ]
151 ]
152
152
153 run_powershell(winrm_client, '\n'.join(commands))
153 run_powershell(winrm_client, '\n'.join(commands))
154
154
155
155
156 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
156 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
157 """Synchronize local Mercurial repo to remote EC2 instance."""
157 """Synchronize local Mercurial repo to remote EC2 instance."""
158
158
159 winrm_client = ec2_instance.winrm_client
159 winrm_client = ec2_instance.winrm_client
160
160
161 with tempfile.TemporaryDirectory() as temp_dir:
161 with tempfile.TemporaryDirectory() as temp_dir:
162 temp_dir = pathlib.Path(temp_dir)
162 temp_dir = pathlib.Path(temp_dir)
163
163
164 ssh_dir = temp_dir / '.ssh'
164 ssh_dir = temp_dir / '.ssh'
165 ssh_dir.mkdir()
165 ssh_dir.mkdir()
166 ssh_dir.chmod(0o0700)
166 ssh_dir.chmod(0o0700)
167
167
168 # Generate SSH key to use for communication.
168 # Generate SSH key to use for communication.
169 subprocess.run(
169 subprocess.run(
170 [
170 [
171 'ssh-keygen',
171 'ssh-keygen',
172 '-t',
172 '-t',
173 'rsa',
173 'rsa',
174 '-b',
174 '-b',
175 '4096',
175 '4096',
176 '-N',
176 '-N',
177 '',
177 '',
178 '-f',
178 '-f',
179 str(ssh_dir / 'id_rsa'),
179 str(ssh_dir / 'id_rsa'),
180 ],
180 ],
181 check=True,
181 check=True,
182 capture_output=True,
182 capture_output=True,
183 )
183 )
184
184
185 # Add it to ~/.ssh/authorized_keys on remote.
185 # Add it to ~/.ssh/authorized_keys on remote.
186 # This assumes the file doesn't already exist.
186 # This assumes the file doesn't already exist.
187 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
187 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
188 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
188 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
189 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
189 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
190 fix_authorized_keys_permissions(winrm_client, authorized_keys)
190 fix_authorized_keys_permissions(winrm_client, authorized_keys)
191
191
192 public_ip = ec2_instance.public_ip_address
192 public_ip = ec2_instance.public_ip_address
193
193
194 ssh_config = temp_dir / '.ssh' / 'config'
194 ssh_config = temp_dir / '.ssh' / 'config'
195
195
196 with open(ssh_config, 'w', encoding='utf-8') as fh:
196 with open(ssh_config, 'w', encoding='utf-8') as fh:
197 fh.write('Host %s\n' % public_ip)
197 fh.write('Host %s\n' % public_ip)
198 fh.write(' User Administrator\n')
198 fh.write(' User Administrator\n')
199 fh.write(' StrictHostKeyChecking no\n')
199 fh.write(' StrictHostKeyChecking no\n')
200 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
200 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
201 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
201 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
202
202
203 if not (hg_repo / '.hg').is_dir():
203 if not (hg_repo / '.hg').is_dir():
204 raise Exception(
204 raise Exception(
205 '%s is not a Mercurial repository; '
205 '%s is not a Mercurial repository; '
206 'synchronization not yet supported' % hg_repo
206 'synchronization not yet supported' % hg_repo
207 )
207 )
208
208
209 env = dict(os.environ)
209 env = dict(os.environ)
210 env['HGPLAIN'] = '1'
210 env['HGPLAIN'] = '1'
211 env['HGENCODING'] = 'utf-8'
211 env['HGENCODING'] = 'utf-8'
212
212
213 hg_bin = hg_repo / 'hg'
213 hg_bin = hg_repo / 'hg'
214
214
215 res = subprocess.run(
215 res = subprocess.run(
216 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
216 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
217 cwd=str(hg_repo),
217 cwd=str(hg_repo),
218 env=env,
218 env=env,
219 check=True,
219 check=True,
220 capture_output=True,
220 capture_output=True,
221 )
221 )
222
222
223 full_revision = res.stdout.decode('ascii')
223 full_revision = res.stdout.decode('ascii')
224
224
225 args = [
225 args = [
226 'python2.7',
226 'python2.7',
227 hg_bin,
227 hg_bin,
228 '--config',
228 '--config',
229 'ui.ssh=ssh -F %s' % ssh_config,
229 'ui.ssh=ssh -F %s' % ssh_config,
230 '--config',
230 '--config',
231 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
231 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
232 # Also ensure .hgtags changes are present so auto version
232 # Also ensure .hgtags changes are present so auto version
233 # calculation works.
233 # calculation works.
234 'push',
234 'push',
235 '-f',
235 '-f',
236 '-r',
236 '-r',
237 full_revision,
237 full_revision,
238 '-r',
238 '-r',
239 'file(.hgtags)',
239 'file(.hgtags)',
240 'ssh://%s/c:/hgdev/src' % public_ip,
240 'ssh://%s/c:/hgdev/src' % public_ip,
241 ]
241 ]
242
242
243 res = subprocess.run(args, cwd=str(hg_repo), env=env)
243 res = subprocess.run(args, cwd=str(hg_repo), env=env)
244
244
245 # Allow 1 (no-op) to not trigger error.
245 # Allow 1 (no-op) to not trigger error.
246 if res.returncode not in (0, 1):
246 if res.returncode not in (0, 1):
247 res.check_returncode()
247 res.check_returncode()
248
248
249 run_powershell(
249 run_powershell(
250 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
250 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
251 )
251 )
252
252
253 # TODO detect dirty local working directory and synchronize accordingly.
253 # TODO detect dirty local working directory and synchronize accordingly.
254
254
255
255
256 def purge_hg(winrm_client):
256 def purge_hg(winrm_client):
257 """Purge the Mercurial source repository on an EC2 instance."""
257 """Purge the Mercurial source repository on an EC2 instance."""
258 run_powershell(winrm_client, HG_PURGE)
258 run_powershell(winrm_client, HG_PURGE)
259
259
260
260
261 def find_latest_dist(winrm_client, pattern):
261 def find_latest_dist(winrm_client, pattern):
262 """Find path to newest file in dist/ directory matching a pattern."""
262 """Find path to newest file in dist/ directory matching a pattern."""
263
263
264 res = winrm_client.execute_ps(
264 res = winrm_client.execute_ps(
265 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
265 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
266 '| Sort-Object LastWriteTime -Descending '
266 '| Sort-Object LastWriteTime -Descending '
267 '| Select-Object -First 1\n'
267 '| Select-Object -First 1\n'
268 '$v.name' % pattern
268 '$v.name' % pattern
269 )
269 )
270 return res[0]
270 return res[0]
271
271
272
272
273 def copy_latest_dist(winrm_client, pattern, dest_path):
273 def copy_latest_dist(winrm_client, pattern, dest_path):
274 """Copy latest file matching pattern in dist/ directory.
274 """Copy latest file matching pattern in dist/ directory.
275
275
276 Given a WinRM client and a file pattern, find the latest file on the remote
276 Given a WinRM client and a file pattern, find the latest file on the remote
277 matching that pattern and copy it to the ``dest_path`` directory on the
277 matching that pattern and copy it to the ``dest_path`` directory on the
278 local machine.
278 local machine.
279 """
279 """
280 latest = find_latest_dist(winrm_client, pattern)
280 latest = find_latest_dist(winrm_client, pattern)
281 source = r'C:\hgdev\src\dist\%s' % latest
281 source = r'C:\hgdev\src\dist\%s' % latest
282 dest = dest_path / latest
282 dest = dest_path / latest
283 print('copying %s to %s' % (source, dest))
283 print('copying %s to %s' % (source, dest))
284 winrm_client.fetch(source, str(dest))
284 winrm_client.fetch(source, str(dest))
285
285
286
286
287 def build_inno_installer(
287 def build_inno_installer(
288 winrm_client, arch: str, dest_path: pathlib.Path, version=None
288 winrm_client, arch: str, dest_path: pathlib.Path, version=None
289 ):
289 ):
290 """Build the Inno Setup installer on a remote machine.
290 """Build the Inno Setup installer on a remote machine.
291
291
292 Using a WinRM client, remote commands are executed to build
292 Using a WinRM client, remote commands are executed to build
293 a Mercurial Inno Setup installer.
293 a Mercurial Inno Setup installer.
294 """
294 """
295 print('building Inno Setup installer for %s' % arch)
295 print('building Inno Setup installer for %s' % arch)
296
296
297 extra_args = []
297 extra_args = []
298 if version:
298 if version:
299 extra_args.extend(['--version', version])
299 extra_args.extend(['--version', version])
300
300
301 ps = get_vc_prefix(arch) + BUILD_INNO.format(
301 ps = get_vc_prefix(arch) + BUILD_INNO.format(
302 arch=arch, extra_args=' '.join(extra_args)
302 arch=arch, extra_args=' '.join(extra_args)
303 )
303 )
304 run_powershell(winrm_client, ps)
304 run_powershell(winrm_client, ps)
305 copy_latest_dist(winrm_client, '*.exe', dest_path)
305 copy_latest_dist(winrm_client, '*.exe', dest_path)
306
306
307
307
308 def build_wheel(
308 def build_wheel(
309 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
309 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
310 ):
310 ):
311 """Build Python wheels on a remote machine.
311 """Build Python wheels on a remote machine.
312
312
313 Using a WinRM client, remote commands are executed to build a Python wheel
313 Using a WinRM client, remote commands are executed to build a Python wheel
314 for Mercurial.
314 for Mercurial.
315 """
315 """
316 print('Building Windows wheel for Python %s %s' % (python_version, arch))
316 print('Building Windows wheel for Python %s %s' % (python_version, arch))
317
317
318 ps = BUILD_WHEEL.format(
318 ps = BUILD_WHEEL.format(
319 python_version=python_version.replace(".", ""), arch=arch
319 python_version=python_version.replace(".", ""), arch=arch
320 )
320 )
321
321
322 # Python 2.7 requires an activated environment.
322 # Python 2.7 requires an activated environment.
323 if python_version == "2.7":
323 if python_version == "2.7":
324 ps = get_vc_prefix(arch) + ps
324 ps = get_vc_prefix(arch) + ps
325
325
326 run_powershell(winrm_client, ps)
326 run_powershell(winrm_client, ps)
327 copy_latest_dist(winrm_client, '*.whl', dest_path)
327 copy_latest_dist(winrm_client, '*.whl', dest_path)
328
328
329
329
330 def build_wix_installer(
330 def build_wix_installer(
331 winrm_client, arch: str, dest_path: pathlib.Path, version=None
331 winrm_client, arch: str, dest_path: pathlib.Path, version=None
332 ):
332 ):
333 """Build the WiX installer on a remote machine.
333 """Build the WiX installer on a remote machine.
334
334
335 Using a WinRM client, remote commands are executed to build a WiX installer.
335 Using a WinRM client, remote commands are executed to build a WiX installer.
336 """
336 """
337 print('Building WiX installer for %s' % arch)
337 print('Building WiX installer for %s' % arch)
338 extra_args = []
338 extra_args = []
339 if version:
339 if version:
340 extra_args.extend(['--version', version])
340 extra_args.extend(['--version', version])
341
341
342 ps = get_vc_prefix(arch) + BUILD_WIX.format(
342 ps = get_vc_prefix(arch) + BUILD_WIX.format(
343 arch=arch, extra_args=' '.join(extra_args)
343 arch=arch, extra_args=' '.join(extra_args)
344 )
344 )
345 run_powershell(winrm_client, ps)
345 run_powershell(winrm_client, ps)
346 copy_latest_dist(winrm_client, '*.msi', dest_path)
346 copy_latest_dist(winrm_client, '*.msi', dest_path)
347
347
348
348
349 def run_tests(winrm_client, python_version, arch, test_flags=''):
349 def run_tests(winrm_client, python_version, arch, test_flags=''):
350 """Run tests on a remote Windows machine.
350 """Run tests on a remote Windows machine.
351
351
352 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
352 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
353 ``arch`` is ``x86`` or ``x64``.
353 ``arch`` is ``x86`` or ``x64``.
354 ``test_flags`` is a str representing extra arguments to pass to
354 ``test_flags`` is a str representing extra arguments to pass to
355 ``run-tests.py``.
355 ``run-tests.py``.
356 """
356 """
357 if not re.match(r'\d\.\d', python_version):
357 if not re.match(r'\d\.\d', python_version):
358 raise ValueError(
358 raise ValueError(
359 r'python_version must be \d.\d; got %s' % python_version
359 r'python_version must be \d.\d; got %s' % python_version
360 )
360 )
361
361
362 if arch not in ('x86', 'x64'):
362 if arch not in ('x86', 'x64'):
363 raise ValueError('arch must be x86 or x64; got %s' % arch)
363 raise ValueError('arch must be x86 or x64; got %s' % arch)
364
364
365 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
365 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
366
366
367 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
367 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
368
368
369 run_powershell(winrm_client, ps)
369 run_powershell(winrm_client, ps)
370
370
371
371
372 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
372 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
373 return (
373 return (
374 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
374 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
375 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
375 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
376 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
376 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
377 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
377 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
378 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
378 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
379 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
379 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
380 )
380 )
381
381
382
382
383 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
383 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
384 return (
384 return (
385 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
385 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
386 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
386 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
387 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
387 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
388 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
388 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
389 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
389 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
390 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
390 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
391 dist_path / X86_EXE_FILENAME.format(version=version),
391 dist_path / X86_EXE_FILENAME.format(version=version),
392 dist_path / X64_EXE_FILENAME.format(version=version),
392 dist_path / X64_EXE_FILENAME.format(version=version),
393 dist_path / X86_MSI_FILENAME.format(version=version),
393 dist_path / X86_MSI_FILENAME.format(version=version),
394 dist_path / X64_MSI_FILENAME.format(version=version),
394 dist_path / X64_MSI_FILENAME.format(version=version),
395 )
395 )
396
396
397
397
398 def generate_latest_dat(version: str):
398 def generate_latest_dat(version: str):
399 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
399 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
400 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
400 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
401 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
401 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
402 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
402 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
403
403
404 entries = (
404 entries = (
405 (
405 (
406 '10',
406 '10',
407 version,
407 version,
408 X86_USER_AGENT_PATTERN,
408 X86_USER_AGENT_PATTERN,
409 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
409 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
410 X86_EXE_DESCRIPTION.format(version=version),
410 X86_EXE_DESCRIPTION.format(version=version),
411 ),
411 ),
412 (
412 (
413 '10',
413 '10',
414 version,
414 version,
415 X64_USER_AGENT_PATTERN,
415 X64_USER_AGENT_PATTERN,
416 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
416 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
417 X64_EXE_DESCRIPTION.format(version=version),
417 X64_EXE_DESCRIPTION.format(version=version),
418 ),
418 ),
419 (
419 (
420 '10',
420 '10',
421 version,
421 version,
422 X86_USER_AGENT_PATTERN,
422 X86_USER_AGENT_PATTERN,
423 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
423 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
424 X86_MSI_DESCRIPTION.format(version=version),
424 X86_MSI_DESCRIPTION.format(version=version),
425 ),
425 ),
426 (
426 (
427 '10',
427 '10',
428 version,
428 version,
429 X64_USER_AGENT_PATTERN,
429 X64_USER_AGENT_PATTERN,
430 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
430 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
431 X64_MSI_DESCRIPTION.format(version=version),
431 X64_MSI_DESCRIPTION.format(version=version),
432 ),
432 ),
433 )
433 )
434
434
435 lines = ['\t'.join(e) for e in entries]
435 lines = ['\t'.join(e) for e in entries]
436
436
437 return '\n'.join(lines) + '\n'
437 return '\n'.join(lines) + '\n'
438
438
439
439
440 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
440 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
441 """Publish Windows release artifacts to PyPI."""
441 """Publish Windows release artifacts to PyPI."""
442
442
443 wheel_paths = resolve_wheel_artifacts(dist_path, version)
443 wheel_paths = resolve_wheel_artifacts(dist_path, version)
444
444
445 for p in wheel_paths:
445 for p in wheel_paths:
446 if not p.exists():
446 if not p.exists():
447 raise Exception('%s not found' % p)
447 raise Exception('%s not found' % p)
448
448
449 print('uploading wheels to PyPI (you may be prompted for credentials)')
449 print('uploading wheels to PyPI (you may be prompted for credentials)')
450 pypi_upload(wheel_paths)
450 pypi_upload(wheel_paths)
451
451
452
452
453 def publish_artifacts_mercurial_scm_org(
453 def publish_artifacts_mercurial_scm_org(
454 dist_path: pathlib.Path, version: str, ssh_username=None
454 dist_path: pathlib.Path, version: str, ssh_username=None
455 ):
455 ):
456 """Publish Windows release artifacts to mercurial-scm.org."""
456 """Publish Windows release artifacts to mercurial-scm.org."""
457 all_paths = resolve_all_artifacts(dist_path, version)
457 all_paths = resolve_all_artifacts(dist_path, version)
458
458
459 for p in all_paths:
459 for p in all_paths:
460 if not p.exists():
460 if not p.exists():
461 raise Exception('%s not found' % p)
461 raise Exception('%s not found' % p)
462
462
463 client = paramiko.SSHClient()
463 client = paramiko.SSHClient()
464 client.load_system_host_keys()
464 client.load_system_host_keys()
465 # We assume the system SSH configuration knows how to connect.
465 # We assume the system SSH configuration knows how to connect.
466 print('connecting to mercurial-scm.org via ssh...')
466 print('connecting to mercurial-scm.org via ssh...')
467 try:
467 try:
468 client.connect('mercurial-scm.org', username=ssh_username)
468 client.connect('mercurial-scm.org', username=ssh_username)
469 except paramiko.AuthenticationException:
469 except paramiko.AuthenticationException:
470 print('error authenticating; is an SSH key available in an SSH agent?')
470 print('error authenticating; is an SSH key available in an SSH agent?')
471 raise
471 raise
472
472
473 print('SSH connection established')
473 print('SSH connection established')
474
474
475 print('opening SFTP client...')
475 print('opening SFTP client...')
476 sftp = client.open_sftp()
476 sftp = client.open_sftp()
477 print('SFTP client obtained')
477 print('SFTP client obtained')
478
478
479 for p in all_paths:
479 for p in all_paths:
480 dest_path = '/var/www/release/windows/%s' % p.name
480 dest_path = '/var/www/release/windows/%s' % p.name
481 print('uploading %s to %s' % (p, dest_path))
481 print('uploading %s to %s' % (p, dest_path))
482
482
483 with p.open('rb') as fh:
483 with p.open('rb') as fh:
484 data = fh.read()
484 data = fh.read()
485
485
486 with sftp.open(dest_path, 'wb') as fh:
486 with sftp.open(dest_path, 'wb') as fh:
487 fh.write(data)
487 fh.write(data)
488 fh.chmod(0o0664)
488 fh.chmod(0o0664)
489
489
490 latest_dat_path = '/var/www/release/windows/latest.dat'
490 latest_dat_path = '/var/www/release/windows/latest.dat'
491
491
492 now = datetime.datetime.utcnow()
492 now = datetime.datetime.utcnow()
493 backup_path = dist_path / (
493 backup_path = dist_path / (
494 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
494 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
495 )
495 )
496 print('backing up %s to %s' % (latest_dat_path, backup_path))
496 print('backing up %s to %s' % (latest_dat_path, backup_path))
497
497
498 with sftp.open(latest_dat_path, 'rb') as fh:
498 with sftp.open(latest_dat_path, 'rb') as fh:
499 latest_dat_old = fh.read()
499 latest_dat_old = fh.read()
500
500
501 with backup_path.open('wb') as fh:
501 with backup_path.open('wb') as fh:
502 fh.write(latest_dat_old)
502 fh.write(latest_dat_old)
503
503
504 print('writing %s with content:' % latest_dat_path)
504 print('writing %s with content:' % latest_dat_path)
505 latest_dat_content = generate_latest_dat(version)
505 latest_dat_content = generate_latest_dat(version)
506 print(latest_dat_content)
506 print(latest_dat_content)
507
507
508 with sftp.open(latest_dat_path, 'wb') as fh:
508 with sftp.open(latest_dat_path, 'wb') as fh:
509 fh.write(latest_dat_content.encode('ascii'))
509 fh.write(latest_dat_content.encode('ascii'))
510
510
511
511
512 def publish_artifacts(
512 def publish_artifacts(
513 dist_path: pathlib.Path,
513 dist_path: pathlib.Path,
514 version: str,
514 version: str,
515 pypi=True,
515 pypi=True,
516 mercurial_scm_org=True,
516 mercurial_scm_org=True,
517 ssh_username=None,
517 ssh_username=None,
518 ):
518 ):
519 """Publish Windows release artifacts.
519 """Publish Windows release artifacts.
520
520
521 Files are found in `dist_path`. We will look for files with version string
521 Files are found in `dist_path`. We will look for files with version string
522 `version`.
522 `version`.
523
523
524 `pypi` controls whether we upload to PyPI.
524 `pypi` controls whether we upload to PyPI.
525 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
525 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
526 """
526 """
527 if pypi:
527 if pypi:
528 publish_artifacts_pypi(dist_path, version)
528 publish_artifacts_pypi(dist_path, version)
529
529
530 if mercurial_scm_org:
530 if mercurial_scm_org:
531 publish_artifacts_mercurial_scm_org(
531 publish_artifacts_mercurial_scm_org(
532 dist_path, version, ssh_username=ssh_username
532 dist_path, version, ssh_username=ssh_username
533 )
533 )
@@ -1,227 +1,232 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 run_pyoxidizer
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_INSTALL_RULES = [
36 EXTRA_INSTALL_RULES = [
37 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
37 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
38 ]
38 ]
39
39
40 PACKAGE_FILES_METADATA = {
40 PACKAGE_FILES_METADATA = {
41 'ReadMe.html': 'Flags: isreadme',
41 'ReadMe.html': 'Flags: isreadme',
42 }
42 }
43
43
44
44
45 def build_with_py2exe(
45 def build_with_py2exe(
46 source_dir: pathlib.Path,
46 source_dir: pathlib.Path,
47 build_dir: pathlib.Path,
47 build_dir: pathlib.Path,
48 python_exe: pathlib.Path,
48 python_exe: pathlib.Path,
49 iscc_exe: pathlib.Path,
49 iscc_exe: pathlib.Path,
50 version=None,
50 version=None,
51 ):
51 ):
52 """Build the Inno installer using py2exe.
52 """Build the Inno installer using py2exe.
53
53
54 Build files will be placed in ``build_dir``.
54 Build files will be placed in ``build_dir``.
55
55
56 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
56 py2exe's setup.py doesn't use setuptools. It doesn't have modern logic
57 for finding the Python 2.7 toolchain. So, we require the environment
57 for finding the Python 2.7 toolchain. So, we require the environment
58 to already be configured with an active toolchain.
58 to already be configured with an active toolchain.
59 """
59 """
60 if not iscc_exe.exists():
60 if not iscc_exe.exists():
61 raise Exception('%s does not exist' % iscc_exe)
61 raise Exception('%s does not exist' % iscc_exe)
62
62
63 vc_x64 = r'\x64' in os.environ.get('LIB', '')
63 vc_x64 = r'\x64' in os.environ.get('LIB', '')
64 arch = 'x64' if vc_x64 else 'x86'
64 arch = 'x64' if vc_x64 else 'x86'
65 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
65 inno_build_dir = build_dir / ('inno-py2exe-%s' % arch)
66 staging_dir = inno_build_dir / 'stage'
66 staging_dir = inno_build_dir / 'stage'
67
67
68 requirements_txt = (
68 requirements_txt = (
69 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
69 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
70 )
70 )
71
71
72 inno_build_dir.mkdir(parents=True, exist_ok=True)
72 inno_build_dir.mkdir(parents=True, exist_ok=True)
73
73
74 build_py2exe(
74 build_py2exe(
75 source_dir,
75 source_dir,
76 build_dir,
76 build_dir,
77 python_exe,
77 python_exe,
78 'inno',
78 'inno',
79 requirements_txt,
79 requirements_txt,
80 extra_packages=EXTRA_PACKAGES,
80 extra_packages=EXTRA_PACKAGES,
81 )
81 )
82
82
83 # Purge the staging directory for every build so packaging is
83 # Purge the staging directory for every build so packaging is
84 # pristine.
84 # pristine.
85 if staging_dir.exists():
85 if staging_dir.exists():
86 print('purging %s' % staging_dir)
86 print('purging %s' % staging_dir)
87 shutil.rmtree(staging_dir)
87 shutil.rmtree(staging_dir)
88
88
89 # Now assemble all the packaged files into the staging directory.
89 # Now assemble all the packaged files into the staging directory.
90 stage_install(source_dir, staging_dir)
90 stage_install(source_dir, staging_dir)
91
91
92 # We also install some extra files.
92 # We also install some extra files.
93 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
93 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
94
94
95 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
95 # hg.exe depends on VC9 runtime DLLs. Copy those into place.
96 for f in find_legacy_vc_runtime_files(vc_x64):
96 for f in find_legacy_vc_runtime_files(vc_x64):
97 if f.name.endswith('.manifest'):
97 if f.name.endswith('.manifest'):
98 basename = 'Microsoft.VC90.CRT.manifest'
98 basename = 'Microsoft.VC90.CRT.manifest'
99 else:
99 else:
100 basename = f.name
100 basename = f.name
101
101
102 dest_path = staging_dir / basename
102 dest_path = staging_dir / basename
103
103
104 print('copying %s to %s' % (f, dest_path))
104 print('copying %s to %s' % (f, dest_path))
105 shutil.copyfile(f, dest_path)
105 shutil.copyfile(f, dest_path)
106
106
107 build_installer(
107 build_installer(
108 source_dir,
108 source_dir,
109 inno_build_dir,
109 inno_build_dir,
110 staging_dir,
110 staging_dir,
111 iscc_exe,
111 iscc_exe,
112 version,
112 version,
113 arch="x64" if vc_x64 else None,
113 arch="x64" if vc_x64 else None,
114 suffix="-python2",
114 )
115 )
115
116
116
117
117 def build_with_pyoxidizer(
118 def build_with_pyoxidizer(
118 source_dir: pathlib.Path,
119 source_dir: pathlib.Path,
119 build_dir: pathlib.Path,
120 build_dir: pathlib.Path,
120 target_triple: str,
121 target_triple: str,
121 iscc_exe: pathlib.Path,
122 iscc_exe: pathlib.Path,
122 version=None,
123 version=None,
123 ):
124 ):
124 """Build the Inno installer using PyOxidizer."""
125 """Build the Inno installer using PyOxidizer."""
125 if not iscc_exe.exists():
126 if not iscc_exe.exists():
126 raise Exception("%s does not exist" % iscc_exe)
127 raise Exception("%s does not exist" % iscc_exe)
127
128
128 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
129 inno_build_dir = build_dir / ("inno-pyoxidizer-%s" % target_triple)
129 staging_dir = inno_build_dir / "stage"
130 staging_dir = inno_build_dir / "stage"
130
131
131 inno_build_dir.mkdir(parents=True, exist_ok=True)
132 inno_build_dir.mkdir(parents=True, exist_ok=True)
132 run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
133 run_pyoxidizer(source_dir, inno_build_dir, staging_dir, target_triple)
133
134
134 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
135 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
135
136
136 build_installer(
137 build_installer(
137 source_dir,
138 source_dir,
138 inno_build_dir,
139 inno_build_dir,
139 staging_dir,
140 staging_dir,
140 iscc_exe,
141 iscc_exe,
141 version,
142 version,
142 arch="x64" if "x86_64" in target_triple else None,
143 arch="x64" if "x86_64" in target_triple else None,
143 )
144 )
144
145
145
146
146 def build_installer(
147 def build_installer(
147 source_dir: pathlib.Path,
148 source_dir: pathlib.Path,
148 inno_build_dir: pathlib.Path,
149 inno_build_dir: pathlib.Path,
149 staging_dir: pathlib.Path,
150 staging_dir: pathlib.Path,
150 iscc_exe: pathlib.Path,
151 iscc_exe: pathlib.Path,
151 version,
152 version,
152 arch=None,
153 arch=None,
154 suffix="",
153 ):
155 ):
154 """Build an Inno installer from staged Mercurial files.
156 """Build an Inno installer from staged Mercurial files.
155
157
156 This function is agnostic about how to build Mercurial. It just
158 This function is agnostic about how to build Mercurial. It just
157 cares that Mercurial files are in ``staging_dir``.
159 cares that Mercurial files are in ``staging_dir``.
158 """
160 """
159 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
161 inno_source_dir = source_dir / "contrib" / "packaging" / "inno"
160
162
161 # The final package layout is simply a mirror of the staging directory.
163 # The final package layout is simply a mirror of the staging directory.
162 package_files = []
164 package_files = []
163 for root, dirs, files in os.walk(staging_dir):
165 for root, dirs, files in os.walk(staging_dir):
164 dirs.sort()
166 dirs.sort()
165
167
166 root = pathlib.Path(root)
168 root = pathlib.Path(root)
167
169
168 for f in sorted(files):
170 for f in sorted(files):
169 full = root / f
171 full = root / f
170 rel = full.relative_to(staging_dir)
172 rel = full.relative_to(staging_dir)
171 if str(rel.parent) == '.':
173 if str(rel.parent) == '.':
172 dest_dir = '{app}'
174 dest_dir = '{app}'
173 else:
175 else:
174 dest_dir = '{app}\\%s' % rel.parent
176 dest_dir = '{app}\\%s' % rel.parent
175
177
176 package_files.append(
178 package_files.append(
177 {
179 {
178 'source': rel,
180 'source': rel,
179 'dest_dir': dest_dir,
181 'dest_dir': dest_dir,
180 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
182 'metadata': PACKAGE_FILES_METADATA.get(str(rel), None),
181 }
183 }
182 )
184 )
183
185
184 print('creating installer')
186 print('creating installer')
185
187
186 # Install Inno files by rendering a template.
188 # Install Inno files by rendering a template.
187 jinja_env = jinja2.Environment(
189 jinja_env = jinja2.Environment(
188 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
190 loader=jinja2.FileSystemLoader(str(inno_source_dir)),
189 # Need to change these to prevent conflict with Inno Setup.
191 # Need to change these to prevent conflict with Inno Setup.
190 comment_start_string='{##',
192 comment_start_string='{##',
191 comment_end_string='##}',
193 comment_end_string='##}',
192 )
194 )
193
195
194 try:
196 try:
195 template = jinja_env.get_template('mercurial.iss')
197 template = jinja_env.get_template('mercurial.iss')
196 except jinja2.TemplateSyntaxError as e:
198 except jinja2.TemplateSyntaxError as e:
197 raise Exception(
199 raise Exception(
198 'template syntax error at %s:%d: %s'
200 'template syntax error at %s:%d: %s'
199 % (e.name, e.lineno, e.message,)
201 % (e.name, e.lineno, e.message,)
200 )
202 )
201
203
202 content = template.render(package_files=package_files)
204 content = template.render(package_files=package_files)
203
205
204 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
206 with (inno_build_dir / 'mercurial.iss').open('w', encoding='utf-8') as fh:
205 fh.write(content)
207 fh.write(content)
206
208
207 # Copy additional files used by Inno.
209 # Copy additional files used by Inno.
208 for p in ('mercurial.ico', 'postinstall.txt'):
210 for p in ('mercurial.ico', 'postinstall.txt'):
209 shutil.copyfile(
211 shutil.copyfile(
210 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
212 source_dir / 'contrib' / 'win32' / p, inno_build_dir / p
211 )
213 )
212
214
213 args = [str(iscc_exe)]
215 args = [str(iscc_exe)]
214
216
215 if arch:
217 if arch:
216 args.append('/dARCH=%s' % arch)
218 args.append('/dARCH=%s' % arch)
219 args.append('/dSUFFIX=-%s%s' % (arch, suffix))
220 else:
221 args.append('/dSUFFIX=-x86%s' % suffix)
217
222
218 if not version:
223 if not version:
219 version = read_version_py(source_dir)
224 version = read_version_py(source_dir)
220
225
221 args.append('/dVERSION=%s' % version)
226 args.append('/dVERSION=%s' % version)
222 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
227 args.append('/dQUAD_VERSION=%s' % normalize_windows_version(version))
223
228
224 args.append('/Odist')
229 args.append('/Odist')
225 args.append(str(inno_build_dir / 'mercurial.iss'))
230 args.append(str(inno_build_dir / 'mercurial.iss'))
226
231
227 subprocess.run(args, cwd=str(source_dir), check=True)
232 subprocess.run(args, cwd=str(source_dir), check=True)
@@ -1,531 +1,535 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 run_pyoxidizer
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
42
43 EXTRA_INSTALL_RULES = [
43 EXTRA_INSTALL_RULES = [
44 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
44 ('contrib/packaging/wix/COPYING.rtf', 'COPYING.rtf'),
45 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
45 ('contrib/win32/mercurial.ini', 'defaultrc/mercurial.rc'),
46 ]
46 ]
47
47
48 STAGING_REMOVE_FILES = [
48 STAGING_REMOVE_FILES = [
49 # We use the RTF variant.
49 # We use the RTF variant.
50 'copying.txt',
50 'copying.txt',
51 ]
51 ]
52
52
53 SHORTCUTS = {
53 SHORTCUTS = {
54 # hg.1.html'
54 # hg.1.html'
55 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
55 'hg.file.5d3e441c_28d9_5542_afd0_cdd4234f12d5': {
56 'Name': 'Mercurial Command Reference',
56 'Name': 'Mercurial Command Reference',
57 },
57 },
58 # hgignore.5.html
58 # hgignore.5.html
59 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
59 'hg.file.5757d8e0_f207_5e10_a2ec_3ba0a062f431': {
60 'Name': 'Mercurial Ignore Files',
60 'Name': 'Mercurial Ignore Files',
61 },
61 },
62 # hgrc.5.html
62 # hgrc.5.html
63 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
63 'hg.file.92e605fd_1d1a_5dc6_9fc0_5d2998eb8f5e': {
64 'Name': 'Mercurial Configuration Files',
64 'Name': 'Mercurial Configuration Files',
65 },
65 },
66 }
66 }
67
67
68
68
69 def find_version(source_dir: pathlib.Path):
69 def find_version(source_dir: pathlib.Path):
70 version_py = source_dir / 'mercurial' / '__version__.py'
70 version_py = source_dir / 'mercurial' / '__version__.py'
71
71
72 with version_py.open('r', encoding='utf-8') as fh:
72 with version_py.open('r', encoding='utf-8') as fh:
73 source = fh.read().strip()
73 source = fh.read().strip()
74
74
75 m = re.search('version = b"(.*)"', source)
75 m = re.search('version = b"(.*)"', source)
76 return m.group(1)
76 return m.group(1)
77
77
78
78
79 def ensure_vc90_merge_modules(build_dir):
79 def ensure_vc90_merge_modules(build_dir):
80 x86 = (
80 x86 = (
81 download_entry(
81 download_entry(
82 'vc9-crt-x86-msm',
82 'vc9-crt-x86-msm',
83 build_dir,
83 build_dir,
84 local_name='microsoft.vcxx.crt.x86_msm.msm',
84 local_name='microsoft.vcxx.crt.x86_msm.msm',
85 )[0],
85 )[0],
86 download_entry(
86 download_entry(
87 'vc9-crt-x86-msm-policy',
87 'vc9-crt-x86-msm-policy',
88 build_dir,
88 build_dir,
89 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
89 local_name='policy.x.xx.microsoft.vcxx.crt.x86_msm.msm',
90 )[0],
90 )[0],
91 )
91 )
92
92
93 x64 = (
93 x64 = (
94 download_entry(
94 download_entry(
95 'vc9-crt-x64-msm',
95 'vc9-crt-x64-msm',
96 build_dir,
96 build_dir,
97 local_name='microsoft.vcxx.crt.x64_msm.msm',
97 local_name='microsoft.vcxx.crt.x64_msm.msm',
98 )[0],
98 )[0],
99 download_entry(
99 download_entry(
100 'vc9-crt-x64-msm-policy',
100 'vc9-crt-x64-msm-policy',
101 build_dir,
101 build_dir,
102 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
102 local_name='policy.x.xx.microsoft.vcxx.crt.x64_msm.msm',
103 )[0],
103 )[0],
104 )
104 )
105 return {
105 return {
106 'x86': x86,
106 'x86': x86,
107 'x64': x64,
107 'x64': x64,
108 }
108 }
109
109
110
110
111 def run_candle(wix, cwd, wxs, source_dir, defines=None):
111 def run_candle(wix, cwd, wxs, source_dir, defines=None):
112 args = [
112 args = [
113 str(wix / 'candle.exe'),
113 str(wix / 'candle.exe'),
114 '-nologo',
114 '-nologo',
115 str(wxs),
115 str(wxs),
116 '-dSourceDir=%s' % source_dir,
116 '-dSourceDir=%s' % source_dir,
117 ]
117 ]
118
118
119 if defines:
119 if defines:
120 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
120 args.extend('-d%s=%s' % define for define in sorted(defines.items()))
121
121
122 subprocess.run(args, cwd=str(cwd), check=True)
122 subprocess.run(args, cwd=str(cwd), check=True)
123
123
124
124
125 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
125 def make_files_xml(staging_dir: pathlib.Path, is_x64) -> str:
126 """Create XML string listing every file to be installed."""
126 """Create XML string listing every file to be installed."""
127
127
128 # We derive GUIDs from a deterministic file path identifier.
128 # We derive GUIDs from a deterministic file path identifier.
129 # We shoehorn the name into something that looks like a URL because
129 # We shoehorn the name into something that looks like a URL because
130 # the UUID namespaces are supposed to work that way (even though
130 # the UUID namespaces are supposed to work that way (even though
131 # the input data probably is never validated).
131 # the input data probably is never validated).
132
132
133 doc = xml.dom.minidom.parseString(
133 doc = xml.dom.minidom.parseString(
134 '<?xml version="1.0" encoding="utf-8"?>'
134 '<?xml version="1.0" encoding="utf-8"?>'
135 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
135 '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">'
136 '</Wix>'
136 '</Wix>'
137 )
137 )
138
138
139 # Assemble the install layout by directory. This makes it easier to
139 # Assemble the install layout by directory. This makes it easier to
140 # emit XML, since each directory has separate entities.
140 # emit XML, since each directory has separate entities.
141 manifest = collections.defaultdict(dict)
141 manifest = collections.defaultdict(dict)
142
142
143 for root, dirs, files in os.walk(staging_dir):
143 for root, dirs, files in os.walk(staging_dir):
144 dirs.sort()
144 dirs.sort()
145
145
146 root = pathlib.Path(root)
146 root = pathlib.Path(root)
147 rel_dir = root.relative_to(staging_dir)
147 rel_dir = root.relative_to(staging_dir)
148
148
149 for i in range(len(rel_dir.parts)):
149 for i in range(len(rel_dir.parts)):
150 parent = '/'.join(rel_dir.parts[0 : i + 1])
150 parent = '/'.join(rel_dir.parts[0 : i + 1])
151 manifest.setdefault(parent, {})
151 manifest.setdefault(parent, {})
152
152
153 for f in sorted(files):
153 for f in sorted(files):
154 full = root / f
154 full = root / f
155 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
155 manifest[str(rel_dir).replace('\\', '/')][full.name] = full
156
156
157 component_groups = collections.defaultdict(list)
157 component_groups = collections.defaultdict(list)
158
158
159 # Now emit a <Fragment> for each directory.
159 # Now emit a <Fragment> for each directory.
160 # Each directory is composed of a <DirectoryRef> pointing to its parent
160 # Each directory is composed of a <DirectoryRef> pointing to its parent
161 # and defines child <Directory>'s and a <Component> with all the files.
161 # and defines child <Directory>'s and a <Component> with all the files.
162 for dir_name, entries in sorted(manifest.items()):
162 for dir_name, entries in sorted(manifest.items()):
163 # The directory id is derived from the path. But the root directory
163 # The directory id is derived from the path. But the root directory
164 # is special.
164 # is special.
165 if dir_name == '.':
165 if dir_name == '.':
166 parent_directory_id = 'INSTALLDIR'
166 parent_directory_id = 'INSTALLDIR'
167 else:
167 else:
168 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
168 parent_directory_id = 'hg.dir.%s' % dir_name.replace('/', '.')
169
169
170 fragment = doc.createElement('Fragment')
170 fragment = doc.createElement('Fragment')
171 directory_ref = doc.createElement('DirectoryRef')
171 directory_ref = doc.createElement('DirectoryRef')
172 directory_ref.setAttribute('Id', parent_directory_id)
172 directory_ref.setAttribute('Id', parent_directory_id)
173
173
174 # Add <Directory> entries for immediate children directories.
174 # Add <Directory> entries for immediate children directories.
175 for possible_child in sorted(manifest.keys()):
175 for possible_child in sorted(manifest.keys()):
176 if (
176 if (
177 dir_name == '.'
177 dir_name == '.'
178 and '/' not in possible_child
178 and '/' not in possible_child
179 and possible_child != '.'
179 and possible_child != '.'
180 ):
180 ):
181 child_directory_id = 'hg.dir.%s' % possible_child
181 child_directory_id = 'hg.dir.%s' % possible_child
182 name = possible_child
182 name = possible_child
183 else:
183 else:
184 if not possible_child.startswith('%s/' % dir_name):
184 if not possible_child.startswith('%s/' % dir_name):
185 continue
185 continue
186 name = possible_child[len(dir_name) + 1 :]
186 name = possible_child[len(dir_name) + 1 :]
187 if '/' in name:
187 if '/' in name:
188 continue
188 continue
189
189
190 child_directory_id = 'hg.dir.%s' % possible_child.replace(
190 child_directory_id = 'hg.dir.%s' % possible_child.replace(
191 '/', '.'
191 '/', '.'
192 )
192 )
193
193
194 directory = doc.createElement('Directory')
194 directory = doc.createElement('Directory')
195 directory.setAttribute('Id', child_directory_id)
195 directory.setAttribute('Id', child_directory_id)
196 directory.setAttribute('Name', name)
196 directory.setAttribute('Name', name)
197 directory_ref.appendChild(directory)
197 directory_ref.appendChild(directory)
198
198
199 # Add <Component>s for files in this directory.
199 # Add <Component>s for files in this directory.
200 for rel, source_path in sorted(entries.items()):
200 for rel, source_path in sorted(entries.items()):
201 if dir_name == '.':
201 if dir_name == '.':
202 full_rel = rel
202 full_rel = rel
203 else:
203 else:
204 full_rel = '%s/%s' % (dir_name, rel)
204 full_rel = '%s/%s' % (dir_name, rel)
205
205
206 component_unique_id = (
206 component_unique_id = (
207 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
207 'https://www.mercurial-scm.org/wix-installer/0/component/%s'
208 % full_rel
208 % full_rel
209 )
209 )
210 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
210 component_guid = uuid.uuid5(uuid.NAMESPACE_URL, component_unique_id)
211 component_id = 'hg.component.%s' % str(component_guid).replace(
211 component_id = 'hg.component.%s' % str(component_guid).replace(
212 '-', '_'
212 '-', '_'
213 )
213 )
214
214
215 component = doc.createElement('Component')
215 component = doc.createElement('Component')
216
216
217 component.setAttribute('Id', component_id)
217 component.setAttribute('Id', component_id)
218 component.setAttribute('Guid', str(component_guid).upper())
218 component.setAttribute('Guid', str(component_guid).upper())
219 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
219 component.setAttribute('Win64', 'yes' if is_x64 else 'no')
220
220
221 # Assign this component to a top-level group.
221 # Assign this component to a top-level group.
222 if dir_name == '.':
222 if dir_name == '.':
223 component_groups['ROOT'].append(component_id)
223 component_groups['ROOT'].append(component_id)
224 elif '/' in dir_name:
224 elif '/' in dir_name:
225 component_groups[dir_name[0 : dir_name.index('/')]].append(
225 component_groups[dir_name[0 : dir_name.index('/')]].append(
226 component_id
226 component_id
227 )
227 )
228 else:
228 else:
229 component_groups[dir_name].append(component_id)
229 component_groups[dir_name].append(component_id)
230
230
231 unique_id = (
231 unique_id = (
232 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
232 'https://www.mercurial-scm.org/wix-installer/0/%s' % full_rel
233 )
233 )
234 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
234 file_guid = uuid.uuid5(uuid.NAMESPACE_URL, unique_id)
235
235
236 # IDs have length limits. So use GUID to derive them.
236 # IDs have length limits. So use GUID to derive them.
237 file_guid_normalized = str(file_guid).replace('-', '_')
237 file_guid_normalized = str(file_guid).replace('-', '_')
238 file_id = 'hg.file.%s' % file_guid_normalized
238 file_id = 'hg.file.%s' % file_guid_normalized
239
239
240 file_element = doc.createElement('File')
240 file_element = doc.createElement('File')
241 file_element.setAttribute('Id', file_id)
241 file_element.setAttribute('Id', file_id)
242 file_element.setAttribute('Source', str(source_path))
242 file_element.setAttribute('Source', str(source_path))
243 file_element.setAttribute('KeyPath', 'yes')
243 file_element.setAttribute('KeyPath', 'yes')
244 file_element.setAttribute('ReadOnly', 'yes')
244 file_element.setAttribute('ReadOnly', 'yes')
245
245
246 component.appendChild(file_element)
246 component.appendChild(file_element)
247 directory_ref.appendChild(component)
247 directory_ref.appendChild(component)
248
248
249 fragment.appendChild(directory_ref)
249 fragment.appendChild(directory_ref)
250 doc.documentElement.appendChild(fragment)
250 doc.documentElement.appendChild(fragment)
251
251
252 for group, component_ids in sorted(component_groups.items()):
252 for group, component_ids in sorted(component_groups.items()):
253 fragment = doc.createElement('Fragment')
253 fragment = doc.createElement('Fragment')
254 component_group = doc.createElement('ComponentGroup')
254 component_group = doc.createElement('ComponentGroup')
255 component_group.setAttribute('Id', 'hg.group.%s' % group)
255 component_group.setAttribute('Id', 'hg.group.%s' % group)
256
256
257 for component_id in component_ids:
257 for component_id in component_ids:
258 component_ref = doc.createElement('ComponentRef')
258 component_ref = doc.createElement('ComponentRef')
259 component_ref.setAttribute('Id', component_id)
259 component_ref.setAttribute('Id', component_id)
260 component_group.appendChild(component_ref)
260 component_group.appendChild(component_ref)
261
261
262 fragment.appendChild(component_group)
262 fragment.appendChild(component_group)
263 doc.documentElement.appendChild(fragment)
263 doc.documentElement.appendChild(fragment)
264
264
265 # Add <Shortcut> to files that have it defined.
265 # Add <Shortcut> to files that have it defined.
266 for file_id, metadata in sorted(SHORTCUTS.items()):
266 for file_id, metadata in sorted(SHORTCUTS.items()):
267 els = doc.getElementsByTagName('File')
267 els = doc.getElementsByTagName('File')
268 els = [el for el in els if el.getAttribute('Id') == file_id]
268 els = [el for el in els if el.getAttribute('Id') == file_id]
269
269
270 if not els:
270 if not els:
271 raise Exception('could not find File[Id=%s]' % file_id)
271 raise Exception('could not find File[Id=%s]' % file_id)
272
272
273 for el in els:
273 for el in els:
274 shortcut = doc.createElement('Shortcut')
274 shortcut = doc.createElement('Shortcut')
275 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
275 shortcut.setAttribute('Id', 'hg.shortcut.%s' % file_id)
276 shortcut.setAttribute('Directory', 'ProgramMenuDir')
276 shortcut.setAttribute('Directory', 'ProgramMenuDir')
277 shortcut.setAttribute('Icon', 'hgIcon.ico')
277 shortcut.setAttribute('Icon', 'hgIcon.ico')
278 shortcut.setAttribute('IconIndex', '0')
278 shortcut.setAttribute('IconIndex', '0')
279 shortcut.setAttribute('Advertise', 'yes')
279 shortcut.setAttribute('Advertise', 'yes')
280 for k, v in sorted(metadata.items()):
280 for k, v in sorted(metadata.items()):
281 shortcut.setAttribute(k, v)
281 shortcut.setAttribute(k, v)
282
282
283 el.appendChild(shortcut)
283 el.appendChild(shortcut)
284
284
285 return doc.toprettyxml()
285 return doc.toprettyxml()
286
286
287
287
288 def build_installer_py2exe(
288 def build_installer_py2exe(
289 source_dir: pathlib.Path,
289 source_dir: pathlib.Path,
290 python_exe: pathlib.Path,
290 python_exe: pathlib.Path,
291 msi_name='mercurial',
291 msi_name='mercurial',
292 version=None,
292 version=None,
293 extra_packages_script=None,
293 extra_packages_script=None,
294 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
294 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
295 extra_features: typing.Optional[typing.List[str]] = None,
295 extra_features: typing.Optional[typing.List[str]] = None,
296 signing_info: typing.Optional[typing.Dict[str, str]] = None,
296 signing_info: typing.Optional[typing.Dict[str, str]] = None,
297 ):
297 ):
298 """Build a WiX MSI installer using py2exe.
298 """Build a WiX MSI installer using py2exe.
299
299
300 ``source_dir`` is the path to the Mercurial source tree to use.
300 ``source_dir`` is the path to the Mercurial source tree to use.
301 ``arch`` is the target architecture. either ``x86`` or ``x64``.
301 ``arch`` is the target architecture. either ``x86`` or ``x64``.
302 ``python_exe`` is the path to the Python executable to use/bundle.
302 ``python_exe`` is the path to the Python executable to use/bundle.
303 ``version`` is the Mercurial version string. If not defined,
303 ``version`` is the Mercurial version string. If not defined,
304 ``mercurial/__version__.py`` will be consulted.
304 ``mercurial/__version__.py`` will be consulted.
305 ``extra_packages_script`` is a command to be run to inject extra packages
305 ``extra_packages_script`` is a command to be run to inject extra packages
306 into the py2exe binary. It should stage packages into the virtualenv and
306 into the py2exe binary. It should stage packages into the virtualenv and
307 print a null byte followed by a newline-separated list of packages that
307 print a null byte followed by a newline-separated list of packages that
308 should be included in the exe.
308 should be included in the exe.
309 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
309 ``extra_wxs`` is a dict of {wxs_name: working_dir_for_wxs_build}.
310 ``extra_features`` is a list of additional named Features to include in
310 ``extra_features`` is a list of additional named Features to include in
311 the build. These must match Feature names in one of the wxs scripts.
311 the build. These must match Feature names in one of the wxs scripts.
312 """
312 """
313 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
313 arch = 'x64' if r'\x64' in os.environ.get('LIB', '') else 'x86'
314
314
315 hg_build_dir = source_dir / 'build'
315 hg_build_dir = source_dir / 'build'
316
316
317 requirements_txt = (
317 requirements_txt = (
318 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
318 source_dir / 'contrib' / 'packaging' / 'requirements_win32.txt'
319 )
319 )
320
320
321 build_py2exe(
321 build_py2exe(
322 source_dir,
322 source_dir,
323 hg_build_dir,
323 hg_build_dir,
324 python_exe,
324 python_exe,
325 'wix',
325 'wix',
326 requirements_txt,
326 requirements_txt,
327 extra_packages=EXTRA_PACKAGES,
327 extra_packages=EXTRA_PACKAGES,
328 extra_packages_script=extra_packages_script,
328 extra_packages_script=extra_packages_script,
329 )
329 )
330
330
331 build_dir = hg_build_dir / ('wix-%s' % arch)
331 build_dir = hg_build_dir / ('wix-%s' % arch)
332 staging_dir = build_dir / 'stage'
332 staging_dir = build_dir / 'stage'
333
333
334 build_dir.mkdir(exist_ok=True)
334 build_dir.mkdir(exist_ok=True)
335
335
336 # Purge the staging directory for every build so packaging is pristine.
336 # Purge the staging directory for every build so packaging is pristine.
337 if staging_dir.exists():
337 if staging_dir.exists():
338 print('purging %s' % staging_dir)
338 print('purging %s' % staging_dir)
339 shutil.rmtree(staging_dir)
339 shutil.rmtree(staging_dir)
340
340
341 stage_install(source_dir, staging_dir, lower_case=True)
341 stage_install(source_dir, staging_dir, lower_case=True)
342
342
343 # We also install some extra files.
343 # We also install some extra files.
344 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
344 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
345
345
346 # And remove some files we don't want.
346 # And remove some files we don't want.
347 for f in STAGING_REMOVE_FILES:
347 for f in STAGING_REMOVE_FILES:
348 p = staging_dir / f
348 p = staging_dir / f
349 if p.exists():
349 if p.exists():
350 print('removing %s' % p)
350 print('removing %s' % p)
351 p.unlink()
351 p.unlink()
352
352
353 return run_wix_packaging(
353 return run_wix_packaging(
354 source_dir,
354 source_dir,
355 build_dir,
355 build_dir,
356 staging_dir,
356 staging_dir,
357 arch,
357 arch,
358 version=version,
358 version=version,
359 python2=True,
359 python2=True,
360 msi_name=msi_name,
360 msi_name=msi_name,
361 suffix="-python2",
361 extra_wxs=extra_wxs,
362 extra_wxs=extra_wxs,
362 extra_features=extra_features,
363 extra_features=extra_features,
363 signing_info=signing_info,
364 signing_info=signing_info,
364 )
365 )
365
366
366
367
367 def build_installer_pyoxidizer(
368 def build_installer_pyoxidizer(
368 source_dir: pathlib.Path,
369 source_dir: pathlib.Path,
369 target_triple: str,
370 target_triple: str,
370 msi_name='mercurial',
371 msi_name='mercurial',
371 version=None,
372 version=None,
372 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
373 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
373 extra_features: typing.Optional[typing.List[str]] = None,
374 extra_features: typing.Optional[typing.List[str]] = None,
374 signing_info: typing.Optional[typing.Dict[str, str]] = None,
375 signing_info: typing.Optional[typing.Dict[str, str]] = None,
375 ):
376 ):
376 """Build a WiX MSI installer using PyOxidizer."""
377 """Build a WiX MSI installer using PyOxidizer."""
377 hg_build_dir = source_dir / "build"
378 hg_build_dir = source_dir / "build"
378 build_dir = hg_build_dir / ("wix-%s" % target_triple)
379 build_dir = hg_build_dir / ("wix-%s" % target_triple)
379 staging_dir = build_dir / "stage"
380 staging_dir = build_dir / "stage"
380
381
381 arch = "x64" if "x86_64" in target_triple else "x86"
382 arch = "x64" if "x86_64" in target_triple else "x86"
382
383
383 build_dir.mkdir(parents=True, exist_ok=True)
384 build_dir.mkdir(parents=True, exist_ok=True)
384 run_pyoxidizer(source_dir, build_dir, staging_dir, target_triple)
385 run_pyoxidizer(source_dir, build_dir, staging_dir, target_triple)
385
386
386 # We also install some extra files.
387 # We also install some extra files.
387 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
388 process_install_rules(EXTRA_INSTALL_RULES, source_dir, staging_dir)
388
389
389 # And remove some files we don't want.
390 # And remove some files we don't want.
390 for f in STAGING_REMOVE_FILES:
391 for f in STAGING_REMOVE_FILES:
391 p = staging_dir / f
392 p = staging_dir / f
392 if p.exists():
393 if p.exists():
393 print('removing %s' % p)
394 print('removing %s' % p)
394 p.unlink()
395 p.unlink()
395
396
396 return run_wix_packaging(
397 return run_wix_packaging(
397 source_dir,
398 source_dir,
398 build_dir,
399 build_dir,
399 staging_dir,
400 staging_dir,
400 arch,
401 arch,
401 version,
402 version,
402 python2=False,
403 python2=False,
403 msi_name=msi_name,
404 msi_name=msi_name,
404 extra_wxs=extra_wxs,
405 extra_wxs=extra_wxs,
405 extra_features=extra_features,
406 extra_features=extra_features,
406 signing_info=signing_info,
407 signing_info=signing_info,
407 )
408 )
408
409
409
410
410 def run_wix_packaging(
411 def run_wix_packaging(
411 source_dir: pathlib.Path,
412 source_dir: pathlib.Path,
412 build_dir: pathlib.Path,
413 build_dir: pathlib.Path,
413 staging_dir: pathlib.Path,
414 staging_dir: pathlib.Path,
414 arch: str,
415 arch: str,
415 version: str,
416 version: str,
416 python2: bool,
417 python2: bool,
417 msi_name: typing.Optional[str] = "mercurial",
418 msi_name: typing.Optional[str] = "mercurial",
419 suffix: str = "",
418 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
420 extra_wxs: typing.Optional[typing.Dict[str, str]] = None,
419 extra_features: typing.Optional[typing.List[str]] = None,
421 extra_features: typing.Optional[typing.List[str]] = None,
420 signing_info: typing.Optional[typing.Dict[str, str]] = None,
422 signing_info: typing.Optional[typing.Dict[str, str]] = None,
421 ):
423 ):
422 """Invokes WiX to package up a built Mercurial.
424 """Invokes WiX to package up a built Mercurial.
423
425
424 ``signing_info`` is a dict defining properties to facilitate signing the
426 ``signing_info`` is a dict defining properties to facilitate signing the
425 installer. Recognized keys include ``name``, ``subject_name``,
427 installer. Recognized keys include ``name``, ``subject_name``,
426 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
428 ``cert_path``, ``cert_password``, and ``timestamp_url``. If populated,
427 we will sign both the hg.exe and the .msi using the signing credentials
429 we will sign both the hg.exe and the .msi using the signing credentials
428 specified.
430 specified.
429 """
431 """
430
432
431 orig_version = version or find_version(source_dir)
433 orig_version = version or find_version(source_dir)
432 version = normalize_windows_version(orig_version)
434 version = normalize_windows_version(orig_version)
433 print('using version string: %s' % version)
435 print('using version string: %s' % version)
434 if version != orig_version:
436 if version != orig_version:
435 print('(normalized from: %s)' % orig_version)
437 print('(normalized from: %s)' % orig_version)
436
438
437 if signing_info:
439 if signing_info:
438 sign_with_signtool(
440 sign_with_signtool(
439 staging_dir / "hg.exe",
441 staging_dir / "hg.exe",
440 "%s %s" % (signing_info["name"], version),
442 "%s %s" % (signing_info["name"], version),
441 subject_name=signing_info["subject_name"],
443 subject_name=signing_info["subject_name"],
442 cert_path=signing_info["cert_path"],
444 cert_path=signing_info["cert_path"],
443 cert_password=signing_info["cert_password"],
445 cert_password=signing_info["cert_password"],
444 timestamp_url=signing_info["timestamp_url"],
446 timestamp_url=signing_info["timestamp_url"],
445 )
447 )
446
448
447 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
449 wix_dir = source_dir / 'contrib' / 'packaging' / 'wix'
448
450
449 wix_pkg, wix_entry = download_entry('wix', build_dir)
451 wix_pkg, wix_entry = download_entry('wix', build_dir)
450 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
452 wix_path = build_dir / ('wix-%s' % wix_entry['version'])
451
453
452 if not wix_path.exists():
454 if not wix_path.exists():
453 extract_zip_to_directory(wix_pkg, wix_path)
455 extract_zip_to_directory(wix_pkg, wix_path)
454
456
455 if python2:
457 if python2:
456 ensure_vc90_merge_modules(build_dir)
458 ensure_vc90_merge_modules(build_dir)
457
459
458 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
460 source_build_rel = pathlib.Path(os.path.relpath(source_dir, build_dir))
459
461
460 defines = {'Platform': arch}
462 defines = {'Platform': arch}
461
463
462 # Derive a .wxs file with the staged files.
464 # Derive a .wxs file with the staged files.
463 manifest_wxs = build_dir / 'stage.wxs'
465 manifest_wxs = build_dir / 'stage.wxs'
464 with manifest_wxs.open('w', encoding='utf-8') as fh:
466 with manifest_wxs.open('w', encoding='utf-8') as fh:
465 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
467 fh.write(make_files_xml(staging_dir, is_x64=arch == 'x64'))
466
468
467 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
469 run_candle(wix_path, build_dir, manifest_wxs, staging_dir, defines=defines)
468
470
469 for source, rel_path in sorted((extra_wxs or {}).items()):
471 for source, rel_path in sorted((extra_wxs or {}).items()):
470 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
472 run_candle(wix_path, build_dir, source, rel_path, defines=defines)
471
473
472 source = wix_dir / 'mercurial.wxs'
474 source = wix_dir / 'mercurial.wxs'
473 defines['Version'] = version
475 defines['Version'] = version
474 defines['Comments'] = 'Installs Mercurial version %s' % version
476 defines['Comments'] = 'Installs Mercurial version %s' % version
475
477
476 if python2:
478 if python2:
477 defines["PythonVersion"] = "2"
479 defines["PythonVersion"] = "2"
478 defines['VCRedistSrcDir'] = str(build_dir)
480 defines['VCRedistSrcDir'] = str(build_dir)
479 else:
481 else:
480 defines["PythonVersion"] = "3"
482 defines["PythonVersion"] = "3"
481
483
482 if (staging_dir / "lib").exists():
484 if (staging_dir / "lib").exists():
483 defines["MercurialHasLib"] = "1"
485 defines["MercurialHasLib"] = "1"
484
486
485 if extra_features:
487 if extra_features:
486 assert all(';' not in f for f in extra_features)
488 assert all(';' not in f for f in extra_features)
487 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
489 defines['MercurialExtraFeatures'] = ';'.join(extra_features)
488
490
489 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
491 run_candle(wix_path, build_dir, source, source_build_rel, defines=defines)
490
492
491 msi_path = (
493 msi_path = (
492 source_dir / 'dist' / ('%s-%s-%s.msi' % (msi_name, orig_version, arch))
494 source_dir
495 / 'dist'
496 / ('%s-%s-%s%s.msi' % (msi_name, orig_version, arch, suffix))
493 )
497 )
494
498
495 args = [
499 args = [
496 str(wix_path / 'light.exe'),
500 str(wix_path / 'light.exe'),
497 '-nologo',
501 '-nologo',
498 '-ext',
502 '-ext',
499 'WixUIExtension',
503 'WixUIExtension',
500 '-sw1076',
504 '-sw1076',
501 '-spdb',
505 '-spdb',
502 '-o',
506 '-o',
503 str(msi_path),
507 str(msi_path),
504 ]
508 ]
505
509
506 for source, rel_path in sorted((extra_wxs or {}).items()):
510 for source, rel_path in sorted((extra_wxs or {}).items()):
507 assert source.endswith('.wxs')
511 assert source.endswith('.wxs')
508 source = os.path.basename(source)
512 source = os.path.basename(source)
509 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
513 args.append(str(build_dir / ('%s.wixobj' % source[:-4])))
510
514
511 args.extend(
515 args.extend(
512 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
516 [str(build_dir / 'stage.wixobj'), str(build_dir / 'mercurial.wixobj'),]
513 )
517 )
514
518
515 subprocess.run(args, cwd=str(source_dir), check=True)
519 subprocess.run(args, cwd=str(source_dir), check=True)
516
520
517 print('%s created' % msi_path)
521 print('%s created' % msi_path)
518
522
519 if signing_info:
523 if signing_info:
520 sign_with_signtool(
524 sign_with_signtool(
521 msi_path,
525 msi_path,
522 "%s %s" % (signing_info["name"], version),
526 "%s %s" % (signing_info["name"], version),
523 subject_name=signing_info["subject_name"],
527 subject_name=signing_info["subject_name"],
524 cert_path=signing_info["cert_path"],
528 cert_path=signing_info["cert_path"],
525 cert_password=signing_info["cert_password"],
529 cert_password=signing_info["cert_password"],
526 timestamp_url=signing_info["timestamp_url"],
530 timestamp_url=signing_info["timestamp_url"],
527 )
531 )
528
532
529 return {
533 return {
530 'msi_path': msi_path,
534 'msi_path': msi_path,
531 }
535 }
@@ -1,83 +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-2020 Matt Mackall and others
9 AppCopyright=Copyright 2005-2020 Matt Mackall and others
10 AppName=Mercurial
10 AppName=Mercurial
11 AppVersion={#VERSION}
11 AppVersion={#VERSION}
12 OutputBaseFilename=Mercurial-{#VERSION}{#SUFFIX}
12 #if ARCH == "x64"
13 #if ARCH == "x64"
13 AppVerName=Mercurial {#VERSION} (64-bit)
14 AppVerName=Mercurial {#VERSION} (64-bit)
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}
20 #endif
19 #endif
21 InfoAfterFile=../postinstall.txt
20 InfoAfterFile=../postinstall.txt
22 LicenseFile=Copying.txt
21 LicenseFile=Copying.txt
23 ShowLanguageDialog=yes
22 ShowLanguageDialog=yes
24 AppPublisher=Matt Mackall and others
23 AppPublisher=Matt Mackall and others
25 AppPublisherURL=https://mercurial-scm.org/
24 AppPublisherURL=https://mercurial-scm.org/
26 AppSupportURL=https://mercurial-scm.org/
25 AppSupportURL=https://mercurial-scm.org/
27 AppUpdatesURL=https://mercurial-scm.org/
26 AppUpdatesURL=https://mercurial-scm.org/
28 {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }}
27 {{ 'AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3}' }}
29 AppContact=mercurial@mercurial-scm.org
28 AppContact=mercurial@mercurial-scm.org
30 DefaultDirName={pf}\Mercurial
29 DefaultDirName={pf}\Mercurial
31 SourceDir=stage
30 SourceDir=stage
32 VersionInfoDescription=Mercurial distributed SCM (version {#VERSION})
31 VersionInfoDescription=Mercurial distributed SCM (version {#VERSION})
33 VersionInfoCopyright=Copyright 2005-2020 Matt Mackall and others
32 VersionInfoCopyright=Copyright 2005-2020 Matt Mackall and others
34 VersionInfoCompany=Matt Mackall and others
33 VersionInfoCompany=Matt Mackall and others
35 VersionInfoVersion={#QUAD_VERSION}
34 VersionInfoVersion={#QUAD_VERSION}
36 InternalCompressLevel=max
35 InternalCompressLevel=max
37 SolidCompression=true
36 SolidCompression=true
38 SetupIconFile=../mercurial.ico
37 SetupIconFile=../mercurial.ico
39 AllowNoIcons=true
38 AllowNoIcons=true
40 DefaultGroupName=Mercurial
39 DefaultGroupName=Mercurial
41 PrivilegesRequired=none
40 PrivilegesRequired=none
42 ChangesEnvironment=true
41 ChangesEnvironment=true
43
42
44 [Files]
43 [Files]
45 {% for entry in package_files -%}
44 {% for entry in package_files -%}
46 Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }}
45 Source: {{ entry.source }}; DestDir: {{ entry.dest_dir }}
47 {%- if entry.metadata %}; {{ entry.metadata }}{% endif %}
46 {%- if entry.metadata %}; {{ entry.metadata }}{% endif %}
48 {% endfor %}
47 {% endfor %}
49
48
50 [INI]
49 [INI]
51 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/
52
51
53 [UninstallDelete]
52 [UninstallDelete]
54 Type: files; Name: {app}\Mercurial.url
53 Type: files; Name: {app}\Mercurial.url
55 Type: filesandordirs; Name: {app}\defaultrc
54 Type: filesandordirs; Name: {app}\defaultrc
56
55
57 [Icons]
56 [Icons]
58 Name: {group}\Uninstall Mercurial; Filename: {uninstallexe}
57 Name: {group}\Uninstall Mercurial; Filename: {uninstallexe}
59 Name: {group}\Mercurial Command Reference; Filename: {app}\Docs\hg.1.html
58 Name: {group}\Mercurial Command Reference; Filename: {app}\Docs\hg.1.html
60 Name: {group}\Mercurial Configuration Files; Filename: {app}\Docs\hgrc.5.html
59 Name: {group}\Mercurial Configuration Files; Filename: {app}\Docs\hgrc.5.html
61 Name: {group}\Mercurial Ignore Files; Filename: {app}\Docs\hgignore.5.html
60 Name: {group}\Mercurial Ignore Files; Filename: {app}\Docs\hgignore.5.html
62 Name: {group}\Mercurial Web Site; Filename: {app}\Mercurial.url
61 Name: {group}\Mercurial Web Site; Filename: {app}\Mercurial.url
63
62
64 [Tasks]
63 [Tasks]
65 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
66
65
67 [Code]
66 [Code]
68 procedure Touch(fn: String);
67 procedure Touch(fn: String);
69 begin
68 begin
70 SaveStringToFile(ExpandConstant(fn), '', False);
69 SaveStringToFile(ExpandConstant(fn), '', False);
71 end;
70 end;
72
71
73 const
72 const
74 ModPathName = 'modifypath';
73 ModPathName = 'modifypath';
75 ModPathType = 'user';
74 ModPathType = 'user';
76
75
77 function ModPathDir(): TArrayOfString;
76 function ModPathDir(): TArrayOfString;
78 begin
77 begin
79 setArrayLength(Result, 1)
78 setArrayLength(Result, 1)
80 Result[0] := ExpandConstant('{app}');
79 Result[0] := ExpandConstant('{app}');
81 end;
80 end;
82
81
83 {% include 'modpath.iss' %}
82 {% include 'modpath.iss' %}
General Comments 0
You need to be logged in to leave comments. Login now