##// END OF EJS Templates
automation: allow exit code of 1 for `hg push`...
Gregory Szorc -
r42884:24cd5b0b stable
parent child Browse files
Show More
@@ -1,545 +1,549 b''
1 1 # linux.py - Linux specific automation functionality
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 os
11 11 import pathlib
12 12 import shlex
13 13 import subprocess
14 14 import tempfile
15 15
16 16 from .ssh import (
17 17 exec_command,
18 18 )
19 19
20 20
21 21 # Linux distributions that are supported.
22 22 DISTROS = {
23 23 'debian9',
24 24 'ubuntu18.04',
25 25 'ubuntu18.10',
26 26 'ubuntu19.04',
27 27 }
28 28
29 29 INSTALL_PYTHONS = r'''
30 30 PYENV2_VERSIONS="2.7.16 pypy2.7-7.1.1"
31 31 PYENV3_VERSIONS="3.5.7 3.6.8 3.7.3 3.8-dev pypy3.5-7.0.0 pypy3.6-7.1.1"
32 32
33 33 git clone https://github.com/pyenv/pyenv.git /hgdev/pyenv
34 34 pushd /hgdev/pyenv
35 35 git checkout 3faeda67bb33e07750d1a104271369a7384ca45c
36 36 popd
37 37
38 38 export PYENV_ROOT="/hgdev/pyenv"
39 39 export PATH="$PYENV_ROOT/bin:$PATH"
40 40
41 41 # pip 19.0.3.
42 42 PIP_SHA256=efe99298f3fbb1f56201ce6b81d2658067d2f7d7dfc2d412e0d3cacc9a397c61
43 43 wget -O get-pip.py --progress dot:mega https://github.com/pypa/get-pip/raw/fee32c376da1ff6496a798986d7939cd51e1644f/get-pip.py
44 44 echo "${PIP_SHA256} get-pip.py" | sha256sum --check -
45 45
46 46 VIRTUALENV_SHA256=984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39
47 47 VIRTUALENV_TARBALL=virtualenv-16.4.3.tar.gz
48 48 wget -O ${VIRTUALENV_TARBALL} --progress dot:mega https://files.pythonhosted.org/packages/37/db/89d6b043b22052109da35416abc3c397655e4bd3cff031446ba02b9654fa/${VIRTUALENV_TARBALL}
49 49 echo "${VIRTUALENV_SHA256} ${VIRTUALENV_TARBALL}" | sha256sum --check -
50 50
51 51 for v in ${PYENV2_VERSIONS}; do
52 52 pyenv install -v ${v}
53 53 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
54 54 ${PYENV_ROOT}/versions/${v}/bin/pip install ${VIRTUALENV_TARBALL}
55 55 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py2.txt
56 56 done
57 57
58 58 for v in ${PYENV3_VERSIONS}; do
59 59 pyenv install -v ${v}
60 60 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
61 61 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py3.txt
62 62 done
63 63
64 64 pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
65 65 '''.lstrip().replace('\r\n', '\n')
66 66
67 67
68 68 BOOTSTRAP_VIRTUALENV = r'''
69 69 /usr/bin/virtualenv /hgdev/venv-bootstrap
70 70
71 71 HG_SHA256=1bdd21bb87d1e05fb5cd395d488d0e0cc2f2f90ce0fd248e31a03595da5ccb47
72 72 HG_TARBALL=mercurial-4.9.1.tar.gz
73 73
74 74 wget -O ${HG_TARBALL} --progress dot:mega https://www.mercurial-scm.org/release/${HG_TARBALL}
75 75 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
76 76
77 77 /hgdev/venv-bootstrap/bin/pip install ${HG_TARBALL}
78 78 '''.lstrip().replace('\r\n', '\n')
79 79
80 80
81 81 BOOTSTRAP_DEBIAN = r'''
82 82 #!/bin/bash
83 83
84 84 set -ex
85 85
86 86 DISTRO=`grep DISTRIB_ID /etc/lsb-release | awk -F= '{{print $2}}'`
87 87 DEBIAN_VERSION=`cat /etc/debian_version`
88 88 LSB_RELEASE=`lsb_release -cs`
89 89
90 90 sudo /usr/sbin/groupadd hg
91 91 sudo /usr/sbin/groupadd docker
92 92 sudo /usr/sbin/useradd -g hg -G sudo,docker -d /home/hg -m -s /bin/bash hg
93 93 sudo mkdir /home/hg/.ssh
94 94 sudo cp ~/.ssh/authorized_keys /home/hg/.ssh/authorized_keys
95 95 sudo chown -R hg:hg /home/hg/.ssh
96 96 sudo chmod 700 /home/hg/.ssh
97 97 sudo chmod 600 /home/hg/.ssh/authorized_keys
98 98
99 99 cat << EOF | sudo tee /etc/sudoers.d/90-hg
100 100 hg ALL=(ALL) NOPASSWD:ALL
101 101 EOF
102 102
103 103 sudo apt-get update
104 104 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade
105 105
106 106 # Install packages necessary to set up Docker Apt repo.
107 107 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends \
108 108 apt-transport-https \
109 109 gnupg
110 110
111 111 cat > docker-apt-key << EOF
112 112 -----BEGIN PGP PUBLIC KEY BLOCK-----
113 113
114 114 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
171 171 YT90qFF93M3v01BbxP+EIY2/9tiIPbrd
172 172 =0YYh
173 173 -----END PGP PUBLIC KEY BLOCK-----
174 174 EOF
175 175
176 176 sudo apt-key add docker-apt-key
177 177
178 178 if [ "$DEBIAN_VERSION" = "9.8" ]; then
179 179 cat << EOF | sudo tee -a /etc/apt/sources.list
180 180 # Need backports for clang-format-6.0
181 181 deb http://deb.debian.org/debian stretch-backports main
182 182
183 183 # Sources are useful if we want to compile things locally.
184 184 deb-src http://deb.debian.org/debian stretch main
185 185 deb-src http://security.debian.org/debian-security stretch/updates main
186 186 deb-src http://deb.debian.org/debian stretch-updates main
187 187 deb-src http://deb.debian.org/debian stretch-backports main
188 188
189 189 deb [arch=amd64] https://download.docker.com/linux/debian stretch stable
190 190 EOF
191 191
192 192 elif [ "$DISTRO" = "Ubuntu" ]; then
193 193 cat << EOF | sudo tee -a /etc/apt/sources.list
194 194 deb [arch=amd64] https://download.docker.com/linux/ubuntu $LSB_RELEASE stable
195 195 EOF
196 196
197 197 fi
198 198
199 199 sudo apt-get update
200 200
201 201 PACKAGES="\
202 202 btrfs-progs \
203 203 build-essential \
204 204 bzr \
205 205 clang-format-6.0 \
206 206 cvs \
207 207 darcs \
208 208 debhelper \
209 209 devscripts \
210 210 dpkg-dev \
211 211 dstat \
212 212 emacs \
213 213 gettext \
214 214 git \
215 215 htop \
216 216 iotop \
217 217 jfsutils \
218 218 libbz2-dev \
219 219 libexpat1-dev \
220 220 libffi-dev \
221 221 libgdbm-dev \
222 222 liblzma-dev \
223 223 libncurses5-dev \
224 224 libnss3-dev \
225 225 libreadline-dev \
226 226 libsqlite3-dev \
227 227 libssl-dev \
228 228 netbase \
229 229 ntfs-3g \
230 230 nvme-cli \
231 231 pyflakes \
232 232 pyflakes3 \
233 233 pylint \
234 234 pylint3 \
235 235 python-all-dev \
236 236 python-dev \
237 237 python-docutils \
238 238 python-fuzzywuzzy \
239 239 python-pygments \
240 240 python-subversion \
241 241 python-vcr \
242 242 python3-dev \
243 243 python3-docutils \
244 244 python3-fuzzywuzzy \
245 245 python3-pygments \
246 246 python3-vcr \
247 247 rsync \
248 248 sqlite3 \
249 249 subversion \
250 250 tcl-dev \
251 251 tk-dev \
252 252 tla \
253 253 unzip \
254 254 uuid-dev \
255 255 vim \
256 256 virtualenv \
257 257 wget \
258 258 xfsprogs \
259 259 zip \
260 260 zlib1g-dev"
261 261
262 262 if [ "$DEBIAN_VERSION" = "9.8" ]; then
263 263 PACKAGES="$PACKAGES linux-perf"
264 264 elif [ "$DISTRO" = "Ubuntu" ]; then
265 265 PACKAGES="$PACKAGES linux-tools-common"
266 266 fi
267 267
268 268 # Ubuntu 19.04 removes monotone.
269 269 if [ "$LSB_RELEASE" != "disco" ]; then
270 270 PACKAGES="$PACKAGES monotone"
271 271 fi
272 272
273 273 # As of April 27, 2019, Docker hasn't published packages for
274 274 # Ubuntu 19.04 yet.
275 275 if [ "$LSB_RELEASE" != "disco" ]; then
276 276 PACKAGES="$PACKAGES docker-ce"
277 277 fi
278 278
279 279 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends $PACKAGES
280 280
281 281 # Create clang-format symlink so test harness finds it.
282 282 sudo update-alternatives --install /usr/bin/clang-format clang-format \
283 283 /usr/bin/clang-format-6.0 1000
284 284
285 285 sudo mkdir /hgdev
286 286 # Will be normalized to hg:hg later.
287 287 sudo chown `whoami` /hgdev
288 288
289 289 cp requirements-py2.txt /hgdev/requirements-py2.txt
290 290 cp requirements-py3.txt /hgdev/requirements-py3.txt
291 291
292 292 # Disable the pip version check because it uses the network and can
293 293 # be annoying.
294 294 cat << EOF | sudo tee -a /etc/pip.conf
295 295 [global]
296 296 disable-pip-version-check = True
297 297 EOF
298 298
299 299 {install_pythons}
300 300 {bootstrap_virtualenv}
301 301
302 302 /hgdev/venv-bootstrap/bin/hg clone https://www.mercurial-scm.org/repo/hg /hgdev/src
303 303
304 304 # Mark the repo as non-publishing.
305 305 cat >> /hgdev/src/.hg/hgrc << EOF
306 306 [phases]
307 307 publish = false
308 308 EOF
309 309
310 310 sudo chown -R hg:hg /hgdev
311 311 '''.lstrip().format(
312 312 install_pythons=INSTALL_PYTHONS,
313 313 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV
314 314 ).replace('\r\n', '\n')
315 315
316 316
317 317 # Prepares /hgdev for operations.
318 318 PREPARE_HGDEV = '''
319 319 #!/bin/bash
320 320
321 321 set -e
322 322
323 323 FS=$1
324 324
325 325 ensure_device() {
326 326 if [ -z "${DEVICE}" ]; then
327 327 echo "could not find block device to format"
328 328 exit 1
329 329 fi
330 330 }
331 331
332 332 # Determine device to partition for extra filesystem.
333 333 # If only 1 volume is present, it will be the root volume and
334 334 # should be /dev/nvme0. If multiple volumes are present, the
335 335 # root volume could be nvme0 or nvme1. Use whichever one doesn't have
336 336 # a partition.
337 337 if [ -e /dev/nvme1n1 ]; then
338 338 if [ -e /dev/nvme0n1p1 ]; then
339 339 DEVICE=/dev/nvme1n1
340 340 else
341 341 DEVICE=/dev/nvme0n1
342 342 fi
343 343 else
344 344 DEVICE=
345 345 fi
346 346
347 347 sudo mkdir /hgwork
348 348
349 349 if [ "${FS}" != "default" -a "${FS}" != "tmpfs" ]; then
350 350 ensure_device
351 351 echo "creating ${FS} filesystem on ${DEVICE}"
352 352 fi
353 353
354 354 if [ "${FS}" = "default" ]; then
355 355 :
356 356
357 357 elif [ "${FS}" = "btrfs" ]; then
358 358 sudo mkfs.btrfs ${DEVICE}
359 359 sudo mount ${DEVICE} /hgwork
360 360
361 361 elif [ "${FS}" = "ext3" ]; then
362 362 # lazy_journal_init speeds up filesystem creation at the expense of
363 363 # integrity if things crash. We are an ephemeral instance, so we don't
364 364 # care about integrity.
365 365 sudo mkfs.ext3 -E lazy_journal_init=1 ${DEVICE}
366 366 sudo mount ${DEVICE} /hgwork
367 367
368 368 elif [ "${FS}" = "ext4" ]; then
369 369 sudo mkfs.ext4 -E lazy_journal_init=1 ${DEVICE}
370 370 sudo mount ${DEVICE} /hgwork
371 371
372 372 elif [ "${FS}" = "jfs" ]; then
373 373 sudo mkfs.jfs ${DEVICE}
374 374 sudo mount ${DEVICE} /hgwork
375 375
376 376 elif [ "${FS}" = "tmpfs" ]; then
377 377 echo "creating tmpfs volume in /hgwork"
378 378 sudo mount -t tmpfs -o size=1024M tmpfs /hgwork
379 379
380 380 elif [ "${FS}" = "xfs" ]; then
381 381 sudo mkfs.xfs ${DEVICE}
382 382 sudo mount ${DEVICE} /hgwork
383 383
384 384 else
385 385 echo "unsupported filesystem: ${FS}"
386 386 exit 1
387 387 fi
388 388
389 389 echo "/hgwork ready"
390 390
391 391 sudo chown hg:hg /hgwork
392 392 mkdir /hgwork/tmp
393 393 chown hg:hg /hgwork/tmp
394 394
395 395 rsync -a /hgdev/src /hgwork/
396 396 '''.lstrip().replace('\r\n', '\n')
397 397
398 398
399 399 HG_UPDATE_CLEAN = '''
400 400 set -ex
401 401
402 402 HG=/hgdev/venv-bootstrap/bin/hg
403 403
404 404 cd /hgwork/src
405 405 ${HG} --config extensions.purge= purge --all
406 406 ${HG} update -C $1
407 407 ${HG} log -r .
408 408 '''.lstrip().replace('\r\n', '\n')
409 409
410 410
411 411 def prepare_exec_environment(ssh_client, filesystem='default'):
412 412 """Prepare an EC2 instance to execute things.
413 413
414 414 The AMI has an ``/hgdev`` bootstrapped with various Python installs
415 415 and a clone of the Mercurial repo.
416 416
417 417 In EC2, EBS volumes launched from snapshots have wonky performance behavior.
418 418 Notably, blocks have to be copied on first access, which makes volume
419 419 I/O extremely slow on fresh volumes.
420 420
421 421 Furthermore, we may want to run operations, tests, etc on alternative
422 422 filesystems so we examine behavior on different filesystems.
423 423
424 424 This function is used to facilitate executing operations on alternate
425 425 volumes.
426 426 """
427 427 sftp = ssh_client.open_sftp()
428 428
429 429 with sftp.open('/hgdev/prepare-hgdev', 'wb') as fh:
430 430 fh.write(PREPARE_HGDEV)
431 431 fh.chmod(0o0777)
432 432
433 433 command = 'sudo /hgdev/prepare-hgdev %s' % filesystem
434 434 chan, stdin, stdout = exec_command(ssh_client, command)
435 435 stdin.close()
436 436
437 437 for line in stdout:
438 438 print(line, end='')
439 439
440 440 res = chan.recv_exit_status()
441 441
442 442 if res:
443 443 raise Exception('non-0 exit code updating working directory; %d'
444 444 % res)
445 445
446 446
447 447 def synchronize_hg(source_path: pathlib.Path, ec2_instance, revision: str=None):
448 448 """Synchronize a local Mercurial source path to remote EC2 instance."""
449 449
450 450 with tempfile.TemporaryDirectory() as temp_dir:
451 451 temp_dir = pathlib.Path(temp_dir)
452 452
453 453 ssh_dir = temp_dir / '.ssh'
454 454 ssh_dir.mkdir()
455 455 ssh_dir.chmod(0o0700)
456 456
457 457 public_ip = ec2_instance.public_ip_address
458 458
459 459 ssh_config = ssh_dir / 'config'
460 460
461 461 with ssh_config.open('w', encoding='utf-8') as fh:
462 462 fh.write('Host %s\n' % public_ip)
463 463 fh.write(' User hg\n')
464 464 fh.write(' StrictHostKeyChecking no\n')
465 465 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
466 466 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
467 467
468 468 if not (source_path / '.hg').is_dir():
469 469 raise Exception('%s is not a Mercurial repository; synchronization '
470 470 'not yet supported' % source_path)
471 471
472 472 env = dict(os.environ)
473 473 env['HGPLAIN'] = '1'
474 474 env['HGENCODING'] = 'utf-8'
475 475
476 476 hg_bin = source_path / 'hg'
477 477
478 478 res = subprocess.run(
479 479 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
480 480 cwd=str(source_path), env=env, check=True, capture_output=True)
481 481
482 482 full_revision = res.stdout.decode('ascii')
483 483
484 484 args = [
485 485 'python2.7', str(hg_bin),
486 486 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
487 487 '--config', 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
488 488 'push', '-f', '-r', full_revision,
489 489 'ssh://%s//hgwork/src' % public_ip,
490 490 ]
491 491
492 subprocess.run(args, cwd=str(source_path), env=env, check=True)
492 res = subprocess.run(args, cwd=str(source_path), env=env)
493
494 # Allow 1 (no-op) to not trigger error.
495 if res.returncode not in (0, 1):
496 res.check_returncode()
493 497
494 498 # TODO support synchronizing dirty working directory.
495 499
496 500 sftp = ec2_instance.ssh_client.open_sftp()
497 501
498 502 with sftp.open('/hgdev/hgup', 'wb') as fh:
499 503 fh.write(HG_UPDATE_CLEAN)
500 504 fh.chmod(0o0700)
501 505
502 506 chan, stdin, stdout = exec_command(
503 507 ec2_instance.ssh_client, '/hgdev/hgup %s' % full_revision)
504 508 stdin.close()
505 509
506 510 for line in stdout:
507 511 print(line, end='')
508 512
509 513 res = chan.recv_exit_status()
510 514
511 515 if res:
512 516 raise Exception('non-0 exit code updating working directory; %d'
513 517 % res)
514 518
515 519
516 520 def run_tests(ssh_client, python_version, test_flags=None):
517 521 """Run tests on a remote Linux machine via an SSH client."""
518 522 test_flags = test_flags or []
519 523
520 524 print('running tests')
521 525
522 526 if python_version == 'system2':
523 527 python = '/usr/bin/python2'
524 528 elif python_version == 'system3':
525 529 python = '/usr/bin/python3'
526 530 elif python_version.startswith('pypy'):
527 531 python = '/hgdev/pyenv/shims/%s' % python_version
528 532 else:
529 533 python = '/hgdev/pyenv/shims/python%s' % python_version
530 534
531 535 test_flags = ' '.join(shlex.quote(a) for a in test_flags)
532 536
533 537 command = (
534 538 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
535 539 'cd /hgwork/src/tests && %s run-tests.py %s"' % (
536 540 python, test_flags))
537 541
538 542 chan, stdin, stdout = exec_command(ssh_client, command)
539 543
540 544 stdin.close()
541 545
542 546 for line in stdout:
543 547 print(line, end='')
544 548
545 549 return chan.recv_exit_status()
@@ -1,292 +1,296 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 os
11 11 import pathlib
12 12 import re
13 13 import subprocess
14 14 import tempfile
15 15
16 16 from .winrm import (
17 17 run_powershell,
18 18 )
19 19
20 20
21 21 # PowerShell commands to activate a Visual Studio 2008 environment.
22 22 # This is essentially a port of vcvarsall.bat to PowerShell.
23 23 ACTIVATE_VC9_AMD64 = r'''
24 24 Write-Output "activating Visual Studio 2008 environment for AMD64"
25 25 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
26 26 $Env:VCINSTALLDIR = "${root}\VC\"
27 27 $Env:WindowsSdkDir = "${root}\WinSDK\"
28 28 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
29 29 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
30 30 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
31 31 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
32 32 '''.lstrip()
33 33
34 34 ACTIVATE_VC9_X86 = r'''
35 35 Write-Output "activating Visual Studio 2008 environment for x86"
36 36 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
37 37 $Env:VCINSTALLDIR = "${root}\VC\"
38 38 $Env:WindowsSdkDir = "${root}\WinSDK\"
39 39 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
40 40 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
41 41 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
42 42 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
43 43 '''.lstrip()
44 44
45 45 HG_PURGE = r'''
46 46 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
47 47 Set-Location C:\hgdev\src
48 48 hg.exe --config extensions.purge= purge --all
49 49 if ($LASTEXITCODE -ne 0) {
50 50 throw "process exited non-0: $LASTEXITCODE"
51 51 }
52 52 Write-Output "purged Mercurial repo"
53 53 '''
54 54
55 55 HG_UPDATE_CLEAN = r'''
56 56 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
57 57 Set-Location C:\hgdev\src
58 58 hg.exe --config extensions.purge= purge --all
59 59 if ($LASTEXITCODE -ne 0) {{
60 60 throw "process exited non-0: $LASTEXITCODE"
61 61 }}
62 62 hg.exe update -C {revision}
63 63 if ($LASTEXITCODE -ne 0) {{
64 64 throw "process exited non-0: $LASTEXITCODE"
65 65 }}
66 66 hg.exe log -r .
67 67 Write-Output "updated Mercurial working directory to {revision}"
68 68 '''.lstrip()
69 69
70 70 BUILD_INNO = r'''
71 71 Set-Location C:\hgdev\src
72 72 $python = "C:\hgdev\python27-{arch}\python.exe"
73 73 C:\hgdev\python37-x64\python.exe contrib\packaging\inno\build.py --python $python
74 74 if ($LASTEXITCODE -ne 0) {{
75 75 throw "process exited non-0: $LASTEXITCODE"
76 76 }}
77 77 '''.lstrip()
78 78
79 79 BUILD_WHEEL = r'''
80 80 Set-Location C:\hgdev\src
81 81 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
82 82 if ($LASTEXITCODE -ne 0) {{
83 83 throw "process exited non-0: $LASTEXITCODE"
84 84 }}
85 85 '''
86 86
87 87 BUILD_WIX = r'''
88 88 Set-Location C:\hgdev\src
89 89 $python = "C:\hgdev\python27-{arch}\python.exe"
90 90 C:\hgdev\python37-x64\python.exe contrib\packaging\wix\build.py --python $python {extra_args}
91 91 if ($LASTEXITCODE -ne 0) {{
92 92 throw "process exited non-0: $LASTEXITCODE"
93 93 }}
94 94 '''
95 95
96 96 RUN_TESTS = r'''
97 97 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 98 if ($LASTEXITCODE -ne 0) {{
99 99 throw "process exited non-0: $LASTEXITCODE"
100 100 }}
101 101 '''
102 102
103 103
104 104 def get_vc_prefix(arch):
105 105 if arch == 'x86':
106 106 return ACTIVATE_VC9_X86
107 107 elif arch == 'x64':
108 108 return ACTIVATE_VC9_AMD64
109 109 else:
110 110 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
111 111
112 112
113 113 def fix_authorized_keys_permissions(winrm_client, path):
114 114 commands = [
115 115 '$ErrorActionPreference = "Stop"',
116 116 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
117 117 r'icacls %s /remove:g "NT Service\sshd"' % path,
118 118 ]
119 119
120 120 run_powershell(winrm_client, '\n'.join(commands))
121 121
122 122
123 123 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
124 124 """Synchronize local Mercurial repo to remote EC2 instance."""
125 125
126 126 winrm_client = ec2_instance.winrm_client
127 127
128 128 with tempfile.TemporaryDirectory() as temp_dir:
129 129 temp_dir = pathlib.Path(temp_dir)
130 130
131 131 ssh_dir = temp_dir / '.ssh'
132 132 ssh_dir.mkdir()
133 133 ssh_dir.chmod(0o0700)
134 134
135 135 # Generate SSH key to use for communication.
136 136 subprocess.run([
137 137 'ssh-keygen', '-t', 'rsa', '-b', '4096', '-N', '',
138 138 '-f', str(ssh_dir / 'id_rsa')],
139 139 check=True, capture_output=True)
140 140
141 141 # Add it to ~/.ssh/authorized_keys on remote.
142 142 # This assumes the file doesn't already exist.
143 143 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
144 144 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
145 145 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
146 146 fix_authorized_keys_permissions(winrm_client, authorized_keys)
147 147
148 148 public_ip = ec2_instance.public_ip_address
149 149
150 150 ssh_config = temp_dir / '.ssh' / 'config'
151 151
152 152 with open(ssh_config, 'w', encoding='utf-8') as fh:
153 153 fh.write('Host %s\n' % public_ip)
154 154 fh.write(' User Administrator\n')
155 155 fh.write(' StrictHostKeyChecking no\n')
156 156 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
157 157 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
158 158
159 159 if not (hg_repo / '.hg').is_dir():
160 160 raise Exception('%s is not a Mercurial repository; '
161 161 'synchronization not yet supported' % hg_repo)
162 162
163 163 env = dict(os.environ)
164 164 env['HGPLAIN'] = '1'
165 165 env['HGENCODING'] = 'utf-8'
166 166
167 167 hg_bin = hg_repo / 'hg'
168 168
169 169 res = subprocess.run(
170 170 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
171 171 cwd=str(hg_repo), env=env, check=True, capture_output=True)
172 172
173 173 full_revision = res.stdout.decode('ascii')
174 174
175 175 args = [
176 176 'python2.7', hg_bin,
177 177 '--config', 'ui.ssh=ssh -F %s' % ssh_config,
178 178 '--config', 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
179 179 'push', '-f', '-r', full_revision,
180 180 'ssh://%s/c:/hgdev/src' % public_ip,
181 181 ]
182 182
183 subprocess.run(args, cwd=str(hg_repo), env=env, check=True)
183 res = subprocess.run(args, cwd=str(hg_repo), env=env)
184
185 # Allow 1 (no-op) to not trigger error.
186 if res.returncode not in (0, 1):
187 res.check_returncode()
184 188
185 189 run_powershell(winrm_client,
186 190 HG_UPDATE_CLEAN.format(revision=full_revision))
187 191
188 192 # TODO detect dirty local working directory and synchronize accordingly.
189 193
190 194
191 195 def purge_hg(winrm_client):
192 196 """Purge the Mercurial source repository on an EC2 instance."""
193 197 run_powershell(winrm_client, HG_PURGE)
194 198
195 199
196 200 def find_latest_dist(winrm_client, pattern):
197 201 """Find path to newest file in dist/ directory matching a pattern."""
198 202
199 203 res = winrm_client.execute_ps(
200 204 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
201 205 '| Sort-Object LastWriteTime -Descending '
202 206 '| Select-Object -First 1\n'
203 207 '$v.name' % pattern
204 208 )
205 209 return res[0]
206 210
207 211
208 212 def copy_latest_dist(winrm_client, pattern, dest_path):
209 213 """Copy latest file matching pattern in dist/ directory.
210 214
211 215 Given a WinRM client and a file pattern, find the latest file on the remote
212 216 matching that pattern and copy it to the ``dest_path`` directory on the
213 217 local machine.
214 218 """
215 219 latest = find_latest_dist(winrm_client, pattern)
216 220 source = r'C:\hgdev\src\dist\%s' % latest
217 221 dest = dest_path / latest
218 222 print('copying %s to %s' % (source, dest))
219 223 winrm_client.fetch(source, str(dest))
220 224
221 225
222 226 def build_inno_installer(winrm_client, arch: str, dest_path: pathlib.Path,
223 227 version=None):
224 228 """Build the Inno Setup installer on a remote machine.
225 229
226 230 Using a WinRM client, remote commands are executed to build
227 231 a Mercurial Inno Setup installer.
228 232 """
229 233 print('building Inno Setup installer for %s' % arch)
230 234
231 235 extra_args = []
232 236 if version:
233 237 extra_args.extend(['--version', version])
234 238
235 239 ps = get_vc_prefix(arch) + BUILD_INNO.format(arch=arch,
236 240 extra_args=' '.join(extra_args))
237 241 run_powershell(winrm_client, ps)
238 242 copy_latest_dist(winrm_client, '*.exe', dest_path)
239 243
240 244
241 245 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
242 246 """Build Python wheels on a remote machine.
243 247
244 248 Using a WinRM client, remote commands are executed to build a Python wheel
245 249 for Mercurial.
246 250 """
247 251 print('Building Windows wheel for %s' % arch)
248 252 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
249 253 run_powershell(winrm_client, ps)
250 254 copy_latest_dist(winrm_client, '*.whl', dest_path)
251 255
252 256
253 257 def build_wix_installer(winrm_client, arch: str, dest_path: pathlib.Path,
254 258 version=None):
255 259 """Build the WiX installer on a remote machine.
256 260
257 261 Using a WinRM client, remote commands are executed to build a WiX installer.
258 262 """
259 263 print('Building WiX installer for %s' % arch)
260 264 extra_args = []
261 265 if version:
262 266 extra_args.extend(['--version', version])
263 267
264 268 ps = get_vc_prefix(arch) + BUILD_WIX.format(arch=arch,
265 269 extra_args=' '.join(extra_args))
266 270 run_powershell(winrm_client, ps)
267 271 copy_latest_dist(winrm_client, '*.msi', dest_path)
268 272
269 273
270 274 def run_tests(winrm_client, python_version, arch, test_flags=''):
271 275 """Run tests on a remote Windows machine.
272 276
273 277 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
274 278 ``arch`` is ``x86`` or ``x64``.
275 279 ``test_flags`` is a str representing extra arguments to pass to
276 280 ``run-tests.py``.
277 281 """
278 282 if not re.match(r'\d\.\d', python_version):
279 283 raise ValueError(r'python_version must be \d.\d; got %s' %
280 284 python_version)
281 285
282 286 if arch not in ('x86', 'x64'):
283 287 raise ValueError('arch must be x86 or x64; got %s' % arch)
284 288
285 289 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
286 290
287 291 ps = RUN_TESTS.format(
288 292 python_path=python_path,
289 293 test_flags=test_flags or '',
290 294 )
291 295
292 296 run_powershell(winrm_client, ps)
General Comments 0
You need to be logged in to leave comments. Login now