Skip to content
Snippets Groups Projects
wrap.c 88 KiB
Newer Older
          for (j = 0; j < egl_icd_details->len; j++)
            {
              IcdDetails *details = g_ptr_array_index (egl_icd_details, j);
              SrtEglIcd *icd = SRT_EGL_ICD (details->icd);

              if (!srt_egl_icd_check_error (icd, NULL))
                continue;

              details->resolved_library = srt_egl_icd_resolve_library_path (icd);
              g_assert (details->resolved_library != NULL);

              if (!bind_icd (i,
                             j,
                             tool_path,
                             mount_runtime_on_scratch,
                             scratch,
                             libdir_on_host,
                             libdir_in_container,
                             "glvnd",
                             details,
                             error))
                return FALSE;
            }

          g_debug ("Collecting %s Vulkan drivers from host system...",
                   multiarch_tuples[i]);

          temp_bwrap = flatpak_bwrap_new (NULL);
          g_warn_if_fail (mount_runtime_on_scratch->fds == NULL
                          || mount_runtime_on_scratch->fds->len == 0);
          flatpak_bwrap_append_bwrap (temp_bwrap, mount_runtime_on_scratch);
          flatpak_bwrap_add_args (temp_bwrap,
                                  tool_path,
                                  "--container", scratch,
                                  "--link-target", "/run/host",
                                  "--dest", libdir_on_host,
                                  "--provider", "/",
                                  "if-exists:if-same-abi:soname:libvulkan.so.1",
                                  NULL);
          flatpak_bwrap_finish (temp_bwrap);

          if (!pv_bwrap_run_sync (temp_bwrap, NULL, error))
            return FALSE;

          g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);

          for (j = 0; j < vulkan_icd_details->len; j++)
            {
              IcdDetails *details = g_ptr_array_index (vulkan_icd_details, j);
              SrtVulkanIcd *icd = SRT_VULKAN_ICD (details->icd);

              if (!srt_vulkan_icd_check_error (icd, NULL))
                continue;

              details->resolved_library = srt_vulkan_icd_resolve_library_path (icd);
              g_assert (details->resolved_library != NULL);

              if (!bind_icd (i,
                             j,
                             tool_path,
                             mount_runtime_on_scratch,
                             scratch,
                             libdir_on_host,
                             libdir_in_container,
                             "vulkan",
                             details,
                             error))
                return FALSE;
            }

          libc = g_build_filename (libdir_on_host, "libc.so.6", NULL);

          /* If we are going to use the host system's libc6 (likely)
           * then we have to use its ld.so too. */
          if (g_file_test (libc, G_FILE_TEST_IS_SYMLINK))
            {
              g_autofree gchar *ld_so_in_host = NULL;
              g_debug ("Making host ld.so visible in container");

              ld_so_in_host = flatpak_canonicalize_filename (ld_so);
              g_debug ("Host path: %s -> %s", ld_so, ld_so_in_host);

              g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);

              temp_bwrap = flatpak_bwrap_new (NULL);
              flatpak_bwrap_add_args (temp_bwrap,
              if (!pv_bwrap_bind_usr (temp_bwrap, runtime, "/", error))

              flatpak_bwrap_add_args (temp_bwrap,
                                      "readlink", "-f", ld_so,
                                      NULL);
              flatpak_bwrap_finish (temp_bwrap);

              g_debug ("Container path: %s -> %s", ld_so, ld_so_in_runtime);
              flatpak_bwrap_add_args (bwrap,
                                      "--ro-bind", ld_so_in_host,
                                      ld_so_in_runtime,
              /* Collect miscellaneous libraries that libc might dlopen.
               * At the moment this is just libidn2. */
              temp_bwrap = flatpak_bwrap_new (NULL);
              g_warn_if_fail (mount_runtime_on_scratch->fds == NULL
                              || mount_runtime_on_scratch->fds->len == 0);
              flatpak_bwrap_append_bwrap (temp_bwrap, mount_runtime_on_scratch);
              flatpak_bwrap_add_args (temp_bwrap,
                                      tool_path,
                                      "--container", scratch,
                                      "--link-target", "/run/host",
                                      "--dest", libdir_on_host,
                                      "--provider", "/",
                                      "if-exists:libidn2.so.0",
                                      NULL);
              flatpak_bwrap_finish (temp_bwrap);

              if (!pv_bwrap_run_sync (temp_bwrap, NULL, error))

              g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);

              any_libc_from_host = TRUE;
            }
          else
            {
              all_libc_from_host = FALSE;
          g_assert (i < G_N_ELEMENTS (libquals));
          dirs[0] = g_build_filename ("/lib", multiarch_tuples[i], NULL);
          dirs[1] = g_build_filename ("/usr", "lib", multiarch_tuples[i], NULL);
          dirs[2] = g_strdup ("/lib");
          dirs[3] = g_strdup ("/usr/lib");
          dirs[4] = g_build_filename ("/", libqual, NULL);
          dirs[5] = g_build_filename ("/usr", libqual, NULL);
              if (!try_bind_dri (bwrap, mount_runtime_on_scratch,
                                 overrides, scratch, tool_path, dirs[j],
                                 libdir_on_host, error))
            }
        }
      else
        {
          g_debug ("Cannot determine ld.so for %s", multiarch_tuples[i]);
        }
    }

  if (!any_architecture_works)
    {
      GString *archs = g_string_new ("");

      g_assert (multiarch_tuples[G_N_ELEMENTS (multiarch_tuples) - 1] == NULL);

      for (i = 0; i < G_N_ELEMENTS (multiarch_tuples) - 1; i++)
        {
          if (archs->len > 0)
            g_string_append (archs, ", ");

          g_string_append (archs, multiarch_tuples[i]);
        }

      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "None of the supported CPU architectures are common to "
                   "the host system and the container (tried: %s)",
                   archs->str);
      g_string_free (archs, TRUE);
  if (any_libc_from_host && !all_libc_from_host)
    {
      /*
       * This shouldn't happen. It would mean that there exist at least
       * two architectures (let's say aaa and bbb) for which we have:
       * host libc6:aaa < container libc6 < host libc6:bbb
       * (we know that the container's libc6:aaa and libc6:bbb are
       * constrained to be the same version because that's how multiarch
       * works).
       *
       * If the host system locales work OK with both the aaa and bbb
       * versions, let's assume they will also work with the intermediate
       * version from the container...
       */
      g_warning ("Using glibc from host system for some but not all "
                 "architectures! Arbitrarily using host locales.");
    }

  if (any_libc_from_host)
    {
      g_debug ("Making host locale data visible in container");

      if (g_file_test ("/usr/lib/locale", G_FILE_TEST_EXISTS))
        flatpak_bwrap_add_args (bwrap,
                                "--ro-bind", "/usr/lib/locale",
                                "/usr/lib/locale",
                                NULL);

      if (g_file_test ("/usr/share/i18n", G_FILE_TEST_EXISTS))
        flatpak_bwrap_add_args (bwrap,
                                "--ro-bind", "/usr/share/i18n",
                                "/usr/share/i18n",
                                NULL);

      localedef = g_find_program_in_path ("localedef");

      if (localedef == NULL)
        {
          g_warning ("Cannot find localedef in PATH");
        }
      else
        {
          g_autofree gchar *target = g_build_filename ("/run/host",
                                                       localedef, NULL);

          flatpak_bwrap_add_args (bwrap,
                                  "--symlink", target,
                                      "/overrides/bin/localedef",
                                  NULL);
        }
    }
  else
    {
      g_debug ("Using included locale data from container");
    }

  g_debug ("Setting up EGL ICD JSON...");

  dir_on_host = g_build_filename (overrides,
                                  "share", "glvnd", "egl_vendor.d", NULL);

  if (g_mkdir_with_parents (dir_on_host, 0700) != 0)
    {
      glnx_throw_errno_prefix (error, "Unable to create %s", dir_on_host);
      return FALSE;
    }

  for (j = 0; j < egl_icd_details->len; j++)
    {
      IcdDetails *details = g_ptr_array_index (egl_icd_details, j);
      SrtEglIcd *icd = SRT_EGL_ICD (details->icd);
      gboolean need_host_json = FALSE;

      if (!srt_egl_icd_check_error (icd, NULL))
        continue;

      for (i = 0; i < G_N_ELEMENTS (multiarch_tuples) - 1; i++)
        {
          g_assert (i < G_N_ELEMENTS (details->kinds));
          g_assert (i < G_N_ELEMENTS (details->paths_in_container));

          if (details->kinds[i] == ICD_KIND_ABSOLUTE)
            {
              g_autoptr(SrtEglIcd) replacement = NULL;
              g_autofree gchar *json_on_host = NULL;
              g_autofree gchar *json_in_container = NULL;
              g_autofree gchar *json_base = NULL;

              g_assert (details->paths_in_container[i] != NULL);

              json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json",
                                           j, multiarch_tuples[i]);
              json_on_host = g_build_filename (dir_on_host, json_base, NULL);
              json_in_container = g_build_filename ("/overrides", "share",
                                                    "glvnd", "egl_vendor.d",
                                                    json_base, NULL);

              replacement = srt_egl_icd_new_replace_library_path (icd,
                                                                  details->paths_in_container[i]);

              if (!srt_egl_icd_write_to_file (replacement, json_on_host,
                                                 error))
                return FALSE;

              search_path_append (egl_path, json_in_container);
            }
          else if (details->kinds[i] == ICD_KIND_SONAME)
            {
              need_host_json = TRUE;
            }
        }

      if (need_host_json)
        {
          g_autofree gchar *json_in_container = NULL;
          g_autofree gchar *json_base = NULL;
          const char *json_on_host = srt_egl_icd_get_json_path (icd);

          json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j);
          json_in_container = g_build_filename ("/overrides", "share",
                                                "glvnd", "egl_vendor.d",
                                                json_base, NULL);

          flatpak_bwrap_add_args (bwrap,
                                  "--ro-bind", json_on_host, json_in_container,
                                  NULL);
          search_path_append (egl_path, json_in_container);
        }
    }

  g_debug ("Setting up Vulkan ICD JSON...");

  dir_on_host = g_build_filename (overrides,
                                  "share", "vulkan", "icd.d", NULL);

  if (g_mkdir_with_parents (dir_on_host, 0700) != 0)
    {
      glnx_throw_errno_prefix (error, "Unable to create %s", dir_on_host);
      return FALSE;
    }

  for (j = 0; j < vulkan_icd_details->len; j++)
    {
      IcdDetails *details = g_ptr_array_index (vulkan_icd_details, j);
      SrtVulkanIcd *icd = SRT_VULKAN_ICD (details->icd);
      gboolean need_host_json = FALSE;

      if (!srt_vulkan_icd_check_error (icd, NULL))
        continue;

      for (i = 0; i < G_N_ELEMENTS (multiarch_tuples) - 1; i++)
        {
          g_assert (i < G_N_ELEMENTS (details->kinds));
          g_assert (i < G_N_ELEMENTS (details->paths_in_container));

          if (details->kinds[i] == ICD_KIND_ABSOLUTE)
            {
              g_autoptr(SrtVulkanIcd) replacement = NULL;
              g_autofree gchar *json_on_host = NULL;
              g_autofree gchar *json_in_container = NULL;
              g_autofree gchar *json_base = NULL;

              g_assert (details->paths_in_container[i] != NULL);

              json_base = g_strdup_printf ("%" G_GSIZE_FORMAT "-%s.json",
                                           j, multiarch_tuples[i]);
              json_on_host = g_build_filename (dir_on_host, json_base, NULL);
              json_in_container = g_build_filename ("/overrides", "share",
                                                    "vulkan", "icd.d",
                                                    json_base, NULL);

              replacement = srt_vulkan_icd_new_replace_library_path (icd,
                                                                     details->paths_in_container[i]);

              if (!srt_vulkan_icd_write_to_file (replacement, json_on_host,
                                                 error))
                return FALSE;

              search_path_append (vulkan_path, json_in_container);
            }
          else if (details->kinds[i] == ICD_KIND_SONAME)
            {
              need_host_json = TRUE;
            }
        }

      if (need_host_json)
        {
          g_autofree gchar *json_in_container = NULL;
          g_autofree gchar *json_base = NULL;
          const char *json_on_host = srt_vulkan_icd_get_json_path (icd);

          json_base = g_strdup_printf ("%" G_GSIZE_FORMAT ".json", j);
          json_in_container = g_build_filename ("/overrides", "share",
                                                "vulkan", "icd.d",
                                                json_base, NULL);

          flatpak_bwrap_add_args (bwrap,
                                  "--ro-bind", json_on_host, json_in_container,
                                  NULL);
          search_path_append (vulkan_path, json_in_container);
        }
    }

  if (!pv_bwrap_bind_usr (bwrap, "/", "/run/host", error))
  ensure_locales (any_libc_from_host, tools_dir, bwrap, overrides);

  if (dri_path->len != 0)
      flatpak_bwrap_add_args (bwrap,
                              "--setenv", "LIBGL_DRIVERS_PATH", dri_path->str,
                              NULL);
  else
      flatpak_bwrap_add_args (bwrap,
                              "--unsetenv", "LIBGL_DRIVERS_PATH",
                              NULL);

  if (egl_path->len != 0)
      flatpak_bwrap_add_args (bwrap,
                              "--setenv", "__EGL_VENDOR_LIBRARY_FILENAMES",
                              egl_path->str, NULL);
  else
      flatpak_bwrap_add_args (bwrap,
                              "--unsetenv", "__EGL_VENDOR_LIBRARY_FILENAMES",
                              NULL);

  flatpak_bwrap_add_args (bwrap,
                          "--unsetenv", "__EGL_VENDOR_LIBRARY_DIRS",
                          NULL);

  if (vulkan_path->len != 0)
      flatpak_bwrap_add_args (bwrap,
                              "--setenv", "VK_ICD_FILENAMES",
                              vulkan_path->str, NULL);
  else
      flatpak_bwrap_add_args (bwrap,
                              "--unsetenv", "VK_ICD_FILENAMES",
                              NULL);

  /* These can add data fds to @bwrap, so they must come last - after
   * other functions stop using @bwrap as a basis for their own bwrap
   * invocations with flatpak_bwrap_append_bwrap().
   * Otherwise, when flatpak_bwrap_append_bwrap() calls
   * flatpak_bwrap_steal_fds(), it will make the original FlatpakBwrap
   * unusable. */

  flatpak_run_add_wayland_args (bwrap);
  flatpak_run_add_x11_args (bwrap, TRUE);
  flatpak_run_add_pulseaudio_args (bwrap);
  flatpak_run_add_session_dbus_args (bwrap);
  flatpak_run_add_system_dbus_args (bwrap);
  pv_bwrap_copy_tree (bwrap, overrides, "/overrides");
  /* /etc/localtime and /etc/resolv.conf can not exist (or be symlinks to
   * non-existing targets), in which case we don't want to attempt to create
   * bogus symlinks or bind mounts, as that will cause flatpak run to fail.
   */
  if (g_file_test ("/etc/localtime", G_FILE_TEST_EXISTS))
    {
      g_autofree char *target = NULL;
      g_autofree char *tz = flatpak_get_timezone ();
      g_autofree char *timezone_content = g_strdup_printf ("%s\n", tz);
      target = glnx_readlinkat_malloc (-1, "/etc/localtime", NULL, NULL);
      if (target != NULL)
        {
          g_autoptr(GFile) base_file = NULL;
          g_autoptr(GFile) target_file = NULL;
          g_autofree char *target_canonical = NULL;

          base_file = g_file_new_for_path ("/etc");
          target_file = g_file_resolve_relative_path (base_file, target);
          target_canonical = g_file_get_path (target_file);

          is_reachable = g_str_has_prefix (target_canonical, "/usr/");
        }

      if (is_reachable)
        {
          flatpak_bwrap_add_args (bwrap,
                                  "--symlink", target, "/etc/localtime",
                                  NULL);
        }
      else
        {
          flatpak_bwrap_add_args (bwrap,
                                  "--ro-bind", "/etc/localtime", "/etc/localtime",
                                  NULL);
        }

      flatpak_bwrap_add_args_data (bwrap, "timezone",
                                   timezone_content, -1, "/etc/timezone",
                                   NULL);
    }
