##// END OF EJS Templates
automation: add extra arguments when building Inno...
Gregory Szorc -
r45263:fc6fb941 default draft
parent child Browse files
Show More
@@ -1,533 +1,533 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 = r'''
72 72 Set-Location C:\hgdev\src
73 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 75 if ($LASTEXITCODE -ne 0) {{
76 76 throw "process exited non-0: $LASTEXITCODE"
77 77 }}
78 78 '''.lstrip()
79 79
80 80 BUILD_WHEEL = r'''
81 81 Set-Location C:\hgdev\src
82 82 C:\hgdev\python{python_version}-{arch}\python.exe -m pip wheel --wheel-dir dist .
83 83 if ($LASTEXITCODE -ne 0) {{
84 84 throw "process exited non-0: $LASTEXITCODE"
85 85 }}
86 86 '''
87 87
88 88 BUILD_WIX = r'''
89 89 Set-Location C:\hgdev\src
90 90 $python = "C:\hgdev\python27-{arch}\python.exe"
91 91 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
92 92 if ($LASTEXITCODE -ne 0) {{
93 93 throw "process exited non-0: $LASTEXITCODE"
94 94 }}
95 95 '''
96 96
97 97 RUN_TESTS = r'''
98 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 99 if ($LASTEXITCODE -ne 0) {{
100 100 throw "process exited non-0: $LASTEXITCODE"
101 101 }}
102 102 '''
103 103
104 104 WHEEL_FILENAME_PYTHON27_X86 = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 105 WHEEL_FILENAME_PYTHON27_X64 = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
106 106 WHEEL_FILENAME_PYTHON37_X86 = 'mercurial-{version}-cp37-cp37m-win32.whl'
107 107 WHEEL_FILENAME_PYTHON37_X64 = 'mercurial-{version}-cp37-cp37m-win_amd64.whl'
108 108 WHEEL_FILENAME_PYTHON38_X86 = 'mercurial-{version}-cp38-cp38-win32.whl'
109 109 WHEEL_FILENAME_PYTHON38_X64 = 'mercurial-{version}-cp38-cp38-win_amd64.whl'
110 110
111 111 X86_EXE_FILENAME = 'Mercurial-{version}-x86-python2.exe'
112 112 X64_EXE_FILENAME = 'Mercurial-{version}-x64-python2.exe'
113 113 X86_MSI_FILENAME = 'mercurial-{version}-x86-python2.msi'
114 114 X64_MSI_FILENAME = 'mercurial-{version}-x64-python2.msi'
115 115
116 116 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
117 117
118 118 X86_USER_AGENT_PATTERN = '.*Windows.*'
119 119 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
120 120
121 121 X86_EXE_DESCRIPTION = (
122 122 'Mercurial {version} Inno Setup installer - x86 Windows '
123 123 '- does not require admin rights'
124 124 )
125 125 X64_EXE_DESCRIPTION = (
126 126 'Mercurial {version} Inno Setup installer - x64 Windows '
127 127 '- does not require admin rights'
128 128 )
129 129 X86_MSI_DESCRIPTION = (
130 130 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
131 131 )
132 132 X64_MSI_DESCRIPTION = (
133 133 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
134 134 )
135 135
136 136
137 137 def get_vc_prefix(arch):
138 138 if arch == 'x86':
139 139 return ACTIVATE_VC9_X86
140 140 elif arch == 'x64':
141 141 return ACTIVATE_VC9_AMD64
142 142 else:
143 143 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
144 144
145 145
146 146 def fix_authorized_keys_permissions(winrm_client, path):
147 147 commands = [
148 148 '$ErrorActionPreference = "Stop"',
149 149 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
150 150 r'icacls %s /remove:g "NT Service\sshd"' % path,
151 151 ]
152 152
153 153 run_powershell(winrm_client, '\n'.join(commands))
154 154
155 155
156 156 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
157 157 """Synchronize local Mercurial repo to remote EC2 instance."""
158 158
159 159 winrm_client = ec2_instance.winrm_client
160 160
161 161 with tempfile.TemporaryDirectory() as temp_dir:
162 162 temp_dir = pathlib.Path(temp_dir)
163 163
164 164 ssh_dir = temp_dir / '.ssh'
165 165 ssh_dir.mkdir()
166 166 ssh_dir.chmod(0o0700)
167 167
168 168 # Generate SSH key to use for communication.
169 169 subprocess.run(
170 170 [
171 171 'ssh-keygen',
172 172 '-t',
173 173 'rsa',
174 174 '-b',
175 175 '4096',
176 176 '-N',
177 177 '',
178 178 '-f',
179 179 str(ssh_dir / 'id_rsa'),
180 180 ],
181 181 check=True,
182 182 capture_output=True,
183 183 )
184 184
185 185 # Add it to ~/.ssh/authorized_keys on remote.
186 186 # This assumes the file doesn't already exist.
187 187 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
188 188 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
189 189 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
190 190 fix_authorized_keys_permissions(winrm_client, authorized_keys)
191 191
192 192 public_ip = ec2_instance.public_ip_address
193 193
194 194 ssh_config = temp_dir / '.ssh' / 'config'
195 195
196 196 with open(ssh_config, 'w', encoding='utf-8') as fh:
197 197 fh.write('Host %s\n' % public_ip)
198 198 fh.write(' User Administrator\n')
199 199 fh.write(' StrictHostKeyChecking no\n')
200 200 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
201 201 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
202 202
203 203 if not (hg_repo / '.hg').is_dir():
204 204 raise Exception(
205 205 '%s is not a Mercurial repository; '
206 206 'synchronization not yet supported' % hg_repo
207 207 )
208 208
209 209 env = dict(os.environ)
210 210 env['HGPLAIN'] = '1'
211 211 env['HGENCODING'] = 'utf-8'
212 212
213 213 hg_bin = hg_repo / 'hg'
214 214
215 215 res = subprocess.run(
216 216 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
217 217 cwd=str(hg_repo),
218 218 env=env,
219 219 check=True,
220 220 capture_output=True,
221 221 )
222 222
223 223 full_revision = res.stdout.decode('ascii')
224 224
225 225 args = [
226 226 'python2.7',
227 227 hg_bin,
228 228 '--config',
229 229 'ui.ssh=ssh -F %s' % ssh_config,
230 230 '--config',
231 231 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
232 232 # Also ensure .hgtags changes are present so auto version
233 233 # calculation works.
234 234 'push',
235 235 '-f',
236 236 '-r',
237 237 full_revision,
238 238 '-r',
239 239 'file(.hgtags)',
240 240 'ssh://%s/c:/hgdev/src' % public_ip,
241 241 ]
242 242
243 243 res = subprocess.run(args, cwd=str(hg_repo), env=env)
244 244
245 245 # Allow 1 (no-op) to not trigger error.
246 246 if res.returncode not in (0, 1):
247 247 res.check_returncode()
248 248
249 249 run_powershell(
250 250 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
251 251 )
252 252
253 253 # TODO detect dirty local working directory and synchronize accordingly.
254 254
255 255
256 256 def purge_hg(winrm_client):
257 257 """Purge the Mercurial source repository on an EC2 instance."""
258 258 run_powershell(winrm_client, HG_PURGE)
259 259
260 260
261 261 def find_latest_dist(winrm_client, pattern):
262 262 """Find path to newest file in dist/ directory matching a pattern."""
263 263
264 264 res = winrm_client.execute_ps(
265 265 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
266 266 '| Sort-Object LastWriteTime -Descending '
267 267 '| Select-Object -First 1\n'
268 268 '$v.name' % pattern
269 269 )
270 270 return res[0]
271 271
272 272
273 273 def copy_latest_dist(winrm_client, pattern, dest_path):
274 274 """Copy latest file matching pattern in dist/ directory.
275 275
276 276 Given a WinRM client and a file pattern, find the latest file on the remote
277 277 matching that pattern and copy it to the ``dest_path`` directory on the
278 278 local machine.
279 279 """
280 280 latest = find_latest_dist(winrm_client, pattern)
281 281 source = r'C:\hgdev\src\dist\%s' % latest
282 282 dest = dest_path / latest
283 283 print('copying %s to %s' % (source, dest))
284 284 winrm_client.fetch(source, str(dest))
285 285
286 286
287 287 def build_inno_installer(
288 288 winrm_client, arch: str, dest_path: pathlib.Path, version=None
289 289 ):
290 290 """Build the Inno Setup installer on a remote machine.
291 291
292 292 Using a WinRM client, remote commands are executed to build
293 293 a Mercurial Inno Setup installer.
294 294 """
295 295 print('building Inno Setup installer for %s' % arch)
296 296
297 297 extra_args = []
298 298 if version:
299 299 extra_args.extend(['--version', version])
300 300
301 301 ps = get_vc_prefix(arch) + BUILD_INNO.format(
302 302 arch=arch, extra_args=' '.join(extra_args)
303 303 )
304 304 run_powershell(winrm_client, ps)
305 305 copy_latest_dist(winrm_client, '*.exe', dest_path)
306 306
307 307
308 308 def build_wheel(
309 309 winrm_client, python_version: str, arch: str, dest_path: pathlib.Path
310 310 ):
311 311 """Build Python wheels on a remote machine.
312 312
313 313 Using a WinRM client, remote commands are executed to build a Python wheel
314 314 for Mercurial.
315 315 """
316 316 print('Building Windows wheel for Python %s %s' % (python_version, arch))
317 317
318 318 ps = BUILD_WHEEL.format(
319 319 python_version=python_version.replace(".", ""), arch=arch
320 320 )
321 321
322 322 # Python 2.7 requires an activated environment.
323 323 if python_version == "2.7":
324 324 ps = get_vc_prefix(arch) + ps
325 325
326 326 run_powershell(winrm_client, ps)
327 327 copy_latest_dist(winrm_client, '*.whl', dest_path)
328 328
329 329
330 330 def build_wix_installer(
331 331 winrm_client, arch: str, dest_path: pathlib.Path, version=None
332 332 ):
333 333 """Build the WiX installer on a remote machine.
334 334
335 335 Using a WinRM client, remote commands are executed to build a WiX installer.
336 336 """
337 337 print('Building WiX installer for %s' % arch)
338 338 extra_args = []
339 339 if version:
340 340 extra_args.extend(['--version', version])
341 341
342 342 ps = get_vc_prefix(arch) + BUILD_WIX.format(
343 343 arch=arch, extra_args=' '.join(extra_args)
344 344 )
345 345 run_powershell(winrm_client, ps)
346 346 copy_latest_dist(winrm_client, '*.msi', dest_path)
347 347
348 348
349 349 def run_tests(winrm_client, python_version, arch, test_flags=''):
350 350 """Run tests on a remote Windows machine.
351 351
352 352 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
353 353 ``arch`` is ``x86`` or ``x64``.
354 354 ``test_flags`` is a str representing extra arguments to pass to
355 355 ``run-tests.py``.
356 356 """
357 357 if not re.match(r'\d\.\d', python_version):
358 358 raise ValueError(
359 359 r'python_version must be \d.\d; got %s' % python_version
360 360 )
361 361
362 362 if arch not in ('x86', 'x64'):
363 363 raise ValueError('arch must be x86 or x64; got %s' % arch)
364 364
365 365 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
366 366
367 367 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
368 368
369 369 run_powershell(winrm_client, ps)
370 370
371 371
372 372 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
373 373 return (
374 374 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
375 375 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
376 376 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
377 377 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
378 378 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
379 379 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
380 380 )
381 381
382 382
383 383 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
384 384 return (
385 385 dist_path / WHEEL_FILENAME_PYTHON27_X86.format(version=version),
386 386 dist_path / WHEEL_FILENAME_PYTHON27_X64.format(version=version),
387 387 dist_path / WHEEL_FILENAME_PYTHON37_X86.format(version=version),
388 388 dist_path / WHEEL_FILENAME_PYTHON37_X64.format(version=version),
389 389 dist_path / WHEEL_FILENAME_PYTHON38_X86.format(version=version),
390 390 dist_path / WHEEL_FILENAME_PYTHON38_X64.format(version=version),
391 391 dist_path / X86_EXE_FILENAME.format(version=version),
392 392 dist_path / X64_EXE_FILENAME.format(version=version),
393 393 dist_path / X86_MSI_FILENAME.format(version=version),
394 394 dist_path / X64_MSI_FILENAME.format(version=version),
395 395 )
396 396
397 397
398 398 def generate_latest_dat(version: str):
399 399 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
400 400 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
401 401 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
402 402 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
403 403
404 404 entries = (
405 405 (
406 406 '10',
407 407 version,
408 408 X86_USER_AGENT_PATTERN,
409 409 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
410 410 X86_EXE_DESCRIPTION.format(version=version),
411 411 ),
412 412 (
413 413 '10',
414 414 version,
415 415 X64_USER_AGENT_PATTERN,
416 416 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
417 417 X64_EXE_DESCRIPTION.format(version=version),
418 418 ),
419 419 (
420 420 '10',
421 421 version,
422 422 X86_USER_AGENT_PATTERN,
423 423 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
424 424 X86_MSI_DESCRIPTION.format(version=version),
425 425 ),
426 426 (
427 427 '10',
428 428 version,
429 429 X64_USER_AGENT_PATTERN,
430 430 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
431 431 X64_MSI_DESCRIPTION.format(version=version),
432 432 ),
433 433 )
434 434
435 435 lines = ['\t'.join(e) for e in entries]
436 436
437 437 return '\n'.join(lines) + '\n'
438 438
439 439
440 440 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
441 441 """Publish Windows release artifacts to PyPI."""
442 442
443 443 wheel_paths = resolve_wheel_artifacts(dist_path, version)
444 444
445 445 for p in wheel_paths:
446 446 if not p.exists():
447 447 raise Exception('%s not found' % p)
448 448
449 449 print('uploading wheels to PyPI (you may be prompted for credentials)')
450 450 pypi_upload(wheel_paths)
451 451
452 452
453 453 def publish_artifacts_mercurial_scm_org(
454 454 dist_path: pathlib.Path, version: str, ssh_username=None
455 455 ):
456 456 """Publish Windows release artifacts to mercurial-scm.org."""
457 457 all_paths = resolve_all_artifacts(dist_path, version)
458 458
459 459 for p in all_paths:
460 460 if not p.exists():
461 461 raise Exception('%s not found' % p)
462 462
463 463 client = paramiko.SSHClient()
464 464 client.load_system_host_keys()
465 465 # We assume the system SSH configuration knows how to connect.
466 466 print('connecting to mercurial-scm.org via ssh...')
467 467 try:
468 468 client.connect('mercurial-scm.org', username=ssh_username)
469 469 except paramiko.AuthenticationException:
470 470 print('error authenticating; is an SSH key available in an SSH agent?')
471 471 raise
472 472
473 473 print('SSH connection established')
474 474
475 475 print('opening SFTP client...')
476 476 sftp = client.open_sftp()
477 477 print('SFTP client obtained')
478 478
479 479 for p in all_paths:
480 480 dest_path = '/var/www/release/windows/%s' % p.name
481 481 print('uploading %s to %s' % (p, dest_path))
482 482
483 483 with p.open('rb') as fh:
484 484 data = fh.read()
485 485
486 486 with sftp.open(dest_path, 'wb') as fh:
487 487 fh.write(data)
488 488 fh.chmod(0o0664)
489 489
490 490 latest_dat_path = '/var/www/release/windows/latest.dat'
491 491
492 492 now = datetime.datetime.utcnow()
493 493 backup_path = dist_path / (
494 494 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
495 495 )
496 496 print('backing up %s to %s' % (latest_dat_path, backup_path))
497 497
498 498 with sftp.open(latest_dat_path, 'rb') as fh:
499 499 latest_dat_old = fh.read()
500 500
501 501 with backup_path.open('wb') as fh:
502 502 fh.write(latest_dat_old)
503 503
504 504 print('writing %s with content:' % latest_dat_path)
505 505 latest_dat_content = generate_latest_dat(version)
506 506 print(latest_dat_content)
507 507
508 508 with sftp.open(latest_dat_path, 'wb') as fh:
509 509 fh.write(latest_dat_content.encode('ascii'))
510 510
511 511
512 512 def publish_artifacts(
513 513 dist_path: pathlib.Path,
514 514 version: str,
515 515 pypi=True,
516 516 mercurial_scm_org=True,
517 517 ssh_username=None,
518 518 ):
519 519 """Publish Windows release artifacts.
520 520
521 521 Files are found in `dist_path`. We will look for files with version string
522 522 `version`.
523 523
524 524 `pypi` controls whether we upload to PyPI.
525 525 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
526 526 """
527 527 if pypi:
528 528 publish_artifacts_pypi(dist_path, version)
529 529
530 530 if mercurial_scm_org:
531 531 publish_artifacts_mercurial_scm_org(
532 532 dist_path, version, ssh_username=ssh_username
533 533 )
General Comments 0
You need to be logged in to leave comments. Login now