unify mirror support and make it independant of the 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
28 import bb
29 from   bb import data
30 from   bb import persist_data
31
32 class FetchError(Exception):
33     """Exception raised when a download fails"""
34
35 class NoMethodError(Exception):
36     """Exception raised when there is no method to obtain a supplied url or set of urls"""
37
38 class MissingParameterError(Exception):
39     """Exception raised when a fetch method is missing a critical parameter in the url"""
40
41 class ParameterError(Exception):
42     """Exception raised when a url cannot be proccessed due to invalid parameters."""
43
44 class MD5SumError(Exception):
45     """Exception raised when a MD5SUM of a file does not match the expected one"""
46
47 class InvalidSRCREV(Exception):
48     """Exception raised when an invalid SRCREV is encountered"""
49
50 def uri_replace(uri, uri_find, uri_replace, d):
51 #   bb.msg.note(1, bb.msg.domain.Fetcher, "uri_replace: operating on %s" % uri)
52     if not uri or not uri_find or not uri_replace:
53         bb.msg.debug(1, bb.msg.domain.Fetcher, "uri_replace: passed an undefined value, not replacing")
54     uri_decoded = list(bb.decodeurl(uri))
55     uri_find_decoded = list(bb.decodeurl(uri_find))
56     uri_replace_decoded = list(bb.decodeurl(uri_replace))
57     result_decoded = ['','','','','',{}]
58     for i in uri_find_decoded:
59         loc = uri_find_decoded.index(i)
60         result_decoded[loc] = uri_decoded[loc]
61         import types
62         if type(i) == types.StringType:
63             if (re.match(i, uri_decoded[loc])):
64                 result_decoded[loc] = re.sub(i, uri_replace_decoded[loc], uri_decoded[loc])
65                 if uri_find_decoded.index(i) == 2:
66                     if d:
67                         localfn = bb.fetch.localpath(uri, d)
68                         if localfn:
69                             result_decoded[loc] = os.path.dirname(result_decoded[loc]) + "/" + os.path.basename(bb.fetch.localpath(uri, d))
70 #                       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]))
71             else:
72 #               bb.msg.note(1, bb.msg.domain.Fetcher, "uri_replace: no match")
73                 return uri
74 #           else:
75 #               for j in i.keys():
76 #                   FIXME: apply replacements against options
77     return bb.encodeurl(result_decoded)
78
79 methods = []
80 urldata_cache = {}
81 saved_headrevs = {}
82
83 def fetcher_init(d):
84     """
85     Called to initilize the fetchers once the configuration data is known
86     Calls before this must not hit the cache.
87     """
88     pd = persist_data.PersistData(d)
89     # When to drop SCM head revisions controled by user policy
90     srcrev_policy = bb.data.getVar('BB_SRCREV_POLICY', d, 1) or "clear"
91     if srcrev_policy == "cache":
92         bb.msg.debug(1, bb.msg.domain.Fetcher, "Keeping SRCREV cache due to cache policy of: %s" % srcrev_policy)
93     elif srcrev_policy == "clear":
94         bb.msg.debug(1, bb.msg.domain.Fetcher, "Clearing SRCREV cache due to cache policy of: %s" % srcrev_policy)
95         try:
96             bb.fetch.saved_headrevs = pd.getKeyValues("BB_URI_HEADREVS")
97         except:
98             pass
99         pd.delDomain("BB_URI_HEADREVS")
100     else:
101         bb.msg.fatal(bb.msg.domain.Fetcher, "Invalid SRCREV cache policy of: %s" % srcrev_policy)
102
103     for m in methods:
104         if hasattr(m, "init"):
105             m.init(d)
106
107     # Make sure our domains exist
108     pd.addDomain("BB_URI_HEADREVS")
109     pd.addDomain("BB_URI_LOCALCOUNT")
110
111 def fetcher_compare_revisons(d):
112     """
113     Compare the revisions in the persistant cache with current values and
114     return true/false on whether they've changed.
115     """
116
117     pd = persist_data.PersistData(d)
118     data = pd.getKeyValues("BB_URI_HEADREVS")
119     data2 = bb.fetch.saved_headrevs
120
121     changed = False
122     for key in data:
123         if key not in data2 or data2[key] != data[key]:
124             bb.msg.debug(1, bb.msg.domain.Fetcher, "%s changed" % key)
125             changed = True
126             return True
127         else:
128             bb.msg.debug(2, bb.msg.domain.Fetcher, "%s did not change" % key)
129     return False
130
131 # Function call order is usually:
132 #   1. init
133 #   2. go
134 #   3. localpaths
135 # localpath can be called at any time
136
137 def init(urls, d, setup = True):
138     urldata = {}
139     fn = bb.data.getVar('FILE', d, 1)
140     if fn in urldata_cache:
141         urldata = urldata_cache[fn]
142
143     for url in urls:
144         if url not in urldata:
145             urldata[url] = FetchData(url, d)
146
147     if setup:
148         for url in urldata:
149             if not urldata[url].setup:
150                 urldata[url].setup_localpath(d) 
151
152     urldata_cache[fn] = urldata
153     return urldata
154
155 def go(d, urls = None):
156     """
157     Fetch all urls
158     init must have previously been called
159     """
160     if not urls:
161         urls = d.getVar("SRC_URI", 1).split()
162     urldata = init(urls, d, True)
163
164     for u in urls:
165         ud = urldata[u]
166         m = ud.method
167         if ud.localfile:
168             if not m.forcefetch(u, ud, d) and os.path.exists(ud.md5):
169                 # File already present along with md5 stamp file
170                 # Touch md5 file to show activity
171                 try:
172                     os.utime(ud.md5, None)
173                 except:
174                     # Errors aren't fatal here
175                     pass
176                 continue
177             lf = bb.utils.lockfile(ud.lockfile)
178             if not m.forcefetch(u, ud, d) and os.path.exists(ud.md5):
179                 # If someone else fetched this before we got the lock, 
180                 # notice and don't try again
181                 try:
182                     os.utime(ud.md5, None)
183                 except:
184                     # Errors aren't fatal here
185                     pass
186                 bb.utils.unlockfile(lf)
187                 continue
188
189         # First try fetching uri, u, from PREMIRRORS
190         mirrors = [ i.split() for i in (bb.data.getVar('PREMIRRORS', d, 1) or "").split('\n') if i ]
191         if not  try_mirrors(d, u, mirrors):
192             # Next try fetching from the original uri, u
193             try:
194                 m.go(u, ud, d)
195             except:
196                 # Finally, try fetching uri, u, from MIRRORS
197                 mirrors = [ i.split() for i in (bb.data.getVar('MIRRORS', d, 1) or "").split('\n') if i ]
198                 try_mirrors (d, u, mirrors)
199
200         if ud.localfile:
201             if not m.forcefetch(u, ud, d):
202                 Fetch.write_md5sum(u, ud, d)
203             bb.utils.unlockfile(lf)
204
205
206 def checkstatus(d):
207     """
208     Check all urls exist upstream
209     init must have previously been called
210     """
211     urldata = init([], d, True)
212
213     for u in urldata:
214         ud = urldata[u]
215         m = ud.method
216         bb.msg.note(1, bb.msg.domain.Fetcher, "Testing URL %s" % u)
217         ret = m.checkstatus(u, ud, d)
218         if not ret:
219             bb.msg.fatal(bb.msg.domain.Fetcher, "URL %s doesn't work" % u)
220
221 def localpaths(d):
222     """
223     Return a list of the local filenames, assuming successful fetch
224     """
225     local = []
226     urldata = init([], d, True)
227
228     for u in urldata:
229         ud = urldata[u]      
230         local.append(ud.localpath)
231
232     return local
233
234 srcrev_internal_call = False
235
236 def get_srcrev(d):
237     """
238     Return the version string for the current package
239     (usually to be used as PV)
240     Most packages usually only have one SCM so we just pass on the call.
241     In the multi SCM case, we build a value based on SRCREV_FORMAT which must 
242     have been set.
243     """
244
245     #
246     # Ugly code alert. localpath in the fetchers will try to evaluate SRCREV which 
247     # could translate into a call to here. If it does, we need to catch this
248     # and provide some way so it knows get_srcrev is active instead of being
249     # some number etc. hence the srcrev_internal_call tracking and the magic  
250     # "SRCREVINACTION" return value.
251     #
252     # Neater solutions welcome!
253     #
254     if bb.fetch.srcrev_internal_call:
255         return "SRCREVINACTION"
256
257     scms = []
258
259     # Only call setup_localpath on URIs which suppports_srcrev() 
260     urldata = init(bb.data.getVar('SRC_URI', d, 1).split(), d, False)
261     for u in urldata:
262         ud = urldata[u]
263         if ud.method.suppports_srcrev():
264             if not ud.setup:
265                 ud.setup_localpath(d)
266             scms.append(u)
267
268     if len(scms) == 0:
269         bb.msg.error(bb.msg.domain.Fetcher, "SRCREV was used yet no valid SCM was found in SRC_URI")
270         raise ParameterError
271
272     bb.data.setVar('__BB_DONT_CACHE','1', d)
273
274     if len(scms) == 1:
275         return urldata[scms[0]].method.sortable_revision(scms[0], urldata[scms[0]], d)
276
277     #
278     # Mutiple SCMs are in SRC_URI so we resort to SRCREV_FORMAT
279     #
280     format = bb.data.getVar('SRCREV_FORMAT', d, 1)
281     if not format:
282         bb.msg.error(bb.msg.domain.Fetcher, "The SRCREV_FORMAT variable must be set when multiple SCMs are used.")
283         raise ParameterError
284
285     for scm in scms:
286         if 'name' in urldata[scm].parm:
287             name = urldata[scm].parm["name"]
288             rev = urldata[scm].method.sortable_revision(scm, urldata[scm], d)
289             format = format.replace(name, rev)
290
291     return format
292
293 def localpath(url, d, cache = True):
294     """
295     Called from the parser with cache=False since the cache isn't ready 
296     at this point. Also called from classed in OE e.g. patch.bbclass
297     """
298     ud = init([url], d)
299     if ud[url].method:
300         return ud[url].localpath
301     return url
302
303 def runfetchcmd(cmd, d, quiet = False):
304     """
305     Run cmd returning the command output
306     Raise an error if interrupted or cmd fails
307     Optionally echo command output to stdout
308     """
309
310     # Need to export PATH as binary could be in metadata paths
311     # rather than host provided
312     # Also include some other variables.
313     # FIXME: Should really include all export varaiables?
314     exportvars = ['PATH', 'GIT_PROXY_COMMAND', 'GIT_PROXY_HOST', 'GIT_PROXY_PORT', 'GIT_CONFIG', 'http_proxy', 'ftp_proxy', 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', 'HOME']
315
316     for var in exportvars:
317         val = data.getVar(var, d, True)
318         if val:
319             cmd = 'export ' + var + '=%s; %s' % (val, cmd)
320
321     bb.msg.debug(1, bb.msg.domain.Fetcher, "Running %s" % cmd)
322
323     # redirect stderr to stdout
324     stdout_handle = os.popen(cmd + " 2>&1", "r")
325     output = ""
326
327     while 1:
328         line = stdout_handle.readline()
329         if not line:
330             break
331         if not quiet:
332             print line,
333         output += line
334
335     status =  stdout_handle.close() or 0
336     signal = status >> 8
337     exitstatus = status & 0xff
338
339     if signal:
340         raise FetchError("Fetch command %s failed with signal %s, output:\n%s" % (cmd, signal, output))
341     elif status != 0:
342         raise FetchError("Fetch command %s failed with exit code %s, output:\n%s" % (cmd, status, output))
343
344     return output
345
346 def try_mirrors(d, uri, mirrors):
347     """
348     Try to use a mirrored version of the sources.
349     This method will be automatically called before the fetchers go.
350
351     d Is a bb.data instance
352     uri is the original uri we're trying to download
353     mirrors is the list of mirrors we're going to try
354     """
355     fpath = os.path.join(data.getVar("DL_DIR", d, 1), os.path.basename(uri))
356     if os.access(fpath, os.R_OK):
357         bb.msg.debug(1, bb.msg.domain.Fetcher, "%s already exists, skipping checkout." % fpath)
358         return True
359
360     ld = d.createCopy()
361     for (find, replace) in mirrors:
362         newuri = uri_replace(uri, find, replace, ld)
363         if newuri != uri:
364             try:
365                 ud = FetchData(newuri, ld)
366             except bb.fetch.NoMethodError:
367                 bb.msg.debug(1, bb.msg.domain.Fetcher, "No method for %s" % url)
368                 continue
369
370             ud.setup_localpath(ld)
371
372             try:
373                 ud.method.go(newuri, ud, ld)
374                 return True
375             except (bb.fetch.MissingParameterError,
376                     bb.fetch.FetchError,
377                     bb.fetch.MD5SumError):
378                 import sys
379                 (type, value, traceback) = sys.exc_info()
380                 bb.msg.debug(2, bb.msg.domain.Fetcher, "Mirror fetch failure: %s" % value)
381                 return False
382
383
384 class FetchData(object):
385     """
386     A class which represents the fetcher state for a given URI.
387     """
388     def __init__(self, url, d):
389         self.localfile = ""
390         (self.type, self.host, self.path, self.user, self.pswd, self.parm) = bb.decodeurl(data.expand(url, d))
391         self.date = Fetch.getSRCDate(self, d)
392         self.url = url
393         if not self.user and "user" in self.parm:
394             self.user = self.parm["user"]
395         if not self.pswd and "pswd" in self.parm:
396             self.pswd = self.parm["pswd"]
397         self.setup = False
398         for m in methods:
399             if m.supports(url, self, d):
400                 self.method = m
401                 return
402         raise NoMethodError("Missing implementation for url %s" % url)
403
404     def setup_localpath(self, d):
405         self.setup = True
406         if "localpath" in self.parm:
407             # if user sets localpath for file, use it instead.
408             self.localpath = self.parm["localpath"]
409         else:
410             try:
411                 bb.fetch.srcrev_internal_call = True
412                 self.localpath = self.method.localpath(self.url, self, d)
413             finally:
414                 bb.fetch.srcrev_internal_call = False
415             # We have to clear data's internal caches since the cached value of SRCREV is now wrong.
416             # Horrible...
417             bb.data.delVar("ISHOULDNEVEREXIST", d)
418         self.md5 = self.localpath + '.md5'
419         self.lockfile = self.localpath + '.lock'
420
421
422 class Fetch(object):
423     """Base class for 'fetch'ing data"""
424
425     def __init__(self, urls = []):
426         self.urls = []
427
428     def supports(self, url, urldata, d):
429         """
430         Check to see if this fetch class supports a given url.
431         """
432         return 0
433
434     def localpath(self, url, urldata, d):
435         """
436         Return the local filename of a given url assuming a successful fetch.
437         Can also setup variables in urldata for use in go (saving code duplication 
438         and duplicate code execution)
439         """
440         return url
441
442     def setUrls(self, urls):
443         self.__urls = urls
444
445     def getUrls(self):
446         return self.__urls
447
448     urls = property(getUrls, setUrls, None, "Urls property")
449
450     def forcefetch(self, url, urldata, d):
451         """
452         Force a fetch, even if localpath exists?
453         """
454         return False
455
456     def suppports_srcrev(self):
457         """
458         The fetcher supports auto source revisions (SRCREV)
459         """
460         return False
461
462     def go(self, url, urldata, d):
463         """
464         Fetch urls
465         Assumes localpath was called first
466         """
467         raise NoMethodError("Missing implementation for url")
468
469     def checkstatus(self, url, urldata, d):
470         """
471         Check the status of a URL
472         Assumes localpath was called first
473         """
474         bb.msg.note(1, bb.msg.domain.Fetcher, "URL %s could not be checked for status since no method exists." % url)
475         return True
476
477     def getSRCDate(urldata, d):
478         """
479         Return the SRC Date for the component
480
481         d the bb.data module
482         """
483         if "srcdate" in urldata.parm:
484             return urldata.parm['srcdate']
485
486         pn = data.getVar("PN", d, 1)
487
488         if pn:
489             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)
490
491         return data.getVar("SRCDATE", d, 1) or data.getVar("CVSDATE", d, 1) or data.getVar("DATE", d, 1)
492     getSRCDate = staticmethod(getSRCDate)
493
494     def srcrev_internal_helper(ud, d):
495         """
496         Return:
497             a) a source revision if specified
498             b) True if auto srcrev is in action
499             c) False otherwise
500         """
501
502         if 'rev' in ud.parm:
503             return ud.parm['rev']
504
505         if 'tag' in ud.parm:
506             return ud.parm['tag']
507
508         rev = None
509         if 'name' in ud.parm:
510             pn = data.getVar("PN", d, 1)
511             rev = data.getVar("SRCREV_pn-" + pn + "_" + ud.parm['name'], d, 1)
512         if not rev:
513             rev = data.getVar("SRCREV", d, 1)
514         if rev == "INVALID":
515             raise InvalidSRCREV("Please set SRCREV to a valid value")
516         if not rev:
517             return False
518         if rev is "SRCREVINACTION":
519             return True
520         return rev
521
522     srcrev_internal_helper = staticmethod(srcrev_internal_helper)
523
524     def localcount_internal_helper(ud, d):
525         """
526         Return:
527             a) a locked localcount if specified
528             b) None otherwise
529         """
530
531         localcount= None
532         if 'name' in ud.parm:
533             pn = data.getVar("PN", d, 1)
534             localcount = data.getVar("LOCALCOUNT_" + ud.parm['name'], d, 1)
535         if not localcount:
536             localcount = data.getVar("LOCALCOUNT", d, 1)
537         return localcount
538
539     localcount_internal_helper = staticmethod(localcount_internal_helper)
540
541     def verify_md5sum(ud, got_sum):
542         """
543         Verify the md5sum we wanted with the one we got
544         """
545         wanted_sum = None
546         if 'md5sum' in ud.parm:
547             wanted_sum = ud.parm['md5sum']
548         if not wanted_sum:
549             return True
550
551         return wanted_sum == got_sum
552     verify_md5sum = staticmethod(verify_md5sum)
553
554     def write_md5sum(url, ud, d):
555         md5data = bb.utils.md5_file(ud.localpath)
556         # verify the md5sum
557         if not Fetch.verify_md5sum(ud, md5data):
558             raise MD5SumError(url)
559
560         md5out = file(ud.md5, 'w')
561         md5out.write(md5data)
562         md5out.close()
563     write_md5sum = staticmethod(write_md5sum)
564
565     def latest_revision(self, url, ud, d):
566         """
567         Look in the cache for the latest revision, if not present ask the SCM.
568         """
569         if not hasattr(self, "_latest_revision"):
570             raise ParameterError
571
572         pd = persist_data.PersistData(d)
573         key = self.generate_revision_key(url, ud, d)
574         rev = pd.getValue("BB_URI_HEADREVS", key)
575         if rev != None:
576             return str(rev)
577
578         rev = self._latest_revision(url, ud, d)
579         pd.setValue("BB_URI_HEADREVS", key, rev)
580         return rev
581
582     def sortable_revision(self, url, ud, d):
583         """
584         
585         """
586         if hasattr(self, "_sortable_revision"):
587             return self._sortable_revision(url, ud, d)
588
589         pd = persist_data.PersistData(d)
590         key = self.generate_revision_key(url, ud, d)
591
592         latest_rev = self._build_revision(url, ud, d)
593         last_rev = pd.getValue("BB_URI_LOCALCOUNT", key + "_rev")
594         uselocalcount = bb.data.getVar("BB_LOCALCOUNT_OVERRIDE", d, True) or False
595         count = None
596         if uselocalcount:
597             count = Fetch.localcount_internal_helper(ud, d)
598         if count is None:
599             count = pd.getValue("BB_URI_LOCALCOUNT", key + "_count")
600
601         if last_rev == latest_rev:
602             return str(count + "+" + latest_rev)
603
604         buildindex_provided = hasattr(self, "_sortable_buildindex")
605         if buildindex_provided:
606             count = self._sortable_buildindex(url, ud, d, latest_rev)
607
608         if count is None:
609             count = "0"
610         elif uselocalcount or buildindex_provided:
611             count = str(count)
612         else:
613             count = str(int(count) + 1)
614
615         pd.setValue("BB_URI_LOCALCOUNT", key + "_rev", latest_rev)
616         pd.setValue("BB_URI_LOCALCOUNT", key + "_count", count)
617
618         return str(count + "+" + latest_rev)
619
620     def generate_revision_key(self, url, ud, d):
621         key = self._revision_key(url, ud, d)
622         return "%s-%s" % (key, bb.data.getVar("PN", d, True) or "")
623
624 import cvs
625 import git
626 import local
627 import svn
628 import wget
629 import svk
630 import ssh
631 import perforce
632 import bzr
633 import hg
634 import osc
635
636 methods.append(local.Local())
637 methods.append(wget.Wget())
638 methods.append(svn.Svn())
639 methods.append(git.Git())
640 methods.append(cvs.Cvs())
641 methods.append(svk.Svk())
642 methods.append(ssh.SSH())
643 methods.append(perforce.Perforce())
644 methods.append(bzr.Bzr())
645 methods.append(hg.Hg())
646 methods.append(osc.Osc())