-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.py
More file actions
346 lines (310 loc) · 11.6 KB
/
Copy pathbuild.py
File metadata and controls
346 lines (310 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import sys
import json
import shutil
import hashlib
import logging
import argparse
import platform as _platform
import subprocess
import logging.config
from pathlib import Path
import requests
ERRORS = {
'UNSUPPORTED_PLATFORM': 1,
'AMBIGUOUS_INSTALL': 2,
'HASH_MISMATCH': 3,
}
HERE = Path(__file__).parent
cli = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
cli.add_argument(
"-p",
"--python-version",
help="The version of cPython to target. (i.e. 3.14.3)",
default="3.14.3",
)
cli.add_argument(
"--hash-buff-size",
help="The buffer size for reads when calculating hashes",
default=65536,
)
cli.add_argument(
"--build-directory",
help="The directory to use for downloads and other file "
"operations, WARNING: This directory will be deleted",
default=HERE.joinpath("build"),
)
cli.add_argument(
"--dist-directory",
help="The directory to store the distributable artifacts "
"WARNING: This directory will be deleted",
default=HERE.joinpath("dist"),
)
cli.add_argument(
"--log-level",
help="The level (0-50) at which to filter log levels (lower is more verbose)",
default=30,
type=int,
)
args = cli.parse_args()
TARGET_PYTHON_VERSION = args.python_version
HASH_BUFF_SIZE = args.hash_buff_size
BUILD_DIRECTORY = args.build_directory
shutil.rmtree(BUILD_DIRECTORY, ignore_errors=True)
BUILD_DIRECTORY.mkdir()
ASSEMBLE_DIRECTORY = BUILD_DIRECTORY.joinpath('assemble')
shutil.rmtree(ASSEMBLE_DIRECTORY, ignore_errors=True)
ASSEMBLE_DIRECTORY.mkdir()
DIST_DIRECTORY = args.dist_directory
shutil.rmtree(DIST_DIRECTORY, ignore_errors=True)
DIST_DIRECTORY.mkdir()
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "%(levelname)-8s: %(asctime)s: %(message)s"
}
},
"handlers": {
"stdout": {
"class": "logging.StreamHandler",
"level": args.log_level,
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.FileHandler",
"formatter": "simple",
"level": args.log_level,
"filename": HERE.joinpath('build.log'),
"mode": "w"
}
},
"root": {
"level": args.log_level,
"handlers": [
"stdout",
"file"
]
}
}
)
log = logging.getLogger(__name__)
log.debug(f"Found TARGET_PYTHON_VERSION: {TARGET_PYTHON_VERSION}")
def fail_for_unsupported_platform(platform):
log.critical(f"Unable to continue due to unsupported platform: {platform}")
sys.exit(ERRORS['UNSUPPORTED_PLATFORM'])
def fail_for_ambiguity(release_names):
log.critical(f"Unable to determine correct Python build out of: {', '.join(release_names)}")
sys.exit(ERRORS['AMBIGUOUS_INSTALL'])
def fail_for_hash_mismatch(expected, actual):
log.critical(f"Unable to continue. Release file does not match hash. Expected: '{expected}', Actual: '{actual}'")
sys.exit(ERRORS['HASH_MISMATCH'])
def get_file_sha256(p):
sha256 = hashlib.sha256()
with p.open('rb') as fp:
while True:
data = fp.read(HASH_BUFF_SIZE)
if not data:
break
else:
sha256.update(data)
return sha256.hexdigest()
platform = sys.platform
if platform.startswith('linux'):
log.debug(f"Found Linux platform: {platform}")
platform = 'linux'
elif platform.startswith('win32'):
log.debug(f"Found Windows platform: {platform}")
platform = 'windows'
elif platform.startswith('darwin'):
log.debug(f"Found MacOS platform: {platform}")
platform = 'darwin'
# The elifs here are strictly here for documentation if we need to
# support these platforms in the future
elif platform.startswith('freebsd'):
fail_for_unsupported_platform(platform)
elif platform.startswith('aix'):
fail_for_unsupported_platform(platform)
elif platform.startswith('emscripten'):
fail_for_unsupported_platform(platform)
elif platform.startswith('wasi'):
fail_for_unsupported_platform(platform)
elif platform.startswith('cygwin'):
fail_for_unsupported_platform(platform)
else:
fail_for_unsupported_platform(platform)
requirements_file = HERE.joinpath(f'{platform}-requirements.txt')
log.debug(f"Found requirements_file: {requirements_file}")
# Download a prebuilt Python distribution
# First, get the latest release tag
response = requests.get(
'https://raw.githubusercontent.com/astral-sh/python-build-standalone/latest-release/latest-release.json',
)
response_json = response.json()
log.debug(f"Found response json: {response_json}")
tag = response_json['tag']
log.debug(f"Found tag: {tag}")
# Second, Get the release ID from Github API
response = requests.get(
f'https://api.github.com/repos/astral-sh/python-build-standalone/releases/tags/{tag}',
headers={
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/vnd.github+json'
}
)
response_json = response.json()
log.debug(f"Found response json: {response_json}")
assets = response_json['assets']
log.debug(f"Found assets: {assets}")
release_names = [d['name'] for d in assets]
log.debug(f"Found release_names: {release_names}")
# Filter to install_only build configuration
release_names = [name for name in release_names if 'install_only_stripped' in name]
log.debug(f"Found 'install_only_stripped' release_names: {release_names}")
# Filter to only the python version that we are interested in
release_names = [name for name in release_names if TARGET_PYTHON_VERSION in name]
log.debug(f"Found correct python version release_names: {release_names}")
# Filter to only platform and architecture that we are interested in
if platform == 'windows':
release_names = [name for name in release_names if 'windows-msvc' in name]
elif platform == 'linux':
release_names = [name for name in release_names if 'unknown-linux-gnu' in name]
elif platform == 'darwin':
release_names = [name for name in release_names if 'apple-darwin' in name]
log.debug(f"Found platform specific release_names: {release_names}")
# Filter to only bitness that we are interested in
if platform == "windows":
if sys.maxsize > 2**32:
release_names = [name for name in release_names if 'x86_64' in name]
else:
release_names = [name for name in release_names if 'i686' in name]
elif platform == "linux":
if sys.maxsize > 2**32:
release_names = [name for name in release_names if 'x86_64' in name]
release_names = [name for name in release_names if 'x86_64_v2' not in name]
release_names = [name for name in release_names if 'x86_64_v3' not in name]
release_names = [name for name in release_names if 'x86_64_v4' not in name]
else:
release_names = [name for name in release_names if 'i686' in name]
elif platform == "darwin":
if _platform.machine() == "x86_64":
release_names = [name for name in release_names if 'x86_64' in name]
elif _platform.machine() == "arm64":
release_names = [name for name in release_names if 'aarch64' in name]
log.debug(f"Found bitness-correct release_names: {release_names}")
# There should be only one: the actual release file
if len(release_names) > 1:
fail_for_ambiguity(release_names)
# Third, set the release file and hash file
hash_file = "SHA256SUMS"
release_file = release_names[0]
log.debug(f'Found release_file: {release_file}')
# log.debug(f'Found hash_file: {hash_file}')
log.debug(f'Found hash_file: {hash_file}')
# Pull out the url for the assets we identified
for asset in assets:
if asset['name'] == hash_file:
# get url for hash_file
hash_file_url = asset['url']
elif asset['name'] == release_file:
# get url for release_file
release_file_url = asset['url']
log.debug(f'Found hash_file_url: {hash_file_url}')
log.debug(f'Found release_file_url: {release_file_url}')
# Fourth Download Hash file
hash_file_response = requests.get(
hash_file_url,
headers={
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/octet-stream'
}
)
hash_file_path = BUILD_DIRECTORY.joinpath(hash_file)
with hash_file_path.open('wb') as fp:
fp.write(hash_file_response.content)
for line in hash_file_path.read_text().splitlines():
if release_file in line:
# We found the line with the release file name
hash_hex = line.split()[0]
log.debug(f"Found hash_hex: {hash_hex}")
break
log.debug(hash_hex)
# Fifth Download Release file
release_file_response = requests.get(
release_file_url,
headers={
'X-GitHub-Api-Version': '2022-11-28',
'Accept': 'application/octet-stream'
}
)
release_file_path = BUILD_DIRECTORY.joinpath(release_file)
with release_file_path.open('wb') as fp:
fp.write(release_file_response.content)
# Verify the hash of the downloaded release file
release_file_sha256 = get_file_sha256(release_file_path)
if hash_hex != release_file_sha256:
fail_for_hash_mismatch(hash_hex, release_file_sha256)
else:
log.info("Downloaded Python release matches expected hash value")
# Extract the release file into the dist directory
shutil.unpack_archive(
release_file_path,
ASSEMBLE_DIRECTORY,
)
# Move everything under python directory into subdirectory based on python version
src_dir = ASSEMBLE_DIRECTORY.joinpath('python')
dst_dir = src_dir.joinpath(TARGET_PYTHON_VERSION)
for item in src_dir.iterdir():
if item != dst_dir:
shutil.move(item, dst_dir.joinpath(item.name))
if platform == "windows":
python_executable = ASSEMBLE_DIRECTORY.joinpath('python').joinpath(TARGET_PYTHON_VERSION).joinpath('python')
else:
python_executable = ASSEMBLE_DIRECTORY.joinpath('python').joinpath(TARGET_PYTHON_VERSION).joinpath('bin').joinpath('python3')
# Upgrade pip
command = f'{python_executable} -m pip install --upgrade pip'
subprocess.run(command, shell=True)
# Install dependencies
command = f'{python_executable} -m pip install --no-warn-script-location -r {requirements_file}'
subprocess.run(command, shell=True)
# Install mast package
command = f'{python_executable} -m pip install {HERE}'
subprocess.run(command, shell=True)
# Copy all files from files/mast_home
files_directory = HERE.joinpath('files')
mast_home_directory = files_directory.joinpath('mast_home')
for item in mast_home_directory.iterdir():
if item.is_dir():
shutil.copytree(
item,
ASSEMBLE_DIRECTORY.joinpath(item.name)
)
else:
log.critical("Non-directory item in files/mast_home")
sys.exit(5)
# Copy all invocation scripts from files/invocation_scripts/{platform} to DIST_DIRECTORY
script_dir = files_directory.joinpath('invocation_scripts').joinpath(platform)
for item in script_dir.iterdir():
if item.name.startswith('set-env'):
contents = item.read_text()
contents = contents.replace('{PYTHON_VERSION}', TARGET_PYTHON_VERSION)
file_path = ASSEMBLE_DIRECTORY.joinpath(item.name)
file_path.write_text(contents)
else:
file_path = ASSEMBLE_DIRECTORY.joinpath(item.name)
shutil.copy(item, file_path)
if platform.lower() != "windows":
file_path.chmod(0o750)
command = f'{python_executable} -c "import mast; print(mast.__version__)"'
output = subprocess.check_output(command, shell=True)
mast_version = output.strip().decode()
shutil.make_archive(
DIST_DIRECTORY.joinpath(
f'MAST-{mast_version}_py{TARGET_PYTHON_VERSION}_{platform}',
),
format='zip',
root_dir=ASSEMBLE_DIRECTORY,
base_dir='.',
)