diff --git a/docs/treefile.md b/docs/treefile.md index 1103d1529a..f9b5e77465 100644 --- a/docs/treefile.md +++ b/docs/treefile.md @@ -78,6 +78,15 @@ It supports the following parameters: * `packages`: Array of strings, required: List of packages to install. * `repo`: String, required: Name of the repo from which to fetch packages. + * `modules`: Object, optional: Describes RPM modules to enable or install. Two + keys are supported: + * `enable`: Array of strings, required: Set of RPM module specs to enable + (the same formats as dnf are supported, e.g. `NAME[:STREAM]`). + One can then cherry-pick specific packages from the enabled modules via + `packages`. + * `install`: Array of strings, required: Set of RPM module specs to install + (the same formats as dnf are supported, e.g. `NAME[:STREAM][/PROFILE]`). + * `ostree-layers`: Array of strings, optional: After all packages are unpacked, check out these OSTree refs, which must already be in the destination repository. Any conflicts with packages will be an error. diff --git a/libdnf b/libdnf index fefe0b69e8..5d2579148a 160000 --- a/libdnf +++ b/libdnf @@ -1 +1 @@ -Subproject commit fefe0b69e8368a8e9ceeed107add174c64f00f5e +Subproject commit 5d2579148a4296b83432c1ba35b79964edd937eb diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ae858223ad..85cebd8db5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -546,6 +546,7 @@ mod lockfile; pub(crate) use self::lockfile::*; mod live; pub(crate) use self::live::*; +pub mod modularity; mod nameservice; // An origin parser in Rust but only built when testing until // we're ready to try porting the C++ code. diff --git a/rust/src/main.rs b/rust/src/main.rs index 9c2eb523ab..89508e4ca5 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -8,6 +8,7 @@ fn inner_main(args: &Vec<&str>) -> Result<()> { // Add custom Rust commands here, and also in `libmain.cxx` if user-visible. Some("countme") => rpmostree_rust::countme::entrypoint(args), Some("ex-container") => rpmostree_rust::container::entrypoint(args), + Some("module") => rpmostree_rust::modularity::entrypoint(args), _ => { // Otherwise fall through to C++ main(). Ok(rpmostree_rust::ffi::rpmostree_main(&args)?) diff --git a/rust/src/modularity.rs b/rust/src/modularity.rs new file mode 100644 index 0000000000..ec6fbe04d3 --- /dev/null +++ b/rust/src/modularity.rs @@ -0,0 +1,127 @@ +//! Implementation of the client-side of "rpm-ostree module". + +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use anyhow::{anyhow, bail, Result}; +use gio::DBusProxyExt; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "rpm-ostree module", no_version)] +#[structopt(rename_all = "kebab-case")] +enum Opt { + /// Enable a module + Enable(InstallOpts), + /// Disable a module + Disable(InstallOpts), + /// Install a module + Install(InstallOpts), + /// Uninstall a module + Uninstall(InstallOpts), +} + +#[derive(Debug, StructOpt)] +struct InstallOpts { + #[structopt(parse(from_str))] + modules: Vec, + #[structopt(long)] + reboot: bool, + #[structopt(long)] + lock_finalization: bool, + #[structopt(long)] + dry_run: bool, +} + +const OPT_KEY_ENABLE_MODULES: &str = "enable-modules"; +const OPT_KEY_DISABLE_MODULES: &str = "disable-modules"; +const OPT_KEY_INSTALL_MODULES: &str = "install-modules"; +const OPT_KEY_UNINSTALL_MODULES: &str = "uninstall-modules"; + +pub fn entrypoint(args: &[&str]) -> Result<()> { + match Opt::from_iter(args.iter().skip(1)) { + Opt::Enable(ref opts) => enable(opts), + Opt::Disable(ref opts) => disable(opts), + Opt::Install(ref opts) => install(opts), + Opt::Uninstall(ref opts) => uninstall(opts), + } +} + +// XXX: Should split out a lot of the below into a more generic Rust wrapper around +// UpdateDeployment() like we have on the C side. + +fn get_modifiers_variant(key: &str, modules: &[String]) -> Result { + let r = glib::VariantDict::new(None); + r.insert_value(key, &crate::variant_utils::new_variant_strv(modules)); + Ok(r.end()) +} + +fn get_options_variant(opts: &InstallOpts) -> Result { + let r = glib::VariantDict::new(None); + r.insert("no-pull-base", &true); + r.insert("reboot", &opts.reboot); + r.insert("lock-finalization", &opts.lock_finalization); + r.insert("dry-run", &opts.dry_run); + Ok(r.end()) +} + +fn enable(opts: &InstallOpts) -> Result<()> { + modules_impl(OPT_KEY_ENABLE_MODULES, opts) +} + +fn disable(opts: &InstallOpts) -> Result<()> { + modules_impl(OPT_KEY_DISABLE_MODULES, opts) +} + +fn install(opts: &InstallOpts) -> Result<()> { + modules_impl(OPT_KEY_INSTALL_MODULES, opts) +} + +fn uninstall(opts: &InstallOpts) -> Result<()> { + modules_impl(OPT_KEY_UNINSTALL_MODULES, opts) +} + +fn modules_impl(key: &str, opts: &InstallOpts) -> Result<()> { + if opts.modules.is_empty() { + bail!("At least one module must be specified"); + } + + let client = &mut crate::client::ClientConnection::new()?; + let previous_deployment = client + .get_os_proxy() + .get_cached_property("DefaultDeployment") + .ok_or_else(|| anyhow!("Failed to find default-deployment property"))?; + let modifiers = get_modifiers_variant(key, &opts.modules)?; + let options = get_options_variant(opts)?; + let params = crate::variant_utils::new_variant_tuple(&[modifiers, options]); + let reply = &client.get_os_proxy().call_sync( + "UpdateDeployment", + Some(¶ms), + gio::DBusCallFlags::NONE, + -1, + gio::NONE_CANCELLABLE, + )?; + let reply_child = crate::variant_utils::variant_tuple_get(reply, 0) + .ok_or_else(|| anyhow!("Invalid reply"))?; + let txn_address = reply_child + .get_str() + .ok_or_else(|| anyhow!("Expected string transaction address"))?; + client.transaction_connect_progress_sync(txn_address)?; + let new_deployment = client + .get_os_proxy() + .get_cached_property("DefaultDeployment") + .ok_or_else(|| anyhow!("Failed to find default-deployment property"))?; + if previous_deployment != new_deployment { + unsafe { + crate::ffi::print_treepkg_diff_from_sysroot_path( + "/", + crate::ffi::RpmOstreeDiffPrintFormat::RPMOSTREE_DIFF_PRINT_FORMAT_FULL_MULTILINE, + 0, + std::ptr::null_mut(), + ); + } + } + if opts.dry_run { + println!("Exiting because of '--dry-run' option"); + } + Ok(()) +} diff --git a/rust/src/treefile.rs b/rust/src/treefile.rs index 386ec46081..31419747fa 100644 --- a/rust/src/treefile.rs +++ b/rust/src/treefile.rs @@ -136,6 +136,17 @@ fn treefile_parse_stream( } } + // to be consistent, we also support whitespace-separated modules + if let Some(mut modules) = treefile.modules.take() { + if let Some(enable) = modules.enable.take() { + modules.enable = Some(whitespace_split_packages(&enable)?); + } + if let Some(install) = modules.install.take() { + modules.install = Some(whitespace_split_packages(&install)?); + } + treefile.modules = Some(modules); + } + if let Some(repo_packages) = treefile.repo_packages.take() { treefile.repo_packages = Some( repo_packages @@ -320,6 +331,18 @@ fn merge_hashset_field( } } +/// Merge modules fields. +fn merge_modules(dest: &mut Option, src: &mut Option) { + if let Some(mut srcv) = src.take() { + if let Some(mut destv) = dest.take() { + merge_vec_field(&mut destv.enable, &mut srcv.enable); + merge_vec_field(&mut destv.install, &mut srcv.install); + srcv = destv; + } + *dest = Some(srcv); + } +} + /// Given two configs, merge them. fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) { macro_rules! merge_basics { @@ -389,6 +412,8 @@ fn treefile_merge(dest: &mut TreeComposeConfig, src: &mut TreeComposeConfig) { remove_from_packages, repo_packages ); + + merge_modules(&mut dest.modules, &mut src.modules); } /// Merge the treefile externals. There are currently only two keys that @@ -1094,6 +1119,8 @@ pub(crate) struct TreeComposeConfig { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "repo-packages")] pub(crate) repo_packages: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) modules: Option, // Deprecated option #[serde(skip_serializing_if = "Option::is_none")] pub(crate) bootstrap_packages: Option>, @@ -1218,6 +1245,14 @@ pub(crate) struct RepoPackage { pub(crate) packages: Vec, } +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub(crate) struct ModulesConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) enable: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) install: Option>, +} + #[derive(Serialize, Deserialize, Debug, Default)] pub(crate) struct LegacyTreeComposeConfigFields { #[serde(skip_serializing)] @@ -1351,6 +1386,12 @@ pub(crate) mod tests { - repo: baserepo packages: - blah bloo + modules: + enable: + - foobar:2.0 + install: + - nodejs:15 + - swig:3.0/complete sway:rolling "#}; // This one has "comments" (hence unknown keys) @@ -1616,6 +1657,11 @@ pub(crate) mod tests { - repo: foo2 packages: - qwert + modules: + enable: + - dodo + install: + - bazboo "}, )?; let mut buf = VALID_PRELUDE.to_string(); @@ -1637,6 +1683,18 @@ pub(crate) mod tests { } ]) ); + assert_eq!( + tf.parsed.modules, + Some(ModulesConfig { + enable: Some(vec!["dodo".into(), "foobar:2.0".into()]), + install: Some(vec![ + "bazboo".into(), + "nodejs:15".into(), + "swig:3.0/complete".into(), + "sway:rolling".into(), + ]) + },) + ); Ok(()) } diff --git a/rust/src/variant_utils.rs b/rust/src/variant_utils.rs index 32ea234d68..426f86d5b2 100644 --- a/rust/src/variant_utils.rs +++ b/rust/src/variant_utils.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use glib::translate::*; +use glib::ToVariant; // These constants should really be in gtk-rs lazy_static::lazy_static! { @@ -51,6 +52,11 @@ pub(crate) fn new_variant_array(ty: &glib::VariantTy, children: &[glib::Variant] } } +pub(crate) fn new_variant_strv(strv: &[impl AsRef]) -> glib::Variant { + let v: Vec = strv.iter().map(|s| s.as_ref().to_variant()).collect(); + new_variant_array(&TY_S, &v) +} + pub(crate) fn is_container(v: &glib::Variant) -> bool { unsafe { glib_sys::g_variant_is_container(v.to_glib_none().0) != glib_sys::GFALSE } } diff --git a/src/app/libmain.cxx b/src/app/libmain.cxx index 61d17aa149..bf28f83e7b 100644 --- a/src/app/libmain.cxx +++ b/src/app/libmain.cxx @@ -103,10 +103,8 @@ static RpmOstreeCommand commands[] = { /* Rust-implemented commands; they're here so that they show up in `rpm-ostree * --help` alongside the other commands, but the command itself is fully * handled Rust side. */ - /* - { "my-rust-command", static_cast(0), - "Cool thing my command does", NULL }, - */ + { "module", static_cast(0), + "Commands to install/uninstall modules", NULL }, /* Legacy aliases */ { "pkg-add", static_cast(RPM_OSTREE_BUILTIN_FLAG_HIDDEN), NULL, rpmostree_builtin_install }, diff --git a/src/app/rpmostree-builtin-status.cxx b/src/app/rpmostree-builtin-status.cxx index 5e217a6787..cad0e8e13b 100644 --- a/src/app/rpmostree-builtin-status.cxx +++ b/src/app/rpmostree-builtin-status.cxx @@ -573,7 +573,10 @@ print_one_deployment (RPMOSTreeSysroot *sysroot_proxy, const gchar *origin_refspec; g_autofree const gchar **origin_packages = NULL; + g_autofree const gchar **origin_modules = NULL; + g_autofree const gchar **origin_modules_enabled = NULL; g_autofree const gchar **origin_requested_packages = NULL; + g_autofree const gchar **origin_requested_modules = NULL; g_autofree const gchar **origin_requested_local_packages = NULL; g_autoptr(GVariant) origin_base_removals = NULL; g_autofree const gchar **origin_requested_base_removals = NULL; @@ -583,8 +586,14 @@ print_one_deployment (RPMOSTreeSysroot *sysroot_proxy, { origin_packages = lookup_array_and_canonicalize (dict, "packages"); + origin_modules = + lookup_array_and_canonicalize (dict, "modules"); + origin_modules_enabled = + lookup_array_and_canonicalize (dict, "modules-enabled"); origin_requested_packages = lookup_array_and_canonicalize (dict, "requested-packages"); + origin_requested_modules = + lookup_array_and_canonicalize (dict, "requested-modules"); origin_requested_local_packages = lookup_array_and_canonicalize (dict, "requested-local-packages"); origin_base_removals = @@ -966,10 +975,20 @@ print_one_deployment (RPMOSTreeSysroot *sysroot_proxy, /* requested-packages - packages = inactive (i.e. dormant requests) */ print_packages ("InactiveRequests", max_key_len, origin_requested_packages, origin_packages); + if (origin_requested_modules && opt_verbose) + /* requested-modules - modules = inactive (i.e. dormant requests) */ + print_packages ("InactiveModuleRequests", max_key_len, + origin_requested_modules, origin_modules); if (origin_packages) print_packages ("LayeredPackages", max_key_len, origin_packages, NULL); + if (origin_modules) + print_packages ("LayeredModules", max_key_len, + origin_modules, NULL); + if (origin_modules_enabled) + print_packages ("EnabledModules", max_key_len, + origin_modules_enabled, NULL); if (origin_requested_local_packages) print_packages ("LocalPackages", max_key_len, diff --git a/src/app/rpmostree-compose-builtin-tree.cxx b/src/app/rpmostree-compose-builtin-tree.cxx index 973977d035..47ac41a241 100644 --- a/src/app/rpmostree-compose-builtin-tree.cxx +++ b/src/app/rpmostree-compose-builtin-tree.cxx @@ -841,6 +841,12 @@ impl_install_tree (RpmOstreeTreeComposeContext *self, return glnx_throw_errno_prefix (error, "fchdir"); } + /* We don't support installing modules in non-unified mode, because it relies + * on the core writing metadata to the commit metadata (obviously this could + * be supported, but meh...) */ + if (!opt_unified_core && json_object_has_member (self->treefile, "modules")) + return glnx_throw (error, "Composing with modules requires --unified-core"); + /* Read the previous commit. Note we don't actually *need* the full commit; really, only * if one uses `check-passwd: { "type": "previous" }`. There are a few other optimizations * too, e.g. using the previous SELinux policy in unified core. Also, we might need the @@ -953,6 +959,15 @@ impl_install_tree (RpmOstreeTreeComposeContext *self, if (!inject_advisories (self, cancellable, error)) return FALSE; + /* embed modules layered in fully resolved NSVCA/P form */ + GHashTable *modules = rpmostree_context_get_modules_installed (self->corectx); + if (modules && g_hash_table_size (modules) > 0) + { + g_autofree char** strv = (char**)g_hash_table_get_keys_as_array (modules, NULL); + g_hash_table_insert (self->metadata, g_strdup ("rpmostree.modules.nsvcap"), + g_variant_new_strv (strv, -1)); + } + /* Destroy this now so the libdnf stack won't have any references * into the filesystem before we manipulate it. */ diff --git a/src/app/rpmostree-composeutil.cxx b/src/app/rpmostree-composeutil.cxx index c8090df362..01a5b835e8 100644 --- a/src/app/rpmostree-composeutil.cxx +++ b/src/app/rpmostree-composeutil.cxx @@ -188,6 +188,15 @@ rpmostree_composeutil_get_treespec (RpmOstreeContext *ctx, if (!treespec_bind_array (treedata, treespec, "install-langs", "instlangs", FALSE, error)) return NULL; + if (json_object_has_member (treedata, "modules")) + { + JsonObject *modules = json_object_get_object_member (treedata, "modules"); + if (!treespec_bind_array (modules, treespec, "enable", "modules-enable", FALSE, error)) + return NULL; + if (!treespec_bind_array (modules, treespec, "install", "modules-install", FALSE, error)) + return NULL; + } + return rpmostree_treespec_new_from_keyfile (treespec, error); } diff --git a/src/daemon/org.projectatomic.rpmostree1.xml b/src/daemon/org.projectatomic.rpmostree1.xml index e66c3bc537..4ccfa64af4 100644 --- a/src/daemon/org.projectatomic.rpmostree1.xml +++ b/src/daemon/org.projectatomic.rpmostree1.xml @@ -298,6 +298,8 @@ "install-packages" (type 'as') "uninstall-packages" (type 'as') "install-local-packages" (type 'ah') + "install-modules" (type 'as') + "uninstall-modules" (type 'as') "override-remove-packages" (type 'as') "override-reset-packages" (type 'as') "override-replace-packages" (type 'as') diff --git a/src/daemon/rpmostree-sysroot-upgrader.cxx b/src/daemon/rpmostree-sysroot-upgrader.cxx index 72f82f7385..59ab359601 100644 --- a/src/daemon/rpmostree-sysroot-upgrader.cxx +++ b/src/daemon/rpmostree-sysroot-upgrader.cxx @@ -82,6 +82,7 @@ struct RpmOstreeSysrootUpgrader { DnfSack *rpmmd_sack; /* sack from core */ GPtrArray *overlay_packages; /* Finalized list of pkgs to overlay */ + GPtrArray *overlay_modules; /* Finalized list of modules to overlay */ GPtrArray *override_remove_packages; /* Finalized list of base pkgs to remove */ GPtrArray *override_replace_local_packages; /* Finalized list of local base pkgs to replace */ @@ -224,6 +225,7 @@ rpmostree_sysroot_upgrader_finalize (GObject *object) g_free (self->final_revision); g_strfreev (self->kargs_strv); g_clear_pointer (&self->overlay_packages, (GDestroyNotify)g_ptr_array_unref); + g_clear_pointer (&self->overlay_modules, (GDestroyNotify)g_ptr_array_unref); g_clear_pointer (&self->override_remove_packages, (GDestroyNotify)g_ptr_array_unref); G_OBJECT_CLASS (rpmostree_sysroot_upgrader_parent_class)->finalize (object); @@ -680,6 +682,22 @@ generate_treespec (RpmOstreeSysrootUpgrader *self) self->overlay_packages->len); } + GHashTable *enable_modules = rpmostree_origin_get_modules_enable (self->origin); + if (g_hash_table_size (enable_modules) > 0) + { + guint length = 0; + g_autofree char **modules = (char**)g_hash_table_get_keys_as_array (enable_modules, &length); + g_key_file_set_string_list (treespec, "tree", "modules-enable", + (const char* const*)modules, length); + } + + if (self->overlay_modules->len > 0) + { + g_key_file_set_string_list (treespec, "tree", "modules-install", + (const char* const*)self->overlay_modules->pdata, + self->overlay_modules->len); + } + GHashTable *local_packages = rpmostree_origin_get_local_packages (self->origin); if (g_hash_table_size (local_packages) > 0) { @@ -833,7 +851,8 @@ finalize_overlays (RpmOstreeSysrootUpgrader *self, /* request (owned by origin) --> providing nevra */ g_autoptr(GHashTable) inactive_requests = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); - g_autoptr(GPtrArray) ret_missing_pkgs = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) missing_pkgs = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GPtrArray) missing_modules = g_ptr_array_new_with_free_func (g_free); /* Add the local pkgs as if they were installed: since they're unconditionally * layered, we treat them as part of the base wrt regular requested pkgs. E.g. @@ -884,7 +903,7 @@ finalize_overlays (RpmOstreeSysrootUpgrader *self, if (matches->len == 0) { /* no matches, so we'll need to layer it */ - g_ptr_array_add (ret_missing_pkgs, g_strdup (pattern)); + g_ptr_array_add (missing_pkgs, g_strdup (pattern)); continue; } @@ -914,6 +933,57 @@ finalize_overlays (RpmOstreeSysrootUpgrader *self, g_strdup (providing_nevra)); } + if (g_hash_table_size (rpmostree_origin_get_modules_install (self->origin)) > 0) + { + g_autoptr(GHashTable) base_modules = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + /* The source of truth for enabled modules for dnf/libdnf is in + * /etc/dnf/modules.d, which isn't a good fit for us. Instead, the compose + * puts any enabled modules in the commit metadata. */ + g_autoptr(GVariant) base_commit = NULL; + if (!ostree_repo_load_variant (self->repo, OSTREE_OBJECT_TYPE_COMMIT, + self->base_revision, &base_commit, error)) + return FALSE; + g_autoptr(GVariant) metadata = g_variant_get_child_value (base_commit, 0); + g_autoptr(GVariantDict) metadata_dict = g_variant_dict_new (metadata); + g_autofree char **modules = NULL; + g_variant_dict_lookup (metadata_dict, "rpmostree.modules.nsvcap", "^a&s", &modules); + /* just build a basic -> table, we don't try to be smarter + * than that for inactive requests (see below) */ + for (char **mod = modules; mod && *mod; mod++) + { + g_autofree char *name = NULL; + g_autofree char *stream = NULL; + if (hy_parse_module_spec (*mod, &name, &stream, NULL, NULL, NULL, NULL)) + { + /* that really should never happen; but we shouldn't crash on bad metadata */ + return glnx_throw (error, "Corrupted NSVCA/P in commit metadata: '%s'", *mod); + } + g_hash_table_insert (base_modules, g_steal_pointer (&name), g_steal_pointer (&stream)); + } + + /* now, check if each module is already in the base */ + GLNX_HASH_TABLE_FOREACH (rpmostree_origin_get_modules_install (self->origin), + const char*, module_spec) + { + /* Technically, this could be globby pattern so e.g. it could match a base module, + * but the core will reject it when trying to depsolve. We just do a best-effort + * here to detect inactive requests to make it feasible to support the same + * declarative model as with regular packages. */ + g_autofree char *name = NULL; + g_autofree char *stream = NULL; + if (hy_parse_module_spec (module_spec, &name, &stream, NULL, NULL, NULL, NULL)) + return glnx_throw (error, "Invalid module spec '%s'", module_spec); + const char *base_stream = (const char*)g_hash_table_lookup (base_modules, name); + if (base_stream && (!*stream || g_str_equal (stream, base_stream))) + g_hash_table_insert (inactive_requests, (gpointer)module_spec, + g_strdup_printf ("base module %s:%s", name, base_stream)); + else /* not in base, so we'll need to layer it */ + g_ptr_array_add (missing_modules, g_strdup (module_spec)); + } + } + if (g_hash_table_size (inactive_requests) > 0) { rpmostree_output_message ("Inactive requests:"); @@ -922,7 +992,9 @@ finalize_overlays (RpmOstreeSysrootUpgrader *self, } g_assert (!self->overlay_packages); - self->overlay_packages = util::move_nullify (ret_missing_pkgs); + g_assert (!self->overlay_modules); + self->overlay_packages = util::move_nullify (missing_pkgs); + self->overlay_modules = util::move_nullify (missing_modules); return TRUE; } @@ -1002,6 +1074,7 @@ prep_local_assembly (RpmOstreeSysrootUpgrader *self, } const gboolean have_packages = (self->overlay_packages->len > 0 || + self->overlay_modules->len > 0 || g_hash_table_size (local_pkgs) > 0 || self->override_remove_packages->len > 0 || self->override_replace_local_packages->len > 0); @@ -1209,6 +1282,7 @@ requires_local_assembly (RpmOstreeSysrootUpgrader *self) */ return self->overlay_packages->len > 0 || + self->overlay_modules->len > 0 || self->override_remove_packages->len > 0 || self->override_replace_local_packages->len > 0 || g_hash_table_size (rpmostree_origin_get_local_packages (self->origin)) > 0 || diff --git a/src/daemon/rpmostreed-deployment-utils.cxx b/src/daemon/rpmostreed-deployment-utils.cxx index 0ccdc07944..0275fc1b21 100644 --- a/src/daemon/rpmostreed-deployment-utils.cxx +++ b/src/daemon/rpmostreed-deployment-utils.cxx @@ -257,10 +257,11 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, gboolean is_layered = FALSE; g_autofree char *base_checksum = NULL; g_auto(GStrv) layered_pkgs = NULL; + g_auto(GStrv) layered_modules = NULL; g_autoptr(GVariant) removed_base_pkgs = NULL; g_autoptr(GVariant) replaced_base_pkgs = NULL; if (!rpmostree_deployment_get_layered_info (repo, deployment, &is_layered, NULL, - &base_checksum, &layered_pkgs, + &base_checksum, &layered_pkgs, &layered_modules, &removed_base_pkgs, &replaced_base_pkgs, error)) return NULL; @@ -350,6 +351,10 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, variant_add_from_hash_table (dict, "requested-packages", rpmostree_origin_get_packages (origin)); + variant_add_from_hash_table (dict, "modules-enabled", + rpmostree_origin_get_modules_enable (origin)); + variant_add_from_hash_table (dict, "requested-modules", + rpmostree_origin_get_modules_install (origin)); variant_add_from_hash_table (dict, "requested-local-packages", rpmostree_origin_get_local_packages (origin)); variant_add_from_hash_table (dict, "requested-base-removals", @@ -358,6 +363,7 @@ rpmostreed_deployment_generate_variant (OstreeSysroot *sysroot, rpmostree_origin_get_overrides_local_replace (origin)); g_variant_dict_insert (dict, "packages", "^as", layered_pkgs); + g_variant_dict_insert (dict, "modules", "^as", layered_modules); g_variant_dict_insert_value (dict, "base-removals", removed_base_pkgs); g_variant_dict_insert_value (dict, "base-local-replacements", replaced_base_pkgs); diff --git a/src/daemon/rpmostreed-os.cxx b/src/daemon/rpmostreed-os.cxx index d8961b54a3..1f2d953b81 100644 --- a/src/daemon/rpmostreed-os.cxx +++ b/src/daemon/rpmostreed-os.cxx @@ -172,6 +172,14 @@ os_authorize_method (GDBusInterfaceSkeleton *interface, vardict_lookup_strv (&modifiers_dict, "install-packages"); g_autofree char **uninstall_pkgs = vardict_lookup_strv (&modifiers_dict, "uninstall-packages"); + g_autofree char **enable_modules = + vardict_lookup_strv (&modifiers_dict, "enable-modules"); + g_autofree char **disable_modules = + vardict_lookup_strv (&modifiers_dict, "disable-modules"); + g_autofree char **install_modules = + vardict_lookup_strv (&modifiers_dict, "install-modules"); + g_autofree char **uninstall_modules = + vardict_lookup_strv (&modifiers_dict, "uninstall-modules"); g_autofree const char *const *override_replace_pkgs = vardict_lookup_strv (&modifiers_dict, "override-replace-packages"); g_autofree const char *const *override_remove_pkgs = @@ -201,7 +209,10 @@ os_authorize_method (GDBusInterfaceSkeleton *interface, else if (!no_pull_base) g_ptr_array_add (actions, (void*)"org.projectatomic.rpmostree1.upgrade"); - if (install_pkgs != NULL || uninstall_pkgs != NULL || no_layering) + if (install_pkgs != NULL || uninstall_pkgs != NULL || + enable_modules != NULL || disable_modules != NULL || + install_modules != NULL || uninstall_modules != NULL || + no_layering) g_ptr_array_add (actions, (void*)"org.projectatomic.rpmostree1.install-uninstall-packages"); if (install_local_pkgs != NULL && g_variant_n_children (install_local_pkgs) > 0) diff --git a/src/daemon/rpmostreed-transaction-types.cxx b/src/daemon/rpmostreed-transaction-types.cxx index 14d1bcf336..3f833993f0 100644 --- a/src/daemon/rpmostreed-transaction-types.cxx +++ b/src/daemon/rpmostreed-transaction-types.cxx @@ -609,6 +609,10 @@ typedef struct { char **install_pkgs; /* strv but strings owned by modifiers */ GUnixFDList *install_local_pkgs; char **uninstall_pkgs; /* strv but strings owned by modifiers */ + char **enable_modules; /* strv but strings owned by modifiers */ + char **disable_modules; /* strv but strings owned by modifiers */ + char **install_modules; /* strv but strings owned by modifiers */ + char **uninstall_modules; /* strv but strings owned by modifiers */ char **override_replace_pkgs; /* strv but strings owned by modifiers */ GUnixFDList *override_replace_local_pkgs; char **override_remove_pkgs; /* strv but strings owned by modifiers */ @@ -638,6 +642,10 @@ deploy_transaction_finalize (GObject *object) g_free (self->install_pkgs); g_clear_pointer (&self->install_local_pkgs, g_object_unref); g_free (self->uninstall_pkgs); + g_free (self->enable_modules); + g_free (self->disable_modules); + g_free (self->install_modules); + g_free (self->uninstall_modules); g_free (self->override_replace_pkgs); g_clear_pointer (&self->override_replace_local_pkgs, g_object_unref); g_free (self->override_remove_pkgs); @@ -907,7 +915,7 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, const gboolean idempotent_layering = deploy_has_bool_option (self, "idempotent-layering"); const gboolean download_only = ((self->flags & RPMOSTREE_TRANSACTION_DEPLOY_FLAG_DOWNLOAD_ONLY) > 0); - /* Mainly for the `install` and `override` commands */ + /* Mainly for the `install`, `module install`, and `override` commands */ const gboolean no_pull_base = ((self->flags & RPMOSTREE_TRANSACTION_DEPLOY_FLAG_NO_PULL_BASE) > 0); /* Used to background check for updates; this essentially means downloading the minimum @@ -933,7 +941,7 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, no_overrides); if (!is_override) { - if (self->install_pkgs || self->install_local_pkgs) + if (self->install_pkgs || self->install_local_pkgs || self->install_modules) is_install = TRUE; else is_uninstall = TRUE; @@ -984,12 +992,24 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, if (self->uninstall_pkgs) g_string_append_printf (txn_title, "; uninstall: %u", g_strv_length (self->uninstall_pkgs)); + if (self->disable_modules) + g_string_append_printf (txn_title, "; module disable: %u", + g_strv_length (self->disable_modules)); + if (self->uninstall_modules) + g_string_append_printf (txn_title, "; module uninstall: %u", + g_strv_length (self->uninstall_modules)); if (self->install_pkgs) g_string_append_printf (txn_title, "; install: %u", g_strv_length (self->install_pkgs)); if (self->install_local_pkgs) g_string_append_printf (txn_title, "; localinstall: %u", g_unix_fd_list_get_length (self->install_local_pkgs)); + if (self->enable_modules) + g_string_append_printf (txn_title, "; module enable: %u", + g_strv_length (self->enable_modules)); + if (self->install_modules) + g_string_append_printf (txn_title, "; module install: %u", + g_strv_length (self->install_modules)); rpmostree_transaction_set_title (RPMOSTREE_TRANSACTION (transaction), txn_title->str); } @@ -1146,11 +1166,30 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, if (!rpmostree_origin_remove_all_packages (origin, &remove_changed, error)) return FALSE; } - else if (self->uninstall_pkgs) + else { - if (!rpmostree_origin_remove_packages (origin, self->uninstall_pkgs, - idempotent_layering, &remove_changed, error)) - return FALSE; + gboolean local_changed = FALSE; + if (self->uninstall_pkgs) + { + if (!rpmostree_origin_remove_packages (origin, self->uninstall_pkgs, + idempotent_layering, &local_changed, error)) + return FALSE; + } + remove_changed = remove_changed || local_changed; + if (self->disable_modules) + { + if (!rpmostree_origin_remove_modules (origin, self->disable_modules, + TRUE, &local_changed, error)) + return FALSE; + } + remove_changed = remove_changed || local_changed; + if (self->uninstall_modules) + { + if (!rpmostree_origin_remove_modules (origin, self->uninstall_modules, + FALSE, &local_changed, error)) + return FALSE; + } + remove_changed = remove_changed || local_changed; } /* In reality, there may not be any new layer required even if `remove_changed` is TRUE @@ -1201,6 +1240,24 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, changed = changed || add_changed; } + if (self->enable_modules) + { + gboolean add_changed = FALSE; + if (!rpmostree_origin_add_modules (origin, self->enable_modules, TRUE, &add_changed, error)) + return FALSE; + + changed = changed || add_changed; + } + + if (self->install_modules) + { + gboolean add_changed = FALSE; + if (!rpmostree_origin_add_modules (origin, self->install_modules, FALSE, &add_changed, error)) + return FALSE; + + changed = changed || add_changed; + } + if (self->install_local_pkgs != NULL) { g_autoptr(GPtrArray) pkgs = NULL; @@ -1240,7 +1297,7 @@ deploy_transaction_execute (RpmostreedTransaction *transaction, g_autoptr(GVariant) removed = NULL; g_autoptr(GVariant) replaced = NULL; if (!rpmostree_deployment_get_layered_info (repo, merge_deployment, NULL, NULL, NULL, - NULL, &removed, &replaced, error)) + NULL, NULL, &removed, &replaced, error)) return FALSE; g_autoptr(GHashTable) nevra_to_name = g_hash_table_new (g_str_hash, g_str_equal); @@ -1720,6 +1777,10 @@ rpmostreed_transaction_new_deploy (GDBusMethodInvocation *invocation, self->override_replace_pkgs = vardict_lookup_strv_canonical (self->modifiers, "override-replace-packages"); self->override_remove_pkgs = vardict_lookup_strv_canonical (self->modifiers, "override-remove-packages"); self->override_reset_pkgs = vardict_lookup_strv_canonical (self->modifiers, "override-reset-packages"); + self->enable_modules = vardict_lookup_strv_canonical (self->modifiers, "enable-modules"); + self->disable_modules = vardict_lookup_strv_canonical (self->modifiers, "disable-modules"); + self->install_modules = vardict_lookup_strv_canonical (self->modifiers, "install-modules"); + self->uninstall_modules = vardict_lookup_strv_canonical (self->modifiers, "uninstall-modules"); /* default to allowing downgrades for rebases & deploys (without --disallow-downgrade) */ if (vardict_lookup_bool (self->options, "allow-downgrade", refspec_or_revision)) diff --git a/src/libpriv/rpmostree-core-private.h b/src/libpriv/rpmostree-core-private.h index eee90131bb..6eca224262 100644 --- a/src/libpriv/rpmostree-core-private.h +++ b/src/libpriv/rpmostree-core-private.h @@ -81,6 +81,8 @@ struct _RpmOstreeContext { GHashTable *pkgs_to_remove; /* pkgname --> gv_nevra */ GHashTable *pkgs_to_replace; /* new gv_nevra --> old gv_nevra */ + GHashTable *modules_installed; /* All fully resolved modules being installed, in NSVCA/P form. */ + std::optional> lockfile; gboolean lockfile_strict; diff --git a/src/libpriv/rpmostree-core.cxx b/src/libpriv/rpmostree-core.cxx index 054f917e66..fb40c9474f 100644 --- a/src/libpriv/rpmostree-core.cxx +++ b/src/libpriv/rpmostree-core.cxx @@ -237,6 +237,8 @@ rpmostree_treespec_new_from_keyfile (GKeyFile *keyfile, #undef BIND_STRING add_canonicalized_string_array (&builder, "packages", NULL, keyfile); + add_canonicalized_string_array (&builder, "modules-enable", NULL, keyfile); + add_canonicalized_string_array (&builder, "modules-install", NULL, keyfile); add_canonicalized_string_array (&builder, "exclude-packages", NULL, keyfile); add_canonicalized_string_array (&builder, "cached-packages", NULL, keyfile); add_canonicalized_string_array (&builder, "removed-base-packages", NULL, keyfile); @@ -324,6 +326,8 @@ rpmostree_context_finalize (GObject *object) g_clear_pointer (&rctx->pkgs_to_remove, g_hash_table_unref); g_clear_pointer (&rctx->pkgs_to_replace, g_hash_table_unref); + g_clear_pointer (&rctx->modules_installed, g_hash_table_unref); + (void)glnx_tmpdir_delete (&rctx->tmpdir, NULL, NULL); (void)glnx_tmpdir_delete (&rctx->repo_tmpdir, NULL, NULL); @@ -523,6 +527,12 @@ rpmostree_context_get_ref (RpmOstreeContext *self) return self->ref; } +GHashTable* +rpmostree_context_get_modules_installed (RpmOstreeContext *self) +{ + return self->modules_installed; +} + /* XXX: or put this in new_system() instead? */ void rpmostree_context_set_repos (RpmOstreeContext *self, @@ -748,6 +758,28 @@ rpmostree_context_setup (RpmOstreeContext *self, if (!dnf_context_setup (self->dnfctx, cancellable, error)) return FALSE; + /* XXX: If we have modules to install, then we need libdnf to handle it, and + * we can't avoid not parsing repodata because modules are entirely a repodata + * concept. So for now, force off pkgcache-only. This means that e.g. client + * side operations that are normally cache-only like `rpm-ostree uninstall` + * will still try to fetch metadata, and might install newer versions of other + * packages... we can probably hack that in the future. */ + if (self->pkgcache_only) + { + gboolean disable_cacheonly = FALSE; + g_autofree char **modules_enable = NULL; + if (g_variant_dict_lookup (self->spec->dict, "modules-enable", "^a&s", &modules_enable)) + disable_cacheonly = disable_cacheonly || (modules_enable && g_strv_length (modules_enable) > 0); + g_autofree char **modules_install = NULL; + if (g_variant_dict_lookup (self->spec->dict, "modules-install", "^a&s", &modules_install)) + disable_cacheonly = disable_cacheonly || (modules_install && g_strv_length (modules_install) > 0); + if (disable_cacheonly) + { + self->pkgcache_only = FALSE; + sd_journal_print (LOG_WARNING, "Ignoring pkgcache-only request in presence of module requests"); + } + } + /* disable all repos in pkgcache-only mode, otherwise obey "repos" key */ if (self->pkgcache_only) { @@ -1184,18 +1216,6 @@ rpmostree_context_download_metadata (RpmOstreeContext *self, g_signal_handler_disconnect (hifstate, progress_sigid); } - /* For now, we don't natively support modules. But we still want to be able to install - * modular packages if the repos are enabled, but libdnf automatically filters them out. - * So for now, let's tell libdnf that we do want to be able to see them. See: - * https://github.com/projectatomic/rpm-ostree/issues/1435 */ - dnf_sack_set_module_excludes (dnf_context_get_sack (self->dnfctx), NULL); - /* And also mark all repos as hotfix repos so that we can indiscriminately cherry-pick - * from modular repos and non-modular repos alike. */ - g_autoptr(GPtrArray) repos = - rpmostree_get_enabled_rpmmd_repos (self->dnfctx, DNF_REPO_ENABLED_PACKAGES); - for (guint i = 0; i < repos->len; i++) - dnf_repo_set_module_hotfixes (static_cast(repos->pdata[i]), TRUE); - return TRUE; } @@ -1992,6 +2012,14 @@ rpmostree_context_prepare (RpmOstreeContext *self, g_variant_dict_lookup (self->spec->dict, "exclude-packages", "^a&s", &exclude_packages); + g_autofree char **modules_enable = NULL; + g_assert (g_variant_dict_lookup (self->spec->dict, "modules-enable", + "^a&s", &modules_enable)); + + g_autofree char **modules_install = NULL; + g_assert (g_variant_dict_lookup (self->spec->dict, "modules-install", + "^a&s", &modules_install)); + g_autofree char **cached_pkgnames = NULL; g_assert (g_variant_dict_lookup (self->spec->dict, "cached-packages", "^a&s", &cached_pkgnames)); @@ -2027,6 +2055,8 @@ rpmostree_context_prepare (RpmOstreeContext *self, if (self->rojig_pure) { g_assert_cmpint (g_strv_length (pkgnames), ==, 0); + g_assert_cmpint (g_strv_length (modules_enable), ==, 0); + g_assert_cmpint (g_strv_length (modules_install), ==, 0); g_assert_cmpint (g_strv_length (cached_pkgnames), ==, 0); g_assert_cmpint (g_strv_length (cached_replace_pkgs), ==, 0); g_assert_cmpint (g_strv_length (removed_base_pkgnames), ==, 0); @@ -2201,8 +2231,11 @@ rpmostree_context_prepare (RpmOstreeContext *self, hy_goal_install (goal, pkg); /* Now repo-packages; only supported during server composes for now. */ + g_autoptr(DnfPackageSet) pinned_pkgs = NULL; if (!self->is_system) { + pinned_pkgs = dnf_packageset_new (sack); + Map *pinned_pkgs_map = dnf_packageset_get_map (pinned_pkgs); auto repo_pkgs = self->treefile_rs->get_repo_packages(); for (auto & repo_pkg : repo_pkgs) { @@ -2225,10 +2258,48 @@ rpmostree_context_prepare (RpmOstreeContext *self, hy_selector_pkg_set (selector, pset); if (!hy_goal_install_selector (goal, selector, error)) return FALSE; + + map_or (pinned_pkgs_map, dnf_packageset_get_map (pset)); } } } + gboolean we_got_modules = FALSE; + if (g_strv_length (modules_enable) > 0) + { + if (!dnf_context_module_enable (dnfctx, (const char**)modules_enable, error)) + return FALSE; + we_got_modules = TRUE; + } + + if (g_strv_length (modules_install) > 0) + { + g_auto(GStrv) resolved_modules = + dnf_context_module_install (dnfctx, (const char**)modules_install, error); + if (!resolved_modules) + return glnx_prefix_error (error, "Installing modules"); + + /* Just convert to a hash table to make sure we have no duplicates */ + self->modules_installed = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + for (char **mod = resolved_modules; mod && *mod; mod++) + g_hash_table_add (self->modules_installed, g_strdup (*mod)); + we_got_modules = TRUE; + } + + /* By default, when enabling a module, trying to install a package "foo" will + * always prioritize the "foo" in the module. This is what we want, but in the + * case of pinned repo packages, we want to be able to override that. So we + * need to fiddle with the modular excludes. */ + if (we_got_modules && pinned_pkgs && dnf_packageset_count (pinned_pkgs) > 0) + { + g_autoptr(DnfPackageSet) excludes = dnf_sack_get_module_excludes (sack); + g_autoptr(DnfPackageSet) cloned_pkgs = dnf_packageset_clone (pinned_pkgs); + Map *m = dnf_packageset_get_map (cloned_pkgs); + map_invertall (m); + map_and (dnf_packageset_get_map (excludes), m); + dnf_sack_set_module_excludes (sack, excludes); + } + /* And finally, handle repo packages to install */ g_autoptr(GPtrArray) missing_pkgs = NULL; for (char **it = pkgnames; it && *it; it++) @@ -4741,6 +4812,21 @@ rpmostree_context_commit (RpmOstreeContext *self, g_assert (pkgs); g_variant_builder_add (&metadata_builder, "{sv}", "rpmostree.packages", pkgs); + /* embed modules layered */ + g_autoptr(GVariant) modules = + g_variant_dict_lookup_value (self->spec->dict, "modules-install", G_VARIANT_TYPE ("as")); + g_assert (modules); + g_variant_builder_add (&metadata_builder, "{sv}", "rpmostree.modules", modules); + + /* also embed the fully resolved NSVCAPs */ + if (self->modules_installed && g_hash_table_size (self->modules_installed) > 0) + { + g_autofree char** strv = + (char**)g_hash_table_get_keys_as_array (self->modules_installed, NULL); + g_variant_builder_add (&metadata_builder, "{sv}", "rpmostree.modules.nsvcap", + g_variant_new_strv (strv, -1)); + } + /* embed packages removed */ /* we have to embed both the pkgname and the full nevra to make it easier to match * them up with origin directives. the full nevra is used for status -v */ @@ -4777,7 +4863,7 @@ rpmostree_context_commit (RpmOstreeContext *self, /* be nice to our future selves */ g_variant_builder_add (&metadata_builder, "{sv}", "rpmostree.clientlayer_version", - g_variant_new_uint32 (4)); + g_variant_new_uint32 (5)); } else if (assemble_type == RPMOSTREE_ASSEMBLE_TYPE_SERVER_BASE) { diff --git a/src/libpriv/rpmostree-core.h b/src/libpriv/rpmostree-core.h index 5f611048c7..786a66aef5 100644 --- a/src/libpriv/rpmostree-core.h +++ b/src/libpriv/rpmostree-core.h @@ -126,6 +126,7 @@ rpmostree_context_configure_from_deployment (RpmOstreeContext *self, void rpmostree_context_set_is_empty (RpmOstreeContext *self); void rpmostree_context_disable_selinux (RpmOstreeContext *self); const char *rpmostree_context_get_ref (RpmOstreeContext *self); +GHashTable* rpmostree_context_get_modules_installed (RpmOstreeContext *self); void rpmostree_context_set_repos (RpmOstreeContext *self, OstreeRepo *base_repo, diff --git a/src/libpriv/rpmostree-origin.cxx b/src/libpriv/rpmostree-origin.cxx index d66b0a969f..a5a03a29ae 100644 --- a/src/libpriv/rpmostree-origin.cxx +++ b/src/libpriv/rpmostree-origin.cxx @@ -48,6 +48,8 @@ struct RpmOstreeOrigin { char **cached_initramfs_args; GHashTable *cached_initramfs_etc_files; /* set of paths */ GHashTable *cached_packages; /* set of reldeps */ + GHashTable *cached_modules_enable; /* set of module specs to enable */ + GHashTable *cached_modules_install; /* set of module specs to install */ GHashTable *cached_local_packages; /* NEVRA --> header sha256 */ /* GHashTable *cached_overrides_replace; XXX: NOT IMPLEMENTED YET */ GHashTable *cached_overrides_local_replace; /* NEVRA --> header sha256 */ @@ -106,6 +108,8 @@ rpmostree_origin_parse_keyfile (GKeyFile *origin, ret->kf = keyfile_dup (origin); ret->cached_packages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + ret->cached_modules_enable = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + ret->cached_modules_install = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); ret->cached_local_packages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); ret->cached_overrides_local_replace = @@ -156,6 +160,14 @@ rpmostree_origin_parse_keyfile (GKeyFile *origin, ret->cached_local_packages, error)) return FALSE; + if (!parse_packages_strv (ret->kf, "modules", "enable", FALSE, + ret->cached_modules_enable, error)) + return FALSE; + + if (!parse_packages_strv (ret->kf, "modules", "install", FALSE, + ret->cached_modules_install, error)) + return FALSE; + if (!parse_packages_strv (ret->kf, "overrides", "remove", FALSE, ret->cached_overrides_remove, error)) return FALSE; @@ -295,6 +307,18 @@ rpmostree_origin_get_packages (RpmOstreeOrigin *origin) return origin->cached_packages; } +GHashTable * +rpmostree_origin_get_modules_enable (RpmOstreeOrigin *origin) +{ + return origin->cached_modules_enable; +} + +GHashTable * +rpmostree_origin_get_modules_install (RpmOstreeOrigin *origin) +{ + return origin->cached_modules_install; +} + GHashTable * rpmostree_origin_get_local_packages (RpmOstreeOrigin *origin) { @@ -355,6 +379,7 @@ rpmostree_origin_may_require_local_assembly (RpmOstreeOrigin *origin) return rpmostree_origin_get_regenerate_initramfs (origin) || (g_hash_table_size (origin->cached_initramfs_etc_files) > 0) || (g_hash_table_size (origin->cached_packages) > 0) || + (g_hash_table_size (origin->cached_modules_install) > 0) || (g_hash_table_size (origin->cached_local_packages) > 0) || (g_hash_table_size (origin->cached_overrides_local_replace) > 0) || (g_hash_table_size (origin->cached_overrides_remove) > 0); @@ -396,6 +421,8 @@ rpmostree_origin_unref (RpmOstreeOrigin *origin) g_free (origin->cached_unconfigured_state); g_strfreev (origin->cached_initramfs_args); g_clear_pointer (&origin->cached_packages, g_hash_table_unref); + g_clear_pointer (&origin->cached_modules_enable, g_hash_table_unref); + g_clear_pointer (&origin->cached_modules_install, g_hash_table_unref); g_clear_pointer (&origin->cached_local_packages, g_hash_table_unref); g_clear_pointer (&origin->cached_overrides_local_replace, g_hash_table_unref); g_clear_pointer (&origin->cached_overrides_remove, g_hash_table_unref); @@ -815,6 +842,50 @@ rpmostree_origin_remove_packages (RpmOstreeOrigin *origin, return TRUE; } +gboolean +rpmostree_origin_add_modules (RpmOstreeOrigin *origin, + char **modules, + gboolean enable_only, + gboolean *out_changed, + GError **error) +{ + const char *key = enable_only ? "enable" : "install"; + GHashTable *target = enable_only ? origin->cached_modules_enable + : origin->cached_modules_install; + gboolean changed = FALSE; + for (char **mod = modules; mod && *mod; mod++) + changed = (g_hash_table_add (target, g_strdup (*mod)) || changed); + + if (changed) + update_string_list_from_hash_table (origin->kf, "modules", key, target); + if (out_changed) + *out_changed = changed; + + return TRUE; +} + +gboolean +rpmostree_origin_remove_modules (RpmOstreeOrigin *origin, + char **modules, + gboolean enable_only, + gboolean *out_changed, + GError **error) +{ + const char *key = enable_only ? "enable" : "install"; + GHashTable *target = enable_only ? origin->cached_modules_enable + : origin->cached_modules_install; + gboolean changed = FALSE; + for (char **mod = modules; mod && *mod; mod++) + changed = (g_hash_table_remove (target, *mod) || changed); + + if (changed) + update_string_list_from_hash_table (origin->kf, "modules", key, target); + if (out_changed) + *out_changed = changed; + + return TRUE; +} + gboolean rpmostree_origin_remove_all_packages (RpmOstreeOrigin *origin, gboolean *out_changed, @@ -822,6 +893,8 @@ rpmostree_origin_remove_all_packages (RpmOstreeOrigin *origin, { gboolean changed = FALSE; gboolean local_changed = FALSE; + gboolean modules_enable_changed = FALSE; + gboolean modules_install_changed = FALSE; if (g_hash_table_size (origin->cached_packages) > 0) { @@ -835,14 +908,32 @@ rpmostree_origin_remove_all_packages (RpmOstreeOrigin *origin, local_changed = TRUE; } + if (g_hash_table_size (origin->cached_modules_enable) > 0) + { + g_hash_table_remove_all (origin->cached_modules_enable); + modules_enable_changed = TRUE; + } + + if (g_hash_table_size (origin->cached_modules_install) > 0) + { + g_hash_table_remove_all (origin->cached_modules_install); + modules_install_changed = TRUE; + } + if (changed) update_keyfile_pkgs_from_cache (origin, "packages", "requested", origin->cached_packages, FALSE); if (local_changed) update_keyfile_pkgs_from_cache (origin, "packages", "requested-local", origin->cached_local_packages, TRUE); + if (modules_enable_changed) + update_keyfile_pkgs_from_cache (origin, "modules", "enable", + origin->cached_modules_enable, FALSE); + if (modules_install_changed) + update_keyfile_pkgs_from_cache (origin, "modules", "install", + origin->cached_modules_install, FALSE); if (out_changed) - *out_changed = changed || local_changed; + *out_changed = changed || local_changed || modules_enable_changed || modules_install_changed; return TRUE; } diff --git a/src/libpriv/rpmostree-origin.h b/src/libpriv/rpmostree-origin.h index 6d82c99751..ce7b793c07 100644 --- a/src/libpriv/rpmostree-origin.h +++ b/src/libpriv/rpmostree-origin.h @@ -85,6 +85,12 @@ rpmostree_origin_get_custom_description (RpmOstreeOrigin *origin, GHashTable * rpmostree_origin_get_packages (RpmOstreeOrigin *origin); +GHashTable * +rpmostree_origin_get_modules_enable (RpmOstreeOrigin *origin); + +GHashTable * +rpmostree_origin_get_modules_install (RpmOstreeOrigin *origin); + GHashTable * rpmostree_origin_get_local_packages (RpmOstreeOrigin *origin); @@ -181,6 +187,20 @@ rpmostree_origin_remove_all_packages (RpmOstreeOrigin *origin, gboolean *out_changed, GError **error); +gboolean +rpmostree_origin_add_modules (RpmOstreeOrigin *origin, + char **modules, + gboolean enable_only, + gboolean *out_changed, + GError **error); + +gboolean +rpmostree_origin_remove_modules (RpmOstreeOrigin *origin, + char **modules, + gboolean enable_only, + gboolean *out_changed, + GError **error); + typedef enum { /* RPMOSTREE_ORIGIN_OVERRIDE_REPLACE, */ RPMOSTREE_ORIGIN_OVERRIDE_REPLACE_LOCAL, @@ -203,4 +223,4 @@ rpmostree_origin_remove_all_overrides (RpmOstreeOrigin *origin, gboolean *out_changed, GError **error); -G_END_DECLS \ No newline at end of file +G_END_DECLS diff --git a/src/libpriv/rpmostree-util.cxx b/src/libpriv/rpmostree-util.cxx index 7b3cf7336a..de8c3f631c 100644 --- a/src/libpriv/rpmostree-util.cxx +++ b/src/libpriv/rpmostree-util.cxx @@ -511,6 +511,7 @@ rpmostree_deployment_get_layered_info (OstreeRepo *repo, guint *out_layer_version, char **out_base_layer, char ***out_layered_pkgs, + char ***out_layered_modules, GVariant **out_removed_base_pkgs, GVariant **out_replaced_base_pkgs, GError **error) @@ -527,6 +528,7 @@ rpmostree_deployment_get_layered_info (OstreeRepo *repo, /* only fetch pkgs if we have to */ g_auto(GStrv) layered_pkgs = NULL; + g_auto(GStrv) layered_modules = NULL; g_autoptr(GVariant) removed_base_pkgs = NULL; g_autoptr(GVariant) replaced_base_pkgs = NULL; if (layeredmeta.is_layered && (out_layered_pkgs != NULL || out_removed_base_pkgs != NULL)) @@ -567,6 +569,12 @@ rpmostree_deployment_get_layered_info (OstreeRepo *repo, G_VARIANT_TYPE ("a(vv)")); g_assert (replaced_base_pkgs); } + + if (layeredmeta.clientlayer_version >= 5) + { + g_assert (g_variant_dict_lookup (dict, "rpmostree.modules", "^as", + &layered_modules)); + } } /* canonicalize outputs to empty array */ @@ -583,6 +591,12 @@ rpmostree_deployment_get_layered_info (OstreeRepo *repo, layered_pkgs = g_new0 (char*, 1); *out_layered_pkgs = util::move_nullify (layered_pkgs); } + if (out_layered_modules != NULL) + { + if (!layered_modules) + layered_modules = g_new0 (char*, 1); + *out_layered_modules = util::move_nullify (layered_modules); + } if (out_removed_base_pkgs != NULL) { if (!removed_base_pkgs) @@ -609,7 +623,7 @@ rpmostree_deployment_get_base_layer (OstreeRepo *repo, GError **error) { return rpmostree_deployment_get_layered_info (repo, deployment, NULL, NULL, - out_base_layer, NULL, NULL, NULL, error); + out_base_layer, NULL, NULL, NULL, NULL, error); } static gboolean @@ -1140,7 +1154,7 @@ rpmostree_maybe_shell_quote (const char *s) static GRegex *safe_chars_regex; if (g_once_init_enter (®ex_initialized)) { - safe_chars_regex = g_regex_new ("^[[:alnum:]-._/=]+$", (GRegexCompileFlags)0, (GRegexMatchFlags)0, NULL); + safe_chars_regex = g_regex_new ("^[[:alnum:]-._/=:]+$", (GRegexCompileFlags)0, (GRegexMatchFlags)0, NULL); g_assert (safe_chars_regex); g_once_init_leave (®ex_initialized, 1); } diff --git a/src/libpriv/rpmostree-util.h b/src/libpriv/rpmostree-util.h index 1d759659d8..248565ff80 100644 --- a/src/libpriv/rpmostree-util.h +++ b/src/libpriv/rpmostree-util.h @@ -236,6 +236,7 @@ rpmostree_deployment_get_layered_info (OstreeRepo *repo, guint *out_layer_version, char **out_base_layer, char ***out_layered_pkgs, + char ***out_layered_modules, GVariant **out_removed_base_pkgs, GVariant **out_replaced_base_pkgs, GError **error);