blob: a54093174e73365301dd9c9943257c30b479c6b5 [file] [log] [blame]
Aaron Kling5f122e12023-08-20 15:32:58 -05001#! /usr/bin/env python3
2
3# Copyright (c) 2020-2021, The Linux Foundation. All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are
7# met:
8# * Redistributions of source code must retain the above copyright
9# notice, this list of conditions and the following disclaimer.
10# * Redistributions in binary form must reproduce the above
11# copyright notice, this list of conditions and the following
12# disclaimer in the documentation and/or other materials provided
13# with the distribution.
14# * Neither the name of The Linux Foundation nor the names of its
15# contributors may be used to endorse or promote products derived
16# from this software without specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
19# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
20# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
22# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
25# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
28# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30import copy
31import os
32import sys
33import subprocess
34import shutil
35from itertools import product, combinations_with_replacement, chain
36
37def split_array(array, cells):
38 """
39 Helper function for parsing fdtget output
40 """
41 if array is None:
42 return None
43 assert (len(array) % cells) == 0
44 return frozenset(tuple(array[i*cells:(i*cells)+cells]) for i in range(len(array) // cells))
45
46class DeviceTreeInfo(object):
Arian363d93b2022-07-14 18:26:54 +020047 def __init__(self, plat, board, pmic, miboard):
Aaron Kling5f122e12023-08-20 15:32:58 -050048 self.plat_id = plat
49 self.board_id = board
50 self.pmic_id = pmic
Arian363d93b2022-07-14 18:26:54 +020051 self.miboard_id = miboard
Aaron Kling5f122e12023-08-20 15:32:58 -050052
53 def __str__(self):
54 s = ""
55 if self.plat_id is not None:
56 s += " msm-id = <{}>;".format(" ".join(map(str, self.plat_id)))
57 if self.board_id is not None:
58 s += " board-id = <{}>;".format(" ".join(map(str, self.board_id)))
59 if self.pmic_id is not None:
60 s += " pmic-id = <{}>;".format(" ".join(map(str, self.pmic_id)))
Arian363d93b2022-07-14 18:26:54 +020061 if self.miboard_id is not None:
62 s += " miboard-id = <{}>;".format(" ".join(map(str, self.miboard_id)))
Aaron Kling5f122e12023-08-20 15:32:58 -050063 return s.strip()
64
65 def __repr__(self):
66 return "<{} {}>".format(self.__class__.__name__, str(self))
67
68 def has_any_properties(self):
Arian363d93b2022-07-14 18:26:54 +020069 return self.plat_id is not None or self.board_id is not None or self.pmic_id is not None or self.miboard_id is not None
Aaron Kling5f122e12023-08-20 15:32:58 -050070
71 def __sub__(self, other):
72 """
73 This devicetree has plat, board, and pmic id described like this:
74 msm-id = <A>, <B>
75 board-id = <c>, <d>
76 pmic-id = <0, 1>
Arian363d93b2022-07-14 18:26:54 +020077 miboard-id = <e>, <f>
Aaron Kling5f122e12023-08-20 15:32:58 -050078
79 Other has plat, board, pmic are:
80 msm-id = <A>, <B>
81 board-id = <c>
82 pmic-id = <0>
Arian363d93b2022-07-14 18:26:54 +020083 miboard-id = <e>, <f>
Aaron Kling5f122e12023-08-20 15:32:58 -050084
85 (self - other) will split self into a set of devicetrees with different identifers
86 and meets the following requirements:
87 - One of the devicetrees matches the IDs supported by other
88 - The devices which self matches are still supported (through 1 or more extra devicetrees)
89 by creating new devicetrees with different plat/board/pmic IDs
90 """
91 assert self.plat_id is None or isinstance(self.plat_id, (set, frozenset))
92 assert self.board_id is None or isinstance(self.board_id, (set, frozenset))
93 assert self.pmic_id is None or isinstance(self.pmic_id, (set, frozenset))
Arian363d93b2022-07-14 18:26:54 +020094 assert self.miboard_id is None or isinstance(self.miboard_id, (set, frozenset))
Aaron Kling5f122e12023-08-20 15:32:58 -050095 assert other in self
96
97 new_plat = other.plat_id is not None and self.plat_id != other.plat_id
98 new_board = other.board_id is not None and self.board_id != other.board_id
99 new_pmic = other.pmic_id is not None and self.pmic_id != other.pmic_id
Arian363d93b2022-07-14 18:26:54 +0200100 new_miboard = other.miboard_id is not None and self.miboard_id != other.miboard_id
Aaron Kling5f122e12023-08-20 15:32:58 -0500101
102 res = set()
103 # Create the devicetree that matches other exactly
104 s = copy.deepcopy(self)
105 if new_plat:
106 s.plat_id = other.plat_id
107 if new_board:
108 s.board_id = other.board_id
109 if new_pmic:
110 s.pmic_id = other.pmic_id
Arian363d93b2022-07-14 18:26:54 +0200111 if new_miboard:
112 s.miboard_id = other.miboard_id
Aaron Kling5f122e12023-08-20 15:32:58 -0500113 res.add(s)
114
115 # now create the other possibilities by removing any combination of
116 # other's plat, board, and/or pmic. Set logic (unique elemnts) handles
117 # duplicate devicetrees IDs spit out by this loop
Arian363d93b2022-07-14 18:26:54 +0200118 for combo in combinations_with_replacement([True, False], 4):
119 if not any((c and n) for (c, n) in zip(combo, (new_plat, new_board, new_pmic, new_miboard))):
Aaron Kling5f122e12023-08-20 15:32:58 -0500120 continue
121 s = copy.deepcopy(self)
122 if combo[0] and new_plat:
123 s.plat_id -= other.plat_id
124 if combo[1] and new_board:
125 s.board_id -= other.board_id
126 if combo[2] and new_pmic:
127 s.pmic_id -= other.pmic_id
Arian363d93b2022-07-14 18:26:54 +0200128 if combo[3] and new_miboard:
129 s.miboard_id -= other.miboard_id
Aaron Kling5f122e12023-08-20 15:32:58 -0500130 res.add(s)
131 return res
132
133 def __hash__(self):
Arian363d93b2022-07-14 18:26:54 +0200134 # Hash should only consider msm-id/board-id/pmic-id/miboard-id
135 return hash((self.plat_id, self.board_id, self.pmic_id, self.miboard_id))
Aaron Kling5f122e12023-08-20 15:32:58 -0500136
137 def __and__(self, other):
138 s = copy.deepcopy(self)
Arian363d93b2022-07-14 18:26:54 +0200139 for prop in ['plat_id', 'board_id', 'pmic_id', 'miboard_id']:
Aaron Kling5f122e12023-08-20 15:32:58 -0500140 if getattr(self, prop) is None or getattr(other, prop) is None:
141 setattr(s, prop, None)
142 else:
143 setattr(s, prop, getattr(self, prop) & getattr(other, prop))
144 return s
145
146 def _do_equivalent(self, other, property):
147 other_prop = getattr(other, property)
148 self_prop = getattr(self, property)
149 if other_prop is None:
150 return True
151 return self_prop == other_prop
152
153 def __eq__(self, other):
154 """
Arian363d93b2022-07-14 18:26:54 +0200155 Checks whether other plat_id, board_id, pmic_id, miboard_id matches either identically
Aaron Kling5f122e12023-08-20 15:32:58 -0500156 or because the property is none
157 """
158 if not isinstance(other, DeviceTreeInfo):
159 return False
160 if not other.has_any_properties():
161 return False
Arian363d93b2022-07-14 18:26:54 +0200162 return all(map(lambda p: self._do_equivalent(other, p), ['plat_id', 'board_id', 'pmic_id', 'miboard_id']))
Aaron Kling5f122e12023-08-20 15:32:58 -0500163
164
165 def _do_gt(self, other, property):
166 other_prop = getattr(other, property)
167 self_prop = getattr(self, property)
168 # if either property doesn't exist, it could merge in ABL
169 if self_prop is None or other_prop is None:
170 return True
171 # convert to iterable for convenience of below check
172 if isinstance(other_prop, tuple):
173 # if this property is all 0s, ABL coud match with anything on other
174 other_prop = [other_prop]
175 assert hasattr(other_prop, '__iter__')
176 if len(other_prop) == 1 and all(p == 0 for p in next(iter(other_prop))):
177 return True
178 # Test if this property intersects with other property
179 if hasattr(self_prop, '__contains__') and not isinstance(self_prop, tuple):
180 return any(p in self_prop for p in other_prop)
181 else:
182 return self_prop in other_prop
183
184 def __gt__(self, other):
185 """
186 Test if other is a more specific devicetree for self
187
188 This is used to test whether other devicetree applies to self by ABL matching rules
189 """
190 if not isinstance(other, DeviceTreeInfo):
191 return False
192 if not other.has_any_properties():
193 return False
Arian363d93b2022-07-14 18:26:54 +0200194 return all(map(lambda p: self._do_gt(other, p), ['plat_id', 'board_id', 'pmic_id', 'miboard_id']))
Aaron Kling5f122e12023-08-20 15:32:58 -0500195
196
197 def _do_contains(self, other, property):
198 other_prop = getattr(other, property)
199 self_prop = getattr(self, property)
200 # if other property doesn't exist, it can apply here
201 if other_prop is None:
202 return True
203 # if self and other are sets, use "issubset". Handle special case where other set is
204 # empty, in which case they aren't compatible because other_prop should be None
205 if isinstance(self_prop, (set, frozenset)) and isinstance(other_prop, (set, frozenset)):
206 return len(other_prop) > 0 and other_prop.issubset(self_prop)
207 # unpack to one item for convience of below check
208 if hasattr(other_prop, '__len__') and not isinstance(other_prop, tuple):
209 if len(other_prop) == 1:
210 other_prop = next(iter(other_prop))
211 # if this is a single value (tuple), not a list of them, other needs to match exactly
212 if isinstance(self_prop, tuple):
213 return self_prop == other_prop
214 # otherwise, use contains if possible (e.g. list or set of tuples)
215 if hasattr(self_prop, '__contains__'):
216 return other_prop in self_prop
217 return False
218
219 def __contains__(self, other):
220 """
221 Test if other devicetree covers this devicetree. That is, the devices other devicetree
222 matches is a subset of the devices this devicetree matches
223 """
224 if not isinstance(other, DeviceTreeInfo):
225 return False
226 if not other.has_any_properties():
227 return False
Arian363d93b2022-07-14 18:26:54 +0200228 return all(map(lambda p: self._do_contains(other, p), ['plat_id', 'board_id', 'pmic_id', 'miboard_id']))
Aaron Kling5f122e12023-08-20 15:32:58 -0500229
230class DeviceTree(DeviceTreeInfo):
231 def __init__(self, filename):
232 self.filename = filename
233 msm_id = split_array(self.get_prop('/', 'qcom,msm-id', check_output=False), 2)
234 board_id = split_array(self.get_prop('/', 'qcom,board-id', check_output=False), 2)
235 # default pmic-id-size is 4
236 pmic_id_size = self.get_prop('/', 'qcom,pmic-id-size', check_output=False) or 4
237 pmic_id = split_array(self.get_prop('/', 'qcom,pmic-id', check_output=False), pmic_id_size)
Arian363d93b2022-07-14 18:26:54 +0200238 miboard_id = split_array(self.get_prop('/', 'xiaomi,miboard-id', check_output=False), 2)
239 super().__init__(msm_id, board_id, pmic_id, miboard_id)
Aaron Kling5f122e12023-08-20 15:32:58 -0500240
241 if not self.has_any_properties():
242 print('WARNING! {} has no properties and may match with any other devicetree'.format(self.filename))
243
244 def get_prop(self, node, property, prop_type='i', check_output=True):
245 r = subprocess.run(["fdtget", "-t", prop_type, self.filename, node, property],
246 check=check_output, stdout=subprocess.PIPE,
247 stderr=None if check_output else subprocess.DEVNULL)
248 if r.returncode != 0:
249 return None
250 out = r.stdout.decode("utf-8").strip()
251
252 out_array = None
253 if prop_type[-1] == 'i' or prop_type[-1] == 'u':
254 out_array = [int(e) for e in out.split(' ')]
255 if prop_type[-1] == 'x':
256 out_array = [int(e, 16) for e in out.split(' ')]
257 if out_array is not None:
258 if len(out_array) == 0:
259 return None
260 if len(out_array) == 1:
261 return out_array[0]
262 return out_array
263
264 return out
265
266 def __str__(self):
267 return "{} [{}]".format(super().__str__(), self.filename)
268
269class InnerMergedDeviceTree(DeviceTreeInfo):
270 """
271 InnerMergedDeviceTree is an actual representation of a merged devicetree.
272 It has a platform, board, and pmic ID, the "base" devicetree, and some set of add-on
273 devicetrees
274 """
Arian363d93b2022-07-14 18:26:54 +0200275 def __init__(self, filename, plat_id, board_id, pmic_id, miboard_id, techpacks=None):
Aaron Kling5f122e12023-08-20 15:32:58 -0500276 self.base = filename
277 self.techpacks = techpacks or []
Arian363d93b2022-07-14 18:26:54 +0200278 super().__init__(plat_id, board_id, pmic_id, miboard_id)
Aaron Kling5f122e12023-08-20 15:32:58 -0500279
280 def try_add(self, techpack):
281 if not isinstance(techpack, DeviceTree):
282 raise TypeError("{} is not a DeviceTree object".format(repr(techpack)))
283 intersection = techpack & self
284 if intersection in self:
285 self.techpacks.append(intersection)
286 return True
287 return False
288
289 def save(self, name=None, out_dir='.'):
290 if name is None:
291 name = self.get_name()
292
293 out_file = os.path.join(out_dir, name)
294 ext = os.path.splitext(os.path.basename(self.base))[1]
295
296 # This check might fail in future if we get into an edge case
297 # when splitting the base devicetree into multiple merged DTs
298 assert not os.path.exists(out_file)
299
300 if len(self.techpacks) == 0:
301 cmd = ['cp', self.base, out_file]
302 else:
303 if ext == '.dtb':
304 cmd = ['fdtoverlay']
305 else:
306 cmd = ['fdtoverlaymerge']
307 cmd.extend(['-i', self.base])
308 cmd.extend([tp.filename for tp in self.techpacks])
309 cmd.extend(['-o', out_file])
310
311 print(' {}'.format(' '.join(cmd)))
312 subprocess.run(cmd, check=True)
313
314 if self.plat_id:
315 plat_iter = self.plat_id if isinstance(self.plat_id, tuple) else chain.from_iterable(self.plat_id)
316 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,msm-id'] + list(map(str, plat_iter))
317 print(' {}'.format(' '.join(cmd)))
318 subprocess.run(cmd, check=True)
319
320 if self.board_id:
321 board_iter = self.board_id if isinstance(self.board_id, tuple) else chain.from_iterable(self.board_id)
322 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,board-id'] + list(map(str, board_iter))
323 print(' {}'.format(' '.join(cmd)))
324 subprocess.run(cmd, check=True)
325
326 if self.pmic_id:
327 pmic_iter = self.pmic_id if isinstance(self.pmic_id, tuple) else chain.from_iterable(self.pmic_id)
328 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,pmic-id'] + list(map(str, pmic_iter))
329 print(' {}'.format(' '.join(cmd)))
330 subprocess.run(cmd, check=True)
331
Arian363d93b2022-07-14 18:26:54 +0200332 if self.miboard_id:
333 board_iter = self.miboard_id if isinstance(self.miboard_id, tuple) else chain.from_iterable(self.miboard_id)
334 cmd = ['fdtput', '-t', 'i', out_file, '/', 'xiaomi,miboard-id'] + list(map(str, board_iter))
335 print(' {}'.format(' '.join(cmd)))
336 subprocess.run(cmd, check=True)
337
Aaron Kling5f122e12023-08-20 15:32:58 -0500338 return DeviceTree(out_file)
339
340 def get_name(self):
341 ext = os.path.splitext(os.path.basename(self.base))[1]
342 base_parts = self.filename_to_parts(self.base)
343 return '-'.join(chain.from_iterable([base_parts] + [self.filename_to_parts(tp.filename, ignored_parts=base_parts) for tp in self.techpacks])) + ext
344
345 @staticmethod
346 def filename_to_parts(name, ignored_parts=[]):
347 # Extract just the basename, with no suffix
348 filename = os.path.splitext(os.path.basename(name))[0]
349 parts = filename.split('-')
350 return [part for part in parts if part not in ignored_parts]
351
352 def __str__(self):
353 return "{} [{} + {{{}}}]".format(super().__str__(), self.base, " ".join(t.filename for t in self.techpacks))
354
355class MergedDeviceTree(object):
356 def __init__(self, other):
Arian363d93b2022-07-14 18:26:54 +0200357 self.merged_devicetrees = {InnerMergedDeviceTree(other.filename, other.plat_id, other.board_id, other.pmic_id, other.miboard_id)}
Aaron Kling5f122e12023-08-20 15:32:58 -0500358
359 def try_add(self, techpack):
360 did_add = False
361 for mdt in self.merged_devicetrees.copy():
362 # techpack and kernel devicetree need only to overlap in order to merge,
363 # and not match exactly. Think: venn diagram.
364 # Need 2 things: The devicetree part that applies to
365 # both kernel and techpack intersection = (techpack & mdt)
366 # and the part that applies only to kernel difference = (mdt - intersection)
367 # Note that because devicetrees are "multi-dimensional", doing (mdt - intersection)
368 # may result in *multiple* devicetrees
369
370 # techpack may apply to a superset of devices the mdt applies to
371 # reduce the techpack to just the things mdt has:
372 intersection = techpack & mdt
373 if intersection not in mdt:
374 continue
375 # mdt may apply to a superset of devices the techpack DT applies to
376 # (mdt - intersection) splits mdt into appropriate number of devicetrees
377 # such that we can apply techpack onto one of the resulting DTs in the
378 # difference
379 difference = mdt - intersection
380 if len(difference) > 1:
381 print('Splitting {}'.format(mdt))
382 print(' because {}'.format(techpack))
383 self.merged_devicetrees.remove(mdt)
384 self.merged_devicetrees.update(difference)
385
386 for mdt in self.merged_devicetrees:
387 if mdt.try_add(techpack):
388 did_add = True
389 return did_add
390
391
392 def save(self, out_dir):
393 assert len(self.merged_devicetrees) > 0
394 if len(self.merged_devicetrees) == 1:
395 name = os.path.basename(next(iter(self.merged_devicetrees)).base)
396 else:
397 name = None
398 for mdt in self.merged_devicetrees:
399 print()
400 yield mdt.save(name, out_dir)
401
402def parse_dt_files(dt_folder):
403 for root, dirs, files in os.walk(dt_folder):
404 for filename in files:
405 if os.path.splitext(filename)[1] not in ['.dtb', '.dtbo']:
406 continue
407 filepath = os.path.join(root, filename)
408 yield DeviceTree(filepath)
409
410def main():
411 if len(sys.argv) != 4:
412 print("Usage: {} <base dtb folder> <techpack dtb folder> <output folder>"
413 .format(sys.argv[0]))
414 sys.exit(1)
415
Arian363d93b2022-07-14 18:26:54 +0200416 # 1. Parse the devicetrees -- extract the device info (msm-id, board-id, pmic-id, miboard-id)
Aaron Kling5f122e12023-08-20 15:32:58 -0500417 bases = parse_dt_files(sys.argv[1])
418 techpacks = parse_dt_files(sys.argv[2])
419
420 # 2.1: Create an intermediate representation of the merged devicetrees, starting with the base
421 merged_devicetrees = list(map(lambda dt: MergedDeviceTree(dt), bases))
422 # 2.2: Try to add techpack devicetree to each base DT
423 for techpack in techpacks:
424 did_add = False
425 for dt in merged_devicetrees:
426 if dt.try_add(techpack):
427 did_add = True
428 if not did_add:
429 print('WARNING! Could not apply {} to any devicetrees'.format(techpack))
430
431 print()
432 print('==================================')
433 created = []
434 # 3. Save the deviectrees to real files
435 for dt in merged_devicetrees:
436 created.extend(dt.save(sys.argv[3]))
437
438 print()
439 print('==================================')
440 # 4. Try to apply merged DTBOs onto merged DTBs, when appropriate
441 # This checks that DTBOs and DTBs generated by merge_dtbs.py can be merged by bootloader
442 # at runtime.
443 for base, dtbo in product(created, created):
444 if os.path.splitext(base.filename)[1] != '.dtb' or os.path.splitext(dtbo.filename)[1] != '.dtbo':
445 continue
446 # See DeviceTreeInfo.__gt__; this checks whether dtbo is more specific than the base
447 if dtbo > base:
448 cmd = ['ufdt_apply_overlay', base.filename, dtbo.filename, '/dev/null']
449 print(' '.join(cmd))
450 subprocess.run(cmd, check=True)
451
452
453if __name__ == "__main__":
454 main()