blob: abcb16aa0402f1da59a8850cb9f7b1d63c84477c [file] [log] [blame]
#
# Copyright (C) 2018 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 <http://www.gnu.org/licenses/>.
#
# Authors:
# Andrew Leeming <andrew.leeming@codethink.co.uk>
# Tiago Gomes <tiago.gomes@codethink.co.uk>
"""
ostree - stage files from an OSTree repository
==============================================
**Usage:**
.. code:: yaml
# Specify the ostree source kind
kind: ostree
# Optionally specify a relative staging directory
# directory: path/to/stage
# Specify the repository url, using an alias defined
# in your project configuration is recommended.
url: upstream:runtime
# Optionally specify a symbolic tracking branch or tag, this
# will be used to update the 'ref' when refreshing the pipeline.
track: runtime/x86_64/stable
# Specify the commit checksum, this must be specified in order
# to checkout sources and build, but can be automatically
# updated if the 'track' attribute was specified.
ref: d63cbb6fdc0bbdadc4a1b92284826a6d63a7ebcd
# For signed ostree repositories, specify a local project relative
# path to the public verifying GPG key for this remote.
gpg-key: keys/runtime.gpg
"""
import os
import shutil
from buildstream import Source, SourceError, Consistency
from buildstream import utils
class OSTreeSource(Source):
# pylint: disable=attribute-defined-outside-init
def configure(self, node):
self.node_validate(node, ['url', 'ref', 'track', 'gpg-key'] + Source.COMMON_CONFIG_KEYS)
self.ostree = None
self.original_url = self.node_get_member(node, str, 'url')
self.url = self.translate_url(self.original_url)
self.ref = self.node_get_member(node, str, 'ref', None)
self.tracking = self.node_get_member(node, str, 'track', None)
self.mirror = os.path.join(self.get_mirror_directory(),
utils.url_directory_name(self.original_url))
# At this point we now know if the source has a ref and/or a track.
# If it is missing both then we will be unable to track or build.
if self.ref is None and self.tracking is None:
raise SourceError("{}: OSTree sources require a ref and/or track".format(self),
reason="missing-track-and-ref")
# (optional) Not all repos are signed. But if they are, get the gpg key
self.gpg_key_path = None
if self.node_get_member(node, str, 'gpg-key', None):
self.gpg_key = self.node_get_project_path(node, 'gpg-key',
check_is_file=True)
self.gpg_key_path = os.path.join(self.get_project_directory(), self.gpg_key)
# Our OSTree repo handle
self.repo = None
def preflight(self):
# Check if ostree is installed, get the binary at the same time
self.ostree = utils.get_host_tool("ostree")
def get_unique_key(self):
return [self.original_url, self.ref]
def load_ref(self, node):
self.ref = self.node_get_member(node, str, 'ref', None)
def get_ref(self):
return self.ref
def set_ref(self, ref, node):
node['ref'] = self.ref = ref
def track(self):
# If self.tracking is not specified it's not an error, just silently return
if not self.tracking:
return None
self.ensure()
remote_name = self.ensure_remote(self.url)
with self.timed_activity(
"Fetching tracking ref '{}' from origin: {}".format(
self.tracking, self.url
)
):
self.call(
[
self.ostree,
"pull",
"--repo",
self.mirror,
"--mirror",
remote_name,
self.tracking,
],
fail="Failed to fetch tracking ref '{}' from origin {}".format(
self.tracking, self.url
),
)
return self.check_output(
[self.ostree, "rev-parse", "--repo", self.mirror, self.tracking],
fail="Failed to compute checksum of '{}' on '{}'".format(
self.tracking, self.mirror
),
)[1]
def fetch(self):
self.ensure()
remote_name = self.ensure_remote(self.url)
with self.timed_activity(
"Fetching remote ref: {} from origin: {}".format(
self.ref, self.url
)
):
self.call(
[
self.ostree,
"pull",
"--repo",
self.mirror,
"--mirror",
remote_name,
self.ref,
],
fail="Failed to fetch ref '{}' from origin: {}".format(
self.ref, remote_name
),
)
def stage(self, directory):
self.ensure()
# Checkout self.ref into the specified directory
with self.tempdir() as tmpdir:
checkoutdir = os.path.join(tmpdir, "checkout")
with self.timed_activity(
"Staging ref: {} from origin: {}".format(self.ref, self.url)
):
self.call(
[
self.ostree,
"checkout",
"--repo",
self.mirror,
"--user-mode",
self.ref,
checkoutdir,
],
fail="Failed to checkout ref '{}' from origin: {}".format(
self.ref, self.url
),
)
# The target directory is guaranteed to exist, here we must move the
# content of out checkout into the existing target directory.
#
# We may not be able to create the target directory as its parent
# may be readonly, and the directory itself is often a mount point.
#
try:
for entry in os.listdir(checkoutdir):
source_path = os.path.join(checkoutdir, entry)
shutil.move(source_path, directory)
except (shutil.Error, OSError) as e:
raise SourceError(
"{}: Failed to move ostree checkout {} from '{}' to '{}'\n\n{}".format(
self, self.url, tmpdir, directory, e
)
) from e
def get_consistency(self):
if self.ref is None:
return Consistency.INCONSISTENT
elif os.path.exists(self.mirror):
if self.call([self.ostree, "show", "--repo", self.mirror, self.ref]) == 0:
return Consistency.CACHED
return Consistency.RESOLVED
#
# Local helpers
#
def ensure(self):
if not os.path.exists(self.mirror):
self.status("Creating local mirror for {}".format(self.url))
self.call(
[
self.ostree,
"init",
"--repo",
self.mirror,
"--mode",
"archive-z2",
],
fail="Unable to create local mirror for repository",
)
self.call(
[
self.ostree,
"config",
"--repo",
self.mirror,
"set",
"core.min-free-space-percent",
"0",
],
fail="Unable to disable minimum disk space checks",
)
def ensure_remote(self, url):
if self.original_url == self.url:
remote_name = "origin"
else:
remote_name = utils.url_directory_name(url)
command = [
self.ostree,
"remote",
"add",
"--if-not-exists",
"--repo",
self.mirror,
remote_name,
url,
]
if self.gpg_key_path:
command.extend(["--gpg-import", self.gpg_key_path])
else:
command.extend(["--no-gpg-verify"])
self.call(command, fail="Failed to configure origin {}".format(url))
return remote_name
# Plugin entry point
def setup():
return OSTreeSource