4ecce5884d
This is taking us closer to deeper integration in the treecompose side with RPM instead of forking out to things. It works except...we end up with the dreaded __db.001, .dbenv.lock files =/ Best option would be to teach RPM how to open a database really read-only. Failing that, could use the immutable bit?
1186 lines
36 KiB
C
1186 lines
36 KiB
C
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
|
|
*
|
|
* Copyright (C) 2013,2014 Colin Walters <walters@verbum.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published
|
|
* by the Free Software Foundation; either version 2 of the licence or (at
|
|
* your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General
|
|
* Public License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 59 Temple Place, Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
#include <glib-unix.h>
|
|
#include <json-glib/json-glib.h>
|
|
#include <sys/mount.h>
|
|
#include <gio/gunixoutputstream.h>
|
|
|
|
#include "rpmostree-compose-builtins.h"
|
|
#include "rpmostree-util.h"
|
|
#include "rpmostree-hawkey-utils.h"
|
|
#include "rpmostree-treepkgdiff.h"
|
|
#include "rpmostree-postprocess.h"
|
|
|
|
#include "libgsystem.h"
|
|
|
|
static char *opt_workdir;
|
|
static gboolean opt_workdir_tmpfs;
|
|
static char *opt_cachedir;
|
|
static char *opt_proxy;
|
|
static char *opt_output_repodata_dir;
|
|
static char **opt_metadata_strings;
|
|
static char *opt_repo;
|
|
static char **opt_override_pkg_repos;
|
|
static gboolean opt_print_only;
|
|
|
|
static GOptionEntry option_entries[] = {
|
|
{ "add-metadata-string", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_metadata_strings, "Append given key and value (in string format) to metadata", "KEY=VALUE" },
|
|
{ "workdir", 0, 0, G_OPTION_ARG_STRING, &opt_workdir, "Working directory", "WORKDIR" },
|
|
{ "workdir-tmpfs", 0, 0, G_OPTION_ARG_NONE, &opt_workdir_tmpfs, "Use tmpfs for working state", NULL },
|
|
{ "output-repodata-dir", 0, 0, G_OPTION_ARG_STRING, &opt_output_repodata_dir, "Save downloaded repodata in DIR", "DIR" },
|
|
{ "cachedir", 0, 0, G_OPTION_ARG_STRING, &opt_cachedir, "Cached state", "CACHEDIR" },
|
|
{ "repo", 'r', 0, G_OPTION_ARG_STRING, &opt_repo, "Path to OSTree repository", "REPO" },
|
|
{ "add-override-pkg-repo", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_override_pkg_repos, "Include an additional package repository from DIRECTORY", "DIRECTORY" },
|
|
{ "proxy", 0, 0, G_OPTION_ARG_STRING, &opt_proxy, "HTTP proxy", "PROXY" },
|
|
{ "print-only", 0, 0, G_OPTION_ARG_NONE, &opt_print_only, "Just expand any includes and print treefile", NULL },
|
|
{ NULL }
|
|
};
|
|
|
|
typedef struct {
|
|
GPtrArray *treefile_context_dirs;
|
|
|
|
GBytes *serialized_treefile;
|
|
} RpmOstreeTreeComposeContext;
|
|
|
|
static char *
|
|
subprocess_context_print_args (GSSubprocessContext *ctx)
|
|
{
|
|
GString *ret = g_string_new ("");
|
|
gs_strfreev char **argv = NULL;
|
|
char **strviter;
|
|
|
|
g_object_get ((GObject*)ctx, "argv", &argv, NULL);
|
|
for (strviter = argv; strviter && *strviter; strviter++)
|
|
{
|
|
gs_free char *quoted = g_shell_quote (*strviter);
|
|
g_string_append_c (ret, ' ');
|
|
g_string_append (ret, quoted);
|
|
}
|
|
|
|
return g_string_free (ret, FALSE);
|
|
}
|
|
|
|
static gboolean
|
|
object_get_optional_string_member (JsonObject *object,
|
|
const char *member_name,
|
|
const char **out_value,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
JsonNode *node = json_object_get_member (object, member_name);
|
|
|
|
if (node != NULL)
|
|
{
|
|
*out_value = json_node_get_string (node);
|
|
if (!*out_value)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Member '%s' is not a string", member_name);
|
|
goto out;
|
|
}
|
|
}
|
|
else
|
|
*out_value = NULL;
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static const char *
|
|
object_require_string_member (JsonObject *object,
|
|
const char *member_name,
|
|
GError **error)
|
|
{
|
|
const char *ret;
|
|
if (!object_get_optional_string_member (object, member_name, &ret, error))
|
|
return NULL;
|
|
if (!ret)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Member '%s' not found", member_name);
|
|
return NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static const char *
|
|
array_require_string_element (JsonArray *array,
|
|
guint i,
|
|
GError **error)
|
|
{
|
|
const char *ret = json_array_get_string_element (array, i);
|
|
if (!ret)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Element at index %u is not a string", i);
|
|
return NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
append_string_array_to (JsonObject *object,
|
|
GFile *treefile_path,
|
|
const char *member_name,
|
|
GPtrArray *array,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
JsonArray *jarray = json_object_get_array_member (object, member_name);
|
|
guint i, len;
|
|
|
|
if (!jarray)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"No member '%s' found", member_name);
|
|
return FALSE;
|
|
}
|
|
|
|
len = json_array_get_length (jarray);
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
const char *v = array_require_string_element (jarray, i, error);
|
|
if (!v)
|
|
return FALSE;
|
|
g_ptr_array_add (array, g_strdup (v));
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
typedef struct {
|
|
GSSubprocess *process;
|
|
GFile *tmp_reposdir_path;
|
|
GDataOutputStream *stdin;
|
|
/* GDataInputStream *stdout; */
|
|
} YumContext;
|
|
|
|
static gboolean
|
|
yum_context_close (YumContext *yumctx,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
|
|
if (!yumctx)
|
|
return TRUE;
|
|
|
|
if (yumctx->process)
|
|
{
|
|
if (yumctx->stdin)
|
|
{
|
|
if (!g_output_stream_close ((GOutputStream*)yumctx->stdin, cancellable, error))
|
|
goto out;
|
|
g_clear_object (&yumctx->stdin);
|
|
}
|
|
/*
|
|
if (yumctx->stdout)
|
|
{
|
|
if (!g_input_stream_close ((GInputStream*)yumctx->stdout, cancellable, error))
|
|
goto out;
|
|
g_clear_object (&yumctx->stdout);
|
|
}
|
|
*/
|
|
|
|
g_print ("Waiting for yum...\n");
|
|
if (!gs_subprocess_wait_sync_check (yumctx->process, cancellable, error))
|
|
goto out;
|
|
g_print ("Waiting for yum [OK]\n");
|
|
g_clear_object (&yumctx->process);
|
|
}
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
yum_context_free (YumContext *yumctx)
|
|
{
|
|
if (!yumctx)
|
|
return;
|
|
(void) yum_context_close (yumctx, NULL, NULL);
|
|
g_free (yumctx);
|
|
}
|
|
|
|
static inline
|
|
void cleanup_keyfile_unref (void *loc)
|
|
{
|
|
GKeyFile *locp = *((GKeyFile**)loc);
|
|
if (locp)
|
|
g_key_file_unref (locp);
|
|
}
|
|
|
|
static gboolean
|
|
append_repo_and_cache_opts (RpmOstreeTreeComposeContext *self,
|
|
JsonObject *treedata,
|
|
GFile *workdir,
|
|
GPtrArray *args,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
JsonArray *enable_repos = NULL;
|
|
guint i;
|
|
char **iter;
|
|
gs_unref_object GFile *yumcache_lookaside = NULL;
|
|
gs_unref_object GFile *repos_tmpdir = NULL;
|
|
gs_unref_ptrarray GPtrArray *reposdir_args = g_ptr_array_new_with_free_func (g_free);
|
|
|
|
if (opt_output_repodata_dir)
|
|
yumcache_lookaside = g_file_new_for_path (opt_output_repodata_dir);
|
|
else
|
|
{
|
|
yumcache_lookaside = g_file_resolve_relative_path (workdir, "yum-cache");
|
|
if (!gs_file_ensure_directory (yumcache_lookaside, TRUE, cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
repos_tmpdir = g_file_resolve_relative_path (workdir, "tmp-repos");
|
|
if (!gs_shutil_rm_rf (repos_tmpdir, cancellable, error))
|
|
goto out;
|
|
if (!gs_file_ensure_directory (repos_tmpdir, TRUE, cancellable, error))
|
|
goto out;
|
|
|
|
if (g_getenv ("RPM_OSTREE_OFFLINE"))
|
|
g_ptr_array_add (args, g_strdup ("-C"));
|
|
|
|
{
|
|
const char *proxy;
|
|
if (opt_proxy)
|
|
proxy = opt_proxy;
|
|
else
|
|
proxy = g_getenv ("http_proxy");
|
|
|
|
if (proxy)
|
|
g_ptr_array_add (args, g_strconcat ("--setopt=proxy=", proxy, NULL));
|
|
}
|
|
|
|
g_ptr_array_add (args, g_strdup ("--disablerepo=*"));
|
|
|
|
/* Add the directory for each treefile to the reposdir argument */
|
|
for (i = 0; i < self->treefile_context_dirs->len; i++)
|
|
{
|
|
GFile *contextdir = self->treefile_context_dirs->pdata[i];
|
|
g_ptr_array_add (reposdir_args, g_file_get_path (contextdir));
|
|
}
|
|
|
|
/* Process local overrides */
|
|
for (iter = opt_override_pkg_repos; iter && *iter; iter++)
|
|
{
|
|
const char *repodir = *iter;
|
|
gs_free char *bn = g_path_get_basename (repodir);
|
|
gs_free char *reponame = g_strconcat ("rpm-ostree-override-", repodir, NULL);
|
|
gs_free char *baseurl = g_strconcat ("file://", repodir, NULL);
|
|
gs_free char *tmprepo_filename = g_strconcat (reponame, ".repo", NULL);
|
|
gs_unref_object GFile *tmprepo_path = g_file_get_child (repos_tmpdir, tmprepo_filename);
|
|
__attribute__ ((cleanup(cleanup_keyfile_unref))) GKeyFile *keyfile = NULL;
|
|
gs_free char *data = NULL;
|
|
gsize len;
|
|
|
|
keyfile = g_key_file_new ();
|
|
g_key_file_set_string (keyfile, reponame, "name", reponame);
|
|
g_key_file_set_string (keyfile, reponame, "baseurl", baseurl);
|
|
|
|
data = g_key_file_to_data (keyfile, &len, NULL);
|
|
|
|
if (!g_file_replace_contents (tmprepo_path, data, len, NULL, FALSE, 0, NULL,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
g_ptr_array_add (args, g_strconcat ("--enablerepo=", reponame, NULL));
|
|
}
|
|
|
|
if (opt_override_pkg_repos)
|
|
g_ptr_array_add (reposdir_args, g_file_get_path (repos_tmpdir));
|
|
|
|
{
|
|
gboolean first = TRUE;
|
|
GString *reposdir_value = g_string_new ("--setopt=reposdir=");
|
|
|
|
for (i = 0; i < reposdir_args->len; i++)
|
|
{
|
|
const char *reponame = reposdir_args->pdata[i];
|
|
if (first)
|
|
first = FALSE;
|
|
else
|
|
g_string_append_c (reposdir_value, ',');
|
|
g_string_append (reposdir_value, reponame);
|
|
}
|
|
g_ptr_array_add (args, g_string_free (reposdir_value, FALSE));
|
|
}
|
|
|
|
|
|
if (json_object_has_member (treedata, "repos"))
|
|
enable_repos = json_object_get_array_member (treedata, "repos");
|
|
if (enable_repos)
|
|
{
|
|
guint i;
|
|
guint n = json_array_get_length (enable_repos);
|
|
for (i = 0; i < n; i++)
|
|
{
|
|
const char *reponame = array_require_string_element (enable_repos, i, error);
|
|
if (!reponame)
|
|
goto out;
|
|
g_ptr_array_add (args, g_strconcat ("--enablerepo=", reponame, NULL));
|
|
}
|
|
}
|
|
|
|
g_ptr_array_add (args, g_strdup ("--setopt=keepcache=0"));
|
|
g_ptr_array_add (args, g_strconcat ("--setopt=cachedir=",
|
|
gs_file_get_path_cached (yumcache_lookaside),
|
|
NULL));
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static YumContext *
|
|
yum_context_new (RpmOstreeTreeComposeContext *self,
|
|
JsonObject *treedata,
|
|
GFile *yumroot,
|
|
GFile *workdir,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean success = FALSE;
|
|
YumContext *yumctx = NULL;
|
|
GPtrArray *yum_argv = g_ptr_array_new_with_free_func (g_free);
|
|
gs_unref_object GSSubprocessContext *context = NULL;
|
|
gs_unref_object GSSubprocess *yum_process = NULL;
|
|
|
|
g_ptr_array_add (yum_argv, g_strdup ("yum"));
|
|
g_ptr_array_add (yum_argv, g_strdup ("-y"));
|
|
|
|
if (!append_repo_and_cache_opts (self, treedata, workdir, yum_argv,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
g_ptr_array_add (yum_argv, g_strconcat ("--installroot=",
|
|
gs_file_get_path_cached (yumroot),
|
|
NULL));
|
|
|
|
g_ptr_array_add (yum_argv, g_strdup ("shell"));
|
|
|
|
g_ptr_array_add (yum_argv, NULL);
|
|
|
|
context = gs_subprocess_context_new ((char**)yum_argv->pdata);
|
|
{
|
|
gs_strfreev char **duped_environ = g_get_environ ();
|
|
|
|
duped_environ = g_environ_setenv (duped_environ, "OSTREE_KERNEL_INSTALL_NOOP", "1", TRUE);
|
|
/* See fedora's kernel.spec */
|
|
duped_environ = g_environ_setenv (duped_environ, "HARDLINK", "no", TRUE);
|
|
|
|
gs_subprocess_context_set_environment (context, duped_environ);
|
|
}
|
|
|
|
gs_subprocess_context_set_stdin_disposition (context, GS_SUBPROCESS_STREAM_DISPOSITION_PIPE);
|
|
/* gs_subprocess_context_set_stdout_disposition (context, GS_SUBPROCESS_STREAM_DISPOSITION_PIPE); */
|
|
|
|
yumctx = g_new0 (YumContext, 1);
|
|
|
|
{
|
|
gs_free char *cmdline = subprocess_context_print_args (context);
|
|
g_print ("Starting %s\n", cmdline);
|
|
}
|
|
yumctx->process = gs_subprocess_new (context, cancellable, error);
|
|
if (!yumctx->process)
|
|
goto out;
|
|
|
|
yumctx->stdin = (GDataOutputStream*)g_data_output_stream_new (gs_subprocess_get_stdin_pipe (yumctx->process));
|
|
/* yumctx->stdout = (GDataInputStream*)g_data_input_stream_new (gs_subprocess_get_stdout_pipe (yumctx->process)); */
|
|
|
|
success = TRUE;
|
|
out:
|
|
if (!success)
|
|
{
|
|
yum_context_free (yumctx);
|
|
return NULL;
|
|
}
|
|
return yumctx;
|
|
}
|
|
|
|
static gboolean
|
|
yum_context_command (YumContext *yumctx,
|
|
const char *cmd,
|
|
GPtrArray **out_lines,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
gsize bytes_written;
|
|
gs_unref_ptrarray GPtrArray *lines = g_ptr_array_new_with_free_func (g_free);
|
|
gs_free char *cmd_nl = g_strconcat (cmd, "\n", NULL);
|
|
|
|
g_print ("yum> %s", cmd_nl);
|
|
if (!g_output_stream_write_all ((GOutputStream*)yumctx->stdin,
|
|
cmd_nl, strlen (cmd_nl), &bytes_written,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
ret = TRUE;
|
|
gs_transfer_out_value (out_lines, &lines);
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
yuminstall (RpmOstreeTreeComposeContext *self,
|
|
JsonObject *treedata,
|
|
GFile *yumroot,
|
|
GFile *workdir,
|
|
char **packages,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
char **strviter;
|
|
YumContext *yumctx;
|
|
|
|
yumctx = yum_context_new (self, treedata, yumroot, workdir, cancellable, error);
|
|
if (!yumctx)
|
|
goto out;
|
|
|
|
for (strviter = packages; strviter && *strviter; strviter++)
|
|
{
|
|
gs_free char *cmd = NULL;
|
|
const char *package = *strviter;
|
|
gs_unref_ptrarray GPtrArray *lines = NULL;
|
|
|
|
if (g_str_has_prefix (package, "@"))
|
|
cmd = g_strconcat ("group install ", package, NULL);
|
|
else
|
|
cmd = g_strconcat ("install ", package, NULL);
|
|
|
|
if (!yum_context_command (yumctx, cmd, &lines,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
{
|
|
gs_unref_ptrarray GPtrArray *lines = NULL;
|
|
if (!yum_context_command (yumctx, "run", &lines,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
if (!yum_context_close (yumctx, cancellable, error))
|
|
goto out;
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
process_includes (RpmOstreeTreeComposeContext *self,
|
|
GFile *treefile_path,
|
|
guint depth,
|
|
JsonObject *root,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
const char *include_path;
|
|
const guint maxdepth = 50;
|
|
|
|
if (depth > maxdepth)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Exceeded maximum include depth of %u", maxdepth);
|
|
goto out;
|
|
}
|
|
|
|
{
|
|
gs_unref_object GFile *parent = g_file_get_parent (treefile_path);
|
|
gboolean existed = FALSE;
|
|
if (self->treefile_context_dirs->len > 0)
|
|
{
|
|
GFile *prev = self->treefile_context_dirs->pdata[self->treefile_context_dirs->len-1];
|
|
if (g_file_equal (parent, prev))
|
|
existed = TRUE;
|
|
}
|
|
if (!existed)
|
|
{
|
|
g_ptr_array_add (self->treefile_context_dirs, parent);
|
|
parent = NULL; /* Transfer ownership */
|
|
}
|
|
}
|
|
|
|
if (!object_get_optional_string_member (root, "include", &include_path, error))
|
|
goto out;
|
|
|
|
if (include_path)
|
|
{
|
|
gs_unref_object GFile *treefile_dirpath = g_file_get_parent (treefile_path);
|
|
gs_unref_object GFile *parent_path = g_file_resolve_relative_path (treefile_dirpath, include_path);
|
|
gs_unref_object JsonParser *parent_parser = json_parser_new ();
|
|
JsonNode *parent_rootval;
|
|
JsonObject *parent_root;
|
|
GList *members;
|
|
GList *iter;
|
|
|
|
if (!json_parser_load_from_file (parent_parser,
|
|
gs_file_get_path_cached (parent_path),
|
|
error))
|
|
goto out;
|
|
|
|
parent_rootval = json_parser_get_root (parent_parser);
|
|
if (!JSON_NODE_HOLDS_OBJECT (parent_rootval))
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Treefile root is not an object");
|
|
goto out;
|
|
}
|
|
parent_root = json_node_get_object (parent_rootval);
|
|
|
|
if (!process_includes (self, parent_path, depth + 1, parent_root,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
members = json_object_get_members (parent_root);
|
|
for (iter = members; iter; iter = iter->next)
|
|
{
|
|
const char *name = iter->data;
|
|
JsonNode *parent_val = json_object_get_member (parent_root, name);
|
|
JsonNode *val = json_object_get_member (root, name);
|
|
|
|
g_assert (parent_val);
|
|
|
|
if (!val)
|
|
json_object_set_member (root, name, json_node_copy (parent_val));
|
|
else
|
|
{
|
|
JsonNodeType parent_type =
|
|
json_node_get_node_type (parent_val);
|
|
JsonNodeType child_type =
|
|
json_node_get_node_type (val);
|
|
if (parent_type != child_type)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Conflicting element type of '%s'",
|
|
name);
|
|
goto out;
|
|
}
|
|
if (child_type == JSON_NODE_ARRAY)
|
|
{
|
|
JsonArray *parent_array = json_node_get_array (parent_val);
|
|
JsonArray *child_array = json_node_get_array (val);
|
|
JsonArray *new_child = json_array_new ();
|
|
guint i, len;
|
|
|
|
len = json_array_get_length (parent_array);
|
|
for (i = 0; i < len; i++)
|
|
json_array_add_element (new_child, json_node_copy (json_array_get_element (parent_array, i)));
|
|
len = json_array_get_length (child_array);
|
|
for (i = 0; i < len; i++)
|
|
json_array_add_element (new_child, json_node_copy (json_array_get_element (child_array, i)));
|
|
|
|
json_object_set_array_member (root, name, new_child);
|
|
}
|
|
}
|
|
}
|
|
|
|
json_object_remove_member (root, "include");
|
|
}
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static char *
|
|
cachedir_fssafe_key (const char *primary_key)
|
|
{
|
|
GString *ret = g_string_new ("");
|
|
|
|
for (; *primary_key; primary_key++)
|
|
{
|
|
const char c = *primary_key;
|
|
if (!g_ascii_isprint (c) || c == '-')
|
|
g_string_append_printf (ret, "\\%02x", c);
|
|
else if (c == '/')
|
|
g_string_append_c (ret, '-');
|
|
else
|
|
g_string_append_c (ret, c);
|
|
}
|
|
|
|
return g_string_free (ret, FALSE);
|
|
}
|
|
|
|
static GFile *
|
|
cachedir_keypath (GFile *cachedir,
|
|
const char *primary_key)
|
|
{
|
|
gs_free char *fssafe_key = cachedir_fssafe_key (primary_key);
|
|
return g_file_get_child (cachedir, fssafe_key);
|
|
}
|
|
|
|
static gboolean
|
|
cachedir_lookup_string (GFile *cachedir,
|
|
const char *key,
|
|
char **out_value,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
gs_free char *ret_value = NULL;
|
|
|
|
if (cachedir)
|
|
{
|
|
gs_unref_object GFile *keypath = cachedir_keypath (cachedir, key);
|
|
if (!_rpmostree_file_load_contents_utf8_allow_noent (keypath, &ret_value,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
ret = TRUE;
|
|
gs_transfer_out_value (out_value, &ret_value);
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
cachedir_set_string (GFile *cachedir,
|
|
const char *key,
|
|
const char *value,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
gs_unref_object GFile *keypath = NULL;
|
|
|
|
if (!cachedir)
|
|
return TRUE;
|
|
|
|
keypath = cachedir_keypath (cachedir, key);
|
|
if (!g_file_replace_contents (keypath, value, strlen (value), NULL,
|
|
FALSE, 0, NULL,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
ret = TRUE;
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
compute_checksum_for_compose (RpmOstreeTreeComposeContext *self,
|
|
JsonObject *treefile_rootval,
|
|
GFile *yumroot,
|
|
char **out_checksum,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
gs_free char *ret_checksum = NULL;
|
|
GChecksum *checksum = g_checksum_new (G_CHECKSUM_SHA256);
|
|
|
|
{
|
|
gsize len;
|
|
const guint8* buf = g_bytes_get_data (self->serialized_treefile, &len);
|
|
|
|
g_checksum_update (checksum, buf, len);
|
|
}
|
|
|
|
/* Query the generated rpmdb, to see if anything has changed. */
|
|
{
|
|
_cleanup_hysack_ HySack sack = NULL;
|
|
_cleanup_hypackagelist_ HyPackageList pkglist = NULL;
|
|
HyPackage pkg;
|
|
guint i;
|
|
|
|
if (!rpmostree_get_pkglist_for_root (yumroot, &sack, &pkglist,
|
|
cancellable, error))
|
|
{
|
|
g_prefix_error (error, "Reading package set: ");
|
|
goto out;
|
|
}
|
|
|
|
FOR_PACKAGELIST(pkg, pkglist, i)
|
|
{
|
|
gs_free char *nevra = hy_package_get_nevra (pkg);
|
|
g_checksum_update (checksum, (guint8*)nevra, strlen (nevra));
|
|
}
|
|
}
|
|
|
|
ret_checksum = g_strdup (g_checksum_get_string (checksum));
|
|
|
|
ret = TRUE;
|
|
gs_transfer_out_value (out_checksum, &ret_checksum);
|
|
out:
|
|
if (checksum) g_checksum_free (checksum);
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
parse_keyvalue_strings (char **strings,
|
|
GVariant **out_metadata,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
char **iter;
|
|
gs_unref_variant_builder GVariantBuilder *builder = NULL;
|
|
|
|
builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
|
|
|
|
for (iter = strings; *iter; iter++)
|
|
{
|
|
const char *s;
|
|
const char *eq;
|
|
gs_free char *key = NULL;
|
|
|
|
s = *iter;
|
|
|
|
eq = strchr (s, '=');
|
|
if (!eq)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Missing '=' in KEY=VALUE metadata '%s'", s);
|
|
goto out;
|
|
}
|
|
|
|
key = g_strndup (s, eq - s);
|
|
g_variant_builder_add (builder, "{sv}", key,
|
|
g_variant_new_string (eq + 1));
|
|
}
|
|
|
|
ret = TRUE;
|
|
*out_metadata = g_variant_builder_end (builder);
|
|
g_variant_ref_sink (*out_metadata);
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
rpmostree_compose_builtin_tree (int argc,
|
|
char **argv,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
GOptionContext *context = g_option_context_new ("- Run yum and commit the result to an OSTree repository");
|
|
const char *ref;
|
|
RpmOstreeTreeComposeContext selfdata = { NULL, };
|
|
RpmOstreeTreeComposeContext *self = &selfdata;
|
|
JsonNode *treefile_rootval = NULL;
|
|
JsonObject *treefile = NULL;
|
|
JsonArray *units = NULL;
|
|
guint len;
|
|
guint i;
|
|
gs_free char *ref_unix = NULL;
|
|
gs_free char *cachekey = NULL;
|
|
gs_free char *cached_compose_checksum = NULL;
|
|
gs_free char *new_compose_checksum = NULL;
|
|
gs_unref_object GFile *workdir = NULL;
|
|
gs_unref_object GFile *cachedir = NULL;
|
|
gs_unref_object GFile *yumroot = NULL;
|
|
gs_unref_object GFile *targetroot = NULL;
|
|
gs_unref_object GFile *yumroot_varcache = NULL;
|
|
gs_unref_object OstreeRepo *repo = NULL;
|
|
gs_unref_ptrarray GPtrArray *bootstrap_packages = NULL;
|
|
gs_unref_ptrarray GPtrArray *packages = NULL;
|
|
gs_unref_object GFile *treefile_path = NULL;
|
|
gs_unref_object GFile *repo_path = NULL;
|
|
gs_unref_object JsonParser *treefile_parser = NULL;
|
|
gs_unref_variant GVariant *metadata = NULL;
|
|
gboolean workdir_is_tmp = FALSE;
|
|
|
|
self->treefile_context_dirs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref);
|
|
|
|
g_option_context_add_main_entries (context, option_entries, NULL);
|
|
|
|
if (!g_option_context_parse (context, &argc, &argv, error))
|
|
goto out;
|
|
|
|
if (argc < 2)
|
|
{
|
|
g_printerr ("usage: rpm-ostree compose tree TREEFILE\n");
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Option processing failed");
|
|
goto out;
|
|
}
|
|
|
|
if (!opt_repo)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"--repo must be specified");
|
|
goto out;
|
|
}
|
|
|
|
repo_path = g_file_new_for_path (opt_repo);
|
|
repo = ostree_repo_new (repo_path);
|
|
if (!ostree_repo_open (repo, cancellable, error))
|
|
goto out;
|
|
|
|
treefile_path = g_file_new_for_path (argv[1]);
|
|
|
|
if (opt_workdir)
|
|
{
|
|
workdir = g_file_new_for_path (opt_workdir);
|
|
}
|
|
else
|
|
{
|
|
gs_free char *tmpd = g_mkdtemp (g_strdup ("/var/tmp/rpm-ostree.XXXXXX"));
|
|
workdir = g_file_new_for_path (tmpd);
|
|
workdir_is_tmp = TRUE;
|
|
|
|
if (opt_workdir_tmpfs)
|
|
{
|
|
/* Use a private mount namespace to avoid polluting the global
|
|
* namespace, and to ensure the mount gets cleaned up if we exit
|
|
* unexpectedly.
|
|
*/
|
|
if (unshare (CLONE_NEWNS) != 0)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"unshare(CLONE_NEWNS): %s", g_strerror (errno));
|
|
goto out;
|
|
}
|
|
if (mount (NULL, "/", "none", MS_PRIVATE | MS_REC, NULL) == -1)
|
|
{
|
|
int errsv = errno;
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mount(/, MS_PRIVATE | MS_REC): %s",
|
|
g_strerror (errsv));
|
|
goto out;
|
|
}
|
|
|
|
if (mount ("tmpfs", tmpd, "tmpfs", 0, (const void*)"mode=755") != 0)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mount(tmpfs): %s", g_strerror (errno));
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opt_cachedir)
|
|
{
|
|
cachedir = g_file_new_for_path (opt_cachedir);
|
|
if (!gs_file_ensure_directory (cachedir, FALSE, cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
if (opt_metadata_strings)
|
|
{
|
|
if (!parse_keyvalue_strings (opt_metadata_strings,
|
|
&metadata, error))
|
|
goto out;
|
|
}
|
|
|
|
if (chdir (gs_file_get_path_cached (workdir)) != 0)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Failed to chdir to '%s': %s",
|
|
opt_workdir, strerror (errno));
|
|
goto out;
|
|
}
|
|
|
|
treefile_parser = json_parser_new ();
|
|
if (!json_parser_load_from_file (treefile_parser,
|
|
gs_file_get_path_cached (treefile_path),
|
|
error))
|
|
goto out;
|
|
|
|
treefile_rootval = json_parser_get_root (treefile_parser);
|
|
if (!JSON_NODE_HOLDS_OBJECT (treefile_rootval))
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Treefile root is not an object");
|
|
goto out;
|
|
}
|
|
treefile = json_node_get_object (treefile_rootval);
|
|
|
|
if (!process_includes (self, treefile_path, 0, treefile,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
if (opt_print_only)
|
|
{
|
|
gs_unref_object JsonGenerator *generator = json_generator_new ();
|
|
gs_unref_object GOutputStream *stdout = g_unix_output_stream_new (1, FALSE);
|
|
|
|
json_generator_set_pretty (generator, TRUE);
|
|
json_generator_set_root (generator, treefile_rootval);
|
|
(void) json_generator_to_stream (generator, stdout, NULL, NULL);
|
|
|
|
ret = TRUE;
|
|
goto out;
|
|
}
|
|
|
|
yumroot = g_file_get_child (workdir, "rootfs.tmp");
|
|
if (!gs_shutil_rm_rf (yumroot, cancellable, error))
|
|
goto out;
|
|
targetroot = g_file_get_child (workdir, "rootfs");
|
|
|
|
ref = object_require_string_member (treefile, "ref", error);
|
|
if (!ref)
|
|
goto out;
|
|
|
|
ref_unix = g_strdelimit (g_strdup (ref), "/", '_');
|
|
|
|
bootstrap_packages = g_ptr_array_new ();
|
|
packages = g_ptr_array_new ();
|
|
|
|
if (!append_string_array_to (treefile, treefile_path, "bootstrap_packages", packages,
|
|
cancellable, error))
|
|
goto out;
|
|
if (!append_string_array_to (treefile, treefile_path, "packages", packages,
|
|
cancellable, error))
|
|
goto out;
|
|
g_ptr_array_add (packages, NULL);
|
|
|
|
{
|
|
gs_unref_object JsonGenerator *generator = json_generator_new ();
|
|
char *treefile_buf = NULL;
|
|
gsize len;
|
|
|
|
json_generator_set_root (generator, treefile_rootval);
|
|
json_generator_set_pretty (generator, TRUE);
|
|
treefile_buf = json_generator_to_data (generator, &len);
|
|
|
|
self->serialized_treefile = g_bytes_new_take (treefile_buf, len);
|
|
}
|
|
|
|
if (!yuminstall (self, treefile, yumroot, workdir,
|
|
(char**)packages->pdata,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
cachekey = g_strconcat ("treecompose/", ref, NULL);
|
|
if (!cachedir_lookup_string (cachedir, cachekey,
|
|
&cached_compose_checksum,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
if (!compute_checksum_for_compose (self, treefile, yumroot,
|
|
&new_compose_checksum,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
if (g_strcmp0 (cached_compose_checksum, new_compose_checksum) == 0)
|
|
{
|
|
g_print ("No changes to input, reusing cached commit\n");
|
|
ret = TRUE;
|
|
goto out;
|
|
}
|
|
|
|
ref_unix = g_strdelimit (g_strdup (ref), "/", '_');
|
|
|
|
if (g_strcmp0 (g_getenv ("RPM_OSTREE_BREAK"), "post-yum") == 0)
|
|
goto out;
|
|
|
|
{
|
|
const char *boot_location_str = NULL;
|
|
RpmOstreePostprocessBootLocation boot_location =
|
|
RPMOSTREE_POSTPROCESS_BOOT_LOCATION_BOTH;
|
|
|
|
if (!object_get_optional_string_member (treefile, "boot_location",
|
|
&boot_location_str, error))
|
|
goto out;
|
|
|
|
if (boot_location_str != NULL)
|
|
{
|
|
if (strcmp (boot_location_str, "legacy") == 0)
|
|
boot_location = RPMOSTREE_POSTPROCESS_BOOT_LOCATION_LEGACY;
|
|
else if (strcmp (boot_location_str, "both") == 0)
|
|
boot_location = RPMOSTREE_POSTPROCESS_BOOT_LOCATION_BOTH;
|
|
else if (strcmp (boot_location_str, "new") == 0)
|
|
boot_location = RPMOSTREE_POSTPROCESS_BOOT_LOCATION_NEW;
|
|
else
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Invalid boot location '%s'", boot_location_str);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (!rpmostree_postprocess (yumroot, boot_location, cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
if (json_object_has_member (treefile, "units"))
|
|
units = json_object_get_array_member (treefile, "units");
|
|
|
|
if (units)
|
|
len = json_array_get_length (units);
|
|
else
|
|
len = 0;
|
|
|
|
{
|
|
gs_unref_object GFile *multiuser_wants_dir =
|
|
g_file_resolve_relative_path (yumroot, "usr/etc/systemd/system/multi-user.target.wants");
|
|
|
|
if (!gs_file_ensure_directory (multiuser_wants_dir, TRUE, cancellable, error))
|
|
goto out;
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
const char *unitname = array_require_string_element (units, i, error);
|
|
gs_unref_object GFile *unit_link_target = NULL;
|
|
gs_free char *symlink_target = NULL;
|
|
|
|
if (!unitname)
|
|
goto out;
|
|
|
|
symlink_target = g_strconcat ("/usr/lib/systemd/system/", unitname, NULL);
|
|
unit_link_target = g_file_get_child (multiuser_wants_dir, unitname);
|
|
|
|
if (g_file_query_file_type (unit_link_target, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_SYMBOLIC_LINK)
|
|
continue;
|
|
|
|
g_print ("Adding %s to multi-user.target.wants\n", unitname);
|
|
|
|
if (!g_file_make_symbolic_link (unit_link_target, symlink_target,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
{
|
|
gs_unref_object GFile *target_treefile_dir_path =
|
|
g_file_resolve_relative_path (yumroot, "usr/share/rpm-ostree");
|
|
gs_unref_object GFile *target_treefile_path =
|
|
g_file_get_child (target_treefile_dir_path, "treefile.json");
|
|
|
|
if (!gs_file_ensure_directory (target_treefile_dir_path, TRUE,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
g_print ("Copying '%s' to '%s'\n",
|
|
gs_file_get_path_cached (treefile_path),
|
|
gs_file_get_path_cached (target_treefile_path));
|
|
{
|
|
gsize len;
|
|
const guint8 *buf = g_bytes_get_data (self->serialized_treefile, &len);
|
|
|
|
if (!g_file_replace_contents (target_treefile_path, (char*)buf, len,
|
|
NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION,
|
|
NULL, cancellable, error))
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
{
|
|
const char *default_target = NULL;
|
|
|
|
if (!object_get_optional_string_member (treefile, "default_target",
|
|
&default_target, error))
|
|
goto out;
|
|
|
|
if (default_target != NULL)
|
|
{
|
|
gs_unref_object GFile *default_target_path =
|
|
g_file_resolve_relative_path (yumroot, "usr/etc/systemd/system/default.target");
|
|
gs_free char *dest_default_target_path =
|
|
g_strconcat ("/usr/lib/systemd/system/", default_target, NULL);
|
|
|
|
(void) gs_file_unlink (default_target_path, NULL, NULL);
|
|
|
|
if (!g_file_make_symbolic_link (default_target_path, dest_default_target_path,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
{
|
|
JsonArray *remove = NULL;
|
|
|
|
if (json_object_has_member (treefile, "remove-files"))
|
|
{
|
|
remove = json_object_get_array_member (treefile, "remove-files");
|
|
len = json_array_get_length (remove);
|
|
}
|
|
else
|
|
len = 0;
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
const char *val = array_require_string_element (remove, i, error);
|
|
gs_unref_object GFile *child = NULL;
|
|
|
|
if (!val)
|
|
return FALSE;
|
|
if (g_path_is_absolute (val))
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"'remove' elements must be relative");
|
|
goto out;
|
|
}
|
|
|
|
child = g_file_resolve_relative_path (yumroot, val);
|
|
|
|
if (g_file_query_exists (child, NULL))
|
|
{
|
|
g_print ("Removing '%s'\n", val);
|
|
if (!gs_shutil_rm_rf (child, cancellable, error))
|
|
goto out;
|
|
}
|
|
else
|
|
{
|
|
g_printerr ("warning: Targeted path for remove-files does not exist: %s\n",
|
|
gs_file_get_path_cached (child));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
{
|
|
const char *gpgkey;
|
|
if (!object_get_optional_string_member (treefile, "gpg_key", &gpgkey, error))
|
|
goto out;
|
|
|
|
if (!rpmostree_commit (yumroot, repo, ref, metadata, gpgkey,
|
|
json_object_get_boolean_member (treefile, "selinux"),
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
|
|
if (!cachedir_set_string (cachedir, cachekey,
|
|
new_compose_checksum,
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
g_print ("Complete\n");
|
|
|
|
out:
|
|
|
|
if (workdir_is_tmp)
|
|
{
|
|
if (opt_workdir_tmpfs)
|
|
(void) umount (gs_file_get_path_cached (workdir));
|
|
(void) gs_shutil_rm_rf (workdir, NULL, NULL);
|
|
}
|
|
if (self)
|
|
{
|
|
g_clear_pointer (&self->serialized_treefile, g_bytes_unref);
|
|
g_ptr_array_unref (self->treefile_context_dirs);
|
|
}
|
|
return ret;
|
|
}
|