Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/steps/source.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/steps/source.py')
-rw-r--r--buildbot/buildbot/steps/source.py1107
1 files changed, 1107 insertions, 0 deletions
diff --git a/buildbot/buildbot/steps/source.py b/buildbot/buildbot/steps/source.py
new file mode 100644
index 0000000..4571ad5
--- /dev/null
+++ b/buildbot/buildbot/steps/source.py
@@ -0,0 +1,1107 @@
+# -*- test-case-name: buildbot.test.test_vc -*-
+
+from warnings import warn
+from email.Utils import formatdate
+from twisted.python import log
+from buildbot.process.buildstep import LoggingBuildStep, LoggedRemoteCommand
+from buildbot.interfaces import BuildSlaveTooOldError
+from buildbot.status.builder import SKIPPED
+
+
+class Source(LoggingBuildStep):
+ """This is a base class to generate a source tree in the buildslave.
+ Each version control system has a specialized subclass, and is expected
+ to override __init__ and implement computeSourceRevision() and
+ startVC(). The class as a whole builds up the self.args dictionary, then
+ starts a LoggedRemoteCommand with those arguments.
+ """
+
+ # if the checkout fails, there's no point in doing anything else
+ haltOnFailure = True
+ flunkOnFailure = True
+ notReally = False
+
+ branch = None # the default branch, should be set in __init__
+
+ def __init__(self, workdir=None, mode='update', alwaysUseLatest=False,
+ timeout=20*60, retry=None, **kwargs):
+ """
+ @type workdir: string
+ @param workdir: local directory (relative to the Builder's root)
+ where the tree should be placed
+
+ @type mode: string
+ @param mode: the kind of VC operation that is desired:
+ - 'update': specifies that the checkout/update should be
+ performed directly into the workdir. Each build is performed
+ in the same directory, allowing for incremental builds. This
+ minimizes disk space, bandwidth, and CPU time. However, it
+ may encounter problems if the build process does not handle
+ dependencies properly (if you must sometimes do a 'clean
+ build' to make sure everything gets compiled), or if source
+ files are deleted but generated files can influence test
+ behavior (e.g. python's .pyc files), or when source
+ directories are deleted but generated files prevent CVS from
+ removing them.
+
+ - 'copy': specifies that the source-controlled workspace
+ should be maintained in a separate directory (called the
+ 'copydir'), using checkout or update as necessary. For each
+ build, a new workdir is created with a copy of the source
+ tree (rm -rf workdir; cp -R -P -p copydir workdir). This
+ doubles the disk space required, but keeps the bandwidth low
+ (update instead of a full checkout). A full 'clean' build
+ is performed each time. This avoids any generated-file
+ build problems, but is still occasionally vulnerable to
+ problems such as a CVS repository being manually rearranged
+ (causing CVS errors on update) which are not an issue with
+ a full checkout.
+
+ - 'clobber': specifies that the working directory should be
+ deleted each time, necessitating a full checkout for each
+ build. This insures a clean build off a complete checkout,
+ avoiding any of the problems described above, but is
+ bandwidth intensive, as the whole source tree must be
+ pulled down for each build.
+
+ - 'export': is like 'clobber', except that e.g. the 'cvs
+ export' command is used to create the working directory.
+ This command removes all VC metadata files (the
+ CVS/.svn/{arch} directories) from the tree, which is
+ sometimes useful for creating source tarballs (to avoid
+ including the metadata in the tar file). Not all VC systems
+ support export.
+
+ @type alwaysUseLatest: boolean
+ @param alwaysUseLatest: whether to always update to the most
+ recent available sources for this build.
+
+ Normally the Source step asks its Build for a list of all
+ Changes that are supposed to go into the build, then computes a
+ 'source stamp' (revision number or timestamp) that will cause
+ exactly that set of changes to be present in the checked out
+ tree. This is turned into, e.g., 'cvs update -D timestamp', or
+ 'svn update -r revnum'. If alwaysUseLatest=True, bypass this
+ computation and always update to the latest available sources
+ for each build.
+
+ The source stamp helps avoid a race condition in which someone
+ commits a change after the master has decided to start a build
+ but before the slave finishes checking out the sources. At best
+ this results in a build which contains more changes than the
+ buildmaster thinks it has (possibly resulting in the wrong
+ person taking the blame for any problems that result), at worst
+ is can result in an incoherent set of sources (splitting a
+ non-atomic commit) which may not build at all.
+
+ @type retry: tuple of ints (delay, repeats) (or None)
+ @param retry: if provided, VC update failures are re-attempted up
+ to REPEATS times, with DELAY seconds between each
+ attempt. Some users have slaves with poor connectivity
+ to their VC repository, and they say that up to 80% of
+ their build failures are due to transient network
+ failures that could be handled by simply retrying a
+ couple times.
+
+ """
+
+ LoggingBuildStep.__init__(self, **kwargs)
+ self.addFactoryArguments(workdir=workdir,
+ mode=mode,
+ alwaysUseLatest=alwaysUseLatest,
+ timeout=timeout,
+ retry=retry,
+ )
+
+ assert mode in ("update", "copy", "clobber", "export")
+ if retry:
+ delay, repeats = retry
+ assert isinstance(repeats, int)
+ assert repeats > 0
+ self.args = {'mode': mode,
+ 'workdir': workdir,
+ 'timeout': timeout,
+ 'retry': retry,
+ 'patch': None, # set during .start
+ }
+ self.alwaysUseLatest = alwaysUseLatest
+
+ # Compute defaults for descriptions:
+ description = ["updating"]
+ descriptionDone = ["update"]
+ if mode == "clobber":
+ description = ["checkout"]
+ # because checkingouting takes too much space
+ descriptionDone = ["checkout"]
+ elif mode == "export":
+ description = ["exporting"]
+ descriptionDone = ["export"]
+ self.description = description
+ self.descriptionDone = descriptionDone
+
+ def setDefaultWorkdir(self, workdir):
+ self.args['workdir'] = self.args['workdir'] or workdir
+
+ def describe(self, done=False):
+ if done:
+ return self.descriptionDone
+ return self.description
+
+ def computeSourceRevision(self, changes):
+ """Each subclass must implement this method to do something more
+ precise than -rHEAD every time. For version control systems that use
+ repository-wide change numbers (SVN, P4), this can simply take the
+ maximum such number from all the changes involved in this build. For
+ systems that do not (CVS), it needs to create a timestamp based upon
+ the latest Change, the Build's treeStableTimer, and an optional
+ self.checkoutDelay value."""
+ return None
+
+ def start(self):
+ if self.notReally:
+ log.msg("faking %s checkout/update" % self.name)
+ self.step_status.setText(["fake", self.name, "successful"])
+ self.addCompleteLog("log",
+ "Faked %s checkout/update 'successful'\n" \
+ % self.name)
+ return SKIPPED
+
+ # what source stamp would this build like to use?
+ s = self.build.getSourceStamp()
+ # if branch is None, then use the Step's "default" branch
+ branch = s.branch or self.branch
+ # if revision is None, use the latest sources (-rHEAD)
+ revision = s.revision
+ if not revision and not self.alwaysUseLatest:
+ revision = self.computeSourceRevision(s.changes)
+ # if patch is None, then do not patch the tree after checkout
+
+ # 'patch' is None or a tuple of (patchlevel, diff)
+ patch = s.patch
+ if patch:
+ self.addCompleteLog("patch", patch[1])
+
+ self.startVC(branch, revision, patch)
+
+ def commandComplete(self, cmd):
+ if cmd.updates.has_key("got_revision"):
+ got_revision = cmd.updates["got_revision"][-1]
+ if got_revision is not None:
+ self.setProperty("got_revision", str(got_revision), "Source")
+
+
+
+class CVS(Source):
+ """I do CVS checkout/update operations.
+
+ Note: if you are doing anonymous/pserver CVS operations, you will need
+ to manually do a 'cvs login' on each buildslave before the slave has any
+ hope of success. XXX: fix then, take a cvs password as an argument and
+ figure out how to do a 'cvs login' on each build
+ """
+
+ name = "cvs"
+
+ #progressMetrics = ('output',)
+ #
+ # additional things to track: update gives one stderr line per directory
+ # (starting with 'cvs server: Updating ') (and is fairly stable if files
+ # is empty), export gives one line per directory (starting with 'cvs
+ # export: Updating ') and another line per file (starting with U). Would
+ # be nice to track these, requires grepping LogFile data for lines,
+ # parsing each line. Might be handy to have a hook in LogFile that gets
+ # called with each complete line.
+
+ def __init__(self, cvsroot, cvsmodule,
+ global_options=[], branch=None, checkoutDelay=None,
+ login=None,
+ **kwargs):
+
+ """
+ @type cvsroot: string
+ @param cvsroot: CVS Repository from which the source tree should
+ be obtained. '/home/warner/Repository' for local
+ or NFS-reachable repositories,
+ ':pserver:anon@foo.com:/cvs' for anonymous CVS,
+ 'user@host.com:/cvs' for non-anonymous CVS or
+ CVS over ssh. Lots of possibilities, check the
+ CVS documentation for more.
+
+ @type cvsmodule: string
+ @param cvsmodule: subdirectory of CVS repository that should be
+ retrieved
+
+ @type login: string or None
+ @param login: if not None, a string which will be provided as a
+ password to the 'cvs login' command, used when a
+ :pserver: method is used to access the repository.
+ This login is only needed once, but must be run
+ each time (just before the CVS operation) because
+ there is no way for the buildslave to tell whether
+ it was previously performed or not.
+
+ @type branch: string
+ @param branch: the default branch name, will be used in a '-r'
+ argument to specify which branch of the source tree
+ should be used for this checkout. Defaults to None,
+ which means to use 'HEAD'.
+
+ @type checkoutDelay: int or None
+ @param checkoutDelay: if not None, the number of seconds to put
+ between the last known Change and the
+ timestamp given to the -D argument. This
+ defaults to exactly half of the parent
+ Build's .treeStableTimer, but it could be
+ set to something else if your CVS change
+ notification has particularly weird
+ latency characteristics.
+
+ @type global_options: list of strings
+ @param global_options: these arguments are inserted in the cvs
+ command line, before the
+ 'checkout'/'update' command word. See
+ 'cvs --help-options' for a list of what
+ may be accepted here. ['-r'] will make
+ the checked out files read only. ['-r',
+ '-R'] will also assume the repository is
+ read-only (I assume this means it won't
+ use locks to insure atomic access to the
+ ,v files)."""
+
+ self.checkoutDelay = checkoutDelay
+ self.branch = branch
+
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(cvsroot=cvsroot,
+ cvsmodule=cvsmodule,
+ global_options=global_options,
+ branch=branch,
+ checkoutDelay=checkoutDelay,
+ login=login,
+ )
+
+ self.args.update({'cvsroot': cvsroot,
+ 'cvsmodule': cvsmodule,
+ 'global_options': global_options,
+ 'login': login,
+ })
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ lastChange = max([c.when for c in changes])
+ if self.checkoutDelay is not None:
+ when = lastChange + self.checkoutDelay
+ else:
+ lastSubmit = max([r.submittedAt for r in self.build.requests])
+ when = (lastChange + lastSubmit) / 2
+ return formatdate(when)
+
+ def startVC(self, branch, revision, patch):
+ if self.slaveVersionIsOlderThan("cvs", "1.39"):
+ # the slave doesn't know to avoid re-using the same sourcedir
+ # when the branch changes. We have no way of knowing which branch
+ # the last build used, so if we're using a non-default branch and
+ # either 'update' or 'copy' modes, it is safer to refuse to
+ # build, and tell the user they need to upgrade the buildslave.
+ if (branch != self.branch
+ and self.args['mode'] in ("update", "copy")):
+ m = ("This buildslave (%s) does not know about multiple "
+ "branches, and using mode=%s would probably build the "
+ "wrong tree. "
+ "Refusing to build. Please upgrade the buildslave to "
+ "buildbot-0.7.0 or newer." % (self.build.slavename,
+ self.args['mode']))
+ log.msg(m)
+ raise BuildSlaveTooOldError(m)
+
+ if branch is None:
+ branch = "HEAD"
+ self.args['branch'] = branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+
+ if self.args['branch'] == "HEAD" and self.args['revision']:
+ # special case. 'cvs update -r HEAD -D today' gives no files
+ # TODO: figure out why, see if it applies to -r BRANCH
+ self.args['branch'] = None
+
+ # deal with old slaves
+ warnings = []
+ slavever = self.slaveVersion("cvs", "old")
+
+ if slavever == "old":
+ # 0.5.0
+ if self.args['mode'] == "export":
+ self.args['export'] = 1
+ elif self.args['mode'] == "clobber":
+ self.args['clobber'] = 1
+ elif self.args['mode'] == "copy":
+ self.args['copydir'] = "source"
+ self.args['tag'] = self.args['branch']
+ assert not self.args['patch'] # 0.5.0 slave can't do patch
+
+ cmd = LoggedRemoteCommand("cvs", self.args)
+ self.startCommand(cmd, warnings)
+
+
+class SVN(Source):
+ """I perform Subversion checkout/update operations."""
+
+ name = 'svn'
+
+ def __init__(self, svnurl=None, baseURL=None, defaultBranch=None,
+ directory=None, username=None, password=None, **kwargs):
+ """
+ @type svnurl: string
+ @param svnurl: the URL which points to the Subversion server,
+ combining the access method (HTTP, ssh, local file),
+ the repository host/port, the repository path, the
+ sub-tree within the repository, and the branch to
+ check out. Using C{svnurl} does not enable builds of
+ alternate branches: use C{baseURL} to enable this.
+ Use exactly one of C{svnurl} and C{baseURL}.
+
+ @param baseURL: if branches are enabled, this is the base URL to
+ which a branch name will be appended. It should
+ probably end in a slash. Use exactly one of
+ C{svnurl} and C{baseURL}.
+
+ @param defaultBranch: if branches are enabled, this is the branch
+ to use if the Build does not specify one
+ explicitly. It will simply be appended
+ to C{baseURL} and the result handed to
+ the SVN command.
+
+ @param username: username to pass to svn's --username
+ @param password: username to pass to svn's --password
+ """
+
+ if not kwargs.has_key('workdir') and directory is not None:
+ # deal with old configs
+ warn("Please use workdir=, not directory=", DeprecationWarning)
+ kwargs['workdir'] = directory
+
+ self.svnurl = svnurl
+ self.baseURL = baseURL
+ self.branch = defaultBranch
+ self.username = username
+ self.password = password
+
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(svnurl=svnurl,
+ baseURL=baseURL,
+ defaultBranch=defaultBranch,
+ directory=directory,
+ username=username,
+ password=password,
+ )
+
+ if not svnurl and not baseURL:
+ raise ValueError("you must use exactly one of svnurl and baseURL")
+
+
+ def computeSourceRevision(self, changes):
+ if not changes or None in [c.revision for c in changes]:
+ return None
+ lastChange = max([int(c.revision) for c in changes])
+ return lastChange
+
+ def startVC(self, branch, revision, patch):
+
+ # handle old slaves
+ warnings = []
+ slavever = self.slaveVersion("svn", "old")
+ if not slavever:
+ m = "slave does not have the 'svn' command"
+ raise BuildSlaveTooOldError(m)
+
+ if self.slaveVersionIsOlderThan("svn", "1.39"):
+ # the slave doesn't know to avoid re-using the same sourcedir
+ # when the branch changes. We have no way of knowing which branch
+ # the last build used, so if we're using a non-default branch and
+ # either 'update' or 'copy' modes, it is safer to refuse to
+ # build, and tell the user they need to upgrade the buildslave.
+ if (branch != self.branch
+ and self.args['mode'] in ("update", "copy")):
+ m = ("This buildslave (%s) does not know about multiple "
+ "branches, and using mode=%s would probably build the "
+ "wrong tree. "
+ "Refusing to build. Please upgrade the buildslave to "
+ "buildbot-0.7.0 or newer." % (self.build.slavename,
+ self.args['mode']))
+ raise BuildSlaveTooOldError(m)
+
+ if slavever == "old":
+ # 0.5.0 compatibility
+ if self.args['mode'] in ("clobber", "copy"):
+ # TODO: use some shell commands to make up for the
+ # deficiency, by blowing away the old directory first (thus
+ # forcing a full checkout)
+ warnings.append("WARNING: this slave can only do SVN updates"
+ ", not mode=%s\n" % self.args['mode'])
+ log.msg("WARNING: this slave only does mode=update")
+ if self.args['mode'] == "export":
+ raise BuildSlaveTooOldError("old slave does not have "
+ "mode=export")
+ self.args['directory'] = self.args['workdir']
+ if revision is not None:
+ # 0.5.0 can only do HEAD. We have no way of knowing whether
+ # the requested revision is HEAD or not, and for
+ # slowly-changing trees this will probably do the right
+ # thing, so let it pass with a warning
+ m = ("WARNING: old slave can only update to HEAD, not "
+ "revision=%s" % revision)
+ log.msg(m)
+ warnings.append(m + "\n")
+ revision = "HEAD" # interprets this key differently
+ if patch:
+ raise BuildSlaveTooOldError("old slave can't do patch")
+
+ if self.svnurl:
+ assert not branch # we need baseURL= to use branches
+ self.args['svnurl'] = self.svnurl
+ else:
+ self.args['svnurl'] = self.baseURL + branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+
+ if self.username is not None or self.password is not None:
+ if self.slaveVersionIsOlderThan("svn", "2.8"):
+ m = ("This buildslave (%s) does not support svn usernames "
+ "and passwords. "
+ "Refusing to build. Please upgrade the buildslave to "
+ "buildbot-0.7.10 or newer." % (self.build.slavename,))
+ raise BuildSlaveTooOldError(m)
+ if self.username is not None: self.args['username'] = self.username
+ if self.password is not None: self.args['password'] = self.password
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ if revision is not None:
+ revstuff.append("r%s" % revision)
+ if patch is not None:
+ revstuff.append("[patch]")
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("svn", self.args)
+ self.startCommand(cmd, warnings)
+
+
+class Darcs(Source):
+ """Check out a source tree from a Darcs repository at 'repourl'.
+
+ Darcs has no concept of file modes. This means the eXecute-bit will be
+ cleared on all source files. As a result, you may need to invoke
+ configuration scripts with something like:
+
+ C{s(step.Configure, command=['/bin/sh', './configure'])}
+ """
+
+ name = "darcs"
+
+ def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
+ **kwargs):
+ """
+ @type repourl: string
+ @param repourl: the URL which points at the Darcs repository. This
+ is used as the default branch. Using C{repourl} does
+ not enable builds of alternate branches: use
+ C{baseURL} to enable this. Use either C{repourl} or
+ C{baseURL}, not both.
+
+ @param baseURL: if branches are enabled, this is the base URL to
+ which a branch name will be appended. It should
+ probably end in a slash. Use exactly one of
+ C{repourl} and C{baseURL}.
+
+ @param defaultBranch: if branches are enabled, this is the branch
+ to use if the Build does not specify one
+ explicitly. It will simply be appended to
+ C{baseURL} and the result handed to the
+ 'darcs pull' command.
+ """
+ self.repourl = repourl
+ self.baseURL = baseURL
+ self.branch = defaultBranch
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(repourl=repourl,
+ baseURL=baseURL,
+ defaultBranch=defaultBranch,
+ )
+ assert self.args['mode'] != "export", \
+ "Darcs does not have an 'export' mode"
+ if (not repourl and not baseURL) or (repourl and baseURL):
+ raise ValueError("you must provide exactly one of repourl and"
+ " baseURL")
+
+ def startVC(self, branch, revision, patch):
+ slavever = self.slaveVersion("darcs")
+ if not slavever:
+ m = "slave is too old, does not know about darcs"
+ raise BuildSlaveTooOldError(m)
+
+ if self.slaveVersionIsOlderThan("darcs", "1.39"):
+ if revision:
+ # TODO: revisit this once we implement computeSourceRevision
+ m = "0.6.6 slaves can't handle args['revision']"
+ raise BuildSlaveTooOldError(m)
+
+ # the slave doesn't know to avoid re-using the same sourcedir
+ # when the branch changes. We have no way of knowing which branch
+ # the last build used, so if we're using a non-default branch and
+ # either 'update' or 'copy' modes, it is safer to refuse to
+ # build, and tell the user they need to upgrade the buildslave.
+ if (branch != self.branch
+ and self.args['mode'] in ("update", "copy")):
+ m = ("This buildslave (%s) does not know about multiple "
+ "branches, and using mode=%s would probably build the "
+ "wrong tree. "
+ "Refusing to build. Please upgrade the buildslave to "
+ "buildbot-0.7.0 or newer." % (self.build.slavename,
+ self.args['mode']))
+ raise BuildSlaveTooOldError(m)
+
+ if self.repourl:
+ assert not branch # we need baseURL= to use branches
+ self.args['repourl'] = self.repourl
+ else:
+ self.args['repourl'] = self.baseURL + branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("darcs", self.args)
+ self.startCommand(cmd)
+
+
+class Git(Source):
+ """Check out a source tree from a git repository 'repourl'."""
+
+ name = "git"
+
+ def __init__(self, repourl, branch="master", **kwargs):
+ """
+ @type repourl: string
+ @param repourl: the URL which points at the git repository
+
+ @type branch: string
+ @param branch: The branch or tag to check out by default. If
+ a build specifies a different branch, it will
+ be used instead of this.
+ """
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(repourl=repourl, branch=branch)
+ self.args.update({'repourl': repourl,
+ 'branch': branch})
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ return changes[-1].revision
+
+ def startVC(self, branch, revision, patch):
+ if branch is not None:
+ self.args['branch'] = branch
+
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+ slavever = self.slaveVersion("git")
+ if not slavever:
+ raise BuildSlaveTooOldError("slave is too old, does not know "
+ "about git")
+ cmd = LoggedRemoteCommand("git", self.args)
+ self.startCommand(cmd)
+
+
+class Arch(Source):
+ """Check out a source tree from an Arch repository named 'archive'
+ available at 'url'. 'version' specifies which version number (development
+ line) will be used for the checkout: this is mostly equivalent to a
+ branch name. This version uses the 'tla' tool to do the checkout, to use
+ 'baz' see L{Bazaar} instead.
+ """
+
+ name = "arch"
+ # TODO: slaves >0.6.6 will accept args['build-config'], so use it
+
+ def __init__(self, url, version, archive=None, **kwargs):
+ """
+ @type url: string
+ @param url: the Arch coordinates of the repository. This is
+ typically an http:// URL, but could also be the absolute
+ pathname of a local directory instead.
+
+ @type version: string
+ @param version: the category--branch--version to check out. This is
+ the default branch. If a build specifies a different
+ branch, it will be used instead of this.
+
+ @type archive: string
+ @param archive: The archive name. If provided, it must match the one
+ that comes from the repository. If not, the
+ repository's default will be used.
+ """
+ self.branch = version
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(url=url,
+ version=version,
+ archive=archive,
+ )
+ self.args.update({'url': url,
+ 'archive': archive,
+ })
+
+ def computeSourceRevision(self, changes):
+ # in Arch, fully-qualified revision numbers look like:
+ # arch@buildbot.sourceforge.net--2004/buildbot--dev--0--patch-104
+ # For any given builder, all of this is fixed except the patch-104.
+ # The Change might have any part of the fully-qualified string, so we
+ # just look for the last part. We return the "patch-NN" string.
+ if not changes:
+ return None
+ lastChange = None
+ for c in changes:
+ if not c.revision:
+ continue
+ if c.revision.endswith("--base-0"):
+ rev = 0
+ else:
+ i = c.revision.rindex("patch")
+ rev = int(c.revision[i+len("patch-"):])
+ lastChange = max(lastChange, rev)
+ if lastChange is None:
+ return None
+ if lastChange == 0:
+ return "base-0"
+ return "patch-%d" % lastChange
+
+ def checkSlaveVersion(self, cmd, branch):
+ warnings = []
+ slavever = self.slaveVersion(cmd)
+ if not slavever:
+ m = "slave is too old, does not know about %s" % cmd
+ raise BuildSlaveTooOldError(m)
+
+ # slave 1.28 and later understand 'revision'
+ if self.slaveVersionIsOlderThan(cmd, "1.28"):
+ if not self.alwaysUseLatest:
+ # we don't know whether our requested revision is the latest
+ # or not. If the tree does not change very quickly, this will
+ # probably build the right thing, so emit a warning rather
+ # than refuse to build at all
+ m = "WARNING, buildslave is too old to use a revision"
+ log.msg(m)
+ warnings.append(m + "\n")
+
+ if self.slaveVersionIsOlderThan(cmd, "1.39"):
+ # the slave doesn't know to avoid re-using the same sourcedir
+ # when the branch changes. We have no way of knowing which branch
+ # the last build used, so if we're using a non-default branch and
+ # either 'update' or 'copy' modes, it is safer to refuse to
+ # build, and tell the user they need to upgrade the buildslave.
+ if (branch != self.branch
+ and self.args['mode'] in ("update", "copy")):
+ m = ("This buildslave (%s) does not know about multiple "
+ "branches, and using mode=%s would probably build the "
+ "wrong tree. "
+ "Refusing to build. Please upgrade the buildslave to "
+ "buildbot-0.7.0 or newer." % (self.build.slavename,
+ self.args['mode']))
+ log.msg(m)
+ raise BuildSlaveTooOldError(m)
+
+ return warnings
+
+ def startVC(self, branch, revision, patch):
+ self.args['version'] = branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+ warnings = self.checkSlaveVersion("arch", branch)
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ if revision is not None:
+ revstuff.append("patch%s" % revision)
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("arch", self.args)
+ self.startCommand(cmd, warnings)
+
+
+class Bazaar(Arch):
+ """Bazaar is an alternative client for Arch repositories. baz is mostly
+ compatible with tla, but archive registration is slightly different."""
+
+ # TODO: slaves >0.6.6 will accept args['build-config'], so use it
+
+ def __init__(self, url, version, archive, **kwargs):
+ """
+ @type url: string
+ @param url: the Arch coordinates of the repository. This is
+ typically an http:// URL, but could also be the absolute
+ pathname of a local directory instead.
+
+ @type version: string
+ @param version: the category--branch--version to check out
+
+ @type archive: string
+ @param archive: The archive name (required). This must always match
+ the one that comes from the repository, otherwise the
+ buildslave will attempt to get sources from the wrong
+ archive.
+ """
+ self.branch = version
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(url=url,
+ version=version,
+ archive=archive,
+ )
+ self.args.update({'url': url,
+ 'archive': archive,
+ })
+
+ def startVC(self, branch, revision, patch):
+ self.args['version'] = branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+ warnings = self.checkSlaveVersion("bazaar", branch)
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ if revision is not None:
+ revstuff.append("patch%s" % revision)
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("bazaar", self.args)
+ self.startCommand(cmd, warnings)
+
+class Bzr(Source):
+ """Check out a source tree from a bzr (Bazaar) repository at 'repourl'.
+
+ """
+
+ name = "bzr"
+
+ def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
+ **kwargs):
+ """
+ @type repourl: string
+ @param repourl: the URL which points at the bzr repository. This
+ is used as the default branch. Using C{repourl} does
+ not enable builds of alternate branches: use
+ C{baseURL} to enable this. Use either C{repourl} or
+ C{baseURL}, not both.
+
+ @param baseURL: if branches are enabled, this is the base URL to
+ which a branch name will be appended. It should
+ probably end in a slash. Use exactly one of
+ C{repourl} and C{baseURL}.
+
+ @param defaultBranch: if branches are enabled, this is the branch
+ to use if the Build does not specify one
+ explicitly. It will simply be appended to
+ C{baseURL} and the result handed to the
+ 'bzr checkout pull' command.
+ """
+ self.repourl = repourl
+ self.baseURL = baseURL
+ self.branch = defaultBranch
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(repourl=repourl,
+ baseURL=baseURL,
+ defaultBranch=defaultBranch,
+ )
+ if (not repourl and not baseURL) or (repourl and baseURL):
+ raise ValueError("you must provide exactly one of repourl and"
+ " baseURL")
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ lastChange = max([int(c.revision) for c in changes])
+ return lastChange
+
+ def startVC(self, branch, revision, patch):
+ slavever = self.slaveVersion("bzr")
+ if not slavever:
+ m = "slave is too old, does not know about bzr"
+ raise BuildSlaveTooOldError(m)
+
+ if self.repourl:
+ assert not branch # we need baseURL= to use branches
+ self.args['repourl'] = self.repourl
+ else:
+ self.args['repourl'] = self.baseURL + branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("bzr", self.args)
+ self.startCommand(cmd)
+
+
+class Mercurial(Source):
+ """Check out a source tree from a mercurial repository 'repourl'."""
+
+ name = "hg"
+
+ def __init__(self, repourl=None, baseURL=None, defaultBranch=None,
+ branchType='dirname', **kwargs):
+ """
+ @type repourl: string
+ @param repourl: the URL which points at the Mercurial repository.
+ This uses the 'default' branch unless defaultBranch is
+ specified below and the C{branchType} is set to
+ 'inrepo'. It is an error to specify a branch without
+ setting the C{branchType} to 'inrepo'.
+
+ @param baseURL: if 'dirname' branches are enabled, this is the base URL
+ to which a branch name will be appended. It should
+ probably end in a slash. Use exactly one of C{repourl}
+ and C{baseURL}.
+
+ @param defaultBranch: if branches are enabled, this is the branch
+ to use if the Build does not specify one
+ explicitly.
+ For 'dirname' branches, It will simply be
+ appended to C{baseURL} and the result handed to
+ the 'hg update' command.
+ For 'inrepo' branches, this specifies the named
+ revision to which the tree will update after a
+ clone.
+
+ @param branchType: either 'dirname' or 'inrepo' depending on whether
+ the branch name should be appended to the C{baseURL}
+ or the branch is a mercurial named branch and can be
+ found within the C{repourl}
+ """
+ self.repourl = repourl
+ self.baseURL = baseURL
+ self.branch = defaultBranch
+ self.branchType = branchType
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(repourl=repourl,
+ baseURL=baseURL,
+ defaultBranch=defaultBranch,
+ branchType=branchType,
+ )
+ if (not repourl and not baseURL) or (repourl and baseURL):
+ raise ValueError("you must provide exactly one of repourl and"
+ " baseURL")
+
+ def startVC(self, branch, revision, patch):
+ slavever = self.slaveVersion("hg")
+ if not slavever:
+ raise BuildSlaveTooOldError("slave is too old, does not know "
+ "about hg")
+
+ if self.repourl:
+ # we need baseURL= to use dirname branches
+ assert self.branchType == 'inrepo' or not branch
+ self.args['repourl'] = self.repourl
+ if branch:
+ self.args['branch'] = branch
+ else:
+ self.args['repourl'] = self.baseURL + branch
+ self.args['revision'] = revision
+ self.args['patch'] = patch
+
+ revstuff = []
+ if branch is not None and branch != self.branch:
+ revstuff.append("[branch]")
+ self.description.extend(revstuff)
+ self.descriptionDone.extend(revstuff)
+
+ cmd = LoggedRemoteCommand("hg", self.args)
+ self.startCommand(cmd)
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ # without knowing the revision ancestry graph, we can't sort the
+ # changes at all. So for now, assume they were given to us in sorted
+ # order, and just pay attention to the last one. See ticket #103 for
+ # more details.
+ if len(changes) > 1:
+ log.msg("Mercurial.computeSourceRevision: warning: "
+ "there are %d changes here, assuming the last one is "
+ "the most recent" % len(changes))
+ return changes[-1].revision
+
+
+class P4(Source):
+ """ P4 is a class for accessing perforce revision control"""
+ name = "p4"
+
+ def __init__(self, p4base, defaultBranch=None, p4port=None, p4user=None,
+ p4passwd=None, p4extra_views=[],
+ p4client='buildbot_%(slave)s_%(builder)s', **kwargs):
+ """
+ @type p4base: string
+ @param p4base: A view into a perforce depot, typically
+ "//depot/proj/"
+
+ @type defaultBranch: string
+ @param defaultBranch: Identify a branch to build by default. Perforce
+ is a view based branching system. So, the branch
+ is normally the name after the base. For example,
+ branch=1.0 is view=//depot/proj/1.0/...
+ branch=1.1 is view=//depot/proj/1.1/...
+
+ @type p4port: string
+ @param p4port: Specify the perforce server to connection in the format
+ <host>:<port>. Example "perforce.example.com:1666"
+
+ @type p4user: string
+ @param p4user: The perforce user to run the command as.
+
+ @type p4passwd: string
+ @param p4passwd: The password for the perforce user.
+
+ @type p4extra_views: list of tuples
+ @param p4extra_views: Extra views to be added to
+ the client that is being used.
+
+ @type p4client: string
+ @param p4client: The perforce client to use for this buildslave.
+ """
+
+ self.branch = defaultBranch
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(p4base=p4base,
+ defaultBranch=defaultBranch,
+ p4port=p4port,
+ p4user=p4user,
+ p4passwd=p4passwd,
+ p4extra_views=p4extra_views,
+ p4client=p4client,
+ )
+ self.args['p4port'] = p4port
+ self.args['p4user'] = p4user
+ self.args['p4passwd'] = p4passwd
+ self.args['p4base'] = p4base
+ self.args['p4extra_views'] = p4extra_views
+ self.p4client = p4client
+
+ def setBuild(self, build):
+ Source.setBuild(self, build)
+ self.args['p4client'] = self.p4client % {
+ 'slave': build.slavename,
+ 'builder': build.builder.name,
+ }
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ lastChange = max([int(c.revision) for c in changes])
+ return lastChange
+
+ def startVC(self, branch, revision, patch):
+ slavever = self.slaveVersion("p4")
+ assert slavever, "slave is too old, does not know about p4"
+ args = dict(self.args)
+ args['branch'] = branch or self.branch
+ args['revision'] = revision
+ args['patch'] = patch
+ cmd = LoggedRemoteCommand("p4", args)
+ self.startCommand(cmd)
+
+class P4Sync(Source):
+ """This is a partial solution for using a P4 source repository. You are
+ required to manually set up each build slave with a useful P4
+ environment, which means setting various per-slave environment variables,
+ and creating a P4 client specification which maps the right files into
+ the slave's working directory. Once you have done that, this step merely
+ performs a 'p4 sync' to update that workspace with the newest files.
+
+ Each slave needs the following environment:
+
+ - PATH: the 'p4' binary must be on the slave's PATH
+ - P4USER: each slave needs a distinct user account
+ - P4CLIENT: each slave needs a distinct client specification
+
+ You should use 'p4 client' (?) to set up a client view spec which maps
+ the desired files into $SLAVEBASE/$BUILDERBASE/source .
+ """
+
+ name = "p4sync"
+
+ def __init__(self, p4port, p4user, p4passwd, p4client, **kwargs):
+ assert kwargs['mode'] == "copy", "P4Sync can only be used in mode=copy"
+ self.branch = None
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(p4port=p4port,
+ p4user=p4user,
+ p4passwd=p4passwd,
+ p4client=p4client,
+ )
+ self.args['p4port'] = p4port
+ self.args['p4user'] = p4user
+ self.args['p4passwd'] = p4passwd
+ self.args['p4client'] = p4client
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ lastChange = max([int(c.revision) for c in changes])
+ return lastChange
+
+ def startVC(self, branch, revision, patch):
+ slavever = self.slaveVersion("p4sync")
+ assert slavever, "slave is too old, does not know about p4"
+ cmd = LoggedRemoteCommand("p4sync", self.args)
+ self.startCommand(cmd)
+
+class Monotone(Source):
+ """Check out a revision from a monotone server at 'server_addr',
+ branch 'branch'. 'revision' specifies which revision id to check
+ out.
+
+ This step will first create a local database, if necessary, and then pull
+ the contents of the server into the database. Then it will do the
+ checkout/update from this database."""
+
+ name = "monotone"
+
+ def __init__(self, server_addr, branch, db_path="monotone.db",
+ monotone="monotone",
+ **kwargs):
+ Source.__init__(self, **kwargs)
+ self.addFactoryArguments(server_addr=server_addr,
+ branch=branch,
+ db_path=db_path,
+ monotone=monotone,
+ )
+ self.args.update({"server_addr": server_addr,
+ "branch": branch,
+ "db_path": db_path,
+ "monotone": monotone})
+
+ def computeSourceRevision(self, changes):
+ if not changes:
+ return None
+ return changes[-1].revision
+
+ def startVC(self):
+ slavever = self.slaveVersion("monotone")
+ assert slavever, "slave is too old, does not know about monotone"
+ cmd = LoggedRemoteCommand("monotone", self.args)
+ self.startCommand(cmd)
+