blob: 43d090328bdd94febebd9a4fbec488d828c2a2a2 [file] [log] [blame]
Simon Glass50883f92011-07-12 16:19:16 -07001#!/usr/bin/python
2
3# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""This implements calling of scripts and other utilities/tools.
8
9We support running inside and outside the chroot. To do this, we permit
10a ## prefix which resolves to the chroot. Within the chroot this will be
11/ but outside it will be the full path to the chroot.
12
13So we can use filenames like this:
14
15 ##/usr/share/vboot/bitmaps/make_bmp_images.sh
16
17"""
18
19import optparse
20import os
Simon Glass951a2db2011-07-17 15:58:58 -070021import shutil
Simon Glass50883f92011-07-12 16:19:16 -070022import sys
Simon Glass951a2db2011-07-17 15:58:58 -070023import tempfile
Simon Glass50883f92011-07-12 16:19:16 -070024
25from cros_build_lib import RunCommandCaptureOutput
26import cros_build_lib
Simon Glassab344e32011-07-17 09:17:07 -070027import cros_output
Simon Glass50883f92011-07-12 16:19:16 -070028
29
30class CmdError(Exception):
31 """An error in the execution of a command."""
32 pass
33
34
35class Tools:
36 """A class to encapsulate the external tools we want to run
37
38 This provides convenient functions for running tools inside/outside the
Simon Glass290a1802011-07-17 13:54:32 -070039 chroot.
40
41 Public properties:
42 outdir: The output directory to write output files to.
Simon Glass6e336fb2012-03-09 15:45:40 -080043 search_paths: The list of directories to search for files we are asked
44 to read.
Simon Glass290a1802011-07-17 13:54:32 -070045
46 The tools class also provides common paths:
Simon Glass50883f92011-07-12 16:19:16 -070047
48 chroot_path: chroot directory
49 src_path: source directory
50 script_path: scripts directory (src/scripts)
51 overlay_path: overlays directory (src/overlays)
52 priv_overlay_path: private overlays directory (src/private-overlays)
53 board_path: build directory (/build in chroot)
54 third_party_path: third_parth directory (src/third_party)
55 cros_overlay_path: Chromium OS overlay (src/chromiumos-overlay)
56 """
Simon Glassab344e32011-07-17 09:17:07 -070057 def __init__(self, output):
Simon Glass50883f92011-07-12 16:19:16 -070058 """Set up the tools system.
59
60 Args:
Simon Glassab344e32011-07-17 09:17:07 -070061 output: cros_output object to use for output.
Simon Glass50883f92011-07-12 16:19:16 -070062 """
63 # Detect whether we're inside a chroot or not
64 self.in_chroot = cros_build_lib.IsInsideChroot()
Simon Glassab344e32011-07-17 09:17:07 -070065 self._out = output
Simon Glass64db3062011-07-14 21:58:54 -070066 self._root = None
67 if self.in_chroot:
68 root_dir = os.getenv('CROS_WORKON_SRCROOT')
69 else:
70 repo = cros_build_lib.FindRepoDir()
71 if not repo:
72 raise IOError('Cannot find .repo directory (must be below cwd level)')
73 root_dir = os.path.dirname(repo)
74 self._SetRoot(root_dir)
Simon Glass50883f92011-07-12 16:19:16 -070075
Simon Glassab344e32011-07-17 09:17:07 -070076 self._out.Info("Chroot is at '%s'" % self.chroot_path)
Simon Glass50883f92011-07-12 16:19:16 -070077 self._tools = {
Simon Glass1b8fa152012-03-09 15:22:54 -080078 'make_bmp_image' : '##/usr/share/vboot/bitmaps/make_bmp_images.sh',
79 'bct_dump' : '##/usr/bin/bct_dump',
80 'nvflash' : '##/usr/bin/nvflash',
81 'gbb_utility' : '##/usr/bin/gbb_utility',
82 'cbfstool' : '##/usr/bin/cbfstool',
Simon Glass50883f92011-07-12 16:19:16 -070083 }
Simon Glass951a2db2011-07-17 15:58:58 -070084 self.outdir = None # We have no output directory yet
85 self._delete_tempdir = None # And no temporary directory to delete
Simon Glass6e336fb2012-03-09 15:45:40 -080086 self.search_paths = []
Simon Glass951a2db2011-07-17 15:58:58 -070087
Simon Glass1f778c92011-08-09 13:30:43 -070088 def __enter__(self):
89 return self
90
91 def __exit__(self, type, value, traceback):
92 self.FinalizeOutputDir()
93 return False
Simon Glass50883f92011-07-12 16:19:16 -070094
95 def _SetRoot(self, root_dir):
96 """Sets the root directory for the build envionrment.
97
98 The root directory is the one containing .repo, chroot and src.
99
100 This should be called once the root is known. All other parts are
101 calculated from this.
102
103 Args:
Simon Glass05db7fd2011-07-14 22:02:19 -0700104 root_dir: The path to the root directory.
Simon Glass50883f92011-07-12 16:19:16 -0700105 """
106 self._root = os.path.normpath(root_dir)
107
108 # Set up the path to prepend to get to the chroot
109 if self.in_chroot:
110 self.chroot_path = '/'
111 else:
112 self.chroot_path = cros_build_lib.PrependChrootPath('')
113 self.src_path = os.path.join(self._root, 'src')
114 self.script_path = os.path.join(self.src_path, 'scripts')
115 self.overlay_path = os.path.join(self.src_path, 'overlays')
116 self.priv_overlay_path = os.path.join(self.src_path,
117 'private-overlays')
118 self.board_path = os.path.join(self.chroot_path, 'build')
119 self.third_party_path = os.path.join(self.src_path, 'third_party')
120 self.cros_overlay_path = os.path.join(self.third_party_path,
121 'chromiumos-overlay')
122
123 def Filename(self, fname):
124 """Resolve a chroot-relative filename to an absolute path.
125
126 This looks for ## at the beginning of the filename, and changes it to
127 the chroot directory, which will be / if inside the chroot, or a path
128 to the chroot if not.
129
130 Args:
131 fname: Filename to convert.
132
133 Returns
134 Absolute path to filename.
135 """
136 if fname.startswith('##/'):
137 fname = os.path.join(self.chroot_path, fname[3:])
Simon Glass6e336fb2012-03-09 15:45:40 -0800138
139 # Search for a pathname that exists, and return it if found
140 if not os.path.exists(fname):
141 for path in self.search_paths:
142 pathname = os.path.join(path, os.path.basename(fname))
143 if os.path.exists(pathname):
144 return pathname
145
146 # If not found, just return the standard, unchanged path
Simon Glass50883f92011-07-12 16:19:16 -0700147 return fname
148
Simon Glass86d16aa2012-03-09 15:29:05 -0800149 def Run(self, tool, args, cwd=None, sudo=False):
Simon Glass50883f92011-07-12 16:19:16 -0700150 """Run a tool with given arguments.
151
152 The tool name may be used unchanged or substituted with a full path if
153 required.
154
155 The tool and arguments can use ##/ to signify the chroot (at the beginning
156 of the tool/argument).
157
158 Args:
159 tool: Name of tool to run.
160 args: List of arguments to pass to tool.
161 cwd: Directory to change into before running tool (None if none).
Simon Glass86d16aa2012-03-09 15:29:05 -0800162 sudo: True to run the tool with sudo
Simon Glass50883f92011-07-12 16:19:16 -0700163
164 Returns:
165 Output of tool (stdout).
166
167 Raises
168 CmdError if running the tool, or the tool itself creates an error"""
169 if tool in self._tools:
170 tool = self._tools[tool]
171 tool = self.Filename(tool)
172 args = [self.Filename(arg) for arg in args]
173 cmd = [tool] + args
Simon Glass86d16aa2012-03-09 15:29:05 -0800174 if sudo:
175 cmd.insert(0, 'sudo')
Simon Glass50883f92011-07-12 16:19:16 -0700176 try:
Simon Glassab344e32011-07-17 09:17:07 -0700177 rc, stdout, err = RunCommandCaptureOutput(cmd,
178 print_cmd=self._out.verbose > 3, cwd=cwd)
Simon Glass50883f92011-07-12 16:19:16 -0700179 except OSError:
180 raise CmdError('Command not found: %s' % (' '.join(cmd)))
181 if rc:
Simon Glassab344e32011-07-17 09:17:07 -0700182 raise CmdError('Command failed: %s\n%s' % (' '.join(cmd), stdout))
183 self._out.Debug(stdout)
184 return stdout
Simon Glass50883f92011-07-12 16:19:16 -0700185
186 def ReadFile(self, fname):
187 """Read and return the contents of a file.
188
189 Args:
190 fname: path to filename to read, where ## signifiies the chroot.
191
192 Returns:
193 data read from file, as a string.
194 """
195 fd = open(self.Filename(fname), 'rb')
196 data = fd.read()
197 fd.close()
Simon Glassab344e32011-07-17 09:17:07 -0700198 self._out.Info("Read file '%s' size %d (%#0x)" %
199 (fname, len(data), len(data)))
Simon Glass50883f92011-07-12 16:19:16 -0700200 return data
201
202 def WriteFile(self, fname, data):
203 """Write data into a file.
204
205 Args:
206 fname: path to filename to write, where ## signifiies the chroot.
207 data: data to write to file, as a string.
208 """
Simon Glassab344e32011-07-17 09:17:07 -0700209 self._out.Info("Write file '%s' size %d (%#0x)" %
210 (fname, len(data), len(data)))
Simon Glass50883f92011-07-12 16:19:16 -0700211 fd = open(self.Filename(fname), 'wb')
212 fd.write(data)
213 fd.close()
214
215 def GetChromeosVersion(self):
216 """Returns the ChromeOS version string
217
218 This works by finding and executing the version script:
219
220 src/third_party/chromiumos-overlay/chromeos/config/chromeos_version.sh
221
222 Returns:
223 Version string in the form '0.14.726.2011_07_07_1635'
224
225 Raises:
226 CmdError: If the version script cannot be found, or is found but cannot
227 be executed.
228 """
229 version_script = os.path.join(self.cros_overlay_path, 'chromeos', 'config',
230 'chromeos_version.sh')
231
232 if os.path.exists(version_script):
233 str = self.Run('sh', ['-c', '. %s >/dev/null; '
234 'echo ${CHROMEOS_VERSION_STRING}'
235 % version_script])
236 return str.strip()
237 raise CmdError("Cannot find version script 'chromeos_version.sh'")
238
239 def CheckTool(self, filename, ebuild=None):
240 """Check that the specified tool exists.
241
242 If it does not exist in the PATH, then generate a useful error message
243 indicating how to install the ebuild that contains the required tool.
244
245 Args:
246 filename: filename of tool to look for on path.
247 ebuild: name of ebuild which should be emerged to install this tool,
248 or None if it is the same as the filename.
249
250 Raises:
251 CmdError(msg) if the tool is not found.
252 """
253 try:
254 if filename in self._tools:
255 filename = self._tools[filename]
256 filename = self.Filename(filename)
257 self.Run('which', [filename])
258 except CmdError as err:
259 raise CmdError("The '%s' utility was not found in your path. "
260 "Run the following command in \nyour chroot to install it: "
261 "sudo -E emerge %s" % (filename, ebuild or filename))
262
Simon Glassab344e32011-07-17 09:17:07 -0700263 def OutputSize(self, label, filename, level=cros_output.NOTICE):
264 """Display the filename and size of an object.
265
266 Args:
267 label: Label for this file.
268 filename: Filename to output.
269 level: Verbosity level to attach to this message
270 """
271 filename = self.Filename(filename)
272 size = os.stat(filename).st_size
273 self._out.DoOutput(level, "%s: %s; size: %d / %#x" %
274 (label, filename, size, size))
275
Simon Glass951a2db2011-07-17 15:58:58 -0700276 def PrepareOutputDir(self, outdir, preserve=False):
277 """Select an output directory, ensuring it exists.
278
279 This either creates a temporary directory or checks that the one supplied
280 by the user is valid. For a temporary directory, it makes a note to
281 remove it later if required.
282
283 Args:
284 outdir: Output directory to use, or None to use a temporary dir.
285
286 Raises:
287 OSError: If it cannot create the output directory.
288 """
289 self.outdir = outdir
290 self.preserve_outdir = preserve
291 if self.outdir:
292 if not os.path.isdir(self.outdir):
293 try:
294 os.makedirs(self.outdir)
295 except OSError as err:
296 raise CmdError("Cannot make output directory '%s': '%s'" %
297 (self.outdir, err))
298
299 else:
300 self.outdir = tempfile.mkdtemp()
301 self._delete_tempdir = self.outdir
Simon Glass290a1802011-07-17 13:54:32 -0700302 self._out.Debug("Using temporary directory '%s'" %
303 self._delete_tempdir)
Simon Glass951a2db2011-07-17 15:58:58 -0700304
305 def FinalizeOutputDir(self):
306 """Tidy up the output direcory, deleting it if temporary"""
307 if self._delete_tempdir and not self.preserve_outdir:
308 shutil.rmtree(self._delete_tempdir)
309 self._out.Debug("Deleted temporary directory '%s'" %
310 self._delete_tempdir)
311 self._delete_tempdir = None
312 elif self.outdir:
313 self._out.Debug("Output directory '%s'" % self.outdir)
314
Simon Glass50883f92011-07-12 16:19:16 -0700315def _Test():
316 """Run any built-in tests."""
317 import doctest
318 doctest.testmod()
319
320def main():
321 """Main function for tools.
322
323 We provide a way to call a few of our useful functions.
324
325 TODO(sjg) Move into the Chromite libraries when these are ready.
326 """
327 parser = optparse.OptionParser()
328 parser.add_option('-v', '--verbosity', dest='verbosity', default=1,
329 type='int', help='Control verbosity: 0=silent, 1=progress, 3=full, '
330 '4=debug')
331
332 help_str = '%s [options] cmd [args]\n\nAvailable commands:\n' % sys.argv[0]
333 help_str += '\tchromeos-version\tDisplay Chrome OS version'
334 parser.usage = help_str
335
336 (options, args) = parser.parse_args(sys.argv)
337 args = args[1:]
338
Simon Glassab344e32011-07-17 09:17:07 -0700339 out = cros_output.Output(options.verbosity)
340 tools = Tools(out)
Simon Glass50883f92011-07-12 16:19:16 -0700341 if not args:
342 parser.error('No command provided')
343 elif args[0] == 'chromeos-version':
344 print tools.GetChromeosVersion()
345 else:
346 parser.error("Unknown command '%s'" % args[0])
347
348if __name__ == '__main__':
349 if sys.argv[1:2] == ["--test"]:
350 _Test(*sys.argv[2:])
351 else:
352 main()