|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | # This script will download and extract required tools into the current directory. |
3 | 3 | # 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 |
8 | 7 | import shutil |
9 | | -import errno |
10 | | -import os.path |
11 | 8 | import hashlib |
12 | 9 | import json |
| 10 | +import pathlib |
13 | 11 | import platform |
14 | 12 | import sys |
15 | 13 | import tarfile |
16 | 14 | import zipfile |
17 | 15 | import re |
18 | 16 |
|
19 | | -verbose = True |
20 | | - |
| 17 | +from typing import Optional, Literal, List |
21 | 18 | from urllib.request import urlretrieve |
22 | 19 |
|
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"} |
25 | 25 | else: |
26 | 26 | TARFILE_EXTRACT_ARGS = {} |
27 | 27 |
|
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 | + |
29 | 40 |
|
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: |
33 | 44 | 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() |
36 | 48 |
|
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 |
43 | 49 |
|
44 | 50 | 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) |
62 | 62 | zfile.extractall(destination) |
63 | | - dirname = zfile.namelist()[0] |
| 63 | + outdir = destination / zfile.namelist()[0] |
64 | 64 | 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}") |
66 | 71 |
|
67 | 72 | # 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("-") |
85 | 76 | 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 | + |
134 | 207 | 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 | + |
136 | 242 |
|
137 | | -if __name__ == '__main__': |
138 | | - main() |
| 243 | +if __name__ == "__main__": |
| 244 | + main(parse_args()) |
0 commit comments