diff --git a/Makefile-ostree.am b/Makefile-ostree.am
index fb377075eb..bd4d456ebf 100644
--- a/Makefile-ostree.am
+++ b/Makefile-ostree.am
@@ -27,6 +27,7 @@ ostree_SOURCES = src/ostree/main.c \
src/ostree/ot-builtin-checkout.c \
src/ostree/ot-builtin-checksum.c \
src/ostree/ot-builtin-commit.c \
+ src/ostree/ot-builtin-composefs.c \
src/ostree/ot-builtin-create-usb.c \
src/ostree/ot-builtin-diff.c \
src/ostree/ot-builtin-export.c \
diff --git a/bash/ostree b/bash/ostree
index 46363315c4..1d50ff3dc7 100644
--- a/bash/ostree
+++ b/bash/ostree
@@ -292,6 +292,55 @@ _ostree_checkout() {
return 0
}
+_ostree_composefs_generate() {
+ local boolean_options="
+ $main_boolean_options
+ --from-stdin
+ --union-add
+ --union-identical
+ --whiteouts
+ "
+
+ local options_with_args="
+ --from-file
+ --repo
+ --subpath
+ "
+
+ local options_with_args_glob=$( __ostree_to_extglob "$options_with_args" )
+
+ case "$prev" in
+ --from-file)
+ __ostree_compreply_all_files
+ return 0
+ ;;
+ --repo|--subpath)
+ __ostree_compreply_dirs_only
+ return 0
+ ;;
+ $options_with_args_glob )
+ return 0
+ ;;
+ esac
+
+ case "$cur" in
+ -*)
+ local all_options="$boolean_options $options_with_args"
+ __ostree_compreply_all_options
+ ;;
+ *)
+ local argpos=$( __ostree_pos_first_nonflag $( __ostree_to_alternatives "$options_with_args" ) )
+
+ if [ $cword -eq $argpos ]; then
+ __ostree_compreply_commits
+ elif [ $cword -eq $(($argpos + 1)) ]; then
+ __ostree_compreply_all_files
+ fi
+ esac
+
+ return 0
+}
+
_ostree_checksum() {
local boolean_options="
$main_boolean_options
@@ -1852,6 +1901,7 @@ _ostree() {
checkout
checksum
commit
+ composefs-generate
config
create-usb
diff
diff --git a/man/ostree-composefs-generate.xml b/man/ostree-composefs-generate.xml
new file mode 100644
index 0000000000..f5fef63241
--- /dev/null
+++ b/man/ostree-composefs-generate.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+ ostree composefs-generate
+ OSTree
+
+
+
+ Developer
+ Colin
+ Walters
+ walters@verbum.org
+
+
+
+
+
+ ostree composefs-generate
+ 1
+
+
+
+ ostree-composefs-generate
+ Create a composefs image out of commits
+
+
+
+
+ ostree composefs-generate OPTIONS COMMIT DESTINATION
+
+
+
+
+ Description
+
+
+ Creates a composefs image out the given commit(s) into the filesystem under directory DESTINATION. If DESTINATION is not specified, the COMMIT will become the destination checkout target.
+
+
+
+
+ Options
+
+
+
+
+ ="PATH"
+
+
+ Checkout sub-directory PATH.
+
+
+
+
+
+
+
+ When combining multiple commits, keep existing files. The default is to overwrite existing files.
+
+
+
+
+
+
+ When combining multiple commits, error out
+ if a file would be replaced with a different file. Add new files
+ and directories, ignore identical files, and keep existing
+ directories.
+
+
+
+
+
+
+ Process whiteout files (Docker style).
+
+
+
+
+
+
+
+ Process many checkouts from standard input.
+
+
+
+
+ ="FILE"
+
+
+ Process many checkouts from input file.
+
+
+
+
+
+
+
+ Example
+ $ ostree composefs-generate --repo=/my/repo my-branch image.cfs
+
+
diff --git a/src/ostree/main.c b/src/ostree/main.c
index 7d17080cf4..bcbfb0af40 100644
--- a/src/ostree/main.c
+++ b/src/ostree/main.c
@@ -51,6 +51,9 @@ static OstreeCommand commands[] = {
{ "commit", OSTREE_BUILTIN_FLAG_NONE,
ostree_builtin_commit,
"Commit a new revision" },
+ { "composefs-generate", OSTREE_BUILTIN_FLAG_NONE,
+ ostree_builtin_composefs_generate,
+ "Create a composefs image from a commit" },
{ "config", OSTREE_BUILTIN_FLAG_NONE,
ostree_builtin_config,
"Change repo configuration settings" },
diff --git a/src/ostree/ot-builtin-composefs.c b/src/ostree/ot-builtin-composefs.c
new file mode 100644
index 0000000000..efb406307c
--- /dev/null
+++ b/src/ostree/ot-builtin-composefs.c
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022 Alexander Larsson
+ *
+ * SPDX-License-Identifier: LGPL-2.0+
+ *
+ * This library 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 License, 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, see .
+ *
+ * Author: Alexander Larsson
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "ot-main.h"
+#include "ot-builtins.h"
+#include "ostree.h"
+#include "otutil.h"
+
+static char *opt_subpath;
+static gboolean opt_union_add;
+static gboolean opt_union_identical;
+static gboolean opt_whiteouts;
+static gboolean opt_from_stdin;
+static gboolean opt_fsverity;
+static char *opt_from_file;
+
+/* ATTENTION:
+ * Please remember to update the bash-completion script (bash/ostree) and
+ * man page (man/ostree-composefs-generate.xml) when changing the option list.
+ */
+
+static GOptionEntry options[] = {
+ { "subpath", 0, 0, G_OPTION_ARG_FILENAME, &opt_subpath, "Process sub-directory PATH", "PATH" },
+ { "fsverity", 0, 0, G_OPTION_ARG_NONE, &opt_fsverity, "Add fsverity digests for files", NULL },
+ { "union-add", 0, 0, G_OPTION_ARG_NONE, &opt_union_add, "Keep existing files/directories, only add new", NULL },
+ { "union-identical", 0, 0, G_OPTION_ARG_NONE, &opt_union_identical, "When layering checkouts, error out if a file would be replaced with a different version, but add new files and directories", NULL },
+ { "whiteouts", 0, 0, G_OPTION_ARG_NONE, &opt_whiteouts, "Process 'whiteout' (Docker style) entries", NULL },
+ { "from-stdin", 0, 0, G_OPTION_ARG_NONE, &opt_from_stdin, "Process many checkouts from standard input", NULL },
+ { "from-file", 0, 0, G_OPTION_ARG_STRING, &opt_from_file, "Process many checkouts from input file", "FILE" },
+ { NULL }
+};
+
+static gboolean
+process_many_commits (OstreeRepo *repo,
+ OstreeRepoCheckoutComposefsOptions *checkout_options,
+ OstreeComposefsTarget *composefs,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret = FALSE;
+ gsize len;
+ GError *temp_error = NULL;
+ g_autoptr(GInputStream) instream = NULL;
+ g_autoptr(GDataInputStream) datastream = NULL;
+ g_autofree char *revision = NULL;
+ g_autofree char *subpath = NULL;
+ g_autofree char *resolved_commit = NULL;
+
+ if (opt_from_stdin)
+ {
+ instream = (GInputStream*)g_unix_input_stream_new (0, FALSE);
+ }
+ else
+ {
+ g_autoptr(GFile) f = g_file_new_for_path (opt_from_file);
+
+ instream = (GInputStream*)g_file_read (f, cancellable, error);
+ if (!instream)
+ goto out;
+ }
+
+ datastream = g_data_input_stream_new (instream);
+
+ while ((revision = g_data_input_stream_read_upto (datastream, "", 1, &len,
+ cancellable, &temp_error)) != NULL)
+ {
+ if (revision[0] == '\0')
+ break;
+
+ /* Read the null byte */
+ (void) g_data_input_stream_read_byte (datastream, cancellable, NULL);
+ g_free (subpath);
+ subpath = g_data_input_stream_read_upto (datastream, "", 1, &len,
+ cancellable, &temp_error);
+ if (temp_error)
+ {
+ g_propagate_error (error, temp_error);
+ goto out;
+ }
+
+ /* Read the null byte */
+ (void) g_data_input_stream_read_byte (datastream, cancellable, NULL);
+
+ if (!ostree_repo_resolve_rev (repo, revision, FALSE, &resolved_commit, error))
+ goto out;
+
+ checkout_options->subpath = subpath;
+ if (!ostree_repo_checkout_composefs (repo, checkout_options, composefs,
+ resolved_commit, cancellable, error))
+ {
+ g_prefix_error (error, "Processing tree %s: ", resolved_commit);
+ goto out;
+ }
+
+ g_free (revision);
+ }
+
+ if (temp_error)
+ {
+ g_propagate_error (error, temp_error);
+ goto out;
+ }
+
+ ret = TRUE;
+ out:
+ return ret;
+}
+
+gboolean
+ostree_builtin_composefs_generate (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
+{
+ g_autoptr(GOptionContext) context = g_option_context_new ("COMMIT [DESTINATION]");
+ g_autoptr(OstreeRepo) repo = NULL;
+ g_autoptr(OstreeComposefsTarget) composefs = ostree_composefs_target_new ();
+ const char *destination;
+ OstreeRepoCheckoutComposefsOptions checkout_options = { 0 };
+
+ if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable, error))
+ return FALSE;
+
+ if (argc < 2)
+ {
+ g_autofree char *help = g_option_context_get_help (context, TRUE, NULL);
+ g_printerr ("%s\n", help);
+ return glnx_throw (error, "COMMIT must be specified");
+ }
+
+ if (opt_union_add && opt_union_identical)
+ return glnx_throw (error, "Cannot specify both --union-add and --union-identical");
+ if (opt_union_add)
+ checkout_options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_ADD_FILES;
+ else if (opt_union_identical)
+ checkout_options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_IDENTICAL;
+ else
+ checkout_options.overwrite_mode = OSTREE_REPO_CHECKOUT_OVERWRITE_UNION_FILES;
+
+
+ if (opt_whiteouts)
+ checkout_options.process_whiteouts = TRUE;
+
+ if (opt_subpath)
+ checkout_options.subpath = opt_subpath;
+
+ if (opt_fsverity)
+ checkout_options.enable_fsverity = TRUE;
+
+ if (opt_from_stdin || opt_from_file)
+ {
+ destination = argv[1];
+
+ if (!process_many_commits (repo, &checkout_options, composefs, cancellable, error))
+ return FALSE;
+ }
+ else
+ {
+ const char *commit = argv[1];
+ if (argc < 3)
+ destination = commit;
+ else
+ destination = argv[2];
+
+ g_autofree char *resolved_commit = NULL;
+ if (!ostree_repo_resolve_rev (repo, commit, FALSE, &resolved_commit, error))
+ return FALSE;
+
+ if (!ostree_repo_checkout_composefs (repo, &checkout_options, composefs,
+ resolved_commit, cancellable, error))
+ return FALSE;
+ }
+
+ if (!ostree_composefs_target_write_at (composefs,
+ AT_FDCWD, destination, NULL,
+ cancellable, error))
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/src/ostree/ot-builtins.h b/src/ostree/ot-builtins.h
index 286c2e998e..603cf5688d 100644
--- a/src/ostree/ot-builtins.h
+++ b/src/ostree/ot-builtins.h
@@ -35,6 +35,7 @@ BUILTINPROTO(config);
BUILTINPROTO(checkout);
BUILTINPROTO(checksum);
BUILTINPROTO(commit);
+BUILTINPROTO(composefs_generate);
BUILTINPROTO(diff);
BUILTINPROTO(export);
BUILTINPROTO(find_remotes);