Arduino: python cleanup
- Move code in arduinobuilder.py to their own modules:
- __main__.py
- builder.py
- core_installer.py
- file_operations.py
- New log.py module
- os.system -> subprocess.run
- Move --run-* args in the 'show' subcommand under the 'run' subcommand.
This allows running multiple steps in the same invocation. For
example, this will execute 3 steps:
arduino_builder run \
--run-objcopy \
--run-postbuild \
--run-upload-command teensyloader
Change-Id: Ibe5493689f39aa6e6aaeecf5bc7065929111499d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/19143
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index bf30cd4..c214632 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -32,7 +32,7 @@
}
arduino_builder_script =
- get_path_info("py/pw_arduino_build/arduinobuilder.py", "abspath")
+ get_path_info("py/pw_arduino_build/__main__.py", "abspath")
_arduino_core_path =
rebase_path("../third_party/arduino/cores/$arduino_core_name")
@@ -46,12 +46,20 @@
arduino_package_name,
"--compiler-path-override",
_compiler_path_override,
- "show",
- "--delimit-with-newlines",
+]
+
+arduino_board_args = [
"--build-path",
rebase_path(root_build_dir),
"--board",
arduino_board,
"--menu-options",
]
-arduino_global_args += arduino_menu_options
+arduino_board_args += arduino_menu_options
+
+arduino_show_command_args = arduino_global_args + [
+ "show",
+ "--delimit-with-newlines",
+ ] + arduino_board_args
+
+arduino_run_command_args = arduino_global_args + [ "run" ] + arduino_board_args
diff --git a/pw_arduino_build/py/pw_arduino_build/arduinobuilder_test.py b/pw_arduino_build/py/builder_test.py
similarity index 100%
rename from pw_arduino_build/py/pw_arduino_build/arduinobuilder_test.py
rename to pw_arduino_build/py/builder_test.py
diff --git a/pw_arduino_build/py/file_operations_test.py b/pw_arduino_build/py/file_operations_test.py
new file mode 100644
index 0000000..7306fbb
--- /dev/null
+++ b/pw_arduino_build/py/file_operations_test.py
@@ -0,0 +1,103 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for file_operations module."""
+
+import os
+import shutil
+import tempfile
+import unittest
+from pathlib import Path
+from parameterized import parameterized
+
+import pw_arduino_build.file_operations as file_operations
+
+
+def file_set():
+ return [
+ "app.ino",
+ "core/asm.S",
+ "core/asm.s",
+ "core/pwm/pulse.c",
+ "core/pwm/pulse.h",
+ "libraries/a.c",
+ "libraries/b.cpp",
+ "libraries/c.cc",
+ "libraries/c.h",
+ ]
+
+
+def create_files(root_dir, file_names):
+ for file_name in file_names:
+ folder_path = Path(root_dir) / Path(os.path.dirname(file_name))
+ folder_path.mkdir(parents=True, exist_ok=True)
+ file_path = Path(root_dir) / Path(file_name)
+ file_path.touch(exist_ok=True)
+
+
+class TestFileOperations(unittest.TestCase):
+ """Tests to ensure arduino core library source files can be found."""
+ def setUp(self):
+ self.test_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.test_dir)
+
+ @parameterized.expand([
+ (
+ "sources recursive", file_set(), ["**/*.ino", "**/*.h", "**/*.cpp"],
+ [
+ "app.ino",
+ "core/pwm/pulse.h",
+ "libraries/b.cpp",
+ "libraries/c.h",
+ ]
+ ),
+ (
+ "directories recursive", file_set(), ["**"],
+ [
+ "core",
+ "core/pwm",
+ "libraries",
+ ]
+ ),
+ (
+ "directories one level deep", file_set(), ["*"],
+ [
+ "core",
+ "libraries",
+ ]
+ ),
+ (
+ "items one level deep", file_set(), ["*"],
+ [
+ "app.ino",
+ "core",
+ "libraries",
+ ]
+ )
+ ]) # yapf: disable
+ def test_find_files(self, test_case, base_fileset, patterns,
+ expected_results):
+ """Test find_files on source files and directories.
+ """
+ create_files(self.test_dir, base_fileset)
+ result = file_operations.find_files(self.test_dir,
+ patterns,
+ directories_only=("directories"
+ in test_case))
+ self.assertSequenceEqual(expected_results, result)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_arduino_build/py/pw_arduino_build/__main__.py b/pw_arduino_build/py/pw_arduino_build/__main__.py
new file mode 100644
index 0000000..0bdbcf1
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/__main__.py
@@ -0,0 +1,457 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Command line interface for arduino_builder."""
+
+import argparse
+import glob
+import logging
+import os
+import pprint
+import shlex
+import subprocess
+import sys
+from typing import List
+
+from pw_arduino_build import core_installer, log
+from pw_arduino_build.builder import ArduinoBuilder
+
+_LOG = logging.getLogger(__name__)
+
+_pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
+_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+
+
+def list_boards_command(unused_args, builder):
+ # list-boards subcommand
+ # (does not need a selected board or default menu options)
+
+ # TODO(tonymd): Print this sorted with auto-ljust columns
+ longest_name_length = 0
+ for board_name, board_dict in builder.board.items():
+ if len(board_name) > longest_name_length:
+ longest_name_length = len(board_name)
+ longest_name_length += 2
+
+ print("Board Name".ljust(longest_name_length), "Description")
+ for board_name, board_dict in builder.board.items():
+ print(board_name.ljust(longest_name_length), board_dict['name'])
+ sys.exit(0)
+
+
+def list_menu_options_command(args, builder):
+ # List all menu options for the selected board.
+ builder.select_board(args.board)
+
+ print("All Options")
+ all_options, all_column_widths = builder.get_menu_options()
+ separator = "-" * (all_column_widths[0] + all_column_widths[1] + 2)
+ print(separator)
+
+ for name, description in all_options:
+ print(name.ljust(all_column_widths[0] + 1), description)
+
+ print("\nDefault Options")
+ print(separator)
+
+ default_options, unused_column_widths = builder.get_default_menu_options()
+ for name, description in default_options:
+ print(name.ljust(all_column_widths[0] + 1), description)
+
+
+def show_command_print_string_list(args, string_list: List[str]):
+ join_token = " "
+ if args.delimit_with_newlines:
+ join_token = "\n"
+ print(join_token.join(string_list))
+
+
+def show_command_print_flag_string(args, flag_string):
+ if args.delimit_with_newlines:
+ flag_string_with_newlines = shlex.split(flag_string)
+ print("\n".join(flag_string_with_newlines))
+ else:
+ print(flag_string)
+
+
+def run_command_lines(args, command_lines: List[str]):
+ for command_line in command_lines:
+ if not args.quiet:
+ print(command_line)
+ # TODO(tonymd): Exit with sub command exit code.
+ command_line_args = shlex.split(command_line)
+ process = subprocess.run(command_line_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ if process.returncode != 0:
+ _LOG.error('Command failed with exit code %d.', process.returncode)
+ _LOG.error('Full command:')
+ _LOG.error('')
+ _LOG.error(' %s', command_line)
+ _LOG.error('')
+ _LOG.error('Process output:')
+ print(flush=True)
+ sys.stdout.buffer.write(process.stdout)
+ print(flush=True)
+ _LOG.error('')
+
+
+def run_command(args, builder):
+ """Run sub command function.
+
+ Runs Arduino recipes.
+ """
+
+ if args.run_prebuilds:
+ run_command_lines(args, builder.get_prebuild_steps())
+
+ if args.run_link:
+ line = builder.get_link_line()
+ archive_file_path = args.run_link[0] # pylint: disable=unused-variable
+ object_files = args.run_link[1:]
+ line = line.replace("{object_files}", " ".join(object_files), 1)
+ run_command_lines(args, [line])
+
+ if args.run_objcopy:
+ run_command_lines(args, builder.get_objcopy_steps())
+
+ if args.run_postbuilds:
+ run_command_lines(args, builder.get_postbuild_steps())
+
+ if args.run_upload_command:
+ command = builder.get_upload_line(args.run_upload_command,
+ args.serial_port)
+ run_command_lines(args, [command])
+
+
+# pylint: disable=too-many-branches
+def show_command(args, builder):
+ """Show sub command function.
+
+ Prints compiler info and flags.
+ """
+ if args.cc_binary:
+ print(builder.get_cc_binary())
+
+ elif args.cxx_binary:
+ print(builder.get_cxx_binary())
+
+ elif args.objcopy_binary:
+ print(builder.get_objcopy_binary())
+
+ elif args.ar_binary:
+ print(builder.get_ar_binary())
+
+ elif args.size_binary:
+ print(builder.get_size_binary())
+
+ elif args.c_compile:
+ print(builder.get_c_compile_line())
+
+ elif args.cpp_compile:
+ print(builder.get_cpp_compile_line())
+
+ elif args.link:
+ print(builder.get_link_line())
+
+ elif args.objcopy:
+ print(builder.get_objcopy(args.objcopy))
+
+ elif args.objcopy_flags:
+ objcopy_flags = builder.get_objcopy_flags(args.objcopy_flags)
+ show_command_print_flag_string(args, objcopy_flags)
+
+ elif args.c_flags:
+ cflags = builder.get_c_flags()
+ show_command_print_flag_string(args, cflags)
+
+ elif args.s_flags:
+ sflags = builder.get_s_flags()
+ show_command_print_flag_string(args, sflags)
+
+ elif args.cpp_flags:
+ cppflags = builder.get_cpp_flags()
+ show_command_print_flag_string(args, cppflags)
+
+ elif args.ld_flags:
+ ldflags = builder.get_ld_flags()
+ show_command_print_flag_string(args, ldflags)
+
+ elif args.ld_libs:
+ print(builder.get_ld_libs())
+
+ elif args.ar_flags:
+ ar_flags = builder.get_ar_flags()
+ show_command_print_flag_string(args, ar_flags)
+
+ elif args.core_path:
+ print(builder.get_core_path())
+
+ elif args.postbuild:
+ print(builder.get_postbuild_line(args.postbuild))
+
+ elif args.upload_command:
+ print(builder.get_upload_line(args.upload_command, args.serial_port))
+
+ elif args.upload_tools:
+ tools = builder.get_upload_tool_names()
+ for tool_name in tools:
+ print(tool_name)
+
+ elif args.library_includes:
+ show_command_print_string_list(args, builder.library_includes())
+
+ elif args.library_c_files:
+ show_command_print_string_list(args, builder.library_c_files())
+
+ elif args.library_cpp_files:
+ show_command_print_string_list(args, builder.library_cpp_files())
+
+ elif args.core_c_files:
+ show_command_print_string_list(args, builder.core_c_files())
+
+ elif args.core_s_files:
+ show_command_print_string_list(args, builder.core_s_files())
+
+ elif args.core_cpp_files:
+ show_command_print_string_list(args, builder.core_cpp_files())
+
+ elif args.variant_c_files:
+ vfiles = builder.variant_c_files()
+ if vfiles:
+ show_command_print_string_list(args, vfiles)
+
+ elif args.variant_s_files:
+ vfiles = builder.variant_s_files()
+ if vfiles:
+ show_command_print_string_list(args, vfiles)
+
+ elif args.variant_cpp_files:
+ vfiles = builder.variant_cpp_files()
+ if vfiles:
+ show_command_print_string_list(args, vfiles)
+
+
+def add_common_options(parser, serial_port, build_path, build_project_name,
+ project_path, project_source_path):
+ """Add command line options common to the run and show commands."""
+ parser.add_argument(
+ "--serial-port",
+ default=serial_port,
+ help="Serial port for flashing flash. Default: '{}'".format(
+ serial_port))
+ parser.add_argument(
+ "--build-path",
+ default=build_path,
+ help="Build directory. Default: '{}'".format(build_path))
+ parser.add_argument(
+ "--project-path",
+ default=project_path,
+ help="Project directory. Default: '{}'".format(project_path))
+ parser.add_argument(
+ "--project-source-path",
+ default=project_source_path,
+ help="Project directory. Default: '{}'".format(project_source_path))
+ parser.add_argument(
+ "--build-project-name",
+ default=build_project_name,
+ help="Project name. Default: '{}'".format(build_project_name))
+ parser.add_argument("--board",
+ required=True,
+ help="Name of the board to use.")
+ # nargs="+" is one or more args, e.g:
+ # --menu-options menu.usb.serialhid menu.speed.150
+ parser.add_argument("--menu-options", nargs="+", type=str)
+
+
+def main():
+ """Main command line function.
+
+ Parses command line args and dispatches to sub `*_command()` functions.
+ """
+ def log_level(arg: str) -> int:
+ try:
+ return getattr(logging, arg.upper())
+ except AttributeError:
+ raise argparse.ArgumentTypeError(
+ f'{arg.upper()} is not a valid log level')
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-q",
+ "--quiet",
+ help="hide run command output",
+ action="store_true")
+ parser.add_argument('-l',
+ '--loglevel',
+ type=log_level,
+ default=logging.INFO,
+ help='Set the log level '
+ '(debug, info, warning, error, critical)')
+
+ build_path = os.path.realpath(
+ os.path.expanduser(os.path.expandvars("./build")))
+ project_path = os.path.realpath(
+ os.path.expanduser(os.path.expandvars("./")))
+ project_source_path = os.path.join(project_path, "src")
+ build_project_name = os.path.basename(project_path)
+
+ serial_port = "UNKNOWN"
+ # TODO(tonymd): Temp solution to passing in serial port. It should use
+ # arduino core discovery tools.
+ possible_serial_ports = glob.glob("/dev/ttyACM*") + glob.glob(
+ "/dev/ttyUSB*")
+ if possible_serial_ports:
+ serial_port = possible_serial_ports[0]
+
+ # Global command line options
+ parser.add_argument("--arduino-package-path",
+ help="Path to the arduino IDE install location.")
+ parser.add_argument("--arduino-package-name",
+ help="Name of the Arduino board package to use.")
+ parser.add_argument("--compiler-path-override",
+ help="Path to arm-none-eabi-gcc bin folder. "
+ "Default: Arduino core specified gcc")
+
+ # Subcommands
+ subparsers = parser.add_subparsers(title="subcommand",
+ description="valid subcommands",
+ help="sub-command help",
+ dest="subcommand",
+ required=True)
+
+ # install-core command
+ install_core_parser = subparsers.add_parser(
+ "install-core", help="Download and install arduino cores")
+ install_core_parser.set_defaults(func=core_installer.install_core_command)
+ install_core_parser.add_argument("--prefix",
+ required=True,
+ help="Path to install core files.")
+ install_core_parser.add_argument(
+ "--core-name",
+ required=True,
+ choices=core_installer.supported_cores(),
+ help="Name of the arduino core to install.")
+
+ # list-boards command
+ list_boards_parser = subparsers.add_parser("list-boards",
+ help="show supported boards")
+ list_boards_parser.set_defaults(func=list_boards_command)
+
+ # list-menu-options command
+ list_menu_options_parser = subparsers.add_parser(
+ "list-menu-options",
+ help="show available menu options for selected board")
+ list_menu_options_parser.set_defaults(func=list_menu_options_command)
+ list_menu_options_parser.add_argument("--board",
+ required=True,
+ help="Name of the board to use.")
+
+ # show command
+ show_parser = subparsers.add_parser("show",
+ help="Return compiler information.")
+ add_common_options(show_parser, serial_port, build_path,
+ build_project_name, project_path, project_source_path)
+ show_parser.add_argument("--delimit-with-newlines",
+ help="Separate flag output with newlines.",
+ action="store_true")
+
+ output_group = show_parser.add_mutually_exclusive_group(required=True)
+ output_group.add_argument("--c-compile", action="store_true")
+ output_group.add_argument("--cpp-compile", action="store_true")
+ output_group.add_argument("--link", action="store_true")
+ output_group.add_argument("--c-flags", action="store_true")
+ output_group.add_argument("--s-flags", action="store_true")
+ output_group.add_argument("--cpp-flags", action="store_true")
+ output_group.add_argument("--ld-flags", action="store_true")
+ output_group.add_argument("--ar-flags", action="store_true")
+ output_group.add_argument("--ld-libs", action="store_true")
+ output_group.add_argument("--objcopy", help="objcopy step for SUFFIX")
+ output_group.add_argument("--objcopy-flags",
+ help="objcopy flags for SUFFIX")
+ output_group.add_argument("--core-path", action="store_true")
+ output_group.add_argument("--cc-binary", action="store_true")
+ output_group.add_argument("--cxx-binary", action="store_true")
+ output_group.add_argument("--ar-binary", action="store_true")
+ output_group.add_argument("--objcopy-binary", action="store_true")
+ output_group.add_argument("--size-binary", action="store_true")
+ output_group.add_argument("--postbuild",
+ help="Show recipe.hooks.postbuild.*.pattern")
+ output_group.add_argument("--upload-tools", action="store_true")
+ output_group.add_argument("--upload-command")
+ output_group.add_argument("--library-includes", action="store_true")
+ output_group.add_argument("--library-c-files", action="store_true")
+ output_group.add_argument("--library-cpp-files", action="store_true")
+ output_group.add_argument("--core-c-files", action="store_true")
+ output_group.add_argument("--core-s-files", action="store_true")
+ output_group.add_argument("--core-cpp-files", action="store_true")
+ output_group.add_argument("--variant-c-files", action="store_true")
+ output_group.add_argument("--variant-s-files", action="store_true")
+ output_group.add_argument("--variant-cpp-files", action="store_true")
+
+ show_parser.set_defaults(func=show_command)
+
+ # run command
+ run_parser = subparsers.add_parser("run", help="Run Arduino recipes.")
+ add_common_options(run_parser, serial_port, build_path, build_project_name,
+ project_path, project_source_path)
+ run_parser.add_argument("--run-link",
+ nargs="+",
+ type=str,
+ help="Run the link command. Expected arguments: "
+ "the archive file followed by all obj files.")
+ run_parser.add_argument("--run-objcopy", action="store_true")
+ run_parser.add_argument("--run-prebuilds", action="store_true")
+ run_parser.add_argument("--run-postbuilds", action="store_true")
+ run_parser.add_argument("--run-upload-command")
+
+ run_parser.set_defaults(func=run_command)
+
+ args = parser.parse_args()
+
+ log.install()
+ log.set_level(args.loglevel)
+
+ _LOG.debug(_pretty_format(args))
+
+ # Check for and set alternate compiler path.
+ compiler_path_override = False
+ if args.compiler_path_override:
+ compiler_path_override = os.path.realpath(
+ os.path.expanduser(os.path.expandvars(
+ args.compiler_path_override)))
+
+ if args.subcommand == "install-core":
+ args.func(args)
+ elif args.subcommand in ["list-boards", "list-menu-options"]:
+ builder = ArduinoBuilder(args.arduino_package_path,
+ args.arduino_package_name)
+ builder.load_board_definitions()
+ args.func(args, builder)
+ else:
+ builder = ArduinoBuilder(args.arduino_package_path,
+ args.arduino_package_name,
+ build_path=args.build_path,
+ build_project_name=args.build_project_name,
+ project_path=args.project_path,
+ project_source_path=args.project_source_path,
+ compiler_path_override=compiler_path_override)
+ builder.load_board_definitions()
+ builder.select_board(args.board, args.menu_options)
+ args.func(args, builder)
+
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_arduino_build/py/pw_arduino_build/arduinobuilder.py b/pw_arduino_build/py/pw_arduino_build/arduinobuilder.py
deleted file mode 100755
index 239517d..0000000
--- a/pw_arduino_build/py/pw_arduino_build/arduinobuilder.py
+++ /dev/null
@@ -1,1894 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2020 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Extracts build information from Arduino cores."""
-
-import argparse
-import glob
-import hashlib
-import logging
-import os
-import platform
-import pprint
-import re
-import shutil
-import shlex
-import stat
-import sys
-import tarfile
-import time
-import urllib.request
-import zipfile
-from collections import OrderedDict
-from pathlib import Path
-from typing import List, Dict
-
-_LOG = logging.getLogger(__name__)
-_STDERR_HANDLER = logging.StreamHandler()
-
-_pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
-_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
-# yapf: disable
-_ARDUINO_CORE_ARTIFACTS = {
- # pylint: disable=line-too-long
- "teensy": {
- "Linux": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linux64.tar.xz",
- "file_name": "arduino-1.8.13-linux64.tar.xz",
- "sha256": "1b20d0ec850a2a63488009518725f058668bb6cb48c321f82dcf47dc4299b4ad",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux64",
- "file_name": "TeensyduinoInstall.linux64",
- "sha256": "2e6cd99a757bc80593ea3de006de4cc934bcb0a6ec74cad8ec327f0289d40f0b",
- },
- },
- # TODO(tonymd): Handle 32-bit Linux Install?
- "Linux32": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linux32.tar.xz",
- "file_name": "arduino-1.8.13-linux32.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux32",
- "file_name": "TeensyduinoInstall.linux32",
- "sha256": "",
- },
- },
- # TODO(tonymd): Handle ARM32 (Raspberry Pi) Install?
- "LinuxARM32": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxarm.tar.xz",
- "file_name": "arduino-1.8.13-linuxarm.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxarm",
- "file_name": "TeensyduinoInstall.linuxarm",
- "sha256": "",
- },
- },
- # TODO(tonymd): Handle ARM64 Install?
- "LinuxARM64": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxaarch64.tar.xz",
- "file_name": "arduino-1.8.13-linuxaarch64.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxaarch64",
- "file_name": "TeensyduinoInstall.linuxaarch64",
- "sha256": "",
- },
- },
- "Darwin": {
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/Teensyduino_MacOS_Catalina.zip",
- "file_name": "Teensyduino_MacOS_Catalina.zip",
- "sha256": "401ef42c6e83e621cdda20191a4ef9b7db8a214bede5a94a9e26b45f79c64fe2",
- },
- },
- "Windows": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-windows.zip",
- "file_name": "arduino-1.8.13-windows.zip",
- "sha256": "78d3e96827b9e9b31b43e516e601c38d670d29f12483e88cbf6d91a0f89ef524",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.exe",
- "file_name": "TeensyduinoInstall.exe",
- "sha256": "88f58681e5c4772c54e462bc88280320e4276e5b316dcab592fe38d96db990a1",
- },
- }
- },
- "adafruit-samd": {
- "all": {
- "core": {
- "url": "https://github.com/adafruit/ArduinoCore-samd/archive/1.6.2.tar.gz",
- "sha256": "5875f5bc05904c10e6313f02653f28f2f716db639d3d43f5a1d8a83d15339d64",
- }
- },
- "Linux": {},
- "Darwin": {},
- "Windows": {},
- },
- "stm32duino": {
- "all": {
- "core": {
- "url": "https://github.com/stm32duino/Arduino_Core_STM32/archive/1.9.0.tar.gz",
- "sha256": "4f75ba7a117d90392e8f67c58d31d22393749b9cdd3279bc21e7261ec06c62bf",
- }
- },
- "Linux": {},
- "Darwin": {},
- "Windows": {},
- },
-} # type: Dict[str, Dict]
-# yapf: enable
-
-
-def arduino_runtime_os_string():
- arduno_platform = {
- "Linux": "linux",
- "Windows": "windows",
- "Darwin": "macosx"
- }
- return arduno_platform[platform.system()]
-
-
-class FileOperations:
- """File helper functions."""
- @staticmethod
- def find_files(starting_dir: str,
- patterns: List[str],
- directories_only=False) -> List[str]:
- # ["**/*.S", "**/*.ino", "**/*.h", "**/*.c", "**/*.cpp"]
-
- original_working_dir = os.getcwd()
- if not (os.path.exists(starting_dir) and os.path.isdir(starting_dir)):
- _LOG.error("Directory '%s' does not exist.", starting_dir)
- raise FileNotFoundError
-
- os.chdir(starting_dir)
- files = []
- for pattern in patterns:
- for file_path in glob.glob(pattern, recursive=True):
- if not directories_only or (directories_only
- and os.path.isdir(file_path)):
- files.append(file_path)
- os.chdir(original_working_dir)
- return sorted(files)
-
- @staticmethod
- def sha256_sum(file_name):
- hash_sha256 = hashlib.sha256()
- with open(file_name, "rb") as file_handle:
- for chunk in iter(lambda: file_handle.read(4096), b""):
- hash_sha256.update(chunk)
- return hash_sha256.hexdigest()
-
- @staticmethod
- def md5_sum(file_name):
- hash_md5 = hashlib.md5()
- with open(file_name, "rb") as file_handle:
- for chunk in iter(lambda: file_handle.read(4096), b""):
- hash_md5.update(chunk)
- return hash_md5.hexdigest()
-
- @staticmethod
- def verify_file_checksum(file_path,
- expected_checksum,
- sum_function=sha256_sum):
- downloaded_checksum = sum_function(file_path)
- if downloaded_checksum != expected_checksum:
- _LOG.error("Error: Invalid %s", sum_function.__name__)
- _LOG.error("%s %s", downloaded_checksum,
- os.path.basename(file_path))
- _LOG.error("%s (expected)", expected_checksum)
- return sys.exit(1)
-
- _LOG.info(" %s:", sum_function.__name__)
- _LOG.info(" %s %s", downloaded_checksum, os.path.basename(file_path))
- return True
-
- @staticmethod
- def download_to_cache(url: str,
- expected_md5sum=None,
- expected_sha256sum=None,
- cache_directory=".cache") -> str:
-
- cache_dir = os.path.realpath(
- os.path.expanduser(os.path.expandvars(cache_directory)))
- downloaded_file = os.path.join(cache_dir, url.split("/")[-1])
-
- if not os.path.exists(downloaded_file):
- _LOG.info("Downloading: %s", url)
- urllib.request.urlretrieve(url, filename=downloaded_file)
-
- if os.path.exists(downloaded_file):
- _LOG.info("Downloaded: %s", downloaded_file)
- if expected_sha256sum:
- FileOperations.verify_file_checksum(
- downloaded_file,
- expected_sha256sum,
- sum_function=FileOperations.sha256_sum)
- elif expected_md5sum:
- FileOperations.verify_file_checksum(
- downloaded_file,
- expected_md5sum,
- sum_function=FileOperations.md5_sum)
-
- return downloaded_file
-
- @staticmethod
- def extract_zipfile(archive_file: str, dest_dir: str):
- with zipfile.ZipFile(archive_file) as archive:
- archive.extractall(path=dest_dir)
-
- @staticmethod
- def extract_tarfile(archive_file: str, dest_dir: str):
- with tarfile.open(archive_file, 'r') as archive:
- archive.extractall(path=dest_dir)
-
- @staticmethod
- def extract_archive(archive_file: str,
- dest_dir: str,
- cache_dir: str,
- remove_single_toplevel_folder=True):
- """Extract a tar or zip file.
-
- Args:
- archive_file (str): Absolute path to the archive file.
- dest_dir (str): Extraction destination directory.
- cache_dir (str): Directory where temp files can be created.
- remove_single_toplevel_folder (bool): If the archive contains only a
- single folder move the contents of that into the destination
- directory.
- """
- # Make a temporary directory to extract files into
- temp_extract_dir = os.path.join(cache_dir,
- "." + os.path.basename(archive_file))
- os.makedirs(temp_extract_dir, exist_ok=True)
-
- _LOG.info("Extracting: %s", archive_file)
- if zipfile.is_zipfile(archive_file):
- FileOperations.extract_zipfile(archive_file, temp_extract_dir)
- elif tarfile.is_tarfile(archive_file):
- FileOperations.extract_tarfile(archive_file, temp_extract_dir)
- else:
- _LOG.error("Unknown archive format: %s", archive_file)
- return sys.exit(1)
-
- _LOG.info("Installing into: %s", dest_dir)
- path_to_extracted_files = temp_extract_dir
-
- extracted_top_level_files = os.listdir(temp_extract_dir)
- # Check if tarfile has only one folder
- # If yes, make that the new path_to_extracted_files
- if remove_single_toplevel_folder and len(
- extracted_top_level_files) == 1:
- path_to_extracted_files = os.path.join(
- temp_extract_dir, extracted_top_level_files[0])
-
- # Move extracted files to dest_dir
- extracted_files = os.listdir(path_to_extracted_files)
- for file_name in extracted_files:
- source_file = os.path.join(path_to_extracted_files, file_name)
- dest_file = os.path.join(dest_dir, file_name)
- shutil.move(source_file, dest_file)
-
- # rm -rf temp_extract_dir
- shutil.rmtree(temp_extract_dir, ignore_errors=True)
-
- # Return List of extracted files
- return list(Path(dest_dir).rglob("*"))
-
- @staticmethod
- def remove_empty_directories(directory):
- """Recursively remove empty directories."""
-
- for path in sorted(Path(directory).rglob("*"), reverse=True):
- # If broken symlink
- if path.is_symlink() and not path.exists():
- path.unlink()
- # if empty directory
- elif path.is_dir() and len(os.listdir(path)) == 0:
- path.rmdir()
-
-
-class ArduinoCoreInstaller:
- """Simple Arduino core installer."""
- @staticmethod
- def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Windows."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
-
- arduino_artifact = teensy_artifacts["arduino-ide"]
- arduino_zipfile = FileOperations.download_to_cache(
- url=arduino_artifact["url"],
- expected_sha256sum=arduino_artifact["sha256"],
- cache_directory=cache_dir)
-
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_installer = FileOperations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir)
-
- FileOperations.extract_archive(arduino_zipfile, install_dir, cache_dir)
-
- # "teensy" here should match args.core_name
- teensy_core_dir = os.path.join(install_prefix, "teensy")
-
- # Change working directory for installation
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
-
- _LOG.info("Installing Teensyduino to: %s", teensy_core_dir)
-
- install_command = "{} \"--dir={}\"".format(teensyduino_installer,
- "teensy")
- _LOG.info(" Running: %s", install_command)
- _LOG.info(" Please click yes on the Windows 'User Account Control' "
- "dialog.")
- _LOG.info(" You should see: 'Verified publisher: PRJC.COM LLC'")
-
- os.system(install_command)
- if not os.path.exists(
- os.path.join(teensy_core_dir, "hardware", "teensy")):
- _LOG.error(
- "Error: Installation Failed.\n"
- "Please try again and ensure Teensyduino is installed in "
- "the folder:\n"
- "%s", teensy_core_dir)
- sys.exit(1)
- else:
- _LOG.info(" Install complete!")
-
- FileOperations.remove_empty_directories(install_dir)
- os.chdir(original_working_dir)
-
- @staticmethod
- def install_teensy_core_mac(unused_install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Mac."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
-
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_zip = FileOperations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir)
-
- extracted_files = FileOperations.extract_archive(
- teensyduino_zip,
- install_dir,
- cache_dir,
- remove_single_toplevel_folder=False)
- toplevel_folder = sorted(extracted_files)[0]
- os.symlink(os.path.join(toplevel_folder, "Contents", "Java",
- "hardware"),
- os.path.join(install_dir, "hardware"),
- target_is_directory=True)
-
- @staticmethod
- def install_teensy_core_linux(install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Windows."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
-
- arduino_artifact = teensy_artifacts["arduino-ide"]
- arduino_tarfile = FileOperations.download_to_cache(
- url=arduino_artifact["url"],
- expected_sha256sum=arduino_artifact["sha256"],
- cache_directory=cache_dir)
-
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_installer = FileOperations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir)
-
- extracted_files = FileOperations.extract_archive(
- arduino_tarfile, install_dir, cache_dir)
- os.chmod(teensyduino_installer,
- os.stat(teensyduino_installer).st_mode | stat.S_IEXEC)
-
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
- # "teensy" here should match args.core_name
- os.system("{} --dir={}".format(teensyduino_installer, "teensy"))
-
- # Remove original arduino IDE files
- for efile in extracted_files:
- if efile.is_file():
- efile.unlink()
-
- FileOperations.remove_empty_directories(install_dir)
- os.chdir(original_working_dir)
-
- @staticmethod
- def install_arduino_samd_core(install_prefix: str, install_dir: str,
- cache_dir: str):
- # TODO(tonymd): Fetch core/tools as specified by:
- # http://downloads.arduino.cc/packages/package_index.json
- pass
-
- @staticmethod
- def install_adafruit_samd_core(install_prefix: str, install_dir: str,
- cache_dir: str):
- artifacts = _ARDUINO_CORE_ARTIFACTS["adafruit-samd"]["all"]["core"]
- core_tarfile = FileOperations.download_to_cache(
- url=artifacts["url"],
- expected_sha256sum=artifacts["sha256"],
- cache_directory=cache_dir)
-
- package_path = os.path.join(
- install_dir, "hardware", "samd",
- os.path.basename(core_tarfile).replace(".tar.gz", ""))
- os.makedirs(package_path, exist_ok=True)
- FileOperations.extract_archive(core_tarfile, package_path, cache_dir)
-
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
- # TODO(tonymd): Fetch platform specific tools as specified by:
- # https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
- # Specifically:
- # https://github.com/ARM-software/CMSIS_5/archive/5.4.0.tar.gz
- os.chdir(original_working_dir)
- return True
-
- @staticmethod
- def install_stm32duino_core(install_prefix, install_dir, cache_dir):
- artifacts = _ARDUINO_CORE_ARTIFACTS["stm32duino"]["all"]["core"]
- core_tarfile = FileOperations.download_to_cache(
- url=artifacts["url"],
- expected_sha256sum=artifacts["sha256"],
- cache_directory=cache_dir)
-
- package_path = os.path.join(
- install_dir, "hardware", "stm32",
- os.path.basename(core_tarfile).replace(".tar.gz", ""))
- os.makedirs(package_path, exist_ok=True)
- FileOperations.extract_archive(core_tarfile, package_path, cache_dir)
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
- # TODO(tonymd): Fetch platform specific tools as specified by:
- # https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json
- os.chdir(original_working_dir)
- return True
-
-
-class ArduinoBuilder:
- """Used to interpret arduino boards.txt and platform.txt files."""
- # pylint: disable=too-many-instance-attributes,too-many-public-methods
-
- board_menu_regex = re.compile(
- r"^(?P<name>menu\.[^#=]+)=(?P<description>.*)$", re.MULTILINE)
-
- board_name_regex = re.compile(
- r"^(?P<name>[^\s#\.]+)\.name=(?P<description>.*)$", re.MULTILINE)
-
- variable_regex = re.compile(r"^(?P<name>[^\s#=]+)=(?P<value>.*)$",
- re.MULTILINE)
-
- menu_option_regex = re.compile(
- r"^menu\." # starts with "menu"
- r"(?P<menu_option_name>[^.]+)\." # first token after .
- r"(?P<menu_option_value>[^.]+)$") # second (final) token after .
-
- tool_name_regex = re.compile(
- r"^tools\." # starts with "tools"
- r"(?P<tool_name>[^.]+)\.") # first token after .
-
- interpolated_variable_regex = re.compile(r"{[^}]+}", re.MULTILINE)
-
- objcopy_step_name_regex = re.compile(r"^recipe.objcopy.([^.]+).pattern$")
-
- def __init__(self,
- arduino_path,
- package_name,
- build_path=None,
- project_path=None,
- project_source_path=None,
- build_project_name=None,
- compiler_path_override=False):
- self.arduino_path = arduino_path
- self.arduino_package_name = package_name
- self.selected_board = None
- self.build_path = build_path
- self.project_path = project_path
- self.project_source_path = project_source_path
- self.build_project_name = build_project_name
- self.compiler_path_override = compiler_path_override
- self.variant_includes = ""
- self.build_variant_path = False
-
- self.compiler_path_override_binaries = []
- if self.compiler_path_override:
- self.compiler_path_override_binaries = FileOperations.find_files(
- self.compiler_path_override, "*")
-
- # Container dicts for boards.txt and platform.txt file data.
- self.board = OrderedDict()
- self.platform = OrderedDict()
- self.menu_options = OrderedDict({
- "global_options": {},
- "default_board_values": {},
- "selected": {}
- })
- self.tools_variables = {}
-
- # Set and check for valid hardware folder.
- self.hardware_path = os.path.join(self.arduino_path, "hardware")
-
- if not os.path.exists(self.hardware_path):
- _LOG.error("Error: Arduino package path '%s' does not exist.",
- self.arduino_path)
- raise FileNotFoundError
-
- # Set and check for valid package name
- self.package_path = os.path.join(self.arduino_path, "hardware",
- package_name)
- # {build.arch} is the first folder name of the package (upcased)
- self.build_arch = os.path.split(package_name)[0].upper()
-
- if not os.path.exists(self.package_path):
- _LOG.error("Error: Arduino package name '%s' does not exist.",
- package_name)
- _LOG.error("Did you mean:\n")
- # TODO(tonymd): On Windows concatenating "/" may not work
- possible_alternatives = [
- d.replace(self.hardware_path + os.sep, "", 1)
- for d in glob.glob(self.hardware_path + "/*/*")
- ]
- _LOG.error("\n".join(possible_alternatives))
- sys.exit(1)
-
- # Grab all folder names in the cores directory. These are typically
- # sub-core source files.
- self.sub_core_folders = os.listdir(
- os.path.join(self.package_path, "cores"))
-
- self._find_tools_variables()
-
- self.boards_txt = os.path.join(self.package_path, "boards.txt")
- self.platform_txt = os.path.join(self.package_path, "platform.txt")
-
- def select_board(self, board_name, menu_option_overrides=False):
- self.selected_board = board_name
-
- # Load default menu options for a selected board.
- if not self.selected_board in self.board.keys():
- _LOG.error("Error board: '%s' not supported.", self.selected_board)
- # TODO(tonymd): Print supported boards here
- sys.exit(1)
-
- # Override default menu options if any are specified.
- if menu_option_overrides:
- for moption in menu_option_overrides:
- if not self.set_menu_option(moption):
- # TODO(tonymd): Print supported menu options here
- sys.exit(1)
-
- self._copy_default_menu_options_to_build_variables()
- self._apply_recipe_overrides()
- self._substitute_variables()
-
- def _apply_recipe_overrides(self):
- # Override link recipes with per-core exceptions
- # Teensyduino cores
- if self.build_arch == 'TEENSY':
- # Change {build.path}/{archive_file}
- # To {archive_file_path} (which should contain the core.a file)
- new_link_line = self.platform["recipe.c.combine.pattern"].replace(
- "{object_files} \"{build.path}/{archive_file}\"",
- "{object_files} {archive_file_path}", 1)
- # Add the teensy provided toolchain lib folder for link access to
- # libarm_cortexM*_math.a
- new_link_line = new_link_line.replace(
- "\"-L{build.path}\"",
- "\"-L{build.path}\" -L{compiler.path}/arm/arm-none-eabi/lib",
- 1)
- self.platform["recipe.c.combine.pattern"] = new_link_line
-
- # Adafruit-samd core
- # TODO(tonymd): This build_arch may clash with Arduino-SAMD core
- elif self.build_arch == 'SAMD':
- new_link_line = self.platform["recipe.c.combine.pattern"].replace(
- "\"{build.path}/{archive_file}\" -Wl,--end-group",
- "{archive_file_path} -Wl,--end-group", 1)
- self.platform["recipe.c.combine.pattern"] = new_link_line
-
- # STM32L4 Core:
- # https://github.com/GrumpyOldPizza/arduino-STM32L4
- elif self.build_arch == 'STM32L4':
- # TODO(tonymd): {build.path}/{archive_file} for the link step always
- # seems to be core.a (except STM32 core)
- line_to_delete = "-Wl,--start-group \"{build.path}/{archive_file}\""
- new_link_line = self.platform["recipe.c.combine.pattern"].replace(
- line_to_delete, "-Wl,--start-group {archive_file_path}", 1)
- self.platform["recipe.c.combine.pattern"] = new_link_line
-
- # stm32duino core
- elif self.build_arch == 'STM32':
- pass
-
- def _copy_default_menu_options_to_build_variables(self):
- # Clear existing options
- self.menu_options["selected"] = {}
- # Set default menu options for selected board
- for menu_key, menu_dict in self.menu_options["default_board_values"][
- self.selected_board].items():
- for name, var in self.board[self.selected_board].items():
- starting_key = "{}.{}.".format(menu_key, menu_dict["name"])
- if name.startswith(starting_key):
- new_var_name = name.replace(starting_key, "", 1)
- self.menu_options["selected"][new_var_name] = var
-
- def set_menu_option(self, moption):
- if moption not in self.board[self.selected_board]:
- _LOG.error("Error: '%s' is not a valid menu option.", moption)
- return False
-
- # Override default menu option with new value.
- menu_match_result = self.menu_option_regex.match(moption)
- if menu_match_result:
- menu_match = menu_match_result.groupdict()
- menu_value = menu_match["menu_option_value"]
- menu_key = "menu.{}".format(menu_match["menu_option_name"])
- self.menu_options["default_board_values"][
- self.selected_board][menu_key]["name"] = menu_value
-
- # Update build variables
- self._copy_default_menu_options_to_build_variables()
- return True
-
- def _set_global_arduino_variables(self):
- """Set some global variables defined by the Arduino-IDE.
-
- See Docs:
- https://arduino.github.io/arduino-cli/platform-specification/#global-predefined-properties
- """
-
- for current_board_name in self.board.keys():
- if self.build_path:
- self.board[current_board_name]["build.path"] = self.build_path
- if self.build_project_name:
- self.board[current_board_name][
- "build.project_name"] = self.build_project_name
- # {archive_file} is the final *.elf
- archive_file = "{}.elf".format(self.build_project_name)
- self.board[current_board_name]["archive_file"] = archive_file
- # {archive_file_path} is the final core.a archive
- if self.build_path:
- self.board[current_board_name][
- "archive_file_path"] = os.path.join(
- self.build_path, "core.a")
- if self.project_source_path:
- self.board[current_board_name][
- "build.source.path"] = self.project_source_path
-
- self.board[current_board_name]["extra.time.local"] = str(
- int(time.time()))
- self.board[current_board_name]["runtime.ide.version"] = "10812"
- self.board[current_board_name][
- "runtime.hardware.path"] = self.hardware_path
-
- # Copy {runtime.tools.TOOL_NAME.path} vars
- self._set_tools_variables(self.board[current_board_name])
-
- self.board[current_board_name][
- "runtime.platform.path"] = self.package_path
- if self.platform["name"] == "Teensyduino":
- # Teensyduino is installed into the arduino IDE folder
- # rather than ~/.arduino15/packages/
- self.board[current_board_name][
- "runtime.hardware.path"] = os.path.join(
- self.hardware_path, "teensy")
-
- self.board[current_board_name]["build.system.path"] = os.path.join(
- self.package_path, "system")
-
- # Set the {build.core.path} variable that pointing to a sub-core
- # folder. For Teensys this is:
- # 'teensy/hardware/teensy/avr/cores/teensy{3,4}'. For other cores
- # it's typically just the 'arduino' folder. For example:
- # 'arduino-samd/hardware/samd/1.8.8/cores/arduino'
- core_path = os.path.join(
- self.package_path, "cores",
- self.board[current_board_name].get("build.core",
- self.sub_core_folders[0]))
- self.board[current_board_name]["build.core.path"] = core_path
-
- self.board[current_board_name]["build.arch"] = self.build_arch
-
- for name, var in self.board[current_board_name].items():
- self.board[current_board_name][name] = var.replace(
- "{build.core.path}", core_path)
-
- def load_board_definitions(self):
- """Loads Arduino boards.txt and platform.txt files into dictionaries.
-
- Populates the following dictionaries:
- self.menu_options
- self.boards
- self.platform
- """
- # Load platform.txt
- with open(self.platform_txt, "r") as pfile:
- platform_file = pfile.read()
- platform_var_matches = self.variable_regex.finditer(platform_file)
- for p_match in [m.groupdict() for m in platform_var_matches]:
- self.platform[p_match["name"]] = p_match["value"]
-
- # Load boards.txt
- with open(self.boards_txt, "r") as bfile:
- board_file = bfile.read()
- # Get all top-level menu options, e.g. menu.usb=USB Type
- board_menu_matches = self.board_menu_regex.finditer(board_file)
- for menuitem in [m.groupdict() for m in board_menu_matches]:
- self.menu_options["global_options"][menuitem["name"]] = {
- "description": menuitem["description"]
- }
-
- # Get all board names, e.g. teensy40.name=Teensy 4.0
- board_name_matches = self.board_name_regex.finditer(board_file)
- for b_match in [m.groupdict() for m in board_name_matches]:
- self.board[b_match["name"]] = OrderedDict()
- self.menu_options["default_board_values"][
- b_match["name"]] = OrderedDict()
-
- # Get all board variables, e.g. teensy40.*
- for current_board_name in self.board.keys():
- board_line_matches = re.finditer(
- fr"^\s*{current_board_name}\."
- fr"(?P<key>[^#=]+)=(?P<value>.*)$", board_file,
- re.MULTILINE)
- for b_match in [m.groupdict() for m in board_line_matches]:
- # Check if this line is a menu option
- # (e.g. 'menu.usb.serial') and save as default if it's the
- # first one seen.
- ArduinoBuilder.save_default_menu_option(
- current_board_name, b_match["key"], b_match["value"],
- self.menu_options)
- self.board[current_board_name][
- b_match["key"]] = b_match["value"].strip()
-
- self._set_global_arduino_variables()
-
- @staticmethod
- def save_default_menu_option(current_board_name, key, value, menu_options):
- """Save a given menu option as the default.
-
- Saves the key and value into menu_options["default_board_values"]
- if it doesn't already exist. Assumes menu options are added in the order
- specified in boards.txt. The first value for a menu key is the default.
- """
- # Check if key is a menu option
- # e.g. menu.usb.serial
- # menu.usb.serial.build.usbtype
- menu_match_result = re.match(
- r'^menu\.' # starts with "menu"
- r'(?P<menu_option_name>[^.]+)\.' # first token after .
- r'(?P<menu_option_value>[^.]+)' # second token after .
- r'(\.(?P<rest>.+))?', # optionally any trailing tokens after a .
- key)
- if menu_match_result:
- menu_match = menu_match_result.groupdict()
- current_menu_key = "menu.{}".format(menu_match["menu_option_name"])
- # If this is the first menu option seen for current_board_name, save
- # as the default.
- if current_menu_key not in menu_options["default_board_values"][
- current_board_name]:
- menu_options["default_board_values"][current_board_name][
- current_menu_key] = {
- "name": menu_match["menu_option_value"],
- "description": value
- }
-
- def _replace_variables(self, line, variable_lookup_source):
- """Replace {variables} from loaded boards.txt or platform.txt.
-
- Replace interpolated variables surrounded by curly braces in line with
- definitions from variable_lookup_source.
- """
- new_line = line
- for current_var_match in self.interpolated_variable_regex.findall(
- line):
- # {build.flags.c} --> build.flags.c
- current_var = current_var_match.strip("{}")
-
- # check for matches in board definition
- if current_var in variable_lookup_source:
- replacement = variable_lookup_source.get(current_var, "")
- new_line = new_line.replace(current_var_match, replacement)
- return new_line
-
- def _find_tools_variables(self):
- # Gather tool directories in order of increasing precedence
- runtime_tool_paths = []
-
- # Check for tools installed in ~/.arduino15/packages/arduino/tools/
- # TODO(tonymd): Is this Mac & Linux specific?
- runtime_tool_paths += glob.glob(
- os.path.join(
- os.path.realpath(os.path.expanduser(os.path.expandvars("~"))),
- ".arduino15", "packages", "arduino", "tools", "*"))
-
- # <ARDUINO_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
- runtime_tool_paths += glob.glob(
- os.path.join(self.arduino_path, "tools",
- arduino_runtime_os_string(), "*"))
- # <ARDUINO_PATH>/tools/<TOOL_NAMES>
- # This will grab linux/windows/macosx/share as <TOOL_NAMES>.
- runtime_tool_paths += glob.glob(
- os.path.join(self.arduino_path, "tools", "*"))
-
- # Process package tools after arduino tools.
- # They should overwrite vars & take precedence.
-
- # <PACKAGE_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
- runtime_tool_paths += glob.glob(
- os.path.join(self.package_path, "tools",
- arduino_runtime_os_string(), "*"))
- # <PACKAGE_PATH>/tools/<TOOL_NAMES>
- # This will grab linux/windows/macosx/share as <TOOL_NAMES>.
- runtime_tool_paths += glob.glob(
- os.path.join(self.package_path, "tools", "*"))
-
- for path in runtime_tool_paths:
- # Make sure TOOL_NAME is not an OS string
- if not (path.endswith("linux") or path.endswith("windows")
- or path.endswith("macosx") or path.endswith("share")):
- # TODO(tonymd): Check if a file & do nothing?
-
- # Check if it's a directory with subdir(s) as a version string
- # create all 'runtime.tools.{tool_folder}-{version.path}'
- # (for each version)
- # create 'runtime.tools.{tool_folder}.path'
- # (with latest version)
- if os.path.isdir(path):
- # Grab the tool name (folder) by itself.
- tool_folder = os.path.basename(path)
- # Sort so that [-1] is the latest version.
- version_paths = sorted(glob.glob(os.path.join(path, "*")))
- # Check if all sub folders start with a version string.
- if len(version_paths) == sum(
- bool(re.match(r"^[0-9.]+", os.path.basename(vp)))
- for vp in version_paths):
- for version_path in version_paths:
- version_string = os.path.basename(version_path)
- var_name = "runtime.tools.{}-{}.path".format(
- tool_folder, version_string)
- self.tools_variables[var_name] = os.path.join(
- path, version_string)
- var_name = "runtime.tools.{}.path".format(tool_folder)
- self.tools_variables[var_name] = os.path.join(
- path, os.path.basename(version_paths[-1]))
- # Else set toolpath to path.
- else:
- var_name = "runtime.tools.{}.path".format(tool_folder)
- self.tools_variables[var_name] = path
-
- _LOG.debug("TOOL VARIABLES: %s", _pretty_format(self.tools_variables))
-
- # Copy self.tools_variables into destination
- def _set_tools_variables(self, destination):
- for key, value in self.tools_variables.items():
- destination[key] = value
-
- def _substitute_variables(self):
- """Perform variable substitution in board and platform metadata."""
-
- # menu -> board
- # Copy selected menu variables into board definiton
- for name, value in self.menu_options["selected"].items():
- self.board[self.selected_board][name] = value
-
- # board -> board
- # Replace any {vars} in the selected board with values defined within
- # (and from copied in menu options).
- for var, value in self.board[self.selected_board].items():
- self.board[self.selected_board][var] = self._replace_variables(
- value, self.board[self.selected_board])
-
- # Check for build.variant variable
- # This will be set in selected board after menu options substitution
- build_variant = self.board[self.selected_board].get(
- "build.variant", None)
- if build_variant:
- # Set build.variant.path
- bvp = os.path.join(self.package_path, "variants", build_variant)
- self.build_variant_path = bvp
- self.board[self.selected_board]["build.variant.path"] = bvp
- # Add the variant folder as an include directory
- # (used in stm32l4 core)
- self.variant_includes = "-I{}".format(bvp)
-
- _LOG.debug("PLATFORM INITIAL: %s", _pretty_format(self.platform))
-
- # board -> platform
- # Replace {vars} in platform from the selected board definition
- for var, value in self.platform.items():
- self.platform[var] = self._replace_variables(
- value, self.board[self.selected_board])
-
- # platform -> platform
- # Replace any remaining {vars} in platform from platform
- for var, value in self.platform.items():
- self.platform[var] = self._replace_variables(value, self.platform)
-
- # Repeat platform -> platform for any lingering variables
- # Example: {build.opt.name} in STM32 core
- for var, value in self.platform.items():
- self.platform[var] = self._replace_variables(value, self.platform)
-
- _LOG.debug("MENU_OPTIONS: %s", _pretty_format(self.menu_options))
- _LOG.debug("SELECTED_BOARD: %s",
- _pretty_format(self.board[self.selected_board]))
- _LOG.debug("PLATFORM: %s", _pretty_format(self.platform))
-
- def selected_board_spec(self):
- return self.board[self.selected_board]
-
- def get_menu_options(self):
- all_options = []
- max_string_length = [0, 0]
-
- for key_name, description in self.board[self.selected_board].items():
- menu_match_result = self.menu_option_regex.match(key_name)
- if menu_match_result:
- menu_match = menu_match_result.groupdict()
- name = "menu.{}.{}".format(menu_match["menu_option_name"],
- menu_match["menu_option_value"])
- if len(name) > max_string_length[0]:
- max_string_length[0] = len(name)
- if len(description) > max_string_length[1]:
- max_string_length[1] = len(description)
- all_options.append((name, description))
-
- return all_options, max_string_length
-
- def get_default_menu_options(self):
- default_options = []
- max_string_length = [0, 0]
-
- for key_name, value in self.menu_options["default_board_values"][
- self.selected_board].items():
- full_key = key_name + "." + value["name"]
- if len(full_key) > max_string_length[0]:
- max_string_length[0] = len(full_key)
- if len(value["description"]) > max_string_length[1]:
- max_string_length[1] = len(value["description"])
- default_options.append((full_key, value["description"]))
-
- return default_options, max_string_length
-
- @staticmethod
- def split_binary_from_arguments(compile_line):
- compile_binary = None
- rest_of_line = compile_line
-
- compile_binary_match = re.search(r'^("[^"]+") ', compile_line)
- if compile_binary_match:
- compile_binary = compile_binary_match[1]
- rest_of_line = compile_line.replace(compile_binary_match[0], "", 1)
-
- return compile_binary, rest_of_line
-
- def _strip_includes_source_file_object_file_vars(self, compile_line):
- line = compile_line
- if self.variant_includes:
- line = compile_line.replace(
- "{includes} \"{source_file}\" -o \"{object_file}\"",
- self.variant_includes, 1)
- else:
- line = compile_line.replace(
- "{includes} \"{source_file}\" -o \"{object_file}\"", "", 1)
- return line
-
- def _get_tool_name(self, line):
- tool_match_result = self.tool_name_regex.match(line)
- if tool_match_result:
- return tool_match_result[1]
- return False
-
- def get_upload_tool_names(self):
- return [
- self._get_tool_name(t) for t in self.platform.keys()
- if self.tool_name_regex.match(t) and 'upload.pattern' in t
- ]
-
- # TODO(tonymd): Use these getters in _replace_variables() or
- # _substitute_variables()
-
- def _get_platform_variable(self, variable):
- # TODO(tonymd): Check for '.macos' '.linux' '.windows' in variable name,
- # compare with platform.system() and return that instead.
- return self.platform.get(variable, False)
-
- def _get_platform_variable_with_substitutions(self, variable, namespace):
- line = self.platform.get(variable, False)
- # Get all unique variables used in this line in line.
- unique_vars = sorted(
- set(self.interpolated_variable_regex.findall(line)))
- # Search for each unique_vars in namespace and global.
- for var in unique_vars:
- v_raw_name = var.strip("{}")
-
- # Check for namespace.variable
- # eg: 'tools.stm32CubeProg.cmd'
- possible_var_name = "{}.{}".format(namespace, v_raw_name)
- result = self._get_platform_variable(possible_var_name)
- # Check for os overriden variable
- # eg:
- # ('tools.stm32CubeProg.cmd', 'stm32CubeProg.sh'),
- # ('tools.stm32CubeProg.cmd.windows', 'stm32CubeProg.bat'),
- possible_var_name = "{}.{}.{}".format(namespace, v_raw_name,
- arduino_runtime_os_string())
- os_override_result = self._get_platform_variable(possible_var_name)
-
- if os_override_result:
- line = line.replace(var, os_override_result)
- elif result:
- line = line.replace(var, result)
- # Check for variable at top level?
- # elif self._get_platform_variable(v_raw_name):
- # line = line.replace(self._get_platform_variable(v_raw_name),
- # result)
- return line
-
- def get_upload_line(self, tool_name, serial_port=False):
- # TODO(tonymd): Error if tool_name does not exist
- tool_namespace = "tools.{}".format(tool_name)
- pattern = "tools.{}.upload.pattern".format(tool_name)
-
- if not self._get_platform_variable(pattern):
- _LOG.error("Error: upload tool '%s' does not exist.", tool_name)
- tools = self.get_upload_tool_names()
- _LOG.error("Valid tools: %s", ", ".join(tools))
- return sys.exit(1)
-
- line = self._get_platform_variable_with_substitutions(
- pattern, tool_namespace)
-
- # TODO(tonymd): Teensy specific tool overrides.
- if tool_name == "teensyloader":
- # Remove un-necessary lines
- # {serial.port.label} and {serial.port.protocol} are returned by
- # the teensy_ports binary.
- line = line.replace("\"-portlabel={serial.port.label}\"", "", 1)
- line = line.replace("\"-portprotocol={serial.port.protocol}\"", "",
- 1)
-
- if serial_port:
- line = line.replace("{serial.port}", serial_port, 1)
-
- return line
-
- def _get_binary_path(self, variable_pattern):
- compile_line = self.replace_compile_binary_with_override_path(
- self._get_platform_variable(variable_pattern))
- compile_binary, _ = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- return compile_binary
-
- def get_cc_binary(self):
- return self._get_binary_path("recipe.c.o.pattern")
-
- def get_cxx_binary(self):
- return self._get_binary_path("recipe.cpp.o.pattern")
-
- def get_objcopy_binary(self):
- objcopy_step_name = self.get_objcopy_step_names()[0]
- objcopy_binary = self._get_binary_path(objcopy_step_name)
- return objcopy_binary
-
- def get_ar_binary(self):
- return self._get_binary_path("recipe.ar.pattern")
-
- def get_size_binary(self):
- return self._get_binary_path("recipe.size.pattern")
-
- def replace_command_args_with_compiler_override_path(self, compile_line):
- if not self.compiler_path_override:
- return compile_line
- replacement_line = compile_line
- replacement_line_args = compile_line.split()
- for arg in replacement_line_args:
- compile_binary_basename = os.path.basename(arg.strip("\""))
- if compile_binary_basename in self.compiler_path_override_binaries:
- new_compiler = os.path.join(self.compiler_path_override,
- compile_binary_basename)
- replacement_line = replacement_line.replace(
- arg, new_compiler, 1)
- return replacement_line
-
- def replace_compile_binary_with_override_path(self, compile_line):
- replacement_compile_line = compile_line
-
- # Change the compiler path if there's an override path set
- if self.compiler_path_override:
- compile_binary, line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- compile_binary_basename = os.path.basename(
- compile_binary.strip("\""))
- new_compiler = os.path.join(self.compiler_path_override,
- compile_binary_basename)
- if platform.system() == "Windows" and not re.match(
- r".*\.exe$", new_compiler, flags=re.IGNORECASE):
- new_compiler += ".exe"
-
- if os.path.isfile(new_compiler):
- replacement_compile_line = "\"{}\" {}".format(
- new_compiler, line)
-
- return replacement_compile_line
-
- def get_c_compile_line(self):
- _LOG.debug("ARDUINO_C_COMPILE: %s",
- _pretty_format(self.platform["recipe.c.o.pattern"]))
-
- compile_line = self.platform["recipe.c.o.pattern"]
- compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
- compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
-
- compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
- return compile_line
-
- def get_s_compile_line(self):
- _LOG.debug("ARDUINO_S_COMPILE %s",
- _pretty_format(self.platform["recipe.S.o.pattern"]))
-
- compile_line = self.platform["recipe.S.o.pattern"]
- compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
- compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
-
- compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
- return compile_line
-
- def get_ar_compile_line(self):
- _LOG.debug("ARDUINO_AR_COMPILE: %s",
- _pretty_format(self.platform["recipe.ar.pattern"]))
-
- compile_line = self.platform["recipe.ar.pattern"].replace(
- "\"{object_file}\"", "", 1)
-
- compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
- return compile_line
-
- def get_cpp_compile_line(self):
- _LOG.debug("ARDUINO_CPP_COMPILE: %s",
- _pretty_format(self.platform["recipe.cpp.o.pattern"]))
-
- compile_line = self.platform["recipe.cpp.o.pattern"]
- compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
- compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
-
- compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
- return compile_line
-
- def get_link_line(self):
- _LOG.debug("ARDUINO_LINK: %s",
- _pretty_format(self.platform["recipe.c.combine.pattern"]))
-
- compile_line = self.platform["recipe.c.combine.pattern"]
-
- compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
- return compile_line
-
- def get_objcopy_step_names(self):
- names = [
- name for name, line in self.platform.items()
- if self.objcopy_step_name_regex.match(name)
- ]
- return names
-
- def get_objcopy_steps(self) -> List[str]:
- lines = [
- line for name, line in self.platform.items()
- if self.objcopy_step_name_regex.match(name)
- ]
- lines = [
- self.replace_compile_binary_with_override_path(line)
- for line in lines
- ]
- return lines
-
- # TODO(tonymd): These recipes are probably run in sorted order
- def get_objcopy(self, suffix):
- # Expected vars:
- # teensy:
- # recipe.objcopy.eep.pattern
- # recipe.objcopy.hex.pattern
-
- pattern = "recipe.objcopy.{}.pattern".format(suffix)
- objcopy_step_names = self.get_objcopy_step_names()
-
- objcopy_suffixes = [
- m[1] for m in [
- self.objcopy_step_name_regex.match(line)
- for line in objcopy_step_names
- ] if m
- ]
- if pattern not in objcopy_step_names:
- _LOG.error("Error: objcopy suffix '%s' does not exist.", suffix)
- _LOG.error("Valid suffixes: %s", ", ".join(objcopy_suffixes))
- return sys.exit(1)
-
- line = self._get_platform_variable(pattern)
-
- _LOG.debug("ARDUINO_OBJCOPY_%s: %s", suffix, line)
-
- line = self.replace_compile_binary_with_override_path(line)
-
- return line
-
- def get_objcopy_flags(self, suffix):
- # TODO(tonymd): Possibly teensy specific variables.
- flags = ""
- if suffix == "hex":
- flags = self.platform.get("compiler.elf2hex.flags", "")
- elif suffix == "bin":
- flags = self.platform.get("compiler.elf2bin.flags", "")
- elif suffix == "eep":
- flags = self.platform.get("compiler.objcopy.eep.flags", "")
- return flags
-
- # TODO(tonymd): There are more recipe hooks besides postbuild.
- # They are run in sorted order.
- # TODO(tonymd): Rename this to get_hooks(hook_name, step).
- # TODO(tonymd): Add a list-hooks and or run-hooks command
- def get_postbuild_line(self, step_number):
- line = self.platform["recipe.hooks.postbuild.{}.pattern".format(
- step_number)]
- line = self.replace_command_args_with_compiler_override_path(line)
- return line
-
- def get_prebuild_steps(self) -> List[str]:
- # Teensy core uses recipe.hooks.sketch.prebuild.1.pattern
- # stm32 core uses recipe.hooks.prebuild.1.pattern
- # TODO(tonymd): STM32 core uses recipe.hooks.prebuild.1.pattern.windows
- # (should override non-windows key)
- lines = [
- line for name, line in self.platform.items() if re.match(
- r"^recipe.hooks.(?:sketch.)?prebuild.[^.]+.pattern$", name)
- ]
- # TODO(tonymd): Write a function to fetch/replace OS specific patterns
- # (ending in an OS string)
- lines = [
- self.replace_compile_binary_with_override_path(line)
- for line in lines
- ]
- return lines
-
- def get_postbuild_steps(self) -> List[str]:
- lines = [
- line for name, line in self.platform.items()
- if re.match(r"^recipe.hooks.postbuild.[^.]+.pattern$", name)
- ]
-
- lines = [
- self.replace_command_args_with_compiler_override_path(line)
- for line in lines
- ]
- return lines
-
- def get_s_flags(self):
- compile_line = self.get_s_compile_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- compile_line = compile_line.replace("-c", "", 1)
- return compile_line.strip()
-
- def get_c_flags(self):
- compile_line = self.get_c_compile_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- compile_line = compile_line.replace("-c", "", 1)
- return compile_line.strip()
-
- def get_cpp_flags(self):
- compile_line = self.get_cpp_compile_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- compile_line = compile_line.replace("-c", "", 1)
- return compile_line.strip()
-
- def get_ar_flags(self):
- compile_line = self.get_ar_compile_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- return compile_line.strip()
-
- def get_ld_flags(self):
- compile_line = self.get_link_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
-
- # TODO(tonymd): This is teensy specific
- line_to_delete = "-o \"{build.path}/{build.project_name}.elf\" " \
- "{object_files} \"-L{build.path}\""
- if self.build_path:
- line_to_delete = line_to_delete.replace("{build.path}",
- self.build_path)
- if self.build_project_name:
- line_to_delete = line_to_delete.replace("{build.project_name}",
- self.build_project_name)
-
- compile_line = compile_line.replace(line_to_delete, "", 1)
- libs = re.findall(r'(-l[^ ]+ ?)', compile_line)
- for lib in libs:
- compile_line = compile_line.replace(lib, "", 1)
- libs = [lib.strip() for lib in libs]
-
- return compile_line.strip()
-
- def get_ld_libs(self):
- compile_line = self.get_link_line()
- _, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
- # TODO(tonymd): This replacement is teensy specific
- compile_line = compile_line.replace(
- "-o \"{build.path}/{build.project_name}.elf\" "
- "{object_files} \"-L{build.path}\"", "", 1)
- libs = re.findall(r'(-l[^ ]+ ?)', compile_line)
- for lib in libs:
- compile_line = compile_line.replace(lib, "", 1)
- libs = [lib.strip() for lib in libs]
-
- return " ".join(libs)
-
- def library_folders(self):
- # Arduino library format documentation:
- # https://arduino.github.io/arduino-cli/library-specification/#layout-of-folders-and-files
- # - If src folder exists,
- # use that as the root include directory -Ilibraries/libname/src
- # - Else lib folder as root include -Ilibraries/libname
- # (exclude source files in the examples folder in this case)
-
- library_path = os.path.join(self.project_path, "libraries")
-
- library_folders = FileOperations.find_files(library_path, ["*"],
- directories_only=True)
- library_source_root_folders = []
- for lib in library_folders:
- lib_dir = os.path.join(library_path, lib)
- src_dir = os.path.join(lib_dir, "src")
- if os.path.exists(src_dir) and os.path.isdir(src_dir):
- library_source_root_folders.append(src_dir)
- else:
- library_source_root_folders.append(lib_dir)
-
- return library_source_root_folders
-
- def library_includes(self):
- include_args = []
- library_folders = self.library_folders()
- for lib_dir in library_folders:
- include_args.append("-I{}".format(os.path.relpath(lib_dir)))
- return include_args
-
- def library_files(self, pattern):
- sources = []
- library_folders = self.library_folders()
- for lib_dir in library_folders:
- for file_path in FileOperations.find_files(lib_dir, [pattern]):
- if not file_path.startswith("examples"):
- sources.append(
- os.path.relpath(os.path.join(lib_dir, file_path)))
- return sources
-
- def library_c_files(self):
- return self.library_files("**/*.c")
-
- def library_cpp_files(self):
- return self.library_files("**/*.cpp")
-
- def get_core_path(self):
- return self.board[self.selected_board]["build.core.path"]
-
- def core_files(self, pattern):
- sources = []
- for file_path in FileOperations.find_files(self.get_core_path(),
- [pattern]):
- sources.append(os.path.join(self.get_core_path(), file_path))
- return sources
-
- def core_c_files(self):
- return self.core_files("**/*.c")
-
- def core_s_files(self):
- return self.core_files("**/*.S")
-
- def core_cpp_files(self):
- return self.core_files("**/*.cpp")
-
- def get_variant_path(self):
- return self.build_variant_path
-
- def variant_files(self, pattern):
- sources = []
- if self.build_variant_path:
- for file_path in FileOperations.find_files(self.get_variant_path(),
- [pattern]):
- sources.append(os.path.join(self.get_variant_path(),
- file_path))
- return sources
-
- def variant_c_files(self):
- return self.variant_files("**/*.c")
-
- def variant_s_files(self):
- return self.variant_files("**/*.S")
-
- def variant_cpp_files(self):
- return self.variant_files("**/*.cpp")
-
- def project_files(self, pattern):
- sources = []
- for file_path in FileOperations.find_files(self.project_path,
- [pattern]):
- if not file_path.startswith(
- "examples") and not file_path.startswith("libraries"):
- sources.append(file_path)
- return sources
-
- def project_c_files(self):
- return self.project_files("**/*.c")
-
- def project_cpp_files(self):
- return self.project_files("**/*.cpp")
-
- def project_ino_files(self):
- return self.project_files("**/*.ino")
-
-
-def install_core_command(args):
- install_prefix = os.path.realpath(
- os.path.expanduser(os.path.expandvars(args.prefix)))
- install_dir = os.path.join(install_prefix, args.core_name)
- cache_dir = os.path.join(install_prefix, ".cache", args.core_name)
-
- shutil.rmtree(install_dir, ignore_errors=True)
- os.makedirs(install_dir, exist_ok=True)
- os.makedirs(cache_dir, exist_ok=True)
-
- if args.core_name == "teensy":
- if platform.system() == "Linux":
- ArduinoCoreInstaller.install_teensy_core_linux(
- install_prefix, install_dir, cache_dir)
- elif platform.system() == "Darwin":
- ArduinoCoreInstaller.install_teensy_core_mac(
- install_prefix, install_dir, cache_dir)
- elif platform.system() == "Windows":
- ArduinoCoreInstaller.install_teensy_core_windows(
- install_prefix, install_dir, cache_dir)
- elif args.core_name == "adafruit-samd":
- ArduinoCoreInstaller.install_adafruit_samd_core(
- install_prefix, install_dir, cache_dir)
- elif args.core_name == "stm32duino":
- ArduinoCoreInstaller.install_stm32duino_core(install_prefix,
- install_dir, cache_dir)
-
- sys.exit(0)
-
-
-def list_boards_command(unused_args, builder):
- # list-boards subcommand
- # (does not need a selected board or default menu options)
-
- # TODO(tonymd): Print this sorted with auto-ljust columns
- longest_name_length = 0
- for board_name, board_dict in builder.board.items():
- if len(board_name) > longest_name_length:
- longest_name_length = len(board_name)
- longest_name_length += 2
-
- print("Board Name".ljust(longest_name_length), "Description")
- for board_name, board_dict in builder.board.items():
- print(board_name.ljust(longest_name_length), board_dict['name'])
- sys.exit(0)
-
-
-def list_menu_options_command(args, builder):
- # List all menu options for the selected board.
- builder.select_board(args.board)
-
- print("All Options")
- all_options, all_column_widths = builder.get_menu_options()
- separator = "-" * (all_column_widths[0] + all_column_widths[1] + 2)
- print(separator)
-
- for name, description in all_options:
- print(name.ljust(all_column_widths[0] + 1), description)
-
- print("\nDefault Options")
- print(separator)
-
- default_options, unused_column_widths = builder.get_default_menu_options()
- for name, description in default_options:
- print(name.ljust(all_column_widths[0] + 1), description)
-
-
-def show_command_print_string_list(args, string_list: List[str]):
- join_token = " "
- if args.delimit_with_newlines:
- join_token = "\n"
- print(join_token.join(string_list))
-
-
-def show_command_print_flag_string(args, flag_string):
- if args.delimit_with_newlines:
- flag_string_with_newlines = shlex.split(flag_string)
- print("\n".join(flag_string_with_newlines))
- else:
- print(flag_string)
-
-
-def run_command_lines(args, command_lines: List[str]):
- for command_line in command_lines:
- if not args.quiet:
- print(command_line)
- # TODO(tonymd): Exit with sub command exit code.
- os.system(command_line)
-
-
-# pylint: disable=too-many-branches
-def show_command(args, builder):
- """Show sub command function.
-
- Prints compile flags and runs Arduino recipes.
- """
- builder.select_board(args.board, args.menu_options)
-
- if args.cc_binary:
- print(builder.get_cc_binary())
-
- elif args.cxx_binary:
- print(builder.get_cxx_binary())
-
- elif args.objcopy_binary:
- print(builder.get_objcopy_binary())
-
- elif args.ar_binary:
- print(builder.get_ar_binary())
-
- elif args.size_binary:
- print(builder.get_size_binary())
-
- elif args.run_c_compile:
- print(builder.get_c_compile_line())
-
- elif args.run_cpp_compile:
- print(builder.get_cpp_compile_line())
-
- elif args.run_link:
- line = builder.get_link_line()
- archive_file_path = args.run_link[0] # pylint: disable=unused-variable
- object_files = args.run_link[1:]
- line = line.replace("{object_files}", " ".join(object_files), 1)
- run_command_lines(args, [line])
-
- elif args.run_objcopy:
- run_command_lines(args, builder.get_objcopy_steps())
-
- elif args.objcopy:
- print(builder.get_objcopy(args.objcopy))
-
- elif args.objcopy_flags:
- objcopy_flags = builder.get_objcopy_flags(args.objcopy_flags)
- show_command_print_flag_string(args, objcopy_flags)
-
- elif args.c_flags:
- cflags = builder.get_c_flags()
- show_command_print_flag_string(args, cflags)
-
- elif args.s_flags:
- sflags = builder.get_s_flags()
- show_command_print_flag_string(args, sflags)
-
- elif args.cpp_flags:
- cppflags = builder.get_cpp_flags()
- show_command_print_flag_string(args, cppflags)
-
- elif args.ld_flags:
- ldflags = builder.get_ld_flags()
- show_command_print_flag_string(args, ldflags)
-
- elif args.ld_libs:
- print(builder.get_ld_libs())
-
- elif args.ar_flags:
- ar_flags = builder.get_ar_flags()
- show_command_print_flag_string(args, ar_flags)
-
- elif args.core_path:
- print(builder.get_core_path())
-
- elif args.run_prebuilds:
- run_command_lines(args, builder.get_prebuild_steps())
-
- elif args.run_postbuilds:
- run_command_lines(args, builder.get_postbuild_steps())
-
- elif args.postbuild:
- print(builder.get_postbuild_line(args.postbuild))
-
- elif args.run_upload_command:
- command = builder.get_upload_line(args.run_upload_command,
- args.serial_port)
- run_command_lines(args, [command])
-
- elif args.upload_command:
- print(builder.get_upload_line(args.upload_command, args.serial_port))
-
- elif args.upload_tools:
- tools = builder.get_upload_tool_names()
- for tool_name in tools:
- print(tool_name)
-
- elif args.library_includes:
- show_command_print_string_list(args, builder.library_includes())
-
- elif args.library_c_files:
- show_command_print_string_list(args, builder.library_c_files())
-
- elif args.library_cpp_files:
- show_command_print_string_list(args, builder.library_cpp_files())
-
- elif args.core_c_files:
- show_command_print_string_list(args, builder.core_c_files())
-
- elif args.core_s_files:
- show_command_print_string_list(args, builder.core_s_files())
-
- elif args.core_cpp_files:
- show_command_print_string_list(args, builder.core_cpp_files())
-
- elif args.variant_c_files:
- vfiles = builder.variant_c_files()
- if vfiles:
- show_command_print_string_list(args, vfiles)
-
- elif args.variant_s_files:
- vfiles = builder.variant_s_files()
- if vfiles:
- show_command_print_string_list(args, vfiles)
-
- elif args.variant_cpp_files:
- vfiles = builder.variant_cpp_files()
- if vfiles:
- show_command_print_string_list(args, vfiles)
-
-
-def main():
- """Main command line function.
-
- Parses command line args and dispatches to sub `*_command()` functions.
- """
- def log_level(arg: str) -> int:
- try:
- return getattr(logging, arg.upper())
- except AttributeError:
- raise argparse.ArgumentTypeError(
- f'{arg.upper()} is not a valid log level')
-
- parser = argparse.ArgumentParser()
- parser.add_argument("-q",
- "--quiet",
- help="hide run command output",
- action="store_true")
- parser.add_argument('-l',
- '--loglevel',
- type=log_level,
- default=logging.INFO,
- help='Set the log level '
- '(debug, info, warning, error, critical)')
-
- build_path = os.path.realpath(
- os.path.expanduser(os.path.expandvars("./build")))
- project_path = os.path.realpath(
- os.path.expanduser(os.path.expandvars("./")))
- project_source_path = os.path.join(project_path, "src")
- build_project_name = os.path.basename(project_path)
-
- serial_port = "UNKNOWN"
- # TODO(tonymd): Temp solution to passing in serial port. It should use
- # arduino core discovery tools.
- possible_serial_ports = glob.glob("/dev/ttyACM*") + glob.glob(
- "/dev/ttyUSB*")
- if possible_serial_ports:
- serial_port = possible_serial_ports[0]
-
- project_name = os.path.basename(
- os.path.realpath(os.path.expanduser(os.path.expandvars("./"))))
-
- # Global command line options
- parser.add_argument("--arduino-package-path",
- help="Path to the arduino IDE install location.")
- parser.add_argument("--arduino-package-name",
- help="Name of the Arduino board package to use.")
- parser.add_argument("--compiler-path-override",
- help="Path to arm-none-eabi-gcc bin folder. "
- "Default: Arduino core specified gcc")
-
- # Subcommands
- subparsers = parser.add_subparsers(title="subcommand",
- description="valid subcommands",
- help="sub-command help",
- dest="subcommand",
- required=True)
-
- # install-core command
- install_core_parser = subparsers.add_parser(
- "install-core", help="Download and install arduino cores")
- install_core_parser.set_defaults(func=install_core_command)
- install_core_parser.add_argument("--prefix",
- required=True,
- help="Path to install core files.")
- install_core_parser.add_argument(
- "--core-name",
- required=True,
- choices=[
- "teensy",
- "stm32duino",
- "adafruit-samd",
- ],
- help="Name of the arduino core to install.")
-
- # list-boards command
- list_boards_parser = subparsers.add_parser("list-boards",
- help="show supported boards")
- list_boards_parser.set_defaults(func=list_boards_command)
-
- # list-menu-options command
- list_menu_options_parser = subparsers.add_parser(
- "list-menu-options",
- help="show available menu options for selected board")
- list_menu_options_parser.set_defaults(func=list_menu_options_command)
- list_menu_options_parser.add_argument("--board",
- required=True,
- help="Name of the board to use.")
-
- # show command
- show_parser = subparsers.add_parser("show",
- help="Return compiler information.")
- show_parser.add_argument(
- "--serial-port",
- default=serial_port,
- help="Serial port for flashing flash. Default: '{}'".format(
- serial_port))
- show_parser.add_argument(
- "--build-path",
- default=build_path,
- help="Build directory. Default: '{}'".format(build_path))
- show_parser.add_argument(
- "--project-path",
- default=build_path,
- help="Project directory. Default: '{}'".format(project_path))
- show_parser.add_argument(
- "--project-source-path",
- default=project_source_path,
- help="Project directory. Default: '{}'".format(project_source_path))
- show_parser.add_argument(
- "--build-project-name",
- default=project_name,
- help="Project name. Default: '{}'".format(build_project_name))
- show_parser.add_argument("--board",
- required=True,
- help="Name of the board to use.")
- show_parser.add_argument("--delimit-with-newlines",
- help="Separate flags output with newlines.",
- action="store_true")
-
- output_group = show_parser.add_mutually_exclusive_group(required=True)
- output_group.add_argument("--run-c-compile", action="store_true")
- output_group.add_argument("--run-cpp-compile", action="store_true")
- output_group.add_argument("--run-link",
- nargs="+",
- type=str,
- help="Run the link command. Expected arguments: "
- "the archive file followed by all obj files.")
- output_group.add_argument("--run-objcopy", action="store_true")
- output_group.add_argument("--run-prebuilds", action="store_true")
- output_group.add_argument("--run-postbuilds", action="store_true")
- output_group.add_argument("--c-flags", action="store_true")
- output_group.add_argument("--s-flags", action="store_true")
- output_group.add_argument("--cpp-flags", action="store_true")
- output_group.add_argument("--ld-flags", action="store_true")
- output_group.add_argument("--ar-flags", action="store_true")
- output_group.add_argument("--ld-libs", action="store_true")
- output_group.add_argument("--objcopy", help="objcopy step for SUFFIX")
- output_group.add_argument("--objcopy-flags",
- help="objcopy flags for SUFFIX")
- output_group.add_argument("--core-path", action="store_true")
- output_group.add_argument("--cc-binary", action="store_true")
- output_group.add_argument("--cxx-binary", action="store_true")
- output_group.add_argument("--ar-binary", action="store_true")
- output_group.add_argument("--objcopy-binary", action="store_true")
- output_group.add_argument("--size-binary", action="store_true")
- output_group.add_argument("--postbuild",
- help="Show recipe.hooks.postbuild.*.pattern")
- output_group.add_argument("--upload-tools", action="store_true")
- output_group.add_argument("--upload-command")
- output_group.add_argument("--run-upload-command")
- output_group.add_argument("--library-includes", action="store_true")
- output_group.add_argument("--library-c-files", action="store_true")
- output_group.add_argument("--library-cpp-files", action="store_true")
- output_group.add_argument("--core-c-files", action="store_true")
- output_group.add_argument("--core-s-files", action="store_true")
- output_group.add_argument("--core-cpp-files", action="store_true")
- output_group.add_argument("--variant-c-files", action="store_true")
- output_group.add_argument("--variant-s-files", action="store_true")
- output_group.add_argument("--variant-cpp-files", action="store_true")
-
- # nargs="+" is one or more args, e.g:
- # --menu-options menu.usb.serialhid menu.speed.150
- show_parser.add_argument("--menu-options", nargs="+", type=str)
- show_parser.set_defaults(func=show_command)
-
- args = parser.parse_args()
-
- # Set logging level and output handler
- _LOG.setLevel(args.loglevel)
- _STDERR_HANDLER.setLevel(args.loglevel)
- _STDERR_HANDLER.setFormatter(
- logging.Formatter("[%(asctime)s] "
- "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"))
- _LOG.addHandler(_STDERR_HANDLER)
-
- _LOG.debug(_pretty_format(args))
-
- compiler_path_override = False
- if args.compiler_path_override:
- compiler_path_override = os.path.realpath(
- os.path.expanduser(os.path.expandvars(
- args.compiler_path_override)))
-
- if args.subcommand == "install-core":
- args.func(args)
- elif args.subcommand in ["list-boards", "list-menu-options"]:
- builder = ArduinoBuilder(args.arduino_package_path,
- args.arduino_package_name)
- builder.load_board_definitions()
- args.func(args, builder)
- else:
- builder = ArduinoBuilder(args.arduino_package_path,
- args.arduino_package_name,
- build_path=args.build_path,
- build_project_name=args.build_project_name,
- project_path=args.project_path,
- project_source_path=args.project_source_path,
- compiler_path_override=compiler_path_override)
- builder.load_board_definitions()
- args.func(args, builder)
-
- sys.exit(0)
-
-
-if __name__ == '__main__':
- main()
diff --git a/pw_arduino_build/py/pw_arduino_build/builder.py b/pw_arduino_build/py/pw_arduino_build/builder.py
new file mode 100755
index 0000000..06c4117
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/builder.py
@@ -0,0 +1,1039 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Extracts build information from Arduino cores."""
+
+import glob
+import logging
+import os
+import platform
+import pprint
+import re
+import sys
+import time
+from collections import OrderedDict
+from typing import List
+
+from pw_arduino_build import file_operations
+
+_LOG = logging.getLogger(__name__)
+
+_pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
+_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+
+
+def arduino_runtime_os_string():
+ arduno_platform = {
+ "Linux": "linux",
+ "Windows": "windows",
+ "Darwin": "macosx"
+ }
+ return arduno_platform[platform.system()]
+
+
+class ArduinoBuilder:
+ """Used to interpret arduino boards.txt and platform.txt files."""
+ # pylint: disable=too-many-instance-attributes,too-many-public-methods
+
+ board_menu_regex = re.compile(
+ r"^(?P<name>menu\.[^#=]+)=(?P<description>.*)$", re.MULTILINE)
+
+ board_name_regex = re.compile(
+ r"^(?P<name>[^\s#\.]+)\.name=(?P<description>.*)$", re.MULTILINE)
+
+ variable_regex = re.compile(r"^(?P<name>[^\s#=]+)=(?P<value>.*)$",
+ re.MULTILINE)
+
+ menu_option_regex = re.compile(
+ r"^menu\." # starts with "menu"
+ r"(?P<menu_option_name>[^.]+)\." # first token after .
+ r"(?P<menu_option_value>[^.]+)$") # second (final) token after .
+
+ tool_name_regex = re.compile(
+ r"^tools\." # starts with "tools"
+ r"(?P<tool_name>[^.]+)\.") # first token after .
+
+ interpolated_variable_regex = re.compile(r"{[^}]+}", re.MULTILINE)
+
+ objcopy_step_name_regex = re.compile(r"^recipe.objcopy.([^.]+).pattern$")
+
+ def __init__(self,
+ arduino_path,
+ package_name,
+ build_path=None,
+ project_path=None,
+ project_source_path=None,
+ build_project_name=None,
+ compiler_path_override=False):
+ self.arduino_path = arduino_path
+ self.arduino_package_name = package_name
+ self.selected_board = None
+ self.build_path = build_path
+ self.project_path = project_path
+ self.project_source_path = project_source_path
+ self.build_project_name = build_project_name
+ self.compiler_path_override = compiler_path_override
+ self.variant_includes = ""
+ self.build_variant_path = False
+
+ self.compiler_path_override_binaries = []
+ if self.compiler_path_override:
+ self.compiler_path_override_binaries = file_operations.find_files(
+ self.compiler_path_override, "*")
+
+ # Container dicts for boards.txt and platform.txt file data.
+ self.board = OrderedDict()
+ self.platform = OrderedDict()
+ self.menu_options = OrderedDict({
+ "global_options": {},
+ "default_board_values": {},
+ "selected": {}
+ })
+ self.tools_variables = {}
+
+ # Set and check for valid hardware folder.
+ self.hardware_path = os.path.join(self.arduino_path, "hardware")
+
+ if not os.path.exists(self.hardware_path):
+ _LOG.error("Error: Arduino package path '%s' does not exist.",
+ self.arduino_path)
+ raise FileNotFoundError
+
+ # Set and check for valid package name
+ self.package_path = os.path.join(self.arduino_path, "hardware",
+ package_name)
+ # {build.arch} is the first folder name of the package (upcased)
+ self.build_arch = os.path.split(package_name)[0].upper()
+
+ if not os.path.exists(self.package_path):
+ _LOG.error("Error: Arduino package name '%s' does not exist.",
+ package_name)
+ _LOG.error("Did you mean:\n")
+ # TODO(tonymd): On Windows concatenating "/" may not work
+ possible_alternatives = [
+ d.replace(self.hardware_path + os.sep, "", 1)
+ for d in glob.glob(self.hardware_path + "/*/*")
+ ]
+ _LOG.error("\n".join(possible_alternatives))
+ sys.exit(1)
+
+ # Grab all folder names in the cores directory. These are typically
+ # sub-core source files.
+ self.sub_core_folders = os.listdir(
+ os.path.join(self.package_path, "cores"))
+
+ self._find_tools_variables()
+
+ self.boards_txt = os.path.join(self.package_path, "boards.txt")
+ self.platform_txt = os.path.join(self.package_path, "platform.txt")
+
+ def select_board(self, board_name, menu_option_overrides=False):
+ self.selected_board = board_name
+
+ # Load default menu options for a selected board.
+ if not self.selected_board in self.board.keys():
+ _LOG.error("Error board: '%s' not supported.", self.selected_board)
+ # TODO(tonymd): Print supported boards here
+ sys.exit(1)
+
+ # Override default menu options if any are specified.
+ if menu_option_overrides:
+ for moption in menu_option_overrides:
+ if not self.set_menu_option(moption):
+ # TODO(tonymd): Print supported menu options here
+ sys.exit(1)
+
+ self._copy_default_menu_options_to_build_variables()
+ self._apply_recipe_overrides()
+ self._substitute_variables()
+
+ def _apply_recipe_overrides(self):
+ # Override link recipes with per-core exceptions
+ # Teensyduino cores
+ if self.build_arch == 'TEENSY':
+ # Change {build.path}/{archive_file}
+ # To {archive_file_path} (which should contain the core.a file)
+ new_link_line = self.platform["recipe.c.combine.pattern"].replace(
+ "{object_files} \"{build.path}/{archive_file}\"",
+ "{object_files} {archive_file_path}", 1)
+ # Add the teensy provided toolchain lib folder for link access to
+ # libarm_cortexM*_math.a
+ new_link_line = new_link_line.replace(
+ "\"-L{build.path}\"",
+ "\"-L{build.path}\" -L{compiler.path}/arm/arm-none-eabi/lib",
+ 1)
+ self.platform["recipe.c.combine.pattern"] = new_link_line
+
+ # Adafruit-samd core
+ # TODO(tonymd): This build_arch may clash with Arduino-SAMD core
+ elif self.build_arch == 'SAMD':
+ new_link_line = self.platform["recipe.c.combine.pattern"].replace(
+ "\"{build.path}/{archive_file}\" -Wl,--end-group",
+ "{archive_file_path} -Wl,--end-group", 1)
+ self.platform["recipe.c.combine.pattern"] = new_link_line
+
+ # STM32L4 Core:
+ # https://github.com/GrumpyOldPizza/arduino-STM32L4
+ elif self.build_arch == 'STM32L4':
+ # TODO(tonymd): {build.path}/{archive_file} for the link step always
+ # seems to be core.a (except STM32 core)
+ line_to_delete = "-Wl,--start-group \"{build.path}/{archive_file}\""
+ new_link_line = self.platform["recipe.c.combine.pattern"].replace(
+ line_to_delete, "-Wl,--start-group {archive_file_path}", 1)
+ self.platform["recipe.c.combine.pattern"] = new_link_line
+
+ # stm32duino core
+ elif self.build_arch == 'STM32':
+ pass
+
+ def _copy_default_menu_options_to_build_variables(self):
+ # Clear existing options
+ self.menu_options["selected"] = {}
+ # Set default menu options for selected board
+ for menu_key, menu_dict in self.menu_options["default_board_values"][
+ self.selected_board].items():
+ for name, var in self.board[self.selected_board].items():
+ starting_key = "{}.{}.".format(menu_key, menu_dict["name"])
+ if name.startswith(starting_key):
+ new_var_name = name.replace(starting_key, "", 1)
+ self.menu_options["selected"][new_var_name] = var
+
+ def set_menu_option(self, moption):
+ if moption not in self.board[self.selected_board]:
+ _LOG.error("Error: '%s' is not a valid menu option.", moption)
+ return False
+
+ # Override default menu option with new value.
+ menu_match_result = self.menu_option_regex.match(moption)
+ if menu_match_result:
+ menu_match = menu_match_result.groupdict()
+ menu_value = menu_match["menu_option_value"]
+ menu_key = "menu.{}".format(menu_match["menu_option_name"])
+ self.menu_options["default_board_values"][
+ self.selected_board][menu_key]["name"] = menu_value
+
+ # Update build variables
+ self._copy_default_menu_options_to_build_variables()
+ return True
+
+ def _set_global_arduino_variables(self):
+ """Set some global variables defined by the Arduino-IDE.
+
+ See Docs:
+ https://arduino.github.io/arduino-cli/platform-specification/#global-predefined-properties
+ """
+
+ for current_board_name in self.board.keys():
+ if self.build_path:
+ self.board[current_board_name]["build.path"] = self.build_path
+ if self.build_project_name:
+ self.board[current_board_name][
+ "build.project_name"] = self.build_project_name
+ # {archive_file} is the final *.elf
+ archive_file = "{}.elf".format(self.build_project_name)
+ self.board[current_board_name]["archive_file"] = archive_file
+ # {archive_file_path} is the final core.a archive
+ if self.build_path:
+ self.board[current_board_name][
+ "archive_file_path"] = os.path.join(
+ self.build_path, "core.a")
+ if self.project_source_path:
+ self.board[current_board_name][
+ "build.source.path"] = self.project_source_path
+
+ self.board[current_board_name]["extra.time.local"] = str(
+ int(time.time()))
+ self.board[current_board_name]["runtime.ide.version"] = "10812"
+ self.board[current_board_name][
+ "runtime.hardware.path"] = self.hardware_path
+
+ # Copy {runtime.tools.TOOL_NAME.path} vars
+ self._set_tools_variables(self.board[current_board_name])
+
+ self.board[current_board_name][
+ "runtime.platform.path"] = self.package_path
+ if self.platform["name"] == "Teensyduino":
+ # Teensyduino is installed into the arduino IDE folder
+ # rather than ~/.arduino15/packages/
+ self.board[current_board_name][
+ "runtime.hardware.path"] = os.path.join(
+ self.hardware_path, "teensy")
+
+ self.board[current_board_name]["build.system.path"] = os.path.join(
+ self.package_path, "system")
+
+ # Set the {build.core.path} variable that pointing to a sub-core
+ # folder. For Teensys this is:
+ # 'teensy/hardware/teensy/avr/cores/teensy{3,4}'. For other cores
+ # it's typically just the 'arduino' folder. For example:
+ # 'arduino-samd/hardware/samd/1.8.8/cores/arduino'
+ core_path = os.path.join(
+ self.package_path, "cores",
+ self.board[current_board_name].get("build.core",
+ self.sub_core_folders[0]))
+ self.board[current_board_name]["build.core.path"] = core_path
+
+ self.board[current_board_name]["build.arch"] = self.build_arch
+
+ for name, var in self.board[current_board_name].items():
+ self.board[current_board_name][name] = var.replace(
+ "{build.core.path}", core_path)
+
+ def load_board_definitions(self):
+ """Loads Arduino boards.txt and platform.txt files into dictionaries.
+
+ Populates the following dictionaries:
+ self.menu_options
+ self.boards
+ self.platform
+ """
+ # Load platform.txt
+ with open(self.platform_txt, "r") as pfile:
+ platform_file = pfile.read()
+ platform_var_matches = self.variable_regex.finditer(platform_file)
+ for p_match in [m.groupdict() for m in platform_var_matches]:
+ self.platform[p_match["name"]] = p_match["value"]
+
+ # Load boards.txt
+ with open(self.boards_txt, "r") as bfile:
+ board_file = bfile.read()
+ # Get all top-level menu options, e.g. menu.usb=USB Type
+ board_menu_matches = self.board_menu_regex.finditer(board_file)
+ for menuitem in [m.groupdict() for m in board_menu_matches]:
+ self.menu_options["global_options"][menuitem["name"]] = {
+ "description": menuitem["description"]
+ }
+
+ # Get all board names, e.g. teensy40.name=Teensy 4.0
+ board_name_matches = self.board_name_regex.finditer(board_file)
+ for b_match in [m.groupdict() for m in board_name_matches]:
+ self.board[b_match["name"]] = OrderedDict()
+ self.menu_options["default_board_values"][
+ b_match["name"]] = OrderedDict()
+
+ # Get all board variables, e.g. teensy40.*
+ for current_board_name in self.board.keys():
+ board_line_matches = re.finditer(
+ fr"^\s*{current_board_name}\."
+ fr"(?P<key>[^#=]+)=(?P<value>.*)$", board_file,
+ re.MULTILINE)
+ for b_match in [m.groupdict() for m in board_line_matches]:
+ # Check if this line is a menu option
+ # (e.g. 'menu.usb.serial') and save as default if it's the
+ # first one seen.
+ ArduinoBuilder.save_default_menu_option(
+ current_board_name, b_match["key"], b_match["value"],
+ self.menu_options)
+ self.board[current_board_name][
+ b_match["key"]] = b_match["value"].strip()
+
+ self._set_global_arduino_variables()
+
+ @staticmethod
+ def save_default_menu_option(current_board_name, key, value, menu_options):
+ """Save a given menu option as the default.
+
+ Saves the key and value into menu_options["default_board_values"]
+ if it doesn't already exist. Assumes menu options are added in the order
+ specified in boards.txt. The first value for a menu key is the default.
+ """
+ # Check if key is a menu option
+ # e.g. menu.usb.serial
+ # menu.usb.serial.build.usbtype
+ menu_match_result = re.match(
+ r'^menu\.' # starts with "menu"
+ r'(?P<menu_option_name>[^.]+)\.' # first token after .
+ r'(?P<menu_option_value>[^.]+)' # second token after .
+ r'(\.(?P<rest>.+))?', # optionally any trailing tokens after a .
+ key)
+ if menu_match_result:
+ menu_match = menu_match_result.groupdict()
+ current_menu_key = "menu.{}".format(menu_match["menu_option_name"])
+ # If this is the first menu option seen for current_board_name, save
+ # as the default.
+ if current_menu_key not in menu_options["default_board_values"][
+ current_board_name]:
+ menu_options["default_board_values"][current_board_name][
+ current_menu_key] = {
+ "name": menu_match["menu_option_value"],
+ "description": value
+ }
+
+ def _replace_variables(self, line, variable_lookup_source):
+ """Replace {variables} from loaded boards.txt or platform.txt.
+
+ Replace interpolated variables surrounded by curly braces in line with
+ definitions from variable_lookup_source.
+ """
+ new_line = line
+ for current_var_match in self.interpolated_variable_regex.findall(
+ line):
+ # {build.flags.c} --> build.flags.c
+ current_var = current_var_match.strip("{}")
+
+ # check for matches in board definition
+ if current_var in variable_lookup_source:
+ replacement = variable_lookup_source.get(current_var, "")
+ new_line = new_line.replace(current_var_match, replacement)
+ return new_line
+
+ def _find_tools_variables(self):
+ # Gather tool directories in order of increasing precedence
+ runtime_tool_paths = []
+
+ # Check for tools installed in ~/.arduino15/packages/arduino/tools/
+ # TODO(tonymd): Is this Mac & Linux specific?
+ runtime_tool_paths += glob.glob(
+ os.path.join(
+ os.path.realpath(os.path.expanduser(os.path.expandvars("~"))),
+ ".arduino15", "packages", "arduino", "tools", "*"))
+
+ # <ARDUINO_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
+ runtime_tool_paths += glob.glob(
+ os.path.join(self.arduino_path, "tools",
+ arduino_runtime_os_string(), "*"))
+ # <ARDUINO_PATH>/tools/<TOOL_NAMES>
+ # This will grab linux/windows/macosx/share as <TOOL_NAMES>.
+ runtime_tool_paths += glob.glob(
+ os.path.join(self.arduino_path, "tools", "*"))
+
+ # Process package tools after arduino tools.
+ # They should overwrite vars & take precedence.
+
+ # <PACKAGE_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
+ runtime_tool_paths += glob.glob(
+ os.path.join(self.package_path, "tools",
+ arduino_runtime_os_string(), "*"))
+ # <PACKAGE_PATH>/tools/<TOOL_NAMES>
+ # This will grab linux/windows/macosx/share as <TOOL_NAMES>.
+ runtime_tool_paths += glob.glob(
+ os.path.join(self.package_path, "tools", "*"))
+
+ for path in runtime_tool_paths:
+ # Make sure TOOL_NAME is not an OS string
+ if not (path.endswith("linux") or path.endswith("windows")
+ or path.endswith("macosx") or path.endswith("share")):
+ # TODO(tonymd): Check if a file & do nothing?
+
+ # Check if it's a directory with subdir(s) as a version string
+ # create all 'runtime.tools.{tool_folder}-{version.path}'
+ # (for each version)
+ # create 'runtime.tools.{tool_folder}.path'
+ # (with latest version)
+ if os.path.isdir(path):
+ # Grab the tool name (folder) by itself.
+ tool_folder = os.path.basename(path)
+ # Sort so that [-1] is the latest version.
+ version_paths = sorted(glob.glob(os.path.join(path, "*")))
+ # Check if all sub folders start with a version string.
+ if len(version_paths) == sum(
+ bool(re.match(r"^[0-9.]+", os.path.basename(vp)))
+ for vp in version_paths):
+ for version_path in version_paths:
+ version_string = os.path.basename(version_path)
+ var_name = "runtime.tools.{}-{}.path".format(
+ tool_folder, version_string)
+ self.tools_variables[var_name] = os.path.join(
+ path, version_string)
+ var_name = "runtime.tools.{}.path".format(tool_folder)
+ self.tools_variables[var_name] = os.path.join(
+ path, os.path.basename(version_paths[-1]))
+ # Else set toolpath to path.
+ else:
+ var_name = "runtime.tools.{}.path".format(tool_folder)
+ self.tools_variables[var_name] = path
+
+ _LOG.debug("TOOL VARIABLES: %s", _pretty_format(self.tools_variables))
+
+ # Copy self.tools_variables into destination
+ def _set_tools_variables(self, destination):
+ for key, value in self.tools_variables.items():
+ destination[key] = value
+
+ def _substitute_variables(self):
+ """Perform variable substitution in board and platform metadata."""
+
+ # menu -> board
+ # Copy selected menu variables into board definiton
+ for name, value in self.menu_options["selected"].items():
+ self.board[self.selected_board][name] = value
+
+ # board -> board
+ # Replace any {vars} in the selected board with values defined within
+ # (and from copied in menu options).
+ for var, value in self.board[self.selected_board].items():
+ self.board[self.selected_board][var] = self._replace_variables(
+ value, self.board[self.selected_board])
+
+ # Check for build.variant variable
+ # This will be set in selected board after menu options substitution
+ build_variant = self.board[self.selected_board].get(
+ "build.variant", None)
+ if build_variant:
+ # Set build.variant.path
+ bvp = os.path.join(self.package_path, "variants", build_variant)
+ self.build_variant_path = bvp
+ self.board[self.selected_board]["build.variant.path"] = bvp
+ # Add the variant folder as an include directory
+ # (used in stm32l4 core)
+ self.variant_includes = "-I{}".format(bvp)
+
+ _LOG.debug("PLATFORM INITIAL: %s", _pretty_format(self.platform))
+
+ # board -> platform
+ # Replace {vars} in platform from the selected board definition
+ for var, value in self.platform.items():
+ self.platform[var] = self._replace_variables(
+ value, self.board[self.selected_board])
+
+ # platform -> platform
+ # Replace any remaining {vars} in platform from platform
+ for var, value in self.platform.items():
+ self.platform[var] = self._replace_variables(value, self.platform)
+
+ # Repeat platform -> platform for any lingering variables
+ # Example: {build.opt.name} in STM32 core
+ for var, value in self.platform.items():
+ self.platform[var] = self._replace_variables(value, self.platform)
+
+ _LOG.debug("MENU_OPTIONS: %s", _pretty_format(self.menu_options))
+ _LOG.debug("SELECTED_BOARD: %s",
+ _pretty_format(self.board[self.selected_board]))
+ _LOG.debug("PLATFORM: %s", _pretty_format(self.platform))
+
+ def selected_board_spec(self):
+ return self.board[self.selected_board]
+
+ def get_menu_options(self):
+ all_options = []
+ max_string_length = [0, 0]
+
+ for key_name, description in self.board[self.selected_board].items():
+ menu_match_result = self.menu_option_regex.match(key_name)
+ if menu_match_result:
+ menu_match = menu_match_result.groupdict()
+ name = "menu.{}.{}".format(menu_match["menu_option_name"],
+ menu_match["menu_option_value"])
+ if len(name) > max_string_length[0]:
+ max_string_length[0] = len(name)
+ if len(description) > max_string_length[1]:
+ max_string_length[1] = len(description)
+ all_options.append((name, description))
+
+ return all_options, max_string_length
+
+ def get_default_menu_options(self):
+ default_options = []
+ max_string_length = [0, 0]
+
+ for key_name, value in self.menu_options["default_board_values"][
+ self.selected_board].items():
+ full_key = key_name + "." + value["name"]
+ if len(full_key) > max_string_length[0]:
+ max_string_length[0] = len(full_key)
+ if len(value["description"]) > max_string_length[1]:
+ max_string_length[1] = len(value["description"])
+ default_options.append((full_key, value["description"]))
+
+ return default_options, max_string_length
+
+ @staticmethod
+ def split_binary_from_arguments(compile_line):
+ compile_binary = None
+ rest_of_line = compile_line
+
+ compile_binary_match = re.search(r'^("[^"]+") ', compile_line)
+ if compile_binary_match:
+ compile_binary = compile_binary_match[1]
+ rest_of_line = compile_line.replace(compile_binary_match[0], "", 1)
+
+ return compile_binary, rest_of_line
+
+ def _strip_includes_source_file_object_file_vars(self, compile_line):
+ line = compile_line
+ if self.variant_includes:
+ line = compile_line.replace(
+ "{includes} \"{source_file}\" -o \"{object_file}\"",
+ self.variant_includes, 1)
+ else:
+ line = compile_line.replace(
+ "{includes} \"{source_file}\" -o \"{object_file}\"", "", 1)
+ return line
+
+ def _get_tool_name(self, line):
+ tool_match_result = self.tool_name_regex.match(line)
+ if tool_match_result:
+ return tool_match_result[1]
+ return False
+
+ def get_upload_tool_names(self):
+ return [
+ self._get_tool_name(t) for t in self.platform.keys()
+ if self.tool_name_regex.match(t) and 'upload.pattern' in t
+ ]
+
+ # TODO(tonymd): Use these getters in _replace_variables() or
+ # _substitute_variables()
+
+ def _get_platform_variable(self, variable):
+ # TODO(tonymd): Check for '.macos' '.linux' '.windows' in variable name,
+ # compare with platform.system() and return that instead.
+ return self.platform.get(variable, False)
+
+ def _get_platform_variable_with_substitutions(self, variable, namespace):
+ line = self.platform.get(variable, False)
+ # Get all unique variables used in this line in line.
+ unique_vars = sorted(
+ set(self.interpolated_variable_regex.findall(line)))
+ # Search for each unique_vars in namespace and global.
+ for var in unique_vars:
+ v_raw_name = var.strip("{}")
+
+ # Check for namespace.variable
+ # eg: 'tools.stm32CubeProg.cmd'
+ possible_var_name = "{}.{}".format(namespace, v_raw_name)
+ result = self._get_platform_variable(possible_var_name)
+ # Check for os overriden variable
+ # eg:
+ # ('tools.stm32CubeProg.cmd', 'stm32CubeProg.sh'),
+ # ('tools.stm32CubeProg.cmd.windows', 'stm32CubeProg.bat'),
+ possible_var_name = "{}.{}.{}".format(namespace, v_raw_name,
+ arduino_runtime_os_string())
+ os_override_result = self._get_platform_variable(possible_var_name)
+
+ if os_override_result:
+ line = line.replace(var, os_override_result)
+ elif result:
+ line = line.replace(var, result)
+ # Check for variable at top level?
+ # elif self._get_platform_variable(v_raw_name):
+ # line = line.replace(self._get_platform_variable(v_raw_name),
+ # result)
+ return line
+
+ def get_upload_line(self, tool_name, serial_port=False):
+ # TODO(tonymd): Error if tool_name does not exist
+ tool_namespace = "tools.{}".format(tool_name)
+ pattern = "tools.{}.upload.pattern".format(tool_name)
+
+ if not self._get_platform_variable(pattern):
+ _LOG.error("Error: upload tool '%s' does not exist.", tool_name)
+ tools = self.get_upload_tool_names()
+ _LOG.error("Valid tools: %s", ", ".join(tools))
+ return sys.exit(1)
+
+ line = self._get_platform_variable_with_substitutions(
+ pattern, tool_namespace)
+
+ # TODO(tonymd): Teensy specific tool overrides.
+ if tool_name == "teensyloader":
+ # Remove un-necessary lines
+ # {serial.port.label} and {serial.port.protocol} are returned by
+ # the teensy_ports binary.
+ line = line.replace("\"-portlabel={serial.port.label}\"", "", 1)
+ line = line.replace("\"-portprotocol={serial.port.protocol}\"", "",
+ 1)
+
+ if serial_port:
+ line = line.replace("{serial.port}", serial_port, 1)
+
+ return line
+
+ def _get_binary_path(self, variable_pattern):
+ compile_line = self.replace_compile_binary_with_override_path(
+ self._get_platform_variable(variable_pattern))
+ compile_binary, _ = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ return compile_binary
+
+ def get_cc_binary(self):
+ return self._get_binary_path("recipe.c.o.pattern")
+
+ def get_cxx_binary(self):
+ return self._get_binary_path("recipe.cpp.o.pattern")
+
+ def get_objcopy_binary(self):
+ objcopy_step_name = self.get_objcopy_step_names()[0]
+ objcopy_binary = self._get_binary_path(objcopy_step_name)
+ return objcopy_binary
+
+ def get_ar_binary(self):
+ return self._get_binary_path("recipe.ar.pattern")
+
+ def get_size_binary(self):
+ return self._get_binary_path("recipe.size.pattern")
+
+ def replace_command_args_with_compiler_override_path(self, compile_line):
+ if not self.compiler_path_override:
+ return compile_line
+ replacement_line = compile_line
+ replacement_line_args = compile_line.split()
+ for arg in replacement_line_args:
+ compile_binary_basename = os.path.basename(arg.strip("\""))
+ if compile_binary_basename in self.compiler_path_override_binaries:
+ new_compiler = os.path.join(self.compiler_path_override,
+ compile_binary_basename)
+ replacement_line = replacement_line.replace(
+ arg, new_compiler, 1)
+ return replacement_line
+
+ def replace_compile_binary_with_override_path(self, compile_line):
+ replacement_compile_line = compile_line
+
+ # Change the compiler path if there's an override path set
+ if self.compiler_path_override:
+ compile_binary, line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ compile_binary_basename = os.path.basename(
+ compile_binary.strip("\""))
+ new_compiler = os.path.join(self.compiler_path_override,
+ compile_binary_basename)
+ if platform.system() == "Windows" and not re.match(
+ r".*\.exe$", new_compiler, flags=re.IGNORECASE):
+ new_compiler += ".exe"
+
+ if os.path.isfile(new_compiler):
+ replacement_compile_line = "\"{}\" {}".format(
+ new_compiler, line)
+
+ return replacement_compile_line
+
+ def get_c_compile_line(self):
+ _LOG.debug("ARDUINO_C_COMPILE: %s",
+ _pretty_format(self.platform["recipe.c.o.pattern"]))
+
+ compile_line = self.platform["recipe.c.o.pattern"]
+ compile_line = self._strip_includes_source_file_object_file_vars(
+ compile_line)
+ compile_line += " -I{}".format(
+ self.board[self.selected_board]["build.core.path"])
+
+ compile_line = self.replace_compile_binary_with_override_path(
+ compile_line)
+ return compile_line
+
+ def get_s_compile_line(self):
+ _LOG.debug("ARDUINO_S_COMPILE %s",
+ _pretty_format(self.platform["recipe.S.o.pattern"]))
+
+ compile_line = self.platform["recipe.S.o.pattern"]
+ compile_line = self._strip_includes_source_file_object_file_vars(
+ compile_line)
+ compile_line += " -I{}".format(
+ self.board[self.selected_board]["build.core.path"])
+
+ compile_line = self.replace_compile_binary_with_override_path(
+ compile_line)
+ return compile_line
+
+ def get_ar_compile_line(self):
+ _LOG.debug("ARDUINO_AR_COMPILE: %s",
+ _pretty_format(self.platform["recipe.ar.pattern"]))
+
+ compile_line = self.platform["recipe.ar.pattern"].replace(
+ "\"{object_file}\"", "", 1)
+
+ compile_line = self.replace_compile_binary_with_override_path(
+ compile_line)
+ return compile_line
+
+ def get_cpp_compile_line(self):
+ _LOG.debug("ARDUINO_CPP_COMPILE: %s",
+ _pretty_format(self.platform["recipe.cpp.o.pattern"]))
+
+ compile_line = self.platform["recipe.cpp.o.pattern"]
+ compile_line = self._strip_includes_source_file_object_file_vars(
+ compile_line)
+ compile_line += " -I{}".format(
+ self.board[self.selected_board]["build.core.path"])
+
+ compile_line = self.replace_compile_binary_with_override_path(
+ compile_line)
+ return compile_line
+
+ def get_link_line(self):
+ _LOG.debug("ARDUINO_LINK: %s",
+ _pretty_format(self.platform["recipe.c.combine.pattern"]))
+
+ compile_line = self.platform["recipe.c.combine.pattern"]
+
+ compile_line = self.replace_compile_binary_with_override_path(
+ compile_line)
+ return compile_line
+
+ def get_objcopy_step_names(self):
+ names = [
+ name for name, line in self.platform.items()
+ if self.objcopy_step_name_regex.match(name)
+ ]
+ return names
+
+ def get_objcopy_steps(self) -> List[str]:
+ lines = [
+ line for name, line in self.platform.items()
+ if self.objcopy_step_name_regex.match(name)
+ ]
+ lines = [
+ self.replace_compile_binary_with_override_path(line)
+ for line in lines
+ ]
+ return lines
+
+ # TODO(tonymd): These recipes are probably run in sorted order
+ def get_objcopy(self, suffix):
+ # Expected vars:
+ # teensy:
+ # recipe.objcopy.eep.pattern
+ # recipe.objcopy.hex.pattern
+
+ pattern = "recipe.objcopy.{}.pattern".format(suffix)
+ objcopy_step_names = self.get_objcopy_step_names()
+
+ objcopy_suffixes = [
+ m[1] for m in [
+ self.objcopy_step_name_regex.match(line)
+ for line in objcopy_step_names
+ ] if m
+ ]
+ if pattern not in objcopy_step_names:
+ _LOG.error("Error: objcopy suffix '%s' does not exist.", suffix)
+ _LOG.error("Valid suffixes: %s", ", ".join(objcopy_suffixes))
+ return sys.exit(1)
+
+ line = self._get_platform_variable(pattern)
+
+ _LOG.debug("ARDUINO_OBJCOPY_%s: %s", suffix, line)
+
+ line = self.replace_compile_binary_with_override_path(line)
+
+ return line
+
+ def get_objcopy_flags(self, suffix):
+ # TODO(tonymd): Possibly teensy specific variables.
+ flags = ""
+ if suffix == "hex":
+ flags = self.platform.get("compiler.elf2hex.flags", "")
+ elif suffix == "bin":
+ flags = self.platform.get("compiler.elf2bin.flags", "")
+ elif suffix == "eep":
+ flags = self.platform.get("compiler.objcopy.eep.flags", "")
+ return flags
+
+ # TODO(tonymd): There are more recipe hooks besides postbuild.
+ # They are run in sorted order.
+ # TODO(tonymd): Rename this to get_hooks(hook_name, step).
+ # TODO(tonymd): Add a list-hooks and or run-hooks command
+ def get_postbuild_line(self, step_number):
+ line = self.platform["recipe.hooks.postbuild.{}.pattern".format(
+ step_number)]
+ line = self.replace_command_args_with_compiler_override_path(line)
+ return line
+
+ def get_prebuild_steps(self) -> List[str]:
+ # Teensy core uses recipe.hooks.sketch.prebuild.1.pattern
+ # stm32 core uses recipe.hooks.prebuild.1.pattern
+ # TODO(tonymd): STM32 core uses recipe.hooks.prebuild.1.pattern.windows
+ # (should override non-windows key)
+ lines = [
+ line for name, line in self.platform.items() if re.match(
+ r"^recipe.hooks.(?:sketch.)?prebuild.[^.]+.pattern$", name)
+ ]
+ # TODO(tonymd): Write a function to fetch/replace OS specific patterns
+ # (ending in an OS string)
+ lines = [
+ self.replace_compile_binary_with_override_path(line)
+ for line in lines
+ ]
+ return lines
+
+ def get_postbuild_steps(self) -> List[str]:
+ lines = [
+ line for name, line in self.platform.items()
+ if re.match(r"^recipe.hooks.postbuild.[^.]+.pattern$", name)
+ ]
+
+ lines = [
+ self.replace_command_args_with_compiler_override_path(line)
+ for line in lines
+ ]
+ return lines
+
+ def get_s_flags(self):
+ compile_line = self.get_s_compile_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ compile_line = compile_line.replace("-c", "", 1)
+ return compile_line.strip()
+
+ def get_c_flags(self):
+ compile_line = self.get_c_compile_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ compile_line = compile_line.replace("-c", "", 1)
+ return compile_line.strip()
+
+ def get_cpp_flags(self):
+ compile_line = self.get_cpp_compile_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ compile_line = compile_line.replace("-c", "", 1)
+ return compile_line.strip()
+
+ def get_ar_flags(self):
+ compile_line = self.get_ar_compile_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ return compile_line.strip()
+
+ def get_ld_flags(self):
+ compile_line = self.get_link_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+
+ # TODO(tonymd): This is teensy specific
+ line_to_delete = "-o \"{build.path}/{build.project_name}.elf\" " \
+ "{object_files} \"-L{build.path}\""
+ if self.build_path:
+ line_to_delete = line_to_delete.replace("{build.path}",
+ self.build_path)
+ if self.build_project_name:
+ line_to_delete = line_to_delete.replace("{build.project_name}",
+ self.build_project_name)
+
+ compile_line = compile_line.replace(line_to_delete, "", 1)
+ libs = re.findall(r'(-l[^ ]+ ?)', compile_line)
+ for lib in libs:
+ compile_line = compile_line.replace(lib, "", 1)
+ libs = [lib.strip() for lib in libs]
+
+ return compile_line.strip()
+
+ def get_ld_libs(self):
+ compile_line = self.get_link_line()
+ _, compile_line = ArduinoBuilder.split_binary_from_arguments(
+ compile_line)
+ # TODO(tonymd): This replacement is teensy specific
+ compile_line = compile_line.replace(
+ "-o \"{build.path}/{build.project_name}.elf\" "
+ "{object_files} \"-L{build.path}\"", "", 1)
+ libs = re.findall(r'(-l[^ ]+ ?)', compile_line)
+ for lib in libs:
+ compile_line = compile_line.replace(lib, "", 1)
+ libs = [lib.strip() for lib in libs]
+
+ return " ".join(libs)
+
+ def library_folders(self):
+ # Arduino library format documentation:
+ # https://arduino.github.io/arduino-cli/library-specification/#layout-of-folders-and-files
+ # - If src folder exists,
+ # use that as the root include directory -Ilibraries/libname/src
+ # - Else lib folder as root include -Ilibraries/libname
+ # (exclude source files in the examples folder in this case)
+
+ library_path = os.path.join(self.project_path, "libraries")
+
+ library_folders = file_operations.find_files(library_path, ["*"],
+ directories_only=True)
+ library_source_root_folders = []
+ for lib in library_folders:
+ lib_dir = os.path.join(library_path, lib)
+ src_dir = os.path.join(lib_dir, "src")
+ if os.path.exists(src_dir) and os.path.isdir(src_dir):
+ library_source_root_folders.append(src_dir)
+ else:
+ library_source_root_folders.append(lib_dir)
+
+ return library_source_root_folders
+
+ def library_includes(self):
+ include_args = []
+ library_folders = self.library_folders()
+ for lib_dir in library_folders:
+ include_args.append("-I{}".format(os.path.relpath(lib_dir)))
+ return include_args
+
+ def library_files(self, pattern):
+ sources = []
+ library_folders = self.library_folders()
+ for lib_dir in library_folders:
+ for file_path in file_operations.find_files(lib_dir, [pattern]):
+ if not file_path.startswith("examples"):
+ sources.append(
+ os.path.relpath(os.path.join(lib_dir, file_path)))
+ return sources
+
+ def library_c_files(self):
+ return self.library_files("**/*.c")
+
+ def library_cpp_files(self):
+ return self.library_files("**/*.cpp")
+
+ def get_core_path(self):
+ return self.board[self.selected_board]["build.core.path"]
+
+ def core_files(self, pattern):
+ sources = []
+ for file_path in file_operations.find_files(self.get_core_path(),
+ [pattern]):
+ sources.append(os.path.join(self.get_core_path(), file_path))
+ return sources
+
+ def core_c_files(self):
+ return self.core_files("**/*.c")
+
+ def core_s_files(self):
+ return self.core_files("**/*.S")
+
+ def core_cpp_files(self):
+ return self.core_files("**/*.cpp")
+
+ def get_variant_path(self):
+ return self.build_variant_path
+
+ def variant_files(self, pattern):
+ sources = []
+ if self.build_variant_path:
+ for file_path in file_operations.find_files(
+ self.get_variant_path(), [pattern]):
+ sources.append(os.path.join(self.get_variant_path(),
+ file_path))
+ return sources
+
+ def variant_c_files(self):
+ return self.variant_files("**/*.c")
+
+ def variant_s_files(self):
+ return self.variant_files("**/*.S")
+
+ def variant_cpp_files(self):
+ return self.variant_files("**/*.cpp")
+
+ def project_files(self, pattern):
+ sources = []
+ for file_path in file_operations.find_files(self.project_path,
+ [pattern]):
+ if not file_path.startswith(
+ "examples") and not file_path.startswith("libraries"):
+ sources.append(file_path)
+ return sources
+
+ def project_c_files(self):
+ return self.project_files("**/*.c")
+
+ def project_cpp_files(self):
+ return self.project_files("**/*.cpp")
+
+ def project_ino_files(self):
+ return self.project_files("**/*.ino")
diff --git a/pw_arduino_build/py/pw_arduino_build/core_installer.py b/pw_arduino_build/py/pw_arduino_build/core_installer.py
new file mode 100644
index 0000000..a1e3d3c
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/core_installer.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Arduino Core Installer."""
+
+import argparse
+import logging
+import os
+import platform
+import stat
+import sys
+import shutil
+import subprocess
+from typing import Dict
+
+import pw_arduino_build.file_operations as file_operations
+
+_LOG = logging.getLogger(__name__)
+
+# yapf: disable
+_ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
+ # pylint: disable=line-too-long
+ "teensy": {
+ "Linux": {
+ "arduino-ide": {
+ "url": "https://downloads.arduino.cc/arduino-1.8.13-linux64.tar.xz",
+ "file_name": "arduino-1.8.13-linux64.tar.xz",
+ "sha256": "1b20d0ec850a2a63488009518725f058668bb6cb48c321f82dcf47dc4299b4ad",
+ },
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux64",
+ "file_name": "TeensyduinoInstall.linux64",
+ "sha256": "2e6cd99a757bc80593ea3de006de4cc934bcb0a6ec74cad8ec327f0289d40f0b",
+ },
+ },
+ # TODO(tonymd): Handle 32-bit Linux Install?
+ "Linux32": {
+ "arduino-ide": {
+ "url": "https://downloads.arduino.cc/arduino-1.8.13-linux32.tar.xz",
+ "file_name": "arduino-1.8.13-linux32.tar.xz",
+ "sha256": "",
+ },
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux32",
+ "file_name": "TeensyduinoInstall.linux32",
+ "sha256": "",
+ },
+ },
+ # TODO(tonymd): Handle ARM32 (Raspberry Pi) Install?
+ "LinuxARM32": {
+ "arduino-ide": {
+ "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxarm.tar.xz",
+ "file_name": "arduino-1.8.13-linuxarm.tar.xz",
+ "sha256": "",
+ },
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxarm",
+ "file_name": "TeensyduinoInstall.linuxarm",
+ "sha256": "",
+ },
+ },
+ # TODO(tonymd): Handle ARM64 Install?
+ "LinuxARM64": {
+ "arduino-ide": {
+ "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxaarch64.tar.xz",
+ "file_name": "arduino-1.8.13-linuxaarch64.tar.xz",
+ "sha256": "",
+ },
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxaarch64",
+ "file_name": "TeensyduinoInstall.linuxaarch64",
+ "sha256": "",
+ },
+ },
+ "Darwin": {
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/Teensyduino_MacOS_Catalina.zip",
+ "file_name": "Teensyduino_MacOS_Catalina.zip",
+ "sha256": "401ef42c6e83e621cdda20191a4ef9b7db8a214bede5a94a9e26b45f79c64fe2",
+ },
+ },
+ "Windows": {
+ "arduino-ide": {
+ "url": "https://downloads.arduino.cc/arduino-1.8.13-windows.zip",
+ "file_name": "arduino-1.8.13-windows.zip",
+ "sha256": "78d3e96827b9e9b31b43e516e601c38d670d29f12483e88cbf6d91a0f89ef524",
+ },
+ "teensyduino": {
+ "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.exe",
+ "file_name": "TeensyduinoInstall.exe",
+ "sha256": "88f58681e5c4772c54e462bc88280320e4276e5b316dcab592fe38d96db990a1",
+ },
+ }
+ },
+ "adafruit-samd": {
+ "all": {
+ "core": {
+ "version": "1.6.2",
+ "url": "https://github.com/adafruit/ArduinoCore-samd/archive/1.6.2.tar.gz",
+ "sha256": "5875f5bc05904c10e6313f02653f28f2f716db639d3d43f5a1d8a83d15339d64",
+ }
+ },
+ "Linux": {},
+ "Darwin": {},
+ "Windows": {},
+ },
+ "arduino-samd": {
+ "all": {
+ "core": {
+ "version": "1.8.8",
+ "url": "http://downloads.arduino.cc/cores/samd-1.8.8.tar.bz2",
+ "file_name": "samd-1.8.8.tar.bz2",
+ "sha256": "7b93eb705cba9125d9ee52eba09b51fb5fe34520ada351508f4253abbc9f27fa",
+ }
+ },
+ "Linux": {},
+ "Darwin": {},
+ "Windows": {},
+ },
+ "stm32duino": {
+ "all": {
+ "core": {
+ "version": "1.9.0",
+ "url": "https://github.com/stm32duino/Arduino_Core_STM32/archive/1.9.0.tar.gz",
+ "sha256": "4f75ba7a117d90392e8f67c58d31d22393749b9cdd3279bc21e7261ec06c62bf",
+ }
+ },
+ "Linux": {},
+ "Darwin": {},
+ "Windows": {},
+ },
+}
+# yapf: enable
+
+
+def install_core_command(args: argparse.Namespace):
+ install_prefix = os.path.realpath(
+ os.path.expanduser(os.path.expandvars(args.prefix)))
+ install_dir = os.path.join(install_prefix, args.core_name)
+ cache_dir = os.path.join(install_prefix, ".cache", args.core_name)
+
+ if args.core_name in supported_cores():
+ shutil.rmtree(install_dir, ignore_errors=True)
+ os.makedirs(install_dir, exist_ok=True)
+ os.makedirs(cache_dir, exist_ok=True)
+
+ if args.core_name == "teensy":
+ if platform.system() == "Linux":
+ install_teensy_core_linux(install_prefix, install_dir, cache_dir)
+ elif platform.system() == "Darwin":
+ install_teensy_core_mac(install_prefix, install_dir, cache_dir)
+ elif platform.system() == "Windows":
+ install_teensy_core_windows(install_prefix, install_dir, cache_dir)
+ elif args.core_name == "adafruit-samd":
+ install_adafruit_samd_core(install_prefix, install_dir, cache_dir)
+ elif args.core_name == "stm32duino":
+ install_stm32duino_core(install_prefix, install_dir, cache_dir)
+ elif args.core_name == "arduino-samd":
+ install_arduino_samd_core(install_prefix, install_dir, cache_dir)
+ else:
+ _LOG.error("Invalid core '%s'. Supported cores: %s", args.core_name,
+ ", ".join(supported_cores()))
+ sys.exit(1)
+
+
+def supported_cores():
+ return _ARDUINO_CORE_ARTIFACTS.keys()
+
+
+def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
+ """Download and install Teensyduino artifacts for Windows."""
+ teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
+
+ arduino_artifact = teensy_artifacts["arduino-ide"]
+ arduino_zipfile = file_operations.download_to_cache(
+ url=arduino_artifact["url"],
+ expected_sha256sum=arduino_artifact["sha256"],
+ cache_directory=cache_dir)
+
+ teensyduino_artifact = teensy_artifacts["teensyduino"]
+ teensyduino_installer = file_operations.download_to_cache(
+ url=teensyduino_artifact["url"],
+ expected_sha256sum=teensyduino_artifact["sha256"],
+ cache_directory=cache_dir)
+
+ file_operations.extract_archive(arduino_zipfile, install_dir, cache_dir)
+
+ # "teensy" here should match args.core_name
+ teensy_core_dir = os.path.join(install_prefix, "teensy")
+
+ # Change working directory for installation
+ original_working_dir = os.getcwd()
+ os.chdir(install_prefix)
+
+ _LOG.info("Installing Teensyduino to: %s", teensy_core_dir)
+
+ install_command = [teensyduino_installer, "--dir=teensy"]
+ _LOG.info(" Running: %s", " ".join(install_command))
+ _LOG.info(" Please click yes on the Windows 'User Account Control' "
+ "dialog.")
+ _LOG.info(" You should see: 'Verified publisher: PRJC.COM LLC'")
+
+ install_command = [teensyduino_installer, "--dir=teensy"]
+ subprocess.run(install_command)
+ if not os.path.exists(os.path.join(teensy_core_dir, "hardware", "teensy")):
+ _LOG.error(
+ "Error: Installation Failed.\n"
+ "Please try again and ensure Teensyduino is installed in "
+ "the folder:\n"
+ "%s", teensy_core_dir)
+ sys.exit(1)
+ else:
+ _LOG.info(" Install complete!")
+
+ file_operations.remove_empty_directories(install_dir)
+ os.chdir(original_working_dir)
+
+
+def install_teensy_core_mac(unused_install_prefix, install_dir, cache_dir):
+ """Download and install Teensyduino artifacts for Mac."""
+ teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
+
+ teensyduino_artifact = teensy_artifacts["teensyduino"]
+ teensyduino_zip = file_operations.download_to_cache(
+ url=teensyduino_artifact["url"],
+ expected_sha256sum=teensyduino_artifact["sha256"],
+ cache_directory=cache_dir)
+
+ extracted_files = file_operations.extract_archive(
+ teensyduino_zip,
+ install_dir,
+ cache_dir,
+ remove_single_toplevel_folder=False)
+ toplevel_folder = sorted(extracted_files)[0]
+ os.symlink(os.path.join(toplevel_folder, "Contents", "Java", "hardware"),
+ os.path.join(install_dir, "hardware"),
+ target_is_directory=True)
+
+
+def install_teensy_core_linux(install_prefix, install_dir, cache_dir):
+ """Download and install Teensyduino artifacts for Windows."""
+ teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
+
+ arduino_artifact = teensy_artifacts["arduino-ide"]
+ arduino_tarfile = file_operations.download_to_cache(
+ url=arduino_artifact["url"],
+ expected_sha256sum=arduino_artifact["sha256"],
+ cache_directory=cache_dir)
+
+ teensyduino_artifact = teensy_artifacts["teensyduino"]
+ teensyduino_installer = file_operations.download_to_cache(
+ url=teensyduino_artifact["url"],
+ expected_sha256sum=teensyduino_artifact["sha256"],
+ cache_directory=cache_dir)
+
+ extracted_files = file_operations.extract_archive(arduino_tarfile,
+ install_dir, cache_dir)
+ os.chmod(teensyduino_installer,
+ os.stat(teensyduino_installer).st_mode | stat.S_IEXEC)
+
+ original_working_dir = os.getcwd()
+ os.chdir(install_prefix)
+ # "teensy" here should match args.core_name
+ install_command = [teensyduino_installer, "--dir=teensy"]
+ subprocess.run(install_command)
+
+ # Remove original arduino IDE files
+ for efile in extracted_files:
+ if efile.is_file():
+ efile.unlink()
+
+ file_operations.remove_empty_directories(install_dir)
+ os.chdir(original_working_dir)
+
+
+def install_arduino_samd_core(install_prefix: str, install_dir: str,
+ cache_dir: str):
+ artifacts = _ARDUINO_CORE_ARTIFACTS["arduino-samd"]["all"]["core"]
+ core_tarfile = file_operations.download_to_cache(
+ url=artifacts["url"],
+ expected_sha256sum=artifacts["sha256"],
+ cache_directory=cache_dir)
+
+ package_path = os.path.join(install_dir, "hardware", "samd",
+ artifacts["version"])
+ os.makedirs(package_path, exist_ok=True)
+ file_operations.extract_archive(core_tarfile, package_path, cache_dir)
+ original_working_dir = os.getcwd()
+ os.chdir(install_prefix)
+ # TODO(tonymd): Fetch core/tools as specified by:
+ # http://downloads.arduino.cc/packages/package_index.json
+ os.chdir(original_working_dir)
+ return True
+
+
+def install_adafruit_samd_core(install_prefix: str, install_dir: str,
+ cache_dir: str):
+ artifacts = _ARDUINO_CORE_ARTIFACTS["adafruit-samd"]["all"]["core"]
+ core_tarfile = file_operations.download_to_cache(
+ url=artifacts["url"],
+ expected_sha256sum=artifacts["sha256"],
+ cache_directory=cache_dir)
+
+ package_path = os.path.join(install_dir, "hardware", "samd",
+ artifacts["version"])
+ os.makedirs(package_path, exist_ok=True)
+ file_operations.extract_archive(core_tarfile, package_path, cache_dir)
+
+ original_working_dir = os.getcwd()
+ os.chdir(install_prefix)
+ # TODO(tonymd): Fetch platform specific tools as specified by:
+ # https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
+ # Specifically:
+ # https://github.com/ARM-software/CMSIS_5/archive/5.4.0.tar.gz
+ os.chdir(original_working_dir)
+ return True
+
+
+def install_stm32duino_core(install_prefix, install_dir, cache_dir):
+ artifacts = _ARDUINO_CORE_ARTIFACTS["stm32duino"]["all"]["core"]
+ core_tarfile = file_operations.download_to_cache(
+ url=artifacts["url"],
+ expected_sha256sum=artifacts["sha256"],
+ cache_directory=cache_dir)
+
+ package_path = os.path.join(install_dir, "hardware", "stm32",
+ artifacts["version"])
+ os.makedirs(package_path, exist_ok=True)
+ file_operations.extract_archive(core_tarfile, package_path, cache_dir)
+ original_working_dir = os.getcwd()
+ os.chdir(install_prefix)
+ # TODO(tonymd): Fetch platform specific tools as specified by:
+ # https://github.com/stm32duino/BoardManagerFiles/raw/master/STM32/package_stm_index.json
+ os.chdir(original_working_dir)
+ return True
diff --git a/pw_arduino_build/py/pw_arduino_build/file_operations.py b/pw_arduino_build/py/pw_arduino_build/file_operations.py
new file mode 100644
index 0000000..2bcdb69
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/file_operations.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""File Helper Functions."""
+
+import glob
+import hashlib
+import logging
+import os
+import shutil
+import sys
+import tarfile
+import urllib.request
+import zipfile
+from pathlib import Path
+from typing import List
+
+_LOG = logging.getLogger(__name__)
+
+
+class InvalidChecksumError(Exception):
+ pass
+
+
+def find_files(starting_dir: str,
+ patterns: List[str],
+ directories_only=False) -> List[str]:
+ original_working_dir = os.getcwd()
+ if not (os.path.exists(starting_dir) and os.path.isdir(starting_dir)):
+ _LOG.error("Directory '%s' does not exist.", starting_dir)
+ raise FileNotFoundError
+
+ os.chdir(starting_dir)
+ files = []
+ for pattern in patterns:
+ for file_path in glob.glob(pattern, recursive=True):
+ if not directories_only or (directories_only
+ and os.path.isdir(file_path)):
+ files.append(file_path)
+ os.chdir(original_working_dir)
+ return sorted(files)
+
+
+def sha256_sum(file_name):
+ hash_sha256 = hashlib.sha256()
+ with open(file_name, "rb") as file_handle:
+ for chunk in iter(lambda: file_handle.read(4096), b""):
+ hash_sha256.update(chunk)
+ return hash_sha256.hexdigest()
+
+
+def md5_sum(file_name):
+ hash_md5 = hashlib.md5()
+ with open(file_name, "rb") as file_handle:
+ for chunk in iter(lambda: file_handle.read(4096), b""):
+ hash_md5.update(chunk)
+ return hash_md5.hexdigest()
+
+
+def verify_file_checksum(file_path,
+ expected_checksum,
+ sum_function=sha256_sum):
+ downloaded_checksum = sum_function(file_path)
+ try:
+ if downloaded_checksum != expected_checksum:
+ raise InvalidChecksumError
+ except InvalidChecksumError:
+ _LOG.exception("Invalid %s\n"
+ "%s %s\n"
+ "%s (expected)",
+ sum_function.__name__, downloaded_checksum,
+ os.path.basename(file_path), expected_checksum)
+ # Exit to stop installation
+ return sys.exit(1)
+
+ _LOG.info(" %s:", sum_function.__name__)
+ _LOG.info(" %s %s", downloaded_checksum, os.path.basename(file_path))
+ return True
+
+
+def download_to_cache(url: str,
+ expected_md5sum=None,
+ expected_sha256sum=None,
+ cache_directory=".cache") -> str:
+
+ cache_dir = os.path.realpath(
+ os.path.expanduser(os.path.expandvars(cache_directory)))
+ downloaded_file = os.path.join(cache_dir, url.split("/")[-1])
+
+ if not os.path.exists(downloaded_file):
+ _LOG.info("Downloading: %s", url)
+ urllib.request.urlretrieve(url, filename=downloaded_file)
+
+ if os.path.exists(downloaded_file):
+ _LOG.info("Downloaded: %s", downloaded_file)
+ if expected_sha256sum:
+ verify_file_checksum(downloaded_file,
+ expected_sha256sum,
+ sum_function=sha256_sum)
+ elif expected_md5sum:
+ verify_file_checksum(downloaded_file,
+ expected_md5sum,
+ sum_function=md5_sum)
+
+ return downloaded_file
+
+
+def extract_zipfile(archive_file: str, dest_dir: str):
+ with zipfile.ZipFile(archive_file) as archive:
+ archive.extractall(path=dest_dir)
+
+
+def extract_tarfile(archive_file: str, dest_dir: str):
+ with tarfile.open(archive_file, 'r') as archive:
+ archive.extractall(path=dest_dir)
+
+
+def extract_archive(archive_file: str,
+ dest_dir: str,
+ cache_dir: str,
+ remove_single_toplevel_folder=True):
+ """Extract a tar or zip file.
+
+ Args:
+ archive_file (str): Absolute path to the archive file.
+ dest_dir (str): Extraction destination directory.
+ cache_dir (str): Directory where temp files can be created.
+ remove_single_toplevel_folder (bool): If the archive contains only a
+ single folder move the contents of that into the destination
+ directory.
+ """
+ # Make a temporary directory to extract files into
+ temp_extract_dir = os.path.join(cache_dir,
+ "." + os.path.basename(archive_file))
+ os.makedirs(temp_extract_dir, exist_ok=True)
+
+ _LOG.info("Extracting: %s", archive_file)
+ if zipfile.is_zipfile(archive_file):
+ extract_zipfile(archive_file, temp_extract_dir)
+ elif tarfile.is_tarfile(archive_file):
+ extract_tarfile(archive_file, temp_extract_dir)
+ else:
+ _LOG.error("Unknown archive format: %s", archive_file)
+ return sys.exit(1)
+
+ _LOG.info("Installing into: %s", dest_dir)
+ path_to_extracted_files = temp_extract_dir
+
+ extracted_top_level_files = os.listdir(temp_extract_dir)
+ # Check if tarfile has only one folder
+ # If yes, make that the new path_to_extracted_files
+ if remove_single_toplevel_folder and len(extracted_top_level_files) == 1:
+ path_to_extracted_files = os.path.join(temp_extract_dir,
+ extracted_top_level_files[0])
+
+ # Move extracted files to dest_dir
+ extracted_files = os.listdir(path_to_extracted_files)
+ for file_name in extracted_files:
+ source_file = os.path.join(path_to_extracted_files, file_name)
+ dest_file = os.path.join(dest_dir, file_name)
+ shutil.move(source_file, dest_file)
+
+ # rm -rf temp_extract_dir
+ shutil.rmtree(temp_extract_dir, ignore_errors=True)
+
+ # Return List of extracted files
+ return list(Path(dest_dir).rglob("*"))
+
+
+def remove_empty_directories(directory):
+ """Recursively remove empty directories."""
+
+ for path in sorted(Path(directory).rglob("*"), reverse=True):
+ # If broken symlink
+ if path.is_symlink() and not path.exists():
+ path.unlink()
+ # if empty directory
+ elif path.is_dir() and len(os.listdir(path)) == 0:
+ path.rmdir()
diff --git a/pw_arduino_build/py/pw_arduino_build/log.py b/pw_arduino_build/py/pw_arduino_build/log.py
new file mode 100644
index 0000000..d5ed9f7
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/log.py
@@ -0,0 +1,39 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Configure the system logger for the default pw command log format."""
+
+import logging
+
+_LOG = logging.getLogger(__name__)
+_STDERR_HANDLER = logging.StreamHandler()
+
+
+def install(level: int = logging.INFO) -> None:
+ """Configure the system logger for the arduino_builder log format."""
+
+ # Set log level on root logger to debug, otherwise any higher levels
+ # elsewhere are ignored.
+ root = logging.getLogger()
+ root.setLevel(logging.DEBUG)
+
+ _STDERR_HANDLER.setLevel(level)
+ _STDERR_HANDLER.setFormatter(
+ logging.Formatter("[%(asctime)s] "
+ "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"))
+ root.addHandler(_STDERR_HANDLER)
+
+
+def set_level(log_level: int):
+ """Sets the log level for logs to stderr."""
+ _STDERR_HANDLER.setLevel(log_level)
diff --git a/pw_arduino_build/py/setup.py b/pw_arduino_build/py/setup.py
index 5c47295..1cb074f 100644
--- a/pw_arduino_build/py/setup.py
+++ b/pw_arduino_build/py/setup.py
@@ -24,8 +24,7 @@
packages=setuptools.find_packages(),
entry_points={
'console_scripts':
- ['arduino_builder = '
- ' pw_arduino_build.arduinobuilder:main']
+ ['arduino_builder = pw_arduino_build.__main__:main']
},
install_requires=[
'pyserial',
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
index 7459ba8..93939d0 100644
--- a/targets/arduino/BUILD.gn
+++ b/targets/arduino/BUILD.gn
@@ -31,32 +31,32 @@
config("arduino_build") {
# Debug: Print out arduinobuilder.py args
- # print(string_join(" ", [rebase_path(arduino_builder_script)] + arduino_global_args))
+ # print(string_join(" ", [rebase_path(arduino_builder_script)] + arduino_show_command_args))
# Run prebuilds
# TODO(tonymd) This only needs to be run once but it's happening multiple times.
exec_script(arduino_builder_script,
- arduino_global_args + [ "--run-prebuilds" ],
+ arduino_run_command_args + [ "--run-prebuilds" ],
"string")
_exclude_flags = [ "-std=gnu++14" ]
_cflags = exec_script(arduino_builder_script,
- arduino_global_args + [ "--c-flags" ],
+ arduino_show_command_args + [ "--c-flags" ],
"list lines")
cflags = filter_exclude(_cflags, _exclude_flags)
asmflags = exec_script(arduino_builder_script,
- arduino_global_args + [ "--s-flags" ],
+ arduino_show_command_args + [ "--s-flags" ],
"list lines")
_cflags_cc = exec_script(arduino_builder_script,
- arduino_global_args + [ "--cpp-flags" ],
+ arduino_show_command_args + [ "--cpp-flags" ],
"list lines")
cflags_cc = filter_exclude(_cflags_cc, _exclude_flags)
_ldflags = exec_script(arduino_builder_script,
- arduino_global_args + [ "--ld-flags" ],
+ arduino_show_command_args + [ "--ld-flags" ],
"list lines")
ldflags =
filter_exclude(_ldflags,
diff --git a/targets/arduino/target_docs.rst b/targets/arduino/target_docs.rst
index a583539..8b91b4e 100644
--- a/targets/arduino/target_docs.rst
+++ b/targets/arduino/target_docs.rst
@@ -140,21 +140,20 @@
ninja -C out arduino
SERIAL_PORT=/dev/ttyACM0
+
for f in $(find out/arduino_debug/obj/ -iname "*.elf"); do
BUILD_PATH=$(dirname $f)
PROJECT_NAME=$(basename -s .elf $f)
- COMMON_ARGS="--quiet --arduino-package-path ./third_party/arduino/cores/teensy
- --arduino-package-name teensy/avr
- --compiler-path-override ./.environment/cipd/pigweed/bin
- show
- --build-path ${BUILD_PATH} --build-project-name ${PROJECT_NAME}
- --board teensy40 --menu-options menu.usb.serial menu.keys.en-us"
- echo "==> OBJCOPY" $f
- arduino_builder $COMMON_ARGS --run-objcopy
- # Optional
- # arduino_builder $COMMON_ARGS --run-postbuild
- echo "==> FLASH" $f
- arduino_builder --serial-port $SERIAL_PORT $COMMON_ARGS --run-upload-command teensyloader
+ CORE_ARGS="--quiet --arduino-package-path ./third_party/arduino/cores/teensy
+ --arduino-package-name teensy/avr
+ --compiler-path-override ./.environment/cipd/pigweed/bin"
+ BOARD_ARGS="--build-path ${BUILD_PATH} \
+ --build-project-name ${PROJECT_NAME}
+ --board teensy40
+ --menu-options menu.usb.serial menu.keys.en-us"
+ # Run objcopy, postbuild, and upload (flash) steps
+ arduino_builder $CORE_ARGS run $BOARD_ARGS --serial-port $SERIAL_PORT \
+ --run-objcopy --run-postbuild --run-upload-command teensyloader
while true; do
sleep .1; ls $SERIAL_PORT 2>/dev/null && break
done
diff --git a/third_party/arduino/BUILD.gn b/third_party/arduino/BUILD.gn
index d0adc06..7894f35 100644
--- a/third_party/arduino/BUILD.gn
+++ b/third_party/arduino/BUILD.gn
@@ -21,26 +21,34 @@
pw_source_set("arduino_core_sources") {
remove_configs = [ "$dir_pw_build:strict_warnings" ]
- _core_c_files = exec_script(arduino_builder_script,
- arduino_global_args + [ "--core-c-files" ],
- "list lines")
- _core_s_files = exec_script(arduino_builder_script,
- arduino_global_args + [ "--core-s-files" ],
- "list lines")
- _core_cpp_files = exec_script(arduino_builder_script,
- arduino_global_args + [ "--core-cpp-files" ],
- "list lines")
+ _core_c_files =
+ exec_script(arduino_builder_script,
+ arduino_show_command_args + [ "--core-c-files" ],
+ "list lines")
+ _core_s_files = filter_exclude(
+ exec_script(arduino_builder_script,
+ arduino_show_command_args + [ "--core-s-files" ],
+ "list lines"),
+ # TODO(tonymd): Conditionally remove this source file unless building for cortex-m0.
+ # Exception for adafruit-samd core
+ # pulse_asm.S is for: '.cpu cortex-m0plus .fpu softvfp'
+ [ "*pulse_asm.S\b" ])
+
+ _core_cpp_files =
+ exec_script(arduino_builder_script,
+ arduino_show_command_args + [ "--core-cpp-files" ],
+ "list lines")
_variant_c_files =
exec_script(arduino_builder_script,
- arduino_global_args + [ "--variant-c-files" ],
+ arduino_show_command_args + [ "--variant-c-files" ],
"list lines")
_variant_s_files =
exec_script(arduino_builder_script,
- arduino_global_args + [ "--variant-s-files" ],
+ arduino_show_command_args + [ "--variant-s-files" ],
"list lines")
_variant_cpp_files =
exec_script(arduino_builder_script,
- arduino_global_args + [ "--variant-cpp-files" ],
+ arduino_show_command_args + [ "--variant-cpp-files" ],
"list lines")
sources = _core_c_files + _core_s_files + _core_cpp_files +