##// END OF EJS Templates
automation: upload Python 3.9 Windows wheels...
Gregory Szorc -
r46345:d1ce0ffd stable
parent child Browse files
Show More
@@ -1,663 +1,669 b''
1 1 # windows.py - Automation specific to Windows
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import datetime
11 11 import os
12 12 import paramiko
13 13 import pathlib
14 14 import re
15 15 import subprocess
16 16 import tempfile
17 17
18 18 from .pypi import upload as pypi_upload
19 19 from .winrm import run_powershell
20 20
21 21
22 22 # PowerShell commands to activate a Visual Studio 2008 environment.
23 23 # This is essentially a port of vcvarsall.bat to PowerShell.
24 24 ACTIVATE_VC9_AMD64 = r'''
25 25 Write-Output "activating Visual Studio 2008 environment for AMD64"
26 26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
27 27 $Env:VCINSTALLDIR = "${root}\VC\"
28 28 $Env:WindowsSdkDir = "${root}\WinSDK\"
29 29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
30 30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
31 31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
32 32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
33 33 '''.lstrip()
34 34
35 35 ACTIVATE_VC9_X86 = r'''
36 36 Write-Output "activating Visual Studio 2008 environment for x86"
37 37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
38 38 $Env:VCINSTALLDIR = "${root}\VC\"
39 39 $Env:WindowsSdkDir = "${root}\WinSDK\"
40 40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
41 41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
42 42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
43 43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
44 44 '''.lstrip()
45 45
46 46 HG_PURGE = r'''
47 47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
48 48 Set-Location C:\hgdev\src
49 49 hg.exe --config extensions.purge= purge --all
50 50 if ($LASTEXITCODE -ne 0) {
51 51 throw "process exited non-0: $LASTEXITCODE"
52 52 }
53 53 Write-Output "purged Mercurial repo"
54 54 '''
55 55
56 56 HG_UPDATE_CLEAN = r'''
57 57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
58 58 Set-Location C:\hgdev\src
59 59 hg.exe --config extensions.purge= purge --all
60 60 if ($LASTEXITCODE -ne 0) {{
61 61 throw "process exited non-0: $LASTEXITCODE"
62 62 }}
63 63 hg.exe update -C {revision}
64 64 if ($LASTEXITCODE -ne 0) {{
65 65 throw "process exited non-0: $LASTEXITCODE"
66 66 }}
67 67 hg.exe log -r .
68 68 Write-Output "updated Mercurial working directory to {revision}"
69 69 '''.lstrip()
70 70
71 71 BUILD_INNO_PYTHON3 = r'''
72 72 $Env:RUSTUP_HOME = "C:\hgdev\rustup"
73 73 $Env:CARGO_HOME = "C:\hgdev\cargo"
74 74 Set-Location C:\hgdev\src
75 75 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --pyoxidizer-target {pyoxidizer_target} --version {version}
76 76 if ($LASTEXITCODE -ne 0) {{
77 77 throw "process exited non-0: $LASTEXITCODE"
78 78 }}
79 79 '''
80 80
81 81 BUILD_INNO_PYTHON2 = r'''
82 82 Set-Location C:\hgdev\src
83 83 $python = "C:\hgdev\python27-{arch}\python.exe"
84 84 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python {extra_args}
85 85 if ($LASTEXITCODE -ne 0) {{
86 86 throw "process exited non-0: $LASTEXITCODE"
87 87 }}
88 88 '''.lstrip()
89 89
90 90 BUILD_WHEEL = r'''
91 91 Set-Location C:\hgdev\src
92 92 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
93 93 if ($LASTEXITCODE -ne 0) {{
94 94 throw "process exited non-0: $LASTEXITCODE"
95 95 }}
96 96 '''
97 97
98 98 BUILD_WIX_PYTHON3 = r'''
99 99 $Env:RUSTUP_HOME = "C:\hgdev\rustup"
100 100 $Env:CARGO_HOME = "C:\hgdev\cargo"
101 101 Set-Location C:\hgdev\src
102 102 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --pyoxidizer-target {pyoxidizer_target} --version {version}
103 103 if ($LASTEXITCODE -ne 0) {{
104 104 throw "process exited non-0: $LASTEXITCODE"
105 105 }}
106 106 '''
107 107
108 108 BUILD_WIX_PYTHON2 = r'''
109 109 Set-Location C:\hgdev\src
110 110 $python = "C:\hgdev\python27-{arch}\python.exe"
111 111 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
112 112 if ($LASTEXITCODE -ne 0) {{
113 113 throw "process exited non-0: $LASTEXITCODE"
114 114 }}
115 115 '''
116 116
117 117 RUN_TESTS = r'''
118 118 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}"
119 119 if ($LASTEXITCODE -ne 0) {{
120 120 throw "process exited non-0: $LASTEXITCODE"
121 121 }}
122 122 '''
123 123
124 124 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
125 125 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
126 126 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
127 127 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
128 128 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
129 129 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
130 WHEEL_FILENAME_PYTHON39_X86 = 'mercurial-{version}-cp39-cp39-win32.whl'
131 WHEEL_FILENAME_PYTHON39_X64 = 'mercurial-{version}-cp39-cp39-win_amd64.whl'
130 132
131 133 EXE_FILENAME_PYTHON2_X86 = 'Mercurial-{version}-x86-python2.exe'
132 134 EXE_FILENAME_PYTHON2_X64 = 'Mercurial-{version}-x64-python2.exe'
133 135 EXE_FILENAME_PYTHON3_X86 = 'Mercurial-{version}-x86.exe'
134 136 EXE_FILENAME_PYTHON3_X64 = 'Mercurial-{version}-x64.exe'
135 137
136 138 MSI_FILENAME_PYTHON2_X86 = 'mercurial-{version}-x86-python2.msi'
137 139 MSI_FILENAME_PYTHON2_X64 = 'mercurial-{version}-x64-python2.msi'
138 140 MSI_FILENAME_PYTHON3_X86 = 'mercurial-{version}-x86.msi'
139 141 MSI_FILENAME_PYTHON3_X64 = 'mercurial-{version}-x64.msi'
140 142
141 143 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
142 144
143 145 X86_USER_AGENT_PATTERN = '.*Windows.*'
144 146 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
145 147
146 148 EXE_PYTHON2_X86_DESCRIPTION = (
147 149 'Mercurial {version} Inno Setup installer - x86 Windows (Python 2) '
148 150 '- does not require admin rights'
149 151 )
150 152 EXE_PYTHON2_X64_DESCRIPTION = (
151 153 'Mercurial {version} Inno Setup installer - x64 Windows (Python 2) '
152 154 '- does not require admin rights'
153 155 )
154 156 # TODO remove Python version once Python 2 is dropped.
155 157 EXE_PYTHON3_X86_DESCRIPTION = (
156 158 'Mercurial {version} Inno Setup installer - x86 Windows (Python 3) '
157 159 '- does not require admin rights'
158 160 )
159 161 EXE_PYTHON3_X64_DESCRIPTION = (
160 162 'Mercurial {version} Inno Setup installer - x64 Windows (Python 3) '
161 163 '- does not require admin rights'
162 164 )
163 165 MSI_PYTHON2_X86_DESCRIPTION = (
164 166 'Mercurial {version} MSI installer - x86 Windows (Python 2) '
165 167 '- requires admin rights'
166 168 )
167 169 MSI_PYTHON2_X64_DESCRIPTION = (
168 170 'Mercurial {version} MSI installer - x64 Windows (Python 2) '
169 171 '- requires admin rights'
170 172 )
171 173 MSI_PYTHON3_X86_DESCRIPTION = (
172 174 'Mercurial {version} MSI installer - x86 Windows (Python 3) '
173 175 '- requires admin rights'
174 176 )
175 177 MSI_PYTHON3_X64_DESCRIPTION = (
176 178 'Mercurial {version} MSI installer - x64 Windows (Python 3) '
177 179 '- requires admin rights'
178 180 )
179 181
180 182
181 183 def get_vc_prefix(arch):
182 184 if arch == 'x86':
183 185 return ACTIVATE_VC9_X86
184 186 elif arch == 'x64':
185 187 return ACTIVATE_VC9_AMD64
186 188 else:
187 189 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
188 190
189 191
190 192 def fix_authorized_keys_permissions(winrm_client, path):
191 193 commands = [
192 194 '$ErrorActionPreference = "Stop"',
193 195 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
194 196 r'icacls %s /remove:g "NT Service\sshd"' % path,
195 197 ]
196 198
197 199 run_powershell(winrm_client, '\n'.join(commands))
198 200
199 201
200 202 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
201 203 """Synchronize local Mercurial repo to remote EC2 instance."""
202 204
203 205 winrm_client = ec2_instance.winrm_client
204 206
205 207 with tempfile.TemporaryDirectory() as temp_dir:
206 208 temp_dir = pathlib.Path(temp_dir)
207 209
208 210 ssh_dir = temp_dir / '.ssh'
209 211 ssh_dir.mkdir()
210 212 ssh_dir.chmod(0o0700)
211 213
212 214 # Generate SSH key to use for communication.
213 215 subprocess.run(
214 216 [
215 217 'ssh-keygen',
216 218 '-t',
217 219 'rsa',
218 220 '-b',
219 221 '4096',
220 222 '-N',
221 223 '',
222 224 '-f',
223 225 str(ssh_dir / 'id_rsa'),
224 226 ],
225 227 check=True,
226 228 capture_output=True,
227 229 )
228 230
229 231 # Add it to ~/.ssh/authorized_keys on remote.
230 232 # This assumes the file doesn't already exist.
231 233 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
232 234 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
233 235 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
234 236 fix_authorized_keys_permissions(winrm_client, authorized_keys)
235 237
236 238 public_ip = ec2_instance.public_ip_address
237 239
238 240 ssh_config = temp_dir / '.ssh' / 'config'
239 241
240 242 with open(ssh_config, 'w', encoding='utf-8') as fh:
241 243 fh.write('Host %s\n' % public_ip)
242 244 fh.write(' User Administrator\n')
243 245 fh.write(' StrictHostKeyChecking no\n')
244 246 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
245 247 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
246 248
247 249 if not (hg_repo / '.hg').is_dir():
248 250 raise Exception(
249 251 '%s is not a Mercurial repository; '
250 252 'synchronization not yet supported' % hg_repo
251 253 )
252 254
253 255 env = dict(os.environ)
254 256 env['HGPLAIN'] = '1'
255 257 env['HGENCODING'] = 'utf-8'
256 258
257 259 hg_bin = hg_repo / 'hg'
258 260
259 261 res = subprocess.run(
260 262 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
261 263 cwd=str(hg_repo),
262 264 env=env,
263 265 check=True,
264 266 capture_output=True,
265 267 )
266 268
267 269 full_revision = res.stdout.decode('ascii')
268 270
269 271 args = [
270 272 'python2.7',
271 273 hg_bin,
272 274 '--config',
273 275 'ui.ssh=ssh -F %s' % ssh_config,
274 276 '--config',
275 277 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
276 278 # Also ensure .hgtags changes are present so auto version
277 279 # calculation works.
278 280 'push',
279 281 '-f',
280 282 '-r',
281 283 full_revision,
282 284 '-r',
283 285 'file(.hgtags)',
284 286 'ssh://%s/c:/hgdev/src' % public_ip,
285 287 ]
286 288
287 289 res = subprocess.run(args, cwd=str(hg_repo), env=env)
288 290
289 291 # Allow 1 (no-op) to not trigger error.
290 292 if res.returncode not in (0, 1):
291 293 res.check_returncode()
292 294
293 295 run_powershell(
294 296 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
295 297 )
296 298
297 299 # TODO detect dirty local working directory and synchronize accordingly.
298 300
299 301
300 302 def purge_hg(winrm_client):
301 303 """Purge the Mercurial source repository on an EC2 instance."""
302 304 run_powershell(winrm_client, HG_PURGE)
303 305
304 306
305 307 def find_latest_dist(winrm_client, pattern):
306 308 """Find path to newest file in dist/ directory matching a pattern."""
307 309
308 310 res = winrm_client.execute_ps(
309 311 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
310 312 '| Sort-Object LastWriteTime -Descending '
311 313 '| Select-Object -First 1\n'
312 314 '$v.name' % pattern
313 315 )
314 316 return res[0]
315 317
316 318
317 319 def copy_latest_dist(winrm_client, pattern, dest_path):
318 320 """Copy latest file matching pattern in dist/ directory.
319 321
320 322 Given a WinRM client and a file pattern, find the latest file on the remote
321 323 matching that pattern and copy it to the ``dest_path`` directory on the
322 324 local machine.
323 325 """
324 326 latest = find_latest_dist(winrm_client, pattern)
325 327 source = r'C:\hgdev\src\dist\%s' % latest
326 328 dest = dest_path / latest
327 329 print('copying %s to %s' % (source, dest))
328 330 winrm_client.fetch(source, str(dest))
329 331
330 332
331 333 def build_inno_installer(
332 334 winrm_client,
333 335 python_version: int,
334 336 arch: str,
335 337 dest_path: pathlib.Path,
336 338 version=None,
337 339 ):
338 340 """Build the Inno Setup installer on a remote machine.
339 341
340 342 Using a WinRM client, remote commands are executed to build
341 343 a Mercurial Inno Setup installer.
342 344 """
343 345 print(
344 346 'building Inno Setup installer for Python %d %s'
345 347 % (python_version, arch)
346 348 )
347 349
348 350 if python_version == 3:
349 351 # TODO fix this limitation in packaging code
350 352 if not version:
351 353 raise Exception(
352 354 "version string is required when building for Python 3"
353 355 )
354 356
355 357 if arch == "x86":
356 358 target_triple = "i686-pc-windows-msvc"
357 359 elif arch == "x64":
358 360 target_triple = "x86_64-pc-windows-msvc"
359 361 else:
360 362 raise Exception("unhandled arch: %s" % arch)
361 363
362 364 ps = BUILD_INNO_PYTHON3.format(
363 365 pyoxidizer_target=target_triple, version=version,
364 366 )
365 367 else:
366 368 extra_args = []
367 369 if version:
368 370 extra_args.extend(['--version', version])
369 371
370 372 ps = get_vc_prefix(arch) + BUILD_INNO_PYTHON2.format(
371 373 arch=arch, extra_args=' '.join(extra_args)
372 374 )
373 375
374 376 run_powershell(winrm_client, ps)
375 377 copy_latest_dist(winrm_client, '*.exe', dest_path)
376 378
377 379
378 380 def build_wheel(
379 381 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
380 382 ):
381 383 """Build Python wheels on a remote machine.
382 384
383 385 Using a WinRM client, remote commands are executed to build a Python wheel
384 386 for Mercurial.
385 387 """
386 388 print('Building Windows wheel for Python %s %s' % (python_version, arch))
387 389
388 390 ps = BUILD_WHEEL.format(
389 391 python_version=python_version.replace(".", ""), arch=arch
390 392 )
391 393
392 394 # Python 2.7 requires an activated environment.
393 395 if python_version == "2.7":
394 396 ps = get_vc_prefix(arch) + ps
395 397
396 398 run_powershell(winrm_client, ps)
397 399 copy_latest_dist(winrm_client, '*.whl', dest_path)
398 400
399 401
400 402 def build_wix_installer(
401 403 winrm_client,
402 404 python_version: int,
403 405 arch: str,
404 406 dest_path: pathlib.Path,
405 407 version=None,
406 408 ):
407 409 """Build the WiX installer on a remote machine.
408 410
409 411 Using a WinRM client, remote commands are executed to build a WiX installer.
410 412 """
411 413 print('Building WiX installer for Python %d %s' % (python_version, arch))
412 414
413 415 if python_version == 3:
414 416 # TODO fix this limitation in packaging code
415 417 if not version:
416 418 raise Exception(
417 419 "version string is required when building for Python 3"
418 420 )
419 421
420 422 if arch == "x86":
421 423 target_triple = "i686-pc-windows-msvc"
422 424 elif arch == "x64":
423 425 target_triple = "x86_64-pc-windows-msvc"
424 426 else:
425 427 raise Exception("unhandled arch: %s" % arch)
426 428
427 429 ps = BUILD_WIX_PYTHON3.format(
428 430 pyoxidizer_target=target_triple, version=version,
429 431 )
430 432 else:
431 433 extra_args = []
432 434 if version:
433 435 extra_args.extend(['--version', version])
434 436
435 437 ps = get_vc_prefix(arch) + BUILD_WIX_PYTHON2.format(
436 438 arch=arch, extra_args=' '.join(extra_args)
437 439 )
438 440
439 441 run_powershell(winrm_client, ps)
440 442 copy_latest_dist(winrm_client, '*.msi', dest_path)
441 443
442 444
443 445 def run_tests(winrm_client, python_version, arch, test_flags=''):
444 446 """Run tests on a remote Windows machine.
445 447
446 448 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
447 449 ``arch`` is ``x86`` or ``x64``.
448 450 ``test_flags`` is a str representing extra arguments to pass to
449 451 ``run-tests.py``.
450 452 """
451 453 if not re.match(r'\d\.\d', python_version):
452 454 raise ValueError(
453 455 r'python_version must be \d.\d; got %s' % python_version
454 456 )
455 457
456 458 if arch not in ('x86', 'x64'):
457 459 raise ValueError('arch must be x86 or x64; got %s' % arch)
458 460
459 461 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
460 462
461 463 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
462 464
463 465 run_powershell(winrm_client, ps)
464 466
465 467
466 468 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
467 469 return (
468 470 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
469 471 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
470 472 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
471 473 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
472 474 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
473 475 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
476 dist_path / WHEEL_FILENAME_PYTHON39_X86.format(version=version),
477 dist_path / WHEEL_FILENAME_PYTHON39_X64.format(version=version),
474 478 )
475 479
476 480
477 481 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
478 482 return (
479 483 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
480 484 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
481 485 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
482 486 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
483 487 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
484 488 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
489 dist_path / WHEEL_FILENAME_PYTHON39_X86.format(version=version),
490 dist_path / WHEEL_FILENAME_PYTHON39_X64.format(version=version),
485 491 dist_path / EXE_FILENAME_PYTHON2_X86.format(version=version),
486 492 dist_path / EXE_FILENAME_PYTHON2_X64.format(version=version),
487 493 dist_path / EXE_FILENAME_PYTHON3_X86.format(version=version),
488 494 dist_path / EXE_FILENAME_PYTHON3_X64.format(version=version),
489 495 dist_path / MSI_FILENAME_PYTHON2_X86.format(version=version),
490 496 dist_path / MSI_FILENAME_PYTHON2_X64.format(version=version),
491 497 dist_path / MSI_FILENAME_PYTHON3_X86.format(version=version),
492 498 dist_path / MSI_FILENAME_PYTHON3_X64.format(version=version),
493 499 )
494 500
495 501
496 502 def generate_latest_dat(version: str):
497 503 python2_x86_exe_filename = EXE_FILENAME_PYTHON2_X86.format(version=version)
498 504 python2_x64_exe_filename = EXE_FILENAME_PYTHON2_X64.format(version=version)
499 505 python3_x86_exe_filename = EXE_FILENAME_PYTHON3_X86.format(version=version)
500 506 python3_x64_exe_filename = EXE_FILENAME_PYTHON3_X64.format(version=version)
501 507 python2_x86_msi_filename = MSI_FILENAME_PYTHON2_X86.format(version=version)
502 508 python2_x64_msi_filename = MSI_FILENAME_PYTHON2_X64.format(version=version)
503 509 python3_x86_msi_filename = MSI_FILENAME_PYTHON3_X86.format(version=version)
504 510 python3_x64_msi_filename = MSI_FILENAME_PYTHON3_X64.format(version=version)
505 511
506 512 entries = (
507 513 (
508 514 '10',
509 515 version,
510 516 X86_USER_AGENT_PATTERN,
511 517 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_exe_filename),
512 518 EXE_PYTHON3_X86_DESCRIPTION.format(version=version),
513 519 ),
514 520 (
515 521 '10',
516 522 version,
517 523 X64_USER_AGENT_PATTERN,
518 524 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_exe_filename),
519 525 EXE_PYTHON3_X64_DESCRIPTION.format(version=version),
520 526 ),
521 527 (
522 528 '9',
523 529 version,
524 530 X86_USER_AGENT_PATTERN,
525 531 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_exe_filename),
526 532 EXE_PYTHON2_X86_DESCRIPTION.format(version=version),
527 533 ),
528 534 (
529 535 '9',
530 536 version,
531 537 X64_USER_AGENT_PATTERN,
532 538 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_exe_filename),
533 539 EXE_PYTHON2_X64_DESCRIPTION.format(version=version),
534 540 ),
535 541 (
536 542 '10',
537 543 version,
538 544 X86_USER_AGENT_PATTERN,
539 545 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x86_msi_filename),
540 546 MSI_PYTHON3_X86_DESCRIPTION.format(version=version),
541 547 ),
542 548 (
543 549 '10',
544 550 version,
545 551 X64_USER_AGENT_PATTERN,
546 552 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python3_x64_msi_filename),
547 553 MSI_PYTHON3_X64_DESCRIPTION.format(version=version),
548 554 ),
549 555 (
550 556 '9',
551 557 version,
552 558 X86_USER_AGENT_PATTERN,
553 559 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x86_msi_filename),
554 560 MSI_PYTHON2_X86_DESCRIPTION.format(version=version),
555 561 ),
556 562 (
557 563 '9',
558 564 version,
559 565 X64_USER_AGENT_PATTERN,
560 566 '%s/%s' % (MERCURIAL_SCM_BASE_URL, python2_x64_msi_filename),
561 567 MSI_PYTHON2_X64_DESCRIPTION.format(version=version),
562 568 ),
563 569 )
564 570
565 571 lines = ['\t'.join(e) for e in entries]
566 572
567 573 return '\n'.join(lines) + '\n'
568 574
569 575
570 576 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
571 577 """Publish Windows release artifacts to PyPI."""
572 578
573 579 wheel_paths = resolve_wheel_artifacts(dist_path, version)
574 580
575 581 for p in wheel_paths:
576 582 if not p.exists():
577 583 raise Exception('%s not found' % p)
578 584
579 585 print('uploading wheels to PyPI (you may be prompted for credentials)')
580 586 pypi_upload(wheel_paths)
581 587
582 588
583 589 def publish_artifacts_mercurial_scm_org(
584 590 dist_path: pathlib.Path, version: str, ssh_username=None
585 591 ):
586 592 """Publish Windows release artifacts to mercurial-scm.org."""
587 593 all_paths = resolve_all_artifacts(dist_path, version)
588 594
589 595 for p in all_paths:
590 596 if not p.exists():
591 597 raise Exception('%s not found' % p)
592 598
593 599 client = paramiko.SSHClient()
594 600 client.load_system_host_keys()
595 601 # We assume the system SSH configuration knows how to connect.
596 602 print('connecting to mercurial-scm.org via ssh...')
597 603 try:
598 604 client.connect('mercurial-scm.org', username=ssh_username)
599 605 except paramiko.AuthenticationException:
600 606 print('error authenticating; is an SSH key available in an SSH agent?')
601 607 raise
602 608
603 609 print('SSH connection established')
604 610
605 611 print('opening SFTP client...')
606 612 sftp = client.open_sftp()
607 613 print('SFTP client obtained')
608 614
609 615 for p in all_paths:
610 616 dest_path = '/var/www/release/windows/%s' % p.name
611 617 print('uploading %s to %s' % (p, dest_path))
612 618
613 619 with p.open('rb') as fh:
614 620 data = fh.read()
615 621
616 622 with sftp.open(dest_path, 'wb') as fh:
617 623 fh.write(data)
618 624 fh.chmod(0o0664)
619 625
620 626 latest_dat_path = '/var/www/release/windows/latest.dat'
621 627
622 628 now = datetime.datetime.utcnow()
623 629 backup_path = dist_path / (
624 630 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
625 631 )
626 632 print('backing up %s to %s' % (latest_dat_path, backup_path))
627 633
628 634 with sftp.open(latest_dat_path, 'rb') as fh:
629 635 latest_dat_old = fh.read()
630 636
631 637 with backup_path.open('wb') as fh:
632 638 fh.write(latest_dat_old)
633 639
634 640 print('writing %s with content:' % latest_dat_path)
635 641 latest_dat_content = generate_latest_dat(version)
636 642 print(latest_dat_content)
637 643
638 644 with sftp.open(latest_dat_path, 'wb') as fh:
639 645 fh.write(latest_dat_content.encode('ascii'))
640 646
641 647
642 648 def publish_artifacts(
643 649 dist_path: pathlib.Path,
644 650 version: str,
645 651 pypi=True,
646 652 mercurial_scm_org=True,
647 653 ssh_username=None,
648 654 ):
649 655 """Publish Windows release artifacts.
650 656
651 657 Files are found in `dist_path`. We will look for files with version string
652 658 `version`.
653 659
654 660 `pypi` controls whether we upload to PyPI.
655 661 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
656 662 """
657 663 if pypi:
658 664 publish_artifacts_pypi(dist_path, version)
659 665
660 666 if mercurial_scm_org:
661 667 publish_artifacts_mercurial_scm_org(
662 668 dist_path, version, ssh_username=ssh_username
663 669 )
General Comments 0
You need to be logged in to leave comments. Login now