/* Order matters here: root, steam and steambeta are or might be symlinks
 * to the root of the Steam installation, so we want to bind-mount their
 * targets before we deal with the rest. */
static const char * const steam_api_subdirs[] =
{
  "root", "steam", "steambeta", "bin", "bin32", "bin64", "sdk32", "sdk64",
};

static gboolean
use_fake_home (FlatpakBwrap *bwrap,
               const gchar *fake_home,
               GError **error)
{
  const gchar *real_home = g_get_home_dir ();
  g_autofree gchar *cache = g_build_filename (fake_home, ".cache", NULL);
  g_autofree gchar *cache2 = g_build_filename (fake_home, "cache", NULL);
  g_autofree gchar *tmp = g_build_filename (cache, "tmp", NULL);
  g_autofree gchar *config = g_build_filename (fake_home, ".config", NULL);
  g_autofree gchar *config2 = g_build_filename (fake_home, "config", NULL);
  g_autofree gchar *local = g_build_filename (fake_home, ".local", NULL);
  g_autofree gchar *data = g_build_filename (local, "share", NULL);
  g_autofree gchar *data2 = g_build_filename (fake_home, "data", NULL);
  g_autofree gchar *steam_pid = NULL;
  g_autofree gchar *steam_pipe = NULL;
  gsize i;

  g_return_val_if_fail (bwrap != NULL, FALSE);
  g_return_val_if_fail (fake_home != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  g_mkdir_with_parents (fake_home, 0700);
  g_mkdir_with_parents (cache, 0700);
  g_mkdir_with_parents (tmp, 0700);
  g_mkdir_with_parents (config, 0700);
  g_mkdir_with_parents (local, 0700);
  g_mkdir_with_parents (data, 0700);

  if (!g_file_test (cache2, G_FILE_TEST_EXISTS))
    {
      g_unlink (cache2);

      if (symlink (".cache", cache2) != 0)
        return glnx_throw_errno_prefix (error,
                                        "Unable to create symlink %s -> .cache",
                                        cache2);
    }

  if (!g_file_test (config2, G_FILE_TEST_EXISTS))
    {
      g_unlink (config2);

      if (symlink (".config", config2) != 0)
        return glnx_throw_errno_prefix (error,
                                        "Unable to create symlink %s -> .config",
                                        config2);
    }

  if (!g_file_test (data2, G_FILE_TEST_EXISTS))
    {
      g_unlink (data2);

      if (symlink (".local/share", data2) != 0)
        return glnx_throw_errno_prefix (error,
                                        "Unable to create symlink %s -> .local/share",
                                        data2);
    }

  flatpak_bwrap_add_args (bwrap,
                          "--bind", fake_home, real_home,
                          "--bind", fake_home, fake_home,
                          "--bind", tmp, "/var/tmp",
                          "--setenv", "XDG_CACHE_HOME", cache,
                          "--setenv", "XDG_CONFIG_HOME", config,
                          "--setenv", "XDG_DATA_HOME", data,
                          NULL);

  mounted = g_hash_table_new_full (g_str_hash, g_str_equal,
                                   g_free, NULL);

  /*
   * These might be API entry points, according to Steam/steam.sh.
   * They're usually symlinks into the Steam root, except for in
   * older steam Debian packages that had Debian bug #916303.
   *
   * TODO: We probably want to hide part or all of root, steam,
   * steambeta?
   */
  for (i = 0; i < G_N_ELEMENTS (steam_api_subdirs); i++)
    {
      g_autofree gchar *dir = g_build_filename (real_home, ".steam",
                                                steam_api_subdirs[i], NULL);
      g_autofree gchar *mount_point = g_build_filename (fake_home, ".steam",
                                                        steam_api_subdirs[i],
                                                        NULL);
      g_autofree gchar *target = NULL;

      target = glnx_readlinkat_malloc (-1, dir, NULL, NULL);
          /* We used to bind-mount these directories, so transition them
           * to symbolic links if we can. */
          if (rmdir (mount_point) != 0 && errno != ENOENT && errno != ENOTDIR)
            g_debug ("rmdir %s: %s", mount_point, g_strerror (errno));

          /* Remove any symlinks that might have already been there. */
          if (unlink (mount_point) != 0 && errno != ENOENT)
            g_debug ("unlink %s: %s", mount_point, g_strerror (errno));

          flatpak_bwrap_add_args (bwrap, "--symlink", target, dir, NULL);

          if (strcmp (steam_api_subdirs[i], "root") == 0
              || strcmp (steam_api_subdirs[i], "steam") == 0
              || strcmp (steam_api_subdirs[i], "steambeta") == 0)
            {
              flatpak_bwrap_add_args (bwrap,
                                      "--ro-bind", target, target,
                                      NULL);
              g_hash_table_add (mounted, g_steal_pointer (&target));
            }
        }
      else if (g_file_test (dir, G_FILE_TEST_EXISTS) &&
               !g_hash_table_contains (mounted, dir))
        {
          flatpak_bwrap_add_args (bwrap, "--ro-bind", dir, dir, NULL);
          g_hash_table_add (mounted, g_steal_pointer (&dir));
        }
    }

  /* steamclient.so relies on this for communication with Steam */
  steam_pid = g_build_filename (real_home, ".steam", "steam.pid", NULL);

  if (g_file_test (steam_pid, G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", steam_pid, steam_pid,
                            NULL);

  /* Make sure Steam IPC is available.
   * TODO: do we need this? do we need more? */
  steam_pipe = g_build_filename (real_home, ".steam", "steam.pipe", NULL);

  if (g_file_test (steam_pipe, G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--bind", steam_pipe, steam_pipe,
                            NULL);

  return TRUE;
}

typedef enum
{
  TRISTATE_NO = 0,
  TRISTATE_YES,
  TRISTATE_MAYBE
} Tristate;

static char **opt_env_if_host = NULL;
static char *opt_fake_home = NULL;
static char *opt_freedesktop_app_id = NULL;
static char *opt_steam_app_id = NULL;
static char *opt_home = NULL;
static gboolean opt_host_fallback = FALSE;
static PvShell opt_shell = PV_SHELL_NONE;
static GPtrArray *opt_ld_preload = NULL;
static char *opt_runtime_base = NULL;
static char *opt_runtime = NULL;
static Tristate opt_share_home = TRISTATE_MAYBE;
static gboolean opt_verbose = FALSE;
static gboolean opt_version = FALSE;
static PvTerminal opt_terminal = PV_TERMINAL_AUTO;

static gboolean
opt_host_ld_preload_cb (const gchar *option_name,
                        const gchar *value,
                        gpointer data,
                        GError **error)
{
  gchar *preload = g_strdup_printf ("host:%s", value);

  if (opt_ld_preload == NULL)
    opt_ld_preload = g_ptr_array_new_with_free_func (g_free);

  g_ptr_array_add (opt_ld_preload, g_steal_pointer (&preload));

  return TRUE;
}

static gboolean
opt_shell_cb (const gchar *option_name,
              const gchar *value,
              gpointer data,
              GError **error)
{
  if (g_strcmp0 (option_name, "--shell-after") == 0)
    value = "after";
  else if (g_strcmp0 (option_name, "--shell-fail") == 0)
    value = "fail";
  else if (g_strcmp0 (option_name, "--shell-instead") == 0)
    value = "instead";

  if (value == NULL || *value == '\0')
    {
      opt_shell = PV_SHELL_NONE;
      return TRUE;
    }

  switch (value[0])
    {
      case 'a':
        if (g_strcmp0 (value, "after") == 0)
          {
            opt_shell = PV_SHELL_AFTER;
            return TRUE;
          }
        break;

      case 'f':
        if (g_strcmp0 (value, "fail") == 0)
          {
            opt_shell = PV_SHELL_FAIL;
            return TRUE;
          }
        break;

      case 'i':
        if (g_strcmp0 (value, "instead") == 0)
          {
            opt_shell = PV_SHELL_INSTEAD;
            return TRUE;
          }
        break;

      case 'n':
        if (g_strcmp0 (value, "none") == 0 || g_strcmp0 (value, "no") == 0)
          {
            opt_shell = PV_SHELL_NONE;
            return TRUE;
          }
        break;

      default:
        /* fall through to error */
        break;
    }

  g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
               "Unknown choice \"%s\" for %s", value, option_name);
  return FALSE;
}

static gboolean
opt_terminal_cb (const gchar *option_name,
                 const gchar *value,
                 gpointer data,
                 GError **error)
{
  if (g_strcmp0 (option_name, "--tty") == 0)
    value = "tty";
  else if (g_strcmp0 (option_name, "--xterm") == 0)
    value = "xterm";

  if (value == NULL || *value == '\0')
    {
      opt_terminal = PV_TERMINAL_AUTO;
      return TRUE;
    }

  switch (value[0])
    {
      case 'a':
        if (g_strcmp0 (value, "auto") == 0)
          {
            opt_terminal = PV_TERMINAL_AUTO;
            return TRUE;
          }
        break;

      case 'n':
        if (g_strcmp0 (value, "none") == 0 || g_strcmp0 (value, "no") == 0)
          {
            opt_terminal = PV_TERMINAL_NONE;
            return TRUE;
          }
        break;

      case 't':
        if (g_strcmp0 (value, "tty") == 0)
          {
            opt_terminal = PV_TERMINAL_TTY;
            return TRUE;
          }
        break;

      case 'x':
        if (g_strcmp0 (value, "xterm") == 0)
          {
            opt_terminal = PV_TERMINAL_XTERM;
            return TRUE;
          }
        break;

      default:
        /* fall through to error */
        break;
    }

  g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
               "Unknown choice \"%s\" for %s", value, option_name);
  return FALSE;
}

