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