##// END OF EJS Templates
automation: add extra arguments when building Inno...
Gregory Szorc -
r45277:47609da1 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 {extra_args}
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}-x86-python2.exe'
111 X86_EXE_FILENAME = 'Mercurial-{version}-x86-python2.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64-python2.exe'
112 X64_EXE_FILENAME = 'Mercurial-{version}-x64-python2.exe'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
113 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
114 X64_MSI_FILENAME = 'mercurial-{version}-x64-python2.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 )
General Comments 0
You need to be logged in to leave comments. Login now