fetch/__init__.py: Improve runfetchcmd so errors are seen and various GIT variables...
[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                 try:
143                     os.utime(ud.md5, None)
144                 except:
145                     # Errors aren't fatal here
146                     pass
147                 continue
148             lf = bb.utils.lockfile(ud.lockfile)
149             if not m.forcefetch(u, ud, d) and os.path.exists(ud.md5):
150                 # If someone else fetched this before we got the lock, 
151                 # notice and don't try again
152                 try:
153                     os.utime(ud.md5, None)
154                 except:
155                     # Errors aren't fatal here
156                     pass
157                 bb.utils.unlockfile(lf)
158                 continue
159         m.go(u, ud, d)
160         if ud.localfile:
161             if not m.forcefetch(u, ud, d):
162                 Fetch.write_md5sum(u, ud, d)
163             bb.utils.unlockfile(lf)
164
165 def localpaths(d):
166     """
167     Return a list of the local filenames, assuming successful fetch
168     """
169     local = []
170     urldata = init([], d, True)
171
172     for u in urldata:
173         ud = urldata[u]      
174         local.append(ud.localpath)
175
176     return local
177
178 srcrev_internal_call = False
179
180 def get_srcrev(d):
181     """
182     Return the version string for the current package
183     (usually to be used as PV)
184     Most packages usually only have one SCM so we just pass on the call.
185     In the multi SCM case, we build a value based on SRCREV_FORMAT which must 
186     have been set.
187     """
188
189     #
190     # Ugly code alert. localpath in the fetchers will try to evaluate SRCREV which 
191     # could translate into a call to here. If it does, we need to catch this
192     # and provide some way so it knows get_srcrev is active instead of being
193     # some number etc. hence the srcrev_internal_call tracking and the magic  
194     # "SRCREVINACTION" return value.
195     #
196     # Neater solutions welcome!
197     #
198     if bb.fetch.srcrev_internal_call:
199         return "SRCREVINACTION"
200
201     scms = []
202
203     # Only call setup_localpath on URIs which suppports_srcrev() 
204     urldata = init(bb.data.getVar('SRC_URI', d, 1).split(), d, False)
205     for u in urldata:
206         ud = urldata[u]
207         if ud.method.suppports_srcrev():
208             if not ud.setup:
209                 ud.setup_localpath(d)
210             scms.append(u)
211
212     if len(scms) == 0:
213         bb.msg.error(bb.msg.domain.Fetcher, "SRCREV was used yet no valid SCM was found in SRC_URI")
214         raise ParameterError
215
216     if len(scms) == 1:
217         return urldata[scms[0]].method.sortable_revision(scms[0], urldata[scms[0]], d)
218
219     #
220     # Mutiple SCMs are in SRC_URI so we resort to SRCREV_FORMAT
221     #
222     format = bb.data.getVar('SRCREV_FORMAT', d, 1)
223     if not format:
224         bb.msg.error(bb.msg.domain.Fetcher, "The SRCREV_FORMAT variable must be set when multiple SCMs are used.")
225         raise ParameterError
226
227     for scm in scms:
228         if 'name' in urldata[scm].parm:
229             name = urldata[scm].parm["name"]
230             rev = urldata[scm].method.sortable_revision(scm, urldata[scm], d)
231             format = format.replace(name, rev)
232
233     return format
234
235 def localpath(url, d, cache = True):
236     """
237     Called from the parser with cache=False since the cache isn't ready 
238     at this point. Also called from classed in OE e.g. patch.bbclass
239     """
240     ud = init([url], d)
241     if ud[url].method:
242         return ud[url].localpath
243     return url
244
245 def runfetchcmd(cmd, d, quiet = False):
246     """
247     Run cmd returning the command output
248     Raise an error if interrupted or cmd fails
249     Optionally echo command output to stdout
250     """
251
252     # Need to export PATH as binary could be in metadata paths
253     # rather than host provided
254     # Also include some other variables.
255     # FIXME: Should really include all export varaiables?
256     exportvars = ['PATH', 'GIT_PROXY_HOST', 'GIT_PROXY_PORT', 'GIT_PROXY_COMMAND']
257
258     for var in exportvars:
259         val = data.getVar(var, d, True)
260         if val:
261             cmd = 'export ' + var + '=%s; %s' % (val, cmd)
262
263     bb.msg.debug(1, bb.msg.domain.Fetcher, "Running %s" % cmd)
264
265     # redirect stderr to stdout
266     stdout_handle = os.popen(cmd + " 2>&1", "r")
267     output = ""
268
269     while 1:
270         line = stdout_handle.readline()
271         if not line:
272             break
273         if not quiet:
274             print line,
275         output += line
276
277     status =  stdout_handle.close() or 0
278     signal = status >> 8
279     exitstatus = status & 0xff
280
281     if signal:
282         raise FetchError("Fetch command %s failed with signal %s, output:\n%s" % (cmd, signal, output))
283     elif status != 0:
284         raise FetchError("Fetch command %s failed with exit code %s, output:\n%s" % (cmd, status, output))
285
286     return output
287
288 class FetchData(object):
289     """
290     A class which represents the fetcher state for a given URI.
291     """
292     def __init__(self, url, d):
293         self.localfile = ""
294         (self.type, self.host, self.path, self.user, self.pswd, self.parm) = bb.decodeurl(data.expand(url, d))
295         self.date = Fetch.getSRCDate(self, d)
296         self.url = url
297         self.setup = False
298         for m in methods:
299             if m.supports(url, self, d):
300                 self.method = m
301                 return
302         raise NoMethodError("Missing implementation for url %s" % url)
303
304     def setup_localpath(self, d):
305         self.setup = True
306         if "localpath" in self.parm:
307             # if user sets localpath for file, use it instead.
308             self.localpath = self.parm["localpath"]
309         else:
310             bb.fetch.srcrev_internal_call = True
311             self.localpath = self.method.localpath(self.url, self, d)
312             bb.fetch.srcrev_internal_call = False
313             # We have to clear data's internal caches since the cached value of SRCREV is now wrong.
314             # Horrible...
315             bb.data.delVar("ISHOULDNEVEREXIST", d)
316         self.md5 = self.localpath + '.md5'
317         self.lockfile = self.localpath + '.lock'
318
319
320 class Fetch(object):
321     """Base class for 'fetch'ing data"""
322
323     def __init__(self, urls = []):
324         self.urls = []
325
326     def supports(self, url, urldata, d):
327         """
328         Check to see if this fetch class supports a given url.
329         """
330         return 0
331
332     def localpath(self, url, urldata, d):
333         """
334         Return the local filename of a given url assuming a successful fetch.
335         Can also setup variables in urldata for use in go (saving code duplication 
336         and duplicate code execution)
337         """
338         return url
339
340     def setUrls(self, urls):
341         self.__urls = urls
342
343     def getUrls(self):
344         return self.__urls
345
346     urls = property(getUrls, setUrls, None, "Urls property")
347
348     def forcefetch(self, url, urldata, d):
349         """
350         Force a fetch, even if localpath exists?
351         """
352         return False
353
354     def suppports_srcrev(self):
355         """
356         The fetcher supports auto source revisions (SRCREV)
357         """
358         return False
359
360     def go(self, url, urldata, d):
361         """
362         Fetch urls
363         Assumes localpath was called first
364         """
365         raise NoMethodError("Missing implementation for url")
366
367     def getSRCDate(urldata, d):
368         """
369         Return the SRC Date for the component
370
371         d the bb.data module
372         """
373         if "srcdate" in urldata.parm:
374             return urldata.parm['srcdate']
375
376         pn = data.getVar("PN", d, 1)
377
378         if pn:
379             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)
380
381         return data.getVar("SRCDATE", d, 1) or data.getVar("CVSDATE", d, 1) or data.getVar("DATE", d, 1)
382     getSRCDate = staticmethod(getSRCDate)
383
384     def srcrev_internal_helper(ud, d):
385         """
386         Return:
387             a) a source revision if specified
388             b) True if auto srcrev is in action
389             c) False otherwise
390         """
391
392         if 'rev' in ud.parm:
393             return ud.parm['rev']
394
395         if 'tag' in ud.parm:
396             return ud.parm['tag']
397
398         rev = None
399         if 'name' in ud.parm:
400             pn = data.getVar("PN", d, 1)
401             rev = data.getVar("SRCREV_pn-" + pn + "_" + ud.parm['name'], d, 1)
402         if not rev:
403             rev = data.getVar("SRCREV", d, 1)
404         if not rev:
405             return False
406         if rev is "SRCREVINACTION":
407             return True
408         return rev
409
410     srcrev_internal_helper = staticmethod(srcrev_internal_helper)
411
412     def try_mirror(d, tarfn):
413         """
414         Try to use a mirrored version of the sources. We do this
415         to avoid massive loads on foreign cvs and svn servers.
416         This method will be used by the different fetcher
417         implementations.
418
419         d Is a bb.data instance
420         tarfn is the name of the tarball
421         """
422         tarpath = os.path.join(data.getVar("DL_DIR", d, 1), tarfn)
423         if os.access(tarpath, os.R_OK):
424             bb.msg.debug(1, bb.msg.domain.Fetcher, "%s already exists, skipping checkout." % tarfn)
425             return True
426
427         pn = data.getVar('PN', d, True)
428         src_tarball_stash = None
429         if pn:
430             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()
431
432         for stash in src_tarball_stash:
433             fetchcmd = data.getVar("FETCHCOMMAND_mirror", d, True) or data.getVar("FETCHCOMMAND_wget", d, True)
434             uri = stash + tarfn
435             bb.msg.note(1, bb.msg.domain.Fetcher, "fetch " + uri)
436             fetchcmd = fetchcmd.replace("${URI}", uri)
437             ret = os.system(fetchcmd)
438             if ret == 0:
439                 bb.msg.note(1, bb.msg.domain.Fetcher, "Fetched %s from tarball stash, skipping checkout" % tarfn)
440                 return True
441         return False
442     try_mirror = staticmethod(try_mirror)
443
444     def verify_md5sum(ud, got_sum):
445         """
446         Verify the md5sum we wanted with the one we got
447         """
448         wanted_sum = None
449         if 'md5sum' in ud.parm:
450             wanted_sum = ud.parm['md5sum']
451         if not wanted_sum:
452             return True
453
454         return wanted_sum == got_sum
455     verify_md5sum = staticmethod(verify_md5sum)
456
457     def write_md5sum(url, ud, d):
458         if bb.which(data.getVar('PATH', d), 'md5sum'):
459             try:
460                 md5pipe = os.popen('md5sum ' + ud.localpath)
461                 md5data = (md5pipe.readline().split() or [ "" ])[0]
462                 md5pipe.close()
463             except OSError:
464                 md5data = ""
465
466         # verify the md5sum
467         if not Fetch.verify_md5sum(ud, md5data):
468             raise MD5SumError(url)
469
470         md5out = file(ud.md5, 'w')
471         md5out.write(md5data)
472         md5out.close()
473     write_md5sum = staticmethod(write_md5sum)
474
475     def latest_revision(self, url, ud, d):
476         """
477         Look in the cache for the latest revision, if not present ask the SCM.
478         """
479         if not hasattr(self, "_latest_revision"):
480             raise ParameterError
481
482         pd = persist_data.PersistData(d)
483         key = self._revision_key(url, ud, d)
484         rev = pd.getValue("BB_URI_HEADREVS", key)
485         if rev != None:
486             return str(rev)
487
488         rev = self._latest_revision(url, ud, d)
489         pd.setValue("BB_URI_HEADREVS", key, rev)
490         return rev
491
492     def sortable_revision(self, url, ud, d):
493         """
494         
495         """
496         if hasattr(self, "_sortable_revision"):
497             return self._sortable_revision(url, ud, d)
498
499         pd = persist_data.PersistData(d)
500         key = self._revision_key(url, ud, d)
501         latest_rev = self._build_revision(url, ud, d)
502         last_rev = pd.getValue("BB_URI_LOCALCOUNT", key + "_rev")
503         count = pd.getValue("BB_URI_LOCALCOUNT", key + "_count")
504
505         if last_rev == latest_rev:
506             return str(count + "+" + latest_rev)
507
508         if count is None:
509             count = "0"
510         else:
511             count = str(int(count) + 1)
512
513         pd.setValue("BB_URI_LOCALCOUNT", key + "_rev", latest_rev)
514         pd.setValue("BB_URI_LOCALCOUNT", key + "_count", count)
515
516         return str(count + "+" + latest_rev)
517
518
519 import cvs
520 import git
521 import local
522 import svn
523 import wget
524 import svk
525 import ssh
526 import perforce
527 import bzr
528 import hg
529
530 methods.append(local.Local())
531 methods.append(wget.Wget())
532 methods.append(svn.Svn())
533 methods.append(git.Git())
534 methods.append(cvs.Cvs())
535 methods.append(svk.Svk())
536 methods.append(ssh.SSH())
537 methods.append(perforce.Perforce())
538 methods.append(bzr.Bzr())
539 methods.append(hg.Hg())