static gboolean
opt_share_home_cb (const gchar *option_name,
                   const gchar *value,
                   gpointer data,
                   GError **error)
{
  if (g_strcmp0 (option_name, "--share-home") == 0)
    opt_share_home = TRISTATE_YES;
  else if (g_strcmp0 (option_name, "--unshare-home") == 0)
    opt_share_home = TRISTATE_NO;
  else
    g_return_val_if_reached (FALSE);

  return TRUE;
}

static GOptionEntry options[] =
{
  { "env-if-host", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &opt_env_if_host,
    "Set VAR=VAL if COMMAND is run with /usr from the host system, "
    "but not if it is run with /usr from RUNTIME.", "VAR=VAL" },
  { "freedesktop-app-id", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_freedesktop_app_id,
    "Make --unshare-home use ~/.var/app/ID as home directory, where ID "
    "is com.example.MyApp or similar. This interoperates with Flatpak. "
    "[Default: $PRESSURE_VESSEL_FDO_APP_ID if set]",
  { "steam-app-id", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_steam_app_id,
    "Make --unshare-home use ~/.var/app/com.steampowered.AppN "
    "as home directory. [Default: $SteamAppId]", "N" },
  { "home", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_home,
    "Use HOME as home directory. Implies --unshare-home. "
    "[Default: $PRESSURE_VESSEL_HOME if set]", "HOME" },
  { "host-fallback", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_host_fallback,
    "Run COMMAND on the host system if we cannot run it in a container.", NULL },
  { "host-ld-preload", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, &opt_host_ld_preload_cb,
    "Add MODULE from the host system to LD_PRELOAD when executing COMMAND.",
    "MODULE" },
  { "runtime", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime,
    "Mount the given sysroot or merged /usr in the container, and augment "
    "it with the host system's graphics stack. The empty string "
    "means don't use a runtime. [Default: $PRESSURE_VESSEL_RUNTIME or '']",
  { "runtime-base", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_base,
    "If a --runtime is a relative path, look for it relative to BASE. "
    "[Default: $PRESSURE_VESSEL_RUNTIME_BASE or '.']",
    "BASE" },
  { "share-home", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
    "Use the real home directory. "
    "[Default unless $PRESSURE_VESSEL_HOME is set or "
    "$PRESSURE_VESSEL_SHARE_HOME is 0]",
    NULL },
  { "unshare-home", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
    "Use an app-specific home directory chosen according to --home, "
    "--freedesktop-app-id, --steam-app-id or $SteamAppId. "
    "[Default if $PRESSURE_VESSEL_HOME is set or "
    "$PRESSURE_VESSEL_SHARE_HOME is 0]",
    NULL },
  { "shell", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_shell_cb,
    "--shell=after is equivalent to --shell-after, and so on. "
    "[Default: $PRESSURE_VESSEL_SHELL or 'none']",
    "{none|after|fail|instead}" },
  { "shell-after", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
    "Run an interactive shell after COMMAND. Executing \"$@\" in that "
    "shell will re-run COMMAND [ARGS].",
    NULL },
  { "shell-fail", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
    "Run an interactive shell after COMMAND, but only if it fails.",
    NULL },
  { "shell-instead", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
    "Run an interactive shell instead of COMMAND. Executing \"$@\" in that "
    "shell will run COMMAND [ARGS].",
    NULL },
  { "terminal", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
    "none: disable features that would use a terminal; "
    "auto: equivalent to xterm if a --shell option is used, or none; "
    "xterm: put game output (and --shell if used) in an xterm; "
    "tty: put game output (and --shell if used) on Steam's "
    "controlling tty "
    "[Default: $PRESSURE_VESSEL_TERMINAL or 'auto']",
    "{none|auto|xterm|tty}" },
  { "tty", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
    "Equivalent to --terminal=tty", NULL },
  { "xterm", '\0',
    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
    "Equivalent to --terminal=xterm", NULL },
  { "verbose", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
    "Be more verbose.", NULL },
  { "version", '\0',
    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version,
    "Print version number and exit.", NULL },
static gboolean
boolean_environment (const gchar *name,
                     gboolean def)
{
  const gchar *value = g_getenv (name);

  if (g_strcmp0 (value, "1") == 0)
    return TRUE;

  if (g_strcmp0 (value, "") == 0 || g_strcmp0 (value, "0") == 0)
    return FALSE;

  if (value != NULL)
    g_warning ("Unrecognised value \"%s\" for $%s", value, name);

  return def;
}

static Tristate
tristate_environment (const gchar *name)
{
  const gchar *value = g_getenv (name);

  if (g_strcmp0 (value, "1") == 0)
    return TRISTATE_YES;

  if (g_strcmp0 (value, "0") == 0)
    return TRISTATE_NO;

  if (value != NULL && value[0] != '\0')
    g_warning ("Unrecognised value \"%s\" for $%s", value, name);

  return TRISTATE_MAYBE;
}

static void
cli_log_func (const gchar *log_domain,
              GLogLevelFlags log_level,
              const gchar *message,
              gpointer user_data)
{
  g_printerr ("%s: %s\n", (const char *) user_data, message);
}

int
main (int argc,
      char *argv[])
{
Simon McVittie's avatar
Simon McVittie committed
  g_autoptr(GOptionContext) context = NULL;
  g_autoptr(GError) local_error = NULL;
  GError **error = &local_error;
  int ret = 2;
  gsize i;
  g_auto(GStrv) original_argv = NULL;
  int original_argc = argc;
  g_autoptr(FlatpakBwrap) bwrap = NULL;
  g_autoptr(FlatpakBwrap) wrapped_command = NULL;
  g_autofree gchar *tmpdir = NULL;
  g_autofree gchar *scratch = NULL;
  g_autofree gchar *overrides = NULL;
  g_autofree gchar *overrides_bin = NULL;
  g_autofree gchar *bwrap_executable = NULL;
  g_autoptr(GString) ld_library_path = g_string_new ("");
  g_autoptr(GString) bin_path = g_string_new ("");
  g_autoptr(GString) adjusted_ld_preload = g_string_new ("");
  g_autofree gchar *cwd_p = NULL;
  g_autofree gchar *cwd_l = NULL;
  const gchar *home;
  g_autofree gchar *bwrap_help = NULL;
  g_autofree gchar *tools_dir = NULL;
  const gchar *bwrap_help_argv[] = { "<bwrap>", "--help", NULL };

Simon McVittie's avatar
Simon McVittie committed
  setlocale (LC_ALL, "");

  g_set_prgname ("pressure-vessel-wrap");

  g_log_set_handler (G_LOG_DOMAIN,
                     G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE,
                     cli_log_func, (void *) g_get_prgname ());

  original_argv = g_new0 (char *, argc + 1);

  for (i = 0; i < argc; i++)
    original_argv[i] = g_strdup (argv[i]);

  if (g_getenv ("STEAM_RUNTIME") != NULL)
    {
      g_printerr ("%s: This program should not be run in the Steam Runtime.",
                  g_get_prgname ());
      g_printerr ("%s: Use pressure-vessel-unruntime instead.",
                  g_get_prgname ());
      ret = 2;
      goto out;
    }