fetch: Add bzr fetcher
[bitbake.git] / lib / bb / fetch / __init__.py
1 # ex:ts=4:sw=4:sts=4:et
2 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
3 """
4 BitBake 'Fetch' implementations
5
6 Classes for obtaining upstream sources for the
7 BitBake build tools.
8 """
9
10 # Copyright (C) 2003, 2004  Chris Larson
11 #
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License version 2 as
14 # published by the Free Software Foundation.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License along
22 # with this program; if not, write to the Free Software Foundation, Inc.,
23 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 #
25 # Based on functions from the base bb module, Copyright 2003 Holger Schurig
26
27 import os, re, fcntl
28 import bb
29 from   bb import data
30 from   bb import persist_data
31
32 try:
33     import cPickle as pickle
34 except ImportError:
35     import pickle
36
37 class FetchError(Exception):
38     """Exception raised when a download fails"""
39
40 class NoMethodError(Exception):
41     """Exception raised when there is no method to obtain a supplied url or set of urls"""
42
43 class MissingParameterError(Exception):
44     """Exception raised when a fetch method is missing a critical parameter in the url"""
45
46 class ParameterError(Exception):
47     """Exception raised when a url cannot be proccessed due to invalid parameters."""
48
49 class MD5SumError(Exception):
50     """Exception raised when a MD5SUM of a file does not match the expected one"""
51
52 def uri_replace(uri, uri_find, uri_replace, d):
53 #   bb.msg.note(1, bb.msg.domain.Fetcher, "uri_replace: operating on %s" % uri)
54     if not uri or not uri_find or not uri_replace:
55         bb.msg.debug(1, bb.msg.domain.Fetcher, "uri_replace: passed an undefined value, not replacing")
56     uri_decoded = list(bb.decodeurl(uri))
57     uri_find_decoded = list(bb.decodeurl(uri_find))
58     uri_replace_decoded = list(bb.decodeurl(uri_replace))
59     result_decoded = ['','','','','',{}]
60     for i in uri_find_decoded:
61         loc = uri_find_decoded.index(i)
62         result_decoded[loc] = uri_decoded[loc]
63         import types
64         if type(i) == types.StringType:
65             import re
66             if (re.match(i, uri_decoded[loc])):
67                 result_decoded[loc] = re.sub(i, uri_replace_decoded[loc], uri_decoded[loc])
68                 if uri_find_decoded.index(i) == 2:
69                     if d:
70                         localfn = bb.fetch.localpath(uri, d)
71                         if localfn:
72                             result_decoded[loc] = os.path.dirname(result_decoded[loc]) + "/" + os.path.basename(bb.fetch.localpath(uri, d))
73 #                       bb.msg.note(1, bb.msg.domain.Fetcher, "uri_replace: matching %s against %s and replacing with %s" % (i, uri_decoded[loc], uri_replace_decoded[loc]))
74             else:
75 #               bb.msg.note(1, bb.msg.domain.Fetcher, "uri_replace: no match")
76                 return uri
77 #           else:
78 #               for j in i.keys():
79 #                   FIXME: apply replacements against options
80     return bb.encodeurl(result_decoded)
81
82 methods = []
83 urldata_cache = {}
84
85 def fetcher_init(d):
86     """
87     Called to initilize the fetchers once the configuration data is known
88     Calls before this must not hit the cache.
89     """
90     pd = persist_data.PersistData(d)
91     # When to drop SCM head revisions controled by user policy
92     srcrev_policy = bb.data.getVar('BB_SRCREV_POLICY', d, 1) or "clear"
93     if srcrev_policy == "cache":
94         bb.msg.debug(1, bb.msg.domain.Fetcher, "Keeping SRCREV cache due to cache policy of: %s" % srcrev_policy)
95     elif srcrev_policy == "clear":
96         bb.msg.debug(1, bb.msg.domain.Fetcher, "Clearing SRCREV cache due to cache policy of: %s" % srcrev_policy)
97         pd.delDomain("BB_URI_HEADREVS")
98     else:
99         bb.msg.fatal(bb.msg.domain.Fetcher, "Invalid SRCREV cache policy of: %s" % srcrev_policy)
100     # Make sure our domains exist
101     pd.addDomain("BB_URI_HEADREVS")
102     pd.addDomain("BB_URI_LOCALCOUNT")
103
104 # Function call order is usually:
105 #   1. init
106 #   2. go
107 #   3. localpaths
108 # localpath can be called at any time
109
110 def init(urls, d, setup = True):
111     urldata = {}
112     fn = bb.data.getVar('FILE', d, 1)
113     if fn in urldata_cache:
114         urldata = urldata_cache[fn]
115
116     for url in urls:
117         if url not in urldata:
118             urldata[url] = FetchData(url, d)
119
120     if setup:
121         for url in urldata:
122             if not urldata[url].setup:
123                 urldata[url].setup_localpath(d) 
124
125     urldata_cache[fn] = urldata
126     return urldata
127
128 def go(d):
129     """
130     Fetch all urls
131     init must have previously been called
132     """
133     urldata = init([], d, True)
134
135     for u in urldata:
136         ud = urldata[u]
137         m = ud.method
138         if ud.localfile:
139             if not m.forcefetch(u, ud, d) and os.path.exists(ud.md5):
140                 # File already present along with md5 stamp file
141                 # Touch md5 file to show activity
142                 os.utime(ud.md5, None)
143                 continue
144             lf = open(ud.lockfile, "a+")
145             fcntl.flock(lf.fileno(), fcntl.LOCK_EX)
146             if not m.forcefetch(u, ud, d) and os.path.exists(ud.md5):
147                 # If someone else fetched this before we got the lock, 
148                 # notice and don't try again
149                 os.utime(ud.md5, None)
150                 fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
151                 lf.close
152                 continue
153         m.go(u, ud, d)
154         if ud.localfile:
155             if not m.forcefetch(u, ud, d):
156                 Fetch.write_md5sum(u, ud, d)
157             fcntl.flock(lf.fileno(), fcntl.LOCK_UN)
158             lf.close
159
160 def localpaths(d):
161     """
162     Return a list of the local filenames, assuming successful fetch
163     """
164     local = []
165     urldata = init([], d, True)
166
167     for u in urldata:
168         ud = urldata[u]      
169         local.append(ud.localpath)
170
171     return local
172
173 def get_srcrev(d):
174     """
175     Return the version string for the current package
176     (usually to be used as PV)
177     Most packages usually only have one SCM so we just pass on the call.
178     In the multi SCM case, we build a value based on SRCREV_FORMAT which must 
179     have been set.
180     """
181     scms = []
182     # Only call setup_localpath on URIs which suppports_srcrev() 
183     urldata = init(bb.data.getVar('SRC_URI', d, 1).split(), d, False)
184     for u in urldata:
185         ud = urldata[u]
186         if ud.method.suppports_srcrev():
187             if not ud.setup:
188                 ud.setup_localpath(d)
189             scms.append(u)
190
191     if len(scms) == 0:
192         bb.msg.error(bb.msg.domain.Fetcher, "SRCREV was used yet no valid SCM was found in SRC_URI")
193         raise ParameterError
194
195     if len(scms) == 1:
196         return urldata[scms[0]].method.sortable_revision(scms[0], urldata[scms[0]], d)
197
198     #
199     # Mutiple SCMs are in SRC_URI so we resort to SRCREV_FORMAT
200     #
201     format = bb.data.getVar('SRCREV_FORMAT', d, 1)
202     if not format:
203         bb.msg.error(bb.msg.domain.Fetcher, "The SRCREV_FORMAT variable must be set when multiple SCMs are used.")
204         raise ParameterError
205
206     for scm in scms:
207         if 'name' in urldata[scm].parm:
208             name = urldata[scm].parm["name"]
209             rev = urldata[scm].method.sortable_revision(scm, urldata[scm], d)
210             format = format.replace(name, rev)
211
212     return format
213
214 def localpath(url, d, cache = True):
215     """
216     Called from the parser with cache=False since the cache isn't ready 
217     at this point. Also called from classed in OE e.g. patch.bbclass
218     """
219     ud = init([url], d)
220     if ud[url].method:
221         return ud[url].localpath
222     return url
223
224 def runfetchcmd(cmd, d, quiet = False):
225     """
226     Run cmd returning the command output
227     Raise an error if interrupted or cmd fails
228     Optionally echo command output to stdout
229     """
230     bb.msg.debug(1, bb.msg.domain.Fetcher, "Running %s" % cmd)
231
232     # Need to export PATH as binary could be in metadata paths
233     # rather than host provided
234     pathcmd = 'export PATH=%s; %s' % (data.expand('${PATH}', d), cmd)
235
236     stdout_handle = os.popen(pathcmd, "r")
237     output = ""
238
239     while 1:
240         line = stdout_handle.readline()
241         if not line:
242             break
243         if not quiet:
244             print line,
245         output += line
246
247     status =  stdout_handle.close() or 0
248     signal = status >> 8
249     exitstatus = status & 0xff
250
251     if signal:
252         raise FetchError("Fetch command %s failed with signal %s, output:\n%s" % (pathcmd, signal, output))
253     elif status != 0:
254         raise FetchError("Fetch command %s failed with exit code %s, output:\n%s" % (pathcmd, status, output))
255
256     return output
257
258 class FetchData(object):
259     """
260     A class which represents the fetcher state for a given URI.
261     """
262     def __init__(self, url, d):
263         self.localfile = ""
264         (self.type, self.host, self.path, self.user, self.pswd, self.parm) = bb.decodeurl(data.expand(url, d))
265         self.date = Fetch.getSRCDate(self, d)
266         self.url = url
267         self.setup = False
268         for m in methods:
269             if m.supports(url, self, d):
270                 self.method = m
271                 break
272
273     def setup_localpath(self, d):
274         self.setup = True
275         if "localpath" in self.parm:
276             self.localpath = self.parm["localpath"]
277         else:
278             self.localpath = self.method.localpath(self.url, self, d)
279         self.md5 = self.localpath + '.md5'
280         self.lockfile = self.localpath + '.lock'
281         # if user sets localpath for file, use it instead.
282
283
284 class Fetch(object):
285     """Base class for 'fetch'ing data"""
286
287     def __init__(self, urls = []):
288         self.urls = []
289
290     def supports(self, url, urldata, d):
291         """
292         Check to see if this fetch class supports a given url.
293         """
294         return 0
295
296     def localpath(self, url, urldata, d):
297         """
298         Return the local filename of a given url assuming a successful fetch.
299         Can also setup variables in urldata for use in go (saving code duplication 
300         and duplicate code execution)
301         """
302         return url
303
304     def setUrls(self, urls):
305         self.__urls = urls
306
307     def getUrls(self):
308         return self.__urls
309
310     urls = property(getUrls, setUrls, None, "Urls property")
311
312     def forcefetch(self, url, urldata, d):
313         """
314         Force a fetch, even if localpath exists?
315         """
316         return False
317
318     def suppports_srcrev(self):
319         """
320         The fetcher supports auto source revisions (SRCREV)
321         """
322         return False
323
324     def go(self, url, urldata, d):
325         """
326         Fetch urls
327         Assumes localpath was called first
328         """
329         raise NoMethodError("Missing implementation for url")
330
331     def getSRCDate(urldata, d):
332         """
333         Return the SRC Date for the component
334
335         d the bb.data module
336         """
337         if "srcdate" in urldata.parm:
338             return urldata.parm['srcdate']
339
340         pn = data.getVar("PN", d, 1)
341
342         if pn:
343             return data.getVar("SRCDATE_%s" % pn, d, 1) or data.getVar("CVSDATE_%s" % pn, d, 1) or data.getVar("SRCDATE", d, 1) or data.getVar("CVSDATE", d, 1) or data.getVar("DATE", d, 1)
344
345         return data.getVar("SRCDATE", d, 1) or data.getVar("CVSDATE", d, 1) or data.getVar("DATE", d, 1)
346     getSRCDate = staticmethod(getSRCDate)
347
348     def try_mirror(d, tarfn):
349         """
350         Try to use a mirrored version of the sources. We do this
351         to avoid massive loads on foreign cvs and svn servers.
352         This method will be used by the different fetcher
353         implementations.
354
355         d Is a bb.data instance
356         tarfn is the name of the tarball
357         """
358         tarpath = os.path.join(data.getVar("DL_DIR", d, 1), tarfn)
359         if os.access(tarpath, os.R_OK):
360             bb.msg.debug(1, bb.msg.domain.Fetcher, "%s already exists, skipping checkout." % tarfn)
361             return True
362
363         pn = data.getVar('PN', d, True)
364         src_tarball_stash = None
365         if pn:
366             src_tarball_stash = (data.getVar('SRC_TARBALL_STASH_%s' % pn, d, True) or data.getVar('CVS_TARBALL_STASH_%s' % pn, d, True) or data.getVar('SRC_TARBALL_STASH', d, True) or data.getVar('CVS_TARBALL_STASH', d, True) or "").split()
367
368         for stash in src_tarball_stash:
369             fetchcmd = data.getVar("FETCHCOMMAND_mirror", d, True) or data.getVar("FETCHCOMMAND_wget", d, True)
370             uri = stash + tarfn
371             bb.msg.note(1, bb.msg.domain.Fetcher, "fetch " + uri)
372             fetchcmd = fetchcmd.replace("${URI}", uri)
373             ret = os.system(fetchcmd)
374             if ret == 0:
375                 bb.msg.note(1, bb.msg.domain.Fetcher, "Fetched %s from tarball stash, skipping checkout" % tarfn)
376                 return True
377         return False
378     try_mirror = staticmethod(try_mirror)
379
380     def verify_md5sum(ud, got_sum):
381         """
382         Verify the md5sum we wanted with the one we got
383         """
384         wanted_sum = None
385         if 'md5sum' in ud.parm:
386             wanted_sum = ud.parm['md5sum']
387         if not wanted_sum:
388             return True
389
390         return wanted_sum == got_sum
391     verify_md5sum = staticmethod(verify_md5sum)
392
393     def write_md5sum(url, ud, d):
394         if bb.which(data.getVar('PATH', d), 'md5sum'):
395             try:
396                 md5pipe = os.popen('md5sum ' + ud.localpath)
397                 md5data = (md5pipe.readline().split() or [ "" ])[0]
398                 md5pipe.close()
399             except OSError:
400                 md5data = ""
401
402         # verify the md5sum
403         if not Fetch.verify_md5sum(ud, md5data):
404             raise MD5SumError(url)
405
406         md5out = file(ud.md5, 'w')
407         md5out.write(md5data)
408         md5out.close()
409     write_md5sum = staticmethod(write_md5sum)
410
411     def latest_revision(self, url, ud, d):
412         """
413         Look in the cache for the latest revision, if not present ask the SCM.
414         """
415         if not hasattr(self, "_latest_revision"):
416             raise ParameterError
417
418         pd = persist_data.PersistData(d)
419         key = self._revision_key(url, ud, d)
420         rev = pd.getValue("BB_URI_HEADREVS", key)
421         if rev != None:
422             return str(rev)
423
424         rev = self._latest_revision(url, ud, d)
425         pd.setValue("BB_URI_HEADREVS", key, rev)
426         return rev
427
428     def sortable_revision(self, url, ud, d):
429         """
430         
431         """
432         if hasattr(self, "_sortable_revision"):
433             return self._sortable_revision(url, ud, d)
434
435         pd = persist_data.PersistData(d)
436         key = self._revision_key(url, ud, d)
437         latest_rev = self.latest_revision(url, ud, d)
438         last_rev = pd.getValue("BB_URI_LOCALCOUNT", key + "_rev")
439         count = pd.getValue("BB_URI_LOCALCOUNT", key + "_count")
440
441         if last_rev == latest_rev:
442             return str(count + "+" + latest_rev)
443
444         if count is None:
445             count = "0"
446         else:
447             count = str(int(count) + 1)
448
449         pd.setValue("BB_URI_LOCALCOUNT", key + "_rev", latest_rev)
450         pd.setValue("BB_URI_LOCALCOUNT", key + "_count", count)
451
452         return str(count + "+" + latest_rev)
453
454
455 import cvs
456 import git
457 import local
458 import svn
459 import wget
460 import svk
461 import ssh
462 import perforce
463 import bzr
464
465 methods.append(local.Local())
466 methods.append(wget.Wget())
467 methods.append(svn.Svn())
468 methods.append(git.Git())
469 methods.append(cvs.Cvs())
470 methods.append(svk.Svk())
471 methods.append(ssh.SSH())
472 methods.append(perforce.Perforce())
473 methods.append(bzr.Bzr())