1#!/usr/bin/env python3 2# Copyright 2020 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15"""Arduino Core Installer.""" 16 17import argparse 18import logging 19import operator 20import os 21import platform 22import shutil 23import stat 24import subprocess 25import sys 26import time 27from pathlib import Path 28from typing import Dict, List 29 30import pw_arduino_build.file_operations as file_operations 31 32_LOG = logging.getLogger(__name__) 33 34 35class ArduinoCoreNotSupported(Exception): 36 """Exception raised when a given core can not be installed.""" 37 38 39# yapf: disable 40_ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = { 41 # pylint: disable=line-too-long 42 "teensy": { 43 "Linux": { 44 "arduino-ide": { 45 "url": "https://downloads.arduino.cc/arduino-1.8.13-linux64.tar.xz", 46 "file_name": "arduino-1.8.13-linux64.tar.xz", 47 "sha256": "1b20d0ec850a2a63488009518725f058668bb6cb48c321f82dcf47dc4299b4ad", 48 }, 49 "teensyduino": { 50 "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux64", 51 "sha256": "2e6cd99a757bc80593ea3de006de4cc934bcb0a6ec74cad8ec327f0289d40f0b", 52 "file_name": "TeensyduinoInstall.linux64", 53 }, 54 }, 55 # TODO(tonymd): Handle 32-bit Linux Install? 56 "Linux32": { 57 "arduino-ide": { 58 "url": "https://downloads.arduino.cc/arduino-1.8.13-linux32.tar.xz", 59 "file_name": "arduino-1.8.13-linux32.tar.xz", 60 "sha256": "", 61 }, 62 "teensyduino": { 63 "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux32", 64 "file_name": "TeensyduinoInstall.linux32", 65 "sha256": "", 66 }, 67 }, 68 # TODO(tonymd): Handle ARM32 (Raspberry Pi) Install? 69 "LinuxARM32": { 70 "arduino-ide": { 71 "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxarm.tar.xz", 72 "file_name": "arduino-1.8.13-linuxarm.tar.xz", 73 "sha256": "", 74 }, 75 "teensyduino": { 76 "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxarm", 77 "file_name": "TeensyduinoInstall.linuxarm", 78 "sha256": "", 79 }, 80 }, 81 # TODO(tonymd): Handle ARM64 Install? 82 "LinuxARM64": { 83 "arduino-ide": { 84 "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxaarch64.tar.xz", 85 "file_name": "arduino-1.8.13-linuxaarch64.tar.xz", 86 "sha256": "", 87 }, 88 "teensyduino": { 89 "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxaarch64", 90 "file_name": "TeensyduinoInstall.linuxaarch64", 91 "sha256": "", 92 }, 93 }, 94 "Darwin": { 95 "teensyduino": { 96 "url": "https://www.pjrc.com/teensy/td_153/Teensyduino_MacOS_Catalina.zip", 97 "file_name": "Teensyduino_MacOS_Catalina.zip", 98 "sha256": "401ef42c6e83e621cdda20191a4ef9b7db8a214bede5a94a9e26b45f79c64fe2", 99 }, 100 }, 101 "Windows": { 102 "arduino-ide": { 103 "url": "https://downloads.arduino.cc/arduino-1.8.13-windows.zip", 104 "file_name": "arduino-1.8.13-windows.zip", 105 "sha256": "78d3e96827b9e9b31b43e516e601c38d670d29f12483e88cbf6d91a0f89ef524", 106 }, 107 "teensyduino": { 108 "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.exe", 109 # The installer should be named 'Teensyduino.exe' instead of 110 # 'TeensyduinoInstall.exe' to trigger a non-admin installation. 111 "file_name": "Teensyduino.exe", 112 "sha256": "88f58681e5c4772c54e462bc88280320e4276e5b316dcab592fe38d96db990a1", 113 }, 114 } 115 }, 116 "adafruit-samd": { 117 "all": { 118 "core": { 119 "version": "1.6.2", 120 "url": "https://github.com/adafruit/ArduinoCore-samd/archive/1.6.2.tar.gz", 121 "file_name": "adafruit-samd-1.6.2.tar.gz", 122 "sha256": "5875f5bc05904c10e6313f02653f28f2f716db639d3d43f5a1d8a83d15339d64", 123 } 124 }, 125 "Linux": {}, 126 "Darwin": {}, 127 "Windows": {}, 128 }, 129 "arduino-samd": { 130 "all": { 131 "core": { 132 "version": "1.8.8", 133 "url": "http://downloads.arduino.cc/cores/samd-1.8.8.tar.bz2", 134 "file_name": "arduino-samd-1.8.8.tar.bz2", 135 "sha256": "7b93eb705cba9125d9ee52eba09b51fb5fe34520ada351508f4253abbc9f27fa", 136 } 137 }, 138 "Linux": {}, 139 "Darwin": {}, 140 "Windows": {}, 141 }, 142 "stm32duino": { 143 "all": { 144 "core": { 145 "version": "1.9.0", 146 "url": "https://github.com/stm32duino/Arduino_Core_STM32/archive/1.9.0.tar.gz", 147 "file_name": "stm32duino-1.9.0.tar.gz", 148 "sha256": "4f75ba7a117d90392e8f67c58d31d22393749b9cdd3279bc21e7261ec06c62bf", 149 } 150 }, 151 "Linux": {}, 152 "Darwin": {}, 153 "Windows": {}, 154 }, 155} 156# yapf: enable 157 158 159def install_core_command(args: argparse.Namespace): 160 install_core(args.prefix, args.core_name) 161 162 163def install_core(prefix, core_name): 164 install_prefix = os.path.realpath( 165 os.path.expanduser(os.path.expandvars(prefix))) 166 install_dir = os.path.join(install_prefix, core_name) 167 cache_dir = os.path.join(install_prefix, ".cache", core_name) 168 169 if core_name in supported_cores(): 170 shutil.rmtree(install_dir, ignore_errors=True) 171 os.makedirs(install_dir, exist_ok=True) 172 os.makedirs(cache_dir, exist_ok=True) 173 174 if core_name == "teensy": 175 if platform.system() == "Linux": 176 install_teensy_core_linux(install_prefix, install_dir, cache_dir) 177 elif platform.system() == "Darwin": 178 install_teensy_core_mac(install_prefix, install_dir, cache_dir) 179 elif platform.system() == "Windows": 180 install_teensy_core_windows(install_prefix, install_dir, cache_dir) 181 apply_teensy_patches(install_dir) 182 elif core_name == "adafruit-samd": 183 install_adafruit_samd_core(install_prefix, install_dir, cache_dir) 184 elif core_name == "stm32duino": 185 install_stm32duino_core(install_prefix, install_dir, cache_dir) 186 elif core_name == "arduino-samd": 187 install_arduino_samd_core(install_prefix, install_dir, cache_dir) 188 else: 189 raise ArduinoCoreNotSupported( 190 "Invalid core '{}'. Supported cores: {}".format( 191 core_name, ", ".join(supported_cores()))) 192 193 194def supported_cores(): 195 return _ARDUINO_CORE_ARTIFACTS.keys() 196 197 198def get_windows_process_names() -> List[str]: 199 result = subprocess.run("wmic process get description", 200 capture_output=True) 201 output = result.stdout.decode().splitlines() 202 return [line.strip() for line in output if line] 203 204 205def install_teensy_core_windows(install_prefix, install_dir, cache_dir): 206 """Download and install Teensyduino artifacts for Windows.""" 207 teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()] 208 209 arduino_artifact = teensy_artifacts["arduino-ide"] 210 arduino_zipfile = file_operations.download_to_cache( 211 url=arduino_artifact["url"], 212 expected_sha256sum=arduino_artifact["sha256"], 213 cache_directory=cache_dir, 214 downloaded_file_name=arduino_artifact["file_name"]) 215 216 teensyduino_artifact = teensy_artifacts["teensyduino"] 217 teensyduino_installer = file_operations.download_to_cache( 218 url=teensyduino_artifact["url"], 219 expected_sha256sum=teensyduino_artifact["sha256"], 220 cache_directory=cache_dir, 221 downloaded_file_name=teensyduino_artifact["file_name"]) 222 223 file_operations.extract_archive(arduino_zipfile, install_dir, cache_dir) 224 225 # "teensy" here should match args.core_name 226 teensy_core_dir = os.path.join(install_prefix, "teensy") 227 228 # Change working directory for installation 229 original_working_dir = os.getcwd() 230 os.chdir(install_prefix) 231 232 install_command = [teensyduino_installer, "--dir=teensy"] 233 _LOG.info(" Running: %s", " ".join(install_command)) 234 _LOG.info(" Please click yes on the Windows 'User Account Control' " 235 "dialog.") 236 _LOG.info(" You should see: 'Verified publisher: PRJC.COM LLC'") 237 238 def wait_for_process(process_name, 239 timeout=30, 240 result_operator=operator.truth): 241 start_time = time.time() 242 while result_operator(process_name in get_windows_process_names()): 243 time.sleep(1) 244 if time.time() > start_time + timeout: 245 _LOG.error( 246 "Error: Installation Failed.\n" 247 "Please click yes on the Windows 'User Account Control' " 248 "dialog.") 249 sys.exit(1) 250 251 # Run Teensyduino installer with admin rights (non-blocking) 252 # User Account Control (UAC) will prompt the user for consent 253 import ctypes # pylint: disable=import-outside-toplevel 254 ctypes.windll.shell32.ShellExecuteW( 255 None, # parent window handle 256 "runas", # operation 257 teensyduino_installer, # file to run 258 subprocess.list2cmdline(install_command), # command parameters 259 install_prefix, # working directory 260 1) # Display mode (SW_SHOWNORMAL: Activates and displays a window) 261 262 # Wait for teensyduino_installer to start running 263 wait_for_process("TeensyduinoInstall.exe", result_operator=operator.not_) 264 265 _LOG.info("Waiting for TeensyduinoInstall.exe to finish.") 266 # Wait till teensyduino_installer is finished 267 wait_for_process("TeensyduinoInstall.exe", timeout=360) 268 269 if not os.path.exists(os.path.join(teensy_core_dir, "hardware", "teensy")): 270 _LOG.error( 271 "Error: Installation Failed.\n" 272 "Please try again and ensure Teensyduino is installed in " 273 "the folder:\n" 274 "%s", teensy_core_dir) 275 sys.exit(1) 276 else: 277 _LOG.info("Install complete!") 278 279 file_operations.remove_empty_directories(install_dir) 280 os.chdir(original_working_dir) 281 282 283def install_teensy_core_mac(unused_install_prefix, install_dir, cache_dir): 284 """Download and install Teensyduino artifacts for Mac.""" 285 teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()] 286 287 teensyduino_artifact = teensy_artifacts["teensyduino"] 288 teensyduino_zip = file_operations.download_to_cache( 289 url=teensyduino_artifact["url"], 290 expected_sha256sum=teensyduino_artifact["sha256"], 291 cache_directory=cache_dir, 292 downloaded_file_name=teensyduino_artifact["file_name"]) 293 294 extracted_files = file_operations.extract_archive( 295 teensyduino_zip, 296 install_dir, 297 cache_dir, 298 remove_single_toplevel_folder=False) 299 toplevel_folder = sorted(extracted_files)[0] 300 os.symlink(os.path.join(toplevel_folder, "Contents", "Java", "hardware"), 301 os.path.join(install_dir, "hardware"), 302 target_is_directory=True) 303 304 305def install_teensy_core_linux(install_prefix, install_dir, cache_dir): 306 """Download and install Teensyduino artifacts for Windows.""" 307 teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()] 308 309 arduino_artifact = teensy_artifacts["arduino-ide"] 310 arduino_tarfile = file_operations.download_to_cache( 311 url=arduino_artifact["url"], 312 expected_sha256sum=arduino_artifact["sha256"], 313 cache_directory=cache_dir, 314 downloaded_file_name=arduino_artifact["file_name"]) 315 316 teensyduino_artifact = teensy_artifacts["teensyduino"] 317 teensyduino_installer = file_operations.download_to_cache( 318 url=teensyduino_artifact["url"], 319 expected_sha256sum=teensyduino_artifact["sha256"], 320 cache_directory=cache_dir, 321 downloaded_file_name=teensyduino_artifact["file_name"]) 322 323 file_operations.extract_archive(arduino_tarfile, install_dir, cache_dir) 324 os.chmod(teensyduino_installer, 325 os.stat(teensyduino_installer).st_mode | stat.S_IEXEC) 326 327 original_working_dir = os.getcwd() 328 os.chdir(install_prefix) 329 # "teensy" here should match args.core_name 330 install_command = [teensyduino_installer, "--dir=teensy"] 331 subprocess.run(install_command) 332 333 file_operations.remove_empty_directories(install_dir) 334 os.chdir(original_working_dir) 335 336 337def apply_teensy_patches(install_dir): 338 # On Mac the "hardware" directory is a symlink: 339 # ls -l third_party/arduino/cores/teensy/ 340 # hardware -> Teensyduino.app/Contents/Java/hardware 341 # Resolve paths since `git apply` doesn't work if a path is beyond a 342 # symbolic link. 343 patch_root_path = (Path(install_dir) / 344 "hardware/teensy/avr/cores").resolve() 345 346 # Get all *.diff files relative to this python file's parent directory. 347 patch_file_paths = sorted( 348 (Path(__file__).parent / "core_patches/teensy").glob("*.diff")) 349 350 # Apply each patch file. 351 for diff_path in patch_file_paths: 352 file_operations.git_apply_patch(patch_root_path.as_posix(), 353 diff_path.as_posix(), 354 unsafe_paths=True) 355 356 357def install_arduino_samd_core(install_prefix: str, install_dir: str, 358 cache_dir: str): 359 artifacts = _ARDUINO_CORE_ARTIFACTS["arduino-samd"]["all"]["core"] 360 core_tarfile = file_operations.download_to_cache( 361 url=artifacts["url"], 362 expected_sha256sum=artifacts["sha256"], 363 cache_directory=cache_dir, 364 downloaded_file_name=artifacts["file_name"]) 365 366 package_path = os.path.join(install_dir, "hardware", "samd", 367 artifacts["version"]) 368 os.makedirs(package_path, exist_ok=True) 369 file_operations.extract_archive(core_tarfile, package_path, cache_dir) 370 original_working_dir = os.getcwd() 371 os.chdir(install_prefix) 372 # TODO(tonymd): Fetch core/tools as specified by: 373 # http://downloads.arduino.cc/packages/package_index.json 374 os.chdir(original_working_dir) 375 return True 376 377 378def install_adafruit_samd_core(install_prefix: str, install_dir: str, 379 cache_dir: str): 380 artifacts = _ARDUINO_CORE_ARTIFACTS["adafruit-samd"]["all"]["core"] 381 core_tarfile = file_operations.download_to_cache( 382 url=artifacts["url"], 383 expected_sha256sum=artifacts["sha256"], 384 cache_directory=cache_dir, 385 downloaded_file_name=artifacts["file_name"]) 386 387 package_path = os.path.join(install_dir, "hardware", "samd", 388 artifacts["version"]) 389 os.makedirs(package_path, exist_ok=True) 390 file_operations.extract_archive(core_tarfile, package_path, cache_dir) 391 392 original_working_dir = os.getcwd() 393 os.chdir(install_prefix) 394 # TODO(tonymd): Fetch platform specific tools as specified by: 395 # https://adafruit.github.io/arduino-board-index/package_adafruit_index.json 396 # Specifically: 397 # https://github.com/ARM-software/CMSIS_5/archive/5.4.0.tar.gz 398 os.chdir(original_working_dir) 399 return True 400 401 402def install_stm32duino_core(install_prefix, install_dir, cache_dir): 403 artifacts = _ARDUINO_CORE_ARTIFACTS["stm32duino"]["all"]["core"] 404 core_tarfile = file_operations.download_to_cache( 405 url=artifacts["url"], 406 expected_sha256sum=artifacts["sha256"], 407 cache_directory=cache_dir, 408 downloaded_file_name=artifacts["file_name"]) 409 410 package_path = os.path.join(install_dir, "hardware", "stm32", 411 artifacts["version"]) 412 os.makedirs(package_path, exist_ok=True) 413 file_operations.extract_archive(core_tarfile, package_path, cache_dir) 414 original_working_dir = os.getcwd() 415 os.chdir(install_prefix) 416 # TODO(tonymd): Fetch platform specific tools as specified by: 417 # https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json 418 os.chdir(original_working_dir) 419 return True 420