xref: /openbmc/openbmc/poky/bitbake/lib/bb/fetch2/wget.py (revision 44b3caf2)
1"""
2BitBake 'Fetch' implementations
3
4Classes for obtaining upstream sources for the
5BitBake build tools.
6
7"""
8
9# Copyright (C) 2003, 2004  Chris Larson
10#
11# SPDX-License-Identifier: GPL-2.0-only
12#
13# Based on functions from the base bb module, Copyright 2003 Holger Schurig
14
15import shlex
16import re
17import tempfile
18import os
19import errno
20import bb
21import bb.progress
22import socket
23import http.client
24import urllib.request, urllib.parse, urllib.error
25from   bb.fetch2 import FetchMethod
26from   bb.fetch2 import FetchError
27from   bb.fetch2 import logger
28from   bb.fetch2 import runfetchcmd
29from   bs4 import BeautifulSoup
30from   bs4 import SoupStrainer
31
32class WgetProgressHandler(bb.progress.LineFilterProgressHandler):
33    """
34    Extract progress information from wget output.
35    Note: relies on --progress=dot (with -v or without -q/-nv) being
36    specified on the wget command line.
37    """
38    def __init__(self, d):
39        super(WgetProgressHandler, self).__init__(d)
40        # Send an initial progress event so the bar gets shown
41        self._fire_progress(0)
42
43    def writeline(self, line):
44        percs = re.findall(r'(\d+)%\s+([\d.]+[A-Z])', line)
45        if percs:
46            progress = int(percs[-1][0])
47            rate = percs[-1][1] + '/s'
48            self.update(progress, rate)
49            return False
50        return True
51
52
53class Wget(FetchMethod):
54    """Class to fetch urls via 'wget'"""
55
56    # CDNs like CloudFlare may do a 'browser integrity test' which can fail
57    # with the standard wget/urllib User-Agent, so pretend to be a modern
58    # browser.
59    user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0"
60
61    def check_certs(self, d):
62        """
63        Should certificates be checked?
64        """
65        return (d.getVar("BB_CHECK_SSL_CERTS") or "1") != "0"
66
67    def supports(self, ud, d):
68        """
69        Check to see if a given url can be fetched with wget.
70        """
71        return ud.type in ['http', 'https', 'ftp', 'ftps']
72
73    def recommends_checksum(self, urldata):
74        return True
75
76    def urldata_init(self, ud, d):
77        if 'protocol' in ud.parm:
78            if ud.parm['protocol'] == 'git':
79                raise bb.fetch2.ParameterError("Invalid protocol - if you wish to fetch from a git repository using http, you need to instead use the git:// prefix with protocol=http", ud.url)
80
81        if 'downloadfilename' in ud.parm:
82            ud.basename = ud.parm['downloadfilename']
83        else:
84            ud.basename = os.path.basename(ud.path)
85
86        ud.localfile = d.expand(urllib.parse.unquote(ud.basename))
87        if not ud.localfile:
88            ud.localfile = d.expand(urllib.parse.unquote(ud.host + ud.path).replace("/", "."))
89
90        self.basecmd = d.getVar("FETCHCMD_wget") or "/usr/bin/env wget -t 2 -T 30"
91
92        if ud.type == 'ftp' or ud.type == 'ftps':
93            self.basecmd += " --passive-ftp"
94
95        if not self.check_certs(d):
96            self.basecmd += " --no-check-certificate"
97
98    def _runwget(self, ud, d, command, quiet, workdir=None):
99
100        progresshandler = WgetProgressHandler(d)
101
102        logger.debug2("Fetching %s using command '%s'" % (ud.url, command))
103        bb.fetch2.check_network_access(d, command, ud.url)
104        runfetchcmd(command + ' --progress=dot -v', d, quiet, log=progresshandler, workdir=workdir)
105
106    def download(self, ud, d):
107        """Fetch urls"""
108
109        fetchcmd = self.basecmd
110
111        localpath = os.path.join(d.getVar("DL_DIR"), ud.localfile) + ".tmp"
112        bb.utils.mkdirhier(os.path.dirname(localpath))
113        fetchcmd += " -O %s" % shlex.quote(localpath)
114
115        if ud.user and ud.pswd:
116            fetchcmd += " --auth-no-challenge"
117            if ud.parm.get("redirectauth", "1") == "1":
118                # An undocumented feature of wget is that if the
119                # username/password are specified on the URI, wget will only
120                # send the Authorization header to the first host and not to
121                # any hosts that it is redirected to.  With the increasing
122                # usage of temporary AWS URLs, this difference now matters as
123                # AWS will reject any request that has authentication both in
124                # the query parameters (from the redirect) and in the
125                # Authorization header.
126                fetchcmd += " --user=%s --password=%s" % (ud.user, ud.pswd)
127
128        uri = ud.url.split(";")[0]
129        if os.path.exists(ud.localpath):
130            # file exists, but we didnt complete it.. trying again..
131            fetchcmd += d.expand(" -c -P ${DL_DIR} '%s'" % uri)
132        else:
133            fetchcmd += d.expand(" -P ${DL_DIR} '%s'" % uri)
134
135        self._runwget(ud, d, fetchcmd, False)
136
137        # Try and verify any checksum now, meaning if it isn't correct, we don't remove the
138        # original file, which might be a race (imagine two recipes referencing the same
139        # source, one with an incorrect checksum)
140        bb.fetch2.verify_checksum(ud, d, localpath=localpath, fatal_nochecksum=False)
141
142        # Remove the ".tmp" and move the file into position atomically
143        # Our lock prevents multiple writers but mirroring code may grab incomplete files
144        os.rename(localpath, localpath[:-4])
145
146        # Sanity check since wget can pretend it succeed when it didn't
147        # Also, this used to happen if sourceforge sent us to the mirror page
148        if not os.path.exists(ud.localpath):
149            raise FetchError("The fetch command returned success for url %s but %s doesn't exist?!" % (uri, ud.localpath), uri)
150
151        if os.path.getsize(ud.localpath) == 0:
152            os.remove(ud.localpath)
153            raise FetchError("The fetch of %s resulted in a zero size file?! Deleting and failing since this isn't right." % (uri), uri)
154
155        return True
156
157    def checkstatus(self, fetch, ud, d, try_again=True):
158        class HTTPConnectionCache(http.client.HTTPConnection):
159            if fetch.connection_cache:
160                def connect(self):
161                    """Connect to the host and port specified in __init__."""
162
163                    sock = fetch.connection_cache.get_connection(self.host, self.port)
164                    if sock:
165                        self.sock = sock
166                    else:
167                        self.sock = socket.create_connection((self.host, self.port),
168                                    self.timeout, self.source_address)
169                        fetch.connection_cache.add_connection(self.host, self.port, self.sock)
170
171                    if self._tunnel_host:
172                        self._tunnel()
173
174        class CacheHTTPHandler(urllib.request.HTTPHandler):
175            def http_open(self, req):
176                return self.do_open(HTTPConnectionCache, req)
177
178            def do_open(self, http_class, req):
179                """Return an addinfourl object for the request, using http_class.
180
181                http_class must implement the HTTPConnection API from httplib.
182                The addinfourl return value is a file-like object.  It also
183                has methods and attributes including:
184                    - info(): return a mimetools.Message object for the headers
185                    - geturl(): return the original request URL
186                    - code: HTTP status code
187                """
188                host = req.host
189                if not host:
190                    raise urllib.error.URLError('no host given')
191
192                h = http_class(host, timeout=req.timeout) # will parse host:port
193                h.set_debuglevel(self._debuglevel)
194
195                headers = dict(req.unredirected_hdrs)
196                headers.update(dict((k, v) for k, v in list(req.headers.items())
197                            if k not in headers))
198
199                # We want to make an HTTP/1.1 request, but the addinfourl
200                # class isn't prepared to deal with a persistent connection.
201                # It will try to read all remaining data from the socket,
202                # which will block while the server waits for the next request.
203                # So make sure the connection gets closed after the (only)
204                # request.
205
206                # Don't close connection when connection_cache is enabled,
207                if fetch.connection_cache is None:
208                    headers["Connection"] = "close"
209                else:
210                    headers["Connection"] = "Keep-Alive" # Works for HTTP/1.0
211
212                headers = dict(
213                    (name.title(), val) for name, val in list(headers.items()))
214
215                if req._tunnel_host:
216                    tunnel_headers = {}
217                    proxy_auth_hdr = "Proxy-Authorization"
218                    if proxy_auth_hdr in headers:
219                        tunnel_headers[proxy_auth_hdr] = headers[proxy_auth_hdr]
220                        # Proxy-Authorization should not be sent to origin
221                        # server.
222                        del headers[proxy_auth_hdr]
223                    h.set_tunnel(req._tunnel_host, headers=tunnel_headers)
224
225                try:
226                    h.request(req.get_method(), req.selector, req.data, headers)
227                except socket.error as err: # XXX what error?
228                    # Don't close connection when cache is enabled.
229                    # Instead, try to detect connections that are no longer
230                    # usable (for example, closed unexpectedly) and remove
231                    # them from the cache.
232                    if fetch.connection_cache is None:
233                        h.close()
234                    elif isinstance(err, OSError) and err.errno == errno.EBADF:
235                        # This happens when the server closes the connection despite the Keep-Alive.
236                        # Apparently urllib then uses the file descriptor, expecting it to be
237                        # connected, when in reality the connection is already gone.
238                        # We let the request fail and expect it to be
239                        # tried once more ("try_again" in check_status()),
240                        # with the dead connection removed from the cache.
241                        # If it still fails, we give up, which can happen for bad
242                        # HTTP proxy settings.
243                        fetch.connection_cache.remove_connection(h.host, h.port)
244                    raise urllib.error.URLError(err)
245                else:
246                    r = h.getresponse()
247
248                # Pick apart the HTTPResponse object to get the addinfourl
249                # object initialized properly.
250
251                # Wrap the HTTPResponse object in socket's file object adapter
252                # for Windows.  That adapter calls recv(), so delegate recv()
253                # to read().  This weird wrapping allows the returned object to
254                # have readline() and readlines() methods.
255
256                # XXX It might be better to extract the read buffering code
257                # out of socket._fileobject() and into a base class.
258                r.recv = r.read
259
260                # no data, just have to read
261                r.read()
262                class fp_dummy(object):
263                    def read(self):
264                        return ""
265                    def readline(self):
266                        return ""
267                    def close(self):
268                        pass
269                    closed = False
270
271                resp = urllib.response.addinfourl(fp_dummy(), r.msg, req.get_full_url())
272                resp.code = r.status
273                resp.msg = r.reason
274
275                # Close connection when server request it.
276                if fetch.connection_cache is not None:
277                    if 'Connection' in r.msg and r.msg['Connection'] == 'close':
278                        fetch.connection_cache.remove_connection(h.host, h.port)
279
280                return resp
281
282        class HTTPMethodFallback(urllib.request.BaseHandler):
283            """
284            Fallback to GET if HEAD is not allowed (405 HTTP error)
285            """
286            def http_error_405(self, req, fp, code, msg, headers):
287                fp.read()
288                fp.close()
289
290                if req.get_method() != 'GET':
291                    newheaders = dict((k, v) for k, v in list(req.headers.items())
292                                      if k.lower() not in ("content-length", "content-type"))
293                    return self.parent.open(urllib.request.Request(req.get_full_url(),
294                                                            headers=newheaders,
295                                                            origin_req_host=req.origin_req_host,
296                                                            unverifiable=True))
297
298                raise urllib.request.HTTPError(req, code, msg, headers, None)
299
300            # Some servers (e.g. GitHub archives, hosted on Amazon S3) return 403
301            # Forbidden when they actually mean 405 Method Not Allowed.
302            http_error_403 = http_error_405
303
304
305        class FixedHTTPRedirectHandler(urllib.request.HTTPRedirectHandler):
306            """
307            urllib2.HTTPRedirectHandler resets the method to GET on redirect,
308            when we want to follow redirects using the original method.
309            """
310            def redirect_request(self, req, fp, code, msg, headers, newurl):
311                newreq = urllib.request.HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, headers, newurl)
312                newreq.get_method = req.get_method
313                return newreq
314
315        # We need to update the environment here as both the proxy and HTTPS
316        # handlers need variables set. The proxy needs http_proxy and friends to
317        # be set, and HTTPSHandler ends up calling into openssl to load the
318        # certificates. In buildtools configurations this will be looking at the
319        # wrong place for certificates by default: we set SSL_CERT_FILE to the
320        # right location in the buildtools environment script but as BitBake
321        # prunes prunes the environment this is lost. When binaries are executed
322        # runfetchcmd ensures these values are in the environment, but this is
323        # pure Python so we need to update the environment.
324        #
325        # Avoid tramping the environment too much by using bb.utils.environment
326        # to scope the changes to the build_opener request, which is when the
327        # environment lookups happen.
328        newenv = bb.fetch2.get_fetcher_environment(d)
329
330        with bb.utils.environment(**newenv):
331            import ssl
332
333            if self.check_certs(d):
334                context = ssl.create_default_context()
335            else:
336                context = ssl._create_unverified_context()
337
338            handlers = [FixedHTTPRedirectHandler,
339                        HTTPMethodFallback,
340                        urllib.request.ProxyHandler(),
341                        CacheHTTPHandler(),
342                        urllib.request.HTTPSHandler(context=context)]
343            opener = urllib.request.build_opener(*handlers)
344
345            try:
346                uri_base = ud.url.split(";")[0]
347                uri = "{}://{}{}".format(urllib.parse.urlparse(uri_base).scheme, ud.host, ud.path)
348                r = urllib.request.Request(uri)
349                r.get_method = lambda: "HEAD"
350                # Some servers (FusionForge, as used on Alioth) require that the
351                # optional Accept header is set.
352                r.add_header("Accept", "*/*")
353                r.add_header("User-Agent", self.user_agent)
354                def add_basic_auth(login_str, request):
355                    '''Adds Basic auth to http request, pass in login:password as string'''
356                    import base64
357                    encodeuser = base64.b64encode(login_str.encode('utf-8')).decode("utf-8")
358                    authheader = "Basic %s" % encodeuser
359                    r.add_header("Authorization", authheader)
360
361                if ud.user and ud.pswd:
362                    add_basic_auth(ud.user + ':' + ud.pswd, r)
363
364                try:
365                    import netrc
366                    auth_data = netrc.netrc().authenticators(urllib.parse.urlparse(uri).hostname)
367                    if auth_data:
368                        login, _, password = auth_data
369                        add_basic_auth("%s:%s" % (login, password), r)
370                except (FileNotFoundError, netrc.NetrcParseError):
371                    pass
372
373                with opener.open(r, timeout=30) as response:
374                    pass
375            except (urllib.error.URLError, ConnectionResetError, TimeoutError) as e:
376                if try_again:
377                    logger.debug2("checkstatus: trying again")
378                    return self.checkstatus(fetch, ud, d, False)
379                else:
380                    # debug for now to avoid spamming the logs in e.g. remote sstate searches
381                    logger.debug2("checkstatus() urlopen failed for %s: %s" % (uri,e))
382                    return False
383
384        return True
385
386    def _parse_path(self, regex, s):
387        """
388        Find and group name, version and archive type in the given string s
389        """
390
391        m = regex.search(s)
392        if m:
393            pname = ''
394            pver = ''
395            ptype = ''
396
397            mdict = m.groupdict()
398            if 'name' in mdict.keys():
399                pname = mdict['name']
400            if 'pver' in mdict.keys():
401                pver = mdict['pver']
402            if 'type' in mdict.keys():
403                ptype = mdict['type']
404
405            bb.debug(3, "_parse_path: %s, %s, %s" % (pname, pver, ptype))
406
407            return (pname, pver, ptype)
408
409        return None
410
411    def _modelate_version(self, version):
412        if version[0] in ['.', '-']:
413            if version[1].isdigit():
414                version = version[1] + version[0] + version[2:len(version)]
415            else:
416                version = version[1:len(version)]
417
418        version = re.sub('-', '.', version)
419        version = re.sub('_', '.', version)
420        version = re.sub('(rc)+', '.1000.', version)
421        version = re.sub('(beta)+', '.100.', version)
422        version = re.sub('(alpha)+', '.10.', version)
423        if version[0] == 'v':
424            version = version[1:len(version)]
425        return version
426
427    def _vercmp(self, old, new):
428        """
429        Check whether 'new' is newer than 'old' version. We use existing vercmp() for the
430        purpose. PE is cleared in comparison as it's not for build, and PR is cleared too
431        for simplicity as it's somehow difficult to get from various upstream format
432        """
433
434        (oldpn, oldpv, oldsuffix) = old
435        (newpn, newpv, newsuffix) = new
436
437        # Check for a new suffix type that we have never heard of before
438        if newsuffix:
439            m = self.suffix_regex_comp.search(newsuffix)
440            if not m:
441                bb.warn("%s has a possible unknown suffix: %s" % (newpn, newsuffix))
442                return False
443
444        # Not our package so ignore it
445        if oldpn != newpn:
446            return False
447
448        oldpv = self._modelate_version(oldpv)
449        newpv = self._modelate_version(newpv)
450
451        return bb.utils.vercmp(("0", oldpv, ""), ("0", newpv, ""))
452
453    def _fetch_index(self, uri, ud, d):
454        """
455        Run fetch checkstatus to get directory information
456        """
457        f = tempfile.NamedTemporaryFile()
458        with tempfile.TemporaryDirectory(prefix="wget-index-") as workdir, tempfile.NamedTemporaryFile(dir=workdir, prefix="wget-listing-") as f:
459            fetchcmd = self.basecmd
460            fetchcmd += " -O " + f.name + " --user-agent='" + self.user_agent + "' '" + uri + "'"
461            try:
462                self._runwget(ud, d, fetchcmd, True, workdir=workdir)
463                fetchresult = f.read()
464            except bb.fetch2.BBFetchException:
465                fetchresult = ""
466
467        return fetchresult
468
469    def _check_latest_version(self, url, package, package_regex, current_version, ud, d):
470        """
471        Return the latest version of a package inside a given directory path
472        If error or no version, return ""
473        """
474        valid = 0
475        version = ['', '', '']
476
477        bb.debug(3, "VersionURL: %s" % (url))
478        soup = BeautifulSoup(self._fetch_index(url, ud, d), "html.parser", parse_only=SoupStrainer("a"))
479        if not soup:
480            bb.debug(3, "*** %s NO SOUP" % (url))
481            return ""
482
483        for line in soup.find_all('a', href=True):
484            bb.debug(3, "line['href'] = '%s'" % (line['href']))
485            bb.debug(3, "line = '%s'" % (str(line)))
486
487            newver = self._parse_path(package_regex, line['href'])
488            if not newver:
489                newver = self._parse_path(package_regex, str(line))
490
491            if newver:
492                bb.debug(3, "Upstream version found: %s" % newver[1])
493                if valid == 0:
494                    version = newver
495                    valid = 1
496                elif self._vercmp(version, newver) < 0:
497                    version = newver
498
499        pupver = re.sub('_', '.', version[1])
500
501        bb.debug(3, "*** %s -> UpstreamVersion = %s (CurrentVersion = %s)" %
502                (package, pupver or "N/A", current_version[1]))
503
504        if valid:
505            return pupver
506
507        return ""
508
509    def _check_latest_version_by_dir(self, dirver, package, package_regex, current_version, ud, d):
510        """
511        Scan every directory in order to get upstream version.
512        """
513        version_dir = ['', '', '']
514        version = ['', '', '']
515
516        dirver_regex = re.compile(r"(?P<pfx>\D*)(?P<ver>(\d+[\.\-_])*(\d+))")
517        s = dirver_regex.search(dirver)
518        if s:
519            version_dir[1] = s.group('ver')
520        else:
521            version_dir[1] = dirver
522
523        dirs_uri = bb.fetch.encodeurl([ud.type, ud.host,
524                ud.path.split(dirver)[0], ud.user, ud.pswd, {}])
525        bb.debug(3, "DirURL: %s, %s" % (dirs_uri, package))
526
527        soup = BeautifulSoup(self._fetch_index(dirs_uri, ud, d), "html.parser", parse_only=SoupStrainer("a"))
528        if not soup:
529            return version[1]
530
531        for line in soup.find_all('a', href=True):
532            s = dirver_regex.search(line['href'].strip("/"))
533            if s:
534                sver = s.group('ver')
535
536                # When prefix is part of the version directory it need to
537                # ensure that only version directory is used so remove previous
538                # directories if exists.
539                #
540                # Example: pfx = '/dir1/dir2/v' and version = '2.5' the expected
541                # result is v2.5.
542                spfx = s.group('pfx').split('/')[-1]
543
544                version_dir_new = ['', sver, '']
545                if self._vercmp(version_dir, version_dir_new) <= 0:
546                    dirver_new = spfx + sver
547                    path = ud.path.replace(dirver, dirver_new, True) \
548                        .split(package)[0]
549                    uri = bb.fetch.encodeurl([ud.type, ud.host, path,
550                        ud.user, ud.pswd, {}])
551
552                    pupver = self._check_latest_version(uri,
553                            package, package_regex, current_version, ud, d)
554                    if pupver:
555                        version[1] = pupver
556
557                    version_dir = version_dir_new
558
559        return version[1]
560
561    def _init_regexes(self, package, ud, d):
562        """
563        Match as many patterns as possible such as:
564                gnome-common-2.20.0.tar.gz (most common format)
565                gtk+-2.90.1.tar.gz
566                xf86-input-synaptics-12.6.9.tar.gz
567                dri2proto-2.3.tar.gz
568                blktool_4.orig.tar.gz
569                libid3tag-0.15.1b.tar.gz
570                unzip552.tar.gz
571                icu4c-3_6-src.tgz
572                genext2fs_1.3.orig.tar.gz
573                gst-fluendo-mp3
574        """
575        # match most patterns which uses "-" as separator to version digits
576        pn_prefix1 = r"[a-zA-Z][a-zA-Z0-9]*([-_][a-zA-Z]\w+)*\+?[-_]"
577        # a loose pattern such as for unzip552.tar.gz
578        pn_prefix2 = r"[a-zA-Z]+"
579        # a loose pattern such as for 80325-quicky-0.4.tar.gz
580        pn_prefix3 = r"[0-9]+[-]?[a-zA-Z]+"
581        # Save the Package Name (pn) Regex for use later
582        pn_regex = r"(%s|%s|%s)" % (pn_prefix1, pn_prefix2, pn_prefix3)
583
584        # match version
585        pver_regex = r"(([A-Z]*\d+[a-zA-Z]*[\.\-_]*)+)"
586
587        # match arch
588        parch_regex = "-source|_all_"
589
590        # src.rpm extension was added only for rpm package. Can be removed if the rpm
591        # packaged will always be considered as having to be manually upgraded
592        psuffix_regex = r"(tar\.\w+|tgz|zip|xz|rpm|bz2|orig\.tar\.\w+|src\.tar\.\w+|src\.tgz|svnr\d+\.tar\.\w+|stable\.tar\.\w+|src\.rpm)"
593
594        # match name, version and archive type of a package
595        package_regex_comp = re.compile(r"(?P<name>%s?\.?v?)(?P<pver>%s)(?P<arch>%s)?[\.-](?P<type>%s$)"
596                                                    % (pn_regex, pver_regex, parch_regex, psuffix_regex))
597        self.suffix_regex_comp = re.compile(psuffix_regex)
598
599        # compile regex, can be specific by package or generic regex
600        pn_regex = d.getVar('UPSTREAM_CHECK_REGEX')
601        if pn_regex:
602            package_custom_regex_comp = re.compile(pn_regex)
603        else:
604            version = self._parse_path(package_regex_comp, package)
605            if version:
606                package_custom_regex_comp = re.compile(
607                    r"(?P<name>%s)(?P<pver>%s)(?P<arch>%s)?[\.-](?P<type>%s)" %
608                    (re.escape(version[0]), pver_regex, parch_regex, psuffix_regex))
609            else:
610                package_custom_regex_comp = None
611
612        return package_custom_regex_comp
613
614    def latest_versionstring(self, ud, d):
615        """
616        Manipulate the URL and try to obtain the latest package version
617
618        sanity check to ensure same name and type.
619        """
620        package = ud.path.split("/")[-1]
621        current_version = ['', d.getVar('PV'), '']
622
623        """possible to have no version in pkg name, such as spectrum-fw"""
624        if not re.search(r"\d+", package):
625            current_version[1] = re.sub('_', '.', current_version[1])
626            current_version[1] = re.sub('-', '.', current_version[1])
627            return (current_version[1], '')
628
629        package_regex = self._init_regexes(package, ud, d)
630        if package_regex is None:
631            bb.warn("latest_versionstring: package %s don't match pattern" % (package))
632            return ('', '')
633        bb.debug(3, "latest_versionstring, regex: %s" % (package_regex.pattern))
634
635        uri = ""
636        regex_uri = d.getVar("UPSTREAM_CHECK_URI")
637        if not regex_uri:
638            path = ud.path.split(package)[0]
639
640            # search for version matches on folders inside the path, like:
641            # "5.7" in http://download.gnome.org/sources/${PN}/5.7/${PN}-${PV}.tar.gz
642            dirver_regex = re.compile(r"(?P<dirver>[^/]*(\d+\.)*\d+([-_]r\d+)*)/")
643            m = dirver_regex.findall(path)
644            if m:
645                pn = d.getVar('PN')
646                dirver = m[-1][0]
647
648                dirver_pn_regex = re.compile(r"%s\d?" % (re.escape(pn)))
649                if not dirver_pn_regex.search(dirver):
650                    return (self._check_latest_version_by_dir(dirver,
651                        package, package_regex, current_version, ud, d), '')
652
653            uri = bb.fetch.encodeurl([ud.type, ud.host, path, ud.user, ud.pswd, {}])
654        else:
655            uri = regex_uri
656
657        return (self._check_latest_version(uri, package, package_regex,
658                current_version, ud, d), '')
659