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