diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c index bb5d72a5916e6d9d5a3c8b999ca137455cfb88de..35632b45cdcb00caaf19256dc92efa7b56fa8c4f 100644 --- a/pressure-vessel/runtime.c +++ b/pressure-vessel/runtime.c @@ -54,6 +54,8 @@ struct _PvRuntime GObject parent; gchar *bubblewrap; + gchar *source; + gchar *id; gchar *deployment; gchar *source_files; /* either deployment or that + "/files" */ gchar *tools_dir; @@ -96,9 +98,10 @@ struct _PvRuntimeClass enum { PROP_0, PROP_BUBBLEWRAP, - PROP_DEPLOYMENT, + PROP_SOURCE, PROP_ORIGINAL_ENVIRON, PROP_FLAGS, + PROP_ID, PROP_VARIABLE_DIR, PROP_PROVIDER_IN_CURRENT_NAMESPACE, PROP_PROVIDER_IN_CONTAINER_NAMESPACE, @@ -399,6 +402,10 @@ pv_runtime_get_property (GObject *object, g_value_set_flags (value, self->flags); break; + case PROP_ID: + g_value_set_string (value, self->id); + break; + case PROP_VARIABLE_DIR: g_value_set_string (value, self->variable_dir); break; @@ -411,8 +418,8 @@ pv_runtime_get_property (GObject *object, g_value_set_string (value, self->provider_in_container_namespace); break; - case PROP_DEPLOYMENT: - g_value_set_string (value, self->deployment); + case PROP_SOURCE: + g_value_set_string (value, self->source); break; case PROP_TOOLS_DIRECTORY: @@ -471,6 +478,12 @@ pv_runtime_set_property (GObject *object, break; + case PROP_ID: + /* Construct-only */ + g_return_if_fail (self->id == NULL); + self->id = g_value_dup_string (value); + break; + case PROP_PROVIDER_IN_CURRENT_NAMESPACE: /* Construct-only */ g_return_if_fail (self->provider_in_current_namespace == NULL); @@ -483,20 +496,20 @@ pv_runtime_set_property (GObject *object, self->provider_in_container_namespace = g_value_dup_string (value); break; - case PROP_DEPLOYMENT: + case PROP_SOURCE: /* Construct-only */ - g_return_if_fail (self->deployment == NULL); + g_return_if_fail (self->source == NULL); path = g_value_get_string (value); if (path != NULL) { - self->deployment = realpath (path, NULL); + self->source = realpath (path, NULL); - if (self->deployment == NULL) + if (self->source == NULL) { /* It doesn't exist. Keep the non-canonical path so we * can warn about it later */ - self->deployment = g_strdup (path); + self->source = g_strdup (path); } } @@ -524,7 +537,7 @@ pv_runtime_constructed (GObject *object) g_return_if_fail (self->original_environ != NULL); g_return_if_fail (self->provider_in_current_namespace != NULL); g_return_if_fail (self->provider_in_container_namespace != NULL); - g_return_if_fail (self->deployment != NULL); + g_return_if_fail (self->source != NULL); g_return_if_fail (self->tools_dir != NULL); } @@ -843,14 +856,103 @@ pv_runtime_create_copy (PvRuntime *self, return TRUE; } +/* + * mutable_lock: (out) (not optional): + */ +static gboolean +pv_runtime_unpack (PvRuntime *self, + PvBwrapLock **mutable_lock, + GError **error) +{ + g_autoptr(FlatpakBwrap) tar = NULL; + g_autofree gchar *deploy_basename = NULL; + g_autofree gchar *unpack_dir = NULL; + + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (mutable_lock != NULL, FALSE); + g_return_val_if_fail (*mutable_lock == NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (self->source != NULL, FALSE); + g_return_val_if_fail (self->id != NULL, FALSE); + g_return_val_if_fail (self->variable_dir != NULL, FALSE); + g_return_val_if_fail (self->variable_dir_fd >= 0, FALSE); + g_return_val_if_fail (self->deployment == NULL, FALSE); + + if (!g_file_test (self->source, G_FILE_TEST_IS_REGULAR)) + return glnx_throw (error, "\"%s\" is not a regular file", self->source); + + if (!g_str_has_suffix (self->source, ".tar.gz")) + return glnx_throw (error, "\"%s\" is not a .tar.gz file", self->source); + + deploy_basename = g_strdup_printf ("deploy-%s", self->id); + self->deployment = g_build_filename (self->variable_dir, + deploy_basename, NULL); + + /* Fast path: if we already unpacked it, nothing more to do! */ + if (g_file_test (self->deployment, G_FILE_TEST_IS_DIR)) + return TRUE; + + /* Lock the parent directory. Anything that directly manipulates the + * unpacked runtimes is expected to do the same, so that + * it cannot be deleting unpacked runtimes at the same time we're + * creating them. + * + * This is an exclusive lock, to avoid two concurrent processes trying + * to unpack the same runtime. */ + *mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WAIT), + error); + + if (*mutable_lock == NULL) + return FALSE; + + /* Slow path: we need to do this the hard way. */ + unpack_dir = g_build_filename (self->variable_dir, "tmp-XXXXXX", NULL); + + if (g_mkdtemp (unpack_dir) == NULL) + return glnx_throw_errno_prefix (error, + "Cannot create temporary directory \"%s\"", + unpack_dir); + + g_info ("Unpacking \"%s\" into \"%s\"...", self->source, unpack_dir); + + tar = flatpak_bwrap_new (NULL); + flatpak_bwrap_add_args (tar, + "tar", + "--force-local", + "-C", unpack_dir, + "-xf", self->source, + NULL); + flatpak_bwrap_finish (tar); + + if (!pv_bwrap_run_sync (tar, NULL, error)) + { + glnx_shutil_rm_rf_at (-1, unpack_dir, NULL, NULL); + return FALSE; + } + + g_info ("Renaming \"%s\" to \"%s\"...", unpack_dir, deploy_basename); + + if (!glnx_renameat (self->variable_dir_fd, unpack_dir, + self->variable_dir_fd, deploy_basename, + error)) + { + glnx_shutil_rm_rf_at (-1, unpack_dir, NULL, NULL); + return FALSE; + } + + return TRUE; +} + static gboolean pv_runtime_initable_init (GInitable *initable, GCancellable *cancellable G_GNUC_UNUSED, GError **error) { PvRuntime *self = PV_RUNTIME (initable); + g_autoptr(PvBwrapLock) mutable_lock = NULL; g_autofree gchar *contents = NULL; - g_autofree gchar *files_ref = NULL; g_autofree gchar *os_release = NULL; gsize len; @@ -868,6 +970,26 @@ pv_runtime_initable_init (GInitable *initable, if (!pv_runtime_init_variable_dir (self, error)) return FALSE; + if (self->flags & PV_RUNTIME_FLAGS_UNPACK_ARCHIVE) + { + if (self->id == NULL) + return glnx_throw (error, "Cannot unpack archive without unique ID"); + + if (self->variable_dir_fd < 0) + return glnx_throw (error, + "Cannot unpack archive without variable directory"); + + if (!pv_runtime_unpack (self, &mutable_lock, error)) + return FALSE; + + /* Set by pv_runtime_unpack */ + g_assert (self->deployment != NULL); + } + else + { + self->deployment = g_strdup (self->source); + } + if (!g_file_test (self->deployment, G_FILE_TEST_IS_DIR)) { return glnx_throw (error, "\"%s\" is not a directory", @@ -904,10 +1026,15 @@ pv_runtime_initable_init (GInitable *initable, * continue to be locked until all processes in the container exit. * If we make a temporary mutable copy, we only hold this lock until * setup has finished. */ - files_ref = g_build_filename (self->source_files, ".ref", NULL); - self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref, - PV_BWRAP_LOCK_FLAGS_CREATE, - error); + if (self->runtime_lock == NULL) + { + g_autofree gchar *files_ref = NULL; + + files_ref = g_build_filename (self->source_files, ".ref", NULL); + self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref, + PV_BWRAP_LOCK_FLAGS_CREATE, + error); + } /* If the runtime is being deleted, ... don't use it, I suppose? */ if (self->runtime_lock == NULL) @@ -915,8 +1042,6 @@ pv_runtime_initable_init (GInitable *initable, if (self->flags & PV_RUNTIME_FLAGS_COPY_RUNTIME) { - g_autoptr(PvBwrapLock) mutable_lock = NULL; - if (self->variable_dir_fd < 0) return glnx_throw (error, "Cannot copy runtime without variable directory"); @@ -925,10 +1050,11 @@ pv_runtime_initable_init (GInitable *initable, * can safely be creating their own temporary copy at the same * time. If another process is doing GC, wait for it to finish, * then take our lock. */ - mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", - (PV_BWRAP_LOCK_FLAGS_CREATE - | PV_BWRAP_LOCK_FLAGS_WAIT), - error); + if (mutable_lock == NULL) + mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WAIT), + error); if (mutable_lock == NULL) return FALSE; @@ -1085,6 +1211,7 @@ pv_runtime_finalize (GObject *object) g_free (self->provider_in_container_namespace); g_free (self->runtime_files_on_host); g_free (self->runtime_usr); + g_free (self->source); g_free (self->source_files); g_free (self->deployment); g_free (self->tools_dir); @@ -1126,6 +1253,13 @@ pv_runtime_class_init (PvRuntimeClass *cls) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + properties[PROP_ID] = + g_param_spec_string ("id", "ID", + "Unique identifier of runtime to be unpacked", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + properties[PROP_VARIABLE_DIR] = g_param_spec_string ("variable-dir", "Variable directory", ("Path to directory for temporary files, or NULL"), @@ -1151,10 +1285,10 @@ pv_runtime_class_init (PvRuntimeClass *cls) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); - properties[PROP_DEPLOYMENT] = - g_param_spec_string ("deployment", "Deployment", + properties[PROP_SOURCE] = + g_param_spec_string ("source", "Source", ("Path to read-only runtime files (merged-/usr " - "or sysroot) in current namespace"), + "or sysroot) or archive, in current namespace"), NULL, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); @@ -1170,7 +1304,8 @@ pv_runtime_class_init (PvRuntimeClass *cls) } PvRuntime * -pv_runtime_new (const char *deployment, +pv_runtime_new (const char *source, + const char *id, const char *variable_dir, const char *bubblewrap, const char *tools_dir, @@ -1180,7 +1315,7 @@ pv_runtime_new (const char *deployment, PvRuntimeFlags flags, GError **error) { - g_return_val_if_fail (deployment != NULL, NULL); + g_return_val_if_fail (source != NULL, NULL); g_return_val_if_fail (bubblewrap != NULL, NULL); g_return_val_if_fail (tools_dir != NULL, NULL); g_return_val_if_fail ((flags & ~(PV_RUNTIME_FLAGS_MASK)) == 0, NULL); @@ -1191,7 +1326,8 @@ pv_runtime_new (const char *deployment, "bubblewrap", bubblewrap, "original-environ", original_environ, "variable-dir", variable_dir, - "deployment", deployment, + "source", source, + "id", id, "tools-directory", tools_dir, "provider-in-current-namespace", provider_in_current_namespace, diff --git a/pressure-vessel/runtime.h b/pressure-vessel/runtime.h index 0050c066ecaa26552e73d50ccf748b03f6a7f7ce..45ab34a027f00f12c19591e25be95942ea32207b 100644 --- a/pressure-vessel/runtime.h +++ b/pressure-vessel/runtime.h @@ -39,6 +39,7 @@ * @PV_RUNTIME_FLAGS_VERBOSE: Be more verbose * @PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS: Include host Vulkan layers * @PV_RUNTIME_FLAGS_COPY_RUNTIME: Copy the runtime and modify the copy + * @PV_RUNTIME_FLAGS_UNPACK_ARCHIVE: Source is an archive, not a deployment * @PV_RUNTIME_FLAGS_NONE: None of the above * * Flags affecting how we set up the runtime. @@ -51,6 +52,7 @@ typedef enum PV_RUNTIME_FLAGS_VERBOSE = (1 << 3), PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS = (1 << 4), PV_RUNTIME_FLAGS_COPY_RUNTIME = (1 << 5), + PV_RUNTIME_FLAGS_UNPACK_ARCHIVE = (1 << 6), PV_RUNTIME_FLAGS_NONE = 0 } PvRuntimeFlags; @@ -61,6 +63,7 @@ typedef enum | PV_RUNTIME_FLAGS_VERBOSE \ | PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS \ | PV_RUNTIME_FLAGS_COPY_RUNTIME \ + | PV_RUNTIME_FLAGS_UNPACK_ARCHIVE \ ) typedef struct _PvRuntime PvRuntime; @@ -74,7 +77,8 @@ typedef struct _PvRuntimeClass PvRuntimeClass; #define PV_RUNTIME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), PV_TYPE_RUNTIME, PvRuntimeClass) GType pv_runtime_get_type (void); -PvRuntime *pv_runtime_new (const char *deployment, +PvRuntime *pv_runtime_new (const char *source, + const char *id, const char *variable_dir, const char *bubblewrap, const char *tools_dir, diff --git a/pressure-vessel/wrap.1.md b/pressure-vessel/wrap.1.md index 7d0106e4f02277d285cdb0a53bfaac58e57909f3..66d5f2cbb64d5cd0b03cf923b9470e33001ca2cd 100644 --- a/pressure-vessel/wrap.1.md +++ b/pressure-vessel/wrap.1.md @@ -115,9 +115,32 @@ pressure-vessel-wrap - run programs in a bubblewrap container (containing bin/sh, bin/env and many other OS files). For example, a Flatpak runtime is a suitable value for *PATH*. +`--runtime-archive` *ARCHIVE* +: Unpack *ARCHIVE* and use it to provide /usr in the container, similar + to `--runtime`. The `--runtime-id` option is also required. + + If this option is used, then `--variable-dir` + (or its environment variable equivalent) is also required. + This option and `--runtime` cannot both be used. + + The archive will be unpacked into a subdirectory of the `--variable-dir`. + Any other subdirectories of the `--variable-dir` that appear to be + different runtimes will be deleted, unless they contain a file + at the top level named `keep` or are currently in use. + + The archive must currently be a gzipped tar file whose name ends + with `.tar.gz`. Other formats might be allowed in future. + `--runtime-base` *PATH* -: If `--runtime` specifies a relative path, look for it relative - to *PATH*. +: If `--runtime` or `--runtime-archive` is specified as a relative path, + look for it relative to *PATH*. + +`--runtime-id` *ID* +: Use *ID* to construct a directory into which the `--runtime-archive` + will be unpacked. If the *ID* is the same as in a previous run of + pressure-vessel-wrap, the content of the `--runtime-archive` will + be assumed to be the same as in that previous run, resulting in + the previous runtime being reused. `--share-home`, `--unshare-home` : If `--unshare-home` is specified, use the home directory given diff --git a/pressure-vessel/wrap.c b/pressure-vessel/wrap.c index 4a48a1fb827c751dc67d235ae714cfe467fcd389..bb2142e708a6d660bb48bca8e6420e9c4f04f13a 100644 --- a/pressure-vessel/wrap.c +++ b/pressure-vessel/wrap.c @@ -724,8 +724,10 @@ static gboolean opt_import_vulkan_layers = TRUE; static PvShell opt_shell = PV_SHELL_NONE; static GPtrArray *opt_ld_preload = NULL; static GArray *opt_pass_fds = NULL; -static char *opt_runtime_base = NULL; static char *opt_runtime = NULL; +static char *opt_runtime_archive = NULL; +static char *opt_runtime_base = NULL; +static char *opt_runtime_id = NULL; static Tristate opt_share_home = TRISTATE_MAYBE; static gboolean opt_share_pid = TRUE; static double opt_terminate_idle_timeout = 0.0; @@ -1105,11 +1107,22 @@ static GOptionEntry options[] = "it with the provider's graphics stack. The empty string " "means don't use a runtime. [Default: $PRESSURE_VESSEL_RUNTIME or '']", "RUNTIME" }, + { "runtime-archive", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_archive, + "Unpack the ARCHIVE and use it as the runtime, using --runtime-id to " + "avoid repeatedly unpacking the same archive. " + "[Default: $PRESSURE_VESSEL_RUNTIME_ARCHIVE]", + "ARCHIVE" }, { "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. " + "If a --runtime or --runtime-archive is a relative path, look for " + "it relative to BASE. " "[Default: $PRESSURE_VESSEL_RUNTIME_BASE or '.']", "BASE" }, + { "runtime-id", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_id, + "Reuse a previously-unpacked --runtime-archive if its ID matched this", + "ID" }, { "share-home", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb, "Use the real home directory. " @@ -1298,6 +1311,7 @@ main (int argc, NULL, NULL); opt_copy_runtime = pv_boolean_environment ("PRESSURE_VESSEL_COPY_RUNTIME", opt_copy_runtime); + opt_runtime_id = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_ID")); { const char *value = g_getenv ("PRESSURE_VESSEL_VARIABLE_DIR"); @@ -1350,21 +1364,57 @@ main (int argc, if (opt_verbose) pv_set_up_logging (opt_verbose); - if (opt_runtime == NULL) - opt_runtime = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME")); + /* Specifying either one of these mutually-exclusive options as a + * command-line option disables use of the environment variable for + * the other one */ + if (opt_runtime == NULL && opt_runtime_archive == NULL) + { + opt_runtime = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME")); + + /* Normalize empty string to NULL to simplify later code */ + if (opt_runtime != NULL && opt_runtime[0] == '\0') + g_clear_pointer (&opt_runtime, g_free); + + opt_runtime_archive = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_ARCHIVE")); + + if (opt_runtime_archive != NULL && opt_runtime_archive[0] == '\0') + g_clear_pointer (&opt_runtime_archive, g_free); + } + + if (opt_runtime_id != NULL) + { + const char *p; + + if (opt_runtime_id[0] == '-' || opt_runtime_id[0] == '.') + { + g_warning ("--runtime-id must not start with dash or dot"); + goto out; + } + + for (p = opt_runtime_id; *p != '\0'; p++) + { + if (!g_ascii_isalnum (*p) && *p != '_' && *p != '-' && *p != '.') + { + g_warning ("--runtime-id may only contain " + "alphanumerics, underscore, dash or dot"); + goto out; + } + } + } 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') + if (opt_runtime != NULL && opt_runtime_archive != NULL) { - g_autofree gchar *tmp = g_steal_pointer (&opt_runtime); + g_warning ("--runtime and --runtime-archive cannot both be used"); + goto out; + } - opt_runtime = g_build_filename (opt_runtime_base, tmp, NULL); + if (opt_runtime_archive != NULL && opt_runtime_id == NULL) + { + g_warning ("--runtime-archive requires --runtime-id"); + goto out; } if (opt_graphics_provider == NULL) @@ -1703,9 +1753,11 @@ main (int argc, NULL); } - if (opt_runtime != NULL && opt_runtime[0] != '\0') + if (opt_runtime != NULL || opt_runtime_archive != NULL) { PvRuntimeFlags flags = PV_RUNTIME_FLAGS_NONE; + g_autofree gchar *runtime_resolved = NULL; + const char *runtime_path = NULL; if (opt_gc_runtimes) flags |= PV_RUNTIME_FLAGS_GC_RUNTIMES; @@ -1725,7 +1777,29 @@ main (int argc, if (opt_copy_runtime) flags |= PV_RUNTIME_FLAGS_COPY_RUNTIME; - g_debug ("Configuring runtime %s...", opt_runtime); + if (opt_runtime != NULL) + { + /* already checked for mutually exclusive options */ + g_assert (opt_runtime_archive == NULL); + runtime_path = opt_runtime; + } + else + { + flags |= PV_RUNTIME_FLAGS_UNPACK_ARCHIVE; + g_assert (opt_runtime_id != NULL); + runtime_path = opt_runtime_archive; + } + + if (!g_path_is_absolute (runtime_path) + && opt_runtime_base != NULL + && opt_runtime_base[0] != '\0') + { + runtime_resolved = g_build_filename (opt_runtime_base, + runtime_path, NULL); + runtime_path = runtime_resolved; + } + + g_debug ("Configuring runtime %s...", runtime_path); if (is_flatpak_env && !opt_copy_runtime) { @@ -1735,7 +1809,8 @@ main (int argc, goto out; } - runtime = pv_runtime_new (opt_runtime, + runtime = pv_runtime_new (runtime_path, + opt_runtime_id, opt_variable_dir, bwrap_executable, tools_dir, @@ -2601,8 +2676,10 @@ out: g_clear_pointer (&opt_steam_app_id, g_free); g_clear_pointer (&opt_home, g_free); g_clear_pointer (&opt_fake_home, g_free); - g_clear_pointer (&opt_runtime_base, g_free); g_clear_pointer (&opt_runtime, g_free); + g_clear_pointer (&opt_runtime_archive, g_free); + g_clear_pointer (&opt_runtime_base, g_free); + g_clear_pointer (&opt_runtime_id, g_free); g_clear_pointer (&opt_pass_fds, g_array_unref); g_clear_pointer (&opt_variable_dir, g_free);