diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f34b97bd..4ede0594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,12 @@ name: Ubuntu CI -on: [push, pull_request] +on: + pull_request: + push: + branches: + - 'ign-fuel-tools[0-9]' + - 'gz-fuel-tools[0-9]?' + - 'main' jobs: jammy-ci: @@ -8,7 +14,7 @@ jobs: name: Ubuntu Jammy CI steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Compile and test id: ci uses: gazebo-tooling/action-gz-ci@jammy diff --git a/.github/workflows/package_xml.yml b/.github/workflows/package_xml.yml new file mode 100644 index 00000000..4bd4a9aa --- /dev/null +++ b/.github/workflows/package_xml.yml @@ -0,0 +1,11 @@ +name: Validate package.xml + +on: + pull_request: + +jobs: + package-xml: + runs-on: ubuntu-latest + name: Validate package.xml + steps: + - uses: gazebo-tooling/action-gz-ci/validate_package_xml@jammy diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 2c94852d..2332244b 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -14,4 +14,3 @@ jobs: with: project-url: https://github.com/orgs/gazebosim/projects/7 github-token: ${{ secrets.TRIAGE_TOKEN }} - diff --git a/Changelog.md b/Changelog.md index ec6d7fe2..34ab4c54 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,37 @@ ## Gazebo Fuel Tools 9.x +### Gazebo Fuel Tools 9.0.3 (2024-04-09) + +1. Use relative install path for gz tool data + * [Pull request #409](https://github.com/gazebosim/gz-fuel-tools/pull/409) + +### Gazebo Fuel Tools 9.0.2 (2024-03-18) + +1. Fix `LocalCache` so that models/worlds downloaded via fuel.ignitionrobotics.org can be found in the cache + * [Pull request #406](https://github.com/gazebosim/gz-fuel-tools/pull/406) + +### Gazebo Fuel Tools 9.0.1 (2024-03-14) + +1. Tidy nested namespaces + * [Pull request #396](https://github.com/gazebosim/gz-fuel-tools/pull/396) + +1. Update CI badges in README + * [Pull request #393](https://github.com/gazebosim/gz-fuel-tools/pull/393) + +1. Create directories and more output on fail + * [Pull request #392](https://github.com/gazebosim/gz-fuel-tools/pull/392) + +1. Disable tests that are known to fail on Windows + * [Pull request #387](https://github.com/gazebosim/gz-fuel-tools/pull/387) + +1. Update github action workflows + * [Pull request #388](https://github.com/gazebosim/gz-fuel-tools/pull/388) + * [Pull request #390](https://github.com/gazebosim/gz-fuel-tools/pull/390) + +1. Re-enabling Windows tests + * [Pull request #376](https://github.com/gazebosim/gz-fuel-tools/pull/376) + ### Gazebo Fuel Tools 9.0.0 (2023-09-29) 1. Added script to update assets to gz @@ -374,6 +405,17 @@ 1. Support link referral download * [Pull request #333](https://github.com/gazebosim/gz-fuel-tools/pull/333) +### Gazebo Fuel Tools 4.9.1 (2024-01-05) + +1. Create directories and more output on fail + * [Pull request #392](https://github.com/gazebosim/gz-fuel-tools/pull/392) + +1. Update github action workflows + * [Pull request #388](https://github.com/gazebosim/gz-fuel-tools/pull/388) + +1. Zip: use non-deprecated methods + * [Pull request #360](https://github.com/gazebosim/gz-fuel-tools/pull/360) + ### Gazebo Fuel Tools 4.9.0 (2023-05-03) 1. Add bash completion diff --git a/README.md b/README.md index 9cef36db..fe40bb12 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,25 @@ Downloading model: Download succeeded. ``` +**Private access tokens** +Private models and worlds can be downloaded using access tokens. +Access tokens are generated on `app.gazebosim.org`. After logging in, +go to `Settings->Access Tokens`. + +An access token can be used with CLI commands via the `--header` option: + +```bash +$ gz fuel download -u https://fuel.gazebosim.org/1.0/openrobotics/models/ambulance --header 'Private-Token: <access_token>' +``` + +Or, an access token can be stored in a `~/.gz/fuel/config.yaml` file. The token is then +automatically used by the command line tool and API calls. Use the `configure` helper +tool create your `~/.gz/fuel/config.yaml` file. + +```bash +$ gz fuel configure +``` + **C++ Get List models** ```cpp // Create a client (uses https://fuel.gazebosim.org by default) diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt index a37a5804..2cffca0e 100644 --- a/conf/CMakeLists.txt +++ b/conf/CMakeLists.txt @@ -17,8 +17,4 @@ configure_file( "${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.yaml" @ONLY) # Install the yaml configuration files in an unversioned location. -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.yaml DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/) - -# Install config.yaml -install (FILES config.yaml DESTINATION - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}/) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.yaml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/gz/) diff --git a/conf/config.yaml b/conf/config.yaml deleted file mode 100644 index bf70a5dd..00000000 --- a/conf/config.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# The list of servers. -servers: - - - name: osrf - url: https://fuel.gazebosim.org - - # - - # name: another_server - # url: https://myserver - -# Where are the assets stored in disk. -# cache: -# path: /tmp/gz/fuel diff --git a/include/gz/fuel_tools/WorldIdentifier.hh b/include/gz/fuel_tools/WorldIdentifier.hh index 9be30374..62c8d738 100644 --- a/include/gz/fuel_tools/WorldIdentifier.hh +++ b/include/gz/fuel_tools/WorldIdentifier.hh @@ -148,6 +148,16 @@ namespace gz::fuel_tools /// \return World information string public: std::string AsPrettyString(const std::string &_prefix = "") const; + /// \brief Returns the privacy setting of the world. + /// \return True if the world is private, false if the world is + /// public. + public: bool Private() const; + + /// \brief Set the privacy setting of the world. + /// \param[in] _private True indicates the world is private, + /// false indicates the world is public. + public: void SetPrivate(bool _private); + /// \brief PIMPL private: std::unique_ptr<WorldIdentifierPrivate> dataPtr; }; diff --git a/package.xml b/package.xml new file mode 100644 index 00000000..1cff2719 --- /dev/null +++ b/package.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> +<package format="2"> + <name>gz-fuel_tools10</name> + <version>10.0.0</version> + <description>Gazebo Fuel Tools: Classes and tools for interacting with Gazebo Fuel</description> + <maintainer email="natekoenig@gmail.com">Nate Koenig</maintainer> + <license>Apache License 2.0</license> + <url type="website">https://github.com/gazebosim/gz-fuel-tools</url> + + <buildtool_depend>cmake</buildtool_depend> + + <build_depend>gz-cmake4</build_depend> + + <depend>gz-common6</depend> + <depend>gz-math8</depend> + <depend>gz-msgs11</depend> + <depend>gz-tools2</depend> + <depend>gz-utils3</depend> + <depend>libcurl-dev</depend> + <depend>libgflags-dev</depend> + <depend>libjsoncpp-dev</depend> + <depend>libyaml-dev</depend> + <depend>libzip-dev</depend> + <depend>tinyxml2</depend> + + <export> + <build_type>cmake</build_type> + </export> +</package> diff --git a/src/ClientConfig.cc b/src/ClientConfig.cc index fe60242e..ba17c0f2 100644 --- a/src/ClientConfig.cc +++ b/src/ClientConfig.cc @@ -78,12 +78,22 @@ ClientConfig::ClientConfig() : dataPtr(new ClientConfigPrivate) return; } - if (!gz::common::isDirectory(gzFuelPath)) + if (!gzFuelPath.empty()) { - gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; - return; + if (!gz::common::isDirectory(gzFuelPath)) + gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; + else + this->SetCacheLocation(gzFuelPath); } - this->SetCacheLocation(gzFuelPath); + + std::string configYamlFile = common::joinPaths(this->CacheLocation(), + "config.yaml"); + std::string configYmlFile = common::joinPaths(this->CacheLocation(), + "config.yml"); + if (gz::common::exists(configYamlFile)) + this->LoadConfig(configYamlFile); + else if (gz::common::exists(configYmlFile)) + this->LoadConfig(configYmlFile); } ////////////////////////////////////////////////// diff --git a/src/FuelClient_TEST.cc b/src/FuelClient_TEST.cc index c7569f75..8b223e1a 100644 --- a/src/FuelClient_TEST.cc +++ b/src/FuelClient_TEST.cc @@ -1159,7 +1159,8 @@ TEST_F(FuelClientTest, DownloadWorld) ///////////////////////////////////////////////// // Windows doesn't support colons in filenames // https://github.com/gazebosim/gz-fuel-tools/issues/106 -TEST_F(FuelClientTest, CachedWorld) +// This is fixed in gz-fuel-tools9+, but not here to preserve behavior +TEST_F(FuelClientTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(CachedWorld)) { ClientConfig config; config.SetCacheLocation(common::joinPaths(common::cwd(), "test_cache")); @@ -1490,8 +1491,11 @@ TEST_F(FuelClientTest, UploadModelFail) EXPECT_EQ(ResultType::UPLOAD_ERROR, result.Type()); } -////////////////////////////////////////////////// -TEST_F(FuelClientTest, PatchModelFail) +///////////////////////////////////////////////// +// Windows doesn't support colons in filenames +// https://github.com/gazebosim/gz-fuel-tools/issues/106 +// This is fixed in gz-fuel-tools9+, but not here to preserve behavior +TEST_F(FuelClientTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(PatchModelFail)) { FuelClient client; ModelIdentifier modelId; diff --git a/src/LocalCache.cc b/src/LocalCache.cc index 8a758bdf..32e15efa 100644 --- a/src/LocalCache.cc +++ b/src/LocalCache.cc @@ -279,16 +279,23 @@ Model LocalCache::MatchingModel(const ModelIdentifier &_id) // For the tip, we have to find the highest version bool tip = (_id.Version() == 0); Model tipModel; + if (!this->dataPtr->config) + return tipModel; - for (ModelIter iter = this->AllModels(); iter; ++iter) + std::string path = common::joinPaths( + this->dataPtr->config->CacheLocation(), uriToPath(_id.Server().Url())); + + auto srvModels = this->dataPtr->ModelsInServer(path); + for (auto &model: srvModels) { - ModelIdentifier id = iter->Identification(); + model.dataPtr->id.SetServer(_id.Server()); + auto id = model.Identification(); if (_id == id) { if (_id.Version() == id.Version()) - return *iter; + return model; else if (tip && id.Version() > tipModel.Identification().Version()) - tipModel = *iter; + tipModel = model; } } @@ -302,16 +309,24 @@ bool LocalCache::MatchingWorld(WorldIdentifier &_id) const bool tip = (_id.Version() == 0); WorldIdentifier tipWorld; - for (auto id = this->AllWorlds(); id; ++id) + if (!this->dataPtr->config) + return false; + + std::string path = common::joinPaths( + this->dataPtr->config->CacheLocation(), uriToPath(_id.Server().Url())); + + auto srvWorlds = this->dataPtr->WorldsInServer(path); + for (auto id: srvWorlds) { + id.SetServer(_id.Server()); if (_id == id) { - if (_id.Version() == id->Version()) + if (_id.Version() == id.Version()) { _id = id; return true; } - else if (tip && id->Version() > tipWorld.Version()) + else if (tip && id.Version() > tipWorld.Version()) { tipWorld = id; } diff --git a/src/RestClient.cc b/src/RestClient.cc index 222f0c8f..67f98c5b 100644 --- a/src/RestClient.cc +++ b/src/RestClient.cc @@ -124,13 +124,13 @@ size_t RestWriteMemoryCallback(void *_buffer, size_t _size, size_t _nmemb, } ///////////////////////////////////////////////// -struct curl_httppost *BuildFormPost( +void AddFormPost( + curl_mime * const multipart, const std::multimap<std::string, std::string> &_form) { - struct curl_httppost *formpost = nullptr; - struct curl_httppost *lastptr = nullptr; for (const auto &[key, value] : _form) { + curl_mimepart *part = curl_mime_addpart(multipart); // follow same convention as curl cmdline tool // field starting with @ indicates path to file to upload // others are standard fields to describe the file @@ -171,26 +171,17 @@ struct curl_httppost *BuildFormPost( } } - curl_formadd(&formpost, - &lastptr, - CURLFORM_COPYNAME, key.c_str(), - CURLFORM_FILENAME, uploadFilename.c_str(), - CURLFORM_FILE, path.c_str(), - CURLFORM_CONTENTTYPE, contentType.c_str(), - CURLFORM_END); + curl_mime_name(part, key.c_str()); + curl_mime_filename(part, uploadFilename.c_str()); + curl_mime_filedata(part, path.c_str()); + curl_mime_type(part, contentType.c_str()); } else { - // standard key:value fields - curl_formadd(&formpost, - &lastptr, - CURLFORM_COPYNAME, key.c_str(), - CURLFORM_COPYCONTENTS, value.c_str(), - CURLFORM_END); + curl_mime_name(part, key.c_str()); + curl_mime_data(part, value.c_str(), CURL_ZERO_TERMINATED); } } - - return formpost; } ///////////////////////////////////////////////// @@ -223,6 +214,7 @@ RestResponse Rest::Request(HttpMethod _method, encodedPath = curl_easy_escape(curl, decodedPath, decodedSize); url = RestJoinUrl(url, encodedPath); + curl_free(decodedPath); } // Process query strings. @@ -293,7 +285,7 @@ RestResponse Rest::Request(HttpMethod _method, curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); std::ifstream ifs; - struct curl_httppost *formpost = nullptr; + curl_mime *multipart = curl_mime_init(curl); // Send the request. if (_method == HttpMethod::GET) @@ -302,9 +294,11 @@ RestResponse Rest::Request(HttpMethod _method, } else if (_method == HttpMethod::PATCH_FORM) { - formpost = BuildFormPost(_form); + AddFormPost(multipart, _form); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH"); - curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost); + + curl_easy_setopt(curl, CURLOPT_MIMEPOST, multipart); } else if (_method == HttpMethod::POST) { @@ -313,8 +307,8 @@ RestResponse Rest::Request(HttpMethod _method, } else if (_method == HttpMethod::POST_FORM) { - formpost = BuildFormPost(_form); - curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost); + AddFormPost(multipart, _form); + curl_easy_setopt(curl, CURLOPT_MIMEPOST, multipart); } else if (_method == HttpMethod::DELETE) { @@ -354,9 +348,6 @@ RestResponse Rest::Request(HttpMethod _method, // Update the header data. res.headers = headerData; - if (formpost) - curl_formfree(formpost); - // free encoded path char* if (encodedPath) curl_free(encodedPath); @@ -365,6 +356,7 @@ RestResponse Rest::Request(HttpMethod _method, curl_slist_free_all(headers); // Cleaning. + curl_mime_free(multipart); curl_easy_cleanup(curl); if (ifs.is_open()) diff --git a/src/WorldIdentifier.cc b/src/WorldIdentifier.cc index fd07e8fd..02d36313 100644 --- a/src/WorldIdentifier.cc +++ b/src/WorldIdentifier.cc @@ -44,8 +44,12 @@ class WorldIdentifierPrivate /// \brief World version. Valid versions start from 1, 0 means the tip. public: unsigned int version{0}; - /// \brief Path of this model in the local cache + /// \brief Path of this world in the local cache public: std::string localPath; + + /// \brief True indicates the world is private, false indicates the + /// world is public. + public: bool privacy{false}; }; ////////////////////////////////////////////////// @@ -229,4 +233,15 @@ std::string WorldIdentifier::AsPrettyString(const std::string &_prefix) const << this->Server().AsPrettyString(_prefix + " "); return out.str(); } +////////////////////////////////////////////////// +bool WorldIdentifier::Private() const +{ + return this->dataPtr->privacy; +} + +////////////////////////////////////////////////// +void WorldIdentifier::SetPrivate(bool _private) +{ + this->dataPtr->privacy = _private; +} } // namespace gz::fuel_tools diff --git a/src/WorldIdentifier_TEST.cc b/src/WorldIdentifier_TEST.cc index 91c2d963..4e293f41 100644 --- a/src/WorldIdentifier_TEST.cc +++ b/src/WorldIdentifier_TEST.cc @@ -37,6 +37,12 @@ TEST(WorldIdentifier, SetFields) EXPECT_EQ(std::string("hello"), id.Name()); EXPECT_EQ(std::string("acai"), id.Owner()); EXPECT_EQ(6u, id.Version()); + + EXPECT_FALSE(id.Private()); + id.SetPrivate(true); + EXPECT_TRUE(id.Private()); + id.SetPrivate(false); + EXPECT_FALSE(id.Private()); } ///////////////////////////////////////////////// diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 6c027466..833f2856 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -57,4 +57,4 @@ install( FILES ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.bash_completion.sh DESTINATION - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/gz${GZ_TOOLS_VER}.completion.d) + ${CMAKE_INSTALL_DATAROOTDIR}/gz/gz${GZ_TOOLS_VER}.completion.d) diff --git a/src/cmd/cmdfuel.rb.in b/src/cmd/cmdfuel.rb.in index 632d9443..23e89a67 100755 --- a/src/cmd/cmdfuel.rb.in +++ b/src/cmd/cmdfuel.rb.in @@ -25,7 +25,10 @@ else include Fiddle end +require 'fileutils' require 'optparse' +require 'uri' +require 'yaml' # Constants. LIBRARY_NAME = '@library_location@' @@ -61,6 +64,7 @@ COMMANDS = { 'fuel' => " gz fuel [action] [options] \n"\ " \n"\ "Available Actions: \n"\ + " configure Create config.yaml configuration file \n"\ " delete Delete resources \n"\ " download Download resources \n"\ " edit Edit a resource \n"\ @@ -80,6 +84,20 @@ COMMANDS = { 'fuel' => } SUBCOMMANDS = { + 'configure' => + "Create `~/.gz/fuel/config.yaml` to hold Fuel server configurations. \n"\ + " \n"\ + " gz fuel configure [options] \n"\ + " \n"\ + " --defaults Use all the defaults and save. \n"\ + " This will overwrite ~/.gz/fuel/config.yaml. \n"\ + " --console Output to the console instead of to a file. \n"\ + " -h [--help] Print this help message. \n"\ + " \n"\ + " --force-version <VERSION> Use a specific library version. \n"\ + " \n"\ + " --versions Show the available versions. \n", + 'delete' => "Delete simulation resources \n"\ " \n"\ @@ -208,7 +226,9 @@ class Cmd 'pbtxt2config' => '', 'private' => '', 'onlymodels' => '0', - 'onlyworlds' => '0' + 'onlyworlds' => '0', + 'defaults' => false, + 'console' => false } usage = COMMANDS[args[0]] @@ -276,6 +296,12 @@ class Cmd opts.on('--onlyworlds', 'Only update worlds') do options['onlyworlds'] = '1' end + opts.on('--defaults', 'Use default values') do + options['defaults'] = true + end + opts.on('--console', 'Output to console') do + options['console'] = true + end end # opt_parser do opt_parser.parse!(args) @@ -347,6 +373,12 @@ class Cmd end # parse() def execute(args) + # Graceful exit on ctrl-c + Signal.trap("SIGINT") do + puts "\nExiting" + exit + end + options = parse(args) # Read the plugin that handles the command. @@ -394,6 +426,8 @@ class Cmd end case options['subcommand'] + when 'configure' + configure(options['defaults'], options['console']) when 'delete' Importer.extern 'int deleteUrl(const char *, const char *)' if not Importer.deleteUrl(options['url'], options['header']) @@ -463,4 +497,118 @@ class Cmd "from #{plugin}." end # begin end # execute + + # Runs `gz fuel configure` + def configure(defaults, console) + # Default fuel directory and configuration path + local_fuel_dir = File.join(Dir.home, '.gz', 'fuel') + config_path = File.join(local_fuel_dir, 'config.yaml') + + # The default Fuel server URL + default_url = 'https://fuel.gazebosim.org' + + # A lambda function that prompts the user to enter Fuel server information + get_server_info = lambda { + server_url = default_url + default_name = 'Fuel' + + # Prompt the user for a server name with a default value + # Repeat until the URL is valid, or the user hits ctrl-c + until defaults + print "Fuel server URL [#{default_url}]: " + server_url = STDIN.gets.chomp + server_url = default_url if server_url.empty? + begin + uri = URI.parse(server_url) + default_name = !uri.host || uri.host.empty? ? uri.to_s : uri.host + break + rescue URI::InvalidURIError + puts 'Invalid URL.\n' + end + end + + # Prompt the user for an access token with a default value of an + # empty string + access_token = '' + unless defaults + print 'Optional access token [None]: ' + access_token = STDIN.gets.chomp + end + + # Get the cache location + cache = local_fuel_dir + unless defaults + print "Local cache path [#{local_fuel_dir}]: " + cache = STDIN.gets.chomp + cache = local_fuel_dir if cache.empty? + end + + # Get a name to associate with this server + server_name = default_name + unless defaults + print "Name this server [#{default_name}]: " + server_name = STDIN.gets.chomp + server_name = default_name if server_name.empty? + end + + [server_name, server_url, access_token, cache] + } + + unless console + puts '# Set Fuel server configurations.' + puts "# This will create or replace `#{config_path}`.\n\n" + end + + servers = [] + confirmation = 'n' + # Allow the user to enter multiple Fuel servers + begin + server_name, server_url, access_token, cache = get_server_info.call + servers << { 'name' => server_name, + 'url' => server_url, + 'private-token' => access_token, + 'cache' => { 'path' => cache } } + unless defaults + print "\nAdd another Fuel server? [y/N]:" + confirmation = STDIN.gets.chomp.downcase + confirmation = 'n' if confirmation.empty? + end + end while confirmation == 'y' + + unless defaults || console + puts "\nReview:\n" + servers.each do |server| + puts " Name: #{server['name']}" + puts " URL: #{server['url']}" + puts " Cache: #{server['cache']['path']}" + puts " Access token: #{server['private-token']}" + puts "\n" if servers.size > 1 + end + end + + confirmation = 'y' + unless defaults || console + print 'Save? [Y/n]:' + confirmation = STDIN.gets.chomp.downcase + confirmation = 'y' if confirmation.empty? + end + + # Save settings + if confirmation == 'y' + config = {'servers' => servers} + + if console + puts config.to_yaml + else + # Make sure the ~/.gz/fuel directory exists. + FileUtils.mkdir_p(local_fuel_dir) + + # Write to the config.yaml file + File.open(config_path, 'w') { |file| file.write(config.to_yaml) } + puts 'Settings saved to ~/.gz/fuel/config.yaml.' + end + else + puts 'Settings not saved.' + end + end end # class diff --git a/src/gz.cc b/src/gz.cc index cb2e2b19..567de676 100644 --- a/src/gz.cc +++ b/src/gz.cc @@ -16,6 +16,7 @@ */ #include <curl/curl.h> +#include <curl/easy.h> #include <string.h> #include <tinyxml2.h> @@ -136,8 +137,10 @@ extern "C" void uglyPrint( std::cout << _serverConfig.Url().Str() << "/" << _serverConfig.Version() << "/" << owner->first << "/" << _resourceType << "/" << std::string(encodedRes) << std::endl; + curl_free(encodedRes); } } + curl_easy_cleanup(curl); } ////////////////////////////////////////////////// diff --git a/src/gz_TEST.cc b/src/gz_TEST.cc index 815fa7f6..eb6ab82b 100644 --- a/src/gz_TEST.cc +++ b/src/gz_TEST.cc @@ -47,7 +47,7 @@ std::string custom_exec_str(std::string _cmd) return result; } -auto g_version = std::string(strdup(GZ_FUEL_TOOLS_VERSION_FULL)); +auto g_version = std::string(GZ_FUEL_TOOLS_VERSION_FULL); auto g_exec = std::string(GZ_PATH); auto g_listCmd = g_exec + " fuel list -v 4 --force-version " + g_version; @@ -100,8 +100,9 @@ TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ListFail)) TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ModelListConfigServerUgly)) { - auto output = custom_exec_str(g_listCmd + " -t model --raw"); - EXPECT_NE(output.find("https://fuel.gazebosim.org/1.0/"), + auto output = custom_exec_str(g_listCmd + + " -t model --raw -u 'https://fuel.gazebosim.org' -o openrobotics"); + EXPECT_NE(output.find("https://fuel.gazebosim.org"), std::string::npos) << output; EXPECT_EQ(output.find("owners"), std::string::npos) << output; } @@ -151,3 +152,19 @@ TEST(CmdLine, EXPECT_NE(output.find("owners"), std::string::npos) << output; EXPECT_NE(output.find("worlds"), std::string::npos) << output; } + +TEST(CmdLine, + GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ConfigureDefaultsConsole)) +{ + std::string output = custom_exec_str( + g_exec + " fuel configure --console --defaults"); + + std::string expected = + "---\n" + "servers:\n" + "- name: Fuel\n" + " url: https://fuel.gazebosim.org\n" + " private-token: ''\n"; + + EXPECT_EQ(output.find(expected), 0); +} diff --git a/src/gz_src_TEST.cc b/src/gz_src_TEST.cc index 082fae62..e3ab3a7e 100644 --- a/src/gz_src_TEST.cc +++ b/src/gz_src_TEST.cc @@ -51,6 +51,8 @@ class CmdLine : public ::testing::Test // instead of on teardown leaves the folder intact for debugging if needed common::removeAll(testCachePath); ASSERT_TRUE(common::createDirectories(testCachePath)); + ASSERT_TRUE(common::createDirectories( + common::joinPaths(testCachePath, "fuel.gazebosim.org"))); setenv("GZ_FUEL_CACHE_PATH", this->testCachePath.c_str(), true); } @@ -85,7 +87,7 @@ TEST_F(CmdLine, ModelListFail) EXPECT_NE(this->stdOutBuffer.str().find("Invalid URL"), std::string::npos) << this->stdOutBuffer.str(); - EXPECT_TRUE(this->stdErrBuffer.str().empty()); + EXPECT_TRUE(this->stdErrBuffer.str().empty()) << this->stdErrBuffer.str(); } ///////////////////////////////////////////////// @@ -93,7 +95,8 @@ TEST_F(CmdLine, ModelListFail) // https://github.com/gazebosim/gz-fuel-tools/issues/105 TEST_F(CmdLine, ModelListConfigServerUgly) { - EXPECT_TRUE(listModels("", "openroboticstest", "true")); + EXPECT_TRUE(listModels("https://fuel.gazebosim.org", + "openroboticstest", "true")); EXPECT_NE(this->stdOutBuffer.str().find("https://fuel.gazebosim.org"), std::string::npos) << this->stdOutBuffer.str(); diff --git a/tutorials/02_configuration.md b/tutorials/02_configuration.md index 620a071a..f1523fee 100644 --- a/tutorials/02_configuration.md +++ b/tutorials/02_configuration.md @@ -35,6 +35,17 @@ The `cache` section captures options related with the local storage of the assets. `path` specifies the local directory where all assets will be downloaded. If not used, all assets are stored under `$HOME/.gz/fuel`. +## Guided Configuration + +The `gz fuel configure` CLI will walk you through the process of creating a +`~/.gz/fuel/config.yaml` file. Just run the following command, and answer +the prompts. Note that this command will replace your existing `~/.gz/fuel/config.yaml` +if you choose to save on the last step. + +```bash +gz fuel configure +``` + ## Custom configuration file path Gazebo Fuel's default configuration file is stored under