diff options
-rw-r--r-- | buildstream/plugins/sources/bzr.py | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/buildstream/plugins/sources/bzr.py b/buildstream/plugins/sources/bzr.py new file mode 100644 index 000000000..943d07974 --- /dev/null +++ b/buildstream/plugins/sources/bzr.py @@ -0,0 +1,205 @@ +#!/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: +# Jonathan Maw <jonathan.maw@codethink.co.uk> + +"""A source implementation for staging bazaar branches + +**Usage:** + +.. code:: yaml + + # Specify the bzr source kind + kind: bzr + + # Specify the bzr url. Bazaar URLs come in many forms, see + # `bzr help urlspec` for more information. Using an alias defined + # in your project configuration is encouraged. + url: https://launchpad.net/bzr + + # Specify the tracking branch. This is mandatory, as bzr cannot identify + # an individual revision outside its branch. bzr URLs that omit the branch + # name implicitly specify the trunk branch, but bst requires this to be + # explicit. + track: trunk + + # Specify the ref. This is a revision number. This is usually a decimal, + # but revisions on a branch are of the form + # <revision-branched-from>.<branch-number>.<revision-since-branching> + # e.g. 6622.1.6. + # The ref must be specified to build, and 'bst track' will update the + # revision number to the one on the tip of the branch specified in 'track'. + ref: 6622 + +""" + +import os +import shutil +from contextlib import contextmanager + +from buildstream import Source, SourceError, Consistency +from buildstream import utils + + +class BzrSource(Source): + + def configure(self, node): + self.original_url = self.node_get_member(node, str, 'url') + self.tracking = self.node_get_member(node, str, 'track') + self.ref = self.node_get_member(node, str, 'ref', '') or None + self.url = self.get_project().translate_url(self.original_url) + + def preflight(self): + # Check if bzr is installed, get the binary at the same time. + self.host_bzr = utils.get_host_tool('bzr') + + def get_unique_key(self): + return [self.original_url, self.track, self.ref] + + def get_consistency(self): + if self.ref is None or self.tracking is None: + return Consistency.INCONSISTENT + + if self._check_ref(): + return Consistency.CACHED + else: + return Consistency.RESOLVED + + def get_ref(self): + return self.ref + + def set_ref(self, ref, node): + node['ref'] = self.ref = ref + + def track(self): + with self.timed_activity("Tracking {}".format(self.url), + silent_nested=True): + self._ensure_mirror() + ret, out = self.check_output([self.host_bzr, "version-info", + "--custom", "--template={revno}", + self._get_branch_dir()], + fail="Failed to read the revision number at '{}'" + .format(self._get_branch_dir())) + if ret != 0: + raise SourceError("{}: Failed to get ref for tracking {}".format(self, self.tracking)) + + return out + + def fetch(self): + with self.timed_activity("Fetching {}".format(self.url), + silent_nested=True): + self._ensure_mirror() + + def stage(self, directory): + self.call([self.host_bzr, "checkout", "--lightweight", + "--revision=revno:{}".format(self.ref), + self._get_branch_dir(), directory], + fail="Failed to checkout revision {} from branch {} to {}" + .format(self.ref, self._get_branch_dir(), directory)) + + def _check_ref(self): + return self.call([self.host_bzr, "revno", + "--revision=revno:{}".format(self.ref), + self._get_branch_dir()]) == 0 + + def _get_branch_dir(self): + return os.path.join(self._get_mirror_dir(), self.tracking) + + def _get_mirror_dir(self): + return os.path.join(self.get_mirror_directory(), + utils.url_directory_name(self.original_url)) + + def _atomic_replace_mirrordir(self, srcdir): + """Helper function to safely replace the mirror dir""" + + if not os.path.exists(self._get_mirror_dir()): + # Just move the srcdir to the mirror dir + try: + os.rename(srcdir, self._get_mirror_dir()) + except OSError as e: + raise SourceError("{}: Failed to move srcdir '{}' to mirror dir '{}'" + .format(str(self), srcdir, self._get_mirror_dir())) from e + else: + # Atomically swap the backup dir. + backupdir = self._get_mirror_dir() + ".bak" + try: + os.rename(self._get_mirror_dir(), backupdir) + except OSError as e: + raise SourceError("{}: Failed to move mirrordir '{}' to backup dir '{}'" + .format(str(self), self._get_mirror_dir(), backupdir)) from e + + try: + os.rename(srcdir, self._get_mirror_dir()) + except OSError as e: + # Attempt to put the backup back! + os.rename(backupdir, self._get_mirror_dir()) + raise SourceError("{}: Failed to replace bzr repo '{}' with '{}" + .format(str(self), srcdir, self._get_mirror_dir())) from e + finally: + if os.path.exists(backupdir): + shutil.rmtree(backupdir) + + @contextmanager + def _atomic_repodir(self): + """Context manager for working in a copy of the bzr repository + + Yields: + (str): A path to the copy of the bzr repo + + This should be used because bzr does not give any guarantees of + atomicity, and aborting an operation at the wrong time (or + accidentally running multiple concurrent operations) can leave the + repo in an inconsistent state. + """ + with self.tempdir() as repodir: + if os.path.exists(self._get_mirror_dir()): + try: + # shutil.copytree doesn't like it if destination exists + shutil.rmtree(repodir) + shutil.copytree(self._get_mirror_dir(), repodir) + except (shutil.Error, OSError) as e: + raise SourceError("{}: Failed to copy bzr repo from '{}' to '{}'" + .format(str(self), self.mirror_dir, tmpdir)) from e + + yield repodir + self._atomic_replace_mirrordir(repodir) + + def _ensure_mirror(self): + with self._atomic_repodir() as repodir: + # Initialize repo if no metadata + bzr_metadata_dir = os.path.join(repodir, ".bzr") + if not os.path.exists(bzr_metadata_dir): + self.call([self.host_bzr, "init-repo", "--no-trees", repodir], + fail="Failed to initialize bzr repository") + + branch_dir = os.path.join(repodir, self.tracking) + if not os.path.exists(branch_dir): + # `bzr branch` the branch if it doesn't exist + # to get the upstream code + branch_url = self.url + "/" + self.tracking + self.call([self.host_bzr, "branch", branch_url, branch_dir], + fail="Failed to branch from {} to {}".format(branch_url, branch_dir)) + + else: + # `bzr pull` the branch if it does exist + # to get any changes to the upstream code + self.call([self.host_bzr, "pull", "--directory={}".format(branch_dir)], + fail="Failed to pull new changes for {}".format(branch_dir)) + + +def setup(): + return BzrSource |