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