Skip to content

Commit 30780cb

Browse files
authored
Tools - get.py updates (#9247)
* Tools - get.py updates Using pathlib for paths, assume relative paths from __file__.parent as PWD Using argparse for arguments, expose previously uncustomizable bits. Reading tarfile with transparent compression. Drop previously untested .t{...} and .tar.{...}, just use "r:*" Remove hard-coded dependency on 'platform' and allow to specify sys_name, sys_platform and bits. Stub for DarwinARM, allow to fetch x86_64 packages in the meantime. * missing mkdir_p
1 parent 92002ec commit 30780cb

File tree

1 file changed

+215
-109
lines changed

1 file changed

+215
-109
lines changed

tools/get.py

Lines changed: 215 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,244 @@
11
#!/usr/bin/env python3
22
# This script will download and extract required tools into the current directory.
33
# Tools list is obtained from package/package_esp8266com_index.template.json file.
4-
# Written by Ivan Grokhotkov, 2015.
5-
#
6-
from __future__ import print_function
7-
import os
4+
# Originally written by Ivan Grokhotkov, 2015.
5+
6+
import argparse
87
import shutil
9-
import errno
10-
import os.path
118
import hashlib
129
import json
10+
import pathlib
1311
import platform
1412
import sys
1513
import tarfile
1614
import zipfile
1715
import re
1816

19-
verbose = True
20-
17+
from typing import Optional, Literal, List
2118
from urllib.request import urlretrieve
2219

23-
if sys.version_info >= (3,12):
24-
TARFILE_EXTRACT_ARGS = {'filter': 'data'}
20+
21+
PWD = pathlib.Path(__file__).parent
22+
23+
if sys.version_info >= (3, 12):
24+
TARFILE_EXTRACT_ARGS = {"filter": "data"}
2525
else:
2626
TARFILE_EXTRACT_ARGS = {}
2727

28-
dist_dir = 'dist/'
28+
PLATFORMS = {
29+
"Darwin": {32: "i386-apple-darwin", 64: "x86_64-apple-darwin"},
30+
"DarwinARM": {32: "arm64-apple-darwin", 64: "arm64-apple-darwin"},
31+
"Linux": {32: "i686-pc-linux-gnu", 64: "x86_64-pc-linux-gnu"},
32+
"LinuxARM": {32: "arm-linux-gnueabihf", 64: "aarch64-linux-gnu"},
33+
"Windows": {32: "i686-mingw32", 64: "x86_64-mingw32"},
34+
}
35+
36+
37+
class HashMismatch(Exception):
38+
pass
39+
2940

30-
def sha256sum(filename, blocksize=65536):
31-
hash = hashlib.sha256()
32-
with open(filename, "rb") as f:
41+
def sha256sum(p: pathlib.Path, blocksize=65536):
42+
hasher = hashlib.sha256()
43+
with p.open("rb") as f:
3344
for block in iter(lambda: f.read(blocksize), b""):
34-
hash.update(block)
35-
return hash.hexdigest()
45+
hasher.update(block)
46+
47+
return hasher.hexdigest()
3648

37-
def mkdir_p(path):
38-
try:
39-
os.makedirs(path)
40-
except OSError as exc:
41-
if exc.errno != errno.EEXIST or not os.path.isdir(path):
42-
raise
4349

4450
def report_progress(count, blockSize, totalSize):
45-
global verbose
46-
if verbose:
47-
percent = int(count*blockSize*100/totalSize)
48-
percent = min(100, percent)
49-
sys.stdout.write("\r%d%%" % percent)
50-
sys.stdout.flush()
51-
52-
def unpack(filename, destination):
53-
dirname = ''
54-
print('Extracting {0}'.format(filename))
55-
extension = filename.split('.')[-1]
56-
if filename.endswith((f'.tar.{extension}', f'.t{extension}')):
57-
tfile = tarfile.open(filename, f'r:{extension}')
58-
tfile.extractall(destination, **TARFILE_EXTRACT_ARGS)
59-
dirname= tfile.getnames()[0]
60-
elif filename.endswith('zip'):
61-
zfile = zipfile.ZipFile(filename)
51+
percent = int(count * blockSize * 100 / totalSize)
52+
percent = min(100, percent)
53+
print(f"\r{percent}%", end="", file=sys.stdout, flush=True)
54+
55+
56+
def unpack(p: pathlib.Path, destination: pathlib.Path):
57+
outdir = None # type: Optional[pathlib.Path]
58+
59+
print(f"Extracting {p}")
60+
if p.suffix == ".zip":
61+
zfile = zipfile.ZipFile(p)
6262
zfile.extractall(destination)
63-
dirname = zfile.namelist()[0]
63+
outdir = destination / zfile.namelist()[0]
6464
else:
65-
raise NotImplementedError('Unsupported archive type')
65+
tfile = tarfile.open(p, "r:*")
66+
tfile.extractall(destination, **TARFILE_EXTRACT_ARGS) # type: ignore
67+
outdir = destination / tfile.getnames()[0]
68+
69+
if not outdir:
70+
raise NotImplementedError(f"Unsupported archive type {p.suffix}")
6671

6772
# a little trick to rename tool directories so they don't contain version number
68-
rename_to = re.match(r'^([a-zA-Z_][^\-]*\-*)+', dirname).group(0).strip('-')
69-
if rename_to != dirname:
70-
print('Renaming {0} to {1}'.format(dirname, rename_to))
71-
if os.path.isdir(rename_to):
72-
shutil.rmtree(rename_to)
73-
shutil.move(dirname, rename_to)
74-
75-
def get_tool(tool):
76-
archive_name = tool['archiveFileName']
77-
local_path = dist_dir + archive_name
78-
url = tool['url']
79-
real_hash = tool['checksum'].split(':')[1]
80-
if not os.path.isfile(local_path):
81-
print('Downloading ' + archive_name);
82-
urlretrieve(url, local_path, report_progress)
83-
sys.stdout.write("\rDone\n")
84-
sys.stdout.flush()
73+
match = re.match(r"^([a-zA-Z_][^\-]*\-*)+", outdir.name)
74+
if match:
75+
rename_to = match.group(0).strip("-")
8576
else:
86-
print('Tool {0} already downloaded'.format(archive_name))
87-
local_hash = sha256sum(local_path)
88-
if local_hash != real_hash:
89-
print('Hash mismatch for {0}, delete the file and try again'.format(local_path))
90-
raise RuntimeError()
91-
unpack(local_path, '.')
92-
93-
def load_tools_list(filename, platform):
94-
tools_info = json.load(open(filename))['packages'][0]['tools']
95-
tools_to_download = []
96-
for t in tools_info:
97-
tool_platform = [p for p in t['systems'] if p['host'] == platform]
98-
if len(tool_platform) == 0:
99-
continue
100-
tools_to_download.append(tool_platform[0])
101-
return tools_to_download
102-
103-
def identify_platform():
104-
arduino_platform_names = {'Darwin' : {32 : 'i386-apple-darwin', 64 : 'x86_64-apple-darwin'},
105-
'Linux' : {32 : 'i686-pc-linux-gnu', 64 : 'x86_64-pc-linux-gnu'},
106-
'LinuxARM': {32 : 'arm-linux-gnueabihf', 64 : 'aarch64-linux-gnu'},
107-
'Windows' : {32 : 'i686-mingw32', 64 : 'x86_64-mingw32'}}
108-
bits = 32
109-
if sys.maxsize > 2**32:
110-
bits = 64
111-
sys_name = platform.system()
112-
if 'Linux' in sys_name and (platform.platform().find('arm') > 0 or platform.platform().find('aarch64') > 0):
113-
sys_name = 'LinuxARM'
114-
if 'CYGWIN_NT' in sys_name:
115-
sys_name = 'Windows'
116-
if 'MSYS_NT' in sys_name:
117-
sys_name = 'Windows'
118-
if 'MINGW' in sys_name:
119-
sys_name = 'Windows'
120-
return arduino_platform_names[sys_name][bits]
121-
122-
def main():
123-
global verbose
124-
# Support optional "-q" quiet mode simply
125-
if len(sys.argv) == 2:
126-
if sys.argv[1] == "-q":
127-
verbose = False
128-
# Remove a symlink generated in 2.6.3 which causes later issues since the tarball can't properly overwrite it
129-
if (os.path.exists('python3/python3')):
130-
os.unlink('python3/python3')
131-
print('Platform: {0}'.format(identify_platform()))
132-
tools_to_download = load_tools_list('../package/package_esp8266com_index.template.json', identify_platform())
133-
mkdir_p(dist_dir)
77+
rename_to = outdir.name
78+
79+
if outdir.name != rename_to:
80+
print(f"Renaming {outdir.name} to {rename_to}")
81+
destdir = destination / rename_to
82+
if destdir.is_dir():
83+
shutil.rmtree(destdir)
84+
shutil.move(outdir, destdir)
85+
86+
87+
# ref. https://docs.arduino.cc/arduino-cli/package_index_json-specification/
88+
def get_tool(tool: dict, *, dist_dir: pathlib.Path, quiet: bool, dry_run: bool):
89+
if not dist_dir.exists():
90+
dist_dir.mkdir(parents=True, exist_ok=True)
91+
92+
archive_name = tool["archiveFileName"]
93+
local_path = dist_dir / archive_name
94+
95+
url = tool["url"]
96+
algorithm, real_hash = tool["checksum"].split(":", 1)
97+
if algorithm != "SHA-256":
98+
raise NotImplementedError(f"Unsupported hash algorithm {algorithm}")
99+
100+
if dry_run:
101+
print(f'{archive_name} ({tool.get("size")} bytes): {url}')
102+
else:
103+
if not quiet:
104+
reporthook = report_progress
105+
else:
106+
reporthook = None
107+
108+
if not local_path.is_file():
109+
print(f"Downloading {archive_name}")
110+
urlretrieve(url, local_path, reporthook)
111+
print("\rDone", file=sys.stdout, flush=True)
112+
else:
113+
print(
114+
f"Tool {archive_name} ({local_path.stat().st_size} bytes) already downloaded"
115+
)
116+
117+
if not dry_run or (dry_run and local_path.exists()):
118+
local_hash = sha256sum(local_path)
119+
if local_hash != real_hash:
120+
raise HashMismatch(
121+
f"Expected {local_hash}, got {real_hash}. Delete {local_path} and try again"
122+
) from None
123+
124+
if not dry_run:
125+
unpack(local_path, PWD / ".")
126+
127+
128+
def load_tools_list(package_index_json: pathlib.Path, hosts: List[str]):
129+
out = []
130+
131+
with package_index_json.open("r") as f:
132+
root = json.load(f)
133+
134+
package = root["packages"][0]
135+
tools = package["tools"]
136+
137+
for info in tools:
138+
found = [p for p in info["systems"] for host in hosts if p["host"] == host]
139+
found.sort(key=lambda p: hosts.index(p["host"]))
140+
if found:
141+
out.append(found[0])
142+
143+
return out
144+
145+
146+
def select_host(
147+
sys_name: Optional[str],
148+
sys_platform: Optional[str],
149+
bits: Optional[Literal[32, 64]],
150+
) -> List[str]:
151+
if not sys_name:
152+
sys_name = platform.system()
153+
154+
if not sys_platform:
155+
sys_platform = platform.platform()
156+
157+
if not bits:
158+
bits = 32
159+
if sys.maxsize > 2**32:
160+
bits = 64
161+
162+
def maybe_arm(s: str) -> bool:
163+
return (s.find("arm") > 0) or (s.find("aarch64") > 0)
164+
165+
if "Darwin" in sys_name and maybe_arm(sys_platform):
166+
sys_name = "DarwinARM"
167+
elif "Linux" in sys_name and maybe_arm(sys_platform):
168+
sys_name = "LinuxARM"
169+
elif "CYGWIN_NT" in sys_name or "MSYS_NT" in sys_name or "MINGW" in sys_name:
170+
sys_name = "Windows"
171+
172+
out = [
173+
PLATFORMS[sys_name][bits],
174+
]
175+
176+
if sys_name == "DarwinARM":
177+
out.append(PLATFORMS["Darwin"][bits])
178+
179+
return out
180+
181+
182+
def main(args: argparse.Namespace):
183+
# #6960 - Remove a symlink generated in 2.6.3 which causes later issues since the tarball can't properly overwrite it
184+
py3symlink = PWD / "python3" / "python3"
185+
if py3symlink.is_symlink():
186+
py3symlink.unlink()
187+
188+
host = args.host
189+
if not host:
190+
host = select_host(
191+
sys_name=args.system,
192+
sys_platform=args.platform,
193+
bits=args.bits,
194+
)
195+
196+
print(f"Platform: {', '.join(host)}")
197+
198+
tools_to_download = load_tools_list(args.package_index_json, host)
199+
if args.tool:
200+
tools_to_download = [
201+
tool
202+
for tool in tools_to_download
203+
for exclude in args.tool
204+
if exclude in tool["archiveFileName"]
205+
]
206+
134207
for tool in tools_to_download:
135-
get_tool(tool)
208+
get_tool(
209+
tool,
210+
dist_dir=args.dist_dir,
211+
quiet=args.quiet,
212+
dry_run=args.dry_run,
213+
)
214+
215+
216+
def parse_args(args: Optional[str] = None, namespace=argparse.Namespace):
217+
parser = argparse.ArgumentParser(
218+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
219+
)
220+
221+
parser.add_argument("-q", "--quiet", action="store_true", default=False)
222+
parser.add_argument("-d", "--dry-run", action="store_true", default=False)
223+
parser.add_argument("-t", "--tool", action="append", type=str)
224+
225+
parser.add_argument("--host", type=str, action="append")
226+
parser.add_argument("--system", type=str)
227+
parser.add_argument("--platform", type=str)
228+
parser.add_argument("--bits", type=int, choices=PLATFORMS["Linux"].keys())
229+
230+
parser.add_argument(
231+
"--no-progress", dest="quiet", action="store_true", default=False
232+
)
233+
parser.add_argument("--dist-dir", type=pathlib.Path, default=PWD / "dist")
234+
parser.add_argument(
235+
"--package-index-json",
236+
type=pathlib.Path,
237+
default=PWD / ".." / "package/package_esp8266com_index.template.json",
238+
)
239+
240+
return parser.parse_args(args, namespace)
241+
136242

137-
if __name__ == '__main__':
138-
main()
243+
if __name__ == "__main__":
244+
main(parse_args())

0 commit comments

Comments
 (0)