• 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
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