diff --git a/bin/identify-library-abi.c b/bin/identify-library-abi.c new file mode 100644 index 0000000000000000000000000000000000000000..6482997148bef5cb14952b64b3a4e516cab9a79d --- /dev/null +++ b/bin/identify-library-abi.c @@ -0,0 +1,295 @@ +/* + * Copyright © 2021 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <errno.h> +#include <fcntl.h> +#include <ftw.h> +#include <gelf.h> +#include <libelf.h> +#include <sysexits.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <glib.h> + +#include <steam-runtime-tools/glib-backports-internal.h> +#include <steam-runtime-tools/utils-internal.h> + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(Elf, elf_end); + +/* nftw() doesn't have a user_data argument so we need to use a global + * variable */ +static GPtrArray *nftw_libraries = NULL; + +static gchar *opt_directory = FALSE; +static gboolean opt_ldconfig = FALSE; +static gboolean opt_print0 = FALSE; +static gboolean opt_print_version = FALSE; +static gboolean opt_skip_unversioned = FALSE; + +static const GOptionEntry option_entries[] = +{ + { "directory", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, + &opt_directory, "Check the word size for the libraries recursively found in this directory", + NULL }, + { "ldconfig", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, + &opt_ldconfig, "Check the word size for the libraries listed in ldconfig", NULL }, + { "print0", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, + &opt_print0, "The generated library=value pairs are terminated with a " + "null character instead of a newline", NULL }, + { "skip-unversioned", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, + &opt_skip_unversioned, "Skip the libraries that have a filename that end with " + "just \".so\"", NULL }, + { "version", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_print_version, + "Print version number and exit", NULL }, + { NULL } +}; + +static gint +list_libraries_helper (const char *fpath, + const struct stat *sb, + int typeflag, + struct FTW *ftwbuf) +{ + if (typeflag == FTW_SL) + { + if (strstr (fpath, ".so.") != NULL + || (!opt_skip_unversioned && g_str_has_suffix (fpath, ".so"))) + g_ptr_array_add (nftw_libraries, g_strdup (fpath)); + } + return 0; +} + +static void +print_library_details (const gchar *library_path, + const gchar separator, + FILE *original_stdout) +{ + glnx_autofd int fd = -1; + g_autoptr(Elf) elf = NULL; + int class = ELFCLASSNONE; + const gchar *identifier = NULL; + + GElf_Ehdr eh; + + if ((fd = open (library_path, O_RDONLY | O_CLOEXEC, 0)) < 0) + { + int saved_errno = errno; + g_debug ("Error reading \"%s\": %s\n", + library_path, strerror (saved_errno)); + return; + } + + if ((elf = elf_begin (fd, ELF_C_READ, NULL)) == NULL) + { + g_debug ("Error reading the library ELF: %s", elf_errmsg (elf_errno ())); + return; + } + + if (gelf_getehdr(elf, &eh) == NULL) + { + g_debug ("Error reading the library ELF header: %s", + elf_errmsg (elf_errno ())); + return; + } + + class = gelf_getclass (elf); + + if (class == ELFCLASS32 && eh.e_machine == EM_386) + identifier = "i386-linux-gnu"; + else if (class == ELFCLASS32 && eh.e_machine == EM_X86_64) + identifier = "x86_64-linux-gnux32"; + else if (class == ELFCLASS64 && eh.e_machine == EM_X86_64) + identifier = "x86_64-linux-gnu"; + else + identifier = "?"; + + fprintf (original_stdout, "%s=%s%c", library_path, identifier, separator); +} + +static gboolean +run (int argc, + char **argv, + GError **error) +{ + g_autoptr(FILE) original_stdout = NULL; + g_autofree gchar *output = NULL; + gint wait_status = 0; + gsize i; + const gchar *ldconfig_argv[] = + { + "/sbin/ldconfig", "-XNv", NULL, + }; + char separator = '\n'; + + /* stdout is reserved for machine-readable output, so avoid having + * things like g_debug() pollute it. */ + original_stdout = _srt_divert_stdout_to_stderr (error); + + if (original_stdout == NULL) + return FALSE; + + if (opt_print0) + separator = '\0'; + + if (elf_version (EV_CURRENT) == EV_NONE) + return glnx_throw (error, "elf_version(EV_CURRENT): %s", elf_errmsg (elf_errno ())); + + if (opt_ldconfig) + { + g_auto(GStrv) ldconfig_entries = NULL; + g_autofree gchar *library_prefix = NULL; + + if (!g_spawn_sync (NULL, /* working directory */ + (gchar **) ldconfig_argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, /* stdout */ + NULL, /* stderr */ + &wait_status, + error)) + { + return FALSE; + } + + if (wait_status != 0) + return glnx_throw (error, "Cannot run ldconfig: wait status %d", wait_status); + + if (output == NULL) + return glnx_throw (error, "ldconfig didn't produce anything in output"); + + ldconfig_entries = g_strsplit (output, "\n", -1); + + if (ldconfig_entries == NULL) + return glnx_throw (error, "ldconfig didn't produce anything in output"); + + for (i = 0; ldconfig_entries[i] != NULL; i++) + { + g_auto(GStrv) line_elements = NULL; + const gchar *library = NULL; + const gchar *colon = NULL; + g_autofree gchar *library_path = NULL; + + /* skip empty lines */ + if (ldconfig_entries[i][0] == '\0') + continue; + + colon = strchr (ldconfig_entries[i], ':'); + + if (colon != NULL) + { + g_clear_pointer (&library_prefix, g_free); + library_prefix = g_strndup (ldconfig_entries[i], colon - ldconfig_entries[i]); + continue; + } + + line_elements = g_strsplit (ldconfig_entries[i], " -> ", 2); + library = g_strstrip (line_elements[0]); + library_path = g_build_filename (library_prefix, library, NULL); + + print_library_details (library_path, separator, original_stdout); + } + } + else if (opt_directory != NULL) + { + g_autofree gchar *real_directory = NULL; + + nftw_libraries = g_ptr_array_new_full (512, g_free); + real_directory = realpath (opt_directory, NULL); + + if (real_directory == NULL) + return glnx_throw_errno_prefix (error, "Unable to realpath \"%s\"", opt_directory); + + if (nftw (real_directory, list_libraries_helper, 10, FTW_DEPTH|FTW_PHYS) < 0) + { + g_ptr_array_free (nftw_libraries, TRUE); + return glnx_throw_errno_prefix (error, "Unable to iterate through \"%s\"", opt_directory); + } + + for (i = 0; i < nftw_libraries->len; i++) + print_library_details (g_ptr_array_index (nftw_libraries, i), separator, + original_stdout); + + g_ptr_array_free (nftw_libraries, TRUE); + } + + return TRUE; +} + +int +main (int argc, + char **argv) +{ + g_autoptr(GOptionContext) option_context = NULL; + g_autoptr(GError) error = NULL; + int status = EXIT_SUCCESS; + + option_context = g_option_context_new (""); + g_option_context_add_main_entries (option_context, option_entries, NULL); + + if (!g_option_context_parse (option_context, &argc, &argv, &error)) + { + status = EX_USAGE; + goto out; + } + + if (opt_print_version) + { + /* Output version number as YAML for machine-readability, + * inspired by `ostree --version` and `docker version` */ + g_print ("%s:\n" + " Package: steam-runtime-tools\n" + " Version: %s\n", + g_get_prgname (), VERSION); + goto out; + } + + if (opt_ldconfig && opt_directory != NULL) + { + glnx_throw (&error, "--ldconfig and --directory cannot be used at the same time"); + status = EX_USAGE; + goto out; + } + + if (!opt_ldconfig && opt_directory == NULL) + { + glnx_throw (&error, "Either --ldconfig or --directory are required"); + status = EX_USAGE; + goto out; + } + + if (!run (argc, argv, &error)) + status = EXIT_FAILURE; + +out: + if (status != EXIT_SUCCESS) + g_printerr ("%s: %s\n", g_get_prgname (), error->message); + + return status; +} diff --git a/bin/identify-library-abi.md b/bin/identify-library-abi.md new file mode 100644 index 0000000000000000000000000000000000000000..de2c46b1890d4be11f2648fbc2ebe54fd86069a6 --- /dev/null +++ b/bin/identify-library-abi.md @@ -0,0 +1,56 @@ +--- +title: steam-runtime-identify-library-abi +section: 1 +... + +# NAME + +steam-runtime-identify-library-abi - Identify the ABI of the libraries stored in a specific directory or from the ldconfig output list + +# SYNOPSIS + +**steam-runtime-identify-library-abi** + +# DESCRIPTION + +# OPTIONS + +**--directory** *DIR* +: The list of libraries to identify is gathered by recursively search in *DIR*. + +**--ldconfig** +: Identify the ABI of the libraries listed by the executable `ldconfig`. + +**--print0** +: The generated library_path=library_ABI pairs are terminated with a null + character instead of a newline. + +**--skip-unversioned** +: If a library filename ends with just `.so`, its ABI will not be identified + and will not be printed in output. + +**--version** +: Instead of performing the libraries identification, write in output the + version number as YAML. + +# OUTPUT + +**steam-runtime-identify-library-abi** standard output is machine parsable, with +pairs of `library_path=library_ABI` separated by a null character, with the option +**--print0**, or by newlines. +Where `library_ABI` follows the Debian-style multiarch tuples convention and +currently can have the following values: `i386-linux-gnu`, `x86_64-linux-gnu`, +`x86_64-linux-gnux32`, or `?` that groups all the other possible ABIs. + +# EXIT STATUS + +0 +: Success. + +64 +: Invalid arguments were given (EX_USAGE). + +Other Nonzero +: An error occurred. + +<!-- vim:set sw=4 sts=4 et: --> diff --git a/bin/meson.build b/bin/meson.build index d0d1975479c3430cf7e72ca3a5fd81be96de28d9..0706659af464caa6d47e658e4ea9f9a21513609e 100644 --- a/bin/meson.build +++ b/bin/meson.build @@ -1,4 +1,4 @@ -# Copyright © 2019 Collabora Ltd. +# Copyright © 2019-2021 Collabora Ltd. # # SPDX-License-Identifier: MIT # @@ -41,6 +41,16 @@ executable( install_rpath : bin_rpath, ) +executable( + 'steam-runtime-identify-library-abi', + 'identify-library-abi.c', + dependencies : [glib, gio_unix, libelf, libglnx_dep, libsteamrt_static_dep], + install : true, + # Use GLib from the adjacent libdir, ignoring LD_LIBRARY_PATH + build_rpath : bin_rpath, + install_rpath : bin_rpath, +) + executable( 'steam-runtime-input-monitor', 'input-monitor.c', @@ -74,6 +84,7 @@ executable( if get_option('man') foreach bin_name : [ 'check-requirements', + 'identify-library-abi', 'input-monitor', 'steam-remote', 'system-info', diff --git a/tests/identify-library-abi-cli.c b/tests/identify-library-abi-cli.c new file mode 100644 index 0000000000000000000000000000000000000000..05bd126d9a88406d9db76f5fd208582055dce994 --- /dev/null +++ b/tests/identify-library-abi-cli.c @@ -0,0 +1,416 @@ +/* + * Copyright © 2021 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include <steam-runtime-tools/steam-runtime-tools.h> +#include <steam-runtime-tools/glib-backports-internal.h> +#include <steam-runtime-tools/utils-internal.h> + +#include <glib.h> + +#include <fcntl.h> +#include <string.h> +#include <sysexits.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "test-utils.h" + +static const char *argv0; +static gchar *empty_temp_dir; + +typedef struct +{ + gchar *srcdir; + gchar *builddir; +} Fixture; + +typedef struct +{ + int unused; +} Config; + +typedef struct +{ + const gchar *path; + const gchar *abi; +} LibInfo; + +typedef struct +{ + const gchar *argv[5]; + int exit_status; + const gchar *stdout_contains; + const gchar *stderr_contains; +} IdentifyLibraryAbi; + +static void +setup (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + /* For the tests we currently have they are not used yet */ + f->srcdir = g_strdup (g_getenv ("G_TEST_SRCDIR")); + f->builddir = g_strdup (g_getenv ("G_TEST_BUILDDIR")); + + if (f->srcdir == NULL) + f->srcdir = g_path_get_dirname (argv0); + + if (f->builddir == NULL) + f->builddir = g_path_get_dirname (argv0); +} + +static void +teardown (Fixture *f, + gconstpointer context) +{ + G_GNUC_UNUSED const Config *config = context; + + g_free (f->srcdir); + g_free (f->builddir); +} + +static void +_spawn_and_check_output (const IdentifyLibraryAbi *t) +{ + g_autofree gchar *child_stdout = NULL; + g_autofree gchar *child_stderr = NULL; + g_autoptr(GError) error = NULL; + gboolean ret; + int wait_status = -1; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) t->argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &child_stdout, + &child_stderr, + &wait_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_true (WIFEXITED (wait_status)); + g_assert_cmpint (WEXITSTATUS (wait_status), ==, t->exit_status); + g_assert_nonnull (child_stdout); + g_assert_true (g_utf8_validate (child_stdout, -1, NULL)); + g_assert_nonnull (child_stderr); + g_assert_true (g_utf8_validate (child_stderr, -1, NULL)); + if (t->stdout_contains != NULL) + g_assert_cmpstr (strstr (child_stdout, t->stdout_contains), !=, NULL); + if (t->stderr_contains != NULL) + g_assert_cmpstr (strstr (child_stderr, t->stderr_contains), !=, NULL); +} + +static void +test_arguments_validation (Fixture *f, + gconstpointer context) +{ + const IdentifyLibraryAbi identify_lib_abi[] = + { + { + .argv = + { + "steam-runtime-identify-library-abi", + "--ldconfig", + NULL, + }, + .exit_status = 0, + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "--ldconfig", + "--print0", + NULL, + }, + .exit_status = 0, + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "--directory", + empty_temp_dir, + NULL, + }, + .exit_status = 0, + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "--this-option-is-unsupported", + NULL, + }, + .exit_status = EX_USAGE, + .stderr_contains = "Unknown option", + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "this-argument-is-unsupported", + NULL, + }, + .exit_status = EX_USAGE, + .stderr_contains = "Either --ldconfig or --directory are required", + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "--ldconfig", + "--directory", + empty_temp_dir, + NULL, + }, + .exit_status = EX_USAGE, + .stderr_contains = "cannot be used at the same time", + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + NULL, + }, + .exit_status = EX_USAGE, + .stderr_contains = "Either --ldconfig or --directory are required", + }, + { + .argv = + { + "steam-runtime-identify-library-abi", + "--directory", + "/this_directory_does_not_exist", + NULL, + }, + .exit_status = 1, + .stderr_contains = "Unable to realpath", + }, + }; + + for (gsize i = 0; i < G_N_ELEMENTS (identify_lib_abi); i++) + _spawn_and_check_output (&identify_lib_abi[i]); +} + +/* + * Test `steam-runtime-identify-library-abi --help` and `--version`. + */ +static void +test_help_and_version (Fixture *f, + gconstpointer context) +{ + const IdentifyLibraryAbi identify_lib_abi[] = + { + { + .argv = + { + "env", + "LC_ALL=C", + "steam-runtime-identify-library-abi", + "--version", + NULL, + }, + .exit_status = 0, + .stdout_contains = VERSION, + }, + { + .argv = + { + "env", + "LC_ALL=C", + "steam-runtime-identify-library-abi", + "--help", + NULL, + }, + .exit_status = 0, + .stdout_contains = "OPTION", + }, + }; + + for (gsize i = 0; i < G_N_ELEMENTS (identify_lib_abi); i++) + _spawn_and_check_output (&identify_lib_abi[i]); +} + +static void +test_library_identification (Fixture *f, + gconstpointer context) +{ + gboolean ret; + int exit_status = -1; + GError *error = NULL; + gchar *child_stdout = NULL; + gchar *child_stderr = NULL; + gsize i; + const gchar *argv[] = + { + "steam-runtime-identify-library-abi", + "--ldconfig", + NULL, + NULL, + }; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &child_stdout, + &child_stderr, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (exit_status, ==, 0); + g_assert_nonnull (child_stdout); + g_assert_cmpstr (child_stdout, !=, ""); + g_assert_true (g_utf8_validate (child_stdout, -1, NULL)); + g_assert_nonnull (child_stderr); + + const LibInfo libc_info[] = + { + { + .path = "/usr/lib/x86_64-linux-gnu/libc.so.6", + .abi = "x86_64-linux-gnu", + }, + { + .path = "/lib/x86_64-linux-gnu/libc.so.6", + .abi = "x86_64-linux-gnu", + }, + { + .path = "/usr/lib/i386-linux-gnu/libc.so.6", + .abi = "i386-linux-gnu", + }, + { + .path = "/lib/i386-linux-gnu/libc.so.6", + .abi = "i386-linux-gnu", + }, + }; + + for (i = 0; i < G_N_ELEMENTS (libc_info); i++) + { + g_autofree gchar *expected_out_line = NULL; + gchar *out_line = strstr (child_stdout, libc_info[i].path); + if (out_line != NULL) + { + gchar *end_of_line = strstr (out_line, "\n"); + g_assert_nonnull (end_of_line); + end_of_line[0] = '\0'; + expected_out_line = g_strdup_printf ("%s=%s", libc_info[i].path, libc_info[i].abi); + g_assert_cmpstr (out_line, ==, expected_out_line); + end_of_line[0] = '\n'; + } + else + { + g_test_message ("\"%s\" seems to not be available in ldconfig output, " + "skipping this part of the test", libc_info[i].path); + } + } + + g_free (child_stdout); + g_free (child_stderr); + + argv[1] = "--directory"; + for (i = 0; i < G_N_ELEMENTS (libc_info); i++) + { + g_autofree gchar *libc_dirname = NULL; + g_autofree gchar *expected_out_line = NULL; + const gchar *out_line; + gchar *end_of_line; /* not owned */ + + if (!g_file_test (libc_info[i].path, G_FILE_TEST_EXISTS)) + { + g_test_message ("\"%s\" is not available in the filesystem, skipping this " + "part of the test", libc_info[i].path); + continue; + } + + libc_dirname = g_path_get_dirname (libc_info[i].path); + argv[2] = libc_dirname; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &child_stdout, + &child_stderr, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (exit_status, ==, 0); + g_assert_nonnull (child_stdout); + g_assert_cmpstr (child_stdout, !=, ""); + g_assert_true (g_utf8_validate (child_stdout, -1, NULL)); + g_assert_nonnull (child_stderr); + + out_line = strstr (child_stdout, libc_info[i].path); + g_assert_nonnull (out_line); + end_of_line = strstr (out_line, "\n"); + g_assert_nonnull (end_of_line); + end_of_line[0] = '\0'; + expected_out_line = g_strdup_printf ("%s=%s", libc_info[i].path, libc_info[i].abi); + g_assert_cmpstr (out_line, ==, expected_out_line); + end_of_line[0] = '\n'; + + g_free (child_stdout); + g_free (child_stderr); + } +} + +int +main (int argc, + char **argv) +{ + int status; + GError *error = NULL; + + argv0 = argv[0]; + + g_test_init (&argc, &argv, NULL); + /* Creates an empty temporary directory to test the --directory option */ + empty_temp_dir = g_dir_make_tmp ("empty-dir-XXXXXX", &error); + g_test_add ("/identify-library-abi-cli/arguments_validation", Fixture, NULL, + setup, test_arguments_validation, teardown); + g_test_add ("/identify-library-abi-cli/help-and-version", Fixture, NULL, + setup, test_help_and_version, teardown); + g_test_add ("/identify-library-abi-cli/library-identification", Fixture, NULL, + setup, test_library_identification, teardown); + + status = g_test_run (); + + _srt_rm_rf (empty_temp_dir); + g_free (empty_temp_dir); + + return status; +} diff --git a/tests/meson.build b/tests/meson.build index a372c1ca1353c03a1bef17ce7d375954073757cf..101e75e31900a75015316012b12cd4b6e6627e54 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -52,6 +52,7 @@ tests = [ if get_option('bin') tests += [ {'name': 'check-requirements-cli'}, + {'name': 'identify-library-abi-cli'}, {'name': 'system-info-cli', 'static': true, 'slow': true}, ] endif