diff --git a/nuclear/.cmake-format.py b/nuclear/.cmake-format.py new file mode 100644 index 0000000..6a414d7 --- /dev/null +++ b/nuclear/.cmake-format.py @@ -0,0 +1,261 @@ +# +# MIT License +# +# Copyright (c) 2019 NUbots +# +# This file is part of the NUbots codebase. +# See https://github.com/NUbots/NUbots for further info. +# +# 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. +# + +# ---------------------------------- +# Options affecting listfile parsing +# ---------------------------------- +with section("parse"): + + # Specify structure for custom cmake functions + additional_commands = { + "HeaderLibrary": {"kwargs": {"NAME": "*", "HEADER": "*", "PATH_SUFFIX": "*", "URL": "*"}}, + "ToolchainLibraryFinder": { + "kwargs": { + "NAME": "*", + "HEADER": "*", + "LIBRARY": "*", + "PATH_SUFFIX": "*", + "BINARY": "*", + "VERSION_FILE": "*", + "VERSION_BINARY_ARGUMENTS": "*", + "VERSION_REGEX": "*", + } + }, + "nuclear_module": { + "kwargs": {"LANGUAGE": "1", "INCLUDES": "*", "LIBRARIES": "*", "SOURCES": "*", "DATA_FILES": "*"} + }, + "GenerateNeutron": {"kwargs": {"PROTO": "1", "PARENT_DIR": "1", "BUILTIN_DIR": "1", "BUILTIN_OUTPUT_DIR": "1"}}, + } + + # Specify variable tags. + vartags = [] + + # Specify property tags. + proptags = [] + +# ----------------------------- +# Options affecting formatting. +# ----------------------------- +with section("format"): + + # How wide to allow formatted cmake files + line_width = 120 + + # How many spaces to tab for indent + tab_size = 2 + + # If an argument group contains more than this many sub-groups (parg or kwarg + # groups) then force it to a vertical layout. + max_subgroups_hwrap = 2 + + # If a positional argument group contains more than this many arguments, then + # force it to a vertical layout. + max_pargs_hwrap = 6 + + # If a cmdline positional group consumes more than this many lines without + # nesting, then invalidate the layout (and nest) + max_rows_cmdline = 2 + + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False + + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False + + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = True + + # If the trailing parenthesis must be 'dangled' on its on line, then align it + # to this reference: `prefix`: the start of the statement, `prefix-indent`: + # the start of the statement, plus one indentation level, `child`: align to + # the column of the arguments + dangle_align = "prefix" + + # If the statement spelling length (including space and parenthesis) is + # smaller than this amount, then force reject nested layouts. + min_prefix_chars = 4 + + # If the statement spelling length (including space and parenthesis) is larger + # than the tab width by more than this amount, then force reject un-nested + # layouts. + max_prefix_chars = 10 + + # If a candidate layout is wrapped horizontally but it exceeds this many + # lines, then reject the layout. + max_lines_hwrap = 2 + + # What style line endings to use in the output. + line_ending = "unix" + + # Format command names consistently as 'lower' or 'upper' case + command_case = "canonical" + + # Format keywords consistently as 'lower' or 'upper' case + keyword_case = "upper" + + # A list of command names which should always be wrapped + always_wrap = ["NUCLEAR_ROLE", "nuclear_role"] + + # If true, the argument lists which are known to be sortable will be sorted + # lexicographicall + enable_sort = True + + # If true, the parsers may infer whether or not an argument list is sortable + # (without annotation). + autosort = True + + # By default, if cmake-format cannot successfully fit everything into the + # desired linewidth it will apply the last, most agressive attempt that it + # made. If this flag is True, however, cmake-format will print error, exit + # with non-zero status code, and write-out nothing + require_valid_layout = False + + # A dictionary mapping layout nodes to a list of wrap decisions. See the + # documentation for more information. + layout_passes = {} + +# ------------------------------------------------ +# Options affecting comment reflow and formatting. +# ------------------------------------------------ +with section("markup"): + + # What character to use for bulleted lists + bullet_char = "*" + + # What character to use as punctuation after numerals in an enumerated list + enum_char = "." + + # If comment markup is enabled, don't reflow the first comment block in each + # listfile. Use this to preserve formatting of your copyright/license + # statements. + first_comment_is_literal = False + + # If comment markup is enabled, don't reflow any comment block which matches + # this (regex) pattern. Default is `None` (disabled). + literal_comment_pattern = None + + # Regular expression to match preformat fences in comments default= + # ``r'^\s*([`~]{3}[`~]*)(.*)$'`` + fence_pattern = "^\\s*([`~]{3}[`~]*)(.*)$" + + # Regular expression to match rulers in comments default= + # ``r'^\s*[^\w\s]{3}.*[^\w\s]{3}$'`` + ruler_pattern = "^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$" + + # If a comment line matches starts with this pattern then it is explicitly a + # trailing comment for the preceeding argument. Default is '#<' + explicit_trailing_pattern = "#<" + + # If a comment line starts with at least this many consecutive hash + # characters, then don't lstrip() them off. This allows for lazy hash rulers + # where the first hash char is not separated by space + hashruler_min_length = 10 + + # If true, then insert a space between the first hash char and remaining hash + # chars in a hash ruler, and normalize its length to fill the column + canonicalize_hashrulers = True + + # enable comment markup parsing and reflow + enable_markup = True + +# ---------------------------- +# Options affecting the linter +# ---------------------------- +with section("lint"): + + # a list of lint codes to disable + disabled_codes = [] + + # regular expression pattern describing valid function names + function_pattern = "[0-9a-z_]+" + + # regular expression pattern describing valid macro names + macro_pattern = "[0-9A-Z_]+" + + # regular expression pattern describing valid names for variables with global + # scope + global_var_pattern = "[0-9A-Z][0-9A-Z_]+" + + # regular expression pattern describing valid names for variables with global + # scope (but internal semantic) + internal_var_pattern = "_[0-9A-Z][0-9A-Z_]+" + + # regular expression pattern describing valid names for variables with local + # scope + local_var_pattern = "[0-9a-z_]+" + + # regular expression pattern describing valid names for privatedirectory + # variables + private_var_pattern = "_[0-9a-z_]+" + + # regular expression pattern describing valid names for publicdirectory + # variables + public_var_pattern = "[0-9A-Z][0-9A-Z_]+" + + # regular expression pattern describing valid names for keywords used in + # functions or macros + keyword_pattern = "[0-9A-Z_]+" + + # In the heuristic for C0201, how many conditionals to match within a loop in + # before considering the loop a parser. + max_conditionals_custom_parser = 2 + + # Require at least this many newlines between statements + min_statement_spacing = 1 + + # Require no more than this many newlines between statements + max_statement_spacing = 1 + max_returns = 6 + max_branches = 12 + max_arguments = 5 + max_localvars = 15 + max_statements = 50 + +# ------------------------------- +# Options affecting file encoding +# ------------------------------- +with section("encode"): + + # If true, emit the unicode byte-order mark (BOM) at the start of the file + emit_byteorder_mark = False + + # Specify the encoding of the input file. Defaults to utf-8 + input_encoding = "utf-8" + + # Specify the encoding of the output file. Defaults to utf-8. Note that cmake + # only claims to support utf-8 so be careful when using anything else + output_encoding = "utf-8" + +# ------------------------------------- +# Miscellaneous configurations options. +# ------------------------------------- +with section("misc"): + + # A dictionary containing any per-command configuration overrides. Currently + # only `command_case` is supported. + per_command = {"ToolchainLibraryFinder": {"command_case": "unchanged"}} diff --git a/nuclear/CMakeLists.txt b/nuclear/CMakeLists.txt index e6b8a6d..5284230 100644 --- a/nuclear/CMakeLists.txt +++ b/nuclear/CMakeLists.txt @@ -1,60 +1,125 @@ -# We use additional modules for the NUClear roles system -SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/") +#[[ +MIT License + +Copyright (c) 2016 NUbots + +This file is part of the NUbots codebase. +See https://github.com/NUbots/NUbots for further info. + +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. -# We need NUClear -FIND_PACKAGE(NUClear REQUIRED) -INCLUDE_DIRECTORIES(SYSTEM ${NUClear_INCLUDE_DIR}) +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. +]] + +# We use additional modules for the NUClear roles system +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules/") # Set to off to ignore building tests -OPTION(BUILD_TESTS "Builds all of the tests for each module." OFF) +option(BUILD_TESTS "Builds all of the tests for each module." OFF) -# Set to on to build as shared libraries -OPTION(NUCLEAR_SHARED_BUILD "Build each module as a separate shared library." ON) +# The ways we can link the libraries together +if(NOT NUCLEAR_LINK_TYPE) + set(NUCLEAR_LINK_TYPE + SHARED + CACHE STRING "Choose method to link the binary" FORCE + ) + # Set the possible values of build type for cmake-gui + set_property(CACHE NUCLEAR_LINK_TYPE PROPERTY STRINGS "SHARED" "STATIC" "OBJECT") +endif() -# Our banner file for placing at the top of the roles -SET(NUCLEAR_ROLE_BANNER_FILE "${CMAKE_CURRENT_SOURCE_DIR}/roles/banner.png" CACHE PATH "The path the banner to print at the start of each role execution") +# RPath variables use, i.e. don't skip the full RPATH for the build tree +set(CMAKE_SKIP_BUILD_RPATH FALSE) -# Our location of our nuclear roles directory -SET(NUCLEAR_ROLES_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CACHE PATH "The path to the nuclear roles system directory") +# Build the RPATH into the binary before install +set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) -SET(NUCLEAR_ADDITIONAL_SHARED_LIBRARIES "" CACHE STRING "Additional libraries used when linking roles, extensions, and utilities") +# Make OSX use the same RPATH as everyone else +set(CMAKE_MACOSX_RPATH ON) -SET(NUCLEAR_TEST_LIBRARIES "" CACHE STRING "Additional libraries used when linking module tests") +# Add some useful places to the RPATH. These will allow the binary to run from the build folder +set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_RPATH} lib/ ../lib/ bin/lib) -# Our variables that are used to locate the shared, module, and message folders -# They are given relative to the current project directory -SET(NUCLEAR_MODULE_DIR "module" CACHE PATH "The path to the module directory for NUClear") -SET(NUCLEAR_MESSAGE_DIR "shared/message" CACHE PATH "The path to the message directory for NUClear") -SET(NUCLEAR_UTILITY_DIR "shared/utility" CACHE PATH "The path to the utility dir for NUClear") -SET(NUCLEAR_EXTENSION_DIR "shared/extension" CACHE PATH "The path to the extension dir for NUClear") +# Our banner file for placing at the top of the roles +set(NUCLEAR_ROLE_BANNER_FILE + "${CMAKE_CURRENT_SOURCE_DIR}/roles/banner.png" + CACHE PATH "The path the banner to print at the start of each role execution" +) + +# We need -fPIC for all code +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Our variables that are used to locate the shared, module, and message folders They are given relative to the current +# project directory +set(NUCLEAR_MODULE_DIR + "module" + CACHE PATH "The path to the module directory for NUClear" +) +set(NUCLEAR_ROLES_DIR + "roles" + CACHE PATH "The path to the nuclear roles system directory" +) +set(NUCLEAR_SHARED_DIR + "shared" + CACHE PATH "The path to the shared directory for NUClear" +) +set(NUCLEAR_MESSAGE_DIR + "${NUCLEAR_SHARED_DIR}/message" + CACHE PATH "The path to the message directory for NUClear" +) +set(NUCLEAR_UTILITY_DIR + "${NUCLEAR_SHARED_DIR}/utility" + CACHE PATH "The path to the utility dir for NUClear" +) +set(NUCLEAR_EXTENSION_DIR + "${NUCLEAR_SHARED_DIR}/extension" + CACHE PATH "The path to the extension dir for NUClear" +) # You generally shouldn't have to change these -MARK_AS_ADVANCED(NUCLEAR_ROLE_BANNER_FILE - NUCLEAR_MODULE_DIR - NUCLEAR_MESSAGE_DIR - NUCLEAR_UTILITY_DIR - NUCLEAR_EXTENSION_DIR) +mark_as_advanced( + NUCLEAR_MODULE_DIR + NUCLEAR_ROLES_DIR + NUCLEAR_SHARED_DIR + NUCLEAR_MESSAGE_DIR + NUCLEAR_UTILITY_DIR + NUCLEAR_EXTENSION_DIR + NUCLEAR_ROLE_BANNER_FILE +) # Make our shared directory to output files too -FILE(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/shared) - -# Settings for the compiler to make NUClear work -INCLUDE(NUClearCompilerSettings) +file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/${NUCLEAR_SHARED_DIR}) # Add the subdirectory for our messages -ADD_SUBDIRECTORY("message") +add_subdirectory("message") -# Add the subdirectory for our utilities -# Is after messages as it can use messages -ADD_SUBDIRECTORY("utility") +# * Add the subdirectory for our utilities. +# * This is after messages as it can use messages +add_subdirectory("utility") # Add the subdirectory for our extensions -ADD_SUBDIRECTORY("extension") +add_subdirectory("extension") # Add the subdirectory for our roles -ADD_SUBDIRECTORY("roles") +add_subdirectory("roles") -# Add the subdirectory for module -# This must be after roles as roles determines which modules to build -ADD_SUBDIRECTORY("module") +# * Add the subdirectory for module +# * This must be after roles as roles determines which modules to build +add_subdirectory("module") +if(BUILD_TESTS) + add_subdirectory(tests) +endif(BUILD_TESTS) diff --git a/nuclear/README.md b/nuclear/README.md index 7917ae0..22ef210 100644 --- a/nuclear/README.md +++ b/nuclear/README.md @@ -1,4 +1,5 @@ # NUClear Roles + The NUClear roles system is a build and messaging system for the NUClear framework. It uses on CMake and Python to manage the construction of various executables made up of a selection of modules. These executables are called roles. @@ -8,15 +9,18 @@ Note that it utilises globbing to find the sources that are used for modules. So if you add or remove a file, make sure you rerun cmake so that it locates the new files. ## Setup + NUClear Roles is designed to exist as a part of another repository as either a git subtree or git submodule. In general subtrees are preferred to submodules as they allow you to make your own local changes to NUClear Roles and still be able to merge in upstream changes. To setup NUClearRoles as a subtree follow the following steps. - First create your repository where the NUClear Roles based system will live. - Once you have a repository to attach to run the following command from the root directory of the repository + ```bash git subtree add --prefix nuclear https://github.com/Fastcode/NUClearRoles.git master --squash ``` + - This will pull in NUClear Roles into the nuclear subdirectory ready for use by your system. Once you have added NUClearRoles to your codebase you must then configure a CMakeLists.txt file to use it. @@ -39,11 +43,13 @@ CMake code that depends on variables created by NUClear Roles should come after It is also recommended that you make a symlink from nuclear/b to ./b to make it easier to access the functionality of the b script. ### Dependencies + NUClear roles has several dependences that must be met before you are able to build the system. These dependencies are: - NUClear - Python3 with the following packages + - argparse - Pillow - stringcase - Optional dependencies: @@ -51,36 +57,41 @@ These dependencies are: - [Google Protobuf 3](https://developers.google.com/protocol-buffers/) (both c++ and python libraries) for Neutron messaging The Python dependencies can be installed using the provided requirements file: + ```bash pip3 install -r requirements.txt ``` ### Banner + NUClear roles generates an ansi coded banner at the top of ever role it runs. This banner file is created from an image file that is provided using the CMake cache variable `NUCLEAR_ROLE_BANNER_FILE` As the banner file is converted into ansi coloured unicode text, there are limitations on how the final result can look. To ensure that your banner looks good when rendered you should consider the following advice. + - Ensure that your image is 160px wide or less. - Many terminals when created are 80 columns wide. - The resolution of the created unicode text is half the resolution of the image. - This means a 160px wide image will make 80 columns of text. + Many terminals when created are 80 columns wide. + The resolution of the created unicode text is half the resolution of the image. + This means a 160px wide image will make 80 columns of text. - Try to make your logo in an editor using a 0.5 pixel aspect ratio. - The resolution of the image will be divided by four for the vertical axis. - This is done as text blocks are (almost) twice as high as they are wide. + The resolution of the image will be divided by four for the vertical axis. + This is done as text blocks are (almost) twice as high as they are wide. - Try to make smooth gradients. - When selecting colours for the text, each character can have 2 colours which are aranged into any combination of the 4 quadrants. - The unicode characters `█ ▄ ▐ ▞ ▟ ▚ ▌ ▙ ▀ ▜ ▛` are used to colour images so if the image has complex gradients the combination of these will look less clear. + When selecting colours for the text, each character can have 2 colours which are aranged into any combination of the 4 quadrants. + The unicode characters `█ ▄ ▐ ▞ ▟ ▚ ▌ ▙ ▀ ▜ ▛` are used to colour images so if the image has complex gradients the combination of these will look less clear. If you do not set your own banner file, ![the default banner](roles/banner.png) will be used ## Directories + There are six main directories that are used within the NUClear Roles system. + - `module` for where NUClear reactors - `message` for message types - `extension` for any NUClear DSL extensions @@ -90,6 +101,7 @@ There are six main directories that are used within the NUClear Roles system. - `tools` for any command line extensions for the `b` script ### Module + The module directory is where all NUClear Reactors are stored. This directory can be selected to be in a non default location by using the CMake cache variable `NUCLEAR_MODULE_DIR`. If this variable is not set it defaults to `module`. @@ -99,12 +111,14 @@ All modules must have the most outer namespace `module`. Take an example module `Camera` which exists in `namespace module::input`. This module must be located at `${NUCLEAR_MODULE_DIR}/input/Camera`. Within this module folder there are three directories that may hold code for the system. + - `${NUCLEAR_MODULE_DIR}/input/Camera/src` holds all of the source code for the module - This directory must contain a header file with the same name as the module followed by hpp, hh or h. E.g. Camera.h. This header must declare the NUClear::Reactor with the same name (Camera). - `${NUCLEAR_MODULE_DIR}/input/Camera/data` holds any non source code files. These will be copied to the build directory when building the code. - `${NUCLEAR_MODULE_DIR}/input/Camera/test` holds any unit test source code. ### Message + This directory can be selected to be in a non default location by using the CMake cache variable `NUCLEAR_MESSAGE_DIR`. If this variable is not set it defaults to `shared/message`. It is highly recommended that the message, utility and extension folders share a common parent folder. @@ -116,8 +130,8 @@ It must set the cmake cache variables `NUCLEAR_MESSAGE_LIBRARIES` to the librari If the message directory does not contain a CMakeLists.txt it will default to using the Neutron messaging system for messages. - #### Neutron Messaging System + While NUClear is able to use any c++ type as a message when emitting/triggering, it is advantageous to be able to serialise data in order to send data over a network or save to a file. The Neutron messaging system is designed to fill the gap using a system based on [Google Protocol Buffers](https://developers.google.com/protocol-buffers/). It uses protocol buffers for serialisation, however instead of using their c++ classes, it generates simplified structures to make it easier to use them within code. @@ -132,9 +146,10 @@ The messages sent to and from python are not serialised but instead, Neutron gen Note that the support for Python NUClear modules is still in alpha YMMV. ### Extension/Utility + These two directories are handled in a similarly to each other. -The extension directory can be selected to a non default location using the CMake cache variable `NUCLEAR_EXTENSION_DIR`. -The utility directory can be selected to a non default location using the CMake cache variable `NUCLEAR_UTILITY_DIR`. +The extension directory can be selected to a non default location using the CMake cache variable `NUCLEAR_EXTENSION_DIR`. +The utility directory can be selected to a non default location using the CMake cache variable `NUCLEAR_UTILITY_DIR`. If the directory contains a CMakeLists.txt file it will use this file to build the extensions/utilities for the system. This CMakeLists.txt file must ensure it sets `NUCLEAR_EXTENSION_INCLUDE_DIRS` and `NUCLEAR_EXTENSION_LIBRARIES` for extensions, or `NUCLEAR_UTILITY_INCLUDE_DIRS` and `NUCLEAR_UTILITY_LIBRARIES` for utilities. @@ -142,6 +157,7 @@ This CMakeLists.txt file must ensure it sets `NUCLEAR_EXTENSION_INCLUDE_DIRS` an If the folders do not contain a CMakeLists.txt file the default behaviour will be to build all c/cc/cpp files within that directory recursively. ### Roles + The NUClear roles directory contains a series of files named `.role` where `` is the name of the final binary that will be created. This directory can be selected to be in a non default location by using the CMake cache variable `NUCLEAR_ROLES_DIR`. If this variable is not set it defaults to `roles`. @@ -152,6 +168,7 @@ This is important to note as it means modules that have dependencies on other mo For example installing log handler modules should happen before installing other modules so their output can be seen. It will use this name to locate the module so the directory structure must match the name. An example of a role file would be: + ```cmake NUCLEAR_ROLE( # Some global modules that are useful @@ -163,10 +180,12 @@ NUCLEAR_ROLE( ) ``` + This role file would create an executable which had the modules `module::extension::FileWatcher`, `module::support::logging::ConsoleLogHandler` and `module::input::Camera`. This file is a cmake file so you are able to use # to declare comments. ## Tools and the `./b` script + NUClear Roles comes with a small python tool called `b` that lives in nuclear/b. This python tool is used to handle common functionality that you may wish to add to you system that takes advantage of information available in NUClear Roles. @@ -178,11 +197,13 @@ The ./b script will look for a `tools` directory above the location where the ac If it finds one, it will load all `.py` files in that directory and attempt to execute a register and run function on them. This allows the tools to register new functionality to the b script that can be accessed from the command line. For example three tools that exist in the NUbots codebase are: -- [this tool](https://github.com/NUbots/NUbots/blob/master/tools/install.py) installs NUClear roles systems onto remote systems using rsync -- [this tool](https://github.com/NUbots/NUbots/blob/master/tools/format.py) that formats all files using clang-format -- [this tool](https://github.com/NUbots/NUbots/blob/master/tools/decode.py) decodes `.nbs` files. + +- [this tool](https://github.com/NUbots/NUbots/blob/main/tools/install.py) installs NUClear roles systems onto remote systems using rsync +- [this tool](https://github.com/NUbots/NUbots/blob/main/tools/format.py) that formats all files using clang-format +- [this tool](https://github.com/NUbots/NUbots/blob/main/tools/decode.py) decodes `.nbs` files. Any of the tools that are created in this way have access to the b import which provides access to several useful variables + - `b.nuclear_dir` the directory that NUClear Roles is stored in - `b.project_dir` the directory above the NUClear Roles directory - `b.cmake_cache` the variables stored in the cmake cache. It attempts to find this either in the `cwd` or in the `build` @@ -190,6 +211,7 @@ Any of the tools that are created in this way have access to the b import which - `b.source_dir` the location of the project source directory as reported by the cmake cache ## NUClear Modules + `TODO` Variables @@ -199,6 +221,7 @@ SOURCES DATA_FILES ## Other Useful Variables + Additional there are options that can be set in NUClear Roles using CMake Cache variables. These options can influence how NUClear Roles generates systems. @@ -211,16 +234,17 @@ TODO descriptions ``` ## NUClear Binary Stream (nbs) files + To make it easier to serialise streams of messages for storage sharing and playback, NUClear Roles defines a format for serialising messages to files. This format is based on the Neutron messaging system and NUClear's networking protocol. An `nbs` file has the following frames repeated continuously. | Name | Type | Description | -|-----------|---------|-----------------------------------------------------------------------------------| +| --------- | ------- | --------------------------------------------------------------------------------- | | header | char[3] | the header sequence 0xE2, 0x98, 0xA2 (the radioactive symbol ☢ in utf-8) | | size | uint32 | the size of the frame after this byte in bytes | | timestamp | uint64 | the timestamp of this frame in microseconds. Does not have to be a utc timestamp. | | hash | uint64 | a 64 bit hash that identifies the type of the message | -| payload | char* | the serialised payload bytes | +| payload | char\* | the serialised payload bytes | All values within this format are little endian. diff --git a/nuclear/b.py b/nuclear/b.py index d408dd2..81df324 100755 --- a/nuclear/b.py +++ b/nuclear/b.py @@ -1,9 +1,38 @@ #!/usr/bin/env python3 -import sys -import os +# +# MIT License +# +# Copyright (c) 2013 NUbots +# +# This file is part of the NUbots codebase. +# See https://github.com/NUbots/NUbots for further info. +# +# 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. +# import argparse +import os import pkgutil import re +import subprocess +import sys + +from dependencies import find_dependency, install_dependency # Don't make .pyc files sys.dont_write_bytecode = True @@ -13,31 +42,31 @@ project_dir = os.path.dirname(nuclear_dir) # Get the tools directories to find b modules -nuclear_tools_path = os.path.join(nuclear_dir, 'tools') -user_tools_path = os.path.join(project_dir, 'tools') +nuclear_tools_path = os.path.join(nuclear_dir, "tools") +user_tools_path = os.path.join(project_dir, "tools") # Build our cmake cache cmake_cache = {} # Try to find our cmake cache file in the pwd -if os.path.isfile('CMakeCache.txt'): - with open('CMakeCache.txt', 'r') as f: +if os.path.isfile("CMakeCache.txt"): + with open("CMakeCache.txt", "r") as f: cmake_cache_text = f.readlines() # Look for a build directory else: - dirs = ['build'] + dirs = ["build", os.path.join(os.pardir, "build")] try: - dirs.extend([os.path.join('build', f) for f in os.listdir('build')]) + dirs.extend([os.path.join("build", f) for f in os.listdir("build")]) except FileNotFoundError: pass for d in dirs: - if os.path.isfile(os.path.join(project_dir, d, 'CMakeCache.txt')): - with open(os.path.join(project_dir, d, 'CMakeCache.txt'), 'r') as f: + if os.path.isfile(os.path.join(project_dir, d, "CMakeCache.txt")): + with open(os.path.join(project_dir, d, "CMakeCache.txt"), "r") as f: cmake_cache_text = f.readlines() - break; + break # If we still didn't find anything try: @@ -52,64 +81,131 @@ l = l.strip() # Remove lines that are comments - if len(l) > 0 and not l.startswith('//') and not l.startswith('#'): + if len(l) > 0 and not l.startswith("//") and not l.startswith("#"): # Extract our variable name from our values - g = re.match(r'([a-zA-Z_$][a-zA-Z_.$0-9-]*):(\w+)=(.*)', l).groups() + g = re.match(r"([a-zA-Z_$][a-zA-Z_.$0-9-]*):(\w+)=(.*)", l).groups() # Store our value and split it into a list if it is a list - cmake_cache[g[0]] = g[2] if ';' not in g[2].strip(';') else g[2].strip(';').split(';'); + cmake_cache[g[0]] = g[2] if ";" not in g[2].strip(";") else g[2].strip(";").split(";") # Try to find our source and binary directories try: - binary_dir = cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + '_BINARY_DIR'] + binary_dir = cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + "_BINARY_DIR"] except KeyError: binary_dir = None try: - source_dir = cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + '_SOURCE_DIR'] + source_dir = cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + "_SOURCE_DIR"] except: source_dir = project_dir if __name__ == "__main__": - if (binary_dir is not None): - # Print some information for the user - print("b script for", cmake_cache["CMAKE_PROJECT_NAME"]) - print("\tSource:", cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + '_SOURCE_DIR']) - print("\tBinary:", cmake_cache[cmake_cache["CMAKE_PROJECT_NAME"] + '_BINARY_DIR']) - print() - - # Add our builtin tools to the path and user tools - sys.path.append(nuclear_tools_path) - sys.path.append(user_tools_path) + # Prepend nuclear and user tools to the path, so we prefer our packages + sys.path.insert(0, nuclear_tools_path) + sys.path.insert(0, user_tools_path) # Root parser information - command = argparse.ArgumentParser(description='This script is an optional helper script for performing common tasks for working with the NUClear roles system.') - subcommands = command.add_subparsers(dest='command') - subcommands.help = "The command to run from the script. See each help for more information." - - # Get all of the packages that are in the build tools - modules = pkgutil.iter_modules(path=[nuclear_tools_path, user_tools_path]) - - # Our tools dictionary + command = argparse.ArgumentParser( + description="This script is an optional helper script for performing common tasks for working with the NUClear roles system." + ) + subcommands = command.add_subparsers( + dest="command", help="The command to run from the script. See each help for more information." + ) + subcommands.required = True + + # Look thorough our tools directories and find all the files and folders that could be a command + candidates = [] + for path in [user_tools_path, nuclear_tools_path]: + for dirpath, dnames, fnames in os.walk(path): + + # Get all the possible commands they might want to run based on folders and python files + candidates.extend( + [ + os.path.relpath(os.path.join(dirpath, os.path.splitext(f)[0]), path).split(os.sep) + for f in fnames + if f != "__init__.py" and os.path.splitext(f)[1] == ".py" + ] + ) + candidates.extend( + [ + os.path.relpath(os.path.join(dirpath, d), path).split(os.sep) + for d in dnames + if os.path.isfile(os.path.join(dirpath, d, "__init__.py")) + ] + ) + + # See if we can find a command that matches what we want to do and sort so the longest match is first + useable = [c for c in candidates if sys.argv[1 : len(c) + 1] == c] + useable.sort(key=lambda x: len(x), reverse=True) + + for components in useable: + if sys.argv[1 : len(components) + 1] == components: + loader = pkgutil.find_loader(".".join(components)) + if loader: + try: + module = loader.load_module() + if hasattr(module, "register") and hasattr(module, "run"): + + # Build up the base subcommands to this point + subcommand = subcommands + for c in components[:-1]: + subcommand = subcommand.add_parser(c).add_subparsers( + dest="{}_command".format(c), + help="Commands related to working with {} functionality".format(c), + ) + subcommand.required = True + + module.register(subcommand.add_parser(components[-1])) + module.run(**vars(command.parse_args())) + + # We're done, exit + exit(0) + + except ModuleNotFoundError as e: + print(f'missing command dependency "{e.name}"') + + dependency = find_dependency(e.name, user_tools_path) + package = dependency["version"] + + print(f'installing missing dependency "{package}"...') + print() + + install_dependency(package) + + # Try re-running the current command now that the library exists + sys.exit(subprocess.call([sys.executable, *sys.argv])) + + # If we reach this point, we couldn't find a tool to use. + # In this case we need to look through all the tools so we can register them all. + # This will provide a complete help for the function call so the user can try again tools = {} - - # Loop through all the modules we have to set them up in the parser - for loader, module_name, ispkg in modules: - - # Get our module, class name and registration function - module = loader.find_module(module_name).load_module(module_name) - tool = getattr(module, 'run') - register = getattr(module, 'register') - - # Let the tool register it's arguments - register(subcommands.add_parser(module_name)) - - # Associate our module_name with this tool - tools[module_name] = tool - - # Parse our arguments - args = command.parse_args() - - # Pass to our tool - tools[args.command](**vars(args)) + for components in candidates: + try: + loader = pkgutil.find_loader(".".join(components)) + if loader: + module = loader.load_module() + if hasattr(module, "register") and hasattr(module, "run"): + + subcommand = subcommands + tool = tools + for c in components[:-1]: + if c in tool: + tool, subcommand = tool[c] + else: + subcommand = subcommand.add_parser(c).add_subparsers( + dest="{}_command".format(c), + help="Commands related to working with {} functionality".format(c), + ) + subcommand.required = True + tool[c] = ({}, subcommand) + tool = tool[c][0] + + module.register(subcommand.add_parser(components[-1])) + except ModuleNotFoundError as e: + pass + except BaseException as e: + pass + + # Given what we know, this will fail here and give the user some help + command.parse_args() diff --git a/nuclear/cmake/Modules/FindEigen3.cmake b/nuclear/cmake/Modules/FindEigen3.cmake deleted file mode 100644 index f00d186..0000000 --- a/nuclear/cmake/Modules/FindEigen3.cmake +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2013-2016 Trent Houliston -# -# 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. - -INCLUDE(ToolchainLibraryFinder) -ToolchainLibraryFinder(NAME Eigen3 - HEADER eigen3/Eigen/Core -) diff --git a/nuclear/cmake/Modules/FindNUClear.cmake b/nuclear/cmake/Modules/FindNUClear.cmake deleted file mode 100644 index c442231..0000000 --- a/nuclear/cmake/Modules/FindNUClear.cmake +++ /dev/null @@ -1,5 +0,0 @@ -INCLUDE(ToolchainLibraryFinder) -ToolchainLibraryFinder(NAME NUClear - HEADER nuclear - LIBRARY nuclear -) diff --git a/nuclear/cmake/Modules/FindPythonLibsNew.cmake b/nuclear/cmake/Modules/FindPythonLibsNew.cmake index 894f530..6f9d969 100644 --- a/nuclear/cmake/Modules/FindPythonLibsNew.cmake +++ b/nuclear/cmake/Modules/FindPythonLibsNew.cmake @@ -1,79 +1,64 @@ -# - Find python libraries -# This module finds the libraries corresponding to the Python interpeter -# FindPythonInterp provides. -# This code sets the following variables: +# * Find python libraries This module finds the libraries corresponding to the Python interpeter FindPythonInterp +# provides. This code sets the following variables: # -# PYTHONLIBS_FOUND - have the Python libs been found -# PYTHON_PREFIX - path to the Python installation -# PYTHON_LIBRARIES - path to the python library -# PYTHON_INCLUDE_DIRS - path to where Python.h is found -# PYTHON_MODULE_EXTENSION - lib extension, e.g. '.so' or '.pyd' -# PYTHON_MODULE_PREFIX - lib name prefix: usually an empty string -# PYTHON_SITE_PACKAGES - path to installation site-packages -# PYTHON_IS_DEBUG - whether the Python interpreter is a debug build +# PYTHONLIBS_FOUND - have the Python libs been found PYTHON_PREFIX - path to the Python +# installation PYTHON_LIBRARIES - path to the python library PYTHON_INCLUDE_DIRS - path to where +# Python.h is found PYTHON_MODULE_EXTENSION - lib extension, e.g. '.so' or '.pyd' PYTHON_MODULE_PREFIX - lib +# name prefix: usually an empty string PYTHON_SITE_PACKAGES - path to installation site-packages PYTHON_IS_DEBUG - +# whether the Python interpreter is a debug build # -# Thanks to talljimbo for the patch adding the 'LDVERSION' config -# variable usage. +# Thanks to talljimbo for the patch adding the 'LDVERSION' config variable usage. -#============================================================================= -# Copyright 2001-2009 Kitware, Inc. -# Copyright 2012 Continuum Analytics, Inc. +# ============================================================================= +# Copyright 2001-2009 Kitware, Inc. Copyright 2012 Continuum Analytics, Inc. # # All rights reserved. # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +# following conditions are met: # -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. +# * Redistributions of source code must retain the above copyright notice, this list of conditions and the following +# disclaimer. # -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. +# * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with the distribution. # -# * Neither the names of Kitware, Inc., the Insight Software Consortium, -# nor the names of their contributors may be used to endorse or promote -# products derived from this software without specific prior written -# permission. +# * Neither the names of Kitware, Inc., the Insight Software Consortium, nor the names of their contributors may be used +# to endorse or promote products derived from this software without specific prior written permission. # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#============================================================================= +# ============================================================================= if(PYTHONLIBS_FOUND) - return() + return() endif() # Use the Python interpreter to find the libs. if(PythonLibsNew_FIND_REQUIRED) - find_package(PythonInterp ${PythonLibsNew_FIND_VERSION} REQUIRED) + find_package(PythonInterp ${PythonLibsNew_FIND_VERSION} REQUIRED) else() - find_package(PythonInterp ${PythonLibsNew_FIND_VERSION}) + find_package(PythonInterp ${PythonLibsNew_FIND_VERSION}) endif() if(NOT PYTHONINTERP_FOUND) - set(PYTHONLIBS_FOUND FALSE) - return() + set(PYTHONLIBS_FOUND FALSE) + return() endif() -# According to http://stackoverflow.com/questions/646518/python-how-to-detect-debug-interpreter -# testing whether sys has the gettotalrefcount function is a reliable, cross-platform -# way to detect a CPython debug interpreter. +# According to http://stackoverflow.com/questions/646518/python-how-to-detect-debug-interpreter testing whether sys has +# the gettotalrefcount function is a reliable, cross-platform way to detect a CPython debug interpreter. # -# The library suffix is from the config var LDVERSION sometimes, otherwise -# VERSION. VERSION will typically be like "2.7" on unix, and "27" on windows. -execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c" - "from distutils import sysconfig as s;import sys;import struct; +# The library suffix is from the config var LDVERSION sometimes, otherwise VERSION. VERSION will typically be like "2.7" +# on unix, and "27" on windows. +execute_process( + COMMAND + "${PYTHON_EXECUTABLE}" "-c" "from distutils import sysconfig as s;import sys;import struct; print('.'.join(str(v) for v in sys.version_info)); print(sys.prefix); print(s.get_python_inc(plat_specific=True)); @@ -83,18 +68,18 @@ print(hasattr(sys, 'gettotalrefcount')+0); print(struct.calcsize('@P')); print(s.get_config_var('LDVERSION') or s.get_config_var('VERSION')); " - RESULT_VARIABLE _PYTHON_SUCCESS - OUTPUT_VARIABLE _PYTHON_VALUES - ERROR_VARIABLE _PYTHON_ERROR_VALUE - OUTPUT_STRIP_TRAILING_WHITESPACE) + RESULT_VARIABLE _PYTHON_SUCCESS + OUTPUT_VARIABLE _PYTHON_VALUES + ERROR_VARIABLE _PYTHON_ERROR_VALUE + OUTPUT_STRIP_TRAILING_WHITESPACE +) if(NOT _PYTHON_SUCCESS MATCHES 0) - if(PythonLibsNew_FIND_REQUIRED) - message(FATAL_ERROR - "Python config failure:\n${_PYTHON_ERROR_VALUE}") - endif() - set(PYTHONLIBS_FOUND FALSE) - return() + if(PythonLibsNew_FIND_REQUIRED) + message(FATAL_ERROR "Python config failure:\n${_PYTHON_ERROR_VALUE}") + endif() + set(PYTHONLIBS_FOUND FALSE) + return() endif() # Convert the process output into a list @@ -109,18 +94,17 @@ list(GET _PYTHON_VALUES 5 PYTHON_IS_DEBUG) list(GET _PYTHON_VALUES 6 PYTHON_SIZEOF_VOID_P) list(GET _PYTHON_VALUES 7 PYTHON_LIBRARY_SUFFIX) -# Make sure the Python has the same pointer-size as the chosen compiler -# Skip if CMAKE_SIZEOF_VOID_P is not defined +# Make sure the Python has the same pointer-size as the chosen compiler Skip if CMAKE_SIZEOF_VOID_P is not defined if(CMAKE_SIZEOF_VOID_P AND (NOT "${PYTHON_SIZEOF_VOID_P}" STREQUAL "${CMAKE_SIZEOF_VOID_P}")) - if(PythonLibsNew_FIND_REQUIRED) - math(EXPR _PYTHON_BITS "${PYTHON_SIZEOF_VOID_P} * 8") - math(EXPR _CMAKE_BITS "${CMAKE_SIZEOF_VOID_P} * 8") - message(FATAL_ERROR - "Python config failure: Python is ${_PYTHON_BITS}-bit, " - "chosen compiler is ${_CMAKE_BITS}-bit") - endif() - set(PYTHONLIBS_FOUND FALSE) - return() + if(PythonLibsNew_FIND_REQUIRED) + math(EXPR _PYTHON_BITS "${PYTHON_SIZEOF_VOID_P} * 8") + math(EXPR _CMAKE_BITS "${CMAKE_SIZEOF_VOID_P} * 8") + message(FATAL_ERROR "Python config failure: Python is ${_PYTHON_BITS}-bit, " + "chosen compiler is ${_CMAKE_BITS}-bit" + ) + endif() + set(PYTHONLIBS_FOUND FALSE) + return() endif() # The built-in FindPython didn't always give the version numbers @@ -136,62 +120,53 @@ string(REGEX REPLACE "\\\\" "/" PYTHON_SITE_PACKAGES ${PYTHON_SITE_PACKAGES}) # TODO: All the nuances of CPython debug builds have not been dealt with/tested. if(PYTHON_IS_DEBUG) - set(PYTHON_MODULE_EXTENSION "_d${PYTHON_MODULE_EXTENSION}") + set(PYTHON_MODULE_EXTENSION "_d${PYTHON_MODULE_EXTENSION}") endif() if(CMAKE_HOST_WIN32) - set(PYTHON_LIBRARY - "${PYTHON_PREFIX}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") - - # when run in a venv, PYTHON_PREFIX points to it. But the libraries remain in the - # original python installation. They may be found relative to PYTHON_INCLUDE_DIR. - if(NOT EXISTS "${PYTHON_LIBRARY}") - get_filename_component(_PYTHON_ROOT ${PYTHON_INCLUDE_DIR} DIRECTORY) - set(PYTHON_LIBRARY - "${_PYTHON_ROOT}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") - endif() - - # raise an error if the python libs are still not found. - if(NOT EXISTS "${PYTHON_LIBRARY}") - message(FATAL_ERROR "Python libraries not found") - endif() + set(PYTHON_LIBRARY "${PYTHON_PREFIX}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") + + # when run in a venv, PYTHON_PREFIX points to it. But the libraries remain in the original python installation. They + # may be found relative to PYTHON_INCLUDE_DIR. + if(NOT EXISTS "${PYTHON_LIBRARY}") + get_filename_component(_PYTHON_ROOT ${PYTHON_INCLUDE_DIR} DIRECTORY) + set(PYTHON_LIBRARY "${_PYTHON_ROOT}/libs/Python${PYTHON_LIBRARY_SUFFIX}.lib") + endif() + + # raise an error if the python libs are still not found. + if(NOT EXISTS "${PYTHON_LIBRARY}") + message(FATAL_ERROR "Python libraries not found") + endif() elseif(APPLE) - set(PYTHON_LIBRARY - "${PYTHON_PREFIX}/lib/libpython${PYTHON_LIBRARY_SUFFIX}.dylib") + set(PYTHON_LIBRARY "${PYTHON_PREFIX}/lib/libpython${PYTHON_LIBRARY_SUFFIX}.dylib") else() - if(${PYTHON_SIZEOF_VOID_P} MATCHES 8) - set(_PYTHON_LIBS_SEARCH "${PYTHON_PREFIX}/lib64" "${PYTHON_PREFIX}/lib") - else() - set(_PYTHON_LIBS_SEARCH "${PYTHON_PREFIX}/lib") - endif() - #message(STATUS "Searching for Python libs in ${_PYTHON_LIBS_SEARCH}") - # Probably this needs to be more involved. It would be nice if the config - # information the python interpreter itself gave us were more complete. - find_library(PYTHON_LIBRARY - NAMES "python${PYTHON_LIBRARY_SUFFIX}" - PATHS ${_PYTHON_LIBS_SEARCH} - NO_DEFAULT_PATH) - - # If all else fails, just set the name/version and let the linker figure out the path. - if(NOT PYTHON_LIBRARY) - set(PYTHON_LIBRARY python${PYTHON_LIBRARY_SUFFIX}) - endif() + if(${PYTHON_SIZEOF_VOID_P} MATCHES 8) + set(_PYTHON_LIBS_SEARCH "${PYTHON_PREFIX}/lib64" "${PYTHON_PREFIX}/lib") + else() + set(_PYTHON_LIBS_SEARCH "${PYTHON_PREFIX}/lib") + endif() + # message(STATUS "Searching for Python libs in ${_PYTHON_LIBS_SEARCH}") Probably this needs to be more involved. It + # would be nice if the config information the python interpreter itself gave us were more complete. + find_library( + PYTHON_LIBRARY + NAMES "python${PYTHON_LIBRARY_SUFFIX}" + PATHS ${_PYTHON_LIBS_SEARCH} + NO_DEFAULT_PATH + ) + + # If all else fails, just set the name/version and let the linker figure out the path. + if(NOT PYTHON_LIBRARY) + set(PYTHON_LIBRARY python${PYTHON_LIBRARY_SUFFIX}) + endif() endif() -MARK_AS_ADVANCED( - PYTHON_LIBRARY - PYTHON_INCLUDE_DIR -) +mark_as_advanced(PYTHON_LIBRARY PYTHON_INCLUDE_DIR) + +# We use PYTHON_INCLUDE_DIR, PYTHON_LIBRARY and PYTHON_DEBUG_LIBRARY for the cache entries because they are meant to +# specify the location of a single library. We now set the variables listed by the documentation for this module. +set(PYTHON_INCLUDE_DIRS "${PYTHON_INCLUDE_DIR}") +set(PYTHON_LIBRARIES "${PYTHON_LIBRARY}") +set(PYTHON_DEBUG_LIBRARIES "${PYTHON_DEBUG_LIBRARY}") -# We use PYTHON_INCLUDE_DIR, PYTHON_LIBRARY and PYTHON_DEBUG_LIBRARY for the -# cache entries because they are meant to specify the location of a single -# library. We now set the variables listed by the documentation for this -# module. -SET(PYTHON_INCLUDE_DIRS "${PYTHON_INCLUDE_DIR}") -SET(PYTHON_LIBRARIES "${PYTHON_LIBRARY}") -SET(PYTHON_DEBUG_LIBRARIES "${PYTHON_DEBUG_LIBRARY}") - -find_package_message(PYTHON - "Found PythonLibs: ${PYTHON_LIBRARY}" - "${PYTHON_EXECUTABLE}${PYTHON_VERSION}") +find_package_message(PYTHON "Found PythonLibs: ${PYTHON_LIBRARY}" "${PYTHON_EXECUTABLE}${PYTHON_VERSION}") diff --git a/nuclear/cmake/Modules/Findpybind11.cmake b/nuclear/cmake/Modules/Findpybind11.cmake index 30cdd60..c297747 100644 --- a/nuclear/cmake/Modules/Findpybind11.cmake +++ b/nuclear/cmake/Modules/Findpybind11.cmake @@ -1,4 +1,2 @@ -INCLUDE(ToolchainLibraryFinder) -ToolchainLibraryFinder(NAME pybind11 - HEADER pybind11/pybind11.h -) +include(ToolchainLibraryFinder) +ToolchainLibraryFinder(NAME pybind11 HEADER pybind11/pybind11.h) diff --git a/nuclear/cmake/Modules/GenerateNeutron.cmake b/nuclear/cmake/Modules/GenerateNeutron.cmake new file mode 100644 index 0000000..b077793 --- /dev/null +++ b/nuclear/cmake/Modules/GenerateNeutron.cmake @@ -0,0 +1,170 @@ +define_property( + TARGET + PROPERTY NEUTRON_CPP_SOURCE + BRIEF_DOCS "Generated neutron C++ source files" + FULL_DOCS "Generated neutron C++ source files" +) +define_property( + TARGET + PROPERTY NEUTRON_PROTOBUF_SOURCE + BRIEF_DOCS "Generated protobuf C/C++ source files" + FULL_DOCS "Generated protobuf C/C++ source files" +) +define_property( + TARGET + PROPERTY NEUTRON_PYTHON_SOURCE + BRIEF_DOCS "Generated protobuf python source files" + FULL_DOCS "Generated protobuf python source files" +) + +include(CMakeParseArguments) +function(GenerateNeutron) + # We need protobuf and python to generate the neutron messages + find_package(Protobuf REQUIRED) + find_package(PythonInterp 3 REQUIRED) + + # Set the path to our generating scripts + set(SCRIPT_SOURCE "${PROJECT_SOURCE_DIR}/nuclear/cmake/Scripts") + + # Files that are used to generate the neutron files + file(GLOB_RECURSE message_class_generator_files "${SCRIPT_SOURCE}/generator/**.py") + + # Extract the arguments from our function call + set(options, "") + set(oneValueArgs "PROTO" "PARENT_DIR" "BUILTIN_DIR" "BUILTIN_OUTPUT_DIR") + set(multiValueArgs "") + cmake_parse_arguments(NEUTRON "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Locations to store each of the output components + set(pb_out "${CMAKE_CURRENT_BINARY_DIR}/protobuf") + set(nt_out "${CMAKE_CURRENT_BINARY_DIR}/neutron") + set(py_out "${CMAKE_CURRENT_BINARY_DIR}/python") + + # Extract the components of the filename that we need + get_filename_component(file_we ${NEUTRON_PROTO} NAME_WE) + file(RELATIVE_PATH output_path ${NEUTRON_PARENT_DIR} ${NEUTRON_PROTO}) + get_filename_component(output_path ${output_path} DIRECTORY) + + # Make sure paths are normalised + cmake_path(SET NEUTRON_PROTO NORMALIZE "${NEUTRON_PROTO}") + cmake_path(SET NEUTRON_PARENT_DIR NORMALIZE "${NEUTRON_PARENT_DIR}") + cmake_path(SET NEUTRON_BUILTIN_DIR NORMALIZE "${NEUTRON_BUILTIN_DIR}") + cmake_path(SET NEUTRON_BUILTIN_OUTPUT_DIR NORMALIZE "${NEUTRON_BUILTIN_OUTPUT_DIR}") + cmake_path(SET pb NORMALIZE "${pb_out}/${output_path}/${file_we}") + cmake_path(SET py NORMALIZE "${py_out}/${output_path}/${file_we}") + cmake_path(SET nt NORMALIZE "${nt_out}/${output_path}/${file_we}") + + # Make sure the output paths exist + file(MAKE_DIRECTORY "${pb_out}/${output_path}") + file(MAKE_DIRECTORY "${nt_out}/${output_path}") + file(MAKE_DIRECTORY "${py_out}/${output_path}") + + # Name of the target that will be created for this neutron set(neutron_target ${file_we}_neutron) + string(REPLACE "/" "_" neutron_target "${output_path}_${file_we}_neutron") + string(REGEX REPLACE "^_([a-zA-Z0-9_]+)$" "\\1" neutron_target ${neutron_target}) + + # + # DEPENDENCIES + # + # Ideally ninja would do this at runtime, but for now we have to work out what dependencies each of the protocol + # buffer files have. If these change we just have to hope that it'll work until someone runs cmake again + execute_process( + COMMAND + ${PROTOBUF_PROTOC_EXECUTABLE} --dependency_out=${CMAKE_CURRENT_BINARY_DIR}/dependencies.txt + --descriptor_set_out=${CMAKE_CURRENT_BINARY_DIR}/descriptor.pb -I${NEUTRON_PARENT_DIR} -I${NEUTRON_BUILTIN_DIR} + ${NEUTRON_PROTO} + ) + file(READ "${CMAKE_CURRENT_BINARY_DIR}/dependencies.txt" dependencies) + string(REGEX REPLACE "\\\\\n" ";" dependencies ${dependencies}) + file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/dependencies.txt" "${CMAKE_CURRENT_BINARY_DIR}/descriptor.pb") + + # Clean our dependency list + foreach(depend ${dependencies}) + string(STRIP ${depend} depend) + string(REGEX REPLACE "^[^:]*:[ \t\r\n]*" "" depend ${depend}) + file(RELATIVE_PATH depend_rel ${NEUTRON_PARENT_DIR} ${depend}) + + # * Add a dependency to the target that generates the neutron for this dependency + # * We specifically exclude adding dependencies for system protobufs (these have "/google/protobuf/" in their path) + # * and NUClearRoles builtin protobufs (these have "/nuclear/message/proto/" in their path) + get_filename_component(depend_we ${depend_rel} NAME_WE) + get_filename_component(output_path ${depend_rel} DIRECTORY) + string(REPLACE "/" "_" target_depend "${output_path}_${depend_we}_neutron") + string(REGEX REPLACE "^_([a-zA-Z0-9_]+)$" "\\1" target_depend ${target_depend}) + + string(FIND "${depend}" "/nuclear/message/proto/" is_builtin) + string(FIND "${depend}" "/google/protobuf/" is_system) + if(is_builtin EQUAL -1 + AND is_system EQUAL -1 + AND NOT neutron_target STREQUAL target_depend + ) + list(APPEND target_depends ${target_depend}) + endif() + + if(depend_rel MATCHES "^\\.\\.") + # Absolute dependencies + cmake_path(SET depend NORMALIZE "${depend}") + list(APPEND source_depends ${depend}) + list(APPEND binary_depends ${depend}) + else() + # Relative dependencies + list(APPEND source_depends ${NEUTRON_PARENT_DIR}/${depend_rel}) + list(APPEND binary_depends ${pb_out}/${depend_rel}) + endif() + endforeach() + + # + # PROTOCOL BUFFERS + # + # Repackage our protocol buffers so they don't collide with the actual classes when we make our c++ protobuf classes + # by adding protobuf to the package + add_custom_command( + OUTPUT "${pb}.proto" + COMMAND ${PYTHON_EXECUTABLE} ARGS "${SCRIPT_SOURCE}/repackage_message.py" "${NEUTRON_PROTO}" "${pb}.proto" + WORKING_DIRECTORY "${SCRIPT_SOURCE}" + DEPENDS "${SCRIPT_SOURCE}/repackage_message.py" ${NEUTRON_PROTO} + COMMENT "Repackaging protobuf ${NEUTRON_PROTO}" + ) + + # Run the protocol buffer compiler on these new protobufs + add_custom_command( + OUTPUT "${pb}.pb.cc" "${pb}.pb.h" "${py}_pb2.py" + COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ARGS --cpp_out=lite:${pb_out} --python_out=${py_out} -I${pb_out} + -I${NEUTRON_BUILTIN_DIR} "${pb}.proto" + DEPENDS ${binary_depends} "${pb}.proto" ${target_depends} + COMMENT "Compiling protocol buffer ${NEUTRON_PROTO}" + ) + + # + # NEUTRONS + # + # Extract the protocol buffer information so we can generate code off it + add_custom_command( + OUTPUT "${nt}.pb" + COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ARGS --descriptor_set_out="${nt}.pb" -I${NEUTRON_PARENT_DIR} + -I${NEUTRON_BUILTIN_DIR} ${NEUTRON_PROTO} + DEPENDS ${source_depends} + COMMENT "Extracting protocol buffer information from ${NEUTRON_PROTO}" + ) + + # Build our c++ class from the extracted information + add_custom_command( + OUTPUT "${nt}.cpp" "${nt}.py.cpp" "${nt}.hpp" + COMMAND ${CMAKE_COMMAND} -E env NEUTRON_BUILTIN_DIR=${NEUTRON_BUILTIN_OUTPUT_DIR} ${PYTHON_EXECUTABLE} ARGS + "${SCRIPT_SOURCE}/build_message_class.py" "${nt}" + WORKING_DIRECTORY "${nt_out}" + DEPENDS "${SCRIPT_SOURCE}/build_message_class.py" ${message_class_generator_files} "${nt}.pb" + nuclear_message_builtins + COMMENT "Building classes for ${NEUTRON_PROTO}" + ) + + # Create a target for people to depend on + add_custom_target(${neutron_target} DEPENDS "${pb}.pb.cc" "${pb}.pb.h" "${nt}.cpp" "${nt}.hpp" "${py}_pb2.py") + set_target_properties( + ${neutron_target} + PROPERTIES NEUTRON_CPP_SOURCE "${nt}.cpp;${nt}.hpp" + NEUTRON_PROTOBUF_SOURCE "${pb}.pb.cc;${pb}.pb.h" + NEUTRON_PYTHON_SOURCE "${py}_pb2.py" + ) + +endfunction(GenerateNeutron) diff --git a/nuclear/cmake/Modules/HeaderLibrary.cmake b/nuclear/cmake/Modules/HeaderLibrary.cmake index ed52819..05dab9d 100644 --- a/nuclear/cmake/Modules/HeaderLibrary.cmake +++ b/nuclear/cmake/Modules/HeaderLibrary.cmake @@ -1,58 +1,69 @@ -INCLUDE(CMakeParseArguments) -FUNCTION(HeaderLibrary) - # Extract the arguments from our function call - SET(options, "") - SET(oneValueArgs "NAME") - SET(multiValueArgs "HEADER" "PATH_SUFFIX" "URL") - CMAKE_PARSE_ARGUMENTS(PACKAGE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Clear our required_vars variable - UNSET(required_vars) - - # Find our include path - FIND_PATH("${PACKAGE_NAME}_INCLUDE_DIR" - NAMES ${PACKAGE_HEADER} - DOC "The ${PACKAGE_NAME} include directory" - PATHS "${CMAKE_BINARY_DIR}/include" - PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} - ) - - # File doesn't exist in standard search paths, download it - IF(NOT ${PACKAGE_NAME}_INCLUDE_DIR) - SET(OUTPUT_DIR "${CMAKE_BINARY_DIR}/include") - - # Create the output folder if it doesn't already exist - IF(NOT EXISTS "${OUTPUT_DIR}") - FILE(MAKE_DIRECTORY "${OUTPUT_DIR}") - ENDIF() - - # Download file. - FILE(DOWNLOAD "${PACKAGE_URL}" "${OUTPUT_DIR}/${PACKAGE_HEADER}" STATUS ${PACKAGE_NAME}_STATUS) - - LIST(GET ${PACKAGE_NAME}_STATUS 0 ${PACKAGE_NAME}_STATUS_CODE) - - # Parse download status - IF(${PACKAGE_NAME}_STATUS_CODE EQUAL 0) - MESSAGE(STATUS "Successfully downloaded ${PACKAGE_NAME} library.") - - SET(${PACKAGE_NAME}_INCLUDE_DIR "${OUTPUT_DIR}") - - ELSE() - MESSAGE(ERROR "Failed to download ${PACKAGE_NAME} library.") - ENDIF() - ENDIF() - - # Setup and export our variables - SET(required_vars ${required_vars} "${PACKAGE_NAME}_INCLUDE_DIR") - SET(${PACKAGE_NAME}_INCLUDE_DIRS ${${PACKAGE_NAME}_INCLUDE_DIR} PARENT_SCOPE) - MARK_AS_ADVANCED(${PACKAGE_NAME}_INCLUDE_DIR ${PACKAGE_NAME}_INCLUDE_DIRS) - - # Find the package - INCLUDE(FindPackageHandleStandardArgs) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(${PACKAGE_NAME} - FOUND_VAR ${PACKAGE_NAME}_FOUND - REQUIRED_VARS ${required_vars} - VERSION_VAR ${PACKAGE_NAME}_VERSION - ) - -ENDFUNCTION(HeaderLibrary) +include(CMakeParseArguments) +function(HeaderLibrary) + # Extract the arguments from our function call + set(options, "") + set(oneValueArgs "NAME") + set(multiValueArgs "HEADER" "PATH_SUFFIX" "URL") + cmake_parse_arguments(PACKAGE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Clear our required_vars variable + unset(required_vars) + + # Find our include path + find_path( + "${PACKAGE_NAME}_INCLUDE_DIR" + NAMES ${PACKAGE_HEADER} + DOC "The ${PACKAGE_NAME} include directory" + PATHS "${CMAKE_BINARY_DIR}/include" + PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} + ) + + # File doesn't exist in standard search paths, download it + if(NOT ${PACKAGE_NAME}_INCLUDE_DIR) + set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/include") + + # Create the output folder if it doesn't already exist + if(NOT EXISTS "${OUTPUT_DIR}") + file(MAKE_DIRECTORY "${OUTPUT_DIR}") + endif() + + # Download file. + file(DOWNLOAD "${PACKAGE_URL}" "${OUTPUT_DIR}/${PACKAGE_HEADER}" STATUS ${PACKAGE_NAME}_STATUS) + + list(GET ${PACKAGE_NAME}_STATUS 0 ${PACKAGE_NAME}_STATUS_CODE) + + # Parse download status + if(${PACKAGE_NAME}_STATUS_CODE EQUAL 0) + message(STATUS "Successfully downloaded ${PACKAGE_NAME} library.") + + set(${PACKAGE_NAME}_INCLUDE_DIR + "${OUTPUT_DIR}" + CACHE PATH "The ${PACKAGE_NAME} include directory" FORCE + ) + + else() + message(ERROR "Failed to download ${PACKAGE_NAME} library.") + endif() + endif() + + # Setup and export our variables + set(required_vars ${required_vars} "${PACKAGE_NAME}_INCLUDE_DIR") + set(${PACKAGE_NAME}_INCLUDE_DIRS + ${${PACKAGE_NAME}_INCLUDE_DIR} + PARENT_SCOPE + ) + mark_as_advanced(${PACKAGE_NAME}_INCLUDE_DIR ${PACKAGE_NAME}_INCLUDE_DIRS) + + # Find the package + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args( + ${PACKAGE_NAME} + FOUND_VAR ${PACKAGE_NAME}_FOUND + REQUIRED_VARS ${required_vars} + VERSION_VAR ${PACKAGE_NAME}_VERSION + ) + + add_library(${PACKAGE_NAME}::${PACKAGE_NAME} INTERFACE IMPORTED) + target_include_directories(${PACKAGE_NAME}::${PACKAGE_NAME} SYSTEM INTERFACE ${${PACKAGE_NAME}_INCLUDE_DIR}) + +endfunction(HeaderLibrary) diff --git a/nuclear/cmake/Modules/NUClearCompilerSettings.cmake b/nuclear/cmake/Modules/NUClearCompilerSettings.cmake deleted file mode 100644 index eeb769f..0000000 --- a/nuclear/cmake/Modules/NUClearCompilerSettings.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# Default to do a debug build -IF(NOT CMAKE_BUILD_TYPE) - SET(CMAKE_BUILD_TYPE Debug CACHE STRING - "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel." - FORCE) -ENDIF() - -# RPath variables -# use, i.e. don't skip the full RPATH for the build tree -SET(CMAKE_SKIP_BUILD_RPATH FALSE) - -# Build the RPATH into the binary before install -SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) - -# Make OSX use the same RPATH as everyone else -SET(CMAKE_MACOSX_RPATH ON) - -# Add some useful places to the RPATH -# These will allow the binary to run from the build folder -SET(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_RPATH} lib/ ../lib/ bin/lib) - -IF(NOT MSVC) - # Compilation must be done with c++14 for NUClear to work - ADD_COMPILE_OPTIONS(-std=c++14 -fPIC) -ENDIF() diff --git a/nuclear/cmake/Modules/ToolchainLibraryFinder.cmake b/nuclear/cmake/Modules/ToolchainLibraryFinder.cmake index a61e806..b61c570 100644 --- a/nuclear/cmake/Modules/ToolchainLibraryFinder.cmake +++ b/nuclear/cmake/Modules/ToolchainLibraryFinder.cmake @@ -1,97 +1,195 @@ -INCLUDE(CMakeParseArguments) -FUNCTION(ToolchainLibraryFinder) - - # Extract the arguments from our function call - SET(options, "") - SET(oneValueArgs "NAME") - SET(multiValueArgs "HEADER" "LIBRARY" "PATH_SUFFIX" "BINARY" "VERSION_FILE" "VERSION_BINARY_ARGUMENTS" "VERSION_REGEX") - CMAKE_PARSE_ARGUMENTS(PACKAGE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # Clear our required_vars variable - UNSET(required_vars) - - # Find our include path from our named headers - IF(PACKAGE_HEADER) - - # Find our include path - FIND_PATH("${PACKAGE_NAME}_INCLUDE_DIR" - NAMES ${PACKAGE_HEADER} - DOC "The ${PACKAGE_NAME} (${PACKAGE_LIBRARY}) include directory" - PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} - ) - - # Setup and export our variables - SET(required_vars ${required_vars} "${PACKAGE_NAME}_INCLUDE_DIR") - SET(${PACKAGE_NAME}_INCLUDE_DIRS ${${PACKAGE_NAME}_INCLUDE_DIR} PARENT_SCOPE) - MARK_AS_ADVANCED(${PACKAGE_NAME}_INCLUDE_DIR ${PACKAGE_NAME}_INCLUDE_DIRS) - - ENDIF(PACKAGE_HEADER) - - # Find our library from the named library files - IF(PACKAGE_LIBRARY) - FIND_LIBRARY("${PACKAGE_NAME}_LIBRARY" - NAMES ${PACKAGE_LIBRARY} - PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} - DOC "The ${PACKAGE_NAME} (${PACKAGE_LIBRARY}) library" - ) - - # Setup and export our variables - SET(required_vars ${required_vars} "${PACKAGE_NAME}_LIBRARY") - SET(${PACKAGE_NAME}_LIBRARIES ${${PACKAGE_NAME}_LIBRARY} PARENT_SCOPE) - MARK_AS_ADVANCED(${PACKAGE_NAME}_LIBRARY ${PACKAGE_NAME}_LIBRARIES) - - ENDIF(PACKAGE_LIBRARY) - - # Find our binary from the named binary files - IF(PACKAGE_BINARY) - FIND_PROGRAM("${PACKAGE_NAME}_BINARY" - NAMES ${PACKAGE_BINARY} - PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} - DOC "The ${PACKAGE_NAME} (${PACKAGE_BINARY}) executable prgram" - ) - - # Setup and export our variables - SET(required_vars ${required_vars} "${PACKAGE_NAME}_BINARY") - SET(${PACKAGE_NAME}_BINARY ${${PACKAGE_NAME}_BINARY} PARENT_SCOPE) - MARK_AS_ADVANCED(${PACKAGE_NAME}_BINARY) - - ENDIF(PACKAGE_BINARY) - - # Find our version if we can - IF((PACKAGE_VERSION_FILE AND PACKAGE_HEADER) OR (PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY)) - UNSET(full_version_string) - - # Read our package version file into a variable - IF(PACKAGE_VERSION_FILE AND PACKAGE_HEADER) - FILE(READ "${${PACKAGE_NAME}_INCLUDE_DIR}/${PACKAGE_VERSION_FILE}" full_version_string) - ENDIF(PACKAGE_VERSION_FILE AND PACKAGE_HEADER) - - # Execute our binary to get a version string - IF(PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY) - EXEC_PROGRAM(${${PACKAGE_NAME}_BINARY} - ARGS ${PACKAGE_VERSION_BINARY_ARGUMENTS} - OUTPUT_VARIABLE full_version_string) - ENDIF(PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY) - - # Build up our version string - SET(${PACKAGE_NAME}_VERSION "") - FOREACH(regex ${PACKAGE_VERSION_REGEX}) - STRING(REGEX REPLACE ".*${regex}.*" "\\1" regex_output ${full_version_string}) - SET(${PACKAGE_NAME}_VERSION ${${PACKAGE_NAME}_VERSION} ${regex_output}) - ENDFOREACH(regex) - STRING(REPLACE ";" "." ${PACKAGE_NAME}_VERSION "${${PACKAGE_NAME}_VERSION}") - - ENDIF((PACKAGE_VERSION_FILE AND PACKAGE_HEADER) OR (PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY)) - - INCLUDE(FindPackageHandleStandardArgs) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(${PACKAGE_NAME} - FOUND_VAR ${PACKAGE_NAME}_FOUND - REQUIRED_VARS ${required_vars} - VERSION_VAR ${PACKAGE_NAME}_VERSION - #VERSION_VAR "${MAJOR}.${MINOR}.${PATCH}") +include(CMakeParseArguments) +function(ToolchainLibraryFinder) + + # Extract the arguments from our function call + set(options, "") + set(oneValueArgs "NAME") + set(multiValueArgs + "HEADER" + "LIBRARY" + "LIBRARIES" + "PATH_SUFFIX" + "BINARY" + "VERSION_FILE" + "VERSION_BINARY_ARGUMENTS" + "VERSION_REGEX" + "LINK_TYPE" + ) + cmake_parse_arguments(PACKAGE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Clear our required_vars variable + unset(required_vars) + + if(PACKAGE_LIBRARY OR PACKAGE_LIBRARIES) + if(PACKAGE_LINK_TYPE) + set(${PACKAGE_NAME}_LINK_TYPE + ${PACKAGE_LINK_TYPE} + CACHE STRING "Choose method to link the library" + ) + else() + set(${PACKAGE_NAME}_LINK_TYPE + UNKNOWN + CACHE STRING "Choose method to link the library" + ) + endif() + set_property(CACHE ${PACKAGE_NAME}_LINK_TYPE PROPERTY STRINGS "SHARED" "STATIC" "MODULE" "UNKNOWN") + mark_as_advanced(${PACKAGE_NAME}_LINK_TYPE) + + # Search only for specified libraries + if(${PACKAGE_NAME}_LINK_TYPE STREQUAL "STATIC") + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX}) + # Uncache the incorrect value + if(${PACKAGE_NAME}_LIBRARY MATCHES ".*\.so$") + unset(${PACKAGE_NAME}_LIBRARY CACHE) + endif() + elseif(${PACKAGE_NAME}_LINK_TYPE STREQUAL "SHARED") + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_SHARED_LIBRARY_SUFFIX}) + # Uncache the incorrect value + if(${PACKAGE_NAME}_LIBRARY MATCHES ".*\.a$") + unset(${PACKAGE_NAME}_LIBRARY CACHE) + endif() + endif() + endif() + + # Find our library from the named library files + if(PACKAGE_LIBRARY) + find_library( + "${PACKAGE_NAME}_LIBRARY" + NAMES ${PACKAGE_LIBRARY} + PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} + DOC "The ${PACKAGE_NAME} (${PACKAGE_LIBRARY}) library" ) - # Export our found variable to parent scope - SET(${PACKAGE_NAME}_FOUND ${PACKAGE_NAME}_FOUND PARENT_SCOPE) + # Setup an imported target for this library + add_library(${PACKAGE_NAME}::${PACKAGE_NAME} ${${PACKAGE_NAME}_LINK_TYPE} IMPORTED) + set_target_properties(${PACKAGE_NAME}::${PACKAGE_NAME} PROPERTIES IMPORTED_LOCATION ${${PACKAGE_NAME}_LIBRARY}) -ENDFUNCTION(ToolchainLibraryFinder) + # Setup and export our variables + list(APPEND required_vars "${PACKAGE_NAME}_LIBRARY") + set(${PACKAGE_NAME}_LIBRARIES + ${${PACKAGE_NAME}_LIBRARY} + PARENT_SCOPE + ) + mark_as_advanced(${PACKAGE_NAME}_LIBRARY ${PACKAGE_NAME}_LIBRARIES) + + elseif(PACKAGE_LIBRARIES) + foreach(lib ${PACKAGE_LIBRARIES}) + find_library( + "${PACKAGE_NAME}_${lib}_LIBRARY" + NAMES ${lib} + PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} + DOC "The ${PACKAGE_NAME} (${lib}) library" + ) + + # Setup an imported target for this library + add_library(${PACKAGE_NAME}::${lib} ${${PACKAGE_NAME}_LINK_TYPE} IMPORTED) + set_target_properties(${PACKAGE_NAME}::${lib} PROPERTIES IMPORTED_LOCATION ${${PACKAGE_NAME}_${lib}_LIBRARY}) + + # Setup and export our variables + set(required_vars ${required_vars} "${PACKAGE_NAME}_${lib}_LIBRARY") + list(APPEND ${PACKAGE_NAME}_LIBRARIES ${PACKAGE_NAME}::${lib}) + mark_as_advanced(${PACKAGE_NAME}_${lib}_LIBRARY) + endforeach(lib ${PACKAGE_LIBRARIES}) + + # Link all of our imported targets to our imported library + add_library(${PACKAGE_NAME}::${PACKAGE_NAME} INTERFACE IMPORTED) + target_link_libraries(${PACKAGE_NAME}::${PACKAGE_NAME} INTERFACE ${${PACKAGE_NAME}_LIBRARIES}) + + # Make sure the libraries exist in the parent scope + set(${PACKAGE_NAME}_LIBRARIES + ${${PACKAGE_NAME}_LIBRARIES} + PARENT_SCOPE + ) + mark_as_advanced(${PACKAGE_NAME}_LIBRARIES) + endif() + + # Find our include path from our named headers + if(PACKAGE_HEADER) + + # Find our include path + find_path( + "${PACKAGE_NAME}_INCLUDE_DIR" + NAMES ${PACKAGE_HEADER} + DOC "The ${PACKAGE_NAME} (${PACKAGE_LIBRARY}) include directory" + PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} + ) + + # Add include directories to our imported library + target_include_directories(${PACKAGE_NAME}::${PACKAGE_NAME} SYSTEM INTERFACE ${${PACKAGE_NAME}_INCLUDE_DIR}) + + # Setup and export our variables + list(APPEND required_vars "${PACKAGE_NAME}_INCLUDE_DIR") + set(${PACKAGE_NAME}_INCLUDE_DIRS + ${${PACKAGE_NAME}_INCLUDE_DIR} + PARENT_SCOPE + ) + mark_as_advanced(${PACKAGE_NAME}_INCLUDE_DIR ${PACKAGE_NAME}_INCLUDE_DIRS) + + endif() + + # Find our binary from the named binary files + if(PACKAGE_BINARY) + find_program( + "${PACKAGE_NAME}_BINARY" + NAMES ${PACKAGE_BINARY} + PATH_SUFFIXES ${PACKAGE_PATH_SUFFIX} + DOC "The ${PACKAGE_NAME} (${PACKAGE_BINARY}) executable program" + ) + + # Created an imported executable + add_executable(${PACKAGE_NAME}::${PACKAGE_BINARY} IMPORTED GLOBAL) + set_target_properties(${PACKAGE_NAME}::${PACKAGE_BINARY} PROPERTIES IMPORTED_LOCATION ${${PACKAGE_NAME}_BINARY}) + + # Setup and export our variables + list(APPEND required_vars "${PACKAGE_NAME}_BINARY") + set(${PACKAGE_NAME}_BINARY + ${${PACKAGE_NAME}_BINARY} + PARENT_SCOPE + ) + mark_as_advanced(${PACKAGE_NAME}_BINARY) + + endif() + + # Find our version if we can + if((PACKAGE_VERSION_FILE AND PACKAGE_HEADER) OR (PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY)) + unset(full_version_string) + + # Read our package version file into a variable + if(PACKAGE_VERSION_FILE AND PACKAGE_HEADER) + file(READ "${${PACKAGE_NAME}_INCLUDE_DIR}/${PACKAGE_VERSION_FILE}" full_version_string) + endif() + + # Execute our binary to get a version string + if(PACKAGE_VERSION_BINARY_ARGUMENTS AND PACKAGE_BINARY) + exec_program( + ${${PACKAGE_NAME}_BINARY} ARGS + ${PACKAGE_VERSION_BINARY_ARGUMENTS} + OUTPUT_VARIABLE full_version_string + ) + endif() + + # Build up our version string + set(${PACKAGE_NAME}_VERSION "") + foreach(regex ${PACKAGE_VERSION_REGEX}) + string(REGEX REPLACE ".*${regex}.*" "\\1" regex_output ${full_version_string}) + set(${PACKAGE_NAME}_VERSION ${${PACKAGE_NAME}_VERSION} ${regex_output}) + endforeach(regex) + string(REPLACE ";" "." ${PACKAGE_NAME}_VERSION "${${PACKAGE_NAME}_VERSION}") + + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args( + ${PACKAGE_NAME} + FOUND_VAR ${PACKAGE_NAME}_FOUND + REQUIRED_VARS ${required_vars} + VERSION_VAR ${PACKAGE_NAME}_VERSION # VERSION_VAR "${MAJOR}.${MINOR}.${PATCH}") + ) + + # Export our found variable to parent scope + set(${PACKAGE_NAME}_FOUND + ${PACKAGE_NAME}_FOUND + PARENT_SCOPE + ) + +endfunction(ToolchainLibraryFinder) diff --git a/nuclear/cmake/Scripts/build_message_class.py b/nuclear/cmake/Scripts/build_message_class.py new file mode 100755 index 0000000..b380544 --- /dev/null +++ b/nuclear/cmake/Scripts/build_message_class.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# MIT License +# +# Copyright (c) 2016 NUbots +# +# This file is part of the NUbots codebase. +# See https://github.com/NUbots/NUbots for further info. +# +# 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. +# +import sys + +import generator.File +from google.protobuf.descriptor_pb2 import FileDescriptorSet + +base_file = sys.argv[1] + +with open("{}.pb".format(base_file), "rb") as f: + # Load the descriptor protobuf file + d = FileDescriptorSet() + d.ParseFromString(f.read()) + + # Check that there is only one file + assert len(d.file) == 1 + + # Load the file + b = generator.File.File(d.file[0], base_file) + + # Generate the c++ file + header, impl, python = b.generate_cpp() + + with open("{}.hpp".format(base_file), "w") as f: + f.write(header) + + with open("{}.cpp".format(base_file), "w") as f: + f.write(impl) + + with open("{}.py.cpp".format(base_file), "w") as f: + f.write(python) diff --git a/nuclear/cmake/Scripts/build_message_reflection.py b/nuclear/cmake/Scripts/build_message_reflection.py new file mode 100644 index 0000000..e867c41 --- /dev/null +++ b/nuclear/cmake/Scripts/build_message_reflection.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# MIT License +# +# Copyright (c) 2021 NUbots +# +# This file is part of the NUbots codebase. +# See https://github.com/NUbots/NUbots for further info. +# +# 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. +# + +import os +import pkgutil +import sys + +import google.protobuf.message +import xxhash +from generator.textutil import dedent, indent + +if __name__ == "__main__": + + python_message_root = sys.argv[1] + reflection_output_header = sys.argv[2] + + # Load all our protocol buffer files as modules into this file + includes = [] + sys.path.append(python_message_root) + for dir_name, subdir, files in os.walk(python_message_root): + modules = pkgutil.iter_modules(path=[dir_name]) + for loader, module_name, ispkg in modules: + if module_name.endswith("pb2"): + + # Work out what header file this came from + include = os.path.join( + os.path.relpath(dir_name, python_message_root), "{}.hpp".format(module_name[:-4]) + ) + + # If it's one of ours include it + if include.startswith("message"): + includes.append(include) + + # Load our protobuf module + fqdn = os.path.normpath( + os.path.join(os.path.relpath(dir_name, python_message_root), module_name) + ).replace(os.sep, ".") + if fqdn not in sys.modules: + loader.find_module(fqdn).load_module(fqdn) + + # Now that we've imported them all get all the subclasses of protobuf message + messages = set() + for message in google.protobuf.message.Message.__subclasses__(): + + # Work out our original protobuf type + pb_type = ".".join(message.DESCRIPTOR.full_name.split(".")[1:]) + + # Only include our own messages + if pb_type.startswith("message.") and not message.DESCRIPTOR.GetOptions().map_entry: + messages.add(pb_type) + + messages = list(messages) + + includes = "\n".join('#include "{}"'.format(i) for i in includes) + + cases_reflect = "\n".join( + [ + "case 0x{}: return std::make_unique>();".format( + xxhash.xxh64(m, seed=0x4E55436C).hexdigest(), "::".join(m.split(".")) + ) + for m in messages + ] + ) + + cases_trait = "\n".join( + [ + "case 0x{}: return TypeTrait<{}>::value;".format( + xxhash.xxh64(m, seed=0x4E55436C).hexdigest(), "::".join(m.split(".")) + ) + for m in messages + ] + ) + + output = dedent( + """\ + #ifndef MESSAGE_REFLECTION_HPP + #define MESSAGE_REFLECTION_HPP + + #include + #include + #include + #include + + #include "utility/reflection/reflection_exceptions.hpp" + #include "utility/type_traits/has_id.hpp" + + {includes} + + namespace message::reflection {{ + using utility::reflection::unknown_message; + + template