|
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