diff --git a/bin/check-requirements.c b/bin/check-requirements.c new file mode 100644 index 0000000000000000000000000000000000000000..a28c252106187ad2995d8f63cbac8e960d5093bb --- /dev/null +++ b/bin/check-requirements.c @@ -0,0 +1,228 @@ +/* + * Copyright © 2020 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. + */ + +/* + * Perform some checks to ensure that the Steam client requirements are met. + * Output a human-readable message on stdout if the current system does not + * meet every requirement. + */ + +#include <steam-runtime-tools/steam-runtime-tools.h> + +#include <errno.h> +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include <glib.h> +#include <glib-object.h> + +#include <steam-runtime-tools/utils-internal.h> + +#define X86_FEATURES_REQUIRED (SRT_X86_FEATURE_X86_64 \ + | SRT_X86_FEATURE_CMPXCHG16B \ + | SRT_X86_FEATURE_SSE3) + +enum +{ + OPTION_HELP = 1, + OPTION_VERSION, +}; + +struct option long_options[] = +{ + { "version", no_argument, NULL, OPTION_VERSION }, + { "help", no_argument, NULL, OPTION_HELP }, + { NULL, 0, NULL, 0 } +}; + +static void usage (int code) __attribute__((__noreturn__)); + +/* + * Print usage information and exit with status @code. + */ +static void +usage (int code) +{ + FILE *fp; + + if (code == 0) + fp = stdout; + else + fp = stderr; + + fprintf (fp, "Usage: %s [OPTIONS]\n", + program_invocation_short_name); + exit (code); +} + +static FILE * +divert_stdout_to_stderr (GError **error) +{ + int original_stdout_fd; + FILE *original_stdout; + + /* Duplicate the original stdout so that we still have a way to write + * machine-readable output. */ + original_stdout_fd = dup (STDOUT_FILENO); + + if (original_stdout_fd < 0) + { + int saved_errno = errno; + + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "Unable to duplicate fd %d: %s", + STDOUT_FILENO, g_strerror (saved_errno)); + return NULL; + } + + /* If something like g_debug writes to stdout, make it come out of + * our original stderr. */ + if (dup2 (STDERR_FILENO, STDOUT_FILENO) != STDOUT_FILENO) + { + int saved_errno = errno; + + close (original_stdout_fd); + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "Unable to make fd %d a copy of fd %d: %s", + STDOUT_FILENO, STDERR_FILENO, g_strerror (saved_errno)); + return NULL; + } + + /* original_stdout takes ownership of original_stdout_fd on success */ + original_stdout = fdopen (original_stdout_fd, "w"); + + if (original_stdout == NULL) + { + int saved_errno = errno; + + close (original_stdout_fd); + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "Unable to create a stdio wrapper for fd %d: %s", + original_stdout_fd, g_strerror (saved_errno)); + return NULL; + } + + return original_stdout; +} + +static gboolean +check_x86_features (SrtX86FeatureFlags features) +{ + return ((features & X86_FEATURES_REQUIRED) == X86_FEATURES_REQUIRED); +} + +int +main (int argc, + char **argv) +{ + FILE *original_stdout = NULL; + GError *error = NULL; + SrtSystemInfo *info; + SrtX86FeatureFlags x86_features = SRT_X86_FEATURE_NONE; + const gchar *output = NULL; + gchar *version = NULL; + int opt; + int exit_code = EXIT_SUCCESS; + + while ((opt = getopt_long (argc, argv, "", long_options, NULL)) != -1) + { + switch (opt) + { + case OPTION_VERSION: + /* Output version number as YAML for machine-readability, + * inspired by `ostree --version` and `docker version` */ + printf ( + "%s:\n" + " Package: steam-runtime-tools\n" + " Version: %s\n", + argv[0], VERSION); + return EXIT_SUCCESS; + + case OPTION_HELP: + usage (0); + break; + + case '?': + default: + usage (EX_USAGE); + break; /* not reached */ + } + } + + if (optind != argc) + usage (EX_USAGE); + + /* stdout is reserved for machine-readable output, so avoid having + * things like g_debug() pollute it. */ + original_stdout = divert_stdout_to_stderr (&error); + + if (original_stdout == NULL) + { + g_warning ("%s", error->message); + g_clear_error (&error); + return EXIT_FAILURE; + } + + _srt_unblock_signals (); + + info = srt_system_info_new (NULL); + + /* This might be required for unit testing */ + srt_system_info_set_sysroot (info, g_getenv ("SRT_TEST_SYSROOT")); + + x86_features = srt_system_info_get_x86_features (info); + if (!check_x86_features (x86_features)) + { + output = "Sorry, this computer's CPU is too old to run Steam.\n" + "\nSteam requires at least an Intel Pentium 4 or AMD Opteron, with the following features:\n" + "\t- x86-64 (AMD64) instruction set (lm in /proc/cpuinfo flags)\n" + "\t- CMPXCHG16B instruction support (cx16 in /proc/cpuinfo flags)\n" + "\t- SSE3 instruction support (pni in /proc/cpuinfo flags)\n"; + exit_code = EX_OSERR; + goto out; + } + +out: + if (output != NULL) + { + if (fputs (output, original_stdout) < 0) + g_warning ("Unable to write output: %s", g_strerror (errno)); + + if (fputs ("\n", original_stdout) < 0) + g_warning ("Unable to write final newline: %s", g_strerror (errno)); + } + + if (fclose (original_stdout) != 0) + g_warning ("Unable to close stdout: %s", g_strerror (errno)); + + g_object_unref (info); + g_free (version); + + return exit_code; +} diff --git a/bin/check-requirements.md b/bin/check-requirements.md new file mode 100644 index 0000000000000000000000000000000000000000..2894e8356c6d28594a020a1a7886513dea086e79 --- /dev/null +++ b/bin/check-requirements.md @@ -0,0 +1,43 @@ +--- +title: steam-runtime-check-requirements +section: 1 +... + +# NAME + +steam-runtime-check-requirements - perform checks to ensure that the Steam client requirements are met + +# SYNOPSIS + +**steam-runtime-check-requirements** + +# DESCRIPTION + +# OPTIONS + +**--version** +: Instead of performing the checks, write in output the version number as + YAML. + +# OUTPUT + +If all the Steam client requirements are met the output will be empty. + +Otherwise if some of the checks fails, the output will have a human-readable +message explaining what failed. + + +# EXIT STATUS + +0 +: **steam-runtime-check-requirements** ran successfully and all the Steam + client requirements are met. + +71 +: At least one of the requirements is not met. In this case the exit status + will be 71 (EX_OSERR). + +Other Nonzero +: An error occurred. + +<!-- vim:set sw=4 sts=4 et: --> diff --git a/bin/meson.build b/bin/meson.build index c99713ded380b34a9537829a17e0d474f3e1fc41..1e7d45f37e0ef3e112eda0ef71600fd1c17976e2 100644 --- a/bin/meson.build +++ b/bin/meson.build @@ -32,23 +32,39 @@ executable( install_rpath : bin_rpath, ) +executable( + 'steam-runtime-check-requirements', + 'check-requirements.c', + dependencies : [glib, gobject, libsteamrt_dep], + install : true, + # Use the adjacent libsteam-runtime-tools and json-glib, ignoring + # LD_LIBRARY_PATH even if set + build_rpath : bin_rpath, + install_rpath : bin_rpath, +) + if get_option('man') - custom_target( - 'steam-runtime-system-info.1', - build_by_default : true, - command : [ - pandoc, - '-s', - '-o', '@OUTPUT@', - '-f', pandoc_markdown_nosmart, - '-t', 'man', - '@INPUT@', - ], - input : 'system-info.md', - output : 'steam-runtime-system-info.1', - install : true, - install_dir : join_paths(get_option('prefix'), get_option('mandir'), 'man1'), - ) + foreach bin_name : [ + 'check-requirements', + 'system-info', + ] + custom_target( + 'steam-runtime-' + bin_name + '.1', + build_by_default : true, + command : [ + pandoc, + '-s', + '-o', '@OUTPUT@', + '-f', pandoc_markdown_nosmart, + '-t', 'man', + '@INPUT@', + ], + input : bin_name + '.md', + output : 'steam-runtime-' + bin_name + '.1', + install : true, + install_dir : join_paths(get_option('prefix'), get_option('mandir'), 'man1'), + ) + endforeach endif # vim:set sw=2 sts=2 et: diff --git a/debian/control b/debian/control index 6f77178cb0d0e659ead22aa36ef0a896117b87c9..9295e13450333fd08ab0d3c75f5cf575608bccea 100644 --- a/debian/control +++ b/debian/control @@ -107,9 +107,10 @@ Description: supporting code used by the Steam client to discover system information. . This package contains symbolic links to libraries depended on by the - steam-runtime-system-info and libsteam-runtime-tools-0-helpers packages, - which make it possible to run those tools from an LD_LIBRARY_PATH-style - Steam Runtime even if the LD_LIBRARY_PATH is not correctly set. + steam-runtime-system-info, steam-runtime-check-requirements and + libsteam-runtime-tools-0-helpers packages, which make it possible to run + those tools from an LD_LIBRARY_PATH-style Steam Runtime even if the + LD_LIBRARY_PATH is not correctly set. Package: libsteam-runtime-tools-0-tests Architecture: any @@ -146,4 +147,5 @@ Description: Steam Runtime utility library - command-line tools . This package contains the command-line tool steam-runtime-system-info, which summarizes everything that the libsteam-runtime-tools library - can find out. + can find out, and steam-runtime-check-requirements, which performs checks + to ensure that the Steam client requirements are met. diff --git a/debian/rules b/debian/rules index e0bef9f6d0846e19b634b4f8bfc15517cf857e79..34f9df78b0c27d2bc0e23f684ad0bee763634ca9 100755 --- a/debian/rules +++ b/debian/rules @@ -91,6 +91,7 @@ override_dh_shlibdeps: -plibsteam-runtime-tools-0-relocatable-libs \ -- \ -prelocatable \ + -e$(DESTDIR)/usr/bin/steam-runtime-check-requirements \ -e$(DESTDIR)/usr/bin/steam-runtime-system-info \ -e$(DESTDIR)/usr/$(pkglibexecdir)/$(DEB_HOST_MULTIARCH)-check-locale \ $(NULL) @@ -108,6 +109,7 @@ override_dh_link: --link-target / \ --no-glibc \ --provider / \ + only-dependencies:path:$(DESTDIR)/usr/bin/steam-runtime-check-requirements \ only-dependencies:path:$(DESTDIR)/usr/bin/steam-runtime-system-info \ only-dependencies:path:$(DESTDIR)/usr/$(pkglibexecdir)/$(DEB_HOST_MULTIARCH)-check-locale \ $(NULL) diff --git a/debian/steam-runtime-tools-bin.install b/debian/steam-runtime-tools-bin.install index 435718331575e056b06729aad8975514fbe2fdb0..cdf8e8d577461f092dcf422e172dff41fa0f6bb0 100644 --- a/debian/steam-runtime-tools-bin.install +++ b/debian/steam-runtime-tools-bin.install @@ -1,2 +1,3 @@ +usr/bin/steam-runtime-check-requirements usr/bin/steam-runtime-system-info usr/share/man/man1 diff --git a/tests/check-requirements-cli.c b/tests/check-requirements-cli.c new file mode 100644 index 0000000000000000000000000000000000000000..684bfa87994532bb81d148344214dc02b2b8181f --- /dev/null +++ b/tests/check-requirements-cli.c @@ -0,0 +1,255 @@ +/* + * Copyright © 2019-2020 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-compat.h> + +#include <glib.h> +#include <glib/gstdio.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; + +typedef struct +{ + gchar *srcdir; + gchar *builddir; +} Fixture; + +typedef struct +{ + int unused; +} Config; + +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 +test_arguments_validation (Fixture *f, + gconstpointer context) +{ + gboolean ret; + int exit_status = -1; + GError *error = NULL; + gchar *output = NULL; + gchar *diagnostics = NULL; + const gchar *argv[] = { "steam-runtime-check-requirements", NULL, NULL }; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, + &diagnostics, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + /* Do not assume the CI workers hardware. So we expect either a success or + * an EX_OSERR status */ + if (exit_status != 0 && WIFEXITED (exit_status)) + g_assert_cmpint (WEXITSTATUS (exit_status), ==, EX_OSERR); + else + g_assert_cmpint (exit_status, ==, 0); + g_assert_nonnull (output); + g_assert_true (g_utf8_validate (output, -1, NULL)); + + if (exit_status != 0) + g_assert_cmpstr (output, !=, ""); + + g_free (output); + g_free (diagnostics); + argv[1] = "--this-option-is-unsupported"; + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, + &diagnostics, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_true (WIFEXITED (exit_status)); + g_assert_cmpint (WEXITSTATUS (exit_status), ==, EX_USAGE); + g_assert_nonnull (output); + g_assert_cmpstr (output, ==, ""); + g_assert_true (g_utf8_validate (output, -1, NULL)); + g_assert_nonnull (diagnostics); + g_assert_cmpstr (diagnostics, !=, ""); + g_assert_true (g_utf8_validate (diagnostics, -1, NULL)); + + g_free (output); + g_free (diagnostics); + argv[1] = "this-argument-is-unsupported"; + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, + &diagnostics, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_true (WIFEXITED (exit_status)); + g_assert_cmpint (WEXITSTATUS (exit_status), ==, EX_USAGE); + g_assert_nonnull (output); + g_assert_cmpstr (output, ==, ""); + g_assert_true (g_utf8_validate (output, -1, NULL)); + g_assert_nonnull (diagnostics); + g_assert_cmpstr (diagnostics, !=, ""); + g_assert_true (g_utf8_validate (diagnostics, -1, NULL)); + + g_free (output); + g_free (diagnostics); + g_clear_error (&error); +} + +/* + * Test `steam-runtime-check-requirements --help` and `--version`. + */ +static void +test_help_and_version (Fixture *f, + gconstpointer context) +{ + gboolean ret; + int exit_status = -1; + GError *error = NULL; + gchar *output = NULL; + gchar *diagnostics = NULL; + const gchar *argv[] = { + "env", + "LC_ALL=C", + "steam-runtime-check-requirements", + "--version", + NULL + }; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, + &diagnostics, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (exit_status, ==, 0); + g_assert_nonnull (output); + g_assert_cmpstr (output, !=, ""); + g_assert_true (g_utf8_validate (output, -1, NULL)); + g_assert_nonnull (diagnostics); + + g_assert_nonnull (strstr (output, VERSION)); + + g_free (output); + g_free (diagnostics); + g_clear_error (&error); + + argv[3] = "--help"; + + ret = g_spawn_sync (NULL, /* working directory */ + (gchar **) argv, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH, + NULL, /* child setup */ + NULL, /* user data */ + &output, + &diagnostics, + &exit_status, + &error); + g_assert_no_error (error); + g_assert_true (ret); + g_assert_cmpint (exit_status, ==, 0); + g_assert_nonnull (output); + g_assert_cmpstr (output, !=, ""); + g_assert_true (g_utf8_validate (output, -1, NULL)); + g_assert_nonnull (diagnostics); + + g_assert_nonnull (strstr (output, "OPTIONS")); + + g_free (output); + g_free (diagnostics); + g_clear_error (&error); +} + +int +main (int argc, + char **argv) +{ + argv0 = argv[0]; + + g_test_init (&argc, &argv, NULL); + g_test_add ("/check-requirements-cli/arguments_validation", Fixture, NULL, + setup, test_arguments_validation, teardown); + g_test_add ("/check-requirements-cli/help-and-version", Fixture, NULL, + setup, test_help_and_version, teardown); + + return g_test_run (); +} diff --git a/tests/meson.build b/tests/meson.build index 1e787097d443ed5acee0cb23a82ae35f050bb866..7e2e59a8d09f19a78c2b6fc39e60773d4b923232 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -29,6 +29,7 @@ test_env.prepend('PATH', join_paths(meson.current_build_dir(), '..', 'bin')) tests = [ 'architecture', + 'check-requirements-cli', 'desktop-entry', 'graphics', 'library',