From d0849700c837d4dbb23dbbc86b2f6dbd76928f4b Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 20 Jan 2022 12:26:47 -0500 Subject: [PATCH] WIP: Adapt to and use new ostree-ext tar-split branch Depends https://github.com/ostreedev/ostree-rs-ext/pull/123 We gain a new `rpm-ostree container-encapsulate` option which is like `ostree container encapsulate`, but generates chunked images using the RPM database. And on the client side, we now know how to handle incremental updates - chunks that haven't changed won't be redownloaded. --- Cargo.lock | 4 +- Cargo.toml | 4 + rust/src/composepost.rs | 2 +- rust/src/container.rs | 378 +++++++++++++++++++++++++++++ rust/src/lib.rs | 16 +- rust/src/main.rs | 5 + rust/src/sysroot_upgrade.rs | 16 +- src/lib/rpmostree-package-priv.h | 3 + src/libpriv/rpmostree-refts.cxx | 73 ++++++ src/libpriv/rpmostree-refts.h | 45 ++++ src/libpriv/rpmostree-rpm-util.cxx | 20 ++ src/libpriv/rpmostree-rpm-util.h | 1 + 12 files changed, 550 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be22eae674..e57c93e26d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1612,8 +1612,7 @@ dependencies = [ [[package]] name = "ostree-ext" version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6d186573b7713d0794d2aa34c32ea5fa45a4f7fe77127c89815029ccc0bf3d" +source = "git+https://github.com/cgwalters/ostree-rs-ext?branch=tar-split#4fe97f1a10ac969ed456dc2b36261cfe7e142dbf" dependencies = [ "anyhow", "async-compression", @@ -1638,6 +1637,7 @@ dependencies = [ "openssl", "ostree", "pin-project", + "regex", "serde", "serde_json", "structopt", diff --git a/Cargo.toml b/Cargo.toml index 18d38a900d..3893468ac8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,3 +109,7 @@ bin-unit-tests = [] sanitizers = [] default = [] + +[patch.crates-io] +ostree-ext = { git = "https://github.com/cgwalters/ostree-rs-ext", branch = "tar-split" } +#ostree-ext = { path = "../../ostreedev/ostree-rs-ext/lib" } diff --git a/rust/src/composepost.rs b/rust/src/composepost.rs index 5ee58b0eb0..2e59b17da1 100644 --- a/rust/src/composepost.rs +++ b/rust/src/composepost.rs @@ -43,7 +43,7 @@ pub(crate) static COMPAT_VARLIB_SYMLINKS: &[&str] = &["alternatives", "vagrant"] /* See rpmostree-core.h */ const RPMOSTREE_BASE_RPMDB: &str = "usr/lib/sysimage/rpm-ostree-base-db"; -const RPMOSTREE_RPMDB_LOCATION: &str = "usr/share/rpm"; +pub(crate) const RPMOSTREE_RPMDB_LOCATION: &str = "usr/share/rpm"; const RPMOSTREE_SYSIMAGE_RPMDB: &str = "usr/lib/sysimage/rpm"; pub(crate) const TRADITIONAL_RPMDB_LOCATION: &str = "var/lib/rpm"; diff --git a/rust/src/container.rs b/rust/src/container.rs index ea18e5ffda..6ba557de59 100644 --- a/rust/src/container.rs +++ b/rust/src/container.rs @@ -2,7 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::convert::TryInto; +use std::num::NonZeroU32; +use std::rc::Rc; + use anyhow::Result; +use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::fs::Dir; +use cap_std_ext::cap_std; +use cap_std_ext::prelude::*; +use chrono::prelude::*; +use ostree::glib; +use ostree_ext::chunking::ObjectMetaSized; +use ostree_ext::container::{Config, ExportOpts, ImageReference}; +use ostree_ext::objectsource::{ + ContentID, ObjectMeta, ObjectMetaMap, ObjectMetaSet, ObjectSourceMeta, +}; +use ostree_ext::prelude::*; +use ostree_ext::{gio, ostree}; +use structopt::StructOpt; + +use crate::cxxrsutil::FFIGObjectReWrap; /// Main entrypoint for container pub async fn entrypoint(args: &[&str]) -> Result { @@ -14,3 +35,360 @@ pub async fn entrypoint(args: &[&str]) -> Result { ostree_ext::cli::run_from_iter(args).await?; Ok(0) } + +#[derive(Debug, StructOpt)] +struct ContainerEncapsulateOpts { + #[structopt(long)] + #[structopt(parse(try_from_str = ostree_ext::cli::parse_repo))] + repo: ostree::Repo, + + #[structopt(long = "ref")] + ostree_ref: String, + + /// Image reference, e.g. registry:quay.io/exampleos/exampleos:latest + #[structopt(parse(try_from_str = ostree_ext::cli::parse_base_imgref))] + imgref: ImageReference, + + /// Additional labels for the container + #[structopt(name = "label", long, short)] + labels: Vec, + + /// Propagate an OSTree commit metadata key to container label + #[structopt(name = "copymeta", long)] + copy_meta_keys: Vec, + + /// Corresponds to the Dockerfile `CMD` instruction. + #[structopt(long)] + cmd: Option>, + + /// Maximum number of container image layers + #[structopt(long)] + max_layers: Option, + + #[structopt(long)] + /// Output content metadata as JSON + write_contentmeta_json: Option, +} + +#[derive(Debug)] +struct MappingBuilder { + /// Maps from package ID to metadata + packagemeta: ObjectMetaSet, + /// Mapping from content object sha256 to package numeric ID + content: ObjectMetaMap, + /// Mapping from content object sha256 to package numeric ID + duplicates: BTreeMap>, + multi_provider: Vec, + + unpackaged_id: Rc, + + /// Files that were processed before the global tree walk + skip: HashSet, + + /// Size according to RPM database + rpmsize: u64, +} + +impl MappingBuilder { + /// For now, we stick everything that isn't a package inside a single "unpackaged" state. + /// In the future though if we support e.g. containers in /usr/share/containers or the + /// like, this will need to change. + const UNPACKAGED_ID: &'static str = "rpmostree-unpackaged-content"; +} + +impl From for ObjectMeta { + fn from(b: MappingBuilder) -> ObjectMeta { + ObjectMeta { + map: b.content, + set: b.packagemeta, + } + } +} + +/// Walk over the whole filesystem, and generate mappings from content object checksums +/// to the package that owns them. +/// +/// In the future, we could compute this much more efficiently by walking that +/// instead. But this design is currently oriented towards accepting a single ostree +/// commit as input. +fn build_mapping_recurse( + path: &mut Utf8PathBuf, + dir: &gio::File, + ts: &crate::ffi::RpmTs, + state: &mut MappingBuilder, +) -> Result<()> { + use std::collections::btree_map::Entry; + let cancellable = gio::NONE_CANCELLABLE; + let e = dir.enumerate_children( + "standard::name,standard::type", + gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS, + cancellable, + )?; + for child in e { + let childi = child?; + let name: Utf8PathBuf = childi.name().try_into()?; + let child = dir.child(&name); + path.push(&name); + match childi.file_type() { + gio::FileType::Regular | gio::FileType::SymbolicLink => { + let child = child.downcast::().unwrap(); + + // Remove the skipped path, since we can't hit it again. + if state.skip.remove(Utf8Path::new(path)) { + path.pop(); + continue; + } + + let mut pkgs = ts.packages_providing_file(path.as_str())?; + // Let's be deterministic (but _unstable because we don't care about behavior of equal strings) + pkgs.sort_unstable(); + // For now, we pick the alphabetically first package providing a file + let mut pkgs = pkgs.into_iter(); + let pkgid = pkgs + .next() + .map(|v| -> Result<_> { + // Safety: we should have the package in metadata + let meta = state.packagemeta.get(v.as_str()).ok_or_else(|| { + anyhow::anyhow!("Internal error: missing pkgmeta for {}", &v) + })?; + Ok(Rc::clone(&meta.identifier)) + }) + .transpose()? + .unwrap_or_else(|| Rc::clone(&state.unpackaged_id)); + // Track cases of duplicate owners + match pkgs.len() { + 0 => {} + _ => { + state.multi_provider.push(path.clone()); + } + } + + let checksum = child.checksum().unwrap().to_string(); + match state.content.entry(checksum) { + Entry::Vacant(v) => { + v.insert(pkgid); + } + Entry::Occupied(_) => { + let checksum = child.checksum().unwrap().to_string(); + let v = state.duplicates.entry(checksum).or_default(); + v.push(pkgid); + } + } + } + gio::FileType::Directory => { + build_mapping_recurse(path, &child, ts, state)?; + } + o => anyhow::bail!("Unhandled file type: {}", o), + } + path.pop(); + } + Ok(()) +} + +fn gv_nevra_to_string(pkg: &glib::Variant) -> String { + let name = pkg.child_value(0); + let name = name.str().unwrap(); + let epoch = pkg.child_value(1); + let epoch = epoch.str().unwrap(); + let version = pkg.child_value(2); + let version = version.str().unwrap(); + let release = pkg.child_value(3); + let release = release.str().unwrap(); + let arch = pkg.child_value(4); + let arch = arch.str().unwrap(); + if epoch == "0" { + format!("{}-{}-{}.{}", name, version, release, arch) + } else { + format!("{}-{}:{}-{}.{}", name, epoch, version, release, arch) + } +} + +/// Like `ostree container encapsulate`, but uses chunks derived from package data. +/// Note this isn't *really* asynchronous in many parts. +pub async fn container_encapsulate(args: &[&str]) -> Result<()> { + let args = args.iter().skip(1); + let opt = ContainerEncapsulateOpts::from_iter(args); + dbg!(&opt); + let repo = &opt.repo; + let (root, rev) = repo.read_commit(opt.ostree_ref.as_str(), gio::NONE_CANCELLABLE)?; + let pkglist = { + let repo = repo.gobj_rewrap(); + let cancellable = gio::Cancellable::new(); + unsafe { + let r = crate::ffi::package_variant_list_for_commit( + repo, + rev.as_str(), + cancellable.gobj_rewrap(), + )?; + let r: glib::Variant = glib::translate::from_glib_full(r as *mut _); + r + } + }; + + // Open the RPM database for this commit. + let q = crate::ffi::rpmts_for_commit(repo.gobj_rewrap(), rev.as_str())?; + + let mut state = MappingBuilder { + unpackaged_id: Rc::from(MappingBuilder::UNPACKAGED_ID), + packagemeta: Default::default(), + content: Default::default(), + duplicates: Default::default(), + multi_provider: Default::default(), + skip: Default::default(), + rpmsize: Default::default(), + }; + // Insert metadata for unpakaged content. + state.packagemeta.insert(ObjectSourceMeta { + identifier: Rc::clone(&state.unpackaged_id), + name: Rc::clone(&state.unpackaged_id), + srcid: Rc::clone(&state.unpackaged_id), + // Assume that content in here changes frequently. + change_time_offset: u32::MAX, + }); + + let mut lowest_change_time = None; + let mut package_meta = HashMap::new(); + for pkg in pkglist.iter() { + let name = pkg.child_value(0); + let name = name.str().unwrap(); + let nevra = Rc::from(gv_nevra_to_string(&pkg).into_boxed_str()); + let pkgmeta = q.package_meta(name)?; + let buildtime = pkgmeta.buildtime(); + if let Some((lowid, lowtime)) = lowest_change_time.as_mut() { + if *lowtime > buildtime { + *lowid = Rc::clone(&nevra); + *lowtime = buildtime; + } + } else { + lowest_change_time = Some((Rc::clone(&nevra), pkgmeta.buildtime())) + } + state.rpmsize += pkgmeta.size(); + package_meta.insert(nevra, pkgmeta); + } + + // SAFETY: There must be at least one package. + let (lowest_change_name, lowest_change_time) = + lowest_change_time.expect("Failed to find any packages"); + // Walk over the packages, and generate the `packagemeta` mapping, which is basically a subset of + // package metadata abstracted for ostree. Note that right now, the package metadata includes + // both a "unique identifer" and a "human readable name", but for rpm-ostree we're just making + // those the same thing. + for (nevra, pkgmeta) in package_meta.iter() { + let buildtime = pkgmeta.buildtime(); + let change_time_offset_secs: u32 = buildtime + .checked_sub(lowest_change_time) + .unwrap() + .try_into() + .unwrap(); + // Convert to hours, because there's no strong use for caring about the relative difference of builds in terms + // of minutes or seconds. + let change_time_offset = change_time_offset_secs / (60 * 60); + state.packagemeta.insert(ObjectSourceMeta { + identifier: Rc::clone(&nevra), + name: Rc::clone(&nevra), + srcid: Rc::from(pkgmeta.src_pkg().to_str().unwrap()), + change_time_offset, + }); + } + + let kernel_dir = ostree_ext::bootabletree::find_kernel_dir(&root, gio::NONE_CANCELLABLE)?; + if let Some(kernel_dir) = kernel_dir { + let kernel_ver: Utf8PathBuf = kernel_dir.basename().unwrap().try_into()?; + let initramfs = kernel_dir.child("initramfs.img"); + if initramfs.query_exists(gio::NONE_CANCELLABLE) { + let path: Utf8PathBuf = initramfs.path().unwrap().try_into()?; + let initramfs = initramfs.downcast_ref::().unwrap(); + let checksum = initramfs.checksum().unwrap(); + let name = format!("initramfs (kernel {})", kernel_ver).into_boxed_str(); + let name = Rc::from(name); + state.content.insert(checksum.to_string(), Rc::clone(&name)); + state.packagemeta.insert(ObjectSourceMeta { + identifier: Rc::clone(&name), + name: Rc::clone(&name), + srcid: Rc::clone(&name), + change_time_offset: u32::MAX, + }); + state.skip.insert(path); + } + } + + let rpmdb = root.resolve_relative_path(crate::composepost::RPMOSTREE_RPMDB_LOCATION); + if rpmdb.query_exists(gio::NONE_CANCELLABLE) { + // TODO add mapping for rpmdb + } + + // Walk the filesystem + build_mapping_recurse(&mut Utf8PathBuf::from("/"), &root, &q, &mut state)?; + let src_pkgs: HashSet<_> = state.packagemeta.iter().map(|p| &p.srcid).collect(); + + // Print out information about what we found + println!( + "{} objects in {} packages ({} source)", + state.content.len(), + state.packagemeta.len(), + src_pkgs.len(), + ); + println!("rpm size: {}", state.rpmsize); + println!( + "Earliest changed package: {} at {}", + lowest_change_name, + Utc.timestamp_opt(lowest_change_time.try_into().unwrap(), 0) + .unwrap() + ); + println!("{} duplicates", state.duplicates.len()); + if !state.multi_provider.is_empty() { + println!("Multiple owners:"); + for v in state.multi_provider.iter() { + println!(" {}", v); + } + } + + // Convert our build state into the state that ostree consumes, discarding + // transient data such as the cases of files owned by multiple packages. + let meta: ObjectMeta = state.into(); + // Now generate the sized version + let meta = ObjectMetaSized::compute_sizes(repo, meta)?; + if let Some(v) = opt.write_contentmeta_json { + let v = v.strip_prefix("/").unwrap_or(&v); + let root = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; + root.replace_file_with(v, |w| { + serde_json::to_writer(w, &meta.sizes).map_err(anyhow::Error::msg) + })?; + } + // TODO: Put this in a public API in ostree-rs-ext? + let labels = opt + .labels + .into_iter() + .map(|l| { + let (k, v) = l + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("Missing '=' in label {}", l))?; + Ok((k.to_string(), v.to_string())) + }) + .collect::>()?; + + let mut copy_meta_keys = opt.copy_meta_keys; + // Default to copying the input hash to support cheap change detection + copy_meta_keys.push("rpmostree.inputhash".to_string()); + + let config = Config { + labels: Some(labels), + cmd: opt.cmd, + }; + let opts = ExportOpts { + copy_meta_keys: copy_meta_keys, + max_layers: opt.max_layers, + ..Default::default() + }; + let digest = ostree_ext::container::encapsulate( + repo, + rev.as_str(), + &config, + Some(opts), + Some(meta), + &opt.imgref, + ) + .await?; + println!("Pushed digest: {}", digest); + Ok(()) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index bb2467393d..eb296febfc 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -679,14 +679,28 @@ pub mod ffi { fn output_message(msg: &str); } - // rpmostree-rpm-util.h unsafe extern "C++" { include!("rpmostree-rpm-util.h"); + #[allow(missing_debug_implementations)] + type RpmTs; + #[allow(missing_debug_implementations)] + type PackageMeta; + // Currently only used in unit tests #[allow(dead_code)] fn nevra_to_cache_branch(nevra: &CxxString) -> Result; fn get_repodata_chksum_repr(pkg: &mut DnfPackage) -> Result; + fn rpmts_for_commit(repo: Pin<&mut OstreeRepo>, rev: &str) -> Result>; + + // Methods on RpmTs + fn packages_providing_file(self: &RpmTs, path: &str) -> Result>; + fn package_meta(self: &RpmTs, name: &str) -> Result>; + + // Methods on PackageMeta + fn size(self: &PackageMeta) -> u64; + fn buildtime(self: &PackageMeta) -> u64; + fn src_pkg(self: &PackageMeta) -> &CxxString; } // rpmostree-package-variants.h diff --git a/rust/src/main.rs b/rust/src/main.rs index bdc2b389f0..617bf2f30e 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -44,6 +44,11 @@ async fn inner_async_main(args: Vec) -> Result { "ex-container" => { return rpmostree_rust::container::entrypoint(&args_borrowed).await; } + // This is a custom wrapper for + "container-encapsulate" => { + rpmostree_rust::container::container_encapsulate(&args_borrowed).await?; + return Ok(0); + } _ => {} } // Everything below here is a blocking API, and run on a worker thread so diff --git a/rust/src/sysroot_upgrade.rs b/rust/src/sysroot_upgrade.rs index 649cfd2c82..1828e3dfb9 100644 --- a/rust/src/sysroot_upgrade.rs +++ b/rust/src/sysroot_upgrade.rs @@ -6,7 +6,7 @@ use crate::cxxrsutil::*; use crate::ffi::{output_message, ContainerImageState}; use anyhow::Result; use ostree::glib; -use ostree_container::store::LayeredImageImporter; +use ostree_container::store::ImageImporter; use ostree_container::store::PrepareResult; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; @@ -31,25 +31,15 @@ async fn pull_container_async( imgref: &OstreeImageReference, ) -> Result { output_message(&format!("Pulling manifest: {}", &imgref)); - let mut imp = LayeredImageImporter::new(repo, imgref, Default::default()).await?; + let mut imp = ImageImporter::new(repo, imgref, Default::default()).await?; let prep = match imp.prepare().await? { PrepareResult::AlreadyPresent(r) => return Ok(r.into()), PrepareResult::Ready(r) => r, }; let digest = prep.manifest_digest.clone(); output_message(&format!("Importing: {} (digest: {})", &imgref, &digest)); - if prep.base_layer.commit.is_none() { - let size = glib::format_size(prep.base_layer.size()); - output_message(&format!( - "Downloading base layer: {} ({})", - prep.base_layer.digest(), - size - )); - } else { - output_message(&format!("Using base: {}", prep.base_layer.digest())); - } // TODO add nice download progress - for layer in prep.layers.iter() { + for layer in prep.all_layers() { if layer.commit.is_some() { output_message(&format!("Using layer: {}", layer.digest())); } else { diff --git a/src/lib/rpmostree-package-priv.h b/src/lib/rpmostree-package-priv.h index 8ea27a0168..f67ff0797e 100644 --- a/src/lib/rpmostree-package-priv.h +++ b/src/lib/rpmostree-package-priv.h @@ -23,6 +23,9 @@ #include "rpmostree-package.h" #include +#ifdef __cplusplus +#include "rust/cxx.h" +#endif G_BEGIN_DECLS diff --git a/src/libpriv/rpmostree-refts.cxx b/src/libpriv/rpmostree-refts.cxx index ec193b4143..553a8e8b90 100644 --- a/src/libpriv/rpmostree-refts.cxx +++ b/src/libpriv/rpmostree-refts.cxx @@ -22,6 +22,9 @@ #include "rpmostree-refts.h" #include "rpmostree-rpm-util.h" +#include "rpmostree-util.h" +#include +#include #include /* @@ -61,3 +64,73 @@ rpmostree_refts_unref (RpmOstreeRefTs *rts) (void)glnx_tmpdir_delete (&rts->tmpdir, NULL, NULL); g_free (rts); } + +namespace rpmostreecxx +{ + +RpmTs::RpmTs (RpmOstreeRefTs *ts) { _ts = ts; } + +RpmTs::~RpmTs () { rpmostree_refts_unref (_ts); } + +rust::Vec +RpmTs::packages_providing_file (const rust::Str path) const +{ + auto path_c = std::string (path); + g_auto (rpmdbMatchIterator) mi + = rpmtsInitIterator (_ts->ts, RPMDBI_INSTFILENAMES, path_c.c_str (), 0); + if (mi == NULL) + mi = rpmtsInitIterator (_ts->ts, RPMDBI_PROVIDENAME, path_c.c_str (), 0); + rust::Vec ret; + if (mi != NULL) + { + Header h; + while ((h = rpmdbNextIterator (mi)) != NULL) + { + g_autofree char *name = headerGetAsString (h, RPMTAG_NEVRA); + ret.push_back (rust::String (name)); + } + } + return ret; +} + +std::unique_ptr +RpmTs::package_meta (const rust::Str name) const +{ + auto name_c = std::string (name); + g_auto (rpmdbMatchIterator) mi = rpmtsInitIterator (_ts->ts, RPMDBI_NAME, name_c.c_str (), 0); + if (mi == NULL) + { + g_autofree char *err = g_strdup_printf ("Package not found: %s", name_c.c_str ()); + throw std::runtime_error (err); + } + Header h; + g_autofree char *previous = NULL; + auto retval = std::make_unique (); + while ((h = rpmdbNextIterator (mi)) != NULL) + { + g_autofree char *nevra = headerGetAsString (h, RPMTAG_NEVRA); + if (previous == NULL) + { + previous = util::move_nullify (nevra); + retval->_size = headerGetNumber (h, RPMTAG_LONGARCHIVESIZE); + retval->_buildtime = headerGetNumber (h, RPMTAG_BUILDTIME); + retval->_src_pkg = headerGetString (h, RPMTAG_SOURCERPM); + } + else + { + // TODO: Somehow we get two `libgcc-8.5.0-10.el8.x86_64` in current RHCOS, I don't + // understand that. + if (!g_str_equal (previous, nevra)) + { + g_autofree char *buf = g_strdup_printf ("Multiple installed '%s' (%s, %s)", + name_c.c_str (), previous, nevra); + throw std::runtime_error (buf); + } + } + } + if (!previous) + g_assert_not_reached (); + return retval; +} + +} diff --git a/src/libpriv/rpmostree-refts.h b/src/libpriv/rpmostree-refts.h index 78e3a59429..b98770054b 100644 --- a/src/libpriv/rpmostree-refts.h +++ b/src/libpriv/rpmostree-refts.h @@ -22,8 +22,11 @@ #pragma once #include "libglnx.h" +#include "rpmostree-util.h" +#include "rust/cxx.h" #include #include +#include G_BEGIN_DECLS @@ -42,4 +45,46 @@ void rpmostree_refts_unref (RpmOstreeRefTs *rts); G_DEFINE_AUTOPTR_CLEANUP_FUNC (RpmOstreeRefTs, rpmostree_refts_unref); +namespace rpmostreecxx +{ + +struct PackageMeta +{ + uint64_t _size; + uint64_t _buildtime; + std::string _src_pkg; + + uint64_t + size () const + { + return _size; + }; + uint64_t + buildtime () const + { + return _buildtime; + }; + const std::string & + src_pkg () const + { + return _src_pkg; + }; +}; + +// A simple C++ wrapper for a librpm C type, so we can expose it to Rust via cxx.rs. +class RpmTs +{ +public: + RpmTs (::RpmOstreeRefTs *ts); + ~RpmTs (); + rpmts get_ts () const; + rust::Vec packages_providing_file (const rust::Str path) const; + std::unique_ptr package_meta (const rust::Str package) const; + +private: + ::RpmOstreeRefTs *_ts; +}; + +} + G_END_DECLS diff --git a/src/libpriv/rpmostree-rpm-util.cxx b/src/libpriv/rpmostree-rpm-util.cxx index b62a67e623..fbcd5f4a82 100644 --- a/src/libpriv/rpmostree-rpm-util.cxx +++ b/src/libpriv/rpmostree-rpm-util.cxx @@ -954,7 +954,10 @@ get_refts_for_rootfs (const char *rootfs, GLnxTmpDir *tmpdir) gboolean rpmostree_get_refts_for_commit (OstreeRepo *repo, const char *ref, RpmOstreeRefTs **out_ts, GCancellable *cancellable, GError **error) + { + CXX_TRY (core_libdnf_process_global_init (), error); + g_auto (GLnxTmpDir) tmpdir = { 0, }; @@ -1596,3 +1599,20 @@ rpmostree_advisories_variant (DnfSack *sack, GPtrArray *pkgs) g_variant_builder_add_value (&builder, advisory_variant_new (advisory, pkgs)); return g_variant_ref_sink (g_variant_builder_end (&builder)); } + +namespace rpmostreecxx +{ + +std::unique_ptr +rpmts_for_commit (OstreeRepo &repo, rust::Str rev) +{ + g_autoptr (GError) local_error = NULL; + RpmOstreeRefTs *refts = NULL; + auto rev_c = std::string (rev); + if (!rpmostree_get_refts_for_commit (&repo, rev_c.c_str (), &refts, NULL, &local_error)) + util::throw_gerror (local_error); + + return std::make_unique (refts); +} + +} \ No newline at end of file diff --git a/src/libpriv/rpmostree-rpm-util.h b/src/libpriv/rpmostree-rpm-util.h index 5c6f3e51d2..9c126be659 100644 --- a/src/libpriv/rpmostree-rpm-util.h +++ b/src/libpriv/rpmostree-rpm-util.h @@ -38,6 +38,7 @@ namespace rpmostreecxx { rust::String nevra_to_cache_branch (const std::string &nevra); rust::String get_repodata_chksum_repr (DnfPackage &pkg); +std::unique_ptr rpmts_for_commit (OstreeRepo &repo, rust::Str rev); } // C code follows