blob: dda8cf5766ee09eb64b9b69d14ec4a277e4d9adb [file] [log] [blame]
Doug Andersona9b10902011-02-01 17:54:31 -08001# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Utility functions shared between files in the chromite shell."""
6
7
8# Python imports
9import ConfigParser
10import cPickle
11import os
Doug Andersona9b10902011-02-01 17:54:31 -080012import tempfile
13
14
15# Local imports
16import chromite.lib.cros_build_lib as cros_lib
17from chromite.lib import text_menu
18
19
20# Find the Chromite root and Chromium OS root... Note: in the chroot we may
21# choose to install Chromite somewhere (/usr/lib/chromite?), so we use the
22# environment variable to get the right place if it exists.
23CHROMITE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
24SRCROOT_PATH = os.environ.get('CROS_WORKON_SRCROOT',
25 os.path.realpath(os.path.join(CHROMITE_PATH,
26 '..')))
27
28
29# Commands can take one of these two types of specs. Note that if a command
30# takes a build spec, we will find the associated chroot spec. This should be
31# a human-readable string. It is printed and also is the name of the spec
32# directory.
33BUILD_SPEC_TYPE = 'build'
34CHROOT_SPEC_TYPE = 'chroot'
35
36
37# This is a special target that indicates that you want to do something just
38# to the host. This means different things to different commands.
39# TODO(dianders): Good idea or bad idea?
40_HOST_TARGET = 'HOST'
41
42
43def GetBoardDir(build_config):
44 """Returns the board directory (inside the chroot) given the name.
45
46 Args:
47 build_config: A SafeConfigParser representing the config that we're
48 building.
49
50 Returns:
51 The absolute path to the board.
52 """
Doug Anderson6f530612011-02-11 13:19:25 -080053 target_name = build_config.get('DEFAULT', 'target')
Doug Andersona9b10902011-02-01 17:54:31 -080054
55 # Extra checks on these, since we sometimes might do a rm -f on the board
56 # directory and these could cause some really bad behavior.
57 assert target_name, "Didn't expect blank target name."
58 assert len(target_name.split()) == 1, 'Target name should have no whitespace.'
59
60 return os.path.join('/', 'build', target_name)
61
62
63def GetChrootAbsDir(chroot_config):
64 """Returns the absolute chroot directory the chroot config.
65
66 Args:
67 chroot_config: A SafeConfigParser representing the config for the chroot.
68
69 Returns:
70 The chroot directory, always absolute.
71 """
72 chroot_dir = chroot_config.get('CHROOT', 'path')
73 chroot_dir = os.path.join(SRCROOT_PATH, chroot_dir)
74
75 return chroot_dir
76
77
78def DoesChrootExist(chroot_config):
79 """Return True if the chroot folder exists.
80
81 Args:
82 chroot_config: A SafeConfigParser representing the config for the chroot.
83
84 Returns:
85 True if the chroot folder exists.
86 """
87 chroot_dir = GetChrootAbsDir(chroot_config)
88 return os.path.isdir(chroot_dir)
89
90
91def FindSpec(spec_name, spec_type=BUILD_SPEC_TYPE, can_show_ui=True):
92 """Find the spec with the given name.
93
94 This tries to be smart about helping the user to find the right spec. See
95 the spec_name parameter for details.
96
97 Args:
98 spec_name: Can be any of the following:
99 1. A full path to a spec file (including the .spec suffix). This is
100 checked first (i.e. if os.path.isfile(spec_name), we assume we've
101 got this case).
102 2. The full name of a spec file somewhere in the spec search path
103 (not including the .spec suffix). This is checked second. Putting
104 this check second means that if one spec name is a substring of
105 another, you can still specify the shorter spec name and know you
106 won't get a menu (the exact match prevents the menu).
107 3. A substring that will be used to pare-down a menu of spec files
108 found in the spec search path. Can be the empty string to show a
109 menu of all spec files in the spec path. NOTE: Only possible if
110 can_show_ui is True.
111 spec_type: The type of spec this is: 'chroot' or 'build'.
112 can_show_ui: If True, enables the spec name to be a substring since we can
113 then show a menu if the substring matches more than one thing.
114
115 Returns:
116 A path to the spec file.
117 """
118 spec_suffix = '.spec'
119
120 # Handle 'HOST' for spec name w/ no searching, so it counts as an exact match.
121 if spec_type == BUILD_SPEC_TYPE and spec_name == _HOST_TARGET.lower():
122 return _HOST_TARGET
123
124 # If we have an exact path name, that's it. No searching.
125 if os.path.isfile(spec_name):
126 return spec_name
127
128 # Figure out what our search path should be.
129 # ...make these lists in anticipation of the need to support specs that live
130 # in private overlays.
131 # TODO(dianders): Should specs be part of the shell, or part of the main
132 # chromite?
133 search_path = [
134 os.path.join(CHROMITE_PATH, 'specs', spec_type),
135 ]
136
137 # Look for an exact match of a spec name. An exact match will go through with
138 # no menu.
139 if spec_name:
140 for dir_path in search_path:
141 spec_path = os.path.join(dir_path, spec_name + spec_suffix)
142 if os.path.isfile(spec_path):
143 return spec_path
144
145 # cros_lib.Die right away if we can't show UI and didn't have an exact match.
146 if not can_show_ui:
147 cros_lib.Die("Couldn't find %s spec: %s" % (spec_type, spec_name))
148
149 # No full path and no exact match. Move onto a menu.
150 # First step is to construct the options. We'll store in a dict keyed by
151 # spec name.
152 options = {}
153 for dir_path in search_path:
154 for file_name in os.listdir(dir_path):
155 # Find any files that end with ".spec" in a case-insensitive manner.
156 if not file_name.lower().endswith(spec_suffix):
157 continue
158
159 this_spec_name, _ = os.path.splitext(file_name)
160
161 # Skip if this spec file doesn't contain our substring. We are _always_
162 # case insensitive here to be helpful to the user.
163 if spec_name.lower() not in this_spec_name.lower():
164 continue
165
166 # Disallow the spec to appear twice in the search path. This is the
167 # safest thing for now. We could revisit it later if we ever found a
168 # good reason (and if we ever have more than one directory in the
169 # search path).
170 if this_spec_name in options:
171 cros_lib.Die('Spec %s was found in two places in the search path' %
172 this_spec_name)
173
174 # Combine to get a full path.
175 full_path = os.path.join(dir_path, file_name)
176
177 # Ignore directories or anything else that isn't a file.
178 if not os.path.isfile(full_path):
179 continue
180
181 # OK, it's good. Store the path.
182 options[this_spec_name] = full_path
183
184 # Add 'HOST'. All caps so it sorts first.
185 if (spec_type == BUILD_SPEC_TYPE and
186 spec_name.lower() in _HOST_TARGET.lower()):
187 options[_HOST_TARGET] = _HOST_TARGET
188
189 # If no match, die.
190 if not options:
191 cros_lib.Die("Couldn't find any matching %s specs for: %s" % (spec_type,
192 spec_name))
193
194 # Avoid showing the user a menu if the user's search string matched exactly
195 # one item.
196 if spec_name and len(options) == 1:
197 _, spec_path = options.popitem()
198 return spec_path
199
200 # If more than one, show a menu...
201 option_keys = sorted(options.iterkeys())
202 choice = text_menu.TextMenu(option_keys, 'Choose a %s spec' % spec_type)
203
204 if choice is None:
205 cros_lib.Die('OK, cancelling...')
206 else:
207 return options[option_keys[choice]]
208
209
210def ReadConfig(spec_path):
211 """Read the a build config or chroot config from a spec file.
212
213 This includes adding thue proper _default stuff in.
214
215 Args:
216 spec_path: The path to the build or chroot spec.
217
218 Returns:
219 config: A SafeConfigParser representing the config.
220 """
221 spec_name, _ = os.path.splitext(os.path.basename(spec_path))
222 spec_dir = os.path.dirname(spec_path)
223
224 config = ConfigParser.SafeConfigParser({'name': spec_name})
225 config.read(os.path.join(spec_dir, '_defaults'))
226 config.read(spec_path)
227
228 return config
229
230
231def GetBuildConfigFromArgs(argv):
232 """Helper for commands that take a build config in the arg list.
233
234 This function can cros_lib.Die() in some instances.
235
236 Args:
237 argv: A list of command line arguments. If non-empty, [0] should be the
238 build spec. These will not be modified.
239
240 Returns:
241 argv: The argv with the build spec stripped off. This might be argv[1:] or
242 just argv. Not guaranteed to be new memory.
243 build_config: The SafeConfigParser for the build config; might be None if
244 this is a host config. TODO(dianders): Should there be a build spec for
245 the host?
246 """
247 # The spec name is optional. If no arguments, we'll show a menu...
248 # Note that if there are arguments, but the first argument is a flag, we'll
249 # assume that we got called before OptionParser. In that case, they might
250 # have specified options but still want the board menu.
251 if argv and not argv[0].startswith('-'):
252 spec_name = argv[0]
253 argv = argv[1:]
254 else:
255 spec_name = ''
256
257 spec_path = FindSpec(spec_name)
258
259 if spec_path == _HOST_TARGET:
260 return argv, None
261
262 build_config = ReadConfig(spec_path)
263
264 # TODO(dianders): Add a config checker class that makes sure that the
265 # target is not a blank string. Might also be good to make sure that the
266 # target has no whitespace (so we don't screw up a subcommand invoked
267 # through a shell).
268
269 return argv, build_config
270
271
Doug Andersona17e0102011-02-07 13:16:55 -0800272def EnterChroot(chroot_config, func, *args, **kwargs):
Doug Andersona9b10902011-02-01 17:54:31 -0800273 """Re-run the given function inside the chroot.
274
275 When the function is run, it will be run in a SEPARATE INSTANCE of chromite,
276 which will be run in the chroot. This is a little weird. Specifically:
277 - When the callee executes, it will be a separate python instance.
278 - Globals will be reset back to defaults.
279 - A different version of python (with different modules) may end up running
280 the script in the chroot.
281 - All arguments are pickled up into a temp file, whose path is passed on the
282 command line.
283 - That means that args must be pickleable.
284 - It also means that modifications to the parameters by the callee are not
285 visible to the caller.
286 - Even the function is "pickled". The way the pickle works, I belive, is it
287 just passes the name of the function. If this name somehow resolves
288 differently in the chroot, you may get weirdness.
289 - Since we're in the chroot, obviously files may have different paths. It's
290 up to you to convert parameters if you need to.
291 - The stdin, stdout, and stderr aren't touched.
292
293 Args:
294 chroot_config: A SafeConfigParser representing the config for the chroot.
Doug Andersona17e0102011-02-07 13:16:55 -0800295 func: Either: a) the function to call or b) A tuple of an object and the
Doug Andersona9b10902011-02-01 17:54:31 -0800296 name of the method to call.
297 args: All other arguments will be passed to the function as is.
298 kwargs: All other arguments will be passed to the function as is.
299 """
300 # Make sure that the chroot exists...
301 chroot_dir = GetChrootAbsDir(chroot_config)
302 if not DoesChrootExist(chroot_config):
303 cros_lib.Die(
304 'Chroot dir does not exist; try the "build host" command.\n %s.' %
305 chroot_dir)
306
Doug Andersona9b10902011-02-01 17:54:31 -0800307 # Save state to a temp file (inside the chroot!) using pickle.
308 tmp_dir = os.path.join(chroot_dir, 'tmp')
309 state_file = tempfile.NamedTemporaryFile(prefix='chromite', dir=tmp_dir)
310 try:
Doug Andersona17e0102011-02-07 13:16:55 -0800311 cPickle.dump((func, args, kwargs), state_file, cPickle.HIGHEST_PROTOCOL)
Doug Andersona9b10902011-02-01 17:54:31 -0800312 state_file.flush()
313
314 # Translate temp file name into a chroot path...
315 chroot_state_path = os.path.join('/tmp', os.path.basename(state_file.name))
316
317 # Put together command. We're going to force the shell to do all of the
318 # splitting of arguments, since we're throwing all of the flags from the
319 # config file in there.
320 # TODO(dianders): It might be nice to run chromite as:
321 # python -m chromite.chromite_main
322 # ...but, at the moment, that fails if you're in src/scripts
323 # which already has a chromite folder.
324 cmd = (
325 './enter_chroot.sh --chroot="%s" %s --'
326 ' python ../../chromite/shell/main.py --resume-state %s') % (
327 chroot_dir,
328 chroot_config.get('CHROOT', 'enter_chroot_flags'),
329 chroot_state_path)
330
331 # We'll put CWD as src/scripts when running the command. Since everyone
332 # running by hand has their cwd there, it is probably the safest.
333 cwd = os.path.join(SRCROOT_PATH, 'src', 'scripts')
334
335 # Run it. We allow "error" so we don't print a confusing error message
336 # filled with out resume-state garbage on control-C.
337 cmd_result = cros_lib.RunCommand(cmd, shell=True, cwd=cwd, print_cmd=False,
338 exit_code=True, error_ok=True, ignore_sigint=True)
339
340 if cmd_result.returncode:
341 cros_lib.Die('Chroot exited with error code %d' % cmd_result.returncode)
342 finally:
343 # Make sure things get closed (and deleted), even upon exception.
344 state_file.close()
345
346
347def ResumeEnterChrootIfNeeded(argv):
348 """Should be called as the first line in main() to support EnterChroot().
349
350 We'll check whether the --resume-state argument is there. If the argument is
351 there, we'll use it and call the proper function (now that we're in the
352 chroot). We'll then return True to indicate to main() that it should exit
353 immediately.
354
355 If the --resume-state argument is not there, this function will return False
356 without doing anything else.
357
358 Args:
359 argv: The value of sys.argv
360
361 Returns:
362 True if we resumed; indicates that main should return without doing any
363 further work.
364 """
365 if argv[1:2] == ['--resume-state']:
366 # Internal mechanism (not documented to users) to resume in the chroot.
367 # ...actual resume state file is passed in argv[2] for simplicity...
368 assert len(argv) == 3, 'Resume State not passed properly.'
Doug Andersona17e0102011-02-07 13:16:55 -0800369 func, args, kwargs = cPickle.load(open(argv[2], 'rb'))
Doug Andersona9b10902011-02-01 17:54:31 -0800370
371 # Handle calling a method in a class; that can't be directly pickled.
Doug Andersona17e0102011-02-07 13:16:55 -0800372 if isinstance(func, tuple):
373 obj, method = func
374 func = getattr(obj, method)
Doug Andersona9b10902011-02-01 17:54:31 -0800375
Doug Andersona17e0102011-02-07 13:16:55 -0800376 func(*args, **kwargs) # pylint: disable=W0142
Doug Andersona9b10902011-02-01 17:54:31 -0800377
378 # Return True to tell main() that it should exit.
379 return True
380 else:
381 # Return False to tell main() that we didn't find the --resume-state
382 # argument and that it should do normal arugment parsing.
383 return False