Skip to content
Snippets Groups Projects
wrap.c 76.9 KiB
Newer Older
    "/etc/machine-id",
    "/etc/resolv.conf",
    "/var/lib/dbus",
    "/var/lib/dhcp",
    "/var/lib/sudo",
    "/var/lib/urandom",
    NULL
  };
  g_autofree gchar *xrd = g_strdup_printf ("/run/user/%ld", (long) geteuid ());
  g_autofree gchar *usr = NULL;
  gsize i, j;
  const gchar *member;
  g_autoptr(GString) dri_path = g_string_new ("");

  g_return_val_if_fail (tools_dir != NULL, FALSE);
  g_return_val_if_fail (runtime != NULL, FALSE);
  g_return_val_if_fail (overrides != NULL, FALSE);
  g_return_val_if_fail (scratch != NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  usr = g_build_filename (runtime, "usr", NULL);

  if (!bind_usr (bwrap, runtime, "/", error))
    return FALSE;

  flatpak_bwrap_add_args (bwrap,
                          "--setenv", "XDG_RUNTIME_DIR", xrd,
                          "--proc", "/proc",
                          "--ro-bind", "/sys", "/sys",
                          "--tmpfs", "/run",
                          "--tmpfs", "/tmp",
                          "--tmpfs", "/var",
                          "--symlink", "../run", "/var/run",
                          NULL);

  for (i = 0; i < G_N_ELEMENTS (bind_mutable); i++)
    {
      g_autofree gchar *path = g_build_filename (runtime, bind_mutable[i],
                                                 NULL);
      g_autoptr(GDir) dir = NULL;

      dir = g_dir_open (path, 0, NULL);

      if (dir == NULL)
        continue;

      for (member = g_dir_read_name (dir);
           member != NULL;
           member = g_dir_read_name (dir))
        {
          g_autofree gchar *dest = g_build_filename ("/", bind_mutable[i],
                                                     member, NULL);
          g_autofree gchar *full = NULL;
          g_autofree gchar *target = NULL;

          if (g_strv_contains (dont_bind, dest))
            continue;

          full = g_build_filename (runtime, bind_mutable[i], member, NULL);
          target = glnx_readlinkat_malloc (-1, full, NULL, NULL);

          if (target != NULL)
            flatpak_bwrap_add_args (bwrap, "--symlink", target, dest, NULL);
          else
            flatpak_bwrap_add_args (bwrap, "--ro-bind", full, dest, NULL);
        }
    }

  if (g_file_test ("/etc/machine-id", G_FILE_TEST_EXISTS))
    {
      flatpak_bwrap_add_args (bwrap,
                              "--ro-bind", "/etc/machine-id", "/etc/machine-id",
                              "--symlink", "/etc/machine-id",
                              "/var/lib/dbus/machine-id",
                              NULL);
    }
  else if (g_file_test ("/var/lib/dbus/machine-id", G_FILE_TEST_EXISTS))
    {
      flatpak_bwrap_add_args (bwrap,
                              "--ro-bind", "/var/lib/dbus/machine-id",
                              "/etc/machine-id",
                              "--symlink", "/etc/machine-id",
                              "/var/lib/dbus/machine-id",
                              NULL);
    }

  /* /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 *localtime = NULL;
      gboolean is_reachable = FALSE;
      g_autofree char *timezone = flatpak_get_timezone ();
      g_autofree char *timezone_content = g_strdup_printf ("%s\n", timezone);

      localtime = glnx_readlinkat_malloc (-1, "/etc/localtime", NULL, NULL);

      if (localtime != 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, localtime);
          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", localtime, "/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);
    }

  if (g_file_test ("/etc/resolv.conf", G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf",
                            NULL);
  if (g_file_test ("/etc/host.conf", G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", "/etc/host.conf", "/etc/host.conf",
                            NULL);
  if (g_file_test ("/etc/hosts", G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", "/etc/hosts", "/etc/hosts",
                            NULL);

  /* TODO: Synthesize a passwd with only the user and nobody,
   * like Flatpak does? */
  if (g_file_test ("/etc/passwd", G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", "/etc/passwd", "/etc/passwd",
                            NULL);
  if (g_file_test ("/etc/group", G_FILE_TEST_EXISTS))
    flatpak_bwrap_add_args (bwrap,
                            "--ro-bind", "/etc/group", "/etc/group",
                            NULL);

  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);

  for (i = 0; i < G_N_ELEMENTS (multiarch_tuples); i++)
    {
      g_autofree gchar *tool = g_strdup_printf ("%s-capsule-capture-libs",
                                                multiarch_tuples[i]);
      g_autofree gchar *tool_path = NULL;
      g_autofree gchar *ld_so = NULL;
      const gchar *argv[] = { NULL, "--print-ld.so", NULL };

      g_debug ("Checking for %s libraries...", multiarch_tuples[i]);

      tool_path = g_build_filename (tools_dir, tool, NULL);
      argv[0] = tool_path;

      /* This has the side-effect of testing whether we can run binaries
       * for this architecture */
      ld_so = capture_output (argv, NULL);

      if (ld_so != NULL)
        {
          g_auto(GStrv) dirs = NULL;
          g_autoptr(FlatpakBwrap) run_in_container = NULL;
          g_autoptr(FlatpakBwrap) temp_bwrap = NULL;
          g_autofree gchar *libdir_in_container = g_build_filename ("/overrides",
                                                                    "lib",
                                                                    multiarch_tuples[i],
                                                                    NULL);
          g_autofree gchar *libdir_on_host = g_build_filename (overrides, "lib",
                                                               multiarch_tuples[i],
                                                               NULL);
          g_autofree gchar *this_dri_path_on_host = g_build_filename (libdir_on_host,
                                                                      "dri", NULL);
          g_autofree gchar *this_dri_path_in_container = g_build_filename (libdir_in_container,
                                                                           "dri", NULL);
          g_autofree gchar *libc = NULL;

          search_path_append (dri_path, this_dri_path_in_container);

          g_mkdir_with_parents (libdir_on_host, 0755);
          g_mkdir_with_parents (this_dri_path_on_host, 0755);

          run_in_container = flatpak_bwrap_new (NULL);
          flatpak_bwrap_add_args (run_in_container,
                                  bwrap->argv->pdata[0],
                                  "--ro-bind", "/", "/",
                                  "--bind", overrides, overrides,
                                  "--tmpfs", scratch,
                                  NULL);
          if (!bind_usr (run_in_container, runtime, scratch, error))
            return FALSE;

          temp_bwrap = flatpak_bwrap_new (NULL);
          flatpak_bwrap_append_bwrap (temp_bwrap, run_in_container);
          flatpak_bwrap_add_args (temp_bwrap,
                                  tool_path,
                                  "--container", scratch,
                                  "--link-target", "/run/host",
                                  "--dest", libdir_on_host,
                                  "--provider", "/",
                                  "gl:",
                                  NULL);
          flatpak_bwrap_finish (temp_bwrap);

          if (!run_bwrap (temp_bwrap, 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 *real_path_in_host = NULL;
              g_autofree gchar *real_path_in_runtime = NULL;

              g_debug ("Making host ld.so visible in container");

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

              g_clear_pointer (&temp_bwrap, flatpak_bwrap_free);

              temp_bwrap = flatpak_bwrap_new (NULL);
              flatpak_bwrap_add_args (temp_bwrap,
                                      bwrap->argv->pdata[0],
                                      NULL);

              if (!bind_usr (temp_bwrap, runtime, "/", error))
                return FALSE;

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

              real_path_in_runtime = capture_output ((const char * const *) temp_bwrap->argv->pdata,
                                                     error);

              if (real_path_in_runtime == NULL)
                return FALSE;

              g_debug ("Container path: %s -> %s", ld_so, real_path_in_runtime);
              flatpak_bwrap_add_args (bwrap,
                                      "--ro-bind", real_path_in_host,
                                      real_path_in_runtime,
                                      NULL);
              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);
            }

          /* /lib32 or /lib64 */
          libqual = libquals[i];
          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, run_in_container, overrides, scratch,
                                 tool_path, dirs[j], libdir_on_host, error))
                return FALSE;
            }
        }
      else
        {
          g_debug ("Cannot determine ld.so for %s", multiarch_tuples[i]);
        }
    }

  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);

  add_symlink_farm (bwrap, overrides, "/overrides");

  if (!bind_usr (bwrap, "/", "/run/host", error))
    return FALSE;

  return TRUE;
}

