pkgs.py got a little cleanup; hopefully python3 ready
[build.git] / pkgs.py
1 #!/usr/bin/python
2 #
3 # This is a replacement for the formerly bash-written function pl_parsePkgs () 
4
5 # Usage: $0  [-a arch] default_arch keyword fcdistro pldistro pkgs-file[..s]
6 # default_arch is $pl_DISTRO_ARCH, but can be overridden
7 #
8
9 #################### original language was (in this example, keyword=package)
10 ## to add to all distros
11 # package: p1 p2
12 ## to add in one distro
13 # package+f12: p1 p2
14 ## to remove in one distro
15 # package-f10: p1 p2
16 #################### replacement language
17 ## add in distro f10
18 # +package=f10: p1 p2
19 # or simply
20 # package=f10: p1 p2
21 ## add in fedora distros starting with f10
22 # +package>=f10: p1 p2
23 # or simply
24 # package>=f10: p1 p2
25 ## ditto but remove instead
26 # -package=centos5: p1 p2
27 # -package<=centos5: p1 p2
28
29 from __future__ import print_function
30
31 import sys
32 from sys import stderr
33 from optparse import OptionParser
34 import re
35
36 default_arch='x86_64'
37 known_arch = ['i386', 'i686', 'x86_64']
38 default_fcdistro = 'f22'
39 known_fcdistros = [
40     'centos5', 'centos6',
41     'f14', 'f18', 'f20', 'f21', 'f22',
42     'sl6', 
43     # debians
44     'wheezy','jessie',
45     # ubuntus
46     'precise', # 12.04 LTS
47     'trusty',  # 14.04 LTS
48     'utopic',  # 14.10
49     'vivid',   # 15.04
50     'wily',    # 15.10
51 ]
52 default_pldistro='onelab'
53
54 known_keywords = [
55     'group', 'groupname', 'groupdesc', 
56      'package', 'pip', 'gem', 
57     'nodeyumexclude', 'plcyumexclude', 'yumexclude',
58     'precious', 'junk', 'mirror',
59 ]
60
61
62 m_fcdistro_cutter = re.compile('([a-z]+)([0-9]+)')
63 re_ident = '[a-z]+'
64
65 class PkgsParser:
66
67     def __init__(self, arch, fcdistro, pldistro, keyword, inputs, options):
68         self.arch = arch
69         self.fcdistro = fcdistro
70         self.pldistro = pldistro
71         self.keyword = keyword
72         self.inputs = inputs
73         # for verbose, new_line, and the like
74         self.options = options
75         ok = False
76         for known in known_fcdistros:
77             if fcdistro == known:
78                 try:
79                     (distro, version) = m_fcdistro_cutter.match(fcdistro).groups()
80                 # debian-like names can't use numbering
81                 except:
82                     distro = fcdistro
83                     version = 0
84                 ok = True
85         if ok:
86             self.distro = distro
87             self.version = int(version)
88         else:
89             print('unrecognized fcdistro', fcdistro, file=stderr)
90             sys.exit(1)
91
92     # qualifier is either '>=','<=', or '='
93     def match (self, qualifier, version):
94         if qualifier == '=':
95             return self.version == version
96         elif qualifier == '>=':
97             return self.version >= version
98         elif qualifier == '<=':
99             return self.version <= version
100         else:
101             raise Exception('Internal error - unexpected qualifier {qualifier}'.format(**locals()))
102
103     m_comment = re.compile('\A\s*#')
104     m_blank = re.compile('\A\s*\Z')
105
106     m_ident = re.compile('\A'+re_ident+'\Z')
107     re_qualified = '\s*'
108     re_qualified += '(?P<plus_minus>[+-]?)'
109     re_qualified += '\s*'
110     re_qualified += '(?P<keyword>{re_ident})'.format(re_ident=re_ident)
111     re_qualified += '\s*'
112     re_qualified += '(?P<qualifier>>=|<=|=)'
113     re_qualified += '\s*'
114     re_qualified += '(?P<fcdistro>{re_ident}[0-9]+)'.format(re_ident=re_ident)
115     re_qualified += '\s*'
116     m_qualified = re.compile('\A{re_qualified}\Z'.format(**locals()))
117
118     re_old = '[a-z]+[+-][a-z]+[0-9]+'
119     m_old = re.compile('\A{re_old}\Z'.format(**locals()))
120     
121     # returns a tuple (included, excluded)
122     def parse(self, filename):
123         ok = True
124         included = []
125         excluded = []
126         lineno = 0
127         try:
128             for line in file(filename).readlines():
129                 lineno += 1
130                 line = line.strip()
131                 if self.m_comment.match(line) or self.m_blank.match(line):
132                     continue
133                 try:
134                     lefts, rights = line.split(':', 1)
135                     for left in lefts.split():
136                         ########## single ident
137                         if self.m_ident.match(left):
138                             if left not in known_keywords:
139                                 raise Exception("Unknown keyword {left}".format(**locals()))
140                             elif left == self.keyword:
141                                 included += rights.split()
142                         else:
143                             m = self.m_qualified.match(left)
144                             if m:
145                                 (plus_minus, kw, qual, fcdistro) = m.groups()
146                                 if kw not in known_keywords:
147                                     raise Exception("Unknown keyword in {left}".format(**locals()))
148                                 if fcdistro not in known_fcdistros:
149                                     raise Exception('Unknown fcdistro {fcdistro}'.format(**locals()))
150                                 # skip if another keyword
151                                 if kw != self.keyword: continue
152                                 # does this fcdistro match ?
153                                 (distro, version) = m_fcdistro_cutter.match(fcdistro).groups()
154                                 version = int (version)
155                                 # skip if another distro family
156                                 if distro != self.distro: continue
157                                 # skip if the qualifier does not fit
158                                 if not self.match (qual, version): 
159                                     if self.options.verbose:
160                                         print('{filename}:{lineno}:qualifer {left} does not apply'
161                                               .format(**locals()), file=stderr)
162                                     continue
163                                 # we're in, let's add (default) or remove (if plus_minus is minus)
164                                 if plus_minus == '-':
165                                     if self.options.verbose:
166                                         print('{filename}:{lineno}: from {left}, excluding {rights}'
167                                               .format(**locals()), file=stderr)
168                                     excluded += rights.split()
169                                 else:
170                                     if self.options.verbose:
171                                         print('{filename}:{lineno}: from {left}, including {rights}'\
172                                               .format(**locals()), file=stderr)
173                                     included += rights.split()
174                             elif self.m_old.match(left):
175                                 raise Exception('Old-fashioned syntax not supported anymore {left}'.\
176                                                 format(**locals()))
177                             else:
178                                 raise Exception('error in left expression {left}'.format(**locals()))
179                                 
180                 except Exception as e:
181                     ok = False
182                     print("{filename}:{lineno}:syntax error: {e}".format(**locals()), file=stderr)
183         except Exception as e:
184             ok = False
185             print('Could not parse file', filename, e, file=stderr)
186         return (ok, included, excluded)
187
188     def run (self):
189         ok = True
190         included = []
191         excluded = []
192         for input in self.inputs:
193             (o, i, e) = self.parse (input)
194             included += i
195             excluded += e
196             ok = ok and o
197         # avoid set operations that would not preserve order
198         results = [ x for x in included if x not in excluded ]
199         
200         results = [ x.replace('@arch@', self.arch).\
201                         replace('@fcdistro@', self.fcdistro).\
202                         replace('@pldistro@', self.pldistro) for x in results]
203         if self.options.sort_results:
204             results.sort()
205         # default is space-separated
206         if not self.options.new_line:
207             print(" ".join(results))
208         # but for tests results are printed each on a line
209         else:
210             for result in results:
211                 print(result)
212         return ok
213
214 def main ():
215     usage = "Usage: %prog [options] keyword input[...]"
216     parser = OptionParser (usage=usage)
217     parser.add_option ('-a', '--arch', dest='arch', action='store', default=default_arch,
218                        help='target arch, e.g. i386 or x86_64, default={}'.format(default_arch))
219     parser.add_option ('-f', '--fcdistro', dest='fcdistro', action='store', default=default_fcdistro,
220                        help='fcdistro, e.g. f12 or centos5')
221     parser.add_option ('-d', '--pldistro', dest='pldistro', action='store', default=default_pldistro,
222                        help='pldistro, e.g. onelab or planetlab')
223     parser.add_option ('-v', '--verbose', dest='verbose', action='store_true', default=False,
224                        help='verbose when using qualifiers')
225     parser.add_option ('-n', '--new-line', dest='new_line', action='store_true', default=False,
226                        help='print outputs separated with newlines rather than with a space')
227     parser.add_option ('-u', '--no-sort', dest='sort_results', default=True, action='store_false',
228                        help='keep results in the same order as in the inputs')
229     (options, args) = parser.parse_args()
230     
231     if len(args) <= 1 :
232         parser.print_help(file=stderr)
233         sys.exit(1)
234     keyword = args[0]
235     inputs = args[1:]
236     if not options.arch in known_arch:
237         print('Unsupported arch', options.arch, file=stderr)
238         parser.print_help(file=stderr)
239         sys.exit(1)
240     if options.arch == 'i686':
241         options.arch='i386'
242     if not options.fcdistro in known_fcdistros:
243         print('Unsupported fcdistro', options.fcdistro, file=stderr)
244         parser.print_help(file=stderr)
245         sys.exit(1)
246
247     pkgs = PkgsParser(options.arch, options.fcdistro, options.pldistro, keyword, inputs, options)
248
249     if pkgs.run():
250         sys.exit(0)
251     else:
252         sys.exit(1)
253
254 if __name__ == '__main__':
255     if main():
256         sys.exit(0)
257     else:
258         sys.exit(1)