diff --git a/loader/src/cmd/CMakeLists.txt b/loader/src/cmd/CMakeLists.txt index f082e2f0..f443bf10 100644 --- a/loader/src/cmd/CMakeLists.txt +++ b/loader/src/cmd/CMakeLists.txt @@ -1,3 +1,26 @@ +add_library(ign STATIC ign.cc) +target_include_directories(ign PUBLIC ${CMAKE_SOURCE_DIR}/loader/include) +target_link_libraries(ign PUBLIC + ${PROJECT_LIBRARY_TARGET_NAME} +) + +set(plugin_executable ign-plugin) +add_executable(${plugin_executable} plugin_main.cc) +target_link_libraries(${plugin_executable} + ign + ignition-utils${IGN_UTILS_VER}::cli + ${loader} +) + +set(EXE_INSTALL_DIR "${IGN_LIB_INSTALL_DIR}/ignition/${IGN_DESIGNATION}${PROJECT_VERSION_MAJOR}") + +install( + TARGETS + ${plugin_executable} + DESTINATION + ${EXE_INSTALL_DIR} +) + #=============================================================================== # Generate the ruby script for internal testing. # Note that the major version of the library is included in the name. @@ -7,7 +30,7 @@ set(cmd_script_configured_test "${CMAKE_CURRENT_BINARY_DIR}/test_cmd${IGN_DESIGN # Set the library_location variable to the full path of the library file within # the build directory. -set(library_location "$") +set(plugin_exe_location "$") configure_file( "cmd${IGN_DESIGNATION}.rb.in" @@ -29,7 +52,7 @@ set(cmd_script_configured "${CMAKE_CURRENT_BINARY_DIR}/cmd${IGN_DESIGNATION}${PR # Set the library_location variable to the relative path to the library file # within the install directory structure. -set(library_location "../../../${CMAKE_INSTALL_LIBDIR}/$") +set(plugin_exe_location "../../../${EXE_INSTALL_DIR}/$") configure_file( "cmd${IGN_DESIGNATION}.rb.in" diff --git a/loader/src/cmd/cmdplugin.rb.in b/loader/src/cmd/cmdplugin.rb.in index d4b5c682..d5f30501 100644 --- a/loader/src/cmd/cmdplugin.rb.in +++ b/loader/src/cmd/cmdplugin.rb.in @@ -1,6 +1,6 @@ -#!/usr/bin/ruby +#!/usr/bin/env ruby -# Copyright (C) 2020 Open Source Robotics Foundation +# Copyright (C) 2021 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. @@ -14,172 +14,52 @@ # 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' +require 'open3' # 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 - } +COMMANDS = { + "plugin" => "@plugin_exe_location@", +} # -# Class for the Ignition plugin command line tools. +# Class for the Ignition @IGN_DESIGNATION@ 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 + command = args[0] + exe_name = COMMANDS[command] - # Read the plugin that handles the command. - if LIBRARY_NAME[0] == '/' + if exe_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 + # absolute path to the executable. This is only used during test mode. 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)) + exe_name = File.expand_path(File.join(File.dirname(__FILE__), exe_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 + exe_version = `#{exe_name} --version`.strip # 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 + unless exe_version.eql? conf_version puts "Error: Version mismatch. Your configuration file version is - [#{conf_version}] but #{plugin} version is [#{plugin_version}]." + [#{conf_version}] but #{exe_name} version is [#{exe_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.' + # Drop command from list of arguments + Open3.popen2e(exe_name, *args[1..-1]) do |_in, out_err, wait_thr| + begin + out_err.each do |line| + print line end - else - puts 'Command error: I do not have an implementation for '\ - "command [ign #{options['command']}]." + exit(wait_thr.value.exitstatus) + rescue Interrupt => e + print e.message + exit(-1) end - rescue - puts "Library error: Problem running [#{options['command']}]() "\ - "from #{plugin}." end end end diff --git a/loader/src/ign.cc b/loader/src/cmd/ign.cc similarity index 87% rename from loader/src/ign.cc rename to loader/src/cmd/ign.cc index 7f17739c..11488923 100644 --- a/loader/src/ign.cc +++ b/loader/src/cmd/ign.cc @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Open Source Robotics Foundation + * Copyright (C) 2021 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. @@ -15,17 +15,17 @@ * */ +#include #include #include +#include #include "ignition/plugin/Loader.hh" #include "ignition/plugin/config.hh" - -using namespace ignition; -using namespace plugin; +#include "ign.hh" ////////////////////////////////////////////////// -extern "C" void IGNITION_PLUGIN_LOADER_VISIBLE cmdPluginInfo( +extern "C" void cmdPluginInfo( const char *_plugin, int _verbose) { if (!_plugin || std::string(_plugin).empty()) @@ -73,7 +73,7 @@ extern "C" void IGNITION_PLUGIN_LOADER_VISIBLE cmdPluginInfo( } ////////////////////////////////////////////////// -extern "C" const char IGNITION_PLUGIN_LOADER_VISIBLE *ignitionVersion() +extern "C" const char *ignitionVersion() { - return IGNITION_PLUGIN_VERSION_FULL; + return strdup(IGNITION_PLUGIN_VERSION_FULL); } diff --git a/loader/src/cmd/ign.hh b/loader/src/cmd/ign.hh new file mode 100644 index 00000000..affc277e --- /dev/null +++ b/loader/src/cmd/ign.hh @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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. + * +*/ + +#ifndef IGNITION_PLUGIN_IGN_HH_ +#define IGNITION_PLUGIN_IGN_HH_ + +#include "ignition/plugin/Export.hh" + +/// \brief External hook to read the library version. +/// \return C-string representing the version. Ex.: 0.1.2 +extern "C" const char *ignitionVersion(); + +/// \brief Plugin info +/// \param[in] _plugin Name of the plugin +/// \param[in] _verbose Verbosity level +extern "C" void cmdPluginInfo(const char *_plugin, int _verbose); + +#endif diff --git a/loader/src/cmd/plugin_main.cc b/loader/src/cmd/plugin_main.cc new file mode 100644 index 00000000..75be2d0e --- /dev/null +++ b/loader/src/cmd/plugin_main.cc @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 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 "ign.hh" + +////////////////////////////////////////////////// +/// \brief Enumeration of available commands +enum class PluginCommand +{ + kNone, + kPluginInfo +}; + +////////////////////////////////////////////////// +/// \brief Structure to hold all available topic options +struct PluginOptions +{ + /// \brief Command to execute + PluginCommand command{PluginCommand::kNone}; + + int verboseLevel = 0; + + std::string pluginName; +}; + +////////////////////////////////////////////////// +void runPluginCommand(const PluginOptions &_opt) +{ + if (_opt.command == PluginCommand::kPluginInfo) + { + cmdPluginInfo(_opt.pluginName.c_str(), _opt.verboseLevel); + } + else if (_opt.command == PluginCommand::kNone) + { + // In the event that there is no command, display help + throw CLI::CallForHelp(); + } +} + +void addPluginFlags(CLI::App &_app) +{ + auto opt = std::make_shared(); + + _app.add_flag_callback("-v,--verbose", + [opt](){ + opt->verboseLevel = 1; + }, "Print verbose info."); + + auto info = _app.add_flag_callback("-i,--info", + [opt](){ + opt->command = PluginCommand::kPluginInfo; + }, "Get info about a plugin.\nRequires the -p option."); + + _app.add_option("-p,--plugin", + opt->pluginName, + "Path to a plugin.\n" + "Required with -i.")->needs(info); + + _app.callback([&_app, opt](){ + runPluginCommand(*opt); + }); +} + +////////////////////////////////////////////////// +int main(int argc, char** argv) +{ + CLI::App app{"Introspect Ignition plugin"}; + + app.set_help_all_flag("--help-all", "Show all help"); + + app.add_flag_callback("--version", [](){ + std::cout << ignitionVersion() << std::endl; + throw CLI::Success(); + }); + + addPluginFlags(app); + CLI11_PARSE(app, argc, argv); +} diff --git a/loader/src/ign_TEST.cc b/loader/src/ign_TEST.cc index e5036537..75c8c925 100644 --- a/loader/src/ign_TEST.cc +++ b/loader/src/ign_TEST.cc @@ -53,6 +53,29 @@ std::string custom_exec_str(std::string _cmd) return result; } +////////////////////////////////////////////////// +/// \brief Check 'ign plugin --help'. +TEST(ignTest, IgnPluginHelp) +{ + // Path to ign executable + std::string ign = std::string(IGN_PATH); + std::string output = custom_exec_str(ign + " plugin --help"); + EXPECT_NE(std::string::npos, + output.find("-i,--info Get info about a plugin.")) + << output; + EXPECT_NE(std::string::npos, + output.find("-p,--plugin TEXT Needs: --info")) + << output; + + output = custom_exec_str(ign + " plugin"); + EXPECT_NE(std::string::npos, + output.find("-i,--info Get info about a plugin.")) + << output; + EXPECT_NE(std::string::npos, + output.find("-p,--plugin TEXT Needs: --info")) + << output; +} + ////////////////////////////////////////////////// /// \brief Check 'ign plugin --info' for a non-existent file. TEST(ignTest, PluginInfoNonexistentLibrary)