1from datetime import datetime
2import os
3from os import path
4import re
5import shutil
6import sys
7from urllib2 import urlopen
8
9from release.paths import makeCandidatesDir
10
11import logging
12log = logging.getLogger(__name__)
13
14# If version has two parts with no trailing specifiers like "rc", we
15# consider it a "final" release for which we only create a _RELEASE tag.
16FINAL_RELEASE_REGEX = "^\d+\.\d+$"
17
18
19class ConfigError(Exception):
20    pass
21
22
23def getBuildID(platform, product, version, buildNumber, nightlyDir='nightly',
24               server='stage.mozilla.org'):
25    infoTxt = makeCandidatesDir(product, version, buildNumber, nightlyDir,
26                                protocol='http', server=server) + \
27        '%s_info.txt' % platform
28    try:
29        buildInfo = urlopen(infoTxt).read()
30    except:
31        log.error("Failed to retrieve %s" % infoTxt)
32        raise
33
34    for line in buildInfo.splitlines():
35        key, value = line.rstrip().split('=', 1)
36        if key == 'buildID':
37            return value
38
39
40def findOldBuildIDs(product, version, buildNumber, platforms,
41                    nightlyDir='nightly', server='stage.mozilla.org'):
42    ids = {}
43    if buildNumber <= 1:
44        return ids
45    for n in range(1, buildNumber):
46        for platform in platforms:
47            if platform not in ids:
48                ids[platform] = []
49            try:
50                id = getBuildID(platform, product, version, n, nightlyDir,
51                                server)
52                ids[platform].append(id)
53            except Exception, e:
54                log.error("Hit exception: %s" % e)
55    return ids
56
57
58def getReleaseConfigName(product, branch, version=None, staging=False):
59    # XXX: Horrible hack for bug 842741. Because Thunderbird release
60    # and esr both build out of esr17 repositories we'll bump the wrong
61    # config for release without this.
62    if product == 'thunderbird' and 'esr17' in branch and version and 'esr' not in version:
63        cfg = 'release-thunderbird-comm-release.py'
64    else:
65        cfg = 'release-%s-%s.py' % (product, branch)
66    if staging:
67        cfg = 'staging_%s' % cfg
68    return cfg
69
70
71def readReleaseConfig(configfile, required=[]):
72    return readConfig(configfile, keys=['releaseConfig'], required=required)
73
74
75def readBranchConfig(dir, localconfig, branch, required=[]):
76    shutil.copy(localconfig, path.join(dir, "localconfig.py"))
77    oldcwd = os.getcwd()
78    os.chdir(dir)
79    sys.path.append(".")
80    try:
81        return readConfig("config.py", keys=['BRANCHES', branch],
82                          required=required)
83    finally:
84        os.chdir(oldcwd)
85        sys.path.remove(".")
86
87
88def readConfig(configfile, keys=[], required=[]):
89    c = {}
90    execfile(configfile, c)
91    for k in keys:
92        c = c[k]
93    items = c.keys()
94    err = False
95    for key in required:
96        if key not in items:
97            err = True
98            log.error("Required item `%s' missing from %s" % (key, c))
99    if err:
100        raise ConfigError("Missing at least one item in config, see above")
101    return c
102
103
104def isFinalRelease(version):
105    return bool(re.match(FINAL_RELEASE_REGEX, version))
106
107
108def getBaseTag(product, version):
109    product = product.upper()
110    version = version.replace('.', '_')
111    return '%s_%s' % (product, version)
112
113
114def getTags(baseTag, buildNumber, buildTag=True):
115    t = ['%s_RELEASE' % baseTag]
116    if buildTag:
117        t.append('%s_BUILD%d' % (baseTag, int(buildNumber)))
118    return t
119
120
121def getRuntimeTag(tag):
122    return "%s_RUNTIME" % tag
123
124
125def getReleaseTag(tag):
126    return "%s_RELEASE" % tag
127
128
129def generateRelbranchName(version, prefix='GECKO'):
130    return '%s%s_%s_RELBRANCH' % (
131        prefix, version.replace('.', ''),
132        datetime.now().strftime('%Y%m%d%H'))
133
134
135def getReleaseName(product, version, buildNumber):
136    return '%s-%s-build%s' % (product.title(), version, str(buildNumber))
137
138
139def getRepoMatchingBranch(branch, sourceRepositories):
140    for sr in sourceRepositories.values():
141        if branch in sr['path']:
142            return sr
143    return None
144
145
146def fileInfo(filepath, product):
147    """Extract information about a release file.  Returns a dictionary with the
148    following keys set:
149    'product', 'version', 'locale', 'platform', 'contents', 'format',
150    'pathstyle'
151
152    'contents' is one of 'complete', 'installer'
153    'format' is one of 'mar' or 'exe'
154    'pathstyle' is either 'short' or 'long', and refers to if files are all in
155        one directory, with the locale as part of the filename ('short' paths,
156        firefox 3.0 style filenames), or if the locale names are part of the
157        directory structure, but not the file name itself ('long' paths,
158        firefox 3.5+ style filenames)
159    """
160    try:
161        # Mozilla 1.9.0 style (aka 'short') paths
162        # e.g. firefox-3.0.12.en-US.win32.complete.mar
163        filename = os.path.basename(filepath)
164        m = re.match("^(%s)-([0-9.]+)\.([-a-zA-Z]+)\.(win32)\.(complete|installer)\.(mar|exe)$" % product, filename)
165        if not m:
166            raise ValueError("Could not parse: %s" % filename)
167        return {'product': m.group(1),
168                'version': m.group(2),
169                'locale': m.group(3),
170                'platform': m.group(4),
171                'contents': m.group(5),
172                'format': m.group(6),
173                'pathstyle': 'short',
174                'leading_path': '',
175                }
176    except:
177        # Mozilla 1.9.1 and on style (aka 'long') paths
178        # e.g. update/win32/en-US/firefox-3.5.1.complete.mar
179        #      win32/en-US/Firefox Setup 3.5.1.exe
180        ret = {'pathstyle': 'long'}
181        if filepath.endswith('.mar'):
182            ret['format'] = 'mar'
183            m = re.search("update/(win32|linux-i686|linux-x86_64|mac|mac64)/([-a-zA-Z]+)/(%s)-(\d+\.\d+(?:\.\d+)?(?:\w+(?:\d+)?)?)\.(complete)\.mar" % product, filepath)
184            if not m:
185                raise ValueError("Could not parse: %s" % filepath)
186            ret['platform'] = m.group(1)
187            ret['locale'] = m.group(2)
188            ret['product'] = m.group(3)
189            ret['version'] = m.group(4)
190            ret['contents'] = m.group(5)
191            ret['leading_path'] = ''
192        elif filepath.endswith('.exe'):
193            ret['format'] = 'exe'
194            ret['contents'] = 'installer'
195            # EUballot builds use a different enough style of path than others
196            # that we can't catch them in the same regexp
197            if filepath.find('win32-EUballot') != -1:
198                ret['platform'] = 'win32'
199                m = re.search("(win32-EUballot/)([-a-zA-Z]+)/((?i)%s) Setup (\d+\.\d+(?:\.\d+)?(?:\w+\d+)?(?:\ \w+\ \d+)?)\.exe" % product, filepath)
200                if not m:
201                    raise ValueError("Could not parse: %s" % filepath)
202                ret['leading_path'] = m.group(1)
203                ret['locale'] = m.group(2)
204                ret['product'] = m.group(3).lower()
205                ret['version'] = m.group(4)
206            else:
207                m = re.search("(partner-repacks/[-a-zA-Z0-9_]+/|)(win32|mac|linux-i686)/([-a-zA-Z]+)/((?i)%s) Setup (\d+\.\d+(?:\.\d+)?(?:\w+(?:\d+)?)?(?:\ \w+\ \d+)?)\.exe" % product, filepath)
208                if not m:
209                    raise ValueError("Could not parse: %s" % filepath)
210                ret['leading_path'] = m.group(1)
211                ret['platform'] = m.group(2)
212                ret['locale'] = m.group(3)
213                ret['product'] = m.group(4).lower()
214                ret['version'] = m.group(5)
215        else:
216            raise ValueError("Unknown filetype for %s" % filepath)
217
218        return ret
219