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