#!/usr/bin/env python3 # # Copyright (C) 2017 Codethink Limited # # 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 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 . # # Authors: # Jürg Billeter # Andrew Leeming # Tristan Van Berkom # # Code based on Jürg's artifact cache and Andrew's ostree plugin # import os import gi gi.require_version('OSTree', '1.0') from gi.repository import GLib, Gio, OSTree # nopep8 from gi.repository.GLib import Variant, VariantDict # nopep8 # For users of this file, they must expect (except) it. class OSTreeError(Exception): pass # ensure() # # Args: # path (str): The file path to where the desired repo should be # compress (bool): use compression or not when creating # # Returns: an OSTree.Repo def ensure(path, compress): # create also succeeds on existing repository repo = OSTree.Repo.new(Gio.File.new_for_path(path)) mode = OSTree.RepoMode.ARCHIVE_Z2 if compress \ else OSTree.RepoMode.BARE_USER repo.create(mode) return repo # checkout() # # Checkout the content at 'commit' from 'repo' in # the specified 'path' # # Args: # repo (OSTree.Repo): The repo # path (str): The checkout path # commit (str): The commit checksum to checkout # def checkout(repo, path, commit): # Check out a full copy of an OSTree at a given ref to some directory. # # Note: OSTree does not like updating directories inline/sync, therefore # make sure you checkout to a clean directory or add additional code to support # union mode or (if it exists) file replacement/update. # # Returns True on success # # cli exmaple: # ostree --repo=repo checkout --user-mode runtime/org.freedesktop.Sdk/x86_64/1.4 foo os.makedirs(os.path.dirname(path), exist_ok=True) # ignore uid/gid to allow checkout as non-root options = OSTree.RepoCheckoutAtOptions() options.mode = OSTree.RepoCheckoutMode.USER # XXX How come this works if we give the CWD as the # file descriptor of the directory os.path.dirname(path) ? # # from fcntl.h AT_FDCWD = -100 try: repo.checkout_at(options, AT_FDCWD, path, commit) except GLib.GError as e: raise OSTreeError("Failed to checkout commit '{}': {}".format(commit, e.message)) from e # commit(): # # Commit built artifact to cache. # # Files are all recorded with uid/gid 0 # # Args: # repo (OSTree.Repo): The repo # dir (str): The source directory to commit to the repo # ref (str): A symbolic reference (tag) for the commit # def commit(repo, dir, ref): # We commit everything with uid/gid 0 def commit_filter(repo, path, file_info): file_info.set_attribute_uint32('unix::uid', 0) file_info.set_attribute_uint32('unix::gid', 0) return OSTree.RepoCommitFilterResult.ALLOW commit_modifier = OSTree.RepoCommitModifier.new( OSTree.RepoCommitModifierFlags.NONE, commit_filter) repo.prepare_transaction() try: # add tree to repository mtree = OSTree.MutableTree.new() repo.write_directory_to_mtree(Gio.File.new_for_path(dir), mtree, commit_modifier) _, root = repo.write_mtree(mtree) # create root commit object, no parent, no branch _, rev = repo.write_commit(None, ref, None, None, root) # create tag repo.transaction_set_ref(None, ref, rev) # complete repo transaction repo.commit_transaction(None) except: repo.abort_transaction() raise # exists(): # # Checks wether a given commit or symbolic ref exists in # the specified repo. # # Args: # repo (OSTree.Repo): The repo # ref (str): A commit checksum or symbolic ref # # Returns: # (bool): Whether 'ref' is valid in 'repo' # def exists(repo, ref): _, commit = repo.resolve_rev(ref, True) return commit is not None # checksum(): # # Returns the commit checksum for a given symbolic ref, # which might be a branch or tag. If it is a branch, # the latest commit checksum for the given branch is returned. # # Args: # repo (OSTree.Repo): The repo # ref (str): The symbolic ref # # Returns: # (str): The commit checksum, or None if ref does not exist. # def checksum(repo, ref): _, checksum = repo.resolve_rev(ref, True) return checksum # fetch() # # Fetch new objects from origin, if configured # # Args: # repo (OSTree.Repo): The repo # ref (str): An optional ref to fetch, will reduce the amount of objects fetched # progress (callable): An optional progress callback # # Note that a commit checksum or a branch reference are both # valid options for the 'ref' parameter. Using the ref parameter # can save a lot of bandwidth but mirroring the full repo is # still possible. # def fetch(repo, ref=None, progress=None): # Fetch metadata of the repo from a remote # # cli example: # ostree --repo=repo pull --mirror freedesktop:runtime/org.freedesktop.Sdk/x86_64/1.4 def progress_callback(info): status = async_progress.get_status() outstanding_fetches = async_progress.get_uint('outstanding-fetches') bytes_transferred = async_progress.get_uint64('bytes-transferred') fetched = async_progress.get_uint('fetched') requested = async_progress.get_uint('requested') if status: progress(0.0, status) elif outstanding_fetches > 0: formatted_bytes = GLib.format_size_full(bytes_transferred, 0) if requested == 0: percent = 0.0 else: percent = (fetched * 1.0 / requested) * 100 progress(percent, "Receiving objects: %d%% (%d/%d) %s" % (percent, fetched, requested, formatted_bytes)) else: progress(100.0, "Writing Objects") async_progress = None if progress is not None: async_progress = OSTree.AsyncProgress.new() async_progress.connect('changed', progress_callback) # FIXME: This hangs the process and ignores keyboard interrupt, # fix this using the Gio.Cancellable refs = None if ref is not None: refs = [ref] try: repo.pull("origin", refs, OSTree.RepoPullFlags.MIRROR, async_progress, None) # Gio.Cancellable except GLib.GError as e: if ref is not None: raise OSTreeError("Failed to fetch ref '{}' from origin: {}".format(ref, e.message)) from e else: raise OSTreeError("Failed to fetch from origin: {}".format(e.message)) from e # configure_origin(): # # Ensures a remote origin is setup to a given url. # # Args: # repo (OSTree.Repo): The repo # url (str): The url of the remote ostree repo # key_url (str): The optional url of a GPG key (should be a local file) # def configure_origin(repo, url, key_url=None): # Add a remote OSTree repo. If no key is given, we disable gpg checking. # # cli exmaple: # wget https://sdk.gnome.org/keys/gnome-sdk.gpg # ostree --repo=repo --gpg-import=gnome-sdk.gpg remote add freedesktop https://sdk.gnome.org/repo options = None # or GLib.Variant of type a{sv} if key_url is None: vd = VariantDict.new() vd.insert_value('gpg-verify', Variant.new_boolean(False)) options = vd.end() repo.remote_change(None, # Optional OSTree.Sysroot OSTree.RepoRemoteChange.ADD_IF_NOT_EXISTS, "origin", # Remote name url, # Remote url options, # Remote options None) # Optional Gio.Cancellable # Remote needs to exist before adding key if key_url is not None: try: gfile = Gio.File.new_for_uri(key_url) stream = gfile.read() repo.remote_gpg_import("origin", stream, None, 0, None) except GLib.GError as e: raise OSTreeError("Failed to add gpg key from url '{}': {}".format(key_url, e.message)) from e