From 59e057fbbf315419724bf71fe9c7f617248e7aee Mon Sep 17 00:00:00 2001 From: Pavel Macenauer Date: Wed, 15 Apr 2020 14:17:26 +0000 Subject: [PATCH] Integration of PyArmNN into CMake Change-Id: Ice37e693f4598a6b3c38bd38d89f1d35cdaa8a18 Signed-off-by: Pavel Macenauer --- CMakeLists.txt | 7 +- cmake/GlobalConfig.cmake | 32 ++++ python/pyarmnn/CMakeLists.txt | 58 +++++++ python/pyarmnn/LICENSE | 22 +++ python/pyarmnn/README.md | 171 ++++++++++++--------- python/pyarmnn/examples/onnx_mobilenetv2.py | 1 + .../examples/tflite_mobilenetv1_quantized.py | 1 + python/pyarmnn/init_devenv.sh | 28 ---- python/pyarmnn/scripts/download_test_resources.py | 3 + python/pyarmnn/scripts/generate_docs.py | 2 + python/pyarmnn/setup.py | 147 +++++++++++++----- python/pyarmnn/src/pyarmnn/_version.py | 24 ++- python/pyarmnn/swig_generate.py | 112 ++++++++++---- python/pyarmnn/test/test_setup.py | 9 +- python/pyarmnn/test/test_version.py | 5 +- 15 files changed, 441 insertions(+), 181 deletions(-) create mode 100644 python/pyarmnn/CMakeLists.txt create mode 100644 python/pyarmnn/LICENSE mode change 100644 => 100755 python/pyarmnn/examples/onnx_mobilenetv2.py mode change 100644 => 100755 python/pyarmnn/examples/tflite_mobilenetv1_quantized.py delete mode 100755 python/pyarmnn/init_devenv.sh mode change 100644 => 100755 python/pyarmnn/scripts/download_test_resources.py mode change 100644 => 100755 python/pyarmnn/scripts/generate_docs.py mode change 100644 => 100755 python/pyarmnn/setup.py diff --git a/CMakeLists.txt b/CMakeLists.txt index bf9f1e7..9998038 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ # -# Copyright © 2017 Arm Ltd. All rights reserved. +# Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT # cmake_minimum_required (VERSION 3.0.2) # 3.0.2 required for return() statement used in AddDllCopyCommands.cmake @@ -1085,3 +1086,7 @@ if(BUILD_GATORD_MOCK) endif() endif() + +if (BUILD_PYTHON_WHL OR BUILD_PYTHON_SRC) + add_subdirectory(python/pyarmnn) +endif() diff --git a/cmake/GlobalConfig.cmake b/cmake/GlobalConfig.cmake index 08cbb1b..968642b 100644 --- a/cmake/GlobalConfig.cmake +++ b/cmake/GlobalConfig.cmake @@ -1,3 +1,8 @@ +# +# Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP +# SPDX-License-Identifier: MIT +# option(BUILD_CAFFE_PARSER "Build Caffe parser" OFF) option(BUILD_TF_PARSER "Build Tensorflow parser" OFF) option(BUILD_ONNX_PARSER "Build Onnx parser" OFF) @@ -25,6 +30,8 @@ option(BUILD_GATORD_MOCK "Build the Gatord simulator for external profiling test option(BUILD_TIMELINE_DECODER "Build the Timeline Decoder for external profiling." ON) option(SHARED_BOOST "Use dynamic linking for boost libraries" OFF) option(BUILD_BASE_PIPE_SERVER "Build the server to handle external profiling pipe traffic" ON) +option(BUILD_PYTHON_WHL "Build Python wheel package" OFF) +option(BUILD_PYTHON_SRC "Build Python source package" OFF) include(SelectLibraryConfigurations) @@ -376,5 +383,30 @@ if(NOT BUILD_ARMNN_QUANTIZER) message(STATUS "ArmNN Quantizer support is disabled") endif() +if(NOT BUILD_PYTHON_WHL) + message(STATUS "PyArmNN wheel package is disabled") +endif() + +if(NOT BUILD_PYTHON_SRC) + message(STATUS "PyArmNN source package is disabled") +endif() + +if(BUILD_PYTHON_WHL OR BUILD_PYTHON_SRC) + find_package(PythonInterp 3 REQUIRED) + if(NOT ${PYTHONINTERP_FOUND}) + message(FATAL_ERROR "Python 3.x required to build PyArmNN, but not found") + endif() + + find_package(PythonLibs 3 REQUIRED) + if(NOT ${PYTHONLIBS_FOUND}) + message(FATAL_ERROR "Python 3.x development package required to build PyArmNN, but not found") + endif() + + find_package(SWIG 4 REQUIRED) + if(NOT ${SWIG_FOUND}) + message(FATAL_ERROR "SWIG 4.x requried to build PyArmNN, but not found") + endif() +endif() + # ArmNN source files required for all build options include_directories(SYSTEM third-party) diff --git a/python/pyarmnn/CMakeLists.txt b/python/pyarmnn/CMakeLists.txt new file mode 100644 index 0000000..5ae6ac2 --- /dev/null +++ b/python/pyarmnn/CMakeLists.txt @@ -0,0 +1,58 @@ +# +# Copyright 2020 NXP +# SPDX-License-Identifier: MIT +# +set(SETUP_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/setup.py") +set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py") +set(SWIG_GENERATE_IN "${CMAKE_CURRENT_SOURCE_DIR}/swig_generate.py") +set(SWIG_GENERATE "${CMAKE_CURRENT_BINARY_DIR}/swig_generate.py") +set(OUT_WRAP "${CMAKE_CURRENT_BINARY_DIR}/pyarmnn.wrap.timestamp") + +configure_file(${SETUP_PY_IN} ${SETUP_PY} COPYONLY) +configure_file(${SWIG_GENERATE_IN} ${SWIG_GENERATE} COPYONLY) + +# local env variables passed down to the python scripts +# scripts can thus be used standalone +set(ARMNN_ENV ARMNN_INCLUDE=${PROJECT_SOURCE_DIR}/include + ARMNN_LIB=${PROJECT_BINARY_DIR} + SWIG_EXECUTABLE=${SWIG_EXECUTABLE}) + +# common step - generates swig wrappers and builds the lib +add_custom_command(OUTPUT ${OUT_WRAP} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/README.md ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_BINARY_DIR}/src + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/test ${CMAKE_CURRENT_BINARY_DIR}/test + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Clearing Python build ..." + COMMAND ${Python3_EXECUTABLE} ${SETUP_PY} --quiet clean --all + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Generating SWIG wrappers ..." + COMMAND ${CMAKE_COMMAND} -E env ${ARMNN_ENV} ${Python3_EXECUTABLE} ${SWIG_GENERATE} + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Building Python extensions ..." + COMMAND ${CMAKE_COMMAND} -E env ${ARMNN_ENV} ${Python3_EXECUTABLE} ${SETUP_PY} --quiet build_ext --inplace + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/src/pyarmnn/_generated ${CMAKE_CURRENT_BINARY_DIR}/src/pyarmnn/_generated + COMMAND ${CMAKE_COMMAND} -E touch ${OUT_WRAP} + DEPENDS armnn + armnnOnnxParser + armnnCaffeParser + armnnTfParser + armnnTfLiteParser) + +# source package +if(BUILD_PYTHON_SRC) + set(OUT_SRC "${CMAKE_CURRENT_BINARY_DIR}/pyarmnn.src.timestamp") + add_custom_command(OUTPUT ${OUT_SRC} + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Building Python source package ..." + COMMAND ${CMAKE_COMMAND} -E env ${ARMNN_ENV} ${Python3_EXECUTABLE} ${SETUP_PY} --quiet sdist + COMMAND ${CMAKE_COMMAND} -E touch ${OUT_SRC} + DEPENDS ${OUT_WRAP}) +endif() +# wheel package +if(BUILD_PYTHON_WHL) + set(OUT_WHL "${CMAKE_CURRENT_BINARY_DIR}/pyarmnn.whl.timestamp") + add_custom_command(OUTPUT ${OUT_WHL} + COMMAND ${CMAKE_COMMAND} -E cmake_echo_color --green "Building Python binary package ..." + COMMAND ${CMAKE_COMMAND} -E env ${ARMNN_ENV} ${Python3_EXECUTABLE} ${SETUP_PY} --quiet bdist_wheel + COMMAND ${CMAKE_COMMAND} -E touch ${OUT_WHL} + DEPENDS ${OUT_WRAP}) +endif() +add_custom_target(pyarmnn ALL DEPENDS ${OUT_WRAP} ${OUT_SRC} ${OUT_WHL}) diff --git a/python/pyarmnn/LICENSE b/python/pyarmnn/LICENSE new file mode 100644 index 0000000..7e2243a --- /dev/null +++ b/python/pyarmnn/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 ARM Limited. +Copyright 2020 NXP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/pyarmnn/README.md b/python/pyarmnn/README.md index 25213bb..4e7311f 100644 --- a/python/pyarmnn/README.md +++ b/python/pyarmnn/README.md @@ -12,14 +12,102 @@ The [SWIG](http://www.swig.org/) project is used to generate the Arm NN python s The following diagram shows the conceptual architecture of this library: ![PyArmNN](./images/pyarmnn.png) +# Setup development environment + +Before, proceeding to the next steps, make sure that: + +1. You have Python 3.6+ installed system-side. The package is not compatible with older Python versions. +2. You have python3.6-dev installed system-side. This contains header files needed to build PyArmNN extension module. +3. In case you build Python from sources manually, make sure that the following libraries are installed and available in you system: +``python3.6-dev build-essential checkinstall libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev`` +4. Install SWIG 4.x. Only 3.x version is typically available in Linux package managers, so you will have to build it and install it from sources. It can be downloaded from the [SWIG project website](http://www.swig.org/download.html) or from [SWIG GitHub](https://github.com/swig/swig). To install it follow the guide on [SWIG GitHub](https://github.com/swig/swig/wiki/Getting-Started). + +## Setup virtual environment + +Now you can proceed with setting up workspace. It is recommended to create a python virtual environment, so you do not pollute your working folder: +```bash +python -m venv env +source env/bin/activate +``` + +You may run into missing python modules such as *wheel*. Make sure to install those using pip: +```bash +pip install wheel +``` + +## Build python distr + +Python supports source and binary distribution packages. + +Source distr contains setup.py script that is executed on the users machine during package installation. +When preparing binary distr (wheel), setup.py is executed on the build machine and the resulting package contains only the result +of the build (generated files and resources, test results etc). + +In our case, PyArmNN depends on Arm NN installation. Thus, binary distr will be linked with +the local build machine libraries and runtime. + +There are 2 ways to build the python packages. Either directly using the python scripts or using CMake. + +### CMake build + +The recommended aproach is to build PyArmNN together with Arm NN by adding the following options to your CMake command: +``` +-DBUILD_PYTHON_SRC=1 +-DBUILD_PYTHON_WHL=1 +``` +This will build either the source package or the wheel or both. Current project headers and build libraries will be used, so there is no need to provide them. + +SWIG is required to generate the wrappers. If CMake did not find the executable during the configure step or it has found an older version, you may provide it manually: +``` +-DSWIG_EXECUTABLE= +``` + +After the build finishes, you will find the python packages in `/python/pyarmnn/dist`. + +### Standalone build + +PyArmNN can also be built using the provided python scripts only. The advantage of that is that you may use prebuilt Arm NN libraries and it is generally much faster if you do not want to build all the Arm NN libraries. + +##### 1. Set environment: + +*ARMNN_INCLUDE* and *ARMNN_LIB* are mandatory and should point to Arm NN includes and libraries against which you will be generating the wrappers. *SWIG_EXECUTABLE* should only be set if you have multiple versions of SWIG installed or you used a custom location for your installation: +```bash +$ export SWIG_EXECUTABLE= +$ export ARMNN_INCLUDE= +$ export ARMNN_LIB= +``` + +##### 2. Clean and build SWIG wrappers: + +```bash +$ python setup.py clean --all +$ python swig_generate.py -v +$ python setup.py build_ext --inplace +``` +This step will put all generated files under `./src/pyarmnn/_generated` folder and can be used repeatedly to re-generate the wrappers. + +##### 4. Build the source package + +```bash +$ python setup.py sdist +``` +As the result you will get `./dist/pyarmnn-21.0.0.tar.gz` file. As you can see it is platform independent. + +##### 5. Build the binary package + +```bash +$ python setup.py bdist_wheel +``` +As the result you will get something like `./dist/pyarmnn-21.0.0-cp36-cp36m-linux_x86_64.whl` file. As you can see it is platform dependent. + # PyArmNN installation PyArmNN can be distributed as a source package or a binary package (wheel). Binary package is platform dependent, the name of the package will indicate the platform it was built for, e.g.: -* Linux x86 64bit machine: pyarmnn-20.2.0-cp36-cp36m-*linux_x86_64*.whl -* Linux Aarch 64 bit machine: pyarmnn-20.2.0-cp36-cp36m-*linux_aarch64*.whl +* Linux x86 64bit machine: pyarmnn-21.0.0-cp36-cp36m-*linux_x86_64*.whl +* Linux Aarch 64 bit machine: pyarmnn-21.0.0-cp36-cp36m-*linux_aarch64*.whl The source package is platform independent but installation involves compilation of Arm NN python extension. You will need to have g++ compatible with C++ 14 standard and a python development library installed on the build machine. @@ -37,7 +125,7 @@ $ gcc --print-search-dirs ``` Install PyArmNN from binary by pointing to the wheel file: ```bash -$ pip install /path/to/pyarmnn-20.2.0-cp36-cp36m-linux_aarch64.whl +$ pip install /path/to/pyarmnn-21.0.0-cp36-cp36m-linux_aarch64.whl ``` ## Installing from source package @@ -54,7 +142,7 @@ $ export ARMNN_INCLUDE=/path/to/headers Install PyArmNN as follows: ```bash -$ pip install /path/to/pyarmnn-20.2.0.tar.gz +$ pip install /path/to/pyarmnn-21.0.0.tar.gz ``` If PyArmNN installation script fails to find Arm NN libraries it will raise an error like this @@ -68,7 +156,7 @@ $ pip show pyarmnn You can also verify it by running the following and getting output similar to below: ```bash $ python -c "import pyarmnn as ann;print(ann.GetVersion())" -'20200200' +'21.0.0' ``` # PyArmNN API overview @@ -132,32 +220,18 @@ Afterwards simply execute the example scripts, e.g.: ```bash $ python tflite_mobilenetv1_quantized.py ``` -All resources are downloaded during execution, so if you do not have access to the internet, you may need to download these manually. `example_utils.py` contains code shared between the examples. - -# Setup development environment - -Before, proceeding to the next steps, make sure that: - -1. You have Python 3.6+ installed system-side. The package is not compatible with older Python versions. -2. You have python3.6-dev installed system-side. This contains header files needed to build PyArmNN extension module. -3. In case you build Python from sources manually, make sure that the following libraries are installed and available in you system: -``python3.6-dev build-essential checkinstall libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev`` -4. install SWIG, swig must be version 4.* - -## Setup virtual environment -Now you can proceed with setting up workspace: +All resources are downloaded during execution, so if you do not have access to the internet, you may need to download these manually. `example_utils.py` contains code shared between the examples. -1. Set environment variables ARMNN_LIB (pointing to Arm NN libraries) and ARMNN_INCLUDE (pointing to Arm NN headers) -2. Create development env using script ``source init_devenv.sh`` +## Tox for automation -## Generating SWIG wrappers -Before building package or running tests you need to generate SWIG wrappers based on the interface files. -It can be done with tox target 'gen': +To make things easier *tox* is available for automating individual tasks or running multiple commands at once such as generating wrappers, running unit tests using multiple python versions or generating documentation. To run it use: ```bash -$ tox -e gen +$ tox ``` +See *tox.ini* for the list of tasks. You may also modify it for your own purposes. To dive deeper into tox read through https://tox.readthedocs.io/en/latest/ + ## Running unit-tests Download resources required to run unit tests by executing the script in the scripts folder: @@ -174,50 +248,3 @@ or run tox which will do both: ```bash $ tox ``` - -## Build python distr - -Python supports source and binary distribution packages. - -Source distr contains setup.py script that is executed on the users machine during package installation. -When preparing binary distr (wheel), setup.py is executed on the build machine and the resulting package contains only the result -of the build (generated files and resources, test results etc). - -In our case, PyArmNN depends on Arm NN installation. Thus, binary distr will be linked with -the local build machine libraries and runtime. - -### Source distr - -```bash -$ python setup.py clean --all -$ python setup.py sdist -``` - -As the result you will get `./dist/pyarmnn-20.2.0.tar.gz` file. As you can see it is platform independent. - -### Wheel - -```bash -$ export ARMNN_LIB=... -$ export ARMNN_INCLUDE=... -$ python setup.py clean --all -$ python setup.py bdist_wheel -``` - -As the result you will get something like `./dist/pyarmnn-20.2.0-cp36-cp36m-linux_x86_64.whl` file. As you can see it is platform dependent. -This command will launch extension build thus you need to have SWIG wrappers generated before running it. - -## Regenerate SWIG stubs inplace - -If you need to regenerate wrappers based on the new swig interfaces files, you will need to clean existing build folders -first and then rebuild extension: -```bash -$ python setup.py clean --all -``` -```bash -$ export ARMNN_LIB=/path/to/armnn/lib -$ export ARMNN_INCLUDE=/path/to/armnn/include -$ python setup.py build_ext --inplace -``` -It will put all generated files under ./src/pyarmnn/_generated folder. -Thus, this command can be used to re-generate new extensions in development env. diff --git a/python/pyarmnn/examples/onnx_mobilenetv2.py b/python/pyarmnn/examples/onnx_mobilenetv2.py old mode 100644 new mode 100755 index b6d5d8c..5ba0849 --- a/python/pyarmnn/examples/onnx_mobilenetv2.py +++ b/python/pyarmnn/examples/onnx_mobilenetv2.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2020 NXP # SPDX-License-Identifier: MIT diff --git a/python/pyarmnn/examples/tflite_mobilenetv1_quantized.py b/python/pyarmnn/examples/tflite_mobilenetv1_quantized.py old mode 100644 new mode 100755 index 8cc5295..aa18a52 --- a/python/pyarmnn/examples/tflite_mobilenetv1_quantized.py +++ b/python/pyarmnn/examples/tflite_mobilenetv1_quantized.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # Copyright 2020 NXP # SPDX-License-Identifier: MIT diff --git a/python/pyarmnn/init_devenv.sh b/python/pyarmnn/init_devenv.sh deleted file mode 100755 index e7654a4..0000000 --- a/python/pyarmnn/init_devenv.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -e - -JENKINS_BUILD=0 -while getopts ":j" opt; do - case "$opt" in - j) JENKINS_BUILD=1 ;; - esac -done - -python3.6 -m venv toxenv -source toxenv/bin/activate -pip install virtualenv==16.3.0 tox - -export ARMNN_INCLUDE=$(pwd)/../../include -python ./swig_generate.py - -tox -e devenv -# If jenkins build, also run unit tests, generate docs, etc -if [ $JENKINS_BUILD == 1 ]; then - tox - tox -e doc -fi - -deactivate -rm -rf toxenv - -source env/bin/activate diff --git a/python/pyarmnn/scripts/download_test_resources.py b/python/pyarmnn/scripts/download_test_resources.py old mode 100644 new mode 100755 index b166ed7..63fe1e9 --- a/python/pyarmnn/scripts/download_test_resources.py +++ b/python/pyarmnn/scripts/download_test_resources.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# Copyright 2020 NXP +# SPDX-License-Identifier: MIT """Downloads and extracts resources for unit tests. It is mandatory to run this script prior to running unit tests. Resources are stored as a tar.gz or a tar.bz2 archive and diff --git a/python/pyarmnn/scripts/generate_docs.py b/python/pyarmnn/scripts/generate_docs.py old mode 100644 new mode 100755 index 66eff6d..d4bc750 --- a/python/pyarmnn/scripts/generate_docs.py +++ b/python/pyarmnn/scripts/generate_docs.py @@ -1,6 +1,8 @@ +#!/usr/bin/env python3 # Copyright © 2020 Arm Ltd. All rights reserved. # SPDX-License-Identifier: MIT """Generate PyArmNN documentation.""" + import os import tarfile diff --git a/python/pyarmnn/setup.py b/python/pyarmnn/setup.py old mode 100644 new mode 100755 index 5f81088..1c84e61 --- a/python/pyarmnn/setup.py +++ b/python/pyarmnn/setup.py @@ -1,8 +1,18 @@ +#!/usr/bin/env python3 # Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT +"""Python bindings for Arm NN + +PyArmNN is a python extension for Arm NN SDK providing an interface similar to Arm NN C++ API. +""" +__version__ = None +__arm_ml_version__ = None + import logging import os import sys +import subprocess from functools import lru_cache from pathlib import Path from itertools import chain @@ -14,20 +24,21 @@ from setuptools.command.build_ext import build_ext logger = logging.Logger(__name__) -__version__ = None -__arm_ml_version__ = None +DOCLINES = __doc__.split("\n") +LIB_ENV_NAME = "ARMNN_LIB" +INCLUDE_ENV_NAME = "ARMNN_INCLUDE" def check_armnn_version(*args): pass +__current_dir = os.path.dirname(os.path.realpath(__file__)) -exec(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src', 'pyarmnn', '_version.py')).read()) +exec(open(os.path.join(__current_dir, 'src', 'pyarmnn', '_version.py')).read()) class ExtensionPriorityBuilder(build_py): - """ - Runs extension builder before other stages. Otherwise generated files are not included to the distribution. + """Runs extension builder before other stages. Otherwise generated files are not included to the distribution. """ def run(self): @@ -36,6 +47,8 @@ class ExtensionPriorityBuilder(build_py): class ArmnnVersionCheckerExtBuilder(build_ext): + """Builds an extension (i.e. wrapper). Additionally checks for version. + """ def __init__(self, dist): super().__init__(dist) @@ -60,49 +73,84 @@ class ArmnnVersionCheckerExtBuilder(build_ext): super().copy_extensions_to_source() -def linux_gcc_lib_search(): +def linux_gcc_name(): + """Returns the name of the `gcc` compiler. Might happen that we are cross-compiling and the + compiler has a longer name. + + Args: + None + + Returns: + str: Name of the `gcc` compiler or None """ - Calls the `gcc` to get linker default system paths. + cc_env = os.getenv('CC') + if cc_env is not None: + if subprocess.Popen([cc_env, "--version"], stdout=subprocess.DEVNULL): + return cc_env + return "gcc" if subprocess.Popen(["gcc", "--version"], stdout=subprocess.DEVNULL) else None + + +def linux_gcc_lib_search(gcc_compiler_name: str = linux_gcc_name()): + """Calls the `gcc` to get linker default system paths. + + Args: + gcc_compiler_name(str): Name of the GCC compiler + Returns: - list of paths + list: A list of paths. + + Raises: + RuntimeError: If unable to find GCC. """ - cmd = 'gcc --print-search-dirs | grep libraries' - cmd_res = os.popen(cmd).read() - cmd_res = cmd_res.split('=') - if len(cmd_res) > 1: - return tuple(cmd_res[1].split(':')) - return None + if gcc_compiler_name is None: + raise RuntimeError("Unable to find gcc compiler") + cmd1 = subprocess.Popen([gcc_compiler_name, "--print-search-dirs"], stdout=subprocess.PIPE) + cmd2 = subprocess.Popen(["grep", "libraries"], stdin=cmd1.stdout, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + cmd1.stdout.close() + out, _ = cmd2.communicate() + out = out.decode("utf-8").split('=') + return tuple(out[1].split(':')) if len(out) > 0 else None + +def find_includes(armnn_include_env: str = INCLUDE_ENV_NAME): + """Searches for ArmNN includes. + + Args: + armnn_include_env(str): Environmental variable to use as path. -def find_includes(armnn_include_env: str = 'ARMNN_INCLUDE'): - armnn_include_path = os.getenv(armnn_include_env, '') - return [armnn_include_path] if armnn_include_path else ['/usr/local/include', '/usr/include'] + Returns: + list: A list of paths to include. + """ + armnn_include_path = os.getenv(armnn_include_env) + if armnn_include_path is not None and os.path.exists(armnn_include_path): + armnn_include_path = [armnn_include_path] + else: + armnn_include_path = ['/usr/local/include', '/usr/include'] + return armnn_include_path @lru_cache(maxsize=1) def find_armnn(lib_name: str, optional: bool = False, - armnn_libs_env: str = 'ARMNN_LIB', + armnn_libs_env: str = LIB_ENV_NAME, default_lib_search: tuple = linux_gcc_lib_search()): - """ - Searches for ArmNN installation on the local machine. + """Searches for ArmNN installation on the local machine. Args: - lib_name: lib name to find - optional: Do not fail if optional. Default is False - fail if library was not found. - armnn_include_env: custom environment variable pointing to ArmNN headers, default is 'ARMNN_INCLUDE' - armnn_libs_env: custom environment variable pointing to ArmNN libraries location, default is 'ARMNN_LIBS' - default_lib_search: list of paths to search for ArmNN if not found within path provided by 'ARMNN_LIBS' + lib_name(str): Lib name to find. + optional(bool): Do not fail if optional. Default is False - fail if library was not found. + armnn_libs_env(str): Custom environment variable pointing to ArmNN libraries location, default is 'ARMNN_LIBS' + default_lib_search(tuple): list of paths to search for ArmNN if not found within path provided by 'ARMNN_LIBS' env variable - Returns: - tuple containing name of the armnn libs, paths to the libs - """ - - armnn_lib_path = os.getenv(armnn_libs_env, "") - - lib_search = [armnn_lib_path] if armnn_lib_path else default_lib_search + tuple: Contains name of the armnn libs, paths to the libs. + Raises: + RuntimeError: If armnn libs are not found. + """ + armnn_lib_path = os.getenv(armnn_libs_env) + lib_search = [armnn_lib_path] if armnn_lib_path is not None else default_lib_search armnn_libs = dict(map(lambda path: (':{}'.format(path.name), path), chain.from_iterable(map(lambda lib_path: Path(lib_path).glob(lib_name), lib_search)))) @@ -117,8 +165,7 @@ def find_armnn(lib_name: str, class LazyArmnnFinderExtension(Extension): - """ - Derived from `Extension` this class adds ArmNN libraries search on the user's machine. + """Derived from `Extension` this class adds ArmNN libraries search on the user's machine. SWIG options and compilation flags are updated with relevant ArmNN libraries files locations (-L) and headers (-I). Search for ArmNN is executed only when attributes include_dirs, library_dirs, runtime_library_dirs, libraries or @@ -195,6 +242,7 @@ class LazyArmnnFinderExtension(Extension): def __hash__(self): return self.name.__hash__() + if __name__ == '__main__': # mandatory extensions pyarmnn_module = LazyArmnnFinderExtension('pyarmnn._generated._pyarmnn', @@ -232,11 +280,30 @@ if __name__ == '__main__': setup( name='pyarmnn', version=__version__, - author='Arm ltd', + author='Arm Ltd, NXP Semiconductors', author_email='support@linaro.org', - description='Arm NN python wrapper', - url='https://www.arm.com', + description=DOCLINES[0], + long_description="\n".join(DOCLINES[2:]), + url='https://mlplatform.org/', license='MIT', + keywords='armnn neural network machine learning', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Artificial Intelligence', + 'Topic :: Software Development', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], package_dir={'': 'src'}, packages=[ 'pyarmnn', @@ -245,8 +312,12 @@ if __name__ == '__main__': 'pyarmnn._tensor', 'pyarmnn._utilities' ], + data_files=[('', ['LICENSE'])], python_requires='>=3.5', install_requires=['numpy'], - cmdclass={'build_py': ExtensionPriorityBuilder, 'build_ext': ArmnnVersionCheckerExtBuilder}, + cmdclass={ + 'build_py': ExtensionPriorityBuilder, + 'build_ext': ArmnnVersionCheckerExtBuilder + }, ext_modules=extensions_to_build ) diff --git a/python/pyarmnn/src/pyarmnn/_version.py b/python/pyarmnn/src/pyarmnn/_version.py index c565520..e797248 100644 --- a/python/pyarmnn/src/pyarmnn/_version.py +++ b/python/pyarmnn/src/pyarmnn/_version.py @@ -1,8 +1,9 @@ # Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT import os -version_info = (20, 2, 0) +version_info = (21, 0, 0) __dev_version_env = os.getenv("PYARMNN_DEV_VER", "") @@ -16,11 +17,22 @@ if __dev_version_env: version_info = (*version_info, __dev_version) __version__ = '.'.join(str(c) for c in version_info) -__arm_ml_version__ = '2{:03d}{:02d}{:02d}'.format(version_info[0], version_info[1], version_info[2]) +__arm_ml_version__ = '{}.{}.{}'.format(version_info[0], version_info[1], version_info[2]) -def check_armnn_version(installed_armnn_version, expected_armnn_version=__arm_ml_version__): - expected_armnn_version = expected_armnn_version[:-2] # cut off minor patch version - installed_armnn_version = installed_armnn_version[:-2] # cut off minor patch version - assert expected_armnn_version == installed_armnn_version, \ +def check_armnn_version(installed_armnn_version: str, expected_armnn_version: str = __arm_ml_version__): + """Compares expected Arm NN version and Arm NN version used to build the package. + + Args: + installed_armnn_version (str): Arm NN version used to generate the package (e.g. 21.0.0) + expected_armnn_version (str): Expected Arm NN version + + Returns: + None + """ + expected = expected_armnn_version.split('.', 2) + installed = installed_armnn_version.split('.', 2) + + # only compare major and minor versions, not patch + assert (expected[0] == installed[0]) and (expected[1] == installed[1]), \ "Expected ArmNN version is {} but installed ArmNN version is {}".format(expected_armnn_version, installed_armnn_version) diff --git a/python/pyarmnn/swig_generate.py b/python/pyarmnn/swig_generate.py index b63afc5..d3488b7 100755 --- a/python/pyarmnn/swig_generate.py +++ b/python/pyarmnn/swig_generate.py @@ -1,64 +1,116 @@ +#!/usr/bin/env python3 # Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT -""" -This script executes SWIG commands to generate armnn and armnn version wrappers. +"""This script executes SWIG commands to generate armnn and armnn version wrappers. This script cannot be moved to ./script dir because it uses find_armnn function from setup.py script. Both scripts must be in the same folder. """ import os import re import subprocess -from pathlib import Path +import argparse from setup import find_includes -__current_dir = Path(__file__).parent.absolute() +__current_dir = os.path.dirname(os.path.realpath(__file__)) +__swig_exec = None +__verbose = False + +SWIG_EXEC_ENV = "SWIG_EXECUTABLE" + + +def get_swig_exec(swig_exec_env: str = SWIG_EXEC_ENV): + """Returns the swig command. Uses either an env variable or the `swig` command + and verifies it works. + Args: + swig_exec_env(str): Env variable pointing to the swig executable. + + Returns: + str: Path to swig executable. + + Raises: + RuntimeError: If unable to execute any version of swig. + """ + swig_exec = os.getenv(swig_exec_env) + if swig_exec is None: + swig_exec = "swig" + if subprocess.Popen([swig_exec, "-version"], stdout=subprocess.DEVNULL): + return swig_exec + else: + raise RuntimeError("Unable to execute swig.") -def check_swig_versoin(version: str): - proc = subprocess.Popen(["swig -version"], - stdout=subprocess.PIPE, shell=True) - result = proc.communicate()[0].decode("utf-8") + +def check_swig_version(expected_version: str): + """Checks version of swig. + + Args: + expected_version(str): String containing expected version. + + Returns: + bool: True if version is correct, False otherwise + """ + cmd = subprocess.Popen([__swig_exec, "-version"], stdout=subprocess.PIPE) + out, _ = cmd.communicate() pattern = re.compile(r"(?<=Version ).+(?=$)", re.MULTILINE) - match = pattern.search(result) + match = pattern.search(out.decode('utf-8')) if match: version_string = match.group(0).strip() - print(f"Swig version = {version_string}") - return version_string.startswith(version) + if __verbose: + print(f"SWIG version: {version_string}") + return version_string.startswith(expected_version) else: - print(f"Failed to find version string in 'swig -version':\n {result}") return False -def generate_wrap(name, extr_includes): - print(f'\nGenerating wrappers for {name}\n') - - code = os.system(f"swig -v -c++ -python" - f" -Wall" - f" -o {__current_dir}/src/pyarmnn/_generated/{name}_wrap.cpp " - f"-outdir {__current_dir}/src/pyarmnn/_generated " - f"{extr_includes} " - f"-I{__current_dir}/src/pyarmnn/swig " - f"{__current_dir}/src/pyarmnn/swig/{name}.i") - +def generate_wrap(name: str, extr_includes): + """Generates the python wrapper using swig. + + Args: + name(str): Name of the wrapper template. + extr_includes(str): Include paths. + + Raises: + RuntimeError: If wrapper fails to be generated. + """ + in_dir = os.path.join(__current_dir, "src", "pyarmnn", "swig") + out_dir = os.path.join(__current_dir, "src", "pyarmnn", "_generated") + if __verbose: + print(f"Generating wrap for {name} ...") + code = os.system(f"{__swig_exec} -c++ -python -Wall " + + "-o {} ".format(os.path.join(out_dir, f"{name}_wrap.cpp")) + + f"-outdir {out_dir} " + + f"{extr_includes} " + + f"-I{in_dir} " + + os.path.join(in_dir, f"{name}.i")) if code != 0: raise RuntimeError(f"Failed to generate {name} ext.") if __name__ == "__main__": - if not check_swig_versoin('4.'): + __swig_exec = get_swig_exec() + + # This check is redundant in case CMake is used, it's here for standalone use + if not check_swig_version('4.'): raise RuntimeError("Wrong swig version was found. Expected SWIG version is 4.x.x") armnn_includes = find_includes() - generate_wrap('armnn_version', f"-I{'-I'.join(armnn_includes)} ") - generate_wrap('armnn', f"-I{'-I'.join(armnn_includes)} ") + parser = argparse.ArgumentParser("Script to generate SWIG wrappers.") + parser.add_argument("-v", "--verbose", help="Verbose output.", action="store_true") + args = parser.parse_args() - generate_wrap('armnn_caffeparser', f"-I{'-I'.join(armnn_includes)} ") - generate_wrap('armnn_onnxparser', f"-I{'-I'.join(armnn_includes)} ") - generate_wrap('armnn_tfparser', f"-I{'-I'.join(armnn_includes)} ") - generate_wrap('armnn_tfliteparser', f"-I{'-I'.join(armnn_includes)} ") + __verbose = args.verbose + wrap_names = ['armnn_version', + 'armnn', + 'armnn_caffeparser', + 'armnn_onnxparser', + 'armnn_tfparser', + 'armnn_tfliteparser'] + for n in wrap_names: + generate_wrap(n, f"-I{'-I'.join(armnn_includes)} ") diff --git a/python/pyarmnn/test/test_setup.py b/python/pyarmnn/test/test_setup.py index 8396ca0..d1e6e0f 100644 --- a/python/pyarmnn/test/test_setup.py +++ b/python/pyarmnn/test/test_setup.py @@ -1,4 +1,5 @@ # Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT import os import sys @@ -86,15 +87,15 @@ def test_gcc_serch_path(): def test_armnn_version(): - check_armnn_version('20190800', '20190800') + check_armnn_version('21.0.0', '21.0.0') def test_incorrect_armnn_version(): with pytest.raises(AssertionError) as err: - check_armnn_version('20190800', '20190500') + check_armnn_version('21.0.0', '21.1.0') - assert 'Expected ArmNN version is 201905 but installed ArmNN version is 201908' in str(err.value) + assert 'Expected ArmNN version is 21.1.0 but installed ArmNN version is 21.0.0' in str(err.value) def test_armnn_version_patch_does_not_matter(): - check_armnn_version('20190800', '20190801') + check_armnn_version('21.0.0', '21.0.1') diff --git a/python/pyarmnn/test/test_version.py b/python/pyarmnn/test/test_version.py index 2ea0fd8..14a9154 100644 --- a/python/pyarmnn/test/test_version.py +++ b/python/pyarmnn/test/test_version.py @@ -1,4 +1,5 @@ # Copyright © 2020 Arm Ltd. All rights reserved. +# Copyright 2020 NXP # SPDX-License-Identifier: MIT import os import importlib @@ -17,7 +18,7 @@ def test_dev_version(): importlib.reload(v) - assert "20.2.0.dev1" == v.__version__ + assert "21.0.0.dev1" == v.__version__ del os.environ["PYARMNN_DEV_VER"] del v @@ -29,7 +30,7 @@ def test_arm_version_not_affected(): importlib.reload(v) - assert "20200200" == v.__arm_ml_version__ + assert "21.0.0" == v.__arm_ml_version__ del os.environ["PYARMNN_DEV_VER"] del v -- 2.7.4