areusch commented on a change in pull request #8380: URL: https://github.com/apache/tvm/pull/8380#discussion_r677863154
########## File path: apps/microtvm/zephyr/template_project/microtvm_api_server.py ########## @@ -0,0 +1,673 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +import collections +import enum +import fcntl +import logging +import os +import os.path +import pathlib +import queue +import re +import select +import shlex +import shutil +import subprocess +import sys +import tarfile +import tempfile +import threading +import time + +import serial +import serial.tools.list_ports +import yaml + +from tvm.micro.project_api import server + + +_LOG = logging.getLogger(__name__) + + +API_SERVER_DIR = pathlib.Path(os.path.dirname(__file__) or os.path.getcwd()) + + +BUILD_DIR = API_SERVER_DIR / "build" + + +MODEL_LIBRARY_FORMAT_RELPATH = "model.tar" + + +IS_TEMPLATE = not (API_SERVER_DIR / MODEL_LIBRARY_FORMAT_RELPATH).exists() + + +def check_call(cmd_args, *args, **kwargs): + cwd_str = "" if "cwd" not in kwargs else f" (in cwd: {kwargs['cwd']})" + _LOG.info("run%s: %s", cwd_str, " ".join(shlex.quote(a) for a in cmd_args)) + return subprocess.check_call(cmd_args, *args, **kwargs) + + +CACHE_ENTRY_RE = re.compile(r"(?P<name>[^:]+):(?P<type>[^=]+)=(?P<value>.*)") + + +CMAKE_BOOL_MAP = dict( + [(k, True) for k in ("1", "ON", "YES", "TRUE", "Y")] + + [(k, False) for k in ("0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND", "")] +) + + +class CMakeCache(collections.Mapping): + def __init__(self, path): + self._path = path + self._dict = None + + def __iter__(self): + return iter(self._dict) + + def __getitem__(self, key): + if self._dict is None: + self._dict = self._read_cmake_cache() + + return self._dict[key] + + def __len__(self): + return len(self._dict) + + def _read_cmake_cache(self): + """Read a CMakeCache.txt-like file and return a dictionary of values.""" + entries = collections.OrderedDict() + with open(self._path, encoding="utf-8") as f: + for line in f: + m = CACHE_ENTRY_RE.match(line.rstrip("\n")) + if not m: + continue + + if m.group("type") == "BOOL": + value = CMAKE_BOOL_MAP[m.group("value").upper()] + else: + value = m.group("value") + + entries[m.group("name")] = value + + return entries + + +CMAKE_CACHE = CMakeCache(BUILD_DIR / "CMakeCache.txt") + + +class BoardError(Exception): + """Raised when an attached board cannot be opened (i.e. missing /dev nodes, etc).""" + + +class BoardAutodetectFailed(Exception): + """Raised when no attached hardware is found matching the board= given to ZephyrCompiler.""" + + +def _get_flash_runner(): + flash_runner = CMAKE_CACHE.get("ZEPHYR_BOARD_FLASH_RUNNER") + if flash_runner is not None: + return flash_runner + + with open(CMAKE_CACHE["ZEPHYR_RUNNERS_YAML"]) as f: + doc = yaml.load(f, Loader=yaml.FullLoader) + return doc["flash-runner"] + + +def _get_device_args(options): + flash_runner = _get_flash_runner() + + if flash_runner == "nrfjprog": + return _get_nrf_device_args(options) + + if flash_runner == "openocd": + return _get_openocd_device_args(options) + + raise BoardError( + f"Don't know how to find serial terminal for board {CMAKE_CACHE['BOARD']} with flash " + f"runner {flash_runner}" + ) + + +# kwargs passed to usb.core.find to find attached boards for the openocd flash runner. +BOARD_USB_FIND_KW = { + "nucleo_l4r5zi": {"idVendor": 0x0483, "idProduct": 0x374B}, + "nucleo_f746zg": {"idVendor": 0x0483, "idProduct": 0x374B}, + "stm32f746g_disco": {"idVendor": 0x0483, "idProduct": 0x374B}, +} + + +def openocd_serial(options): + """Find the serial port to use for a board with OpenOCD flash strategy.""" + if "openocd_serial" in options: + return options["openocd_serial"] + + import usb # pylint: disable=import-outside-toplevel + + find_kw = BOARD_USB_FIND_KW[CMAKE_CACHE["BOARD"]] + boards = usb.core.find(find_all=True, **find_kw) + serials = [] + for b in boards: + serials.append(b.serial_number) + + if len(serials) == 0: + raise BoardAutodetectFailed(f"No attached USB devices matching: {find_kw!r}") + serials.sort() + + autodetected_openocd_serial = serials[0] + _LOG.debug("zephyr openocd driver: autodetected serial %s", serials[0]) + + return autodetected_openocd_serial + + +def _get_openocd_device_args(options): + return ["--serial", openocd_serial(options)] + + +def _get_nrf_device_args(options): + nrfjprog_args = ["nrfjprog", "--ids"] + nrfjprog_ids = subprocess.check_output(nrfjprog_args, encoding="utf-8") + if not nrfjprog_ids.strip("\n"): + raise BoardAutodetectFailed(f'No attached boards recognized by {" ".join(nrfjprog_args)}') + + boards = nrfjprog_ids.split("\n")[:-1] + if len(boards) > 1: + if options["nrfjprog_snr"] is None: + raise BoardError( + "Multiple boards connected; specify one with nrfjprog_snr=: " f'{", ".join(boards)}' + ) + + if str(options["nrfjprog_snr"]) not in boards: + raise BoardError( + f"nrfjprog_snr ({options['nrfjprog_snr']}) not found in {nrfjprog_args}: {boards}" + ) + + return ["--snr", options["nrfjprog_snr"]] + + if not boards: + return [] + + return ["--snr", boards[0]] + + +PROJECT_TYPES = [] +if IS_TEMPLATE: + for d in (API_SERVER_DIR / "src").iterdir(): + if d.is_dir(): + PROJECT_TYPES.append(d.name) + + +PROJECT_OPTIONS = [ + server.ProjectOption( + "extra_files", + help="If given, during generate_project, uncompress the tarball at this path into the project dir", + ), + server.ProjectOption( + "gdbserver_port", help=("If given, port number to use when running the local gdbserver") + ), + server.ProjectOption( + "nrfjprog_snr", + help=( + "When used with nRF targets, serial # of the " "attached board to use, from nrfjprog" + ), + ), + server.ProjectOption( + "openocd_serial", + help=("When used with OpenOCD targets, serial # of the " "attached board to use"), + ), + server.ProjectOption( + "project_type", + help="Type of project to generate.", + choices=tuple(PROJECT_TYPES), + ), + server.ProjectOption("verbose", help="Run build with verbose output"), + server.ProjectOption( + "west_cmd", + help=( + "Path to the west tool. If given, supersedes both the zephyr_base " + "option and ZEPHYR_BASE environment variable." + ), + ), + server.ProjectOption("zephyr_base", help="Path to the zephyr base directory."), + server.ProjectOption("zephyr_board", help="Name of the Zephyr board to build for"), +] + + +class Handler(server.ProjectAPIHandler): + def __init__(self): + super(Handler, self).__init__() + self._proc = None + + def server_info_query(self, tvm_version): + return server.ServerInfo( + platform_name="zephyr", + is_template=IS_TEMPLATE, + model_library_format_path="" + if IS_TEMPLATE + else (API_SERVER_DIR / MODEL_LIBRARY_FORMAT_RELPATH), + project_options=PROJECT_OPTIONS, + ) + + # These files and directories will be recursively copied into generated projects from the CRT. + CRT_COPY_ITEMS = ("include", "Makefile", "src") + + def _create_prj_conf(self, project_dir, options): + with open(project_dir / "prj.conf", "w") as f: + f.write( + "# For UART used from main().\n" + "CONFIG_RING_BUFFER=y\n" + "CONFIG_UART_CONSOLE=n\n" + "CONFIG_UART_INTERRUPT_DRIVEN=y\n" + "\n" + ) + f.write("# For TVMPlatformAbort().\n" "CONFIG_REBOOT=y\n" "\n") + + if True: # options["project_type"] == "host_driven": + f.write("# For RPC server C++ bindings.\n" "CONFIG_CPLUSPLUS=y\n" "\n") + + f.write("# For math routines\n" "CONFIG_NEWLIB_LIBC=y\n" "\n") + + f.write("# For models with floating point.\n" "CONFIG_FPU=y\n" "\n") + + main_stack_size = None + if self._is_qemu(options) and options["project_type"] == "host_driven": + main_stack_size = 1536 + + # Set main stack size, if needed. + if main_stack_size is not None: + f.write(f"CONFIG_MAIN_STACK_SIZE={main_stack_size}\n") + + f.write("# For random number generation.\n" "CONFIG_TEST_RANDOM_GENERATOR=y\n") + + if self._is_qemu(options): + f.write("CONFIG_TIMER_RANDOM_GENERATOR=y\n") + else: + f.write("CONFIG_ENTROPY_GENERATOR=y\n") + f.write("\n") + + API_SERVER_CRT_LIBS_TOKEN = "<API_SERVER_CRT_LIBS>" + + CRT_LIBS_BY_PROJECT_TYPE = { + "host_driven": "microtvm_rpc_server microtvm_rpc_common common", + "aot_demo": "aot_executor memory microtvm_rpc_common common", + } + + def generate_project(self, model_library_format_path, standalone_crt_dir, project_dir, options): + project_dir = pathlib.Path(project_dir) + # Make project directory. + project_dir.mkdir() + + # Copy ourselves to the generated project. TVM may perform further build steps on the generated project + # by launching the copy. + shutil.copy2(__file__, project_dir / os.path.basename(__file__)) + + # Place Model Library Format tarball in the special location, which this script uses to decide + # whether it's being invoked in a template or generated project. + project_model_library_format_tar_path = project_dir / MODEL_LIBRARY_FORMAT_RELPATH + shutil.copy2(model_library_format_path, project_model_library_format_tar_path) + + # Extract Model Library Format tarball.into <project_dir>/model. + extract_path = os.path.splitext(project_model_library_format_tar_path)[0] + with tarfile.TarFile(project_model_library_format_tar_path) as tf: + os.makedirs(extract_path) + tf.extractall(path=extract_path) + + if self._is_qemu(options): + shutil.copytree(API_SERVER_DIR / "qemu-hack", project_dir / "qemu-hack") + + # Populate CRT. + crt_path = project_dir / "crt" + crt_path.mkdir() + for item in self.CRT_COPY_ITEMS: + src_path = os.path.join(standalone_crt_dir, item) + dst_path = crt_path / item + if os.path.isdir(src_path): + shutil.copytree(src_path, dst_path) + else: + shutil.copy2(src_path, dst_path) + + # Populate Makefile. + with open(API_SERVER_DIR / "CMakeLists.txt.template", "r") as cmake_template_f: + with open(project_dir / "CMakeLists.txt", "w") as cmake_f: + for line in cmake_template_f: + if self.API_SERVER_CRT_LIBS_TOKEN in line: + crt_libs = self.CRT_LIBS_BY_PROJECT_TYPE[options["project_type"]] + line = line.replace("<API_SERVER_CRT_LIBS>", crt_libs) + + cmake_f.write(line) + + self._create_prj_conf(project_dir, options) + + # Populate crt-config.h + crt_config_dir = project_dir / "crt_config" + crt_config_dir.mkdir() + shutil.copy2( + API_SERVER_DIR / "crt_config" / "crt_config.h", crt_config_dir / "crt_config.h" + ) + + # Populate src/ + src_dir = project_dir / "src" + shutil.copytree(API_SERVER_DIR / "src" / options["project_type"], src_dir) + + # Populate extra_files + if options.get("extra_files_tar"): + with tarfile.open(options["extra_files_tar"], mode="r:*") as tf: + tf.extractall(project_dir) + + def build(self, options): + BUILD_DIR.mkdir() + + cmake_args = ["cmake", ".."] + if options.get("verbose"): + cmake_args.append("-DCMAKE_VERBOSE_MAKEFILE:BOOL=TRUE") + + if options.get("zephyr_base"): + cmake_args.append(f"-DZEPHYR_BASE:STRING={options['zephyr_base']}") + + cmake_args.append(f"-DBOARD:STRING={options['zephyr_board']}") + + check_call(cmake_args, cwd=BUILD_DIR) + + args = ["make", "-j2"] + if options.get("verbose"): + args.append("VERBOSE=1") + check_call(args, cwd=BUILD_DIR) + + @classmethod + def _is_qemu(cls, options): + return ( + "qemu" in options["zephyr_board"] + or + # NOTE: mps2_an521 naming breaks the rule that "qemu" is included when qemu is the + # launch method. + "mps2_an521" in options["zephyr_board"] Review comment: ah i see. the main reason i went away from it was that the parameter was named `zephyr_board`, so a new user would be confused why they need to add `-qemu` suffix. happy to entertain other parameter names if you have suggestions! -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
