##// END OF EJS Templates
contrib: install PyOxidizer in Linux and Windows environments...
Gregory Szorc -
r45253:f50f66c3 default draft
parent child Browse files
Show More
@@ -1,595 +1,597 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 exec_command
17 17
18 18
19 19 # Linux distributions that are supported.
20 20 DISTROS = {
21 21 'debian9',
22 22 'debian10',
23 23 'ubuntu18.04',
24 24 'ubuntu19.04',
25 25 }
26 26
27 27 INSTALL_PYTHONS = r'''
28 28 PYENV2_VERSIONS="2.7.17 pypy2.7-7.2.0"
29 29 PYENV3_VERSIONS="3.5.9 3.6.10 3.7.7 3.8.2 pypy3.5-7.0.0 pypy3.6-7.3.0"
30 30
31 31 git clone https://github.com/pyenv/pyenv.git /hgdev/pyenv
32 32 pushd /hgdev/pyenv
33 33 git checkout 3005c4664372ae13fbe376be699313eb428c8bdd
34 34 popd
35 35
36 36 export PYENV_ROOT="/hgdev/pyenv"
37 37 export PATH="$PYENV_ROOT/bin:$PATH"
38 38
39 39 # pip 19.2.3.
40 40 PIP_SHA256=57e3643ff19f018f8a00dfaa6b7e4620e3c1a7a2171fd218425366ec006b3bfe
41 41 wget -O get-pip.py --progress dot:mega https://github.com/pypa/get-pip/raw/309a56c5fd94bd1134053a541cb4657a4e47e09d/get-pip.py
42 42 echo "${PIP_SHA256} get-pip.py" | sha256sum --check -
43 43
44 44 VIRTUALENV_SHA256=f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2
45 45 VIRTUALENV_TARBALL=virtualenv-16.7.5.tar.gz
46 46 wget -O ${VIRTUALENV_TARBALL} --progress dot:mega https://files.pythonhosted.org/packages/66/f0/6867af06d2e2f511e4e1d7094ff663acdebc4f15d4a0cb0fed1007395124/${VIRTUALENV_TARBALL}
47 47 echo "${VIRTUALENV_SHA256} ${VIRTUALENV_TARBALL}" | sha256sum --check -
48 48
49 49 for v in ${PYENV2_VERSIONS}; do
50 50 pyenv install -v ${v}
51 51 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
52 52 ${PYENV_ROOT}/versions/${v}/bin/pip install ${VIRTUALENV_TARBALL}
53 53 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py2.txt
54 54 done
55 55
56 56 for v in ${PYENV3_VERSIONS}; do
57 57 pyenv install -v ${v}
58 58 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
59 59 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py3.txt
60 60 done
61 61
62 62 pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
63 63 '''.lstrip().replace(
64 64 '\r\n', '\n'
65 65 )
66 66
67 67
68 68 INSTALL_RUST = r'''
69 69 RUSTUP_INIT_SHA256=a46fe67199b7bcbbde2dcbc23ae08db6f29883e260e23899a88b9073effc9076
70 70 wget -O rustup-init --progress dot:mega https://static.rust-lang.org/rustup/archive/1.18.3/x86_64-unknown-linux-gnu/rustup-init
71 71 echo "${RUSTUP_INIT_SHA256} rustup-init" | sha256sum --check -
72 72
73 73 chmod +x rustup-init
74 74 sudo -H -u hg -g hg ./rustup-init -y
75 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup install 1.31.1 1.34.2
75 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup install 1.31.1 1.42.0
76 76 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup component add clippy
77
78 sudo -H -u hg -g hg /home/hg/.cargo/bin/cargo install --version 0.7.0 pyoxidizer
77 79 '''
78 80
79 81
80 82 BOOTSTRAP_VIRTUALENV = r'''
81 83 /usr/bin/virtualenv /hgdev/venv-bootstrap
82 84
83 85 HG_SHA256=35fc8ba5e0379c1b3affa2757e83fb0509e8ac314cbd9f1fd133cf265d16e49f
84 86 HG_TARBALL=mercurial-5.1.1.tar.gz
85 87
86 88 wget -O ${HG_TARBALL} --progress dot:mega https://www.mercurial-scm.org/release/${HG_TARBALL}
87 89 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
88 90
89 91 /hgdev/venv-bootstrap/bin/pip install ${HG_TARBALL}
90 92 '''.lstrip().replace(
91 93 '\r\n', '\n'
92 94 )
93 95
94 96
95 97 BOOTSTRAP_DEBIAN = (
96 98 r'''
97 99 #!/bin/bash
98 100
99 101 set -ex
100 102
101 103 DISTRO=`grep DISTRIB_ID /etc/lsb-release | awk -F= '{{print $2}}'`
102 104 DEBIAN_VERSION=`cat /etc/debian_version`
103 105 LSB_RELEASE=`lsb_release -cs`
104 106
105 107 sudo /usr/sbin/groupadd hg
106 108 sudo /usr/sbin/groupadd docker
107 109 sudo /usr/sbin/useradd -g hg -G sudo,docker -d /home/hg -m -s /bin/bash hg
108 110 sudo mkdir /home/hg/.ssh
109 111 sudo cp ~/.ssh/authorized_keys /home/hg/.ssh/authorized_keys
110 112 sudo chown -R hg:hg /home/hg/.ssh
111 113 sudo chmod 700 /home/hg/.ssh
112 114 sudo chmod 600 /home/hg/.ssh/authorized_keys
113 115
114 116 cat << EOF | sudo tee /etc/sudoers.d/90-hg
115 117 hg ALL=(ALL) NOPASSWD:ALL
116 118 EOF
117 119
118 120 sudo apt-get update
119 121 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade
120 122
121 123 # Install packages necessary to set up Docker Apt repo.
122 124 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends \
123 125 apt-transport-https \
124 126 gnupg
125 127
126 128 cat > docker-apt-key << EOF
127 129 -----BEGIN PGP PUBLIC KEY BLOCK-----
128 130
129 131 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
186 188 YT90qFF93M3v01BbxP+EIY2/9tiIPbrd
187 189 =0YYh
188 190 -----END PGP PUBLIC KEY BLOCK-----
189 191 EOF
190 192
191 193 sudo apt-key add docker-apt-key
192 194
193 195 if [ "$LSB_RELEASE" = "stretch" ]; then
194 196 cat << EOF | sudo tee -a /etc/apt/sources.list
195 197 # Need backports for clang-format-6.0
196 198 deb http://deb.debian.org/debian stretch-backports main
197 199 EOF
198 200 fi
199 201
200 202 if [ "$LSB_RELEASE" = "stretch" -o "$LSB_RELEASE" = "buster" ]; then
201 203 cat << EOF | sudo tee -a /etc/apt/sources.list
202 204 # Sources are useful if we want to compile things locally.
203 205 deb-src http://deb.debian.org/debian $LSB_RELEASE main
204 206 deb-src http://security.debian.org/debian-security $LSB_RELEASE/updates main
205 207 deb-src http://deb.debian.org/debian $LSB_RELEASE-updates main
206 208 deb-src http://deb.debian.org/debian $LSB_RELEASE-backports main
207 209
208 210 deb [arch=amd64] https://download.docker.com/linux/debian $LSB_RELEASE stable
209 211 EOF
210 212
211 213 elif [ "$DISTRO" = "Ubuntu" ]; then
212 214 cat << EOF | sudo tee -a /etc/apt/sources.list
213 215 deb [arch=amd64] https://download.docker.com/linux/ubuntu $LSB_RELEASE stable
214 216 EOF
215 217
216 218 fi
217 219
218 220 sudo apt-get update
219 221
220 222 PACKAGES="\
221 223 awscli \
222 224 btrfs-progs \
223 225 build-essential \
224 226 bzr \
225 227 clang-format-6.0 \
226 228 cvs \
227 229 darcs \
228 230 debhelper \
229 231 devscripts \
230 232 docker-ce \
231 233 dpkg-dev \
232 234 dstat \
233 235 emacs \
234 236 gettext \
235 237 git \
236 238 htop \
237 239 iotop \
238 240 jfsutils \
239 241 libbz2-dev \
240 242 libexpat1-dev \
241 243 libffi-dev \
242 244 libgdbm-dev \
243 245 liblzma-dev \
244 246 libncurses5-dev \
245 247 libnss3-dev \
246 248 libreadline-dev \
247 249 libsqlite3-dev \
248 250 libssl-dev \
249 251 netbase \
250 252 ntfs-3g \
251 253 nvme-cli \
252 254 pyflakes \
253 255 pyflakes3 \
254 256 pylint \
255 257 pylint3 \
256 258 python-all-dev \
257 259 python-dev \
258 260 python-docutils \
259 261 python-fuzzywuzzy \
260 262 python-pygments \
261 263 python-subversion \
262 264 python-vcr \
263 265 python3-boto3 \
264 266 python3-dev \
265 267 python3-docutils \
266 268 python3-fuzzywuzzy \
267 269 python3-pygments \
268 270 python3-vcr \
269 271 python3-venv \
270 272 rsync \
271 273 sqlite3 \
272 274 subversion \
273 275 tcl-dev \
274 276 tk-dev \
275 277 tla \
276 278 unzip \
277 279 uuid-dev \
278 280 vim \
279 281 virtualenv \
280 282 wget \
281 283 xfsprogs \
282 284 zip \
283 285 zlib1g-dev"
284 286
285 287 if [ "LSB_RELEASE" = "stretch" ]; then
286 288 PACKAGES="$PACKAGES linux-perf"
287 289 elif [ "$DISTRO" = "Ubuntu" ]; then
288 290 PACKAGES="$PACKAGES linux-tools-common"
289 291 fi
290 292
291 293 # Monotone only available in older releases.
292 294 if [ "$LSB_RELEASE" = "stretch" -o "$LSB_RELEASE" = "xenial" ]; then
293 295 PACKAGES="$PACKAGES monotone"
294 296 fi
295 297
296 298 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends $PACKAGES
297 299
298 300 # Create clang-format symlink so test harness finds it.
299 301 sudo update-alternatives --install /usr/bin/clang-format clang-format \
300 302 /usr/bin/clang-format-6.0 1000
301 303
302 304 sudo mkdir /hgdev
303 305 # Will be normalized to hg:hg later.
304 306 sudo chown `whoami` /hgdev
305 307
306 308 {install_rust}
307 309
308 310 cp requirements-py2.txt /hgdev/requirements-py2.txt
309 311 cp requirements-py3.txt /hgdev/requirements-py3.txt
310 312
311 313 # Disable the pip version check because it uses the network and can
312 314 # be annoying.
313 315 cat << EOF | sudo tee -a /etc/pip.conf
314 316 [global]
315 317 disable-pip-version-check = True
316 318 EOF
317 319
318 320 {install_pythons}
319 321 {bootstrap_virtualenv}
320 322
321 323 /hgdev/venv-bootstrap/bin/hg clone https://www.mercurial-scm.org/repo/hg /hgdev/src
322 324
323 325 # Mark the repo as non-publishing.
324 326 cat >> /hgdev/src/.hg/hgrc << EOF
325 327 [phases]
326 328 publish = false
327 329 EOF
328 330
329 331 sudo chown -R hg:hg /hgdev
330 332 '''.lstrip()
331 333 .format(
332 334 install_rust=INSTALL_RUST,
333 335 install_pythons=INSTALL_PYTHONS,
334 336 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV,
335 337 )
336 338 .replace('\r\n', '\n')
337 339 )
338 340
339 341
340 342 # Prepares /hgdev for operations.
341 343 PREPARE_HGDEV = '''
342 344 #!/bin/bash
343 345
344 346 set -e
345 347
346 348 FS=$1
347 349
348 350 ensure_device() {
349 351 if [ -z "${DEVICE}" ]; then
350 352 echo "could not find block device to format"
351 353 exit 1
352 354 fi
353 355 }
354 356
355 357 # Determine device to partition for extra filesystem.
356 358 # If only 1 volume is present, it will be the root volume and
357 359 # should be /dev/nvme0. If multiple volumes are present, the
358 360 # root volume could be nvme0 or nvme1. Use whichever one doesn't have
359 361 # a partition.
360 362 if [ -e /dev/nvme1n1 ]; then
361 363 if [ -e /dev/nvme0n1p1 ]; then
362 364 DEVICE=/dev/nvme1n1
363 365 else
364 366 DEVICE=/dev/nvme0n1
365 367 fi
366 368 else
367 369 DEVICE=
368 370 fi
369 371
370 372 sudo mkdir /hgwork
371 373
372 374 if [ "${FS}" != "default" -a "${FS}" != "tmpfs" ]; then
373 375 ensure_device
374 376 echo "creating ${FS} filesystem on ${DEVICE}"
375 377 fi
376 378
377 379 if [ "${FS}" = "default" ]; then
378 380 :
379 381
380 382 elif [ "${FS}" = "btrfs" ]; then
381 383 sudo mkfs.btrfs ${DEVICE}
382 384 sudo mount ${DEVICE} /hgwork
383 385
384 386 elif [ "${FS}" = "ext3" ]; then
385 387 # lazy_journal_init speeds up filesystem creation at the expense of
386 388 # integrity if things crash. We are an ephemeral instance, so we don't
387 389 # care about integrity.
388 390 sudo mkfs.ext3 -E lazy_journal_init=1 ${DEVICE}
389 391 sudo mount ${DEVICE} /hgwork
390 392
391 393 elif [ "${FS}" = "ext4" ]; then
392 394 sudo mkfs.ext4 -E lazy_journal_init=1 ${DEVICE}
393 395 sudo mount ${DEVICE} /hgwork
394 396
395 397 elif [ "${FS}" = "jfs" ]; then
396 398 sudo mkfs.jfs ${DEVICE}
397 399 sudo mount ${DEVICE} /hgwork
398 400
399 401 elif [ "${FS}" = "tmpfs" ]; then
400 402 echo "creating tmpfs volume in /hgwork"
401 403 sudo mount -t tmpfs -o size=1024M tmpfs /hgwork
402 404
403 405 elif [ "${FS}" = "xfs" ]; then
404 406 sudo mkfs.xfs ${DEVICE}
405 407 sudo mount ${DEVICE} /hgwork
406 408
407 409 else
408 410 echo "unsupported filesystem: ${FS}"
409 411 exit 1
410 412 fi
411 413
412 414 echo "/hgwork ready"
413 415
414 416 sudo chown hg:hg /hgwork
415 417 mkdir /hgwork/tmp
416 418 chown hg:hg /hgwork/tmp
417 419
418 420 rsync -a /hgdev/src /hgwork/
419 421 '''.lstrip().replace(
420 422 '\r\n', '\n'
421 423 )
422 424
423 425
424 426 HG_UPDATE_CLEAN = '''
425 427 set -ex
426 428
427 429 HG=/hgdev/venv-bootstrap/bin/hg
428 430
429 431 cd /hgwork/src
430 432 ${HG} --config extensions.purge= purge --all
431 433 ${HG} update -C $1
432 434 ${HG} log -r .
433 435 '''.lstrip().replace(
434 436 '\r\n', '\n'
435 437 )
436 438
437 439
438 440 def prepare_exec_environment(ssh_client, filesystem='default'):
439 441 """Prepare an EC2 instance to execute things.
440 442
441 443 The AMI has an ``/hgdev`` bootstrapped with various Python installs
442 444 and a clone of the Mercurial repo.
443 445
444 446 In EC2, EBS volumes launched from snapshots have wonky performance behavior.
445 447 Notably, blocks have to be copied on first access, which makes volume
446 448 I/O extremely slow on fresh volumes.
447 449
448 450 Furthermore, we may want to run operations, tests, etc on alternative
449 451 filesystems so we examine behavior on different filesystems.
450 452
451 453 This function is used to facilitate executing operations on alternate
452 454 volumes.
453 455 """
454 456 sftp = ssh_client.open_sftp()
455 457
456 458 with sftp.open('/hgdev/prepare-hgdev', 'wb') as fh:
457 459 fh.write(PREPARE_HGDEV)
458 460 fh.chmod(0o0777)
459 461
460 462 command = 'sudo /hgdev/prepare-hgdev %s' % filesystem
461 463 chan, stdin, stdout = exec_command(ssh_client, command)
462 464 stdin.close()
463 465
464 466 for line in stdout:
465 467 print(line, end='')
466 468
467 469 res = chan.recv_exit_status()
468 470
469 471 if res:
470 472 raise Exception('non-0 exit code updating working directory; %d' % res)
471 473
472 474
473 475 def synchronize_hg(
474 476 source_path: pathlib.Path, ec2_instance, revision: str = None
475 477 ):
476 478 """Synchronize a local Mercurial source path to remote EC2 instance."""
477 479
478 480 with tempfile.TemporaryDirectory() as temp_dir:
479 481 temp_dir = pathlib.Path(temp_dir)
480 482
481 483 ssh_dir = temp_dir / '.ssh'
482 484 ssh_dir.mkdir()
483 485 ssh_dir.chmod(0o0700)
484 486
485 487 public_ip = ec2_instance.public_ip_address
486 488
487 489 ssh_config = ssh_dir / 'config'
488 490
489 491 with ssh_config.open('w', encoding='utf-8') as fh:
490 492 fh.write('Host %s\n' % public_ip)
491 493 fh.write(' User hg\n')
492 494 fh.write(' StrictHostKeyChecking no\n')
493 495 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
494 496 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
495 497
496 498 if not (source_path / '.hg').is_dir():
497 499 raise Exception(
498 500 '%s is not a Mercurial repository; synchronization '
499 501 'not yet supported' % source_path
500 502 )
501 503
502 504 env = dict(os.environ)
503 505 env['HGPLAIN'] = '1'
504 506 env['HGENCODING'] = 'utf-8'
505 507
506 508 hg_bin = source_path / 'hg'
507 509
508 510 res = subprocess.run(
509 511 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
510 512 cwd=str(source_path),
511 513 env=env,
512 514 check=True,
513 515 capture_output=True,
514 516 )
515 517
516 518 full_revision = res.stdout.decode('ascii')
517 519
518 520 args = [
519 521 'python2.7',
520 522 str(hg_bin),
521 523 '--config',
522 524 'ui.ssh=ssh -F %s' % ssh_config,
523 525 '--config',
524 526 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
525 527 # Also ensure .hgtags changes are present so auto version
526 528 # calculation works.
527 529 'push',
528 530 '-f',
529 531 '-r',
530 532 full_revision,
531 533 '-r',
532 534 'file(.hgtags)',
533 535 'ssh://%s//hgwork/src' % public_ip,
534 536 ]
535 537
536 538 res = subprocess.run(args, cwd=str(source_path), env=env)
537 539
538 540 # Allow 1 (no-op) to not trigger error.
539 541 if res.returncode not in (0, 1):
540 542 res.check_returncode()
541 543
542 544 # TODO support synchronizing dirty working directory.
543 545
544 546 sftp = ec2_instance.ssh_client.open_sftp()
545 547
546 548 with sftp.open('/hgdev/hgup', 'wb') as fh:
547 549 fh.write(HG_UPDATE_CLEAN)
548 550 fh.chmod(0o0700)
549 551
550 552 chan, stdin, stdout = exec_command(
551 553 ec2_instance.ssh_client, '/hgdev/hgup %s' % full_revision
552 554 )
553 555 stdin.close()
554 556
555 557 for line in stdout:
556 558 print(line, end='')
557 559
558 560 res = chan.recv_exit_status()
559 561
560 562 if res:
561 563 raise Exception(
562 564 'non-0 exit code updating working directory; %d' % res
563 565 )
564 566
565 567
566 568 def run_tests(ssh_client, python_version, test_flags=None):
567 569 """Run tests on a remote Linux machine via an SSH client."""
568 570 test_flags = test_flags or []
569 571
570 572 print('running tests')
571 573
572 574 if python_version == 'system2':
573 575 python = '/usr/bin/python2'
574 576 elif python_version == 'system3':
575 577 python = '/usr/bin/python3'
576 578 elif python_version.startswith('pypy'):
577 579 python = '/hgdev/pyenv/shims/%s' % python_version
578 580 else:
579 581 python = '/hgdev/pyenv/shims/python%s' % python_version
580 582
581 583 test_flags = ' '.join(shlex.quote(a) for a in test_flags)
582 584
583 585 command = (
584 586 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
585 587 'cd /hgwork/src/tests && %s run-tests.py %s"' % (python, test_flags)
586 588 )
587 589
588 590 chan, stdin, stdout = exec_command(ssh_client, command)
589 591
590 592 stdin.close()
591 593
592 594 for line in stdout:
593 595 print(line, end='')
594 596
595 597 return chan.recv_exit_status()
@@ -1,200 +1,220 b''
1 1 # install-dependencies.ps1 - Install Windows dependencies for building Mercurial
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 # This script can be used to bootstrap a Mercurial build environment on
9 9 # Windows.
10 10 #
11 11 # The script makes a lot of assumptions about how things should work.
12 12 # For example, the install location of Python is hardcoded to c:\hgdev\*.
13 13 #
14 14 # The script should be executed from a PowerShell with elevated privileges
15 15 # if you don't want to see a UAC prompt for various installers.
16 16 #
17 17 # The script is tested on Windows 10 and Windows Server 2019 (in EC2).
18 18
19 19 $VS_BUILD_TOOLS_URL = "https://download.visualstudio.microsoft.com/download/pr/a1603c02-8a66-4b83-b821-811e3610a7c4/aa2db8bb39e0cbd23e9940d8951e0bc3/vs_buildtools.exe"
20 20 $VS_BUILD_TOOLS_SHA256 = "911E292B8E6E5F46CBC17003BDCD2D27A70E616E8D5E6E69D5D489A605CAA139"
21 21
22 22 $VC9_PYTHON_URL = "https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi"
23 23 $VC9_PYTHON_SHA256 = "070474db76a2e625513a5835df4595df9324d820f9cc97eab2a596dcbc2f5cbf"
24 24
25 25 $PYTHON27_x64_URL = "https://www.python.org/ftp/python/2.7.18/python-2.7.18.amd64.msi"
26 26 $PYTHON27_x64_SHA256 = "b74a3afa1e0bf2a6fc566a7b70d15c9bfabba3756fb077797d16fffa27800c05"
27 27 $PYTHON27_X86_URL = "https://www.python.org/ftp/python/2.7.18/python-2.7.18.msi"
28 28 $PYTHON27_X86_SHA256 = "d901802e90026e9bad76b8a81f8dd7e43c7d7e8269d9281c9e9df7a9c40480a9"
29 29
30 30 $PYTHON35_x86_URL = "https://www.python.org/ftp/python/3.5.4/python-3.5.4.exe"
31 31 $PYTHON35_x86_SHA256 = "F27C2D67FD9688E4970F3BFF799BB9D722A0D6C2C13B04848E1F7D620B524B0E"
32 32 $PYTHON35_x64_URL = "https://www.python.org/ftp/python/3.5.4/python-3.5.4-amd64.exe"
33 33 $PYTHON35_x64_SHA256 = "9B7741CC32357573A77D2EE64987717E527628C38FD7EAF3E2AACA853D45A1EE"
34 34
35 35 $PYTHON36_x86_URL = "https://www.python.org/ftp/python/3.6.8/python-3.6.8.exe"
36 36 $PYTHON36_x86_SHA256 = "89871D432BC06E4630D7B64CB1A8451E53C80E68DE29029976B12AAD7DBFA5A0"
37 37 $PYTHON36_x64_URL = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-amd64.exe"
38 38 $PYTHON36_x64_SHA256 = "96088A58B7C43BC83B84E6B67F15E8706C614023DD64F9A5A14E81FF824ADADC"
39 39
40 40 $PYTHON37_x86_URL = "https://www.python.org/ftp/python/3.7.7/python-3.7.7.exe"
41 41 $PYTHON37_x86_SHA256 = "27fbffcd342d5055acc64050db4c35d0025661521e642b59c381dcba2e162c6a"
42 42 $PYTHON37_X64_URL = "https://www.python.org/ftp/python/3.7.7/python-3.7.7-amd64.exe"
43 43 $PYTHON37_x64_SHA256 = "1a0368663ceff999d865de955992b6ea3cb0c8cb15a1a296a8eb7df19cc59e69"
44 44
45 45 $PYTHON38_x86_URL = "https://www.python.org/ftp/python/3.8.2/python-3.8.2.exe"
46 46 $PYTHON38_x86_SHA256 = "03ac5754a69c9c11c08d1f4d694c14625a4d27348ad4dd2d1253e2547819db2c"
47 47 $PYTHON38_x64_URL = "https://www.python.org/ftp/python/3.8.2/python-3.8.2-amd64.exe"
48 48 $PYTHON38_x64_SHA256 = "8e400e3f32cdcb746e62e0db4d3ae4cba1f927141ebc4d0d5a4006b0daee8921"
49 49
50 50 # PIP 19.2.3.
51 51 $PIP_URL = "https://github.com/pypa/get-pip/raw/309a56c5fd94bd1134053a541cb4657a4e47e09d/get-pip.py"
52 52 $PIP_SHA256 = "57e3643ff19f018f8a00dfaa6b7e4620e3c1a7a2171fd218425366ec006b3bfe"
53 53
54 54 $VIRTUALENV_URL = "https://files.pythonhosted.org/packages/66/f0/6867af06d2e2f511e4e1d7094ff663acdebc4f15d4a0cb0fed1007395124/virtualenv-16.7.5.tar.gz"
55 55 $VIRTUALENV_SHA256 = "f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"
56 56
57 57 $INNO_SETUP_URL = "http://files.jrsoftware.org/is/5/innosetup-5.6.1-unicode.exe"
58 58 $INNO_SETUP_SHA256 = "27D49E9BC769E9D1B214C153011978DB90DC01C2ACD1DDCD9ED7B3FE3B96B538"
59 59
60 60 $MINGW_BIN_URL = "https://osdn.net/frs/redir.php?m=constant&f=mingw%2F68260%2Fmingw-get-0.6.3-mingw32-pre-20170905-1-bin.zip"
61 61 $MINGW_BIN_SHA256 = "2AB8EFD7C7D1FC8EAF8B2FA4DA4EEF8F3E47768284C021599BC7435839A046DF"
62 62
63 63 $MERCURIAL_WHEEL_FILENAME = "mercurial-5.1.2-cp27-cp27m-win_amd64.whl"
64 64 $MERCURIAL_WHEEL_URL = "https://files.pythonhosted.org/packages/6d/47/e031e47f7fe9b16e4e3383da47e2b0a7eae6e603996bc67a03ec4fa1b3f4/$MERCURIAL_WHEEL_FILENAME"
65 65 $MERCURIAL_WHEEL_SHA256 = "1d18c7f6ca1456f0f62ee65c9a50c14cbba48ce6e924930cdb10537f5c9eaf5f"
66 66
67 $RUSTUP_INIT_URL = "https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-pc-windows-gnu/rustup-init.exe"
68 $RUSTUP_INIT_SHA256 = "d17df34ba974b9b19cf5c75883a95475aa22ddc364591d75d174090d55711c72"
69
67 70 # Writing progress slows down downloads substantially. So disable it.
68 71 $progressPreference = 'silentlyContinue'
69 72
70 73 function Secure-Download($url, $path, $sha256) {
71 74 if (Test-Path -Path $path) {
72 75 Get-FileHash -Path $path -Algorithm SHA256 -OutVariable hash
73 76
74 77 if ($hash.Hash -eq $sha256) {
75 78 Write-Output "SHA256 of $path verified as $sha256"
76 79 return
77 80 }
78 81
79 82 Write-Output "hash mismatch on $path; downloading again"
80 83 }
81 84
82 85 Write-Output "downloading $url to $path"
83 86 Invoke-WebRequest -Uri $url -OutFile $path
84 87 Get-FileHash -Path $path -Algorithm SHA256 -OutVariable hash
85 88
86 89 if ($hash.Hash -ne $sha256) {
87 90 Remove-Item -Path $path
88 91 throw "hash mismatch when downloading $url; got $($hash.Hash), expected $sha256"
89 92 }
90 93 }
91 94
92 95 function Invoke-Process($path, $arguments) {
93 96 $p = Start-Process -FilePath $path -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
94 97
95 98 if ($p.ExitCode -ne 0) {
96 99 throw "process exited non-0: $($p.ExitCode)"
97 100 }
98 101 }
99 102
100 103 function Install-Python3($name, $installer, $dest, $pip) {
101 104 Write-Output "installing $name"
102 105
103 106 # We hit this when running the script as part of Simple Systems Manager in
104 107 # EC2. The Python 3 installer doesn't seem to like per-user installs
105 108 # when running as the SYSTEM user. So enable global installs if executed in
106 109 # this mode.
107 110 if ($env:USERPROFILE -eq "C:\Windows\system32\config\systemprofile") {
108 111 Write-Output "running with SYSTEM account; installing for all users"
109 112 $allusers = "1"
110 113 }
111 114 else {
112 115 $allusers = "0"
113 116 }
114 117
115 118 Invoke-Process $installer "/quiet TargetDir=${dest} InstallAllUsers=${allusers} AssociateFiles=0 CompileAll=0 PrependPath=0 Include_doc=0 Include_launcher=0 InstallLauncherAllUsers=0 Include_pip=0 Include_test=0"
116 119 Invoke-Process ${dest}\python.exe $pip
117 120 }
118 121
122 function Install-Rust($prefix) {
123 Write-Output "installing Rust"
124 $Env:RUSTUP_HOME = "${prefix}\rustup"
125 $Env:CARGO_HOME = "${prefix}\cargo"
126
127 Invoke-Process "${prefix}\assets\rustup-init.exe" "-y --default-host x86_64-pc-windows-msvc"
128 Invoke-Process "${prefix}\cargo\bin\rustup.exe" "target add i686-pc-windows-msvc"
129 Invoke-Process "${prefix}\cargo\bin\rustup.exe" "install 1.42.0"
130 Invoke-Process "${prefix}\cargo\bin\rustup.exe" "component add clippy"
131
132 # Install PyOxidizer for packaging.
133 Invoke-Process "${prefix}\cargo\bin\cargo.exe" "install --version 0.7.0 pyoxidizer"
134 }
135
119 136 function Install-Dependencies($prefix) {
120 137 if (!(Test-Path -Path $prefix\assets)) {
121 138 New-Item -Path $prefix\assets -ItemType Directory
122 139 }
123 140
124 141 $pip = "${prefix}\assets\get-pip.py"
125 142
126 143 Secure-Download $VC9_PYTHON_URL ${prefix}\assets\VCForPython27.msi $VC9_PYTHON_SHA256
127 144 Secure-Download $PYTHON27_x86_URL ${prefix}\assets\python27-x86.msi $PYTHON27_x86_SHA256
128 145 Secure-Download $PYTHON27_x64_URL ${prefix}\assets\python27-x64.msi $PYTHON27_x64_SHA256
129 146 Secure-Download $PYTHON35_x86_URL ${prefix}\assets\python35-x86.exe $PYTHON35_x86_SHA256
130 147 Secure-Download $PYTHON35_x64_URL ${prefix}\assets\python35-x64.exe $PYTHON35_x64_SHA256
131 148 Secure-Download $PYTHON36_x86_URL ${prefix}\assets\python36-x86.exe $PYTHON36_x86_SHA256
132 149 Secure-Download $PYTHON36_x64_URL ${prefix}\assets\python36-x64.exe $PYTHON36_x64_SHA256
133 150 Secure-Download $PYTHON37_x86_URL ${prefix}\assets\python37-x86.exe $PYTHON37_x86_SHA256
134 151 Secure-Download $PYTHON37_x64_URL ${prefix}\assets\python37-x64.exe $PYTHON37_x64_SHA256
135 152 Secure-Download $PYTHON38_x86_URL ${prefix}\assets\python38-x86.exe $PYTHON38_x86_SHA256
136 153 Secure-Download $PYTHON38_x64_URL ${prefix}\assets\python38-x64.exe $PYTHON38_x64_SHA256
137 154 Secure-Download $PIP_URL ${pip} $PIP_SHA256
138 155 Secure-Download $VIRTUALENV_URL ${prefix}\assets\virtualenv.tar.gz $VIRTUALENV_SHA256
139 156 Secure-Download $VS_BUILD_TOOLS_URL ${prefix}\assets\vs_buildtools.exe $VS_BUILD_TOOLS_SHA256
140 157 Secure-Download $INNO_SETUP_URL ${prefix}\assets\InnoSetup.exe $INNO_SETUP_SHA256
141 158 Secure-Download $MINGW_BIN_URL ${prefix}\assets\mingw-get-bin.zip $MINGW_BIN_SHA256
142 159 Secure-Download $MERCURIAL_WHEEL_URL ${prefix}\assets\${MERCURIAL_WHEEL_FILENAME} $MERCURIAL_WHEEL_SHA256
160 Secure-Download $RUSTUP_INIT_URL ${prefix}\assets\rustup-init.exe $RUSTUP_INIT_SHA256
143 161
144 162 Write-Output "installing Python 2.7 32-bit"
145 163 Invoke-Process msiexec.exe "/i ${prefix}\assets\python27-x86.msi /l* ${prefix}\assets\python27-x86.log /q TARGETDIR=${prefix}\python27-x86 ALLUSERS="
146 164 Invoke-Process ${prefix}\python27-x86\python.exe ${prefix}\assets\get-pip.py
147 165 Invoke-Process ${prefix}\python27-x86\Scripts\pip.exe "install ${prefix}\assets\virtualenv.tar.gz"
148 166
149 167 Write-Output "installing Python 2.7 64-bit"
150 168 Invoke-Process msiexec.exe "/i ${prefix}\assets\python27-x64.msi /l* ${prefix}\assets\python27-x64.log /q TARGETDIR=${prefix}\python27-x64 ALLUSERS="
151 169 Invoke-Process ${prefix}\python27-x64\python.exe ${prefix}\assets\get-pip.py
152 170 Invoke-Process ${prefix}\python27-x64\Scripts\pip.exe "install ${prefix}\assets\virtualenv.tar.gz"
153 171
154 172 Install-Python3 "Python 3.5 32-bit" ${prefix}\assets\python35-x86.exe ${prefix}\python35-x86 ${pip}
155 173 Install-Python3 "Python 3.5 64-bit" ${prefix}\assets\python35-x64.exe ${prefix}\python35-x64 ${pip}
156 174 Install-Python3 "Python 3.6 32-bit" ${prefix}\assets\python36-x86.exe ${prefix}\python36-x86 ${pip}
157 175 Install-Python3 "Python 3.6 64-bit" ${prefix}\assets\python36-x64.exe ${prefix}\python36-x64 ${pip}
158 176 Install-Python3 "Python 3.7 32-bit" ${prefix}\assets\python37-x86.exe ${prefix}\python37-x86 ${pip}
159 177 Install-Python3 "Python 3.7 64-bit" ${prefix}\assets\python37-x64.exe ${prefix}\python37-x64 ${pip}
160 178 Install-Python3 "Python 3.8 32-bit" ${prefix}\assets\python38-x86.exe ${prefix}\python38-x86 ${pip}
161 179 Install-Python3 "Python 3.8 64-bit" ${prefix}\assets\python38-x64.exe ${prefix}\python38-x64 ${pip}
162 180
163 181 Write-Output "installing Visual Studio 2017 Build Tools and SDKs"
164 182 Invoke-Process ${prefix}\assets\vs_buildtools.exe "--quiet --wait --norestart --nocache --channelUri https://aka.ms/vs/15/release/channel --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.Windows10SDK.17763 --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows10SDK --add Microsoft.VisualStudio.Component.VC.140"
165 183
184 Install-Rust ${prefix}
185
166 186 Write-Output "installing Visual C++ 9.0 for Python 2.7"
167 187 Invoke-Process msiexec.exe "/i ${prefix}\assets\VCForPython27.msi /l* ${prefix}\assets\VCForPython27.log /q"
168 188
169 189 Write-Output "installing Inno Setup"
170 190 Invoke-Process ${prefix}\assets\InnoSetup.exe "/SP- /VERYSILENT /SUPPRESSMSGBOXES"
171 191
172 192 Write-Output "extracting MinGW base archive"
173 193 Expand-Archive -Path ${prefix}\assets\mingw-get-bin.zip -DestinationPath "${prefix}\MinGW" -Force
174 194
175 195 Write-Output "updating MinGW package catalogs"
176 196 Invoke-Process ${prefix}\MinGW\bin\mingw-get.exe "update"
177 197
178 198 Write-Output "installing MinGW packages"
179 199 Invoke-Process ${prefix}\MinGW\bin\mingw-get.exe "install msys-base msys-coreutils msys-diffutils msys-unzip"
180 200
181 201 # Construct a virtualenv useful for bootstrapping. It conveniently contains a
182 202 # Mercurial install.
183 203 Write-Output "creating bootstrap virtualenv with Mercurial"
184 204 Invoke-Process "$prefix\python27-x64\Scripts\virtualenv.exe" "${prefix}\venv-bootstrap"
185 205 Invoke-Process "${prefix}\venv-bootstrap\Scripts\pip.exe" "install ${prefix}\assets\${MERCURIAL_WHEEL_FILENAME}"
186 206 }
187 207
188 208 function Clone-Mercurial-Repo($prefix, $repo_url, $dest) {
189 209 Write-Output "cloning $repo_url to $dest"
190 210 # TODO Figure out why CA verification isn't working in EC2 and remove
191 211 # --insecure.
192 212 Invoke-Process "${prefix}\venv-bootstrap\Scripts\hg.exe" "clone --insecure $repo_url $dest"
193 213
194 214 # Mark repo as non-publishing by default for convenience.
195 215 Add-Content -Path "$dest\.hg\hgrc" -Value "`n[phases]`npublish = false"
196 216 }
197 217
198 218 $prefix = "c:\hgdev"
199 219 Install-Dependencies $prefix
200 220 Clone-Mercurial-Repo $prefix "https://www.mercurial-scm.org/repo/hg" $prefix\src
General Comments 0
You need to be logged in to leave comments. Login now