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 | |
| 19 | import optparse |
| 20 | import os |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 21 | import shutil |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 22 | import sys |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 23 | import tempfile |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 24 | |
| 25 | from cros_build_lib import RunCommandCaptureOutput |
| 26 | import cros_build_lib |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 27 | import cros_output |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 28 | |
| 29 | |
| 30 | class CmdError(Exception): |
| 31 | """An error in the execution of a command.""" |
| 32 | pass |
| 33 | |
| 34 | |
| 35 | class 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 Glass | 290a180 | 2011-07-17 13:54:32 -0700 | [diff] [blame] | 39 | chroot. |
| 40 | |
| 41 | Public properties: |
| 42 | outdir: The output directory to write output files to. |
| 43 | |
| 44 | The tools class also provides common paths: |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 45 | |
| 46 | chroot_path: chroot directory |
| 47 | src_path: source directory |
| 48 | script_path: scripts directory (src/scripts) |
| 49 | overlay_path: overlays directory (src/overlays) |
| 50 | priv_overlay_path: private overlays directory (src/private-overlays) |
| 51 | board_path: build directory (/build in chroot) |
| 52 | third_party_path: third_parth directory (src/third_party) |
| 53 | cros_overlay_path: Chromium OS overlay (src/chromiumos-overlay) |
| 54 | """ |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 55 | def __init__(self, output): |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 56 | """Set up the tools system. |
| 57 | |
| 58 | Args: |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 59 | output: cros_output object to use for output. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 60 | """ |
| 61 | # Detect whether we're inside a chroot or not |
| 62 | self.in_chroot = cros_build_lib.IsInsideChroot() |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 63 | self._out = output |
Simon Glass | 64db306 | 2011-07-14 21:58:54 -0700 | [diff] [blame] | 64 | self._root = None |
| 65 | if self.in_chroot: |
| 66 | root_dir = os.getenv('CROS_WORKON_SRCROOT') |
| 67 | else: |
| 68 | repo = cros_build_lib.FindRepoDir() |
| 69 | if not repo: |
| 70 | raise IOError('Cannot find .repo directory (must be below cwd level)') |
| 71 | root_dir = os.path.dirname(repo) |
| 72 | self._SetRoot(root_dir) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 73 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 74 | self._out.Info("Chroot is at '%s'" % self.chroot_path) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 75 | self._tools = { |
| 76 | 'make_bmp_image' : '##/usr/share/vboot/bitmaps/make_bmp_images.sh' |
| 77 | } |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 78 | self.outdir = None # We have no output directory yet |
| 79 | self._delete_tempdir = None # And no temporary directory to delete |
| 80 | |
Simon Glass | 1f778c9 | 2011-08-09 13:30:43 -0700 | [diff] [blame^] | 81 | def __enter__(self): |
| 82 | return self |
| 83 | |
| 84 | def __exit__(self, type, value, traceback): |
| 85 | self.FinalizeOutputDir() |
| 86 | return False |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 87 | |
| 88 | def _SetRoot(self, root_dir): |
| 89 | """Sets the root directory for the build envionrment. |
| 90 | |
| 91 | The root directory is the one containing .repo, chroot and src. |
| 92 | |
| 93 | This should be called once the root is known. All other parts are |
| 94 | calculated from this. |
| 95 | |
| 96 | Args: |
Simon Glass | 05db7fd | 2011-07-14 22:02:19 -0700 | [diff] [blame] | 97 | root_dir: The path to the root directory. |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 98 | """ |
| 99 | self._root = os.path.normpath(root_dir) |
| 100 | |
| 101 | # Set up the path to prepend to get to the chroot |
| 102 | if self.in_chroot: |
| 103 | self.chroot_path = '/' |
| 104 | else: |
| 105 | self.chroot_path = cros_build_lib.PrependChrootPath('') |
| 106 | self.src_path = os.path.join(self._root, 'src') |
| 107 | self.script_path = os.path.join(self.src_path, 'scripts') |
| 108 | self.overlay_path = os.path.join(self.src_path, 'overlays') |
| 109 | self.priv_overlay_path = os.path.join(self.src_path, |
| 110 | 'private-overlays') |
| 111 | self.board_path = os.path.join(self.chroot_path, 'build') |
| 112 | self.third_party_path = os.path.join(self.src_path, 'third_party') |
| 113 | self.cros_overlay_path = os.path.join(self.third_party_path, |
| 114 | 'chromiumos-overlay') |
| 115 | |
| 116 | def Filename(self, fname): |
| 117 | """Resolve a chroot-relative filename to an absolute path. |
| 118 | |
| 119 | This looks for ## at the beginning of the filename, and changes it to |
| 120 | the chroot directory, which will be / if inside the chroot, or a path |
| 121 | to the chroot if not. |
| 122 | |
| 123 | Args: |
| 124 | fname: Filename to convert. |
| 125 | |
| 126 | Returns |
| 127 | Absolute path to filename. |
| 128 | """ |
| 129 | if fname.startswith('##/'): |
| 130 | fname = os.path.join(self.chroot_path, fname[3:]) |
| 131 | return fname |
| 132 | |
| 133 | def Run(self, tool, args, cwd=None): |
| 134 | """Run a tool with given arguments. |
| 135 | |
| 136 | The tool name may be used unchanged or substituted with a full path if |
| 137 | required. |
| 138 | |
| 139 | The tool and arguments can use ##/ to signify the chroot (at the beginning |
| 140 | of the tool/argument). |
| 141 | |
| 142 | Args: |
| 143 | tool: Name of tool to run. |
| 144 | args: List of arguments to pass to tool. |
| 145 | cwd: Directory to change into before running tool (None if none). |
| 146 | |
| 147 | Returns: |
| 148 | Output of tool (stdout). |
| 149 | |
| 150 | Raises |
| 151 | CmdError if running the tool, or the tool itself creates an error""" |
| 152 | if tool in self._tools: |
| 153 | tool = self._tools[tool] |
| 154 | tool = self.Filename(tool) |
| 155 | args = [self.Filename(arg) for arg in args] |
| 156 | cmd = [tool] + args |
| 157 | try: |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 158 | rc, stdout, err = RunCommandCaptureOutput(cmd, |
| 159 | print_cmd=self._out.verbose > 3, cwd=cwd) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 160 | except OSError: |
| 161 | raise CmdError('Command not found: %s' % (' '.join(cmd))) |
| 162 | if rc: |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 163 | raise CmdError('Command failed: %s\n%s' % (' '.join(cmd), stdout)) |
| 164 | self._out.Debug(stdout) |
| 165 | return stdout |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 166 | |
| 167 | def ReadFile(self, fname): |
| 168 | """Read and return the contents of a file. |
| 169 | |
| 170 | Args: |
| 171 | fname: path to filename to read, where ## signifiies the chroot. |
| 172 | |
| 173 | Returns: |
| 174 | data read from file, as a string. |
| 175 | """ |
| 176 | fd = open(self.Filename(fname), 'rb') |
| 177 | data = fd.read() |
| 178 | fd.close() |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 179 | self._out.Info("Read file '%s' size %d (%#0x)" % |
| 180 | (fname, len(data), len(data))) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 181 | return data |
| 182 | |
| 183 | def WriteFile(self, fname, data): |
| 184 | """Write data into a file. |
| 185 | |
| 186 | Args: |
| 187 | fname: path to filename to write, where ## signifiies the chroot. |
| 188 | data: data to write to file, as a string. |
| 189 | """ |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 190 | self._out.Info("Write file '%s' size %d (%#0x)" % |
| 191 | (fname, len(data), len(data))) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 192 | fd = open(self.Filename(fname), 'wb') |
| 193 | fd.write(data) |
| 194 | fd.close() |
| 195 | |
| 196 | def GetChromeosVersion(self): |
| 197 | """Returns the ChromeOS version string |
| 198 | |
| 199 | This works by finding and executing the version script: |
| 200 | |
| 201 | src/third_party/chromiumos-overlay/chromeos/config/chromeos_version.sh |
| 202 | |
| 203 | Returns: |
| 204 | Version string in the form '0.14.726.2011_07_07_1635' |
| 205 | |
| 206 | Raises: |
| 207 | CmdError: If the version script cannot be found, or is found but cannot |
| 208 | be executed. |
| 209 | """ |
| 210 | version_script = os.path.join(self.cros_overlay_path, 'chromeos', 'config', |
| 211 | 'chromeos_version.sh') |
| 212 | |
| 213 | if os.path.exists(version_script): |
| 214 | str = self.Run('sh', ['-c', '. %s >/dev/null; ' |
| 215 | 'echo ${CHROMEOS_VERSION_STRING}' |
| 216 | % version_script]) |
| 217 | return str.strip() |
| 218 | raise CmdError("Cannot find version script 'chromeos_version.sh'") |
| 219 | |
| 220 | def CheckTool(self, filename, ebuild=None): |
| 221 | """Check that the specified tool exists. |
| 222 | |
| 223 | If it does not exist in the PATH, then generate a useful error message |
| 224 | indicating how to install the ebuild that contains the required tool. |
| 225 | |
| 226 | Args: |
| 227 | filename: filename of tool to look for on path. |
| 228 | ebuild: name of ebuild which should be emerged to install this tool, |
| 229 | or None if it is the same as the filename. |
| 230 | |
| 231 | Raises: |
| 232 | CmdError(msg) if the tool is not found. |
| 233 | """ |
| 234 | try: |
| 235 | if filename in self._tools: |
| 236 | filename = self._tools[filename] |
| 237 | filename = self.Filename(filename) |
| 238 | self.Run('which', [filename]) |
| 239 | except CmdError as err: |
| 240 | raise CmdError("The '%s' utility was not found in your path. " |
| 241 | "Run the following command in \nyour chroot to install it: " |
| 242 | "sudo -E emerge %s" % (filename, ebuild or filename)) |
| 243 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 244 | def OutputSize(self, label, filename, level=cros_output.NOTICE): |
| 245 | """Display the filename and size of an object. |
| 246 | |
| 247 | Args: |
| 248 | label: Label for this file. |
| 249 | filename: Filename to output. |
| 250 | level: Verbosity level to attach to this message |
| 251 | """ |
| 252 | filename = self.Filename(filename) |
| 253 | size = os.stat(filename).st_size |
| 254 | self._out.DoOutput(level, "%s: %s; size: %d / %#x" % |
| 255 | (label, filename, size, size)) |
| 256 | |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 257 | def PrepareOutputDir(self, outdir, preserve=False): |
| 258 | """Select an output directory, ensuring it exists. |
| 259 | |
| 260 | This either creates a temporary directory or checks that the one supplied |
| 261 | by the user is valid. For a temporary directory, it makes a note to |
| 262 | remove it later if required. |
| 263 | |
| 264 | Args: |
| 265 | outdir: Output directory to use, or None to use a temporary dir. |
| 266 | |
| 267 | Raises: |
| 268 | OSError: If it cannot create the output directory. |
| 269 | """ |
| 270 | self.outdir = outdir |
| 271 | self.preserve_outdir = preserve |
| 272 | if self.outdir: |
| 273 | if not os.path.isdir(self.outdir): |
| 274 | try: |
| 275 | os.makedirs(self.outdir) |
| 276 | except OSError as err: |
| 277 | raise CmdError("Cannot make output directory '%s': '%s'" % |
| 278 | (self.outdir, err)) |
| 279 | |
| 280 | else: |
| 281 | self.outdir = tempfile.mkdtemp() |
| 282 | self._delete_tempdir = self.outdir |
Simon Glass | 290a180 | 2011-07-17 13:54:32 -0700 | [diff] [blame] | 283 | self._out.Debug("Using temporary directory '%s'" % |
| 284 | self._delete_tempdir) |
Simon Glass | 951a2db | 2011-07-17 15:58:58 -0700 | [diff] [blame] | 285 | |
| 286 | def FinalizeOutputDir(self): |
| 287 | """Tidy up the output direcory, deleting it if temporary""" |
| 288 | if self._delete_tempdir and not self.preserve_outdir: |
| 289 | shutil.rmtree(self._delete_tempdir) |
| 290 | self._out.Debug("Deleted temporary directory '%s'" % |
| 291 | self._delete_tempdir) |
| 292 | self._delete_tempdir = None |
| 293 | elif self.outdir: |
| 294 | self._out.Debug("Output directory '%s'" % self.outdir) |
| 295 | |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 296 | def _Test(): |
| 297 | """Run any built-in tests.""" |
| 298 | import doctest |
| 299 | doctest.testmod() |
| 300 | |
| 301 | def main(): |
| 302 | """Main function for tools. |
| 303 | |
| 304 | We provide a way to call a few of our useful functions. |
| 305 | |
| 306 | TODO(sjg) Move into the Chromite libraries when these are ready. |
| 307 | """ |
| 308 | parser = optparse.OptionParser() |
| 309 | parser.add_option('-v', '--verbosity', dest='verbosity', default=1, |
| 310 | type='int', help='Control verbosity: 0=silent, 1=progress, 3=full, ' |
| 311 | '4=debug') |
| 312 | |
| 313 | help_str = '%s [options] cmd [args]\n\nAvailable commands:\n' % sys.argv[0] |
| 314 | help_str += '\tchromeos-version\tDisplay Chrome OS version' |
| 315 | parser.usage = help_str |
| 316 | |
| 317 | (options, args) = parser.parse_args(sys.argv) |
| 318 | args = args[1:] |
| 319 | |
Simon Glass | ab344e3 | 2011-07-17 09:17:07 -0700 | [diff] [blame] | 320 | out = cros_output.Output(options.verbosity) |
| 321 | tools = Tools(out) |
Simon Glass | 50883f9 | 2011-07-12 16:19:16 -0700 | [diff] [blame] | 322 | if not args: |
| 323 | parser.error('No command provided') |
| 324 | elif args[0] == 'chromeos-version': |
| 325 | print tools.GetChromeosVersion() |
| 326 | else: |
| 327 | parser.error("Unknown command '%s'" % args[0]) |
| 328 | |
| 329 | if __name__ == '__main__': |
| 330 | if sys.argv[1:2] == ["--test"]: |
| 331 | _Test(*sys.argv[2:]) |
| 332 | else: |
| 333 | main() |