Skip to content

Commit f3d72f9

Browse files
committed
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.
1 parent 92002ec commit f3d72f9

File tree

1 file changed

+212
-109
lines changed

1 file changed

+212
-109
lines changed

tools/get.py

Lines changed: 212 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,138 +1,241 @@
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+
archive_name = tool["archiveFileName"]
90+
local_path = dist_dir / archive_name
91+
92+
url = tool["url"]
93+
algorithm, real_hash = tool["checksum"].split(":", 1)
94+
if algorithm != "SHA-256":
95+
raise NotImplementedError(f"Unsupported hash algorithm {algorithm}")
96+
97+
if dry_run:
98+
print(f'{archive_name} ({tool.get("size")} bytes): {url}')
99+
else:
100+
if not quiet:
101+
reporthook = report_progress
102+
else:
103+
reporthook = None
104+
105+
if not local_path.is_file():
106+
print(f"Downloading {archive_name}")
107+
urlretrieve(url, local_path, reporthook)
108+
print("\rDone", file=sys.stdout, flush=True)
109+
else:
110+
print(
111+
f"Tool {archive_name} ({local_path.stat().st_size} bytes) already downloaded"
112+
)
113+
114+
if not dry_run or (dry_run and local_path.exists()):
115+
local_hash = sha256sum(local_path)
116+
if local_hash != real_hash:
117+
raise HashMismatch(
118+
f"Expected {local_hash}, got {real_hash}. Delete {local_path} and try again"
119+
) from None
120+
121+
if not dry_run:
122+
unpack(local_path, PWD / ".")
123+
124+
125+
def load_tools_list(package_index_json: pathlib.Path, hosts: List[str]):
126+
out = []
127+
128+
with package_index_json.open("r") as f:
129+
root = json.load(f)
130+
131+
package = root["packages"][0]
132+
tools = package["tools"]
133+
134+
for info in tools:
135+
found = [p for p in info["systems"] for host in hosts if p["host"] == host]
136+
found.sort(key=lambda p: hosts.index(p["host"]))
137+
if found:
138+
out.append(found[0])
139+
140+
return out
141+
142+
143+
def select_host(
144+
sys_name: Optional[str],
145+
sys_platform: Optional[str],
146+
bits: Optional[Literal[32, 64]],
147+
) -> List[str]:
148+
if not sys_name:
149+
sys_name = platform.system()
150+
151+
if not sys_platform:
152+
sys_platform = platform.platform()
153+
154+
if not bits:
155+
bits = 32
156+
if sys.maxsize > 2**32:
157+
bits = 64
158+
159+
def maybe_arm(s: str) -> bool:
160+
return (s.find("arm") > 0) or (s.find("aarch64") > 0)
161+
162+
if "Darwin" in sys_name and maybe_arm(sys_platform):
163+
sys_name = "DarwinARM"
164+
elif "Linux" in sys_name and maybe_arm(sys_platform):
165+
sys_name = "LinuxARM"
166+
elif "CYGWIN_NT" in sys_name or "MSYS_NT" in sys_name or "MINGW" in sys_name:
167+
sys_name = "Windows"
168+
169+
out = [
170+
PLATFORMS[sys_name][bits],
171+
]
172+
173+
if sys_name == "DarwinARM":
174+
out.append(PLATFORMS["Darwin"][bits])
175+
176+
return out
177+
178+
179+
def main(args: argparse.Namespace):
180+
# #6960 - Remove a symlink generated in 2.6.3 which causes later issues since the tarball can't properly overwrite it
181+
py3symlink = PWD / "python3" / "python3"
182+
if py3symlink.is_symlink():
183+
py3symlink.unlink()
184+
185+
host = args.host
186+
if not host:
187+
host = select_host(
188+
sys_name=args.system,
189+
sys_platform=args.platform,
190+
bits=args.bits,
191+
)
192+
193+
print(f"Platform: {', '.join(host)}")
194+
195+
tools_to_download = load_tools_list(args.package_index_json, host)
196+
if args.tool:
197+
tools_to_download = [
198+
tool
199+
for tool in tools_to_download
200+
for exclude in args.tool
201+
if exclude in tool["archiveFileName"]
202+
]
203+
134204
for tool in tools_to_download:
135-
get_tool(tool)
205+
get_tool(
206+
tool,
207+
dist_dir=args.dist_dir,
208+
quiet=args.quiet,
209+
dry_run=args.dry_run,
210+
)
211+
212+
213+
def parse_args(args: Optional[str] = None, namespace=argparse.Namespace):
214+
parser = argparse.ArgumentParser(
215+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
216+
)
217+
218+
parser.add_argument("-q", "--quiet", action="store_true", default=False)
219+
parser.add_argument("-d", "--dry-run", action="store_true", default=False)
220+
parser.add_argument("-t", "--tool", action="append", type=str)
221+
222+
parser.add_argument("--host", type=str, action="append")
223+
parser.add_argument("--system", type=str)
224+
parser.add_argument("--platform", type=str)
225+
parser.add_argument("--bits", type=int, choices=PLATFORMS["Linux"].keys())
226+
227+
parser.add_argument(
228+
"--no-progress", dest="quiet", action="store_true", default=False
229+
)
230+
parser.add_argument("--dist-dir", type=pathlib.Path, default=PWD / "dist")
231+
parser.add_argument(
232+
"--package-index-json",
233+
type=pathlib.Path,
234+
default=PWD / ".." / "package/package_esp8266com_index.template.json",
235+
)
236+
237+
return parser.parse_args(args, namespace)
238+
136239

137-
if __name__ == '__main__':
138-
main()
240+
if __name__ == "__main__":
241+
main(parse_args())

0 commit comments

Comments
 (0)