/* 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_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;
}

static void
wrap_in_xterm (FlatpakBwrap *wrapped_command)
{
  g_return_if_fail (wrapped_command != NULL);

  flatpak_bwrap_add_args (wrapped_command,
                          "xterm", "-e",
                          /* Original command will go here and become
                           * the argv of xterm -e */
                          NULL);
}

typedef enum
{
  SHELL_NONE = 0,
  SHELL_AFTER,
  SHELL_FAIL,
  SHELL_INSTEAD
} Shell;

static void
wrap_interactive (FlatpakBwrap *wrapped_command,
                  Shell shell)
{
  static const char preamble[] =
    {
      "prgname=\"$1\"\n"
      "shift\n"
    };
  const char *script = "";
  static const char start_shell[] =
    {
      "echo\n"
      "echo\n"
      "echo\n"
      "echo \"$1: Starting interactive shell (original command is in "
      "\\\"\\$@\\\")\"\n"
      "echo\n"
      "echo\n"
      "echo\n"
      "exec bash -i -s \"$@\"\n"
    };
  gchar *command;

  g_return_if_fail (wrapped_command != NULL);

  switch (shell)
    {
      case SHELL_NONE:
        script =
          "e=0\n"
          "\"$@\" || e=$?\n"
          "echo\n"
          "echo \"Press Enter or ^D to continue...\"\n"
          "read reply || true\n"
          "exit \"$e\"\n";
        break;

      case SHELL_AFTER:
        script =
          "e=0\n"
          "\"$@\" || e=$?\n";
        break;

      case SHELL_FAIL:
        script =
          "if \"$@\"; then exit 0; else e=\"$?\"; fi\n"
          "echo \"$1: command exit status $e\"\n";
        break;

      case SHELL_INSTEAD:
        break;

      default:
        g_return_if_reached ();
    }

  command = g_strdup_printf ("%s%s%s",
                             preamble,
                             script,
                             shell == SHELL_NONE ? "" : start_shell);

  flatpak_bwrap_add_args (wrapped_command,
                          "sh",   /* $0 for sh */
                          g_get_prgname (),   /* $1 for sh */
                          /* Original command will go here and become
                           * the argv of command, and eventually
                           * the argv of bash -i -s */
                          NULL);
}

