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