blob: 898be4cccb6442ac3d05498f80518da84a070051 [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):
47 def __init__(self, plat, board, pmic):
48 self.plat_id = plat
49 self.board_id = board
50 self.pmic_id = pmic
51
52 def __str__(self):
53 s = ""
54 if self.plat_id is not None:
55 s += " msm-id = <{}>;".format(" ".join(map(str, self.plat_id)))
56 if self.board_id is not None:
57 s += " board-id = <{}>;".format(" ".join(map(str, self.board_id)))
58 if self.pmic_id is not None:
59 s += " pmic-id = <{}>;".format(" ".join(map(str, self.pmic_id)))
60 return s.strip()
61
62 def __repr__(self):
63 return "<{} {}>".format(self.__class__.__name__, str(self))
64
65 def has_any_properties(self):
66 return self.plat_id is not None or self.board_id is not None or self.pmic_id is not None
67
68 def __sub__(self, other):
69 """
70 This devicetree has plat, board, and pmic id described like this:
71 msm-id = <A>, <B>
72 board-id = <c>, <d>
73 pmic-id = <0, 1>
74
75 Other has plat, board, pmic are:
76 msm-id = <A>, <B>
77 board-id = <c>
78 pmic-id = <0>
79
80 (self - other) will split self into a set of devicetrees with different identifers
81 and meets the following requirements:
82 - One of the devicetrees matches the IDs supported by other
83 - The devices which self matches are still supported (through 1 or more extra devicetrees)
84 by creating new devicetrees with different plat/board/pmic IDs
85 """
86 assert self.plat_id is None or isinstance(self.plat_id, (set, frozenset))
87 assert self.board_id is None or isinstance(self.board_id, (set, frozenset))
88 assert self.pmic_id is None or isinstance(self.pmic_id, (set, frozenset))
89 assert other in self
90
91 new_plat = other.plat_id is not None and self.plat_id != other.plat_id
92 new_board = other.board_id is not None and self.board_id != other.board_id
93 new_pmic = other.pmic_id is not None and self.pmic_id != other.pmic_id
94
95 res = set()
96 # Create the devicetree that matches other exactly
97 s = copy.deepcopy(self)
98 if new_plat:
99 s.plat_id = other.plat_id
100 if new_board:
101 s.board_id = other.board_id
102 if new_pmic:
103 s.pmic_id = other.pmic_id
104 res.add(s)
105
106 # now create the other possibilities by removing any combination of
107 # other's plat, board, and/or pmic. Set logic (unique elemnts) handles
108 # duplicate devicetrees IDs spit out by this loop
109 for combo in combinations_with_replacement([True, False], 3):
110 if not any((c and n) for (c, n) in zip(combo, (new_plat, new_board, new_pmic))):
111 continue
112 s = copy.deepcopy(self)
113 if combo[0] and new_plat:
114 s.plat_id -= other.plat_id
115 if combo[1] and new_board:
116 s.board_id -= other.board_id
117 if combo[2] and new_pmic:
118 s.pmic_id -= other.pmic_id
119 res.add(s)
120 return res
121
122 def __hash__(self):
123 # Hash should only consider msm-id/board-id/pmic-id
124 return hash((self.plat_id, self.board_id, self.pmic_id))
125
126 def __and__(self, other):
127 s = copy.deepcopy(self)
128 for prop in ['plat_id', 'board_id', 'pmic_id']:
129 if getattr(self, prop) is None or getattr(other, prop) is None:
130 setattr(s, prop, None)
131 else:
132 setattr(s, prop, getattr(self, prop) & getattr(other, prop))
133 return s
134
135 def _do_equivalent(self, other, property):
136 other_prop = getattr(other, property)
137 self_prop = getattr(self, property)
138 if other_prop is None:
139 return True
140 return self_prop == other_prop
141
142 def __eq__(self, other):
143 """
144 Checks whether other plat_id, board_id, pmic_id matches either identically
145 or because the property is none
146 """
147 if not isinstance(other, DeviceTreeInfo):
148 return False
149 if not other.has_any_properties():
150 return False
151 return all(map(lambda p: self._do_equivalent(other, p), ['plat_id', 'board_id', 'pmic_id']))
152
153
154 def _do_gt(self, other, property):
155 other_prop = getattr(other, property)
156 self_prop = getattr(self, property)
157 # if either property doesn't exist, it could merge in ABL
158 if self_prop is None or other_prop is None:
159 return True
160 # convert to iterable for convenience of below check
161 if isinstance(other_prop, tuple):
162 # if this property is all 0s, ABL coud match with anything on other
163 other_prop = [other_prop]
164 assert hasattr(other_prop, '__iter__')
165 if len(other_prop) == 1 and all(p == 0 for p in next(iter(other_prop))):
166 return True
167 # Test if this property intersects with other property
168 if hasattr(self_prop, '__contains__') and not isinstance(self_prop, tuple):
169 return any(p in self_prop for p in other_prop)
170 else:
171 return self_prop in other_prop
172
173 def __gt__(self, other):
174 """
175 Test if other is a more specific devicetree for self
176
177 This is used to test whether other devicetree applies to self by ABL matching rules
178 """
179 if not isinstance(other, DeviceTreeInfo):
180 return False
181 if not other.has_any_properties():
182 return False
183 return all(map(lambda p: self._do_gt(other, p), ['plat_id', 'board_id', 'pmic_id']))
184
185
186 def _do_contains(self, other, property):
187 other_prop = getattr(other, property)
188 self_prop = getattr(self, property)
189 # if other property doesn't exist, it can apply here
190 if other_prop is None:
191 return True
192 # if self and other are sets, use "issubset". Handle special case where other set is
193 # empty, in which case they aren't compatible because other_prop should be None
194 if isinstance(self_prop, (set, frozenset)) and isinstance(other_prop, (set, frozenset)):
195 return len(other_prop) > 0 and other_prop.issubset(self_prop)
196 # unpack to one item for convience of below check
197 if hasattr(other_prop, '__len__') and not isinstance(other_prop, tuple):
198 if len(other_prop) == 1:
199 other_prop = next(iter(other_prop))
200 # if this is a single value (tuple), not a list of them, other needs to match exactly
201 if isinstance(self_prop, tuple):
202 return self_prop == other_prop
203 # otherwise, use contains if possible (e.g. list or set of tuples)
204 if hasattr(self_prop, '__contains__'):
205 return other_prop in self_prop
206 return False
207
208 def __contains__(self, other):
209 """
210 Test if other devicetree covers this devicetree. That is, the devices other devicetree
211 matches is a subset of the devices this devicetree matches
212 """
213 if not isinstance(other, DeviceTreeInfo):
214 return False
215 if not other.has_any_properties():
216 return False
217 return all(map(lambda p: self._do_contains(other, p), ['plat_id', 'board_id', 'pmic_id']))
218
219class DeviceTree(DeviceTreeInfo):
220 def __init__(self, filename):
221 self.filename = filename
222 msm_id = split_array(self.get_prop('/', 'qcom,msm-id', check_output=False), 2)
223 board_id = split_array(self.get_prop('/', 'qcom,board-id', check_output=False), 2)
224 # default pmic-id-size is 4
225 pmic_id_size = self.get_prop('/', 'qcom,pmic-id-size', check_output=False) or 4
226 pmic_id = split_array(self.get_prop('/', 'qcom,pmic-id', check_output=False), pmic_id_size)
227 super().__init__(msm_id, board_id, pmic_id)
228
229 if not self.has_any_properties():
230 print('WARNING! {} has no properties and may match with any other devicetree'.format(self.filename))
231
232 def get_prop(self, node, property, prop_type='i', check_output=True):
233 r = subprocess.run(["fdtget", "-t", prop_type, self.filename, node, property],
234 check=check_output, stdout=subprocess.PIPE,
235 stderr=None if check_output else subprocess.DEVNULL)
236 if r.returncode != 0:
237 return None
238 out = r.stdout.decode("utf-8").strip()
239
240 out_array = None
241 if prop_type[-1] == 'i' or prop_type[-1] == 'u':
242 out_array = [int(e) for e in out.split(' ')]
243 if prop_type[-1] == 'x':
244 out_array = [int(e, 16) for e in out.split(' ')]
245 if out_array is not None:
246 if len(out_array) == 0:
247 return None
248 if len(out_array) == 1:
249 return out_array[0]
250 return out_array
251
252 return out
253
254 def __str__(self):
255 return "{} [{}]".format(super().__str__(), self.filename)
256
257class InnerMergedDeviceTree(DeviceTreeInfo):
258 """
259 InnerMergedDeviceTree is an actual representation of a merged devicetree.
260 It has a platform, board, and pmic ID, the "base" devicetree, and some set of add-on
261 devicetrees
262 """
263 def __init__(self, filename, plat_id, board_id, pmic_id, techpacks=None):
264 self.base = filename
265 self.techpacks = techpacks or []
266 super().__init__(plat_id, board_id, pmic_id)
267
268 def try_add(self, techpack):
269 if not isinstance(techpack, DeviceTree):
270 raise TypeError("{} is not a DeviceTree object".format(repr(techpack)))
271 intersection = techpack & self
272 if intersection in self:
273 self.techpacks.append(intersection)
274 return True
275 return False
276
277 def save(self, name=None, out_dir='.'):
278 if name is None:
279 name = self.get_name()
280
281 out_file = os.path.join(out_dir, name)
282 ext = os.path.splitext(os.path.basename(self.base))[1]
283
284 # This check might fail in future if we get into an edge case
285 # when splitting the base devicetree into multiple merged DTs
286 assert not os.path.exists(out_file)
287
288 if len(self.techpacks) == 0:
289 cmd = ['cp', self.base, out_file]
290 else:
291 if ext == '.dtb':
292 cmd = ['fdtoverlay']
293 else:
294 cmd = ['fdtoverlaymerge']
295 cmd.extend(['-i', self.base])
296 cmd.extend([tp.filename for tp in self.techpacks])
297 cmd.extend(['-o', out_file])
298
299 print(' {}'.format(' '.join(cmd)))
300 subprocess.run(cmd, check=True)
301
302 if self.plat_id:
303 plat_iter = self.plat_id if isinstance(self.plat_id, tuple) else chain.from_iterable(self.plat_id)
304 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,msm-id'] + list(map(str, plat_iter))
305 print(' {}'.format(' '.join(cmd)))
306 subprocess.run(cmd, check=True)
307
308 if self.board_id:
309 board_iter = self.board_id if isinstance(self.board_id, tuple) else chain.from_iterable(self.board_id)
310 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,board-id'] + list(map(str, board_iter))
311 print(' {}'.format(' '.join(cmd)))
312 subprocess.run(cmd, check=True)
313
314 if self.pmic_id:
315 pmic_iter = self.pmic_id if isinstance(self.pmic_id, tuple) else chain.from_iterable(self.pmic_id)
316 cmd = ['fdtput', '-t', 'i', out_file, '/', 'qcom,pmic-id'] + list(map(str, pmic_iter))
317 print(' {}'.format(' '.join(cmd)))
318 subprocess.run(cmd, check=True)
319
320 return DeviceTree(out_file)
321
322 def get_name(self):
323 ext = os.path.splitext(os.path.basename(self.base))[1]
324 base_parts = self.filename_to_parts(self.base)
325 return '-'.join(chain.from_iterable([base_parts] + [self.filename_to_parts(tp.filename, ignored_parts=base_parts) for tp in self.techpacks])) + ext
326
327 @staticmethod
328 def filename_to_parts(name, ignored_parts=[]):
329 # Extract just the basename, with no suffix
330 filename = os.path.splitext(os.path.basename(name))[0]
331 parts = filename.split('-')
332 return [part for part in parts if part not in ignored_parts]
333
334 def __str__(self):
335 return "{} [{} + {{{}}}]".format(super().__str__(), self.base, " ".join(t.filename for t in self.techpacks))
336
337class MergedDeviceTree(object):
338 def __init__(self, other):
339 self.merged_devicetrees = {InnerMergedDeviceTree(other.filename, other.plat_id, other.board_id, other.pmic_id)}
340
341 def try_add(self, techpack):
342 did_add = False
343 for mdt in self.merged_devicetrees.copy():
344 # techpack and kernel devicetree need only to overlap in order to merge,
345 # and not match exactly. Think: venn diagram.
346 # Need 2 things: The devicetree part that applies to
347 # both kernel and techpack intersection = (techpack & mdt)
348 # and the part that applies only to kernel difference = (mdt - intersection)
349 # Note that because devicetrees are "multi-dimensional", doing (mdt - intersection)
350 # may result in *multiple* devicetrees
351
352 # techpack may apply to a superset of devices the mdt applies to
353 # reduce the techpack to just the things mdt has:
354 intersection = techpack & mdt
355 if intersection not in mdt:
356 continue
357 # mdt may apply to a superset of devices the techpack DT applies to
358 # (mdt - intersection) splits mdt into appropriate number of devicetrees
359 # such that we can apply techpack onto one of the resulting DTs in the
360 # difference
361 difference = mdt - intersection
362 if len(difference) > 1:
363 print('Splitting {}'.format(mdt))
364 print(' because {}'.format(techpack))
365 self.merged_devicetrees.remove(mdt)
366 self.merged_devicetrees.update(difference)
367
368 for mdt in self.merged_devicetrees:
369 if mdt.try_add(techpack):
370 did_add = True
371 return did_add
372
373
374 def save(self, out_dir):
375 assert len(self.merged_devicetrees) > 0
376 if len(self.merged_devicetrees) == 1:
377 name = os.path.basename(next(iter(self.merged_devicetrees)).base)
378 else:
379 name = None
380 for mdt in self.merged_devicetrees:
381 print()
382 yield mdt.save(name, out_dir)
383
384def parse_dt_files(dt_folder):
385 for root, dirs, files in os.walk(dt_folder):
386 for filename in files:
387 if os.path.splitext(filename)[1] not in ['.dtb', '.dtbo']:
388 continue
389 filepath = os.path.join(root, filename)
390 yield DeviceTree(filepath)
391
392def main():
393 if len(sys.argv) != 4:
394 print("Usage: {} <base dtb folder> <techpack dtb folder> <output folder>"
395 .format(sys.argv[0]))
396 sys.exit(1)
397
398 # 1. Parse the devicetrees -- extract the device info (msm-id, board-id, pmic-id)
399 bases = parse_dt_files(sys.argv[1])
400 techpacks = parse_dt_files(sys.argv[2])
401
402 # 2.1: Create an intermediate representation of the merged devicetrees, starting with the base
403 merged_devicetrees = list(map(lambda dt: MergedDeviceTree(dt), bases))
404 # 2.2: Try to add techpack devicetree to each base DT
405 for techpack in techpacks:
406 did_add = False
407 for dt in merged_devicetrees:
408 if dt.try_add(techpack):
409 did_add = True
410 if not did_add:
411 print('WARNING! Could not apply {} to any devicetrees'.format(techpack))
412
413 print()
414 print('==================================')
415 created = []
416 # 3. Save the deviectrees to real files
417 for dt in merged_devicetrees:
418 created.extend(dt.save(sys.argv[3]))
419
420 print()
421 print('==================================')
422 # 4. Try to apply merged DTBOs onto merged DTBs, when appropriate
423 # This checks that DTBOs and DTBs generated by merge_dtbs.py can be merged by bootloader
424 # at runtime.
425 for base, dtbo in product(created, created):
426 if os.path.splitext(base.filename)[1] != '.dtb' or os.path.splitext(dtbo.filename)[1] != '.dtbo':
427 continue
428 # See DeviceTreeInfo.__gt__; this checks whether dtbo is more specific than the base
429 if dtbo > base:
430 cmd = ['ufdt_apply_overlay', base.filename, dtbo.filename, '/dev/null']
431 print(' '.join(cmd))
432 subprocess.run(cmd, check=True)
433
434
435if __name__ == "__main__":
436 main()