build-essential: illumos-gate switched to gcc-14 as shadow compiler
[oi-userland.git] / tools / userland-component
blob3d1afa6a287203a5a507d2c72c159b870bdfae0e
1 #!/usr/bin/python3.9
4 # This file and its contents are supplied under the terms of the
5 # Common Development and Distribution License ("CDDL"), version 1.0.
6 # You may only use this file in accordance with the terms of version
7 # 1.0 of the CDDL.
9 # A full copy of the text of the CDDL should have accompanied this
10 # source.  A copy of the CDDL is also available via the Internet at
11 # http://www.illumos.org/license/CDDL.
15 # Copyright 2021 Aurelien Larcher
18 import argparse
19 import os
20 import re
21 import sys
22 import json
24 from bass.component import Component
25 from bass.makefiles import Item
26 from bass.makefiles import Keywords
27 from bass.makefiles import Makefile as MK
29 # Refactoring rules
30 #-----------------------------------------------------------------------------
31 # They should be called in-order to avoid unsatisfied assumptions.
32 def format_component(path, verbose):
33     mk = MK(path)
34     kw = Keywords()
35     refactor000(mk)
36     refactor001(mk)
37     refactor002(mk)
38     mk.write()
41 #-----------------------------------------------------------------------------
42 # 000:  Use WS_* variables instead $(WS_TOP)/* 
43 #       If $(WS_TOP)/make-rules is found in an include then replace with the
44 #       variable $(WS_RULES). Do the same for other variables.
45 def refactor000(mk):
46     for i in iter(mk.includes):
47         r = re.match(r"^\$\(WS_TOP\)\/(.*)\/(.*).mk", i.value())
48         if r is not None:
49             subdir = r.group(1)
50             mkfile = r.group(2)
51             print("000: Fix include " + i.value())
52             i.set_value(os.path.join(MK.directory_variable(subdir), mkfile+".mk"))
53             mk.contents[i.line()] = i.include_line()
54     mk.update()
57 #-----------------------------------------------------------------------------
58 # 001:  Use common.mk
59 #       If common.mk is not included then:
60 #           1. infer the build system and set the BUILD_STYLE.
61 #           2. set the BUILD_BITS from the existing targets.
62 #           3. erase default target and keep the custom ones.
63 #           4. fix known target typos
64 def refactor001(mk):
65     kw = Keywords()
66     if mk.has_variable('BUILD_STYLE') or mk.has_mk_include('common'):
67         return
68     # Build style
69     build_style = None
70     for i in iter(mk.includes):
71         r = re.match(r"^\$\(WS_MAKE_RULES\)/(.*).mk$", i.value())
72         if r is not None:
73             build_style = r.group(1) if r.group(1) in kw.variables['BUILD_STYLE'] else None
74             if build_style is not None:
75                 mk.set_variable('BUILD_STYLE', build_style)
76                 break
77     if build_style is None:
78         raise ValueError("Variable BUILD_STYLE cannot be defined")
79     else:
80         print("001: Setting build style to '" + build_style + "'")
81     build_style = mk.variable('BUILD_STYLE').value()
82     # Build bits
83     mk_bits = mk.run("print-value-MK_BITS")[0]
84     if mk_bits not in kw.variables["MK_BITS"]:
85         raise ValueError("Variable MK_BITS cannot be defined")
86     else:
87         print("001: Setting make bits to '" + mk_bits + "'")
88     # Check targets
89     new_mk_bits = None
90     new_targets = {}
91     for t, u in iter(mk.targets.items()):
92         # We do not know how to handle target with defined steps yet
93         if len(u.str) > 1:
94             continue
95         # Amend typos
96         if t == 'test' and u.value() == MK.value('NO_TEST'): 
97             print("001: Fix typo $(NO_TEST) -> $(NO_TESTS)")
98             u.set_value(MK.value('NO_TESTS'))
99         # Process target
100         found = False
101         for v in kw.targets[t]:
102             v = MK.value(v.replace(MK.value("MK_BITS"), mk_bits))
103             # If the target dependency is one of the default values
104             if u.value() == v:
105                 found = True
106                 w = MK.target_value(t, mk_bits)
107                 #print(w)
108                 if v == w:
109                     print("001: Use default target '"+t+"'")
110                     u.str = None 
111                 else:
112                     print("001: Define target '"+t+"': "+u.value())
113                     new_targets[t] = u
114                 break
115         if not found:
116             # Some Python/Perl makefiles actually use NO_ARCH target with MK_BITS=32, or BITS was not set
117             if mk_bits == '32' or mk_bits == '64':
118                 ok_bits = ( 'NO_ARCH', '64', '32_and_64', '64_and_32' )
119                 for b in ok_bits:
120                     if u.value() == MK.target_value(t, b):
121                         if not new_mk_bits:
122                             new_mk_bits = b
123                         elif b != new_mk_bits:
124                             raise ValueError("001: Inconsistent target '"+t+"': "+u.value())
125                         u.str = None
126                         break
127             else:
128                 raise ValueError("001: Unknown target '"+t+"' bitness: "+u.value())
129     if new_mk_bits:
130         print("001: Changing make bits from "+mk_bits+" to '"+new_mk_bits+"'")
131         mk_bits = new_mk_bits
132     # Collect items
133     rem_lines = set()
134     rem_includes = [ MK.makefile_path("prep"), MK.makefile_path("ips")]
135     new_includes = []
136     include_shared_mk = None
137     include_common_mk = None
138     for i in iter(mk.includes): 
139         if i.value() not in rem_includes:
140             if i.value() == MK.makefile_path(build_style):
141                 i.set_value(MK.makefile_path("common"))
142                 include_common_mk = i
143             elif re.match(r".*/shared-macros.mk$", i.value()):
144                 include_shared_mk = i
145             new_includes.append(i)
146         else:
147             rem_lines.add(i.line())
148     mk.includes = new_includes
149     if include_common_mk is None:
150         raise ValueError("Include directive of common.mk not found")
151     if include_shared_mk is None:
152         raise ValueError("Include directive of shared-macros.mk not found")
153     # Add lines to skip for default targets 
154     for u in mk.targets.values():
155         if u.str is None:
156             rem_lines.add(u.line())
157     # Update content 
158     contents = mk.contents[0:include_shared_mk.line()]
159     # Add build macros
160     contents.append(Keywords.assignment('BUILD_STYLE', build_style))
161     contents.append(Keywords.assignment('BUILD_BITS', mk_bits))
162     # Write metadata lines 
163     for idx, line in enumerate(mk.contents[include_shared_mk.line():include_common_mk.line()]):
164         if (include_shared_mk.line() + idx) in rem_lines:
165             continue
166         contents.append(line)
167     # Write new targets
168     for t  in ["build", "install", "test"]:
169         if t in new_targets.keys():
170             contents.append(Keywords.target_variable_assignment(t, new_targets[t].value()))
171             rem_lines.add(new_targets[t].line())
172     # Add common include
173     contents.append(include_common_mk.include_line())
174     # Write lines after common.mk 
175     for idx, line in enumerate(mk.contents[include_common_mk.line()+1:]):
176         if (include_common_mk.line()+1+idx) in rem_lines:
177             continue
178         contents.append(line)
179     mk.update(contents)
182 #-----------------------------------------------------------------------------
183 # 002:  Indent COMPONENT_ variables
184 def refactor002(mk):
185     for k,i in iter(mk.variables.items()):
186         if re.match("^COMPONENT_", k):
187             idx = i.line()
188             lines = i.variable_assignment(k)
189             for i in range(0, i.length()):
190                 mk.contents[idx + i] = lines[i] 
191     mk.update()
194 # Update rules
195 #-----------------------------------------------------------------------------
196 # U000: Update to default OpenSSL
197 #       If openssl is a dependency and the openssl package version is not set
198 #           1. update the dependency to the next openssl X.Y
199 #           2. add macros USE_OPENSSLXY to the makefile 
200 def update000(mk):
201     curr_version = '1.0'
202     next_version = '1.1'
203     curr_macro = 'USE_OPENSSL'+curr_version.replace('.','')
204     next_macro = 'USE_OPENSSL'+next_version.replace('.','')
205     curr_openssl_pkg = 'library/security/openssl'
206     next_openssl_pkg = 'library/security/openssl-11'
207     reqs = mk.required_packages()
208     has_openssl_deps=False
209     for p in reqs.split():
210         if p == curr_openssl_pkg:
211             has_openssl_deps=True
212     if not has_openssl_deps:
213         return
214     # Check whether current version is enforced
215     for line in iter(mk.contents):
216         if re.match("^"+curr_macro+"[\s]*=[\s]*yes", line):
217             return
218     print("U000: update to next openssl")
219     # Replace dependency
220     for idx, line in enumerate(mk.contents):
221         if re.match(r"REQUIRED_PACKAGES(.*)"+curr_openssl_pkg+"[\s]*$", line):
222             mk.contents[idx] = line.replace(curr_openssl_pkg, next_openssl_pkg)
223             break
224     # Add macro before shared-macros
225     include_shared_macros_mk = mk.get_mk_include('shared-macros')
226     if not include_shared_macros_mk:
227         raise ValueError('include shared_macros.mk not found')
228     mk.set_variable(next_macro, 'yes', include_shared_macros_mk.line())
229     mk.update()
232 #-----------------------------------------------------------------------------
233 # Update component makefile for revision or version bump 
234 def update_component(path, version, verbose):
235     format_component(path, verbose)
236     # Nothing to bump, just update the Makefile to current format
237     if version is None:
238         return
239     mk = MK(path)
240     # Apply default update rules
241     update000(mk)
242     # Check current version
243     if not mk.has_variable('COMPONENT_VERSION'):
244         raise ValueError('COMPONENT_VERSION not found')
245     newvers = str(version) 
246     current = mk.variable('COMPONENT_VERSION').value()
247     version_has_changed = False
248     # Bump revision only
249     if newvers == '0' or newvers == current:
250         print("Bump COMPONENT_REVISION")
251         if mk.has_variable('COMPONENT_REVISION'):
252             try:
253                 component_revision = int(mk.variable('COMPONENT_REVISION').value())
254             except ValueError:
255                 print('COMPONENT_REVISION field malformed: {}'.format(component_revision))
256             # Change value
257             mk.set_variable('COMPONENT_REVISION', str(component_revision+1))
258         else:
259             # Add value set to 1 after COMPONENT_VERSION
260             mk.set_variable('COMPONENT_REVISION', str(1), line=mk.variable('COMPONENT_VERSION').line()+1)
261     # Update to given version and remove revision
262     else:
263         if newvers == 'latest':
264             if mk.build_style() == 'setup.py':
265                 print("Trying to get latest version from PyPI")
266                 js = mk.get_pypi_data()
267                 try:
268                     newvers = js['info']['version']
269                 except KeyError:
270                     print("Unable to find version")
271                     return None
272         print("Bump COMPONENT_VERSION to " + newvers)
273         version_has_changed = True
274         mk.set_variable('COMPONENT_VERSION', newvers)
275         if mk.has_variable('COMPONENT_REVISION'):
276             mk.remove_variable('COMPONENT_REVISION')
277     # Update makefile
278     mk.write()
280     if not version_has_changed:
281         return
283     # Try to update archive checksum
284     if mk.uses_pypi():
285         print("Trying to get checksum from PyPI")
286         js = mk.get_pypi_data()
287         try:
288               verblock = js['releases'][newvers]
289         except KeyError:
290             print("Unknown version '%s'" % newvers)
291             return None
292         # Index 0 is for the pypi package and index 1 for the source archive
293         sha256 = verblock[1]['digests']['sha256']
294         print("Found: "+str(sha256))
295         mk.set_archive_hash(sha256)
296         # Update makefile
297         mk.write()
300 def main():
301     parser = argparse.ArgumentParser()
302     parser.add_argument('--path', default='components',
303                         help='Directory holding components')
304     parser.add_argument('--bump', nargs='?', default=None, const=0,
305                         help='Bump component to given version')
306     parser.add_argument('-v', '--verbose', action='store_true',
307                         default=False, help='Verbose output')
308     args = parser.parse_args()
310     path = args.path
311     version = args.bump
312     verbose = args.verbose
314     update_component(path=path, version=version, verbose=verbose)
317 if __name__ == '__main__':
318     main()