• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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