diff --git a/.github/ci/packages.apt b/.github/ci/packages.apt index ee689d74..e222fd3d 100644 --- a/.github/ci/packages.apt +++ b/.github/ci/packages.apt @@ -1 +1,2 @@ libignition-cmake2-dev +libignition-tools-dev diff --git a/CMakeLists.txt b/CMakeLists.txt index f904eeef..04fa34b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,11 @@ ign_find_package(DL PRETTY libdl PURPOSE "Required for loading plugins") +#-------------------------------------- +# Find ignition-tools +ign_find_package(ignition-tools QUIET) + + #============================================================================ # Configure the build #============================================================================ diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 82f5436d..30d4c8cd 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -3,6 +3,11 @@ # "tests" variable ign_get_libsources_and_unittests(sources tests) +# Disable ign_TEST if ignition-tools is not found +if (MSVC OR NOT IGNITION-TOOLS_BINARY_DIRS) + list(REMOVE_ITEM tests src/ign_TEST.cc) +endif() + # Create the library target ign_add_component(loader SOURCES ${sources} @@ -29,8 +34,28 @@ foreach(test ${test_targets}) target_compile_definitions(${test} PRIVATE "IGNDummyPlugins_LIB=\"$\"") + target_compile_definitions(${test} PRIVATE + "IGN_PATH=\"${IGNITION-TOOLS_BINARY_DIRS}\"") + + target_compile_definitions(${test} PRIVATE + "IGN_VERSION_FULL=\"${PROJECT_VERSION_FULL}\"") + endforeach() +if(TARGET UNIT_ign_TEST) + set(_env_vars) + list(APPEND _env_vars "IGN_CONFIG_PATH=${CMAKE_BINARY_DIR}/test/conf") + + set_tests_properties(UNIT_ign_TEST PROPERTIES + ENVIRONMENT "${_env_vars}") +endif() + install( DIRECTORY include/ DESTINATION ${IGN_INCLUDE_INSTALL_DIR_FULL}) + +#============================================================================ +# ign command line support +#============================================================================ +add_subdirectory(conf) +add_subdirectory(src) diff --git a/loader/conf/CMakeLists.txt b/loader/conf/CMakeLists.txt new file mode 100644 index 00000000..6e988372 --- /dev/null +++ b/loader/conf/CMakeLists.txt @@ -0,0 +1,23 @@ +# Used only for internal testing. +set(ign_library_path "${CMAKE_BINARY_DIR}/test/lib/ruby/ignition/cmd${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}") + +# Generate a configuration file for internal testing. +# Note that the major version of the library is included in the name. +# Ex: plugin1.yaml +configure_file( + "${IGN_DESIGNATION}.yaml.in" + "${CMAKE_BINARY_DIR}/test/conf/${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml" @ONLY) + +# Used for the installed version. +set(ign_library_path "${CMAKE_INSTALL_PREFIX}/lib/ruby/ignition/cmd${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}") + +# Generate the configuration file that is installed. +# Note that the major version of the library is included in the name. +# Ex: plugin1.yaml +configure_file( + "${IGN_DESIGNATION}.yaml.in" + "${CMAKE_CURRENT_BINARY_DIR}/${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml" @ONLY) + +# Install the yaml configuration files in an unversioned location. +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}.yaml + DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/ignition/) diff --git a/loader/conf/plugin.yaml.in b/loader/conf/plugin.yaml.in new file mode 100644 index 00000000..f878d39c --- /dev/null +++ b/loader/conf/plugin.yaml.in @@ -0,0 +1,8 @@ +--- # Subcommands available inside ignition-plugin. +format: 1.0.0 +library_name: @PROJECT_NAME_NO_VERSION@ +library_version: @PROJECT_VERSION_FULL@ +library_path: @ign_library_path@ +commands: + - plugin : Print information about plugins. +--- diff --git a/loader/src/CMakeLists.txt b/loader/src/CMakeLists.txt new file mode 100644 index 00000000..f880e721 --- /dev/null +++ b/loader/src/CMakeLists.txt @@ -0,0 +1,2 @@ +# Command line support. +add_subdirectory(cmd) diff --git a/loader/src/cmd/CMakeLists.txt b/loader/src/cmd/CMakeLists.txt new file mode 100644 index 00000000..63d52354 --- /dev/null +++ b/loader/src/cmd/CMakeLists.txt @@ -0,0 +1,44 @@ +#=============================================================================== +# Generate the ruby script for internal testing. +# Note that the major version of the library is included in the name. +# Ex: cmdplugin1.rb +set(cmd_script_generated_test "${CMAKE_BINARY_DIR}/test/lib/ruby/ignition/cmd${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured_test "${cmd_script_generated_test}.configured") + +# Set the library_location variable to the full path of the library file within +# the build directory. +set(library_location "$") + +configure_file( + "cmd${IGN_DESIGNATION}.rb.in" + "${cmd_script_configured_test}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated_test}" + INPUT "${cmd_script_configured_test}") + + +#=============================================================================== +# Used for the installed version. +# Generate the ruby script that gets installed. +# Note that the major version of the library is included in the name. +# Ex: cmdplugin1.rb +set(cmd_script_generated "${CMAKE_CURRENT_BINARY_DIR}/cmd${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured "${cmd_script_generated}.configured") + +# Set the library_location variable to the relative path to the library file +# within the install directory structure. +set(library_location "../../../${CMAKE_INSTALL_LIBDIR}/$") + +configure_file( + "cmd${IGN_DESIGNATION}.rb.in" + "${cmd_script_configured}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated}" + INPUT "${cmd_script_configured}") + +# Install the ruby command line library in an unversioned location. +install(FILES ${cmd_script_generated} DESTINATION lib/ruby/ignition) diff --git a/loader/src/cmd/cmdplugin.rb.in b/loader/src/cmd/cmdplugin.rb.in new file mode 100644 index 00000000..d4b5c682 --- /dev/null +++ b/loader/src/cmd/cmdplugin.rb.in @@ -0,0 +1,185 @@ +#!/usr/bin/ruby + +# Copyright (C) 2020 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# We use 'dl' for Ruby <= 1.9.x and 'fiddle' for Ruby >= 2.0.x +if RUBY_VERSION.split('.')[0] < '2' + require 'dl' + require 'dl/import' + include DL +else + require 'fiddle' + require 'fiddle/import' + include Fiddle +end + +require 'optparse' + +# Constants. +LIBRARY_NAME = '@library_location@' +LIBRARY_VERSION = '@PROJECT_VERSION_FULL@' +COMMON_OPTIONS = + " -h [ --help ] Print this help message.\n"\ + " \n" + + " --force-version Use a specific library version.\n"\ + " \n" + + ' --versions Show the available versions.' +COMMANDS = { 'plugin' => + "Print information about plugins.\n\n" + + " ign plugin [options]\n\n" + + "Options:\n\n" + + " -i [ --info ] Get info about a plugin.\n" + + " Requires the -p option.\n" + + "\n" + + " -p [ --plugin ] arg Path to a plugin.\n" + + " Required with -i.\n" + + "\n" + + " -v [ --verbose ] Print verbose info.\n" + + "\n" + + COMMON_OPTIONS + } + +# +# Class for the Ignition plugin command line tools. +# +class Cmd + # + # Return a structure describing the options. + # + def parse(args) + options = {} + + usage = COMMANDS[args[0]] + + # Read the command line arguments. + opt_parser = OptionParser.new do |opts| + opts.banner = usage + + opts.on('-h', '--help', 'Print this help message') do + puts usage + exit(0) + end + + opts.on('-i', '--info', String, + 'Print information about a plugin') do |t| + options['info'] = t + end + + opts.on('-p plugin', '--plugin', String, + 'Plugin name') do |t| + options['plugin'] = t + end + + opts.on('-v', '--verbose', 'Print verbose info') do + options["verbose"] = 1 + end + + end + begin + opt_parser.parse!(args) + rescue + puts usage + exit(-1) + end + + # Check that there is at least one command and there is a plugin that knows + # how to handle it. + if ARGV.empty? || !COMMANDS.key?(ARGV[0]) || + options.empty? + puts usage + exit(-1) + end + + options['command'] = ARGV[0] + + options + end # parse() + + def execute(args) + options = parse(args) + + # puts 'Parsed:' + # puts options + + # Read the plugin that handles the command. + if LIBRARY_NAME[0] == '/' + # If the first character is a slash, we'll assume that we've been given an + # absolute path to the library. This is only used during test mode. + plugin = LIBRARY_NAME + else + # We're assuming that the library path is relative to the current + # location of this script. + plugin = File.expand_path(File.join(File.dirname(__FILE__), LIBRARY_NAME)) + end + conf_version = LIBRARY_VERSION + + begin + Importer.dlload plugin + rescue DLError + puts "Library error: [#{plugin}] not found." + if plugin.end_with? ".dylib" + puts "If this script was executed with /usr/bin/ruby, this error may be caused by +macOS System Integrity Protection. One workaround is to use a different +version of ruby, for example: + brew install ruby +and add the following line to your shell profile: + export PATH=/usr/local/opt/ruby/bin:$PATH" + end + exit(-1) + end + + # Read the library version. + Importer.extern 'char* ignitionVersion()' + begin + plugin_version = Importer.ignitionVersion.to_s + rescue DLError + puts "Library error: Problem running 'ignitionVersion()' from #{plugin}." + exit(-1) + end + + # Sanity check: Verify that the version of the yaml file matches the version + # of the library that we are using. + unless plugin_version.eql? conf_version + puts "Error: Version mismatch. Your configuration file version is + [#{conf_version}] but #{plugin} version is [#{plugin_version}]." + exit(-1) + end + + begin + case options['command'] + when 'plugin' + if options.key?('info') + if not options.key?('plugin') + puts 'ign plugin --info: missing plugin name (-p )' + puts 'Try ign plugin --help' + else + options["verbose"] = 0 if !options.key?('verbose') + Importer.extern 'void cmdPluginInfo(const char *, int)' + Importer.cmdPluginInfo(options['plugin'], options["verbose"]) + end + else + puts 'Command error: I do not have an implementation '\ + 'for this command.' + end + else + puts 'Command error: I do not have an implementation for '\ + "command [ign #{options['command']}]." + end + rescue + puts "Library error: Problem running [#{options['command']}]() "\ + "from #{plugin}." + end + end +end diff --git a/loader/src/ign.cc b/loader/src/ign.cc new file mode 100644 index 00000000..7f17739c --- /dev/null +++ b/loader/src/ign.cc @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include +#include + +#include "ignition/plugin/Loader.hh" +#include "ignition/plugin/config.hh" + +using namespace ignition; +using namespace plugin; + +////////////////////////////////////////////////// +extern "C" void IGNITION_PLUGIN_LOADER_VISIBLE cmdPluginInfo( + const char *_plugin, int _verbose) +{ + if (!_plugin || std::string(_plugin).empty()) + { + std::cerr << "Invalid plugin file name. Plugin name must not be empty.\n"; + return; + } + + ignition::plugin::Loader pl; + std::cout << "Loading plugin library file [" << _plugin << "]\n"; + + // Print names of plugins exported by library file + std::unordered_set pluginNames = pl.LoadLib(_plugin); + + if (_verbose) + { + std::cout << pl.PrettyStr() << std::endl; + return; + } + + std::cout << "* Found " << pluginNames.size() << " plugin"; + if (pluginNames.size() != 1u) + { + std::cout << 's'; + } + std::cout << " in library file:" << std::endl; + + for (const auto & pluginName : pluginNames) + { + std::cout << " - " << pluginName << std::endl; + } + + auto interfacesImplemented = pl.InterfacesImplemented(); + std::cout << "* Found " << interfacesImplemented.size() << " interface"; + if (interfacesImplemented.size() != 1u) + { + std::cout << 's'; + } + std::cout << " in library file:" << std::endl; + + for (const auto & interfaceImplemented : interfacesImplemented) + { + std::cout << " - " << interfaceImplemented << std::endl; + } +} + +////////////////////////////////////////////////// +extern "C" const char IGNITION_PLUGIN_LOADER_VISIBLE *ignitionVersion() +{ + return IGNITION_PLUGIN_VERSION_FULL; +} diff --git a/loader/src/ign_TEST.cc b/loader/src/ign_TEST.cc new file mode 100644 index 00000000..eb97ed4b --- /dev/null +++ b/loader/src/ign_TEST.cc @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2020 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include +#include + +#include "gtest/gtest.h" +#include "ignition/plugin/Loader.hh" + +#ifdef _MSC_VER +# define popen _popen +# define pclose _pclose +#endif + +using namespace ignition; + +static const std::string g_ignVersion("--force-version " + // NOLINT(*) + std::string(IGN_VERSION_FULL)); + +///////////////////////////////////////////////// +std::string custom_exec_str(std::string _cmd) +{ + _cmd += " 2>&1"; + FILE *pipe = popen(_cmd.c_str(), "r"); + + if (!pipe) + return "ERROR"; + + char buffer[128]; + std::string result = ""; + + while (!feof(pipe)) + { + if (fgets(buffer, 128, pipe) != NULL) + result += buffer; + } + + pclose(pipe); + return result; +} + +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --info' for a non-existent file. +TEST(ignTest, PluginInfoNonexistentLibrary) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH) + "/ign"; + + std::string output = custom_exec_str(ign + " plugin --info --plugin " + + "/path/to/libDoesNotExist.so"); + + EXPECT_NE(std::string::npos, output.find("Error while loading the library")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 plugins in library file")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 interfaces in library")) + << output; +} + +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --info' for a file that isn't a shared library. +TEST(ignTest, PluginInfoNonLibrary) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH) + "/ign"; + + std::string output = custom_exec_str(ign + " plugin --info --plugin " + + std::string(IGN_PLUGIN_SOURCE_DIR) + "/core/src/Plugin.cc"); + + EXPECT_NE(std::string::npos, output.find("Error while loading the library")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 plugins in library file")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 interfaces in library")) + << output; +} + +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --info' for a library that has no plugins. +TEST(ignTest, PluginInfoNonPluginLibrary) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH) + "/ign"; + + std::string output = custom_exec_str(ign + " plugin --info --plugin " + + IGN_PLUGIN_LIB); + + EXPECT_NE(std::string::npos, output.find("does not export any plugins. The " + "symbol [IgnitionPluginHook] is missing, or it is not externally visible.")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 plugins in library file")) + << output; + EXPECT_NE(std::string::npos, output.find("Found 0 interfaces in library")) + << output; +} + +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --info' for a library with plugins. +TEST(ignTest, PluginInfoDummyPlugins) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH) + "/ign"; + + std::string output = custom_exec_str(ign + " plugin --info --plugin " + + IGNDummyPlugins_LIB); + + EXPECT_NE(std::string::npos, output.find("Found 3 plugins in library file")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyMultiPlugin")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyNoAliasPlugin")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummySinglePlugin")) + << output; + + EXPECT_NE(std::string::npos, output.find("Found 7 interfaces in library")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyNameBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyGetObjectBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyIntBase")) + << output; + EXPECT_NE(std::string::npos, + output.find("test::util::DummyGetPluginInstancePtr")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyDoubleBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummySetterBase")) + << output; + EXPECT_NE(std::string::npos, + output.find("ignition::plugin::EnablePluginFromThis")) + << output; +} + +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --info' with verbose output. +TEST(ignTest, PluginInfoVerboseDummyPlugins) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH) + "/ign"; + + std::string output = custom_exec_str(ign + " plugin --info --plugin " + + IGNDummyPlugins_LIB + " --verbose"); + + EXPECT_NE(std::string::npos, output.find("Known Interfaces: 7")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyNameBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyGetObjectBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyIntBase")) + << output; + EXPECT_NE(std::string::npos, + output.find("test::util::DummyGetPluginInstancePtr")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummyDoubleBase")) + << output; + EXPECT_NE(std::string::npos, output.find("test::util::DummySetterBase")) + << output; + EXPECT_NE(std::string::npos, + output.find("ignition::plugin::EnablePluginFromThis")) + << output; + + EXPECT_NE(std::string::npos, output.find("Known Plugins: 3")) + << output; + + EXPECT_NE(std::string::npos, + output.find("There are 2 aliases with a name collision")) + << output; +}