Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 1 | #!/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 | |
| 9 | We support running inside and outside the chroot. To do this, we permit |
| 10 | a ## 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 | |
| 13 | So we can use filenames like this: |
| 14 | |
| 15 | ##/usr/share/vboot/bitmaps/make_bmp_images.sh |
| 16 | |
| 17 | """ |
| 18 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 19 | import doctest |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 20 | import optparse |
| 21 | import os |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 22 | import re |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 23 | import shutil |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 24 | import struct |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 25 | import sys |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 26 | import tempfile |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 27 | import unittest |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 28 | |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 29 | from chromite.lib import cros_build_lib |
| 30 | from chromite.lib import git |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 31 | import cros_output |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 32 | |
Vadim Bendebury | d9e9e68 | 2013-02-14 15:40:34 -0800 | [diff] [blame] | 33 | # Attributes defined outside __init__ |
| 34 | #pylint: disable=W0201 |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 35 | |
| 36 | class CmdError(Exception): |
| 37 | """An error in the execution of a command.""" |
| 38 | pass |
| 39 | |
| 40 | |
| 41 | class Tools: |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 42 | """A class to encapsulate the external tools we want to run. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 43 | |
| 44 | This provides convenient functions for running tools inside/outside the |
Simon Glass | 290a180 | 2011-07-17 13:54:32 -0700 | [diff] [blame] | 45 | chroot. |
| 46 | |
| 47 | Public properties: |
| 48 | outdir: The output directory to write output files to. |
Simon Glass | 6e336fb | 2012-03-09 15:45:40 -0800 | [diff] [blame] | 49 | search_paths: The list of directories to search for files we are asked |
| 50 | to read. |
Simon Glass | 290a180 | 2011-07-17 13:54:32 -0700 | [diff] [blame] | 51 | |
| 52 | The tools class also provides common paths: |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 53 | |
| 54 | chroot_path: chroot directory |
| 55 | src_path: source directory |
| 56 | script_path: scripts directory (src/scripts) |
| 57 | overlay_path: overlays directory (src/overlays) |
| 58 | priv_overlay_path: private overlays directory (src/private-overlays) |
| 59 | board_path: build directory (/build in chroot) |
| 60 | third_party_path: third_parth directory (src/third_party) |
| 61 | cros_overlay_path: Chromium OS overlay (src/chromiumos-overlay) |
| 62 | """ |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 63 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 64 | def __init__(self, output): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 65 | """Set up the tools system. |
| 66 | |
| 67 | Args: |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 68 | output: cros_output object to use for output. |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 69 | |
| 70 | Raises: |
| 71 | IOError: Unable to find .repo directory |
| 72 | |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 73 | """ |
| 74 | # Detect whether we're inside a chroot or not |
| 75 | self.in_chroot = cros_build_lib.IsInsideChroot() |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 76 | self._out = output |
Simon Glass | 64db306 | 2011-07-14 21:58:54 -0700 | [diff] [blame] | 77 | self._root = None |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 78 | self.chroot_path = None |
| 79 | root_dir = None |
Simon Glass | 64db306 | 2011-07-14 21:58:54 -0700 | [diff] [blame] | 80 | if self.in_chroot: |
| 81 | root_dir = os.getenv('CROS_WORKON_SRCROOT') |
| 82 | else: |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 83 | repo = git.FindRepoDir('.') |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 84 | if repo: |
| 85 | root_dir = os.path.dirname(repo) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 86 | |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 87 | if root_dir: |
| 88 | self._SetRoot(root_dir) |
| 89 | self._out.Info("Chroot is at '%s'" % self.chroot_path) |
| 90 | else: |
| 91 | self._out.Info('Running outside chroot') |
| 92 | |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 93 | self._tools = { |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 94 | 'make_bmp_image': '##/usr/share/vboot/bitmaps/make_bmp_images.sh', |
| 95 | 'bct_dump': '##/usr/bin/bct_dump', |
| 96 | 'tegrarcm': '##/usr/bin/tegrarcm', |
| 97 | 'gbb_utility': '##/usr/bin/gbb_utility', |
| 98 | 'cbfstool': '##/usr/bin/cbfstool', |
| 99 | 'fdisk': '##/sbin/fdisk', |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 100 | } |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 101 | self.outdir = None # We have no output directory yet |
Simon Glass | 6e336fb | 2012-03-09 15:45:40 -0800 | [diff] [blame] | 102 | self.search_paths = [] |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 103 | |
Simon Glass | 1f778c9 | 2011-08-09 13:30:43 -0700 | [diff] [blame] | 104 | def __enter__(self): |
| 105 | return self |
| 106 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 107 | def __exit__(self, the_type, value, traceback): |
Simon Glass | 1f778c9 | 2011-08-09 13:30:43 -0700 | [diff] [blame] | 108 | self.FinalizeOutputDir() |
| 109 | return False |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 110 | |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 111 | def _SetRoot(self, root_dir): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 112 | """Sets the root directory for the build envionrment. |
| 113 | |
| 114 | The root directory is the one containing .repo, chroot and src. |
| 115 | |
| 116 | This should be called once the root is known. All other parts are |
| 117 | calculated from this. |
| 118 | |
| 119 | Args: |
Simon Glass | 05db7fd | 2011-07-14 22:02:19 -0700 | [diff] [blame] | 120 | root_dir: The path to the root directory. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 121 | """ |
| 122 | self._root = os.path.normpath(root_dir) |
| 123 | |
| 124 | # Set up the path to prepend to get to the chroot |
| 125 | if self.in_chroot: |
| 126 | self.chroot_path = '/' |
| 127 | else: |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 128 | self.chroot_path = os.path.join(self._root, 'chroot') |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 129 | self.src_path = os.path.join(self._root, 'src') |
| 130 | self.script_path = os.path.join(self.src_path, 'scripts') |
| 131 | self.overlay_path = os.path.join(self.src_path, 'overlays') |
| 132 | self.priv_overlay_path = os.path.join(self.src_path, |
| 133 | 'private-overlays') |
| 134 | self.board_path = os.path.join(self.chroot_path, 'build') |
| 135 | self.third_party_path = os.path.join(self.src_path, 'third_party') |
| 136 | self.cros_overlay_path = os.path.join(self.third_party_path, |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 137 | 'chromiumos-overlay') |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 138 | |
| 139 | def Filename(self, fname): |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 140 | """Resolve a file path to an absolute path. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 141 | |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 142 | If fname starts with ##/ and chroot is available, ##/ gets replaced with |
| 143 | the chroot path. If chroot is not available, this file name can not be |
| 144 | resolved, `None' is returned. |
| 145 | |
| 146 | If fname is not prepended with the above prefix, and is not an existing |
| 147 | file, the actual file name is retrieved from the passed in string and the |
| 148 | search_paths directories (if any) are searched to for the file. If found - |
| 149 | the path to the found file is returned, `None' is returned otherwise. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 150 | |
| 151 | Args: |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 152 | fname: a string, the path to resolve. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 153 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 154 | Returns: |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 155 | Absolute path to the file or None if not found. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 156 | """ |
| 157 | if fname.startswith('##/'): |
Vadim Bendebury | 59ee2d3 | 2013-02-12 13:19:50 -0800 | [diff] [blame] | 158 | if self.chroot_path: |
| 159 | fname = os.path.join(self.chroot_path, fname[3:]) |
| 160 | else: |
| 161 | return None |
Simon Glass | 6e336fb | 2012-03-09 15:45:40 -0800 | [diff] [blame] | 162 | |
| 163 | # Search for a pathname that exists, and return it if found |
Simon Glass | 1841e7d | 2012-07-11 14:50:05 +0200 | [diff] [blame] | 164 | if fname and not os.path.exists(fname): |
Simon Glass | 6e336fb | 2012-03-09 15:45:40 -0800 | [diff] [blame] | 165 | for path in self.search_paths: |
| 166 | pathname = os.path.join(path, os.path.basename(fname)) |
| 167 | if os.path.exists(pathname): |
| 168 | return pathname |
| 169 | |
| 170 | # If not found, just return the standard, unchanged path |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 171 | return fname |
| 172 | |
Simon Glass | 86d16aa | 2012-03-09 15:29:05 -0800 | [diff] [blame] | 173 | def Run(self, tool, args, cwd=None, sudo=False): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 174 | """Run a tool with given arguments. |
| 175 | |
| 176 | The tool name may be used unchanged or substituted with a full path if |
| 177 | required. |
| 178 | |
| 179 | The tool and arguments can use ##/ to signify the chroot (at the beginning |
| 180 | of the tool/argument). |
| 181 | |
| 182 | Args: |
| 183 | tool: Name of tool to run. |
| 184 | args: List of arguments to pass to tool. |
| 185 | cwd: Directory to change into before running tool (None if none). |
Simon Glass | 86d16aa | 2012-03-09 15:29:05 -0800 | [diff] [blame] | 186 | sudo: True to run the tool with sudo |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 187 | |
| 188 | Returns: |
| 189 | Output of tool (stdout). |
| 190 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 191 | Raises: |
| 192 | CmdError: If running the tool, or the tool itself creates an error. |
| 193 | """ |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 194 | if tool in self._tools: |
| 195 | tool = self._tools[tool] |
| 196 | tool = self.Filename(tool) |
| 197 | args = [self.Filename(arg) for arg in args] |
| 198 | cmd = [tool] + args |
Vadim Bendebury | 281b305 | 2013-02-19 12:38:15 -0800 | [diff] [blame] | 199 | if sudo and os.getuid(): |
Simon Glass | 86d16aa | 2012-03-09 15:29:05 -0800 | [diff] [blame] | 200 | cmd.insert(0, 'sudo') |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 201 | try: |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 202 | result = cros_build_lib.RunCommandCaptureOutput( |
| 203 | cmd, cwd=cwd, print_cmd=self._out.verbose > 3, |
| 204 | combine_stdout_stderr=True, error_code_ok=True) |
| 205 | except cros_build_lib.RunCommandError as ex: |
| 206 | raise CmdError(str(ex)) |
| 207 | stdout = result.output |
| 208 | if result.returncode: |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 209 | raise CmdError('Command failed: %s\n%s' % (' '.join(cmd), stdout)) |
| 210 | self._out.Debug(stdout) |
| 211 | return stdout |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 212 | |
| 213 | def ReadFile(self, fname): |
| 214 | """Read and return the contents of a file. |
| 215 | |
| 216 | Args: |
| 217 | fname: path to filename to read, where ## signifiies the chroot. |
| 218 | |
| 219 | Returns: |
| 220 | data read from file, as a string. |
| 221 | """ |
| 222 | fd = open(self.Filename(fname), 'rb') |
| 223 | data = fd.read() |
| 224 | fd.close() |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 225 | self._out.Info("Read file '%s' size %d (%#0x)" % |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 226 | (fname, len(data), len(data))) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 227 | return data |
| 228 | |
| 229 | def WriteFile(self, fname, data): |
| 230 | """Write data into a file. |
| 231 | |
| 232 | Args: |
| 233 | fname: path to filename to write, where ## signifiies the chroot. |
| 234 | data: data to write to file, as a string. |
| 235 | """ |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 236 | self._out.Info("Write file '%s' size %d (%#0x)" % |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 237 | (fname, len(data), len(data))) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 238 | fd = open(self.Filename(fname), 'wb') |
| 239 | fd.write(data) |
| 240 | fd.close() |
| 241 | |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 242 | def ReadFileAndConcat(self, filenames, compress=None, with_index=False): |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 243 | """Read several files and concat them. |
| 244 | |
| 245 | Args: |
| 246 | filenames: a list containing name of the files to read. |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 247 | with_index: If true, an index structure is prepended to the data. |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 248 | |
| 249 | Returns: |
| 250 | A tuple of a string and two list. The string is the concated data read |
| 251 | from file, in the same order as in filenames, aligned to 4-byte. The |
Vic Yang | c09edff | 2012-08-16 07:57:44 +0800 | [diff] [blame] | 252 | first list contains the offset of each file in the data string and |
| 253 | the second one contains the actual (non-padded) length of each file, |
| 254 | both in the same order. |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 255 | |
| 256 | The optional index structure is a 32 bit integer set to the number of |
| 257 | entries in the index, followed by that many pairs of integers which |
| 258 | describe the offset and length of each chunk. |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 259 | """ |
| 260 | data = '' |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 261 | offsets = [] |
| 262 | lengths = [] |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 263 | for fname in filenames: |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 264 | offsets.append(len(data)) |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 265 | content = self.ReadFile(fname) |
| 266 | pad_len = ((len(content) + 3) & ~3) - len(content) |
Vic Yang | c09edff | 2012-08-16 07:57:44 +0800 | [diff] [blame] | 267 | data += content + chr(0xff) * pad_len |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 268 | lengths.append(len(content)) |
| 269 | |
| 270 | if with_index: |
| 271 | index_size = 4 + len(filenames) * 8 |
| 272 | index = struct.pack("<I", len(filenames)) |
| 273 | offsets = tuple(offset + index_size for offset in offsets) |
Vadim Bendebury | d9e9e68 | 2013-02-14 15:40:34 -0800 | [diff] [blame] | 274 | for _, offset, length in zip(filenames, offsets, lengths): |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 275 | index += struct.pack("<II", offset, length) |
| 276 | data = index + data |
Simon Glass | 9bc399e | 2012-12-11 14:36:10 -0800 | [diff] [blame] | 277 | |
| 278 | if compress: |
| 279 | if compress == 'lzo': |
| 280 | # Would be nice to just pipe here. but we don't have RunPipe(). |
| 281 | fname = self.GetOutputFilename('data.tmp') |
| 282 | outname = self.GetOutputFilename('data.tmp.lzo') |
| 283 | if os.path.exists(outname): |
| 284 | os.remove(outname) |
| 285 | self.WriteFile(fname, data) |
| 286 | args = ['-9', fname] |
| 287 | self.Run('lzop', args) |
| 288 | data = self.ReadFile(outname) |
| 289 | else: |
| 290 | raise ValueError("Unknown compression method '%s'" % compress) |
Gabe Black | 0f419b6 | 2013-01-10 22:05:00 -0800 | [diff] [blame] | 291 | return data, offsets, lengths |
Vic Yang | a850b92 | 2012-08-11 14:08:43 +0800 | [diff] [blame] | 292 | |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 293 | def GetChromeosVersion(self): |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 294 | """Returns the ChromeOS version string. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 295 | |
| 296 | This works by finding and executing the version script: |
| 297 | |
| 298 | src/third_party/chromiumos-overlay/chromeos/config/chromeos_version.sh |
| 299 | |
| 300 | Returns: |
| 301 | Version string in the form '0.14.726.2011_07_07_1635' |
| 302 | |
| 303 | Raises: |
| 304 | CmdError: If the version script cannot be found, or is found but cannot |
| 305 | be executed. |
| 306 | """ |
| 307 | version_script = os.path.join(self.cros_overlay_path, 'chromeos', 'config', |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 308 | 'chromeos_version.sh') |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 309 | |
| 310 | if os.path.exists(version_script): |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 311 | result = self.Run('sh', ['-c', '. %s >/dev/null; ' |
| 312 | 'echo ${CHROMEOS_VERSION_STRING}' |
| 313 | % version_script]) |
| 314 | return result.strip() |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 315 | raise CmdError("Cannot find version script 'chromeos_version.sh'") |
| 316 | |
Simon Glass | 2343e8f | 2012-10-01 14:48:24 -0700 | [diff] [blame] | 317 | def CheckTool(self, name, ebuild=None): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 318 | """Check that the specified tool exists. |
| 319 | |
| 320 | If it does not exist in the PATH, then generate a useful error message |
| 321 | indicating how to install the ebuild that contains the required tool. |
| 322 | |
| 323 | Args: |
Simon Glass | 2343e8f | 2012-10-01 14:48:24 -0700 | [diff] [blame] | 324 | name: filename of tool to look for on path. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 325 | ebuild: name of ebuild which should be emerged to install this tool, |
| 326 | or None if it is the same as the filename. |
| 327 | |
| 328 | Raises: |
| 329 | CmdError(msg) if the tool is not found. |
| 330 | """ |
| 331 | try: |
Simon Glass | 2343e8f | 2012-10-01 14:48:24 -0700 | [diff] [blame] | 332 | filename = name |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 333 | if filename in self._tools: |
| 334 | filename = self._tools[filename] |
| 335 | filename = self.Filename(filename) |
| 336 | self.Run('which', [filename]) |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 337 | except CmdError: |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 338 | raise CmdError("The '%s' utility was not found in your path. " |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 339 | "Run the following command in \nyour chroot to install " |
| 340 | "it: sudo -E emerge %s" % (filename, ebuild or name)) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 341 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 342 | def OutputSize(self, label, filename, level=cros_output.NOTICE): |
| 343 | """Display the filename and size of an object. |
| 344 | |
| 345 | Args: |
| 346 | label: Label for this file. |
| 347 | filename: Filename to output. |
| 348 | level: Verbosity level to attach to this message |
| 349 | """ |
| 350 | filename = self.Filename(filename) |
| 351 | size = os.stat(filename).st_size |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 352 | self._out.DoOutput(level, '%s: %s; size: %d / %#x' % |
| 353 | (label, filename, size, size)) |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 354 | |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 355 | def PrepareOutputDir(self, outdir, preserve=False): |
| 356 | """Select an output directory, ensuring it exists. |
| 357 | |
| 358 | This either creates a temporary directory or checks that the one supplied |
| 359 | by the user is valid. For a temporary directory, it makes a note to |
| 360 | remove it later if required. |
| 361 | |
| 362 | Args: |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 363 | outdir: a string, name of the output directory to use to store |
| 364 | intermediate and output files. If is None - create a temporary |
| 365 | directory. |
| 366 | preserve: a Boolean. If outdir above is None and preserve is False, the |
| 367 | created temporary directory will be destroyed on exit. |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 368 | Raises: |
| 369 | OSError: If it cannot create the output directory. |
| 370 | """ |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 371 | self.preserve_outdir = outdir or preserve |
| 372 | if outdir: |
| 373 | self.outdir = outdir |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 374 | if not os.path.isdir(self.outdir): |
| 375 | try: |
| 376 | os.makedirs(self.outdir) |
| 377 | except OSError as err: |
| 378 | raise CmdError("Cannot make output directory '%s': '%s'" % |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 379 | (self.outdir, err.strerror)) |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 380 | else: |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 381 | self.outdir = tempfile.mkdtemp(prefix='cros-dev.') |
| 382 | self._out.Debug("Using temporary directory '%s'" % self.outdir) |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 383 | |
| 384 | def FinalizeOutputDir(self): |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 385 | """Tidy up: delete output directory if temporary and not preserved.""" |
| 386 | if self.outdir and not self.preserve_outdir: |
| 387 | shutil.rmtree(self.outdir) |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 388 | self._out.Debug("Deleted temporary directory '%s'" % |
Vadim Bendebury | 2692ad0 | 2013-02-15 10:19:57 -0800 | [diff] [blame] | 389 | self.outdir) |
| 390 | self.outdir = None |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 391 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 392 | def GetOutputFilename(self, fname): |
| 393 | """Return a filename within the output directory. |
| 394 | |
| 395 | Args: |
| 396 | fname: Filename to use for new file |
| 397 | |
| 398 | Returns: |
| 399 | The full path of the filename, within the output directory |
| 400 | """ |
| 401 | return os.path.join(self.outdir, fname) |
| 402 | |
| 403 | |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 404 | # pylint: disable=W0212 |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 405 | class ToolsTests(unittest.TestCase): |
| 406 | """Unit tests for this module.""" |
| 407 | |
| 408 | def setUp(self): |
| 409 | self.out = cros_output.Output(False) |
| 410 | self.tools = Tools(self.out) |
| 411 | |
| 412 | def MakeOutsideChroot(self, base): |
| 413 | tools = Tools(self.out) |
| 414 | tools.in_chroot = False |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 415 | tools._SetRoot(base) |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 416 | return tools |
| 417 | |
| 418 | def testPaths(self): |
| 419 | tools = self.tools |
| 420 | |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 421 | self.assertTrue(os.path.isdir(os.path.join(tools._root, '.repo'))) |
| 422 | |
| 423 | def _testToolsPaths(self, base, tools): |
| 424 | """Common paths tests to run inside and outside chroot. |
| 425 | |
| 426 | These tests are the same inside and outside the choot, so we put them in a |
| 427 | separate function. |
| 428 | |
| 429 | Args: |
| 430 | base: Base directory to use for testing (contains the 'src' directory). |
| 431 | tools: Tools object to use. |
| 432 | """ |
| 433 | self.assertEqual(tools._root, base[:-1]) |
| 434 | self.assertEqual(tools.src_path, base + 'src') |
| 435 | self.assertEqual(tools.script_path, base + 'src/scripts') |
| 436 | self.assertEqual(tools.overlay_path, base + 'src/overlays') |
| 437 | self.assertEqual(tools.priv_overlay_path, base + 'src/private-overlays') |
| 438 | self.assertEqual(tools.third_party_path, base + 'src/third_party') |
| 439 | self.assertEqual(tools.cros_overlay_path, base + |
| 440 | 'src/third_party/chromiumos-overlay') |
| 441 | |
| 442 | def testSetRootInsideChroot(self): |
| 443 | """Inside the chroot, paths are slightly different from outside.""" |
| 444 | tools = Tools(self.out) |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 445 | tools.in_chroot = True |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 446 | |
| 447 | # Force our own path. |
| 448 | base = '/air/bridge/' |
| 449 | tools._SetRoot(base) |
| 450 | |
| 451 | # We should get a full path from that without the trailing '/'. |
| 452 | self.assertEqual(tools.chroot_path, '/') |
| 453 | self.assertEqual(tools.board_path, '/build') |
| 454 | self._testToolsPaths(base, tools) |
| 455 | |
| 456 | def testSetRootOutsideChroot(self): |
| 457 | """Pretend to be outside the chroot, and check that paths are correct.""" |
| 458 | |
| 459 | # Force our own path, outside the chroot. |
| 460 | base = '/spotty/light/' |
| 461 | tools = self.MakeOutsideChroot(base) |
| 462 | |
| 463 | # We should get a full path from that without the trailing '/'. |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 464 | self.assertEqual(tools.chroot_path, base + 'chroot') |
| 465 | self.assertEqual(tools.board_path, tools.chroot_path + '/build') |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 466 | self._testToolsPaths(base, tools) |
| 467 | |
| 468 | def _testToolsFilenames(self, tools): |
| 469 | """Common filename tests to run inside and outside chroot. |
| 470 | |
| 471 | These tests are the same inside and outside the choot, so we put them in a |
| 472 | separate function. |
| 473 | |
| 474 | Args: |
| 475 | tools: Tools object to use. |
| 476 | """ |
| 477 | self.assertEqual(tools.Filename('/root/based/'), |
| 478 | '/root/based/') |
| 479 | |
| 480 | # Try search paths in /bin and /ls. |
| 481 | tools.search_paths = ['/bin', '/lib'] |
| 482 | file_in_bin = os.listdir('/bin')[0] |
| 483 | self.assertEqual(tools.Filename(file_in_bin), '/bin/%s' % file_in_bin) |
| 484 | file_in_lib = os.listdir('/lib')[0] |
| 485 | self.assertEqual(tools.Filename(file_in_lib), '/lib/%s' % file_in_lib) |
| 486 | self.assertEqual(tools.Filename('i-am-not-here'), 'i-am-not-here') |
| 487 | |
| 488 | # Don't search for an empty file. |
| 489 | self.assertEqual(tools.Filename(''), '') |
| 490 | |
| 491 | def testFilenameInsideChroot(self): |
| 492 | """Test that we can specify search paths and they work correctly. |
| 493 | |
| 494 | Test search patches inside the chroot. |
| 495 | """ |
| 496 | tools = Tools(self.out) |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 497 | tools.in_chroot = True |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 498 | |
| 499 | # Force our own path. |
| 500 | base = '/air/bridge/' |
| 501 | tools._SetRoot(base) |
| 502 | |
| 503 | self.assertEqual(tools.Filename('##/fred'), '/fred') |
| 504 | self.assertEqual(tools.Filename('##/just/a/short/dir/'), |
| 505 | '/just/a/short/dir/') |
| 506 | |
| 507 | self._testToolsFilenames(tools) |
| 508 | |
| 509 | def testFilenameOutsideChroot(self): |
| 510 | """Test that we can specify search paths and they work correctly. |
| 511 | |
| 512 | Test search patches outside the chroot. |
| 513 | """ |
| 514 | base = '/home/' |
| 515 | tools = self.MakeOutsideChroot(base) |
| 516 | |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 517 | self.assertEqual(tools.Filename('##/fred'), base + 'chroot/fred') |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 518 | self.assertEqual(tools.Filename('##/just/a/short/dir/'), |
David James | b0a15c2 | 2013-01-02 18:32:30 -0800 | [diff] [blame] | 519 | base + 'chroot/just/a/short/dir/') |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 520 | |
| 521 | self._testToolsFilenames(tools) |
| 522 | |
| 523 | def testReadWriteFile(self): |
| 524 | """Test our read/write utility functions.""" |
| 525 | tools = Tools(self.out) |
| 526 | tools.PrepareOutputDir(None) |
| 527 | data = 'some context here' * 2 |
| 528 | |
| 529 | fname = tools.GetOutputFilename('bang') |
| 530 | tools.WriteFile(fname, data) |
| 531 | |
| 532 | # Check that the file looks correct. |
| 533 | compare = tools.ReadFile(fname) |
| 534 | self.assertEqual(data, compare) |
| 535 | |
| 536 | def testReadFileAndConcat(self): |
| 537 | """Test 'cat' of several files.""" |
| 538 | tools = Tools(self.out) |
| 539 | tools.PrepareOutputDir(None) |
| 540 | file_list = ['one', 'empty', 'two', 'three', 'four'] |
| 541 | out_list = [tools.GetOutputFilename(fname) for fname in file_list] |
| 542 | file_list[1] = '' # Empty the 'empty' file. |
| 543 | for upto in range(len(file_list)): |
| 544 | tools.WriteFile(out_list[upto], file_list[upto]) |
| 545 | |
| 546 | data, offset, length = tools.ReadFileAndConcat(out_list) |
| 547 | self.assertEqual(len(data), 20) |
| 548 | self.assertEqual(offset, [0, 4, 4, 8, 16]) |
| 549 | self.assertEqual(length, [3, 0, 3, 5, 4]) |
| 550 | |
| 551 | def testGetChromeosVersion(self): |
| 552 | """Test for GetChromeosVersion() inside and outside chroot. |
| 553 | |
| 554 | This function returns a string like '2893.0.2012_09_16_2219'. |
| 555 | """ |
| 556 | tools = self.tools |
| 557 | |
Gilad Arnold | 8047ff4 | 2013-02-20 08:50:14 -0800 | [diff] [blame^] | 558 | re_version_pattern = '^\d{4}.\d+.\d{4}_\d{2}_\d{2}_\d+$' |
| 559 | re_version = re.compile(re_version_pattern) |
| 560 | reported_version = tools.GetChromeosVersion() |
| 561 | self.assertTrue(re_version.match(reported_version), |
| 562 | msg='%s !~= %s' % (reported_version, re_version_pattern)) |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 563 | |
| 564 | tools = Tools(self.out) |
| 565 | |
| 566 | # Force our own path, outside the chroot. This should fail. |
| 567 | base = 'invalid-dir' |
| 568 | tools = self.MakeOutsideChroot(base) |
| 569 | tools.in_chroot = False |
| 570 | self.assertRaises(CmdError, tools.GetChromeosVersion) |
| 571 | |
| 572 | def testCheckTool(self): |
| 573 | """Test for the CheckTool() method.""" |
| 574 | tools = self.tools |
| 575 | |
| 576 | tools.CheckTool('fdisk') |
| 577 | tools.CheckTool('gbb_utility') |
| 578 | self.assertRaises(CmdError, tools.CheckTool, 'non-existent-tool') |
| 579 | tools.CheckTool('fdisk') |
| 580 | self.assertRaises(CmdError, tools.CheckTool, '/usr/bin/fdisk') |
| 581 | |
| 582 | def testRun(self): |
| 583 | """Test for the Run() method.""" |
| 584 | tools = self.tools |
| 585 | |
| 586 | # Ask fdisk for its version - this utility must be in the chroot. |
| 587 | re_fdisk = re.compile('fdisk \(util-linux .*\)') |
| 588 | self.assertTrue(re_fdisk.match(tools.Run('fdisk', ['-v']))) |
| 589 | |
| 590 | # We need sudo for looking at disks. |
| 591 | self.assertEqual(tools.Run('fdisk', ['-l', '/dev/sda']), |
| 592 | 'Cannot open /dev/sda\n') |
| 593 | out = tools.Run('fdisk', ['-l', '/dev/sda'], sudo=True) |
| 594 | |
| 595 | # Don't look at the specific output, but it will have > 5 lines. |
| 596 | self.assertTrue(len(out.splitlines()) > 5) |
| 597 | |
| 598 | self.assertEqual(tools.Run('pwd', [], cwd='/tmp'), '/tmp\n') |
| 599 | |
| 600 | def testOutputDir(self): |
| 601 | """Test output directory creation and deletion.""" |
| 602 | tools = self.tools |
| 603 | |
| 604 | # First check basic operation, creating and deleting a tmpdir. |
| 605 | tools.PrepareOutputDir(None) |
| 606 | fname = tools.GetOutputFilename('fred') |
| 607 | tools.WriteFile(fname, 'You are old, Father William, the young man said') |
| 608 | dirname = tools.outdir |
| 609 | tools.FinalizeOutputDir() |
| 610 | self.assertFalse(os.path.exists(fname)) |
| 611 | self.assertFalse(os.path.exists(dirname)) |
| 612 | |
| 613 | # Try preserving it. |
| 614 | tools.PrepareOutputDir(None, True) |
| 615 | fname = tools.GetOutputFilename('fred') |
| 616 | tools.WriteFile(fname, 'and your hair has become very white') |
| 617 | dirname = tools.outdir |
| 618 | tools.FinalizeOutputDir() |
| 619 | self.assertTrue(os.path.exists(fname)) |
| 620 | self.assertTrue(os.path.exists(dirname)) |
| 621 | shutil.rmtree(dirname) |
| 622 | |
| 623 | # Use our own directory, which is always preserved. |
| 624 | testdir = '/tmp/tools-test.test' |
| 625 | tools.PrepareOutputDir(testdir) |
| 626 | fname = tools.GetOutputFilename('fred') |
| 627 | tools.WriteFile(fname, 'and yet you incessantly stand on your head') |
| 628 | dirname = tools.outdir |
| 629 | tools.FinalizeOutputDir() |
| 630 | self.assertTrue(os.path.exists(fname)) |
| 631 | self.assertTrue(os.path.exists(dirname)) |
| 632 | shutil.rmtree(dirname) |
| 633 | |
| 634 | # Try creating an invalid directory. |
| 635 | testdir = '/sys/cannot/do/this/here' |
| 636 | self.assertRaises(CmdError, tools.PrepareOutputDir, testdir) |
| 637 | fname = tools.GetOutputFilename('fred') |
| 638 | self.assertRaises(IOError, tools.WriteFile, fname, |
| 639 | 'do you think at your age it is right?') |
| 640 | dirname = tools.outdir |
| 641 | tools.FinalizeOutputDir() |
| 642 | |
| 643 | def _OutputMock(self, level, msg, color=None): |
| 644 | self._level = level |
| 645 | self._msg = msg |
| 646 | self._color = color |
| 647 | |
| 648 | def testOutputSize(self): |
| 649 | """Test for OutputSize() function.""" |
| 650 | tools = self.tools |
| 651 | |
| 652 | # Rather than mocks, use a special Output object. |
| 653 | out = tools._out |
| 654 | out._Output = self._OutputMock |
| 655 | |
| 656 | tools.PrepareOutputDir(None) |
| 657 | fname = tools.GetOutputFilename('fred') |
| 658 | text_string = 'test of output size' |
| 659 | tools.WriteFile(fname, text_string) |
| 660 | |
| 661 | re_fname = re.compile('fred') |
| 662 | re_size = re.compile('.*size: (\d*)') |
| 663 | |
| 664 | tools.OutputSize('first', fname, level=cros_output.ERROR) |
| 665 | self.assertEqual(self._level, cros_output.ERROR) |
| 666 | self.assertTrue(re_fname.search(self._msg)) |
| 667 | self.assertEqual(self._color, None) |
| 668 | |
| 669 | # Check the default level, and that the filename length is given. |
| 670 | tools.OutputSize('second', fname) |
| 671 | self.assertEqual(self._level, cros_output.NOTICE) |
| 672 | self.assertTrue(re_fname.search(self._msg)) |
| 673 | self.assertEqual(self._color, None) |
| 674 | m = re_size.match(self._msg) |
| 675 | self.assertEqual(m.group(1), str(len(text_string))) |
| 676 | |
| 677 | tools.FinalizeOutputDir() |
| 678 | |
| 679 | |
| 680 | def _Test(argv): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 681 | """Run any built-in tests.""" |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 682 | unittest.main(argv=argv) |
Vadim Bendebury | 9ddeee1 | 2013-02-14 15:24:17 -0800 | [diff] [blame] | 683 | assert doctest.testmod().failed == 0 |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 684 | |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 685 | |
| 686 | def main(): |
| 687 | """Main function for tools. |
| 688 | |
| 689 | We provide a way to call a few of our useful functions. |
| 690 | |
| 691 | TODO(sjg) Move into the Chromite libraries when these are ready. |
| 692 | """ |
| 693 | parser = optparse.OptionParser() |
| 694 | parser.add_option('-v', '--verbosity', dest='verbosity', default=1, |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 695 | type='int', |
| 696 | help='Control verbosity: 0=silent, 1=progress, 3=full, ' |
| 697 | '4=debug') |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 698 | |
| 699 | help_str = '%s [options] cmd [args]\n\nAvailable commands:\n' % sys.argv[0] |
| 700 | help_str += '\tchromeos-version\tDisplay Chrome OS version' |
| 701 | parser.usage = help_str |
| 702 | |
| 703 | (options, args) = parser.parse_args(sys.argv) |
| 704 | args = args[1:] |
| 705 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 706 | out = cros_output.Output(options.verbosity) |
| 707 | tools = Tools(out) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 708 | if not args: |
| 709 | parser.error('No command provided') |
| 710 | elif args[0] == 'chromeos-version': |
| 711 | print tools.GetChromeosVersion() |
| 712 | else: |
| 713 | parser.error("Unknown command '%s'" % args[0]) |
| 714 | |
| 715 | if __name__ == '__main__': |
Simon Glass | c15a2cb | 2012-09-08 20:51:37 -0700 | [diff] [blame] | 716 | if sys.argv[1:2] == ['--test']: |
| 717 | _Test([sys.argv[0]] + sys.argv[2:]) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 718 | else: |
| 719 | main() |