refine strategy to spot ip address, keep on calling guest_ipv4
[build.git] / pkgs.py
1 #!/usr/bin/env python3
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 # pylint: disable=c0111
30
31
32 import sys
33 from sys import stderr
34 from optparse import OptionParser
35 import re
36
37 default_arch = 'x86_64'
38 known_archs = ['i386', 'i686', 'x86_64']
39 default_fcdistro = 'f39'
40 known_fcdistros = [
41     'centos5', 'centos6',
42     # oldies but we have references to that in the pkgs files
43     'f8', 'f10', 'f12', 'f14', 'f16', 'f18',
44     'f20', 'f21', 'f22', 'f23', 'f24', 'f25', 'f27',
45     # these ones are still relevant;
46     # f32 is mentioned to be able to use create-vms with that distro
47     # as we're running into issues to build a minimal f33 from a f29 host
48     'f29', 'f31', 'f32', 'f33', 'f35', 'f37', 'f39',
49     # scientific linux
50     'sl6',
51     # debians
52     'wheezy', 'jessie',
53     # ubuntus
54     'trusty',  # 14.04 LTS
55     'xenial',  # 16.04 LTS
56     'bionic',  # 18.04 LTS
57     'focal',   # 20.04 LTS
58     'jammy',   # 22.04 LTS
59 ]
60 default_pldistro = 'onelab'
61
62 known_keywords = [
63     'group', 'groupname', 'groupdesc',
64      'package', 'pip', 'gem',
65     'nodeyumexclude', 'plcyumexclude', 'yumexclude',
66     'precious', 'junk', 'mirror',
67 ]
68
69
70 m_fcdistro_cutter = re.compile('([a-z]+)([0-9]+)')
71 re_ident = '[a-z]+'
72
73 class PkgsParser:
74
75     def __init__(self, arch, fcdistro, pldistro, keyword, inputs, options):
76         self.arch = arch
77         self.fcdistro = fcdistro
78         self.pldistro = pldistro
79         self.keyword = keyword
80         self.inputs = inputs
81         # for verbose, new_line, and the like
82         self.options = options
83         ok = False
84         for known in known_fcdistros:
85             if fcdistro == known:
86                 try:
87                     (distro, version) = m_fcdistro_cutter.match(fcdistro).groups()
88                 # debian-like names can't use numbering
89                 except:
90                     distro = fcdistro
91                     version = 0
92                 ok = True
93         if ok:
94             self.distro = distro
95             self.version = int(version)
96         else:
97             print('unrecognized fcdistro', fcdistro, file=stderr)
98             sys.exit(1)
99
100     # qualifier is either '>=','<=', or '='
101     def match(self, qualifier, version):
102         if qualifier == '=':
103             return self.version == version
104         elif qualifier == '>=':
105             return self.version >= version
106         elif qualifier == '<=':
107             return self.version <= version
108         else:
109             raise Exception(
110                 'Internal error - unexpected qualifier {}'.format(qualifier))
111
112     m_comment = re.compile(r'\A\s*#')
113     m_blank = re.compile(r'\A\s*\Z')
114
115     m_ident = re.compile(r'\A'+re_ident+r'\Z')
116     re_qualified = r'\s*'
117     re_qualified += r'(?P<plus_minus>[+-]?)'
118     re_qualified += r'\s*'
119     re_qualified += r'(?P<keyword>{re_ident})'.format(re_ident=re_ident)
120     re_qualified += r'\s*'
121     re_qualified += r'(?P<qualifier>>=|<=|=)'
122     re_qualified += r'\s*'
123     re_qualified += r'(?P<fcdistro>{re_ident}[0-9]+)'.format(re_ident=re_ident)
124     re_qualified += r'\s*'
125     m_qualified = re.compile(r'\A{re_qualified}\Z'.format(re_qualified=re_qualified))
126
127     re_old = '[a-z]+[+-][a-z]+[0-9]+'
128     m_old = re.compile(r'\A{}\Z'.format(re_old))
129
130     # returns a tuple (included, excluded)
131     def parse(self, filename):
132         ok = True
133         included = []
134         excluded = []
135         try:
136             with open(filename) as feed:
137                 for lineno, line in enumerate(feed, 1):
138                     line = line.strip()
139                     if self.m_comment.match(line) or self.m_blank.match(line):
140                         continue
141                     try:
142                         lefts, rights = line.split(':', 1)
143                         for left in lefts.split():
144                             ########## single ident
145                             if self.m_ident.match(left):
146                                 if left not in known_keywords:
147                                     raise Exception("Unknown keyword {left}".format(**locals()))
148                                 elif left == self.keyword:
149                                     included += rights.split()
150                             else:
151                                 m = self.m_qualified.match(left)
152                                 if m:
153                                     (plus_minus, kw, qual, fcdistro) = m.groups()
154                                     if kw not in known_keywords:
155                                         raise Exception("Unknown keyword in {left}".format(**locals()))
156                                     if fcdistro not in known_fcdistros:
157                                         raise Exception('Unknown fcdistro {fcdistro}'.format(**locals()))
158                                     # skip if another keyword
159                                     if kw != self.keyword: continue
160                                     # does this fcdistro match ?
161                                     (distro, version) = m_fcdistro_cutter.match(fcdistro).groups()
162                                     version = int (version)
163                                     # skip if another distro family
164                                     if distro != self.distro: continue
165                                     # skip if the qualifier does not fit
166                                     if not self.match (qual, version):
167                                         if self.options.verbose:
168                                             print('{filename}:{lineno}:qualifer {left} does not apply'
169                                                   .format(**locals()), file=stderr)
170                                         continue
171                                     # we're in, let's add (default) or remove (if plus_minus is minus)
172                                     if plus_minus == '-':
173                                         if self.options.verbose:
174                                             print('{filename}:{lineno}: from {left}, excluding {rights}'
175                                                   .format(**locals()), file=stderr)
176                                         excluded += rights.split()
177                                     else:
178                                         if self.options.verbose:
179                                             print('{filename}:{lineno}: from {left}, including {rights}'\
180                                                   .format(**locals()), file=stderr)
181                                         included += rights.split()
182                                 elif self.m_old.match(left):
183                                     raise Exception('Old-fashioned syntax not supported anymore {left}'.\
184                                                     format(**locals()))
185                                 else:
186                                     raise Exception('error in left expression {left}'.format(**locals()))
187
188                     except Exception as e:
189                         ok = False
190                         print("{filename}:{lineno}:syntax error: {e}".format(**locals()), file=stderr)
191         except Exception as exc:
192             ok = False
193             print('Could not parse file', filename, exc, file=stderr)
194         return (ok, included, excluded)
195
196     def run (self):
197         ok = True
198         included = []
199         excluded = []
200         for input in self.inputs:
201             (o, i, e) = self.parse (input)
202             included += i
203             excluded += e
204             ok = ok and o
205         # avoid set operations that would not preserve order
206         results = [x for x in included if x not in excluded]
207
208         results = [x.replace('@arch@', self.arch)
209                    .replace('@fcdistro@', self.fcdistro)
210                    .replace('@pldistro@', self.pldistro) for x in results]
211         if self.options.sort_results:
212             results.sort()
213         # default is space-separated
214         if not self.options.new_line:
215             print(" ".join(results))
216         # but for tests results are printed each on a line
217         else:
218             for result in results:
219                 print(result)
220         return ok
221
222 def main ():
223     usage = "Usage: %prog [options] keyword input[...]"
224     parser = OptionParser(usage=usage)
225     parser.add_option(
226         '-a', '--arch', dest='arch', action='store', default=default_arch,
227         help='target arch, e.g. i386 or x86_64, default={}'.format(default_arch))
228     parser.add_option(
229         '-f', '--fcdistro', dest='fcdistro', action='store', default=default_fcdistro,
230         help='fcdistro, e.g. f12 or centos5')
231     parser.add_option(
232         '-d', '--pldistro', dest='pldistro', action='store', default=default_pldistro,
233         help='pldistro, e.g. onelab or planetlab')
234     parser.add_option(
235         '-v', '--verbose', dest='verbose', action='store_true', default=False,
236         help='verbose when using qualifiers')
237     parser.add_option(
238         '-n', '--new-line', dest='new_line', action='store_true', default=False,
239         help='print outputs separated with newlines rather than with a space')
240     parser.add_option(
241         '-u', '--no-sort', dest='sort_results', default=True, action='store_false',
242         help='keep results in the same order as in the inputs')
243     (options, args) = parser.parse_args()
244
245     if len(args) <= 1:
246         parser.print_help(file=stderr)
247         sys.exit(1)
248     keyword = args[0]
249     inputs = args[1:]
250     if options.arch not in known_archs:
251         print('Unsupported arch', options.arch, file=stderr)
252         parser.print_help(file=stderr)
253         sys.exit(1)
254     if options.arch == 'i686':
255         options.arch = 'i386'
256     if options.fcdistro not in known_fcdistros:
257         print('Unsupported fcdistro', options.fcdistro, file=stderr)
258         parser.print_help(file=stderr)
259         sys.exit(1)
260
261     pkgs = PkgsParser(options.arch, options.fcdistro, options.pldistro,
262                       keyword, inputs, options)
263
264     if pkgs.run():
265         sys.exit(0)
266     else:
267         sys.exit(1)
268
269 if __name__ == '__main__':
270     if main():
271         sys.exit(0)
272     else:
273         sys.exit(1)