diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e926b8d2fa..c91729e118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,10 @@ jobs: cpp-tests: uses: lf-lang/lingua-franca/.github/workflows/cpp-tests.yml@master + # Run the C++ integration tests on ROS2. + cpp-ros2-tests: + uses: lf-lang/lingua-franca/.github/workflows/cpp-ros2-tests.yml@cpp-ros2-platform + # Run the Python integration tests. py-tests: uses: lf-lang/lingua-franca/.github/workflows/py-tests.yml@master diff --git a/.github/workflows/cpp-ros2-tests.yml b/.github/workflows/cpp-ros2-tests.yml new file mode 100644 index 0000000000..49bbf00131 --- /dev/null +++ b/.github/workflows/cpp-ros2-tests.yml @@ -0,0 +1,47 @@ +name: C++ tests + +on: + workflow_call: + inputs: + compiler-ref: + required: false + type: string + runtime-ref: + required: false + type: string +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Setup Java JDK + uses: actions/setup-java@v1.4.3 + with: + java-version: 11 + - name: Check out lingual-franca repository + uses: actions/checkout@v2 + with: + repository: lf-lang/lingua-franca + submodules: true + ref: ${{ inputs.compiler-ref }} + - name: Check out specific ref of reactor-CPO + uses: actions/checkout@v2 + with: + repository: lf-lang/reactor-cpp + path: org.lflang/src/lib/cpp/reactor-cpp + ref: ${{ inputs.runtime-ref }} + if: ${{ inputs.runtime-ref }} + - name: Setup ROS2 + uses: ros-tooling/setup-ros@0.2.2 + with: + required-ros-distributions: galactic + - name: Run C++ tests; + run: | + source /opt/ros/galactic/setup.bash + ./gradlew test --tests org.lflang.tests.runtime.CppRos2Test.* + - name: Report to CodeCov + uses: codecov/codecov-action@v2.1.0 + with: + file: org.lflang.tests/build/reports/xml/jacoco + fail_ci_if_error: false + verbose: true + if: ${{ !inputs.runtime-ref }} # i.e., if this is part of the main repo's CI diff --git a/example/Cpp/src/ROS/BasicROS.lf b/example/Cpp/src/ROS/BasicROS.lf deleted file mode 100644 index 76dc558183..0000000000 --- a/example/Cpp/src/ROS/BasicROS.lf +++ /dev/null @@ -1,146 +0,0 @@ -/** - * This is port of the BasicROS C example to the Cpp target. - * - * Just like the original, its aim is to exchange messages between reactors using ROS2. - * Also like the original, there is a MessageGenerator reactor that publishes - * String messages on 'topic' and a MessageReceiver reactor that subscribes to 'topic'. - * - * 1- To get this example working, install full ROS 2 desktop - * ('https://index.ros.org/doc/ros2/Installation/Foxy/'). - * - * Please note that 'colcon' should also be installed. - * See 'https://index.ros.org/doc/ros2/Tutorials/Colcon-Tutorial/' for more details. - * - * 2- Follow the instruction in - * https://index.ros.org/doc/ros2/Tutorials/Writing-A-Simple-Cpp-Publisher-And-Subscriber/ - * **section 1** to create a 'cpp_pubsub' package in the current (example/ROS) folder. - * - * 3- Follow section 2.2 to modify the package.xml. - * - * 4- Place this file in the src/ directory as BasicROS.lf and replace the CMakeLists.txt of your project - * by the one in the example directory - * (https://github.com/icyphy/lingua-franca/blob/master/example/Cpp/src/ROS/CMakeLists.txt) - * - * 5- From the root of your project, compile the lingua franca code with - * lfc src/BasicROS.lf - * - * 6- Use colcon to build the cpp_pubsub package - * colcon build --packages-select cpp_pubsub - * - * 7- Source the appropriate setup.bash and run the package - * source install/setup.bash - * ros2 run cpp_pubsub talker - * - */ - -target Cpp { - keepalive: true, - logging: DEBUG, - no-compile: true -}; - -public preamble {= - #include - #include - #include - #include - - #include "rclcpp/rclcpp.hpp" - #include "std_msgs/msg/string.hpp" -=} - -reactor MessageGenerator { - public preamble {= - class MinimalPublisher : public rclcpp::Node { - public: - MinimalPublisher() : Node("minimal_publisher") { - publisher_ = this->create_publisher("topic", 10); - } - - rclcpp::Publisher::SharedPtr publisher_; - }; - =} - - state minimal_publisher:{=std::shared_ptr=}; - state i:int(0); - timer t(0, 500 msec); - - reaction(startup) {= - std::cout << "Executing startup." << std::endl; - this->minimal_publisher = std::make_shared(); - =} - - reaction(t) {= - auto message = std_msgs::msg::String(); - std::cout << "Executing timer reaction." << std::endl; - message.data = "Hello, world! " + std::to_string(this->i++); - RCLCPP_INFO(this->minimal_publisher->get_logger(), - "Sender publishing: '%s'", message.data.c_str()); - this->minimal_publisher->publisher_->publish(message); - rclcpp::spin_some(this->minimal_publisher); - std::cout << "Done executing timer reaction." << std::endl; - =} - - reaction(shutdown) {= - std::cout << "Executing shutdown reaction." << std::endl; - rclcpp::shutdown(); - =} -} - -reactor MessageReceiver { - public preamble {= - class MinimalSubscriber : public rclcpp::Node { - public: - MinimalSubscriber(reactor::PhysicalAction& physical_action, const reactor::Reactor& r) - : Node("minimal_subscriber"), physical_action_(physical_action), reactor_(r) { - subscription_ = this->create_subscription( - "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, std::placeholders::_1) - ); - } - - private: - reactor::PhysicalAction& physical_action_; - const reactor::Reactor& reactor_; - - void topic_callback(const std_msgs::msg::String::SharedPtr msg) const { - RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str()); - std::cout << "At physical time (" << reactor_.get_elapsed_physical_time() - << ") calling schedule_value with value " - << msg->data << " and length " << msg->data.length() - << "." << std::endl; - physical_action_.schedule(msg->data); - } - - rclcpp::Subscription::SharedPtr subscription_; - }; - =} - - physical action ros_message_a:std::string; - timer t(0, 500 msec); - state minimal_subscriber:{=std::shared_ptr=}; - - reaction(startup) -> ros_message_a {= - char *argv[] = {(char*)"BasicROSPub", NULL}; - rclcpp::init(1, argv); - this->minimal_subscriber = std::make_shared(ros_message_a, *this); - =} - - reaction(ros_message_a){= - std::cout << "Physical action triggered." << std::endl; - std::cout << "Received: " << *(ros_message_a.get()) << std::endl; - =} - - reaction(t) {= - rclcpp::spin_some(this->minimal_subscriber); - =} - - reaction(shutdown) {= - rclcpp::shutdown(); - =} -} - -main reactor { - sender = new MessageGenerator(); - receiver = new MessageReceiver(); -} - diff --git a/example/Cpp/src/ROS/CMakeLists.txt b/example/Cpp/src/ROS/CMakeLists.txt deleted file mode 100644 index eff1732ae9..0000000000 --- a/example/Cpp/src/ROS/CMakeLists.txt +++ /dev/null @@ -1,96 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -project(cpp_pubsub) - -# require C++ 17 -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wall -Wextra -Wpedantic) -endif() - -# LF -# ======================= -include(${CMAKE_ROOT}/Modules/ExternalProject.cmake) -include(GNUInstallDirs) - -set(DEFAULT_BUILD_TYPE "Release") -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) -endif() - -if(NOT REACTOR_CPP_BUILD_DIR) - set(REACTOR_CPP_BUILD_DIR "" CACHE STRING "Choose the directory to build reactor-cpp in." FORCE) -endif() - -ExternalProject_Add( -dep-reactor-cpp - PREFIX "${REACTOR_CPP_BUILD_DIR}" - GIT_REPOSITORY "https://github.com/tud-ccc/reactor-cpp.git" - GIT_TAG "26e6e641916924eae2e83bbf40cbc9b933414310" - CMAKE_ARGS - -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE} - -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_INSTALL_PREFIX} - -DCMAKE_INSTALL_BINDIR:PATH=${CMAKE_INSTALL_BINDIR} - -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} - -DREACTOR_CPP_VALIDATE=ON - -DREACTOR_CPP_TRACE=OFF - -DREACTOR_CPP_LOG_LEVEL=4 -) - -set(REACTOR_CPP_LIB_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") -set(REACTOR_CPP_BIN_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") -set(REACTOR_CPP_LIB_NAME "${CMAKE_SHARED_LIBRARY_PREFIX}reactor-cpp${CMAKE_SHARED_LIBRARY_SUFFIX}") -set(REACTOR_CPP_IMPLIB_NAME "${CMAKE_STATIC_LIBRARY_PREFIX}reactor-cpp${CMAKE_STATIC_LIBRARY_SUFFIX}") - -add_library(reactor-cpp SHARED IMPORTED) -add_dependencies(reactor-cpp dep-reactor-cpp) -if(WIN32) - set_target_properties(reactor-cpp PROPERTIES IMPORTED_IMPLIB "${REACTOR_CPP_LIB_DIR}/${REACTOR_CPP_IMPLIB_NAME}") - set_target_properties(reactor-cpp PROPERTIES IMPORTED_LOCATION "${REACTOR_CPP_BIN_DIR}/${REACTOR_CPP_LIB_NAME}") -else() - set_target_properties(reactor-cpp PROPERTIES IMPORTED_LOCATION "${REACTOR_CPP_LIB_DIR}/${REACTOR_CPP_LIB_NAME}") -endif() - -if (APPLE) - file(RELATIVE_PATH REL_LIB_PATH "${REACTOR_CPP_BIN_DIR}" "${REACTOR_CPP_LIB_DIR}") - set(CMAKE_INSTALL_RPATH "@executable_path/${REL_LIB_PATH}") -else () - set(CMAKE_INSTALL_RPATH "${REACTOR_CPP_LIB_DIR}") -endif () - -set(CMAKE_BUILD_WITH_INSTALL_RPATH ON) - -set(LF_MAIN_TARGET BasicROS) -# ======================= - -# find dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(std_msgs REQUIRED) - -add_executable(talker - src-gen/BasicROS/main.cc - src-gen/BasicROS/BasicROS/BasicROS.cc - src-gen/BasicROS/BasicROS/MessageGenerator.cc - src-gen/BasicROS/BasicROS/MessageReceiver.cc - src-gen/BasicROS/BasicROS/_lf_preamble.cc -) - -target_include_directories(talker PUBLIC - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}" - "${PROJECT_SOURCE_DIR}/src-gen/BasicROS" - "${PROJECT_SOURCE_DIR}/src-gen/BasicROS/__include__" - "${PROJECT_SOURCE_DIR}/install/cpp_sub/include/reactor-cpp" -) - -target_link_libraries(talker reactor-cpp) - -ament_target_dependencies(talker rclcpp std_msgs) - -install(TARGETS - talker - DESTINATION lib/${PROJECT_NAME}) - -ament_package() diff --git a/example/Cpp/src/ROS/MinimalPublisher.lf b/example/Cpp/src/ROS/MinimalPublisher.lf new file mode 100644 index 0000000000..c2cbdf2781 --- /dev/null +++ b/example/Cpp/src/ROS/MinimalPublisher.lf @@ -0,0 +1,31 @@ +target Cpp { + ros2: true +} + +public preamble {= + #include "rclcpp/rclcpp.hpp" + #include "std_msgs/msg/string.hpp" +=} + +main reactor { + private preamble {= + // FIXME: forward declaration to make the node visible + extern rclcpp::Node* lf_node; + =} + + state publisher: {= rclcpp::Publisher::SharedPtr =} + state count: unsigned(0) + + timer t(0, 500 ms) + + reaction(startup) {= + publisher = lf_node->create_publisher("topic", 10); + =} + + reaction(t) {= + auto message = std_msgs::msg::String(); + message.data = "Hello, world! " + std::to_string(count++); + reactor::log::Info() << "Publishing: " << message.data; + publisher->publish(message); + =} +} diff --git a/example/Cpp/src/ROS/MinimalSubscriber.lf b/example/Cpp/src/ROS/MinimalSubscriber.lf new file mode 100644 index 0000000000..0c7cf3cd8e --- /dev/null +++ b/example/Cpp/src/ROS/MinimalSubscriber.lf @@ -0,0 +1,32 @@ +target Cpp { + ros2: true, + keepalive: true, +} + +public preamble {= + #include "rclcpp/rclcpp.hpp" + #include "std_msgs/msg/string.hpp" +=} + +main reactor { + private preamble {= + // FIXME: forward declaration to make the node visible + extern rclcpp::Node* lf_node; + =} + + state subscription: {= rclcpp::Subscription::SharedPtr =} + state count: unsigned(0) + + physical action message: std::string; + + reaction(startup) -> message {= + subscription = lf_node->create_subscription( + "topic", 10, [&message](const std_msgs::msg::String::SharedPtr msg) { message.schedule(msg->data); } ); + // FIXME: Why can't we use a reference type in the lambda argument? + // const std_msgs::msg::String::SharedPtr& msg + =} + + reaction(message) {= + reactor::log::Info() << "I heard: " << *message.get(); + =} +} diff --git a/example/Cpp/src/ROS/README.md b/example/Cpp/src/ROS/README.md new file mode 100644 index 0000000000..1ed55731d5 --- /dev/null +++ b/example/Cpp/src/ROS/README.md @@ -0,0 +1,5 @@ +This is an LF reimplementation of the ROS 2 minimal publisher and sunscriber +[example](https://docs.ros.org/en/galactic/Tutorials/Writing-A-Simple-Cpp-Publisher-And-Subscriber.html). + +It consists of two LF files, MinimalPublisher and MinimalSubscriber, each +implementing the corresponding nodes from the original example. diff --git a/org.lflang.tests/src/org/lflang/tests/TestBase.java b/org.lflang.tests/src/org/lflang/tests/TestBase.java index 641f35ff7d..55f13f0f14 100644 --- a/org.lflang.tests/src/org/lflang/tests/TestBase.java +++ b/org.lflang.tests/src/org/lflang/tests/TestBase.java @@ -139,6 +139,7 @@ public static class Message { public static final String DESC_AS_CCPP = "Running C tests as CCpp."; public static final String DESC_FOUR_THREADS = "Run non-concurrent and non-federated tests (threads = 4)."; public static final String DESC_SCHED_SWAPPING = "Running with non-default runtime scheduler "; + public static final String DESC_ROS2 = "Running tests using ROS2."; /* Missing dependency messages */ public static final String MISSING_DOCKER = "Executable 'docker' not found or 'docker' daemon thread not running"; diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CCppTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/CCppTest.java index fb556e4b5e..86d976619c 100644 --- a/org.lflang.tests/src/org/lflang/tests/runtime/CCppTest.java +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CCppTest.java @@ -39,15 +39,14 @@ public void runAsCCpp() { /** * Exclusion function for runAsCCpp test */ - static private boolean isExcludedFromCCpp(TestCategory category) { - boolean excluded = false; + private static boolean isExcludedFromCCpp(TestCategory category) { // Don't need to test examples. // If any of them uses CCpp, it will // be tested when compileExamples is // run. - excluded |= (category == TestCategory.EXAMPLE); - excluded |= (isWindows() && (category == TestCategory.DOCKER_FEDERATED)); - excluded |= (isMac() && (category == TestCategory.DOCKER_FEDERATED || category == TestCategory.DOCKER)); + boolean excluded = category == TestCategory.EXAMPLE; + excluded |= isWindows() && category == TestCategory.DOCKER_FEDERATED; + excluded |= isMac() && (category == TestCategory.DOCKER_FEDERATED || category == TestCategory.DOCKER); return !excluded; } } diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CppRos2Test.java b/org.lflang.tests/src/org/lflang/tests/runtime/CppRos2Test.java new file mode 100644 index 0000000000..5be54b748c --- /dev/null +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CppRos2Test.java @@ -0,0 +1,36 @@ +package org.lflang.tests.runtime; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import org.lflang.ASTUtils; +import org.lflang.Target; +import org.lflang.lf.Element; +import org.lflang.lf.LfFactory; +import org.lflang.tests.TestBase; +import org.lflang.tests.TestRegistry.TestCategory; + +/** + * Run C++ tests using the ROS2 platform. + * + * NOTE: This test does not inherit any tests because it directly extends TestBase. + * + * @author Christian Menard + */ +public class CppRos2Test extends TestBase { + + public CppRos2Test() { super(Target.CPP); } + + /** + * Run C++ tests with the ros2 target property set + */ + @Test + public void runWithRos2() { + Assumptions.assumeTrue(isLinux(), "Only supported on Linux"); + Element trueLiteral = LfFactory.eINSTANCE.createElement(); + trueLiteral.setLiteral("true"); + runTestsForTargets(Message.DESC_ROS2, it -> it != TestCategory.EXAMPLE, + it -> ASTUtils.addTargetProperty(it.fileConfig.resource, "ros2", trueLiteral), + TestLevel.EXECUTION, true); + } +} diff --git a/org.lflang.tests/src/org/lflang/tests/runtime/CppTest.java b/org.lflang.tests/src/org/lflang/tests/runtime/CppTest.java index a68edad4f6..2703a3db7d 100644 --- a/org.lflang.tests/src/org/lflang/tests/runtime/CppTest.java +++ b/org.lflang.tests/src/org/lflang/tests/runtime/CppTest.java @@ -99,4 +99,7 @@ public void runFederatedTests() { super.runFederatedTests(); } + @Test + public void runRos2Tests() { } + } diff --git a/org.lflang/src/lib/cpp/lf_timeout.hh b/org.lflang/src/lib/cpp/lf_timeout.hh new file mode 100644 index 0000000000..e5c265ca46 --- /dev/null +++ b/org.lflang/src/lib/cpp/lf_timeout.hh @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020, TU Dresden. + + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + + * 2. 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. + + * 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. + */ + +#pragma once + +#include "reactor-cpp/reactor-cpp.hh" + +class __lf_Timeout : public reactor::Reactor { + private: + reactor::Timer timer; + reactor::Reaction r_timer{"r_timer", 1, this, [this]() { environment()->sync_shutdown();} }; + public: + __lf_Timeout(const std ::string &name, reactor::Environment *env, reactor::Duration timeout) + : reactor::Reactor(name, env) + , timer{"timer", this, reactor::Duration::zero(), timeout} {} + void assemble() override { + r_timer.declare_trigger(&timer); + } +}; diff --git a/org.lflang/src/org/lflang/ASTUtils.java b/org.lflang/src/org/lflang/ASTUtils.java index 953cb38bfb..63181bc870 100644 --- a/org.lflang/src/org/lflang/ASTUtils.java +++ b/org.lflang/src/org/lflang/ASTUtils.java @@ -70,6 +70,7 @@ import org.lflang.lf.ImportedReactor; import org.lflang.lf.Input; import org.lflang.lf.Instantiation; +import org.lflang.lf.KeyValuePair; import org.lflang.lf.LfFactory; import org.lflang.lf.LfPackage; import org.lflang.lf.Mode; @@ -301,6 +302,28 @@ public static boolean changeTargetName(Resource resource, String newTargetName) targetDecl(resource).setName(newTargetName); return true; } + + /** + * Add a new target property to the given resource. + * + * This also creates a config object if the resource does not yey have one. + * + * @param resource The resource to modify + * @param name Name of the property to add + * @param value Value to be assigned to the property + */ + public static boolean addTargetProperty(final Resource resource, final String name, final Element value) { + var config = targetDecl(resource).getConfig(); + if (config == null) { + config = LfFactory.eINSTANCE.createKeyValuePairs(); + targetDecl(resource).setConfig(config); + } + final var newProperty = LfFactory.eINSTANCE.createKeyValuePair(); + newProperty.setName(name); + newProperty.setValue(value); + config.getPairs().add(newProperty); + return true; + } /** * Return true if the connection involves multiple ports on the left or right side of the connection, or diff --git a/org.lflang/src/org/lflang/TargetConfig.java b/org.lflang/src/org/lflang/TargetConfig.java index 3e667992e0..ea44d31e2d 100644 --- a/org.lflang/src/org/lflang/TargetConfig.java +++ b/org.lflang/src/org/lflang/TargetConfig.java @@ -193,6 +193,11 @@ public class TargetConfig { */ public List protoFiles = new ArrayList<>(); + /** + * If true, generate ROS2 specific code. + */ + public boolean ros2 = false; + /** * The version of the runtime library to be used in the generated target. */ diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index fe0b31f3b4..32d522b571 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -305,6 +305,15 @@ public enum TargetProperty { config.protoFiles = ASTUtils.toListOfStrings(value); }), + + /** + * Directive to specify that ROS2 specific code is generated, + */ + ROS2("ros2", PrimitiveType.BOOLEAN, + List.of(Target.CPP), (config, value, err) -> { + config.ros2 = ASTUtils.toBoolean(value); + }), + /** * Directive for specifying a specific version of the reactor runtime library. */ diff --git a/org.lflang/src/org/lflang/generator/GeneratorBase.java b/org.lflang/src/org/lflang/generator/GeneratorBase.java index 34f6e41e48..8f82df6746 100644 --- a/org.lflang/src/org/lflang/generator/GeneratorBase.java +++ b/org.lflang/src/org/lflang/generator/GeneratorBase.java @@ -127,6 +127,8 @@ public abstract class GeneratorBase extends AbstractLFValidator { */ protected GeneratorCommandFactory commandFactory; + public GeneratorCommandFactory getCommandFactory() { return commandFactory; } + /** * Collection of generated delay classes. */ diff --git a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt index 5de842772b..31eb1356af 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppExtensions.kt @@ -127,3 +127,14 @@ fun fileComment(r: Resource) = """ val InferredType.cppType: String get() = CppTypes.getTargetType(this) + + +/** Convert a log level to a severity number understood by the reactor-cpp runtime. */ +val TargetProperty.LogLevel.severity + get() = when (this) { + TargetProperty.LogLevel.ERROR -> 1 + TargetProperty.LogLevel.WARN -> 2 + TargetProperty.LogLevel.INFO -> 3 + TargetProperty.LogLevel.LOG -> 4 + TargetProperty.LogLevel.DEBUG -> 4 + } \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt index b446c5d6c9..8e509d9ef2 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppGenerator.kt @@ -29,8 +29,6 @@ package org.lflang.generator.cpp import org.eclipse.emf.ecore.resource.Resource import org.lflang.ErrorReporter import org.lflang.Target -import org.lflang.generator.LFGeneratorContext.Mode -import org.lflang.TargetProperty import org.lflang.TimeUnit import org.lflang.TimeValue import org.lflang.generator.CodeMap @@ -38,49 +36,48 @@ import org.lflang.generator.GeneratorBase import org.lflang.generator.GeneratorResult import org.lflang.generator.IntegratedBuilder import org.lflang.generator.LFGeneratorContext +import org.lflang.generator.LFGeneratorContext.Mode import org.lflang.generator.TargetTypes import org.lflang.generator.canGenerate import org.lflang.isGeneric import org.lflang.lf.Action import org.lflang.lf.VarRef import org.lflang.scoping.LFGlobalScopeProvider -import org.lflang.toDefinition -import org.lflang.toUnixString import org.lflang.util.FileUtil -import org.lflang.util.LFCommand import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths @Suppress("unused") class CppGenerator( - private val cppFileConfig: CppFileConfig, + val cppFileConfig: CppFileConfig, errorReporter: ErrorReporter, private val scopeProvider: LFGlobalScopeProvider ) : GeneratorBase(cppFileConfig, errorReporter) { + // keep a list of all source files we generate + val cppSources = mutableListOf() + val codeMaps = mutableMapOf() + companion object { /** Path to the Cpp lib directory (relative to class path) */ const val libDir = "/lib/cpp" } - /** Convert a log level to a severity number understood by the reactor-cpp runtime. */ - private val TargetProperty.LogLevel.severity - get() = when (this) { - TargetProperty.LogLevel.ERROR -> 1 - TargetProperty.LogLevel.WARN -> 2 - TargetProperty.LogLevel.INFO -> 3 - TargetProperty.LogLevel.LOG -> 4 - TargetProperty.LogLevel.DEBUG -> 4 - } - override fun doGenerate(resource: Resource, context: LFGeneratorContext) { super.doGenerate(resource, context) if (!canGenerate(errorsOccurred(), mainDef, errorReporter, context)) return - val codeMaps = generateFiles() + // create a platform specifi generator + val platformGenerator: CppPlatformGenerator = + if (targetConfig.ros2) CppRos2Generator(this) else CppStandaloneGenerator(this) + + // generate all core files + generateFiles(platformGenerator.srcGenPath) + + // generate platform specific files + platformGenerator.generatePlatformFiles() if (targetConfig.noCompile || errorsOccurred()) { println("Exiting before invoking target compiler.") @@ -89,7 +86,8 @@ class CppGenerator( context.reportProgress( "Code generation complete. Validating generated code...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - if (runCmake(context).first == 0) { + + if (platformGenerator.doCompile(context)) { CppValidator(cppFileConfig, errorReporter, codeMaps).doValidate(context) context.finish(GeneratorResult.GENERATED_NO_EXECUTABLE.apply(codeMaps)) } else { @@ -99,7 +97,11 @@ class CppGenerator( context.reportProgress( "Code generation complete. Compiling...", IntegratedBuilder.GENERATED_PERCENT_PROGRESS ) - doCompile(context, codeMaps) + if (platformGenerator.doCompile(context)) { + context.finish(GeneratorResult.Status.COMPILED, fileConfig.name, fileConfig, codeMaps) + } else { + context.unsuccessfulFinish() + } } } @@ -120,30 +122,19 @@ class CppGenerator( commandFactory.createCommand("git", listOf("checkout", version), libPath).run() } - private fun generateFiles(): Map { - val srcGenPath = fileConfig.srcGenPath - - val mainReactor = mainDef.reactorClass.toDefinition() - + private fun generateFiles(srcGenPath: Path) { // copy static library files over to the src-gen directory val genIncludeDir = srcGenPath.resolve("__include__") - FileUtil.copyFileFromClassPath( - "$libDir/lfutil.hh", - genIncludeDir.resolve("lfutil.hh"), - true - ) - FileUtil.copyFileFromClassPath( - "$libDir/time_parser.hh", - genIncludeDir.resolve("time_parser.hh"), - true - ) + listOf("lfutil.hh", "time_parser.hh", "lf_timeout.hh").forEach { + FileUtil.copyFileFromClassPath("$libDir/$it", genIncludeDir.resolve(it), true) + } FileUtil.copyFileFromClassPath( "$libDir/3rd-party/cxxopts.hpp", genIncludeDir.resolve("CLI").resolve("cxxopts.hpp"), true ) - // build reactor-cpp if needed + // copy or download reactor-cpp if (targetConfig.externalRuntimePath == null) { if (targetConfig.runtimeVersion != null) { fetchReactorCpp() @@ -156,17 +147,6 @@ class CppGenerator( } } - // keep a list of all source files we generate - val cppSources = mutableListOf() - val codeMaps = HashMap() - - // generate the main source file (containing main()) - val mainFile = Paths.get("main.cc") - val mainCodeMap = CodeMap.fromGeneratedCode(CppMainGenerator(mainReactor, targetConfig, cppFileConfig).generateCode()) - cppSources.add(mainFile) - codeMaps[srcGenPath.resolve(mainFile)] = mainCodeMap - FileUtil.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile), true) - // generate header and source files for all reactors for (r in reactors) { val generator = CppReactorGenerator(r, cppFileConfig, errorReporter) @@ -179,16 +159,8 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - FileUtil.writeToFile( - headerCodeMap.generatedCode, - srcGenPath.resolve(headerFile), - true - ) - FileUtil.writeToFile( - reactorCodeMap.generatedCode, - srcGenPath.resolve(sourceFile), - true - ) + FileUtil.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) + FileUtil.writeToFile(reactorCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) } // generate file level preambles for all resources @@ -202,172 +174,9 @@ class CppGenerator( val headerCodeMap = CodeMap.fromGeneratedCode(generator.generateHeader()) codeMaps[srcGenPath.resolve(headerFile)] = headerCodeMap - FileUtil.writeToFile( - headerCodeMap.generatedCode, - srcGenPath.resolve(headerFile), - true - ) - FileUtil.writeToFile( - preambleCodeMap.generatedCode, - srcGenPath.resolve(sourceFile), - true - ) - } - - // generate the cmake scripts - val cmakeGenerator = CppCmakeGenerator(targetConfig, cppFileConfig) - val srcGenRoot = fileConfig.srcGenBasePath - val pkgName = fileConfig.srcGenPkgPath.fileName.toString() - FileUtil.writeToFile( - cmakeGenerator.generateRootCmake(pkgName), - srcGenRoot.resolve("CMakeLists.txt"), - true - ) - FileUtil.writeToFile( - cmakeGenerator.generateCmake(cppSources), - srcGenPath.resolve("CMakeLists.txt"), - true - ) - var subdir = srcGenPath.parent - while (subdir != srcGenRoot) { - FileUtil.writeToFile( - cmakeGenerator.generateSubdirCmake(), - subdir.resolve("CMakeLists.txt"), - true - ) - subdir = subdir.parent - } - - return codeMaps - } - - fun getCmakeVersion(buildPath: Path): String? { - val cmd = commandFactory.createCommand("cmake", listOf("--version"), buildPath) - if (cmd != null && cmd.run() == 0) { - val regex = "\\d+(\\.\\d+)+".toRegex() - val version = regex.find(cmd.output.toString()) - return version?.value - } - return null - } - - fun doCompile(context: LFGeneratorContext) { - doCompile(context, HashMap()) - } - - /** - * Run CMake to generate build files. - * @return The CMake return code and the CMake version, or - * (1, "") if no acceptable version of CMake is installed. - */ - private fun runCmake(context: LFGeneratorContext): Pair { - val outPath = fileConfig.outPath - val buildPath = cppFileConfig.buildPath - - // make sure the build directory exists - Files.createDirectories(buildPath) - - // get the installed cmake version and make sure it is at least 3.5 - val version = getCmakeVersion(buildPath) - if (version == null || version.compareVersion("3.5.0") < 0) { - errorReporter.reportError( - "The C++ target requires CMAKE >= 3.5.0 to compile the generated code. " + - "Auto-compiling can be disabled using the \"no-compile: true\" target property." - ) - return Pair(1, "") - } - - // run cmake - val cmakeCommand = createCmakeCommand(buildPath, outPath) - return Pair(cmakeCommand.run(context.cancelIndicator), version) - } - - private fun doCompile(context: LFGeneratorContext, codeMaps: Map) { - val (cmakeReturnCode, version) = runCmake(context) - - if (cmakeReturnCode == 0) { - // If cmake succeeded, run make - val makeCommand = createMakeCommand(cppFileConfig.buildPath, version, fileConfig.name) - val makeReturnCode = CppValidator(cppFileConfig, errorReporter, codeMaps).run(makeCommand, context.cancelIndicator) - var installReturnCode = 0 - if (makeReturnCode == 0) { - val installCommand = createMakeCommand(cppFileConfig.buildPath, version, "install") - installReturnCode = installCommand.run(context.cancelIndicator) - if (installReturnCode == 0) { - println("SUCCESS (compiling generated C++ code)") - println("Generated source code is in ${fileConfig.srcGenPath}") - println("Compiled binary is in ${fileConfig.binPath}") - } - } - if ((makeReturnCode != 0 || installReturnCode != 0) && !errorsOccurred()) { - // If errors occurred but none were reported, then the following message is the best we can do. - errorReporter.reportError("make failed with error code $makeReturnCode") - } - } else if (version.isNotBlank()) { - errorReporter.reportError("cmake failed with error code $cmakeReturnCode") - } - if (errorReporter.errorsOccurred) { - context.unsuccessfulFinish() - } else { - context.finish( - GeneratorResult.Status.COMPILED, cppFileConfig.name, cppFileConfig, codeMaps - ) - } - } - - private fun String.compareVersion(other: String): Int { - val a = this.split(".").map { it.toInt() } - val b = other.split(".").map { it.toInt() } - for (x in (a zip b)) { - val res = x.first.compareTo(x.second) - if (res != 0) - return res - } - return 0 - } - - private fun createMakeCommand(buildPath: Path, version: String, target: String): LFCommand { - val makeArgs: List - if (version.compareVersion("3.12.0") < 0) { - errorReporter.reportWarning("CMAKE is older than version 3.12. Parallel building is not supported.") - makeArgs = - listOf("--build", ".", "--target", target, "--config", targetConfig.cmakeBuildType?.toString() ?: "Release") - } else { - val cores = Runtime.getRuntime().availableProcessors() - makeArgs = listOf( - "--build", - ".", - "--target", - target, - "--parallel", - cores.toString(), - "--config", - targetConfig.cmakeBuildType?.toString() ?: "Release" - ) - } - - return commandFactory.createCommand("cmake", makeArgs, buildPath) - } - - private fun createCmakeCommand(buildPath: Path, outPath: Path): LFCommand { - val cmd = commandFactory.createCommand( - "cmake", listOf( - "-DCMAKE_BUILD_TYPE=${targetConfig.cmakeBuildType}", - "-DCMAKE_INSTALL_PREFIX=${outPath.toUnixString()}", - "-DCMAKE_INSTALL_BINDIR=${outPath.relativize(fileConfig.binPath).toUnixString()}", - "-DREACTOR_CPP_VALIDATE=${if (targetConfig.noRuntimeValidation) "OFF" else "ON"}", - "-DREACTOR_CPP_TRACE=${if (targetConfig.tracing != null) "ON" else "OFF"}", - "-DREACTOR_CPP_LOG_LEVEL=${targetConfig.logLevel.severity}", - fileConfig.srcGenBasePath.toUnixString() - ), - buildPath - ) - - // prepare cmake - if (targetConfig.compiler != null) { - cmd.setEnvironmentVariable("CXX", targetConfig.compiler) + FileUtil.writeToFile(headerCodeMap.generatedCode, srcGenPath.resolve(headerFile), true) + FileUtil.writeToFile(preambleCodeMap.generatedCode, srcGenPath.resolve(sourceFile), true) } - return cmd } /** diff --git a/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt new file mode 100644 index 0000000000..e659a6b565 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/cpp/CppPlatformGenerator.kt @@ -0,0 +1,25 @@ +package org.lflang.generator.cpp + +import org.lflang.ErrorReporter +import org.lflang.TargetConfig +import org.lflang.generator.GeneratorCommandFactory +import org.lflang.generator.LFGeneratorContext +import org.lflang.toDefinition +import java.nio.file.Path + +/** Abstract class for generating platform specific files and invoking the target compiler. */ +abstract class CppPlatformGenerator(protected val generator: CppGenerator) { + protected val codeMaps = generator.codeMaps + protected val cppSources = generator.cppSources + protected val errorReporter: ErrorReporter = generator.errorReporter + protected val fileConfig = generator.cppFileConfig + protected val targetConfig: TargetConfig = generator.targetConfig + protected val commandFactory: GeneratorCommandFactory = generator.commandFactory + protected val mainReactor = generator.mainDef.reactorClass.toDefinition() + + open val srcGenPath: Path = generator.cppFileConfig.srcGenPath + + abstract fun generatePlatformFiles() + + abstract fun doCompile(context: LFGeneratorContext, onlyGenerateBuildFiles: Boolean = false): Boolean +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt b/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt new file mode 100644 index 0000000000..6a6f4aefd9 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/cpp/CppRos2Generator.kt @@ -0,0 +1,71 @@ +package org.lflang.generator.cpp + +import org.lflang.generator.LFGeneratorContext +import org.lflang.util.FileUtil +import java.nio.file.Path + +/** C++ platform generator for the ROS2 platform.*/ +class CppRos2Generator(generator: CppGenerator) : CppPlatformGenerator(generator) { + + override val srcGenPath: Path = generator.cppFileConfig.srcGenPath.resolve("src") + private val packagePath: Path = generator.cppFileConfig.srcGenPath + private val nodeGenerator = CppRos2NodeGenerator(mainReactor, targetConfig, fileConfig); + private val packageGenerator = CppRos2PackageGenerator(generator, nodeGenerator.nodeName) + + override fun generatePlatformFiles() { + FileUtil.writeToFile( + nodeGenerator.generateHeader(), + packagePath.resolve("include").resolve("${nodeGenerator.nodeName}.hh"), + true + ) + FileUtil.writeToFile( + nodeGenerator.generateSource(), + packagePath.resolve("src").resolve("${nodeGenerator.nodeName}.cc"), + true + ) + + FileUtil.writeToFile(packageGenerator.generatePackageXml(), packagePath.resolve("package.xml"), true) + FileUtil.writeToFile( + packageGenerator.generatePackageCmake(generator.cppSources), + packagePath.resolve("CMakeLists.txt"), + true + ) + val scriptPath = fileConfig.binPath.resolve(fileConfig.name); + FileUtil.writeToFile(packageGenerator.generateBinScript(), scriptPath) + scriptPath.toFile().setExecutable(true); + } + + override fun doCompile(context: LFGeneratorContext, onlyGenerateBuildFiles: Boolean): Boolean { + val ros2Version = System.getenv("ROS_DISTRO") + + if (ros2Version.isNullOrBlank()) { + errorReporter.reportError( + "Could not find a ROS2 installation! Please install ROS2 and source the setup script. " + + "Also see https://docs.ros.org/en/galactic/Installation.html" + ) + return false + } else if (ros2Version != "galactic") { + errorReporter.reportWarning("LF support for ROS2 has only been tested on galactic.") + } + + val colconCommand = commandFactory.createCommand( + "colcon", listOf( + "build", + "--packages-select", + fileConfig.name, + packageGenerator.reactorCppName, + "--cmake-args", + "-DLF_REACTOR_CPP_SUFFIX=${packageGenerator.reactorCppSuffix}", + "-DLF_SRC_PKG_PATH=${fileConfig.srcPkgPath}" + ), + fileConfig.outPath + ) + val returnCode = colconCommand.run(context.cancelIndicator); + if (returnCode != 0 && !errorReporter.errorsOccurred) { + // If errors occurred but none were reported, then the following message is the best we can do. + errorReporter.reportError("colcon failed with error code $returnCode") + } + + return !errorReporter.errorsOccurred + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppRos2NodeGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppRos2NodeGenerator.kt new file mode 100644 index 0000000000..f39a00963e --- /dev/null +++ b/org.lflang/src/org/lflang/generator/cpp/CppRos2NodeGenerator.kt @@ -0,0 +1,99 @@ +package org.lflang.generator.cpp + +import org.lflang.TargetConfig +import org.lflang.lf.Reactor +import org.lflang.toUnixString + +/** A C++ code generator for creating a ROS2 node from a main reactor definition */ +class CppRos2NodeGenerator( + private val main: Reactor, + private val targetConfig: TargetConfig, + private val fileConfig: CppFileConfig +) { + + val nodeName = "${fileConfig.name}Node" + + fun generateHeader(): String { + return """ + |#pragma once + | + |#include + |#include "reactor-cpp/reactor-cpp.hh" + |#include "lf_timeout.hh" + | + |#include "${fileConfig.getReactorHeaderPath(main).toUnixString()}" + | + |rclcpp::Node* lf_node{nullptr}; + | + |class $nodeName : public rclcpp::Node { + |private: + | std::unique_ptr lf_env; + | std::unique_ptr<${main.name}> lf_main_reactor; + | std::unique_ptr<__lf_Timeout> lf_timeout_reactor; + | + | // main thread of the LF execution + | std::thread lf_main_thread; + | // an additional thread that we use for waiting for LF termination + | // and then shutting down the LF node + | std::thread lf_shutdown_thread; + | + | void wait_for_lf_shutdown(); + |public: + | $nodeName(const rclcpp::NodeOptions& node_options); + | ~$nodeName(); + |}; + """.trimMargin() + } + + fun generateSource(): String { + return """ + |#include "$nodeName.hh" + |#include + | + |#include + | + |void $nodeName::wait_for_lf_shutdown() { + | lf_main_thread.join(); + | this->get_node_options().context()->shutdown("LF execution terminated"); + |} + | + |$nodeName::$nodeName(const rclcpp::NodeOptions& node_options) + | : Node("$nodeName", node_options) { + | unsigned threads = ${if (targetConfig.threads != 0) targetConfig.threads else "std::thread::hardware_concurrency()"}; + | bool fast{${targetConfig.fastMode}}; + | bool keepalive{${targetConfig.keepalive}}; + | reactor::Duration lf_timeout{${targetConfig.timeout?.toCppCode() ?: "reactor::Duration::zero()"}}; + | + | // provide a globally accessible reference to this node + | // FIXME: this is pretty hacky... + | lf_node = this; + | + | lf_env = std::make_unique(threads, keepalive, fast); + | + | // instantiate the main reactor + | lf_main_reactor = std::make_unique<${main.name}> ("${main.name}", lf_env.get()); + | + | // optionally instantiate the timeout reactor + | if (lf_timeout != reactor::Duration::zero()) { + | lf_timeout_reactor = std::make_unique<__lf_Timeout>("__lf_Timeout", lf_env.get(), lf_timeout); + | } + | + | // assemble reactor program + | lf_env->assemble(); + | + | // start execution + | lf_main_thread = lf_env->startup(); + | lf_shutdown_thread = std::thread([=] { wait_for_lf_shutdown(); }); + |} + | + |$nodeName::~$nodeName() { + | if (lf_env->phase() == reactor::Environment::Phase::Execution) { + | lf_env->async_shutdown(); + | } + | lf_shutdown_thread.join(); + |} + | + |RCLCPP_COMPONENTS_REGISTER_NODE($nodeName) + """.trimMargin() + } +} diff --git a/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt new file mode 100644 index 0000000000..0db69f2d0e --- /dev/null +++ b/org.lflang/src/org/lflang/generator/cpp/CppRos2PackageGenerator.kt @@ -0,0 +1,113 @@ +package org.lflang.generator.cpp + +import org.lflang.generator.PrependOperator +import org.lflang.toUnixString +import java.nio.file.Path + +/** A C++ code generator for creating the required files for defining a ROS2 package. */ +class CppRos2PackageGenerator(generator: CppGenerator, private val nodeName: String) { + private val fileConfig = generator.cppFileConfig + private val targetConfig = generator.targetConfig + val reactorCppSuffix = targetConfig.runtimeVersion ?: "default" + val reactorCppName = "reactor-cpp-$reactorCppSuffix" + + @Suppress("PrivatePropertyName") // allows us to use capital S as variable name below + private val S = '$' // a little trick to escape the dollar sign with $S + + fun generatePackageXml(): String { + return """ + | + | + | + | ${fileConfig.name} + | 0.0.0 + | Autogenerated from ${fileConfig.srcFile} + | Todo + | Todo + | + | ament_cmake + | ament_cmake_auto + | + | rclcpp + | rclcpp_components + | std_msgs + | $reactorCppName + | + | ament_lint_auto + | ament_lint_common + | + | ament_index_python + | + | + | ament_cmake + | + | + """.trimMargin() + } + + fun generatePackageCmake(sources: List): String { + // Resolve path to the cmake include files if any was provided + val includeFiles = targetConfig.cmakeIncludes?.map { fileConfig.srcPath.resolve(it).toUnixString() } + + return with(PrependOperator) { + """ + |cmake_minimum_required(VERSION 3.5) + |project(${fileConfig.name} VERSION 0.0.0 LANGUAGES CXX) + | + |# require C++ 17 + |set(CMAKE_CXX_STANDARD 17 CACHE STRING "The C++ standard is cached for visibility in external tools." FORCE) + |set(CMAKE_CXX_STANDARD_REQUIRED ON) + |set(CMAKE_CXX_EXTENSIONS OFF) + | + |set(DEFAULT_BUILD_TYPE "${targetConfig.cmakeBuildType}") + |if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + |set (CMAKE_BUILD_TYPE "$S{DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE) + |endif() + | + |# Invoke find_package() for all build and buildtool dependencies. + |find_package(ament_cmake_auto REQUIRED) + |ament_auto_find_build_dependencies() + | + |set(LF_MAIN_TARGET ${fileConfig.name}) + | + |ament_auto_add_library($S{LF_MAIN_TARGET} SHARED + | src/$nodeName.cc + ${" | "..sources.joinToString("\n") { "src/$it" }} + |) + |ament_target_dependencies($S{LF_MAIN_TARGET} rclcpp std_msgs) + |target_include_directories($S{LF_MAIN_TARGET} PUBLIC + | "$S{LF_SRC_PKG_PATH}/src" + | "$S{PROJECT_SOURCE_DIR}/src/" + | "$S{PROJECT_SOURCE_DIR}/src/__include__" + |) + |target_link_libraries($S{LF_MAIN_TARGET} $reactorCppName) + | + |rclcpp_components_register_node($S{LF_MAIN_TARGET} + | PLUGIN "$nodeName" + | EXECUTABLE $S{LF_MAIN_TARGET}_exe + |) + | + |if(MSVC) + | target_compile_options($S{LF_MAIN_TARGET} PRIVATE /W4) + |else() + | target_compile_options($S{LF_MAIN_TARGET} PRIVATE -Wall -Wextra -pedantic) + |endif() + | + |ament_auto_package() + | + ${" |"..(includeFiles?.joinToString("\n") { "include(\"$it\")" } ?: "")} + """.trimMargin() + } + } + + fun generateBinScript(): String { + val relPath = fileConfig.binPath.relativize(fileConfig.outPath).toUnixString() + + return """ + |#!/bin/bash + |script_dir="$S(dirname -- "$S(readlink -f -- "${S}0")")" + |source "$S{script_dir}/$relPath/install/setup.sh" + |ros2 run ${fileConfig.name} ${fileConfig.name}_exe + """.trimMargin() + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt similarity index 96% rename from org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt rename to org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt index 826addd986..8c8dd51f6b 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppCmakeGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneCmakeGenerator.kt @@ -29,8 +29,8 @@ import org.lflang.generator.PrependOperator import org.lflang.toUnixString import java.nio.file.Path -/** Code generator for producing a cmake script for compiling all generating C++ sources */ -class CppCmakeGenerator(private val targetConfig: TargetConfig, private val fileConfig: CppFileConfig) { +/** Code generator for producing a cmake script for compiling all generated C++ sources */ +class CppStandaloneCmakeGenerator(private val targetConfig: TargetConfig, private val fileConfig: CppFileConfig) { companion object { /** Return the name of the variable that gives the includes of the given target. */ @@ -127,8 +127,7 @@ class CppCmakeGenerator(private val targetConfig: TargetConfig, private val file ${" | "..sources.joinToString("\n") { it.toUnixString() }} |) |target_include_directories($S{LF_MAIN_TARGET} PUBLIC - | "$S{CMAKE_INSTALL_PREFIX}/$S{CMAKE_INSTALL_INCLUDEDIR}" - | "$S{CMAKE_INSTALL_PREFIX}/src" + | "$S{LF_SRC_PKG_PATH}/src" | "$S{PROJECT_SOURCE_DIR}" | "$S{PROJECT_SOURCE_DIR}/__include__" |) diff --git a/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt new file mode 100644 index 0000000000..7330a43f84 --- /dev/null +++ b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneGenerator.kt @@ -0,0 +1,167 @@ +package org.lflang.generator.cpp + +import org.lflang.generator.CodeMap +import org.lflang.generator.LFGeneratorContext +import org.lflang.toUnixString +import org.lflang.util.FileUtil +import org.lflang.util.LFCommand +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +/** C++ platform generator for the default native platform without additional dependencies.*/ +class CppStandaloneGenerator(generator: CppGenerator) : + CppPlatformGenerator(generator) { + + override fun generatePlatformFiles() { + + // generate the main source file (containing main()) + val mainFile = Paths.get("main.cc") + val mainCodeMap = + CodeMap.fromGeneratedCode(CppStandaloneMainGenerator(mainReactor, generator.targetConfig, fileConfig).generateCode()) + cppSources.add(mainFile) + codeMaps[fileConfig.srcGenPath.resolve(mainFile)] = mainCodeMap + println("Path: $srcGenPath $srcGenPath") + + FileUtil.writeToFile(mainCodeMap.generatedCode, srcGenPath.resolve(mainFile)) + + // generate the cmake scripts + val cmakeGenerator = CppStandaloneCmakeGenerator(targetConfig, fileConfig) + val srcGenRoot = fileConfig.srcGenBasePath + val pkgName = fileConfig.srcGenPkgPath.fileName.toString() + FileUtil.writeToFile(cmakeGenerator.generateRootCmake(pkgName), srcGenRoot.resolve("CMakeLists.txt"), true) + FileUtil.writeToFile(cmakeGenerator.generateCmake(cppSources), srcGenPath.resolve("CMakeLists.txt"), true) + var subdir = srcGenPath.parent + while (subdir != srcGenRoot) { + FileUtil.writeToFile(cmakeGenerator.generateSubdirCmake(), subdir.resolve("CMakeLists.txt"), true) + subdir = subdir.parent + } + } + + override fun doCompile(context: LFGeneratorContext, onlyGenerateBuildFiles: Boolean): Boolean { + var runMake = !onlyGenerateBuildFiles + if (onlyGenerateBuildFiles && !fileConfig.cppBuildDirectories.all { it.toFile().exists() }) { + // Special case: Some build directories do not exist, perhaps because this is the first C++ validation + // that has been done in this LF package since the last time the package was cleaned. + // We must compile in order to install the dependencies. Future validations will be faster. + runMake = true + } + + // make sure the build directory exists + Files.createDirectories(fileConfig.buildPath) + + val version = checkCmakeVersion() + if (version != null) { + val cmakeReturnCode = runCmake(context) + + if (cmakeReturnCode == 0 && runMake) { + // If cmake succeeded, run make + val makeCommand = createMakeCommand(fileConfig.buildPath, version, fileConfig.name) + val makeReturnCode = CppValidator(fileConfig, errorReporter, codeMaps).run(makeCommand, context.cancelIndicator) + var installReturnCode = 0 + if (makeReturnCode == 0) { + val installCommand = createMakeCommand(fileConfig.buildPath, version, "install") + installReturnCode = installCommand.run(context.cancelIndicator) + if (installReturnCode == 0) { + println("SUCCESS (compiling generated C++ code)") + println("Generated source code is in ${fileConfig.srcGenPath}") + println("Compiled binary is in ${fileConfig.binPath}") + } + } + if ((makeReturnCode != 0 || installReturnCode != 0) && !errorReporter.errorsOccurred) { + // If errors occurred but none were reported, then the following message is the best we can do. + errorReporter.reportError("make failed with error code $makeReturnCode") + } + } + if (cmakeReturnCode != 0) { + errorReporter.reportError("cmake failed with error code $cmakeReturnCode") + } + } + return !errorReporter.errorsOccurred + } + + private fun checkCmakeVersion(): String? { + // get the installed cmake version and make sure it is at least 3.5 + val cmd = commandFactory.createCommand("cmake", listOf("--version"), fileConfig.buildPath) + var version: String? = null + if (cmd != null && cmd.run() == 0) { + val regex = "\\d+(\\.\\d+)+".toRegex() + version = regex.find(cmd.output.toString())?.value + } + if (version == null || version.compareVersion("3.5.0") < 0) { + errorReporter.reportError( + "The C++ target requires CMAKE >= 3.5.0 to compile the generated code. " + + "Auto-compiling can be disabled using the \"no-compile: true\" target property." + ) + return null + } + + return version + } + + + /** + * Run CMake to generate build files. + * @return True, if cmake run successfully + */ + private fun runCmake(context: LFGeneratorContext): Int { + val cmakeCommand = createCmakeCommand(fileConfig.buildPath, fileConfig.outPath) + return cmakeCommand.run(context.cancelIndicator) + } + + private fun String.compareVersion(other: String): Int { + val a = this.split(".").map { it.toInt() } + val b = other.split(".").map { it.toInt() } + for (x in (a zip b)) { + val res = x.first.compareTo(x.second) + if (res != 0) + return res + } + return 0 + } + + private fun createMakeCommand(buildPath: Path, version: String, target: String): LFCommand { + val makeArgs: List + if (version.compareVersion("3.12.0") < 0) { + errorReporter.reportWarning("CMAKE is older than version 3.12. Parallel building is not supported.") + makeArgs = + listOf("--build", ".", "--target", target, "--config", targetConfig.cmakeBuildType?.toString() ?: "Release") + } else { + val cores = Runtime.getRuntime().availableProcessors() + makeArgs = listOf( + "--build", + ".", + "--target", + target, + "--parallel", + cores.toString(), + "--config", + targetConfig.cmakeBuildType?.toString() ?: "Release" + ) + } + + return commandFactory.createCommand("cmake", makeArgs, buildPath) + } + + private fun createCmakeCommand(buildPath: Path, outPath: Path): LFCommand { + val cmd = commandFactory.createCommand( + "cmake", listOf( + "-DCMAKE_BUILD_TYPE=${targetConfig.cmakeBuildType}", + "-DCMAKE_INSTALL_PREFIX=${outPath.toUnixString()}", + "-DCMAKE_INSTALL_BINDIR=${outPath.relativize(fileConfig.binPath).toUnixString()}", + "-DREACTOR_CPP_VALIDATE=${if (targetConfig.noRuntimeValidation) "OFF" else "ON"}", + "-DREACTOR_CPP_TRACE=${if (targetConfig.tracing != null) "ON" else "OFF"}", + "-DREACTOR_CPP_LOG_LEVEL=${targetConfig.logLevel.severity}", + "-DLF_SRC_PKG_PATH=${fileConfig.srcPkgPath}", + fileConfig.srcGenBasePath.toUnixString() + ), + buildPath + ) + + // prepare cmake + if (targetConfig.compiler != null) { + cmd.setEnvironmentVariable("CXX", targetConfig.compiler) + } + return cmd + } +} \ No newline at end of file diff --git a/org.lflang/src/org/lflang/generator/cpp/CppMainGenerator.kt b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneMainGenerator.kt similarity index 87% rename from org.lflang/src/org/lflang/generator/cpp/CppMainGenerator.kt rename to org.lflang/src/org/lflang/generator/cpp/CppStandaloneMainGenerator.kt index 4774c1eefb..e6bdbd6e9c 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppMainGenerator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppStandaloneMainGenerator.kt @@ -2,14 +2,13 @@ package org.lflang.generator.cpp import org.lflang.TargetConfig import org.lflang.generator.PrependOperator -import org.lflang.generator.PrependOperator.rangeTo import org.lflang.inferredType import org.lflang.lf.Parameter import org.lflang.lf.Reactor import org.lflang.toUnixString /** C++ code generator responsible for generating the main file including the main() function */ -class CppMainGenerator( +class CppStandaloneMainGenerator( private val main: Reactor, private val targetConfig: TargetConfig, private val fileConfig: CppFileConfig, @@ -58,21 +57,7 @@ class CppMainGenerator( |#include "${fileConfig.getReactorHeaderPath(main).toUnixString()}" | |#include "time_parser.hh" - | - |class __lf_Timeout : public reactor::Reactor { - | private: - | reactor::Timer timer; - | - | reactor::Reaction r_timer{"r_timer", 1, this, [this]() { environment()->sync_shutdown(); }}; - | - | - | public: - | __lf_Timeout(const std ::string& name, reactor::Environment* env, reactor::Duration timeout) - | : reactor::Reactor(name, env) - | , timer{ "timer", this, reactor::Duration::zero(), timeout } {} - | - | void assemble () override { r_timer.declare_trigger(& timer); } - |}; + |#include "lf_timeout.hh" | |int main(int argc, char **argv) { | cxxopts::Options options("${fileConfig.name}", "Reactor Program"); @@ -113,7 +98,7 @@ class CppMainGenerator( | // optionally instantiate the timeout reactor | std::unique_ptr<__lf_Timeout> t{nullptr}; | if (timeout != reactor::Duration::zero()) { - | t = std::make_unique<__lf_Timeout>("__lf_Timeout", & e, timeout); + | t = std::make_unique<__lf_Timeout>("__lf_Timeout", &e, timeout); | } | | // assemble reactor program diff --git a/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt b/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt index b04db1350f..01e2fb2d8a 100644 --- a/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt +++ b/org.lflang/src/org/lflang/generator/cpp/CppValidator.kt @@ -125,7 +125,7 @@ class CppValidator( * CMake and Make. */ override fun getBuildReportingStrategies(): Pair { - val compilerId: String = getFromCache(CppCmakeGenerator.compilerIdName) ?: "GNU" // This is just a guess. + val compilerId: String = getFromCache(CppStandaloneCmakeGenerator.compilerIdName) ?: "GNU" // This is just a guess. val mostSimilarValidationStrategy = CppValidationStrategyFactory.values().find { it.compilerIds.contains(compilerId) } if (mostSimilarValidationStrategy === null) { return Pair(DiagnosticReporting.Strategy { _, _, _ -> }, DiagnosticReporting.Strategy { _, _, _ -> }) @@ -148,7 +148,7 @@ class CppValidator( /** The include directories required by the generated files. */ private val includes: List - get() = getFromCache(CppCmakeGenerator.includesVarName(fileConfig.name))?.split(';')?.map { + get() = getFromCache(CppStandaloneCmakeGenerator.includesVarName(fileConfig.name))?.split(';')?.map { val matcher = CMAKE_GENERATOR_EXPRESSION.matcher(it) if (matcher.matches()) matcher.group("content") else it } ?: listOf()