##// END OF EJS Templates
packaging: consolidate CLI functionality into packaging.py...
Gregory Szorc -
r43913:081a77df default
parent child Browse files
Show More
@@ -0,0 +1,153 b''
1 # cli.py - Command line interface for automation
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 # no-check-code because Python 3 native.
9
10 import argparse
11 import os
12 import pathlib
13
14 from . import (
15 inno,
16 wix,
17 )
18
19 HERE = pathlib.Path(os.path.abspath(os.path.dirname(__file__)))
20 SOURCE_DIR = HERE.parent.parent.parent
21
22
23 def build_inno(python=None, iscc=None, version=None):
24 if not os.path.isabs(python):
25 raise Exception("--python arg must be an absolute path")
26
27 if iscc:
28 iscc = pathlib.Path(iscc)
29 else:
30 iscc = (
31 pathlib.Path(os.environ["ProgramFiles(x86)"])
32 / "Inno Setup 5"
33 / "ISCC.exe"
34 )
35
36 build_dir = SOURCE_DIR / "build"
37
38 inno.build(
39 SOURCE_DIR, build_dir, pathlib.Path(python), iscc, version=version,
40 )
41
42
43 def build_wix(
44 name=None,
45 python=None,
46 version=None,
47 sign_sn=None,
48 sign_cert=None,
49 sign_password=None,
50 sign_timestamp_url=None,
51 extra_packages_script=None,
52 extra_wxs=None,
53 extra_features=None,
54 ):
55 fn = wix.build_installer
56 kwargs = {
57 "source_dir": SOURCE_DIR,
58 "python_exe": pathlib.Path(python),
59 "version": version,
60 }
61
62 if not os.path.isabs(python):
63 raise Exception("--python arg must be an absolute path")
64
65 if extra_packages_script:
66 kwargs["extra_packages_script"] = extra_packages_script
67 if extra_wxs:
68 kwargs["extra_wxs"] = dict(
69 thing.split("=") for thing in extra_wxs.split(",")
70 )
71 if extra_features:
72 kwargs["extra_features"] = extra_features.split(",")
73
74 if sign_sn or sign_cert:
75 fn = wix.build_signed_installer
76 kwargs["name"] = name
77 kwargs["subject_name"] = sign_sn
78 kwargs["cert_path"] = sign_cert
79 kwargs["cert_password"] = sign_password
80 kwargs["timestamp_url"] = sign_timestamp_url
81
82 fn(**kwargs)
83
84
85 def get_parser():
86 parser = argparse.ArgumentParser()
87
88 subparsers = parser.add_subparsers()
89
90 sp = subparsers.add_parser("inno", help="Build Inno Setup installer")
91 sp.add_argument("--python", required=True, help="path to python.exe to use")
92 sp.add_argument("--iscc", help="path to iscc.exe to use")
93 sp.add_argument(
94 "--version",
95 help="Mercurial version string to use "
96 "(detected from __version__.py if not defined",
97 )
98 sp.set_defaults(func=build_inno)
99
100 sp = subparsers.add_parser(
101 "wix", help="Build Windows installer with WiX Toolset"
102 )
103 sp.add_argument("--name", help="Application name", default="Mercurial")
104 sp.add_argument(
105 "--python", help="Path to Python executable to use", required=True
106 )
107 sp.add_argument(
108 "--sign-sn",
109 help="Subject name (or fragment thereof) of certificate "
110 "to use for signing",
111 )
112 sp.add_argument(
113 "--sign-cert", help="Path to certificate to use for signing"
114 )
115 sp.add_argument("--sign-password", help="Password for signing certificate")
116 sp.add_argument(
117 "--sign-timestamp-url",
118 help="URL of timestamp server to use for signing",
119 )
120 sp.add_argument("--version", help="Version string to use")
121 sp.add_argument(
122 "--extra-packages-script",
123 help=(
124 "Script to execute to include extra packages in " "py2exe binary."
125 ),
126 )
127 sp.add_argument(
128 "--extra-wxs", help="CSV of path_to_wxs_file=working_dir_for_wxs_file"
129 )
130 sp.add_argument(
131 "--extra-features",
132 help=(
133 "CSV of extra feature names to include "
134 "in the installer from the extra wxs files"
135 ),
136 )
137 sp.set_defaults(func=build_wix)
138
139 return parser
140
141
142 def main():
143 parser = get_parser()
144 args = parser.parse_args()
145
146 if not hasattr(args, "func"):
147 parser.print_help()
148 return
149
150 kwargs = dict(vars(args))
151 del kwargs["func"]
152
153 args.func(**kwargs)
@@ -0,0 +1,74 b''
1 #!/usr/bin/env python3
2 #
3 # packaging.py - Mercurial packaging functionality
4 #
5 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
6 #
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
9
10 import os
11 import pathlib
12 import subprocess
13 import sys
14 import venv
15
16
17 HERE = pathlib.Path(os.path.abspath(__file__)).parent
18 REQUIREMENTS_TXT = HERE / "requirements.txt"
19 SOURCE_DIR = HERE.parent.parent
20 VENV = SOURCE_DIR / "build" / "venv-packaging"
21
22
23 def bootstrap():
24 venv_created = not VENV.exists()
25
26 VENV.parent.mkdir(exist_ok=True)
27
28 venv.create(VENV, with_pip=True)
29
30 if os.name == "nt":
31 venv_bin = VENV / "Scripts"
32 pip = venv_bin / "pip.exe"
33 python = venv_bin / "python.exe"
34 else:
35 venv_bin = VENV / "bin"
36 pip = venv_bin / "pip"
37 python = venv_bin / "python"
38
39 args = [
40 str(pip),
41 "install",
42 "-r",
43 str(REQUIREMENTS_TXT),
44 "--disable-pip-version-check",
45 ]
46
47 if not venv_created:
48 args.append("-q")
49
50 subprocess.run(args, check=True)
51
52 os.environ["HGPACKAGING_BOOTSTRAPPED"] = "1"
53 os.environ["PATH"] = "%s%s%s" % (venv_bin, os.pathsep, os.environ["PATH"])
54
55 subprocess.run([str(python), __file__] + sys.argv[1:], check=True)
56
57
58 def run():
59 import hgpackaging.cli as cli
60
61 # Need to strip off main Python executable.
62 cli.main()
63
64
65 if __name__ == "__main__":
66 try:
67 if "HGPACKAGING_BOOTSTRAPPED" not in os.environ:
68 bootstrap()
69 else:
70 run()
71 except subprocess.CalledProcessError as e:
72 sys.exit(e.returncode)
73 except KeyboardInterrupt:
74 sys.exit(1)
@@ -0,0 +1,39 b''
1 #
2 # This file is autogenerated by pip-compile
3 # To update, run:
4 #
5 # pip-compile --generate-hashes --output-file=contrib/packaging/requirements.txt contrib/packaging/requirements.txt.in
6 #
7 jinja2==2.10.3 \
8 --hash=sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f \
9 --hash=sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de
10 markupsafe==1.1.1 \
11 --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
12 --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
13 --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
14 --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
15 --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
16 --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \
17 --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
18 --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
19 --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
20 --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
21 --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
22 --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
23 --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
24 --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
25 --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
26 --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
27 --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
28 --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
29 --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
30 --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
31 --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
32 --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
33 --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
34 --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
35 --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
36 --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
37 --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
38 --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
39 # via jinja2
@@ -0,0 +1,1 b''
1 jinja2
@@ -1,510 +1,510 b''
1 # windows.py - Automation specific to Windows
1 # windows.py - Automation specific to Windows
2 #
2 #
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # no-check-code because Python 3 native.
8 # no-check-code because Python 3 native.
9
9
10 import datetime
10 import datetime
11 import os
11 import os
12 import paramiko
12 import paramiko
13 import pathlib
13 import pathlib
14 import re
14 import re
15 import subprocess
15 import subprocess
16 import tempfile
16 import tempfile
17
17
18 from .pypi import upload as pypi_upload
18 from .pypi import upload as pypi_upload
19 from .winrm import run_powershell
19 from .winrm import run_powershell
20
20
21
21
22 # PowerShell commands to activate a Visual Studio 2008 environment.
22 # PowerShell commands to activate a Visual Studio 2008 environment.
23 # This is essentially a port of vcvarsall.bat to PowerShell.
23 # This is essentially a port of vcvarsall.bat to PowerShell.
24 ACTIVATE_VC9_AMD64 = r'''
24 ACTIVATE_VC9_AMD64 = r'''
25 Write-Output "activating Visual Studio 2008 environment for AMD64"
25 Write-Output "activating Visual Studio 2008 environment for AMD64"
26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
26 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
27 $Env:VCINSTALLDIR = "${root}\VC\"
27 $Env:VCINSTALLDIR = "${root}\VC\"
28 $Env:WindowsSdkDir = "${root}\WinSDK\"
28 $Env:WindowsSdkDir = "${root}\WinSDK\"
29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
29 $Env:PATH = "${root}\VC\Bin\amd64;${root}\WinSDK\Bin\x64;${root}\WinSDK\Bin;$Env:PATH"
30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
30 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:PATH"
31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
31 $Env:LIB = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIB"
32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
32 $Env:LIBPATH = "${root}\VC\Lib\amd64;${root}\WinSDK\Lib\x64;$Env:LIBPATH"
33 '''.lstrip()
33 '''.lstrip()
34
34
35 ACTIVATE_VC9_X86 = r'''
35 ACTIVATE_VC9_X86 = r'''
36 Write-Output "activating Visual Studio 2008 environment for x86"
36 Write-Output "activating Visual Studio 2008 environment for x86"
37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
37 $root = "$env:LOCALAPPDATA\Programs\Common\Microsoft\Visual C++ for Python\9.0"
38 $Env:VCINSTALLDIR = "${root}\VC\"
38 $Env:VCINSTALLDIR = "${root}\VC\"
39 $Env:WindowsSdkDir = "${root}\WinSDK\"
39 $Env:WindowsSdkDir = "${root}\WinSDK\"
40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
40 $Env:PATH = "${root}\VC\Bin;${root}\WinSDK\Bin;$Env:PATH"
41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
41 $Env:INCLUDE = "${root}\VC\Include;${root}\WinSDK\Include;$Env:INCLUDE"
42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
42 $Env:LIB = "${root}\VC\Lib;${root}\WinSDK\Lib;$Env:LIB"
43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
43 $Env:LIBPATH = "${root}\VC\lib;${root}\WinSDK\Lib;$Env:LIBPATH"
44 '''.lstrip()
44 '''.lstrip()
45
45
46 HG_PURGE = r'''
46 HG_PURGE = r'''
47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
47 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
48 Set-Location C:\hgdev\src
48 Set-Location C:\hgdev\src
49 hg.exe --config extensions.purge= purge --all
49 hg.exe --config extensions.purge= purge --all
50 if ($LASTEXITCODE -ne 0) {
50 if ($LASTEXITCODE -ne 0) {
51 throw "process exited non-0: $LASTEXITCODE"
51 throw "process exited non-0: $LASTEXITCODE"
52 }
52 }
53 Write-Output "purged Mercurial repo"
53 Write-Output "purged Mercurial repo"
54 '''
54 '''
55
55
56 HG_UPDATE_CLEAN = r'''
56 HG_UPDATE_CLEAN = r'''
57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
57 $Env:PATH = "C:\hgdev\venv-bootstrap\Scripts;$Env:PATH"
58 Set-Location C:\hgdev\src
58 Set-Location C:\hgdev\src
59 hg.exe --config extensions.purge= purge --all
59 hg.exe --config extensions.purge= purge --all
60 if ($LASTEXITCODE -ne 0) {{
60 if ($LASTEXITCODE -ne 0) {{
61 throw "process exited non-0: $LASTEXITCODE"
61 throw "process exited non-0: $LASTEXITCODE"
62 }}
62 }}
63 hg.exe update -C {revision}
63 hg.exe update -C {revision}
64 if ($LASTEXITCODE -ne 0) {{
64 if ($LASTEXITCODE -ne 0) {{
65 throw "process exited non-0: $LASTEXITCODE"
65 throw "process exited non-0: $LASTEXITCODE"
66 }}
66 }}
67 hg.exe log -r .
67 hg.exe log -r .
68 Write-Output "updated Mercurial working directory to {revision}"
68 Write-Output "updated Mercurial working directory to {revision}"
69 '''.lstrip()
69 '''.lstrip()
70
70
71 BUILD_INNO = r'''
71 BUILD_INNO = r'''
72 Set-Location C:\hgdev\src
72 Set-Location C:\hgdev\src
73 $python = "C:\hgdev\python27-{arch}\python.exe"
73 $python = "C:\hgdev\python27-{arch}\python.exe"
74 C:\hgdev\python37-x64\python.exe contrib\packaging\inno\build.py --python $python
74 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py inno --python $python
75 if ($LASTEXITCODE -ne 0) {{
75 if ($LASTEXITCODE -ne 0) {{
76 throw "process exited non-0: $LASTEXITCODE"
76 throw "process exited non-0: $LASTEXITCODE"
77 }}
77 }}
78 '''.lstrip()
78 '''.lstrip()
79
79
80 BUILD_WHEEL = r'''
80 BUILD_WHEEL = r'''
81 Set-Location C:\hgdev\src
81 Set-Location C:\hgdev\src
82 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
82 C:\hgdev\python27-{arch}\Scripts\pip.exe wheel --wheel-dir dist .
83 if ($LASTEXITCODE -ne 0) {{
83 if ($LASTEXITCODE -ne 0) {{
84 throw "process exited non-0: $LASTEXITCODE"
84 throw "process exited non-0: $LASTEXITCODE"
85 }}
85 }}
86 '''
86 '''
87
87
88 BUILD_WIX = r'''
88 BUILD_WIX = r'''
89 Set-Location C:\hgdev\src
89 Set-Location C:\hgdev\src
90 $python = "C:\hgdev\python27-{arch}\python.exe"
90 $python = "C:\hgdev\python27-{arch}\python.exe"
91 C:\hgdev\python37-x64\python.exe contrib\packaging\wix\build.py --python $python {extra_args}
91 C:\hgdev\python37-x64\python.exe contrib\packaging\packaging.py wix --python $python {extra_args}
92 if ($LASTEXITCODE -ne 0) {{
92 if ($LASTEXITCODE -ne 0) {{
93 throw "process exited non-0: $LASTEXITCODE"
93 throw "process exited non-0: $LASTEXITCODE"
94 }}
94 }}
95 '''
95 '''
96
96
97 RUN_TESTS = r'''
97 RUN_TESTS = r'''
98 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
98 C:\hgdev\MinGW\msys\1.0\bin\sh.exe --login -c "cd /c/hgdev/src/tests && /c/hgdev/{python_path}/python.exe run-tests.py {test_flags}"
99 if ($LASTEXITCODE -ne 0) {{
99 if ($LASTEXITCODE -ne 0) {{
100 throw "process exited non-0: $LASTEXITCODE"
100 throw "process exited non-0: $LASTEXITCODE"
101 }}
101 }}
102 '''
102 '''
103
103
104 X86_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win32.whl'
104 X86_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win32.whl'
105 X64_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
105 X64_WHEEL_FILENAME = 'mercurial-{version}-cp27-cp27m-win_amd64.whl'
106 X86_EXE_FILENAME = 'Mercurial-{version}.exe'
106 X86_EXE_FILENAME = 'Mercurial-{version}.exe'
107 X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
107 X64_EXE_FILENAME = 'Mercurial-{version}-x64.exe'
108 X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
108 X86_MSI_FILENAME = 'mercurial-{version}-x86.msi'
109 X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
109 X64_MSI_FILENAME = 'mercurial-{version}-x64.msi'
110
110
111 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
111 MERCURIAL_SCM_BASE_URL = 'https://mercurial-scm.org/release/windows'
112
112
113 X86_USER_AGENT_PATTERN = '.*Windows.*'
113 X86_USER_AGENT_PATTERN = '.*Windows.*'
114 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
114 X64_USER_AGENT_PATTERN = '.*Windows.*(WOW|x)64.*'
115
115
116 X86_EXE_DESCRIPTION = (
116 X86_EXE_DESCRIPTION = (
117 'Mercurial {version} Inno Setup installer - x86 Windows '
117 'Mercurial {version} Inno Setup installer - x86 Windows '
118 '- does not require admin rights'
118 '- does not require admin rights'
119 )
119 )
120 X64_EXE_DESCRIPTION = (
120 X64_EXE_DESCRIPTION = (
121 'Mercurial {version} Inno Setup installer - x64 Windows '
121 'Mercurial {version} Inno Setup installer - x64 Windows '
122 '- does not require admin rights'
122 '- does not require admin rights'
123 )
123 )
124 X86_MSI_DESCRIPTION = (
124 X86_MSI_DESCRIPTION = (
125 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
125 'Mercurial {version} MSI installer - x86 Windows ' '- requires admin rights'
126 )
126 )
127 X64_MSI_DESCRIPTION = (
127 X64_MSI_DESCRIPTION = (
128 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
128 'Mercurial {version} MSI installer - x64 Windows ' '- requires admin rights'
129 )
129 )
130
130
131
131
132 def get_vc_prefix(arch):
132 def get_vc_prefix(arch):
133 if arch == 'x86':
133 if arch == 'x86':
134 return ACTIVATE_VC9_X86
134 return ACTIVATE_VC9_X86
135 elif arch == 'x64':
135 elif arch == 'x64':
136 return ACTIVATE_VC9_AMD64
136 return ACTIVATE_VC9_AMD64
137 else:
137 else:
138 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
138 raise ValueError('illegal arch: %s; must be x86 or x64' % arch)
139
139
140
140
141 def fix_authorized_keys_permissions(winrm_client, path):
141 def fix_authorized_keys_permissions(winrm_client, path):
142 commands = [
142 commands = [
143 '$ErrorActionPreference = "Stop"',
143 '$ErrorActionPreference = "Stop"',
144 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
144 'Repair-AuthorizedKeyPermission -FilePath %s -Confirm:$false' % path,
145 r'icacls %s /remove:g "NT Service\sshd"' % path,
145 r'icacls %s /remove:g "NT Service\sshd"' % path,
146 ]
146 ]
147
147
148 run_powershell(winrm_client, '\n'.join(commands))
148 run_powershell(winrm_client, '\n'.join(commands))
149
149
150
150
151 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
151 def synchronize_hg(hg_repo: pathlib.Path, revision: str, ec2_instance):
152 """Synchronize local Mercurial repo to remote EC2 instance."""
152 """Synchronize local Mercurial repo to remote EC2 instance."""
153
153
154 winrm_client = ec2_instance.winrm_client
154 winrm_client = ec2_instance.winrm_client
155
155
156 with tempfile.TemporaryDirectory() as temp_dir:
156 with tempfile.TemporaryDirectory() as temp_dir:
157 temp_dir = pathlib.Path(temp_dir)
157 temp_dir = pathlib.Path(temp_dir)
158
158
159 ssh_dir = temp_dir / '.ssh'
159 ssh_dir = temp_dir / '.ssh'
160 ssh_dir.mkdir()
160 ssh_dir.mkdir()
161 ssh_dir.chmod(0o0700)
161 ssh_dir.chmod(0o0700)
162
162
163 # Generate SSH key to use for communication.
163 # Generate SSH key to use for communication.
164 subprocess.run(
164 subprocess.run(
165 [
165 [
166 'ssh-keygen',
166 'ssh-keygen',
167 '-t',
167 '-t',
168 'rsa',
168 'rsa',
169 '-b',
169 '-b',
170 '4096',
170 '4096',
171 '-N',
171 '-N',
172 '',
172 '',
173 '-f',
173 '-f',
174 str(ssh_dir / 'id_rsa'),
174 str(ssh_dir / 'id_rsa'),
175 ],
175 ],
176 check=True,
176 check=True,
177 capture_output=True,
177 capture_output=True,
178 )
178 )
179
179
180 # Add it to ~/.ssh/authorized_keys on remote.
180 # Add it to ~/.ssh/authorized_keys on remote.
181 # This assumes the file doesn't already exist.
181 # This assumes the file doesn't already exist.
182 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
182 authorized_keys = r'c:\Users\Administrator\.ssh\authorized_keys'
183 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
183 winrm_client.execute_cmd(r'mkdir c:\Users\Administrator\.ssh')
184 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
184 winrm_client.copy(str(ssh_dir / 'id_rsa.pub'), authorized_keys)
185 fix_authorized_keys_permissions(winrm_client, authorized_keys)
185 fix_authorized_keys_permissions(winrm_client, authorized_keys)
186
186
187 public_ip = ec2_instance.public_ip_address
187 public_ip = ec2_instance.public_ip_address
188
188
189 ssh_config = temp_dir / '.ssh' / 'config'
189 ssh_config = temp_dir / '.ssh' / 'config'
190
190
191 with open(ssh_config, 'w', encoding='utf-8') as fh:
191 with open(ssh_config, 'w', encoding='utf-8') as fh:
192 fh.write('Host %s\n' % public_ip)
192 fh.write('Host %s\n' % public_ip)
193 fh.write(' User Administrator\n')
193 fh.write(' User Administrator\n')
194 fh.write(' StrictHostKeyChecking no\n')
194 fh.write(' StrictHostKeyChecking no\n')
195 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
195 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
196 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
196 fh.write(' IdentityFile %s\n' % (ssh_dir / 'id_rsa'))
197
197
198 if not (hg_repo / '.hg').is_dir():
198 if not (hg_repo / '.hg').is_dir():
199 raise Exception(
199 raise Exception(
200 '%s is not a Mercurial repository; '
200 '%s is not a Mercurial repository; '
201 'synchronization not yet supported' % hg_repo
201 'synchronization not yet supported' % hg_repo
202 )
202 )
203
203
204 env = dict(os.environ)
204 env = dict(os.environ)
205 env['HGPLAIN'] = '1'
205 env['HGPLAIN'] = '1'
206 env['HGENCODING'] = 'utf-8'
206 env['HGENCODING'] = 'utf-8'
207
207
208 hg_bin = hg_repo / 'hg'
208 hg_bin = hg_repo / 'hg'
209
209
210 res = subprocess.run(
210 res = subprocess.run(
211 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
211 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
212 cwd=str(hg_repo),
212 cwd=str(hg_repo),
213 env=env,
213 env=env,
214 check=True,
214 check=True,
215 capture_output=True,
215 capture_output=True,
216 )
216 )
217
217
218 full_revision = res.stdout.decode('ascii')
218 full_revision = res.stdout.decode('ascii')
219
219
220 args = [
220 args = [
221 'python2.7',
221 'python2.7',
222 hg_bin,
222 hg_bin,
223 '--config',
223 '--config',
224 'ui.ssh=ssh -F %s' % ssh_config,
224 'ui.ssh=ssh -F %s' % ssh_config,
225 '--config',
225 '--config',
226 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
226 'ui.remotecmd=c:/hgdev/venv-bootstrap/Scripts/hg.exe',
227 # Also ensure .hgtags changes are present so auto version
227 # Also ensure .hgtags changes are present so auto version
228 # calculation works.
228 # calculation works.
229 'push',
229 'push',
230 '-f',
230 '-f',
231 '-r',
231 '-r',
232 full_revision,
232 full_revision,
233 '-r',
233 '-r',
234 'file(.hgtags)',
234 'file(.hgtags)',
235 'ssh://%s/c:/hgdev/src' % public_ip,
235 'ssh://%s/c:/hgdev/src' % public_ip,
236 ]
236 ]
237
237
238 res = subprocess.run(args, cwd=str(hg_repo), env=env)
238 res = subprocess.run(args, cwd=str(hg_repo), env=env)
239
239
240 # Allow 1 (no-op) to not trigger error.
240 # Allow 1 (no-op) to not trigger error.
241 if res.returncode not in (0, 1):
241 if res.returncode not in (0, 1):
242 res.check_returncode()
242 res.check_returncode()
243
243
244 run_powershell(
244 run_powershell(
245 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
245 winrm_client, HG_UPDATE_CLEAN.format(revision=full_revision)
246 )
246 )
247
247
248 # TODO detect dirty local working directory and synchronize accordingly.
248 # TODO detect dirty local working directory and synchronize accordingly.
249
249
250
250
251 def purge_hg(winrm_client):
251 def purge_hg(winrm_client):
252 """Purge the Mercurial source repository on an EC2 instance."""
252 """Purge the Mercurial source repository on an EC2 instance."""
253 run_powershell(winrm_client, HG_PURGE)
253 run_powershell(winrm_client, HG_PURGE)
254
254
255
255
256 def find_latest_dist(winrm_client, pattern):
256 def find_latest_dist(winrm_client, pattern):
257 """Find path to newest file in dist/ directory matching a pattern."""
257 """Find path to newest file in dist/ directory matching a pattern."""
258
258
259 res = winrm_client.execute_ps(
259 res = winrm_client.execute_ps(
260 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
260 r'$v = Get-ChildItem -Path C:\hgdev\src\dist -Filter "%s" '
261 '| Sort-Object LastWriteTime -Descending '
261 '| Sort-Object LastWriteTime -Descending '
262 '| Select-Object -First 1\n'
262 '| Select-Object -First 1\n'
263 '$v.name' % pattern
263 '$v.name' % pattern
264 )
264 )
265 return res[0]
265 return res[0]
266
266
267
267
268 def copy_latest_dist(winrm_client, pattern, dest_path):
268 def copy_latest_dist(winrm_client, pattern, dest_path):
269 """Copy latest file matching pattern in dist/ directory.
269 """Copy latest file matching pattern in dist/ directory.
270
270
271 Given a WinRM client and a file pattern, find the latest file on the remote
271 Given a WinRM client and a file pattern, find the latest file on the remote
272 matching that pattern and copy it to the ``dest_path`` directory on the
272 matching that pattern and copy it to the ``dest_path`` directory on the
273 local machine.
273 local machine.
274 """
274 """
275 latest = find_latest_dist(winrm_client, pattern)
275 latest = find_latest_dist(winrm_client, pattern)
276 source = r'C:\hgdev\src\dist\%s' % latest
276 source = r'C:\hgdev\src\dist\%s' % latest
277 dest = dest_path / latest
277 dest = dest_path / latest
278 print('copying %s to %s' % (source, dest))
278 print('copying %s to %s' % (source, dest))
279 winrm_client.fetch(source, str(dest))
279 winrm_client.fetch(source, str(dest))
280
280
281
281
282 def build_inno_installer(
282 def build_inno_installer(
283 winrm_client, arch: str, dest_path: pathlib.Path, version=None
283 winrm_client, arch: str, dest_path: pathlib.Path, version=None
284 ):
284 ):
285 """Build the Inno Setup installer on a remote machine.
285 """Build the Inno Setup installer on a remote machine.
286
286
287 Using a WinRM client, remote commands are executed to build
287 Using a WinRM client, remote commands are executed to build
288 a Mercurial Inno Setup installer.
288 a Mercurial Inno Setup installer.
289 """
289 """
290 print('building Inno Setup installer for %s' % arch)
290 print('building Inno Setup installer for %s' % arch)
291
291
292 extra_args = []
292 extra_args = []
293 if version:
293 if version:
294 extra_args.extend(['--version', version])
294 extra_args.extend(['--version', version])
295
295
296 ps = get_vc_prefix(arch) + BUILD_INNO.format(
296 ps = get_vc_prefix(arch) + BUILD_INNO.format(
297 arch=arch, extra_args=' '.join(extra_args)
297 arch=arch, extra_args=' '.join(extra_args)
298 )
298 )
299 run_powershell(winrm_client, ps)
299 run_powershell(winrm_client, ps)
300 copy_latest_dist(winrm_client, '*.exe', dest_path)
300 copy_latest_dist(winrm_client, '*.exe', dest_path)
301
301
302
302
303 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
303 def build_wheel(winrm_client, arch: str, dest_path: pathlib.Path):
304 """Build Python wheels on a remote machine.
304 """Build Python wheels on a remote machine.
305
305
306 Using a WinRM client, remote commands are executed to build a Python wheel
306 Using a WinRM client, remote commands are executed to build a Python wheel
307 for Mercurial.
307 for Mercurial.
308 """
308 """
309 print('Building Windows wheel for %s' % arch)
309 print('Building Windows wheel for %s' % arch)
310 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
310 ps = get_vc_prefix(arch) + BUILD_WHEEL.format(arch=arch)
311 run_powershell(winrm_client, ps)
311 run_powershell(winrm_client, ps)
312 copy_latest_dist(winrm_client, '*.whl', dest_path)
312 copy_latest_dist(winrm_client, '*.whl', dest_path)
313
313
314
314
315 def build_wix_installer(
315 def build_wix_installer(
316 winrm_client, arch: str, dest_path: pathlib.Path, version=None
316 winrm_client, arch: str, dest_path: pathlib.Path, version=None
317 ):
317 ):
318 """Build the WiX installer on a remote machine.
318 """Build the WiX installer on a remote machine.
319
319
320 Using a WinRM client, remote commands are executed to build a WiX installer.
320 Using a WinRM client, remote commands are executed to build a WiX installer.
321 """
321 """
322 print('Building WiX installer for %s' % arch)
322 print('Building WiX installer for %s' % arch)
323 extra_args = []
323 extra_args = []
324 if version:
324 if version:
325 extra_args.extend(['--version', version])
325 extra_args.extend(['--version', version])
326
326
327 ps = get_vc_prefix(arch) + BUILD_WIX.format(
327 ps = get_vc_prefix(arch) + BUILD_WIX.format(
328 arch=arch, extra_args=' '.join(extra_args)
328 arch=arch, extra_args=' '.join(extra_args)
329 )
329 )
330 run_powershell(winrm_client, ps)
330 run_powershell(winrm_client, ps)
331 copy_latest_dist(winrm_client, '*.msi', dest_path)
331 copy_latest_dist(winrm_client, '*.msi', dest_path)
332
332
333
333
334 def run_tests(winrm_client, python_version, arch, test_flags=''):
334 def run_tests(winrm_client, python_version, arch, test_flags=''):
335 """Run tests on a remote Windows machine.
335 """Run tests on a remote Windows machine.
336
336
337 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
337 ``python_version`` is a ``X.Y`` string like ``2.7`` or ``3.7``.
338 ``arch`` is ``x86`` or ``x64``.
338 ``arch`` is ``x86`` or ``x64``.
339 ``test_flags`` is a str representing extra arguments to pass to
339 ``test_flags`` is a str representing extra arguments to pass to
340 ``run-tests.py``.
340 ``run-tests.py``.
341 """
341 """
342 if not re.match(r'\d\.\d', python_version):
342 if not re.match(r'\d\.\d', python_version):
343 raise ValueError(
343 raise ValueError(
344 r'python_version must be \d.\d; got %s' % python_version
344 r'python_version must be \d.\d; got %s' % python_version
345 )
345 )
346
346
347 if arch not in ('x86', 'x64'):
347 if arch not in ('x86', 'x64'):
348 raise ValueError('arch must be x86 or x64; got %s' % arch)
348 raise ValueError('arch must be x86 or x64; got %s' % arch)
349
349
350 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
350 python_path = 'python%s-%s' % (python_version.replace('.', ''), arch)
351
351
352 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
352 ps = RUN_TESTS.format(python_path=python_path, test_flags=test_flags or '',)
353
353
354 run_powershell(winrm_client, ps)
354 run_powershell(winrm_client, ps)
355
355
356
356
357 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
357 def resolve_wheel_artifacts(dist_path: pathlib.Path, version: str):
358 return (
358 return (
359 dist_path / X86_WHEEL_FILENAME.format(version=version),
359 dist_path / X86_WHEEL_FILENAME.format(version=version),
360 dist_path / X64_WHEEL_FILENAME.format(version=version),
360 dist_path / X64_WHEEL_FILENAME.format(version=version),
361 )
361 )
362
362
363
363
364 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
364 def resolve_all_artifacts(dist_path: pathlib.Path, version: str):
365 return (
365 return (
366 dist_path / X86_WHEEL_FILENAME.format(version=version),
366 dist_path / X86_WHEEL_FILENAME.format(version=version),
367 dist_path / X64_WHEEL_FILENAME.format(version=version),
367 dist_path / X64_WHEEL_FILENAME.format(version=version),
368 dist_path / X86_EXE_FILENAME.format(version=version),
368 dist_path / X86_EXE_FILENAME.format(version=version),
369 dist_path / X64_EXE_FILENAME.format(version=version),
369 dist_path / X64_EXE_FILENAME.format(version=version),
370 dist_path / X86_MSI_FILENAME.format(version=version),
370 dist_path / X86_MSI_FILENAME.format(version=version),
371 dist_path / X64_MSI_FILENAME.format(version=version),
371 dist_path / X64_MSI_FILENAME.format(version=version),
372 )
372 )
373
373
374
374
375 def generate_latest_dat(version: str):
375 def generate_latest_dat(version: str):
376 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
376 x86_exe_filename = X86_EXE_FILENAME.format(version=version)
377 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
377 x64_exe_filename = X64_EXE_FILENAME.format(version=version)
378 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
378 x86_msi_filename = X86_MSI_FILENAME.format(version=version)
379 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
379 x64_msi_filename = X64_MSI_FILENAME.format(version=version)
380
380
381 entries = (
381 entries = (
382 (
382 (
383 '10',
383 '10',
384 version,
384 version,
385 X86_USER_AGENT_PATTERN,
385 X86_USER_AGENT_PATTERN,
386 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
386 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_exe_filename),
387 X86_EXE_DESCRIPTION.format(version=version),
387 X86_EXE_DESCRIPTION.format(version=version),
388 ),
388 ),
389 (
389 (
390 '10',
390 '10',
391 version,
391 version,
392 X64_USER_AGENT_PATTERN,
392 X64_USER_AGENT_PATTERN,
393 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
393 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_exe_filename),
394 X64_EXE_DESCRIPTION.format(version=version),
394 X64_EXE_DESCRIPTION.format(version=version),
395 ),
395 ),
396 (
396 (
397 '10',
397 '10',
398 version,
398 version,
399 X86_USER_AGENT_PATTERN,
399 X86_USER_AGENT_PATTERN,
400 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
400 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x86_msi_filename),
401 X86_MSI_DESCRIPTION.format(version=version),
401 X86_MSI_DESCRIPTION.format(version=version),
402 ),
402 ),
403 (
403 (
404 '10',
404 '10',
405 version,
405 version,
406 X64_USER_AGENT_PATTERN,
406 X64_USER_AGENT_PATTERN,
407 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
407 '%s/%s' % (MERCURIAL_SCM_BASE_URL, x64_msi_filename),
408 X64_MSI_DESCRIPTION.format(version=version),
408 X64_MSI_DESCRIPTION.format(version=version),
409 ),
409 ),
410 )
410 )
411
411
412 lines = ['\t'.join(e) for e in entries]
412 lines = ['\t'.join(e) for e in entries]
413
413
414 return '\n'.join(lines) + '\n'
414 return '\n'.join(lines) + '\n'
415
415
416
416
417 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
417 def publish_artifacts_pypi(dist_path: pathlib.Path, version: str):
418 """Publish Windows release artifacts to PyPI."""
418 """Publish Windows release artifacts to PyPI."""
419
419
420 wheel_paths = resolve_wheel_artifacts(dist_path, version)
420 wheel_paths = resolve_wheel_artifacts(dist_path, version)
421
421
422 for p in wheel_paths:
422 for p in wheel_paths:
423 if not p.exists():
423 if not p.exists():
424 raise Exception('%s not found' % p)
424 raise Exception('%s not found' % p)
425
425
426 print('uploading wheels to PyPI (you may be prompted for credentials)')
426 print('uploading wheels to PyPI (you may be prompted for credentials)')
427 pypi_upload(wheel_paths)
427 pypi_upload(wheel_paths)
428
428
429
429
430 def publish_artifacts_mercurial_scm_org(
430 def publish_artifacts_mercurial_scm_org(
431 dist_path: pathlib.Path, version: str, ssh_username=None
431 dist_path: pathlib.Path, version: str, ssh_username=None
432 ):
432 ):
433 """Publish Windows release artifacts to mercurial-scm.org."""
433 """Publish Windows release artifacts to mercurial-scm.org."""
434 all_paths = resolve_all_artifacts(dist_path, version)
434 all_paths = resolve_all_artifacts(dist_path, version)
435
435
436 for p in all_paths:
436 for p in all_paths:
437 if not p.exists():
437 if not p.exists():
438 raise Exception('%s not found' % p)
438 raise Exception('%s not found' % p)
439
439
440 client = paramiko.SSHClient()
440 client = paramiko.SSHClient()
441 client.load_system_host_keys()
441 client.load_system_host_keys()
442 # We assume the system SSH configuration knows how to connect.
442 # We assume the system SSH configuration knows how to connect.
443 print('connecting to mercurial-scm.org via ssh...')
443 print('connecting to mercurial-scm.org via ssh...')
444 try:
444 try:
445 client.connect('mercurial-scm.org', username=ssh_username)
445 client.connect('mercurial-scm.org', username=ssh_username)
446 except paramiko.AuthenticationException:
446 except paramiko.AuthenticationException:
447 print('error authenticating; is an SSH key available in an SSH agent?')
447 print('error authenticating; is an SSH key available in an SSH agent?')
448 raise
448 raise
449
449
450 print('SSH connection established')
450 print('SSH connection established')
451
451
452 print('opening SFTP client...')
452 print('opening SFTP client...')
453 sftp = client.open_sftp()
453 sftp = client.open_sftp()
454 print('SFTP client obtained')
454 print('SFTP client obtained')
455
455
456 for p in all_paths:
456 for p in all_paths:
457 dest_path = '/var/www/release/windows/%s' % p.name
457 dest_path = '/var/www/release/windows/%s' % p.name
458 print('uploading %s to %s' % (p, dest_path))
458 print('uploading %s to %s' % (p, dest_path))
459
459
460 with p.open('rb') as fh:
460 with p.open('rb') as fh:
461 data = fh.read()
461 data = fh.read()
462
462
463 with sftp.open(dest_path, 'wb') as fh:
463 with sftp.open(dest_path, 'wb') as fh:
464 fh.write(data)
464 fh.write(data)
465 fh.chmod(0o0664)
465 fh.chmod(0o0664)
466
466
467 latest_dat_path = '/var/www/release/windows/latest.dat'
467 latest_dat_path = '/var/www/release/windows/latest.dat'
468
468
469 now = datetime.datetime.utcnow()
469 now = datetime.datetime.utcnow()
470 backup_path = dist_path / (
470 backup_path = dist_path / (
471 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
471 'latest-windows-%s.dat' % now.strftime('%Y%m%dT%H%M%S')
472 )
472 )
473 print('backing up %s to %s' % (latest_dat_path, backup_path))
473 print('backing up %s to %s' % (latest_dat_path, backup_path))
474
474
475 with sftp.open(latest_dat_path, 'rb') as fh:
475 with sftp.open(latest_dat_path, 'rb') as fh:
476 latest_dat_old = fh.read()
476 latest_dat_old = fh.read()
477
477
478 with backup_path.open('wb') as fh:
478 with backup_path.open('wb') as fh:
479 fh.write(latest_dat_old)
479 fh.write(latest_dat_old)
480
480
481 print('writing %s with content:' % latest_dat_path)
481 print('writing %s with content:' % latest_dat_path)
482 latest_dat_content = generate_latest_dat(version)
482 latest_dat_content = generate_latest_dat(version)
483 print(latest_dat_content)
483 print(latest_dat_content)
484
484
485 with sftp.open(latest_dat_path, 'wb') as fh:
485 with sftp.open(latest_dat_path, 'wb') as fh:
486 fh.write(latest_dat_content.encode('ascii'))
486 fh.write(latest_dat_content.encode('ascii'))
487
487
488
488
489 def publish_artifacts(
489 def publish_artifacts(
490 dist_path: pathlib.Path,
490 dist_path: pathlib.Path,
491 version: str,
491 version: str,
492 pypi=True,
492 pypi=True,
493 mercurial_scm_org=True,
493 mercurial_scm_org=True,
494 ssh_username=None,
494 ssh_username=None,
495 ):
495 ):
496 """Publish Windows release artifacts.
496 """Publish Windows release artifacts.
497
497
498 Files are found in `dist_path`. We will look for files with version string
498 Files are found in `dist_path`. We will look for files with version string
499 `version`.
499 `version`.
500
500
501 `pypi` controls whether we upload to PyPI.
501 `pypi` controls whether we upload to PyPI.
502 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
502 `mercurial_scm_org` controls whether we upload to mercurial-scm.org.
503 """
503 """
504 if pypi:
504 if pypi:
505 publish_artifacts_pypi(dist_path, version)
505 publish_artifacts_pypi(dist_path, version)
506
506
507 if mercurial_scm_org:
507 if mercurial_scm_org:
508 publish_artifacts_mercurial_scm_org(
508 publish_artifacts_mercurial_scm_org(
509 dist_path, version, ssh_username=ssh_username
509 dist_path, version, ssh_username=ssh_username
510 )
510 )
@@ -1,61 +1,61 b''
1 Requirements
1 Requirements
2 ============
2 ============
3
3
4 Building the Inno installer requires a Windows machine.
4 Building the Inno installer requires a Windows machine.
5
5
6 The following system dependencies must be installed:
6 The following system dependencies must be installed:
7
7
8 * Python 2.7 (download from https://www.python.org/downloads/)
8 * Python 2.7 (download from https://www.python.org/downloads/)
9 * Microsoft Visual C++ Compiler for Python 2.7
9 * Microsoft Visual C++ Compiler for Python 2.7
10 (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
10 (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
11 * Inno Setup (http://jrsoftware.org/isdl.php) version 5.4 or newer.
11 * Inno Setup (http://jrsoftware.org/isdl.php) version 5.4 or newer.
12 Be sure to install the optional Inno Setup Preprocessor feature,
12 Be sure to install the optional Inno Setup Preprocessor feature,
13 which is required.
13 which is required.
14 * Python 3.5+ (to run the ``build.py`` script)
14 * Python 3.5+ (to run the ``packaging.py`` script)
15
15
16 Building
16 Building
17 ========
17 ========
18
18
19 The ``build.py`` script automates the process of producing an
19 The ``packaging.py`` script automates the process of producing an
20 Inno installer. It manages fetching and configuring the
20 Inno installer. It manages fetching and configuring the
21 non-system dependencies (such as py2exe, gettext, and various
21 non-system dependencies (such as py2exe, gettext, and various
22 Python packages).
22 Python packages).
23
23
24 The script requires an activated ``Visual C++ 2008`` command prompt.
24 The script requires an activated ``Visual C++ 2008`` command prompt.
25 A shortcut to such a prompt was installed with ``Microsoft Visual C++
25 A shortcut to such a prompt was installed with ``Microsoft Visual C++
26 Compiler for Python 2.7``. From your Start Menu, look for
26 Compiler for Python 2.7``. From your Start Menu, look for
27 ``Microsoft Visual C++ Compiler Package for Python 2.7`` then launch
27 ``Microsoft Visual C++ Compiler Package for Python 2.7`` then launch
28 either ``Visual C++ 2008 32-bit Command Prompt`` or
28 either ``Visual C++ 2008 32-bit Command Prompt`` or
29 ``Visual C++ 2008 64-bit Command Prompt``.
29 ``Visual C++ 2008 64-bit Command Prompt``.
30
30
31 From the prompt, change to the Mercurial source directory. e.g.
31 From the prompt, change to the Mercurial source directory. e.g.
32 ``cd c:\src\hg``.
32 ``cd c:\src\hg``.
33
33
34 Next, invoke ``build.py`` to produce an Inno installer. You will
34 Next, invoke ``packaging.py`` to produce an Inno installer. You will
35 need to supply the path to the Python interpreter to use.::
35 need to supply the path to the Python interpreter to use.::
36
36
37 $ python3.exe contrib\packaging\inno\build.py \
37 $ python3.exe contrib\packaging\packaging.py \
38 --python c:\python27\python.exe
38 inno --python c:\python27\python.exe
39
39
40 .. note::
40 .. note::
41
41
42 The script validates that the Visual C++ environment is
42 The script validates that the Visual C++ environment is
43 active and that the architecture of the specified Python
43 active and that the architecture of the specified Python
44 interpreter matches the Visual C++ environment and errors
44 interpreter matches the Visual C++ environment and errors
45 if not.
45 if not.
46
46
47 If everything runs as intended, dependencies will be fetched and
47 If everything runs as intended, dependencies will be fetched and
48 configured into the ``build`` sub-directory, Mercurial will be built,
48 configured into the ``build`` sub-directory, Mercurial will be built,
49 and an installer placed in the ``dist`` sub-directory. The final
49 and an installer placed in the ``dist`` sub-directory. The final
50 line of output should print the name of the generated installer.
50 line of output should print the name of the generated installer.
51
51
52 Additional options may be configured. Run ``build.py --help`` to
52 Additional options may be configured. Run
53 see a list of program flags.
53 ``packaging.py inno --help`` to see a list of program flags.
54
54
55 MinGW
55 MinGW
56 =====
56 =====
57
57
58 It is theoretically possible to generate an installer that uses
58 It is theoretically possible to generate an installer that uses
59 MinGW. This isn't well tested and ``build.py`` and may properly
59 MinGW. This isn't well tested and ``packaging.py`` and may properly
60 support it. See old versions of this file in version control for
60 support it. See old versions of this file in version control for
61 potentially useful hints as to how to achieve this.
61 potentially useful hints as to how to achieve this.
@@ -1,71 +1,71 b''
1 WiX Installer
1 WiX Installer
2 =============
2 =============
3
3
4 The files in this directory are used to produce an MSI installer using
4 The files in this directory are used to produce an MSI installer using
5 the WiX Toolset (http://wixtoolset.org/).
5 the WiX Toolset (http://wixtoolset.org/).
6
6
7 The MSI installers require elevated (admin) privileges due to the
7 The MSI installers require elevated (admin) privileges due to the
8 installation of MSVC CRT libraries into the Windows system store. See
8 installation of MSVC CRT libraries into the Windows system store. See
9 the Inno Setup installers in the ``inno`` sibling directory for installers
9 the Inno Setup installers in the ``inno`` sibling directory for installers
10 that do not have this requirement.
10 that do not have this requirement.
11
11
12 Requirements
12 Requirements
13 ============
13 ============
14
14
15 Building the WiX installers requires a Windows machine. The following
15 Building the WiX installers requires a Windows machine. The following
16 dependencies must be installed:
16 dependencies must be installed:
17
17
18 * Python 2.7 (download from https://www.python.org/downloads/)
18 * Python 2.7 (download from https://www.python.org/downloads/)
19 * Microsoft Visual C++ Compiler for Python 2.7
19 * Microsoft Visual C++ Compiler for Python 2.7
20 (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
20 (https://www.microsoft.com/en-us/download/details.aspx?id=44266)
21 * Python 3.5+ (to run the ``build.py`` script)
21 * Python 3.5+ (to run the ``packaging.py`` script)
22
22
23 Building
23 Building
24 ========
24 ========
25
25
26 The ``build.py`` script automates the process of producing an MSI
26 The ``packaging.py`` script automates the process of producing an MSI
27 installer. It manages fetching and configuring non-system dependencies
27 installer. It manages fetching and configuring non-system dependencies
28 (such as py2exe, gettext, and various Python packages).
28 (such as py2exe, gettext, and various Python packages).
29
29
30 The script requires an activated ``Visual C++ 2008`` command prompt.
30 The script requires an activated ``Visual C++ 2008`` command prompt.
31 A shortcut to such a prompt was installed with ``Microsoft Visual
31 A shortcut to such a prompt was installed with ``Microsoft Visual
32 C++ Compiler for Python 2.7``. From your Start Menu, look for
32 C++ Compiler for Python 2.7``. From your Start Menu, look for
33 ``Microsoft Visual C++ Compiler Package for Python 2.7`` then
33 ``Microsoft Visual C++ Compiler Package for Python 2.7`` then
34 launch either ``Visual C++ 2008 32-bit Command Prompt`` or
34 launch either ``Visual C++ 2008 32-bit Command Prompt`` or
35 ``Visual C++ 2008 64-bit Command Prompt``.
35 ``Visual C++ 2008 64-bit Command Prompt``.
36
36
37 From the prompt, change to the Mercurial source directory. e.g.
37 From the prompt, change to the Mercurial source directory. e.g.
38 ``cd c:\src\hg``.
38 ``cd c:\src\hg``.
39
39
40 Next, invoke ``build.py`` to produce an MSI installer. You will need
40 Next, invoke ``packaging.py`` to produce an MSI installer. You will need
41 to supply the path to the Python interpreter to use.::
41 to supply the path to the Python interpreter to use.::
42
42
43 $ python3 contrib\packaging\wix\build.py \
43 $ python3 contrib\packaging\packaging.py \
44 --python c:\python27\python.exe
44 wix --python c:\python27\python.exe
45
45
46 .. note::
46 .. note::
47
47
48 The script validates that the Visual C++ environment is active and
48 The script validates that the Visual C++ environment is active and
49 that the architecture of the specified Python interpreter matches the
49 that the architecture of the specified Python interpreter matches the
50 Visual C++ environment. An error is raised otherwise.
50 Visual C++ environment. An error is raised otherwise.
51
51
52 If everything runs as intended, dependencies will be fetched and
52 If everything runs as intended, dependencies will be fetched and
53 configured into the ``build`` sub-directory, Mercurial will be built,
53 configured into the ``build`` sub-directory, Mercurial will be built,
54 and an installer placed in the ``dist`` sub-directory. The final line
54 and an installer placed in the ``dist`` sub-directory. The final line
55 of output should print the name of the generated installer.
55 of output should print the name of the generated installer.
56
56
57 Additional options may be configured. Run ``build.py --help`` to see
57 Additional options may be configured. Run ``packaging.py wix --help`` to
58 a list of program flags.
58 see a list of program flags.
59
59
60 Relationship to TortoiseHG
60 Relationship to TortoiseHG
61 ==========================
61 ==========================
62
62
63 TortoiseHG uses the WiX files in this directory.
63 TortoiseHG uses the WiX files in this directory.
64
64
65 The code for building TortoiseHG installers lives at
65 The code for building TortoiseHG installers lives at
66 https://bitbucket.org/tortoisehg/thg-winbuild and is maintained by
66 https://bitbucket.org/tortoisehg/thg-winbuild and is maintained by
67 Steve Borho (steve@borho.org).
67 Steve Borho (steve@borho.org).
68
68
69 When changing behavior of the WiX installer, be sure to notify
69 When changing behavior of the WiX installer, be sure to notify
70 the TortoiseHG Project of the changes so they have ample time
70 the TortoiseHG Project of the changes so they have ample time
71 provide feedback and react to those changes.
71 provide feedback and react to those changes.
@@ -1,86 +1,85 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4 $ check_code="$TESTDIR"/../contrib/check-code.py
4 $ check_code="$TESTDIR"/../contrib/check-code.py
5 $ cd "$TESTDIR"/..
5 $ cd "$TESTDIR"/..
6
6
7 New errors are not allowed. Warnings are strongly discouraged.
7 New errors are not allowed. Warnings are strongly discouraged.
8 (The writing "no-che?k-code" is for not skipping this file when checking.)
8 (The writing "no-che?k-code" is for not skipping this file when checking.)
9
9
10 $ testrepohg locate \
10 $ testrepohg locate \
11 > -X contrib/python-zstandard \
11 > -X contrib/python-zstandard \
12 > -X hgext/fsmonitor/pywatchman \
12 > -X hgext/fsmonitor/pywatchman \
13 > -X mercurial/thirdparty \
13 > -X mercurial/thirdparty \
14 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
14 > | sed 's-\\-/-g' | "$check_code" --warnings --per-file=0 - || false
15 Skipping contrib/automation/hgautomation/__init__.py it has no-che?k-code (glob)
15 Skipping contrib/automation/hgautomation/__init__.py it has no-che?k-code (glob)
16 Skipping contrib/automation/hgautomation/aws.py it has no-che?k-code (glob)
16 Skipping contrib/automation/hgautomation/aws.py it has no-che?k-code (glob)
17 Skipping contrib/automation/hgautomation/cli.py it has no-che?k-code (glob)
17 Skipping contrib/automation/hgautomation/cli.py it has no-che?k-code (glob)
18 Skipping contrib/automation/hgautomation/linux.py it has no-che?k-code (glob)
18 Skipping contrib/automation/hgautomation/linux.py it has no-che?k-code (glob)
19 Skipping contrib/automation/hgautomation/pypi.py it has no-che?k-code (glob)
19 Skipping contrib/automation/hgautomation/pypi.py it has no-che?k-code (glob)
20 Skipping contrib/automation/hgautomation/ssh.py it has no-che?k-code (glob)
20 Skipping contrib/automation/hgautomation/ssh.py it has no-che?k-code (glob)
21 Skipping contrib/automation/hgautomation/try_server.py it has no-che?k-code (glob)
21 Skipping contrib/automation/hgautomation/try_server.py it has no-che?k-code (glob)
22 Skipping contrib/automation/hgautomation/windows.py it has no-che?k-code (glob)
22 Skipping contrib/automation/hgautomation/windows.py it has no-che?k-code (glob)
23 Skipping contrib/automation/hgautomation/winrm.py it has no-che?k-code (glob)
23 Skipping contrib/automation/hgautomation/winrm.py it has no-che?k-code (glob)
24 Skipping contrib/packaging/hgpackaging/cli.py it has no-che?k-code (glob)
24 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
25 Skipping contrib/packaging/hgpackaging/downloads.py it has no-che?k-code (glob)
25 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
26 Skipping contrib/packaging/hgpackaging/inno.py it has no-che?k-code (glob)
26 Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
27 Skipping contrib/packaging/hgpackaging/py2exe.py it has no-che?k-code (glob)
27 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/util.py it has no-che?k-code (glob)
28 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/hgpackaging/wix.py it has no-che?k-code (glob)
29 Skipping contrib/packaging/inno/build.py it has no-che?k-code (glob)
30 Skipping contrib/packaging/wix/build.py it has no-che?k-code (glob)
31 Skipping i18n/polib.py it has no-che?k-code (glob)
30 Skipping i18n/polib.py it has no-che?k-code (glob)
32 Skipping mercurial/statprof.py it has no-che?k-code (glob)
31 Skipping mercurial/statprof.py it has no-che?k-code (glob)
33 Skipping tests/badserverext.py it has no-che?k-code (glob)
32 Skipping tests/badserverext.py it has no-che?k-code (glob)
34
33
35 @commands in debugcommands.py should be in alphabetical order.
34 @commands in debugcommands.py should be in alphabetical order.
36
35
37 >>> import re
36 >>> import re
38 >>> commands = []
37 >>> commands = []
39 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
38 >>> with open('mercurial/debugcommands.py', 'rb') as fh:
40 ... for line in fh:
39 ... for line in fh:
41 ... m = re.match(br"^@command\('([a-z]+)", line)
40 ... m = re.match(br"^@command\('([a-z]+)", line)
42 ... if m:
41 ... if m:
43 ... commands.append(m.group(1))
42 ... commands.append(m.group(1))
44 >>> scommands = list(sorted(commands))
43 >>> scommands = list(sorted(commands))
45 >>> for i, command in enumerate(scommands):
44 >>> for i, command in enumerate(scommands):
46 ... if command != commands[i]:
45 ... if command != commands[i]:
47 ... print('commands in debugcommands.py not sorted; first differing '
46 ... print('commands in debugcommands.py not sorted; first differing '
48 ... 'command is %s; expected %s' % (commands[i], command))
47 ... 'command is %s; expected %s' % (commands[i], command))
49 ... break
48 ... break
50
49
51 Prevent adding new files in the root directory accidentally.
50 Prevent adding new files in the root directory accidentally.
52
51
53 $ testrepohg files 'glob:*'
52 $ testrepohg files 'glob:*'
54 .arcconfig
53 .arcconfig
55 .clang-format
54 .clang-format
56 .editorconfig
55 .editorconfig
57 .hgignore
56 .hgignore
58 .hgsigs
57 .hgsigs
59 .hgtags
58 .hgtags
60 .jshintrc
59 .jshintrc
61 CONTRIBUTING
60 CONTRIBUTING
62 CONTRIBUTORS
61 CONTRIBUTORS
63 COPYING
62 COPYING
64 Makefile
63 Makefile
65 README.rst
64 README.rst
66 black.toml
65 black.toml
67 hg
66 hg
68 hgeditor
67 hgeditor
69 hgweb.cgi
68 hgweb.cgi
70 setup.py
69 setup.py
71
70
72 Prevent adding modules which could be shadowed by ancient .so/.dylib.
71 Prevent adding modules which could be shadowed by ancient .so/.dylib.
73
72
74 $ testrepohg files \
73 $ testrepohg files \
75 > mercurial/base85.py \
74 > mercurial/base85.py \
76 > mercurial/bdiff.py \
75 > mercurial/bdiff.py \
77 > mercurial/diffhelpers.py \
76 > mercurial/diffhelpers.py \
78 > mercurial/mpatch.py \
77 > mercurial/mpatch.py \
79 > mercurial/osutil.py \
78 > mercurial/osutil.py \
80 > mercurial/parsers.py \
79 > mercurial/parsers.py \
81 > mercurial/zstd.py
80 > mercurial/zstd.py
82 [1]
81 [1]
83
82
84 Keep python3 tests sorted:
83 Keep python3 tests sorted:
85 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
84 $ sort < contrib/python3-whitelist > $TESTTMP/py3sorted
86 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
85 $ cmp contrib/python3-whitelist $TESTTMP/py3sorted || echo 'Please sort passing tests!'
@@ -1,52 +1,53 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4 $ cd "$TESTDIR"/..
4 $ cd "$TESTDIR"/..
5
5
6 #if no-py3
6 #if no-py3
7 $ testrepohg files 'set:(**.py)' \
7 $ testrepohg files 'set:(**.py)' \
8 > -X contrib/automation/ \
8 > -X contrib/automation/ \
9 > -X contrib/packaging/hgpackaging/ \
9 > -X contrib/packaging/hgpackaging/ \
10 > -X contrib/packaging/inno/ \
10 > -X contrib/packaging/inno/ \
11 > -X contrib/packaging/packaging.py \
11 > -X contrib/packaging/wix/ \
12 > -X contrib/packaging/wix/ \
12 > -X hgdemandimport/demandimportpy2.py \
13 > -X hgdemandimport/demandimportpy2.py \
13 > -X mercurial/thirdparty/cbor \
14 > -X mercurial/thirdparty/cbor \
14 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py
15 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py
15 contrib/python-zstandard/setup.py not using absolute_import
16 contrib/python-zstandard/setup.py not using absolute_import
16 contrib/python-zstandard/setup_zstd.py not using absolute_import
17 contrib/python-zstandard/setup_zstd.py not using absolute_import
17 contrib/python-zstandard/tests/common.py not using absolute_import
18 contrib/python-zstandard/tests/common.py not using absolute_import
18 contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
19 contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
19 contrib/python-zstandard/tests/test_compressor.py not using absolute_import
20 contrib/python-zstandard/tests/test_compressor.py not using absolute_import
20 contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
21 contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
21 contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
22 contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
22 contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
23 contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
23 contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
24 contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
24 contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
25 contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
25 contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
26 contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
26 contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
27 contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
27 contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
28 contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
28 setup.py not using absolute_import
29 setup.py not using absolute_import
29 #endif
30 #endif
30
31
31 #if py3
32 #if py3
32 $ testrepohg files 'set:(**.py) - grep(pygments)' \
33 $ testrepohg files 'set:(**.py) - grep(pygments)' \
33 > -X hgdemandimport/demandimportpy2.py \
34 > -X hgdemandimport/demandimportpy2.py \
34 > -X hgext/fsmonitor/pywatchman \
35 > -X hgext/fsmonitor/pywatchman \
35 > -X mercurial/cffi \
36 > -X mercurial/cffi \
36 > -X mercurial/thirdparty \
37 > -X mercurial/thirdparty \
37 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py \
38 > | sed 's|\\|/|g' | xargs "$PYTHON" contrib/check-py3-compat.py \
38 > | sed 's/[0-9][0-9]*)$/*)/'
39 > | sed 's/[0-9][0-9]*)$/*)/'
39 hgext/convert/transport.py: error importing: <*Error> No module named 'svn.client' (error at transport.py:*) (glob) (?)
40 hgext/convert/transport.py: error importing: <*Error> No module named 'svn.client' (error at transport.py:*) (glob) (?)
40 hgext/infinitepush/sqlindexapi.py: error importing: <*Error> No module named 'mysql' (error at sqlindexapi.py:*) (glob) (?)
41 hgext/infinitepush/sqlindexapi.py: error importing: <*Error> No module named 'mysql' (error at sqlindexapi.py:*) (glob) (?)
41 mercurial/scmwindows.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
42 mercurial/scmwindows.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
42 mercurial/win32.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
43 mercurial/win32.py: error importing: <ValueError> _type_ 'v' not supported (error at win32.py:*) (no-windows !)
43 mercurial/windows.py: error importing: <ModuleNotFoundError> No module named 'msvcrt' (error at windows.py:*) (no-windows !)
44 mercurial/windows.py: error importing: <ModuleNotFoundError> No module named 'msvcrt' (error at windows.py:*) (no-windows !)
44 mercurial/posix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at posix.py:*) (windows !)
45 mercurial/posix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at posix.py:*) (windows !)
45 mercurial/scmposix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at scmposix.py:*) (windows !)
46 mercurial/scmposix.py: error importing: <ModuleNotFoundError> No module named 'fcntl' (error at scmposix.py:*) (windows !)
46 #endif
47 #endif
47
48
48 #if py3 pygments
49 #if py3 pygments
49 $ testrepohg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
50 $ testrepohg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
50 > | xargs "$PYTHON" contrib/check-py3-compat.py \
51 > | xargs "$PYTHON" contrib/check-py3-compat.py \
51 > | sed 's/[0-9][0-9]*)$/*)/'
52 > | sed 's/[0-9][0-9]*)$/*)/'
52 #endif
53 #endif
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now