blob: af0a65e84189af872891d0b4acb935a65def04c7 [file] [log] [blame]
Simon Glassc55a50f2018-09-14 04:57:19 -06001# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2018 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Holds and modifies the state information held by binman
6#
7
Simon Glass03ebc202021-07-06 10:36:41 -06008from collections import defaultdict
Simon Glasse0e5df92018-09-14 04:57:31 -06009import hashlib
Simon Glassc55a50f2018-09-14 04:57:19 -060010import re
Simon Glass03ebc202021-07-06 10:36:41 -060011import time
Simon Glassc69d19c2021-07-06 10:36:37 -060012import threading
Simon Glassc55a50f2018-09-14 04:57:19 -060013
Simon Glass16287932020-04-17 18:09:03 -060014from dtoc import fdt
Simon Glassc55a50f2018-09-14 04:57:19 -060015import os
Simon Glassbf776672020-04-17 18:09:04 -060016from patman import tools
17from patman import tout
Simon Glassc55a50f2018-09-14 04:57:19 -060018
Simon Glassc475dec2021-11-23 11:03:42 -070019OUR_PATH = os.path.dirname(os.path.realpath(__file__))
20
Simon Glass76971702021-03-18 20:25:00 +130021# Map an dtb etype to its expected filename
22DTB_TYPE_FNAME = {
23 'u-boot-spl-dtb': 'spl/u-boot-spl.dtb',
24 'u-boot-tpl-dtb': 'tpl/u-boot-tpl.dtb',
25 }
26
Simon Glassfb5e8b12019-07-20 12:23:32 -060027# Records the device-tree files known to binman, keyed by entry type (e.g.
28# 'u-boot-spl-dtb'). These are the output FDT files, which can be updated by
29# binman. They have been copied to <xxx>.out files.
30#
Simon Glasscb8bebb2021-03-18 20:25:01 +130031# key: entry type (e.g. 'u-boot-dtb)
Simon Glassfb5e8b12019-07-20 12:23:32 -060032# value: tuple:
33# Fdt object
34# Filename
Simon Glass6ca0dcb2019-07-20 12:23:43 -060035output_fdt_info = {}
Simon Glassc55a50f2018-09-14 04:57:19 -060036
Simon Glass10f9d002019-07-20 12:23:50 -060037# Prefix to add to an fdtmap path to turn it into a path to the /binman node
38fdt_path_prefix = ''
39
Simon Glassc55a50f2018-09-14 04:57:19 -060040# Arguments passed to binman to provide arguments to entries
41entry_args = {}
42
Simon Glass539aece2018-09-14 04:57:22 -060043# True to use fake device-tree files for testing (see U_BOOT_DTB_DATA in
44# ftest.py)
Simon Glass93d17412018-09-14 04:57:23 -060045use_fake_dtb = False
Simon Glass539aece2018-09-14 04:57:22 -060046
Simon Glass2a72cc72018-09-14 04:57:20 -060047# The DTB which contains the full image information
48main_dtb = None
49
Simon Glassbf6906b2019-07-08 14:25:36 -060050# Allow entries to expand after they have been packed. This is detected and
51# forces a re-pack. If not allowed, any attempted expansion causes an error in
52# Entry.ProcessContentsUpdate()
53allow_entry_expansion = True
54
Simon Glass61ec04f2019-07-20 12:23:58 -060055# Don't allow entries to contract after they have been packed. Instead just
56# leave some wasted space. If allowed, this is detected and forces a re-pack,
57# but may result in entries that oscillate in size, thus causing a pack error.
58# An example is a compressed device tree where the original offset values
59# result in a larger compressed size than the new ones, but then after updating
60# to the new ones, the compressed size increases, etc.
61allow_entry_contraction = False
62
Simon Glassc69d19c2021-07-06 10:36:37 -060063# Number of threads to use for binman (None means machine-dependent)
64num_threads = None
65
Simon Glass03ebc202021-07-06 10:36:41 -060066
67class Timing:
68 """Holds information about an operation that is being timed
69
70 Properties:
71 name: Operation name (only one of each name is stored)
72 start: Start time of operation in seconds (None if not start)
73 accum:: Amount of time spent on this operation so far, in seconds
74 """
75 def __init__(self, name):
76 self.name = name
77 self.start = None # cause an error if TimingStart() is not called
78 self.accum = 0.0
79
80
81# Holds timing info for each name:
82# key: name of Timing info (Timing.name)
83# value: Timing object
84timing_info = {}
85
86
Simon Glassfb5e8b12019-07-20 12:23:32 -060087def GetFdtForEtype(etype):
88 """Get the Fdt object for a particular device-tree entry
Simon Glassc55a50f2018-09-14 04:57:19 -060089
90 Binman keeps track of at least one device-tree file called u-boot.dtb but
91 can also have others (e.g. for SPL). This function looks up the given
Simon Glassfb5e8b12019-07-20 12:23:32 -060092 entry and returns the associated Fdt object.
Simon Glassc55a50f2018-09-14 04:57:19 -060093
94 Args:
Simon Glassfb5e8b12019-07-20 12:23:32 -060095 etype: Entry type of device tree (e.g. 'u-boot-dtb')
Simon Glassc55a50f2018-09-14 04:57:19 -060096
97 Returns:
Simon Glassfb5e8b12019-07-20 12:23:32 -060098 Fdt object associated with the entry type
Simon Glassc55a50f2018-09-14 04:57:19 -060099 """
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600100 value = output_fdt_info.get(etype);
Simon Glass6a3b5b52019-07-20 12:23:42 -0600101 if not value:
102 return None
103 return value[0]
Simon Glassc55a50f2018-09-14 04:57:19 -0600104
Simon Glassfb5e8b12019-07-20 12:23:32 -0600105def GetFdtPath(etype):
Simon Glassc55a50f2018-09-14 04:57:19 -0600106 """Get the full pathname of a particular Fdt object
107
Simon Glass726e2962019-07-20 12:23:30 -0600108 Similar to GetFdtForEtype() but returns the pathname associated with the
109 Fdt.
Simon Glassc55a50f2018-09-14 04:57:19 -0600110
111 Args:
Simon Glassfb5e8b12019-07-20 12:23:32 -0600112 etype: Entry type of device tree (e.g. 'u-boot-dtb')
Simon Glassc55a50f2018-09-14 04:57:19 -0600113
114 Returns:
115 Full path name to the associated Fdt
116 """
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600117 return output_fdt_info[etype][0]._fname
Simon Glassc55a50f2018-09-14 04:57:19 -0600118
Simon Glassfb5e8b12019-07-20 12:23:32 -0600119def GetFdtContents(etype='u-boot-dtb'):
Simon Glass6ed45ba2018-09-14 04:57:24 -0600120 """Looks up the FDT pathname and contents
121
122 This is used to obtain the Fdt pathname and contents when needed by an
123 entry. It supports a 'fake' dtb, allowing tests to substitute test data for
124 the real dtb.
125
126 Args:
Simon Glassfb5e8b12019-07-20 12:23:32 -0600127 etype: Entry type to look up (e.g. 'u-boot.dtb').
Simon Glass6ed45ba2018-09-14 04:57:24 -0600128
129 Returns:
130 tuple:
131 pathname to Fdt
132 Fdt data (as bytes)
133 """
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600134 if etype not in output_fdt_info:
Simon Glass6a3b5b52019-07-20 12:23:42 -0600135 return None, None
136 if not use_fake_dtb:
Simon Glassfb5e8b12019-07-20 12:23:32 -0600137 pathname = GetFdtPath(etype)
138 data = GetFdtForEtype(etype).GetContents()
Simon Glass6ed45ba2018-09-14 04:57:24 -0600139 else:
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600140 fname = output_fdt_info[etype][1]
Simon Glass6ed45ba2018-09-14 04:57:24 -0600141 pathname = tools.GetInputFilename(fname)
142 data = tools.ReadFile(pathname)
143 return pathname, data
144
Simon Glassf6e02492019-07-20 12:24:08 -0600145def UpdateFdtContents(etype, data):
146 """Update the contents of a particular device tree
147
148 The device tree is updated and written back to its file. This affects what
149 is returned from future called to GetFdtContents(), etc.
150
151 Args:
152 etype: Entry type (e.g. 'u-boot-dtb')
153 data: Data to replace the DTB with
154 """
Simon Glasscb8bebb2021-03-18 20:25:01 +1300155 dtb, fname = output_fdt_info[etype]
Simon Glassf6e02492019-07-20 12:24:08 -0600156 dtb_fname = dtb.GetFilename()
157 tools.WriteFile(dtb_fname, data)
158 dtb = fdt.FdtScan(dtb_fname)
Simon Glasscb8bebb2021-03-18 20:25:01 +1300159 output_fdt_info[etype] = [dtb, fname]
Simon Glassf6e02492019-07-20 12:24:08 -0600160
Simon Glassc55a50f2018-09-14 04:57:19 -0600161def SetEntryArgs(args):
162 """Set the value of the entry args
163
164 This sets up the entry_args dict which is used to supply entry arguments to
165 entries.
166
167 Args:
168 args: List of entry arguments, each in the format "name=value"
169 """
170 global entry_args
171
172 entry_args = {}
Simon Glass06684922021-03-18 20:25:07 +1300173 tout.Debug('Processing entry args:')
Simon Glassc55a50f2018-09-14 04:57:19 -0600174 if args:
175 for arg in args:
176 m = re.match('([^=]*)=(.*)', arg)
177 if not m:
178 raise ValueError("Invalid entry arguemnt '%s'" % arg)
Simon Glass06684922021-03-18 20:25:07 +1300179 name, value = m.groups()
180 tout.Debug(' %20s = %s' % (name, value))
181 entry_args[name] = value
182 tout.Debug('Processing entry args done')
Simon Glassc55a50f2018-09-14 04:57:19 -0600183
184def GetEntryArg(name):
185 """Get the value of an entry argument
186
187 Args:
188 name: Name of argument to retrieve
189
190 Returns:
191 String value of argument
192 """
193 return entry_args.get(name)
Simon Glass2a72cc72018-09-14 04:57:20 -0600194
Simon Glass06684922021-03-18 20:25:07 +1300195def GetEntryArgBool(name):
196 """Get the value of an entry argument as a boolean
197
198 Args:
199 name: Name of argument to retrieve
200
201 Returns:
202 False if the entry argument is consider False (empty, '0' or 'n'), else
203 True
204 """
205 val = GetEntryArg(name)
206 return val and val not in ['n', '0']
207
Simon Glass539aece2018-09-14 04:57:22 -0600208def Prepare(images, dtb):
Simon Glass2a72cc72018-09-14 04:57:20 -0600209 """Get device tree files ready for use
210
Simon Glass4bdd1152019-07-20 12:23:29 -0600211 This sets up a set of device tree files that can be retrieved by
212 GetAllFdts(). This includes U-Boot proper and any SPL device trees.
Simon Glass2a72cc72018-09-14 04:57:20 -0600213
214 Args:
Simon Glass539aece2018-09-14 04:57:22 -0600215 images: List of images being used
Simon Glass2a72cc72018-09-14 04:57:20 -0600216 dtb: Main dtb
217 """
Simon Glass10f9d002019-07-20 12:23:50 -0600218 global output_fdt_info, main_dtb, fdt_path_prefix
Simon Glass2a72cc72018-09-14 04:57:20 -0600219 # Import these here in case libfdt.py is not available, in which case
220 # the above help option still works.
Simon Glass16287932020-04-17 18:09:03 -0600221 from dtoc import fdt
222 from dtoc import fdt_util
Simon Glass2a72cc72018-09-14 04:57:20 -0600223
224 # If we are updating the DTBs we need to put these updated versions
225 # where Entry_blob_dtb can find them. We can ignore 'u-boot.dtb'
226 # since it is assumed to be the one passed in with options.dt, and
227 # was handled just above.
228 main_dtb = dtb
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600229 output_fdt_info.clear()
Simon Glass10f9d002019-07-20 12:23:50 -0600230 fdt_path_prefix = ''
Simon Glasscb8bebb2021-03-18 20:25:01 +1300231 output_fdt_info['u-boot-dtb'] = [dtb, 'u-boot.dtb']
Simon Glass76971702021-03-18 20:25:00 +1300232 if use_fake_dtb:
233 for etype, fname in DTB_TYPE_FNAME.items():
Simon Glasscb8bebb2021-03-18 20:25:01 +1300234 output_fdt_info[etype] = [dtb, fname]
Simon Glass76971702021-03-18 20:25:00 +1300235 else:
Simon Glassf49462e2019-07-20 12:23:34 -0600236 fdt_set = {}
Simon Glass5187b802021-03-18 20:25:03 +1300237 for etype, fname in DTB_TYPE_FNAME.items():
238 infile = tools.GetInputFilename(fname, allow_missing=True)
239 if infile and os.path.exists(infile):
240 fname_dtb = fdt_util.EnsureCompiled(infile)
241 out_fname = tools.GetOutputFilename('%s.out' %
242 os.path.split(fname)[1])
243 tools.WriteFile(out_fname, tools.ReadFile(fname_dtb))
244 other_dtb = fdt.FdtScan(out_fname)
245 output_fdt_info[etype] = [other_dtb, out_fname]
246
Simon Glass2a72cc72018-09-14 04:57:20 -0600247
Simon Glass10f9d002019-07-20 12:23:50 -0600248def PrepareFromLoadedData(image):
249 """Get device tree files ready for use with a loaded image
250
251 Loaded images are different from images that are being created by binman,
252 since there is generally already an fdtmap and we read the description from
253 that. This provides the position and size of every entry in the image with
254 no calculation required.
255
256 This function uses the same output_fdt_info[] as Prepare(). It finds the
257 device tree files, adds a reference to the fdtmap and sets the FDT path
258 prefix to translate from the fdtmap (where the root node is the image node)
259 to the normal device tree (where the image node is under a /binman node).
260
261 Args:
262 images: List of images being used
263 """
264 global output_fdt_info, main_dtb, fdt_path_prefix
265
266 tout.Info('Preparing device trees')
267 output_fdt_info.clear()
268 fdt_path_prefix = ''
Simon Glasscb8bebb2021-03-18 20:25:01 +1300269 output_fdt_info['fdtmap'] = [image.fdtmap_dtb, 'u-boot.dtb']
Simon Glass10f9d002019-07-20 12:23:50 -0600270 main_dtb = None
271 tout.Info(" Found device tree type 'fdtmap' '%s'" % image.fdtmap_dtb.name)
272 for etype, value in image.GetFdts().items():
273 entry, fname = value
274 out_fname = tools.GetOutputFilename('%s.dtb' % entry.etype)
275 tout.Info(" Found device tree type '%s' at '%s' path '%s'" %
276 (etype, out_fname, entry.GetPath()))
277 entry._filename = entry.GetDefaultFilename()
278 data = entry.ReadData()
279
280 tools.WriteFile(out_fname, data)
281 dtb = fdt.Fdt(out_fname)
282 dtb.Scan()
283 image_node = dtb.GetNode('/binman')
284 if 'multiple-images' in image_node.props:
285 image_node = dtb.GetNode('/binman/%s' % image.image_node)
286 fdt_path_prefix = image_node.path
Simon Glasscb8bebb2021-03-18 20:25:01 +1300287 output_fdt_info[etype] = [dtb, None]
Simon Glass10f9d002019-07-20 12:23:50 -0600288 tout.Info(" FDT path prefix '%s'" % fdt_path_prefix)
289
290
Simon Glass4bdd1152019-07-20 12:23:29 -0600291def GetAllFdts():
Simon Glass2a72cc72018-09-14 04:57:20 -0600292 """Yield all device tree files being used by binman
293
294 Yields:
295 Device trees being used (U-Boot proper, SPL, TPL)
296 """
Simon Glass10f9d002019-07-20 12:23:50 -0600297 if main_dtb:
298 yield main_dtb
Simon Glass6ca0dcb2019-07-20 12:23:43 -0600299 for etype in output_fdt_info:
300 dtb = output_fdt_info[etype][0]
Simon Glass77e4ef12019-07-20 12:23:33 -0600301 if dtb != main_dtb:
302 yield dtb
Simon Glass2a72cc72018-09-14 04:57:20 -0600303
Simon Glass12bb1a92019-07-20 12:23:51 -0600304def GetUpdateNodes(node, for_repack=False):
Simon Glassf46621d2018-09-14 04:57:21 -0600305 """Yield all the nodes that need to be updated in all device trees
306
307 The property referenced by this node is added to any device trees which
308 have the given node. Due to removable of unwanted notes, SPL and TPL may
309 not have this node.
310
311 Args:
312 node: Node object in the main device tree to look up
Simon Glass12bb1a92019-07-20 12:23:51 -0600313 for_repack: True if we want only nodes which need 'repack' properties
314 added to them (e.g. 'orig-offset'), False to return all nodes. We
315 don't add repack properties to SPL/TPL device trees.
Simon Glassf46621d2018-09-14 04:57:21 -0600316
317 Yields:
318 Node objects in each device tree that is in use (U-Boot proper, which
319 is node, SPL and TPL)
320 """
321 yield node
Simon Glasscb8bebb2021-03-18 20:25:01 +1300322 for entry_type, (dtb, fname) in output_fdt_info.items():
Simon Glass6ed45ba2018-09-14 04:57:24 -0600323 if dtb != node.GetFdt():
Simon Glasscb8bebb2021-03-18 20:25:01 +1300324 if for_repack and entry_type != 'u-boot-dtb':
Simon Glass12bb1a92019-07-20 12:23:51 -0600325 continue
Simon Glass10f9d002019-07-20 12:23:50 -0600326 other_node = dtb.GetNode(fdt_path_prefix + node.path)
Simon Glass6ed45ba2018-09-14 04:57:24 -0600327 if other_node:
328 yield other_node
Simon Glassf46621d2018-09-14 04:57:21 -0600329
Simon Glass12bb1a92019-07-20 12:23:51 -0600330def AddZeroProp(node, prop, for_repack=False):
Simon Glassf46621d2018-09-14 04:57:21 -0600331 """Add a new property to affected device trees with an integer value of 0.
332
333 Args:
334 prop_name: Name of property
Simon Glass12bb1a92019-07-20 12:23:51 -0600335 for_repack: True is this property is only needed for repacking
Simon Glassf46621d2018-09-14 04:57:21 -0600336 """
Simon Glass12bb1a92019-07-20 12:23:51 -0600337 for n in GetUpdateNodes(node, for_repack):
Simon Glassf46621d2018-09-14 04:57:21 -0600338 n.AddZeroProp(prop)
339
Simon Glass0a98b282018-09-14 04:57:28 -0600340def AddSubnode(node, name):
341 """Add a new subnode to a node in affected device trees
342
343 Args:
344 node: Node to add to
345 name: name of node to add
346
347 Returns:
348 New subnode that was created in main tree
349 """
350 first = None
351 for n in GetUpdateNodes(node):
352 subnode = n.AddSubnode(name)
353 if not first:
354 first = subnode
355 return first
356
357def AddString(node, prop, value):
358 """Add a new string property to affected device trees
359
360 Args:
361 prop_name: Name of property
362 value: String value (which will be \0-terminated in the DT)
363 """
364 for n in GetUpdateNodes(node):
365 n.AddString(prop, value)
366
Simon Glass6eb99322021-01-06 21:35:18 -0700367def AddInt(node, prop, value):
368 """Add a new string property to affected device trees
369
370 Args:
371 prop_name: Name of property
372 val: Integer value of property
373 """
374 for n in GetUpdateNodes(node):
375 n.AddInt(prop, value)
376
Simon Glass12bb1a92019-07-20 12:23:51 -0600377def SetInt(node, prop, value, for_repack=False):
Simon Glassf46621d2018-09-14 04:57:21 -0600378 """Update an integer property in affected device trees with an integer value
379
380 This is not allowed to change the size of the FDT.
381
382 Args:
383 prop_name: Name of property
Simon Glass12bb1a92019-07-20 12:23:51 -0600384 for_repack: True is this property is only needed for repacking
Simon Glassf46621d2018-09-14 04:57:21 -0600385 """
Simon Glass12bb1a92019-07-20 12:23:51 -0600386 for n in GetUpdateNodes(node, for_repack):
387 tout.Detail("File %s: Update node '%s' prop '%s' to %#x" %
Simon Glass51014aa2019-07-20 12:23:56 -0600388 (n.GetFdt().name, n.path, prop, value))
Simon Glassf46621d2018-09-14 04:57:21 -0600389 n.SetInt(prop, value)
Simon Glasse0e5df92018-09-14 04:57:31 -0600390
391def CheckAddHashProp(node):
392 hash_node = node.FindNode('hash')
393 if hash_node:
394 algo = hash_node.props.get('algo')
395 if not algo:
396 return "Missing 'algo' property for hash node"
397 if algo.value == 'sha256':
398 size = 32
399 else:
400 return "Unknown hash algorithm '%s'" % algo
401 for n in GetUpdateNodes(hash_node):
402 n.AddEmptyProp('value', size)
403
404def CheckSetHashValue(node, get_data_func):
405 hash_node = node.FindNode('hash')
406 if hash_node:
407 algo = hash_node.props.get('algo').value
408 if algo == 'sha256':
409 m = hashlib.sha256()
410 m.update(get_data_func())
411 data = m.digest()
412 for n in GetUpdateNodes(hash_node):
413 n.SetData('value', data)
Simon Glassbf6906b2019-07-08 14:25:36 -0600414
415def SetAllowEntryExpansion(allow):
416 """Set whether post-pack expansion of entries is allowed
417
418 Args:
419 allow: True to allow expansion, False to raise an exception
420 """
421 global allow_entry_expansion
422
423 allow_entry_expansion = allow
424
425def AllowEntryExpansion():
426 """Check whether post-pack expansion of entries is allowed
427
428 Returns:
429 True if expansion should be allowed, False if an exception should be
430 raised
431 """
432 return allow_entry_expansion
Simon Glass61ec04f2019-07-20 12:23:58 -0600433
434def SetAllowEntryContraction(allow):
435 """Set whether post-pack contraction of entries is allowed
436
437 Args:
438 allow: True to allow contraction, False to raise an exception
439 """
440 global allow_entry_contraction
441
442 allow_entry_contraction = allow
443
444def AllowEntryContraction():
445 """Check whether post-pack contraction of entries is allowed
446
447 Returns:
448 True if contraction should be allowed, False if an exception should be
449 raised
450 """
451 return allow_entry_contraction
Simon Glassc69d19c2021-07-06 10:36:37 -0600452
453def SetThreads(threads):
454 """Set the number of threads to use when building sections
455
456 Args:
457 threads: Number of threads to use (None for default, 0 for
458 single-threaded)
459 """
460 global num_threads
461
462 num_threads = threads
463
464def GetThreads():
465 """Get the number of threads to use when building sections
466
467 Returns:
468 Number of threads to use (None for default, 0 for single-threaded)
469 """
470 return num_threads
Simon Glass03ebc202021-07-06 10:36:41 -0600471
472def GetTiming(name):
473 """Get the timing info for a particular operation
474
475 The object is created if it does not already exist.
476
477 Args:
478 name: Operation name to get
479
480 Returns:
481 Timing object for the current thread
482 """
483 threaded_name = '%s:%d' % (name, threading.get_ident())
484 timing = timing_info.get(threaded_name)
485 if not timing:
486 timing = Timing(threaded_name)
487 timing_info[threaded_name] = timing
488 return timing
489
490def TimingStart(name):
491 """Start the timer for an operation
492
493 Args:
494 name: Operation name to start
495 """
496 timing = GetTiming(name)
497 timing.start = time.monotonic()
498
499def TimingAccum(name):
500 """Stop and accumlate the time for an operation
501
502 This measures the time since the last TimingStart() and adds that to the
503 accumulated time.
504
505 Args:
506 name: Operation name to start
507 """
508 timing = GetTiming(name)
509 timing.accum += time.monotonic() - timing.start
510
511def TimingShow():
512 """Show all timing information"""
513 duration = defaultdict(float)
514 for threaded_name, timing in timing_info.items():
515 name = threaded_name.split(':')[0]
516 duration[name] += timing.accum
517
518 for name, seconds in duration.items():
519 print('%10s: %10.1fms' % (name, seconds * 1000))
Simon Glassc475dec2021-11-23 11:03:42 -0700520
521def GetVersion(path=OUR_PATH):
522 """Get the version string for binman
523
524 Args:
525 path: Path to 'version' file
526
527 Returns:
528 str: String version, e.g. 'v2021.10'
529 """
530 version_fname = os.path.join(path, 'version')
531 if os.path.exists(version_fname):
532 version = tools.ReadFile(version_fname, binary=False)
533 else:
534 version = '(unreleased)'
535 return version