static gboolean
wrap_tty (FlatpakBwrap *wrapped_command,
          GError **error)
{
  int fd;

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

  fflush (stdout);
  fflush (stderr);

  fd = open ("/dev/tty", O_RDONLY);

  if (fd < 0)
    return glnx_throw_errno_prefix (error,
                                    "Cannot open /dev/tty for reading");

  if (dup2 (fd, 0) < 0)
    return glnx_throw_errno_prefix (error, "Cannot use /dev/tty as stdin");

  g_close (fd, NULL);

  fd = open ("/dev/tty", O_WRONLY);

  if (fd < 0)
    return glnx_throw_errno_prefix (error, "Cannot open /dev/tty for writing");

  if (dup2 (fd, 1) < 0)
    return glnx_throw_errno_prefix (error, "Cannot use /dev/tty as stdout");

  if (dup2 (fd, 2) < 0)
    return glnx_throw_errno_prefix (error, "Cannot use /dev/tty as stderr");

  g_close (fd, NULL);
  return TRUE;
}

typedef enum
{
  TERMINAL_NONE = 0,
  TERMINAL_AUTO,
  TERMINAL_TTY,
  TERMINAL_XTERM,
} Terminal;

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 Shell opt_shell = SHELL_NONE;
static GPtrArray *opt_ld_preload = NULL;
static char *opt_runtime_base = NULL;
static char *opt_runtime = NULL;
static gboolean opt_share_home = TRUE;
static gboolean opt_verbose = FALSE;
static gboolean opt_version = FALSE;
static Terminal opt_terminal = 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 = SHELL_NONE;
      return TRUE;
    }

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

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

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

      case 'n':
        if (g_strcmp0 (value, "none") == 0 || g_strcmp0 (value, "no") == 0)
          {
            opt_shell = 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 = TERMINAL_AUTO;
      return TRUE;
    }

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

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

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

      case 'x':
        if (g_strcmp0 (value, "xterm") == 0)
          {
            opt_terminal = 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 GOptionEntry options[] =
{
  { "env-if-host", 0, 0, 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, 0, 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]",
    "ID" },
  { "steam-app-id", 0, 0, 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, 0, G_OPTION_ARG_FILENAME, &opt_home,
    "Use HOME as home directory. Implies --unshare-home. "
    "[Default: $PRESSURE_VESSEL_HOME if set]", "HOME" },
  { "host-fallback", 0, 0, 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, 0, G_OPTION_ARG_CALLBACK, &opt_host_ld_preload_cb,
    "Add MODULE from the host system to LD_PRELOAD when executing COMMAND.",
    "MODULE" },
  { "runtime", 0, 0, 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, 0, 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, 0, G_OPTION_ARG_NONE, &opt_share_home,
    "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_REVERSE, G_OPTION_ARG_NONE, &opt_share_home,
    "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, 0, 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, 0, 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, 0, G_OPTION_ARG_NONE, &opt_verbose,
    "Be more verbose.", NULL },
  { "version", 0, 0, 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 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 *bwrap_executable = NULL;
  g_autoptr(GString) ld_library_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;
    }

  opt_freedesktop_app_id = g_strdup (g_getenv ("PRESSURE_VESSEL_FDO_APP_ID"));

  if (opt_freedesktop_app_id != NULL && opt_freedesktop_app_id[0] == '\0')
    g_clear_pointer (&opt_freedesktop_app_id, g_free);

  opt_home = g_strdup (g_getenv ("PRESSURE_VESSEL_HOME"));

  if (opt_home != NULL && opt_home[0] == '\0')
    g_clear_pointer (&opt_home, g_free);

  opt_share_home = boolean_environment ("PRESSURE_VESSEL_SHARE_HOME", TRUE);
  opt_verbose = boolean_environment ("PRESSURE_VESSEL_VERBOSE", FALSE);

  if (!opt_shell_cb ("$PRESSURE_VESSEL_SHELL",
                     g_getenv ("PRESSURE_VESSEL_SHELL"), NULL, error))
    goto out;

  if (!opt_terminal_cb ("$PRESSURE_VESSEL_TERMINAL",
                        g_getenv ("PRESSURE_VESSEL_TERMINAL"), NULL, error))
    goto out;

  context = g_option_context_new ("[--] COMMAND [ARGS]\n"
                                  "Run COMMAND [ARGS] in a container.\n");

  g_option_context_add_main_entries (context, options, NULL);

  if (!g_option_context_parse (context, &argc, &argv, error))
    goto out;

  if (opt_runtime == NULL)
    opt_runtime = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME"));

  if (opt_runtime_base == NULL)
    opt_runtime_base = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_BASE"));

  if (opt_runtime != NULL
      && opt_runtime[0] != '\0'
      && !g_path_is_absolute (opt_runtime)
      && opt_runtime_base != NULL
      && opt_runtime_base[0] != '\0')
    {
      g_autofree gchar *tmp = g_steal_pointer (&opt_runtime);

      opt_runtime = g_build_filename (opt_runtime_base, tmp, NULL);
    }

  if (opt_version)
    {
      g_print ("pressure-vessel version %s\n", VERSION);
      ret = 0;
      goto out;
    }

  if (argc < 2)
    {
      g_printerr ("%s: An executable to run is required\n",
                  g_get_prgname ());
      goto out;
    }

  if (opt_terminal == TERMINAL_AUTO)
      if (opt_shell != SHELL_NONE)
        opt_terminal = TERMINAL_XTERM;
      else
        opt_terminal = TERMINAL_NONE;
  if (opt_terminal == TERMINAL_NONE && opt_shell != SHELL_NONE)
      g_printerr ("%s: --terminal=none is incompatible with --shell\n",
                  g_get_prgname ());
      goto out;
  if (strcmp (argv[1], "--") == 0)
    {
      argv++;
      argc--;
    }