blob: 4e0d703601e9e85bdb3c0b30e5a6e4ce77ca9801 [file] [log] [blame]
#!/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 <http://www.gnu.org/licenses/>.
#
# Authors:
# Jürg Billeter <juerg.billeter@codethink.co.uk>
import multiprocessing
import os
import sys
import tempfile
from .. import _ostree, utils
from ..exceptions import _ArtifactError
from ..element import _KeyStrength
from .._ostree import OSTreeError
from .pushreceive import check_push_connection
from .pushreceive import push as push_artifact
from .pushreceive import PushException
def buildref(element, key):
project = element.get_project()
# Normalize ostree ref unsupported chars
element_name = element.normal_name.replace('+', 'X')
# assume project and element names are not allowed to contain slashes
return '{0}/{1}/{2}'.format(project.name, element_name, key)
# An ArtifactCache manages artifacts in an OSTree repository
#
# Args:
# context (Context): The BuildStream context
#
class ArtifactCache():
def __init__(self, context):
self.context = context
os.makedirs(context.artifactdir, exist_ok=True)
ostreedir = os.path.join(context.artifactdir, 'ostree')
self.extractdir = os.path.join(context.artifactdir, 'extract')
self.repo = _ostree.ensure(ostreedir, False)
if self.context.artifact_pull:
self.remote = utils.url_directory_name(context.artifact_pull)
_ostree.configure_remote(self.repo, self.remote, self.context.artifact_pull)
else:
self.remote = None
self.__remote_refs = None
self.__offline = False
def preflight(self):
if self.can_push() and not self.context.artifact_push.startswith("/"):
try:
check_push_connection(self.context.artifact_push,
self.context.artifact_push_port)
except PushException as e:
raise _ArtifactError("BuildStream will be unable to push artifacts "
"to the shared cache: {}".format(e))
# contains():
#
# Check whether the artifact for the specified Element is already available
# in the local artifact cache.
#
# Args:
# element (Element): The Element to check
# strength (_KeyStrength): Either STRONG or WEAK key strength, or None
#
# Returns: True if the artifact is in the cache, False otherwise
#
def contains(self, element, strength=None):
if strength is None:
strength = _KeyStrength.STRONG if self.context.strict_build_plan else _KeyStrength.WEAK
key = element._get_cache_key(strength)
if not key:
return False
ref = buildref(element, key)
return _ostree.exists(self.repo, ref)
# remote_contains_key():
#
# Check whether the artifact for the specified Element is already available
# in the remote artifact cache.
#
# Args:
# element (Element): The Element to check
# key (str): The key to use
#
# Returns: True if the artifact is in the cache, False otherwise
#
def remote_contains_key(self, element, key):
if not self.__remote_refs:
return False
ref = buildref(element, key)
return ref in self.__remote_refs
# remote_contains():
#
# Check whether the artifact for the specified Element is already available
# in the remote artifact cache.
#
# Args:
# element (Element): The Element to check
# strength (_KeyStrength): Either STRONG or WEAK key strength, or None
#
# Returns: True if the artifact is in the cache, False otherwise
#
def remote_contains(self, element, strength=None):
if strength is None:
strength = _KeyStrength.STRONG if self.context.strict_build_plan else _KeyStrength.WEAK
key = element._get_cache_key(strength)
if not key:
return False
return self.remote_contains_key(element, key)
# remove():
#
# Removes the artifact for the specified Element from the local artifact
# cache.
#
# Args:
# element (Element): The Element to remove
#
def remove(self, element):
key = element._get_cache_key()
if not key:
return
ref = buildref(element, key)
_ostree.remove(self.repo, ref)
# extract():
#
# Extract cached artifact for the specified Element if it hasn't
# already been extracted.
#
# Assumes artifact has previously been fetched or committed.
#
# Args:
# element (Element): The Element to extract
#
# Raises:
# _ArtifactError: In cases there was an OSError, or if the artifact
# did not exist.
#
# Returns: path to extracted artifact
#
def extract(self, element):
ref = buildref(element, element._get_cache_key())
# resolve ref to checksum
rev = _ostree.checksum(self.repo, ref)
# resolve weak cache key, if artifact is missing for strong cache key
# and the context allows use of weak cache keys
if not rev and not self.context.strict_build_plan:
ref = buildref(element, element._get_cache_key(strength=_KeyStrength.WEAK))
rev = _ostree.checksum(self.repo, ref)
if not rev:
raise _ArtifactError("Artifact missing for {}".format(ref))
dest = os.path.join(self.extractdir, element.get_project().name, element.normal_name, rev)
if os.path.isdir(dest):
# artifact has already been extracted
return dest
os.makedirs(self.extractdir, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='tmp', dir=self.extractdir) as tmpdir:
checkoutdir = os.path.join(tmpdir, ref)
_ostree.checkout(self.repo, checkoutdir, rev, user=True)
os.makedirs(os.path.dirname(dest), exist_ok=True)
try:
os.rename(checkoutdir, dest)
except OSError as e:
# With rename, it's possible to get either ENOTEMPTY or EEXIST
# in the case that the destination path is a not empty directory.
#
# If rename fails with these errors, another process beat
# us to it so just ignore.
if e.errno not in [os.errno.ENOTEMPTY, os.errno.EEXIST]:
raise _ArtifactError("Failed to extract artifact for ref '{}': {}"
.format(ref, e)) from e
return dest
# commit():
#
# Commit built artifact to cache.
#
# Args:
# element (Element): The Element commit an artifact for
# content (str): The element's content directory
#
def commit(self, key, weak_key, element, content):
# tag with strong cache key based on dependency versions used for the build
ref = buildref(element, key)
# also store under weak cache key
weak_ref = buildref(element, weak_key)
_ostree.commit(self.repo, content, ref, weak_ref)
# can_fetch():
#
# Check whether remote repository is available for fetching.
#
# Returns: True if remote repository is available, False otherwise
#
def can_fetch(self):
return not self.__offline and self.remote is not None
# pull():
#
# Pull artifact from remote repository.
#
# Args:
# element (Element): The Element whose artifact is to be fetched
# progress (callable): The progress callback, if any
#
def pull(self, element, progress=None):
if self.__offline:
raise _ArtifactError("Attempt to pull artifact while offline")
if self.context.artifact_pull.startswith("/"):
remote = "file://" + self.context.artifact_pull
elif self.remote is not None:
remote = self.remote
else:
raise _ArtifactError("Attempt to pull artifact without any pull URL")
weak_ref = buildref(element, element._get_cache_key(strength=_KeyStrength.WEAK))
try:
if self.remote_contains(element, strength=_KeyStrength.STRONG):
# fetch the artifact using the strong cache key
ref = buildref(element, element._get_cache_key())
_ostree.fetch(self.repo, remote=remote,
ref=ref, progress=progress)
# resolve ref to checksum
rev = _ostree.checksum(self.repo, ref)
# update weak ref by pointing it to this newly fetched artifact
_ostree.set_ref(self.repo, weak_ref, rev)
elif self.remote_contains(element):
# fetch the artifact using the weak cache key
_ostree.fetch(self.repo, remote=remote,
ref=weak_ref, progress=progress)
# extract strong cache key from this newly fetched artifact
element._cached(recalculate=True)
ref = buildref(element, element._get_cache_key_from_artifact())
# resolve ref to checksum
rev = _ostree.checksum(self.repo, ref)
# create tag for strong cache key
_ostree.set_ref(self.repo, ref, rev)
else:
raise _ArtifactError("Attempt to pull unavailable artifact for element {}"
.format(element.name))
except OSTreeError as e:
raise _ArtifactError("Failed to pull artifact for element {}: {}"
.format(element.name, e)) from e
# fetch_remote_refs():
#
# Fetch list of artifacts from remote repository.
#
def fetch_remote_refs(self):
if self.context.artifact_pull.startswith("/"):
remote = "file://" + self.context.artifact_pull
elif self.remote is not None:
remote = self.remote
else:
raise _ArtifactError("Attempt to fetch remote refs without any pull URL")
def child_action(repo, remote, q):
try:
q.put((True, _ostree.list_remote_refs(self.repo, remote=remote)))
except OSTreeError as e:
q.put((False, e))
q = multiprocessing.Queue()
p = multiprocessing.Process(target=child_action, args=(self.repo, remote, q))
p.start()
ret, res = q.get()
p.join()
if ret:
self.__remote_refs = res
else:
raise _ArtifactError("Failed to fetch remote refs") from res
# can_push():
#
# Check whether remote repository is available for pushing.
#
# Returns: True if remote repository is available, False otherwise
#
def can_push(self):
return not self.__offline and self.context.artifact_push is not None
# push():
#
# Push committed artifact to remote repository.
#
# Args:
# element (Element): The Element whose artifact is to be pushed
#
# Returns:
# (bool): True if the remote was updated, False if it already existed
# and no updated was required
#
# Raises:
# _ArtifactError if there was an error
def push(self, element):
if self.__offline:
raise _ArtifactError("Attempt to push artifact while offline")
if self.context.artifact_push is None:
raise _ArtifactError("Attempt to push artifact without any push URL")
ref = buildref(element, element._get_cache_key_from_artifact())
weak_ref = buildref(element, element._get_cache_key(strength=_KeyStrength.WEAK))
if self.context.artifact_push.startswith("/"):
# local repository
push_repo = _ostree.ensure(self.context.artifact_push, True)
_ostree.fetch(push_repo, remote=self.repo.get_path().get_uri(), ref=ref)
_ostree.fetch(push_repo, remote=self.repo.get_path().get_uri(), ref=weak_ref)
# Local remotes are not really a thing, just return True here
return True
else:
# Push over ssh
#
with utils._tempdir(dir=self.context.artifactdir, prefix='push-repo-') as temp_repo_dir:
with element.timed_activity("Preparing compressed archive"):
# First create a temporary archive-z2 repository, we can
# only use ostree-push with archive-z2 local repo.
temp_repo = _ostree.ensure(temp_repo_dir, True)
# Now push the ref we want to push into our temporary archive-z2 repo
_ostree.fetch(temp_repo, remote=self.repo.get_path().get_uri(), ref=ref)
_ostree.fetch(temp_repo, remote=self.repo.get_path().get_uri(), ref=weak_ref)
with element.timed_activity("Sending artifact"), \
element._output_file() as output_file:
try:
pushed = push_artifact(temp_repo.get_path().get_path(),
self.context.artifact_push,
self.context.artifact_push_port,
[ref, weak_ref], output_file)
except PushException as e:
raise _ArtifactError("Failed to push artifact {}: {}".format(ref, e)) from e
return pushed
# set_offline():
#
# Do not attempt to pull or push artifacts.
#
def set_offline(self):
self.__offline = True