15ecaacd36
Having content in /boot in OSTree was always ugly, because we ended up mounting over it in the deployment location at boot. This was even worse in the anaconda rpmostreepayload code, because of the juggling of the mount point that needed to take place. Trying to add a GRUB2 backend to OSTree is what finally forced this change. Now, we put kernels (in the tree) by default in *both* /boot and /usr/lib/ostree-boot. OSTree itself knows to look in both locations. Anaconda is going to just hard require trees with the new location though.
1094 lines
34 KiB
C
1094 lines
34 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-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_repo;
|
|
static char **opt_override_pkg_repos;
|
|
static gboolean opt_print_only;
|
|
|
|
static GOptionEntry option_entries[] = {
|
|
{ "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 },
|
|
{ "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,
|
|
const char *member_name,
|
|
GPtrArray *array,
|
|
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);
|
|
|
|
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. */
|
|
{
|
|
int estatus;
|
|
gs_free char *yumroot_var_lib_rpm =
|
|
g_build_filename (gs_file_get_path_cached (yumroot),
|
|
"var/lib/rpm",
|
|
NULL);
|
|
const char *rpmqa_argv[] = { PKGLIBDIR "/rpmqa-sorted-and-clean",
|
|
yumroot_var_lib_rpm,
|
|
NULL };
|
|
gs_free char *rpmqa_result = NULL;
|
|
|
|
if (!g_spawn_sync (NULL, (char**)rpmqa_argv, NULL,
|
|
G_SPAWN_SEARCH_PATH, NULL, NULL,
|
|
&rpmqa_result, NULL, &estatus, error))
|
|
goto out;
|
|
if (!g_spawn_check_exit_status (estatus, error))
|
|
{
|
|
g_prefix_error (error, "Executing %s: ",
|
|
rpmqa_argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
if (!*rpmqa_result)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Empty result from %s", rpmqa_argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
g_checksum_update (checksum, (guint8*)rpmqa_result, strlen (rpmqa_result));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
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 (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, "bootstrap_packages", packages, error))
|
|
goto out;
|
|
if (!append_string_array_to (treefile, "packages", packages, 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;
|
|
}
|
|
}
|
|
|
|
{
|
|
const char *gpgkey;
|
|
if (!object_get_optional_string_member (treefile, "gpg_key", &gpgkey, error))
|
|
goto out;
|
|
|
|
if (!rpmostree_commit (yumroot, repo, ref, 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;
|
|
}
|