Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / cros / commands / cros_chrome_sdk.py
1 # Copyright (c) 2012 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 """The cros chrome-sdk command for the simple chrome workflow."""
6
7 from __future__ import print_function
8
9 import argparse
10 import collections
11 import contextlib
12 import json
13 import logging
14 import os
15 import distutils.version
16
17 from chromite import cros
18 from chromite.lib import cache
19 from chromite.lib import chrome_util
20 from chromite.lib import commandline
21 from chromite.lib import cros_build_lib
22 from chromite.lib import git
23 from chromite.lib import gs
24 from chromite.lib import osutils
25 from chromite.lib import stats
26 from chromite.cbuildbot import constants
27
28
29 COMMAND_NAME = 'chrome-sdk'
30 CUSTOM_VERSION = 'custom'
31
32
33 def Log(*args, **kwargs):
34   """Conditional logging.
35
36   Args:
37     silent: If set to True, then logs with level DEBUG.  logs with level INFO
38       otherwise.  Defaults to False.
39   """
40   silent = kwargs.pop('silent', False)
41   level = logging.DEBUG if silent else logging.INFO
42   logging.log(level, *args, **kwargs)
43
44
45 class MissingSDK(Exception):
46   """Error thrown when we cannot find an SDK."""
47
48   def __init__(self, board, version=None):
49     msg = 'Cannot find SDK for %r' % (board,)
50     if version is not None:
51       msg += ' with version %s' % (version,)
52     Exception.__init__(self, msg)
53
54
55 class SDKFetcher(object):
56   """Functionality for fetching an SDK environment.
57
58   For the version of ChromeOS specified, the class downloads and caches
59   SDK components.
60   """
61   SDK_BOARD_ENV = '%SDK_BOARD'
62   SDK_PATH_ENV = '%SDK_PATH'
63   SDK_VERSION_ENV = '%SDK_VERSION'
64
65   SDKContext = collections.namedtuple(
66       'SDKContext', ['version', 'target_tc', 'key_map'])
67
68   TARBALL_CACHE = 'tarballs'
69   MISC_CACHE = 'misc'
70
71   TARGET_TOOLCHAIN_KEY = 'target_toolchain'
72
73   def __init__(self, cache_dir, board, clear_cache=False, chrome_src=None,
74                sdk_path=None, toolchain_path=None, silent=False):
75     """Initialize the class.
76
77     Args:
78       cache_dir: The toplevel cache dir to use.
79       board: The board to manage the SDK for.
80       clear_cache: Clears the sdk cache during __init__.
81       chrome_src: The location of the chrome checkout.  If unspecified, the
82         cwd is presumed to be within a chrome checkout.
83       sdk_path: The path (whether a local directory or a gs:// path) to fetch
84         SDK components from.
85       toolchain_path: The path (whether a local directory or a gs:// path) to
86         fetch toolchain components from.
87       silent: If set, the fetcher prints less output.
88     """
89     # Delay this import because it is super slow.  http://crbug.com/404575
90     from chromite.cbuildbot import cbuildbot_config
91
92     self.cache_base = os.path.join(cache_dir, COMMAND_NAME)
93     if clear_cache:
94       logging.warning('Clearing the SDK cache.')
95       osutils.RmDir(self.cache_base, ignore_missing=True)
96     self.tarball_cache = cache.TarballCache(
97         os.path.join(self.cache_base, self.TARBALL_CACHE))
98     self.misc_cache = cache.DiskCache(
99         os.path.join(self.cache_base, self.MISC_CACHE))
100     self.board = board
101     self.config = cbuildbot_config.FindCanonicalConfigForBoard(board)
102     self.gs_base = '%s/%s' % (constants.DEFAULT_ARCHIVE_BUCKET,
103                               self.config['name'])
104     self.clear_cache = clear_cache
105     self.chrome_src = chrome_src
106     self.sdk_path = sdk_path
107     self.toolchain_path = toolchain_path
108     self.silent = silent
109
110     # For external configs, there is no need to run 'gsutil config', because
111     # the necessary files are all accessible to anonymous users.
112     internal = self.config['internal']
113     self.gs_ctx = gs.GSContext(cache_dir=cache_dir, init_boto=internal)
114
115     if self.sdk_path is None:
116       self.sdk_path = os.environ.get(self.SDK_PATH_ENV)
117
118     if self.toolchain_path is None:
119       self.toolchain_path = 'gs://%s' % constants.SDK_GS_BUCKET
120
121   def _UpdateTarball(self, url, ref):
122     """Worker function to fetch tarballs"""
123     with osutils.TempDir(base_dir=self.tarball_cache.staging_dir) as tempdir:
124       local_path = os.path.join(tempdir, os.path.basename(url))
125       Log('SDK: Fetching %s', url, silent=self.silent)
126       self.gs_ctx.Copy(url, tempdir, debug_level=logging.DEBUG)
127       ref.SetDefault(local_path, lock=True)
128
129   def _GetMetadata(self, version):
130     """Return metadata (in the form of a dict) for a given version."""
131     raw_json = None
132     version_base = self._GetVersionGSBase(version)
133     with self.misc_cache.Lookup(
134         self._GetCacheKeyForComponent(version, constants.METADATA_JSON)) as ref:
135       if ref.Exists(lock=True):
136         raw_json = osutils.ReadFile(ref.path)
137       else:
138         metadata_path = os.path.join(version_base, constants.METADATA_JSON)
139         partial_metadata_path = os.path.join(version_base,
140                                              constants.PARTIAL_METADATA_JSON)
141         try:
142           raw_json = self.gs_ctx.Cat(metadata_path,
143                                      debug_level=logging.DEBUG)
144         except gs.GSNoSuchKey:
145           logging.info('Could not read %s, falling back to %s',
146                        metadata_path, partial_metadata_path)
147           raw_json = self.gs_ctx.Cat(partial_metadata_path,
148                                      debug_level=logging.DEBUG)
149
150         ref.AssignText(raw_json)
151
152     return json.loads(raw_json)
153
154   def _GetChromeLKGM(self, chrome_src_dir):
155     """Get ChromeOS LKGM checked into the Chrome tree.
156
157     Returns:
158       Version number in format '3929.0.0'.
159     """
160     version = osutils.ReadFile(os.path.join(
161         chrome_src_dir, constants.PATH_TO_CHROME_LKGM))
162     return version
163
164   def _GetRepoCheckoutVersion(self, repo_root):
165     """Get the version specified in chromeos_version.sh.
166
167     Returns:
168       Version number in format '3929.0.0'.
169     """
170     chromeos_version_sh = os.path.join(repo_root, constants.VERSION_FILE)
171     sourced_env = osutils.SourceEnvironment(
172         chromeos_version_sh, ['CHROMEOS_VERSION_STRING'],
173         env={'CHROMEOS_OFFICIAL': '1'})
174     return sourced_env['CHROMEOS_VERSION_STRING']
175
176   def _GetNewestFullVersion(self, version=None):
177     """Gets the full version number of the latest build for the given |version|.
178
179     Args:
180       version: The version number or branch to look at. By default, look at
181         builds on the current branch.
182
183     Returns:
184       Version number in the format 'R30-3929.0.0'.
185     """
186     if version is None:
187       version = git.GetChromiteTrackingBranch()
188     version_file = '%s/LATEST-%s' % (self.gs_base, version)
189     try:
190       full_version = self.gs_ctx.Cat(version_file)
191       assert full_version.startswith('R')
192       return full_version
193     except gs.GSNoSuchKey:
194       return None
195
196   def _GetNewestManifestVersion(self):
197     """Gets the latest uploaded SDK version.
198
199     Returns:
200       Version number in the format '3929.0.0'.
201     """
202     full_version = self._GetNewestFullVersion()
203     return None if full_version is None else full_version.split('-')[1]
204
205   def GetDefaultVersion(self):
206     """Get the default SDK version to use.
207
208     If we are in an existing SDK shell, the default version will just be
209     the current version. Otherwise, we will try to calculate the
210     appropriate version to use based on the checkout.
211     """
212     if os.environ.get(self.SDK_BOARD_ENV) == self.board:
213       sdk_version = os.environ.get(self.SDK_VERSION_ENV)
214       if sdk_version is not None:
215         return sdk_version
216
217     with self.misc_cache.Lookup((self.board, 'latest')) as ref:
218       if ref.Exists(lock=True):
219         version = osutils.ReadFile(ref.path).strip()
220         # Deal with the old version format.
221         if version.startswith('R'):
222           version = version.split('-')[1]
223         return version
224       else:
225         return None
226
227   def _SetDefaultVersion(self, version):
228     """Set the new default version."""
229     with self.misc_cache.Lookup((self.board, 'latest')) as ref:
230       ref.AssignText(version)
231
232   def UpdateDefaultVersion(self):
233     """Update the version that we default to using.
234
235     Returns:
236       A tuple of the form (version, updated), where |version| is the
237       version number in the format '3929.0.0', and |updated| indicates
238       whether the version was indeed updated.
239     """
240     checkout_dir = self.chrome_src if self.chrome_src else os.getcwd()
241     checkout = commandline.DetermineCheckout(checkout_dir)
242     current = self.GetDefaultVersion() or '0'
243     if checkout.chrome_src_dir:
244       target = self._GetChromeLKGM(checkout.chrome_src_dir)
245     elif checkout.type == commandline.CHECKOUT_TYPE_REPO:
246       target = self._GetRepoCheckoutVersion(checkout.root)
247       if target != current:
248         lv_cls = distutils.version.LooseVersion
249         if lv_cls(target) > lv_cls(current):
250           # Hit the network for the newest uploaded version for the branch.
251           newest = self._GetNewestManifestVersion()
252           # The SDK for the version of the checkout has not been uploaded yet,
253           # so fall back to the latest uploaded SDK.
254           if newest is not None and lv_cls(target) > lv_cls(newest):
255             target = newest
256     else:
257       target = self._GetNewestManifestVersion()
258
259     if target is None:
260       raise MissingSDK(self.board)
261
262     self._SetDefaultVersion(target)
263     return target, target != current
264
265   def GetFullVersion(self, version):
266     """Add the release branch to a ChromeOS platform version.
267
268     Args:
269       version: A ChromeOS platform number of the form XXXX.XX.XX, i.e.,
270         3918.0.0.
271
272     Returns:
273       The version with release branch prepended.  I.e., R28-3918.0.0.
274     """
275     assert not version.startswith('R')
276
277     with self.misc_cache.Lookup(('full-version', version)) as ref:
278       if ref.Exists(lock=True):
279         return osutils.ReadFile(ref.path).strip()
280       else:
281         # Find out the newest version from the LATEST (or LATEST-%s) file.
282         full_version = self._GetNewestFullVersion(version=version)
283
284         if full_version is None:
285           raise MissingSDK(self.board, version)
286
287         ref.AssignText(full_version)
288         return full_version
289
290   def _GetVersionGSBase(self, version):
291     """The base path of the SDK for a particular version."""
292     if self.sdk_path is not None:
293       return self.sdk_path
294
295     full_version = self.GetFullVersion(version)
296     return os.path.join(self.gs_base, full_version)
297
298   def _GetCacheKeyForComponent(self, version, component):
299     """Builds the cache key tuple for an SDK component."""
300     version_section = version
301     if self.sdk_path is not None:
302       version_section = self.sdk_path.replace('/', '__').replace(':', '__')
303     return (self.board, version_section, component)
304
305   @contextlib.contextmanager
306   def Prepare(self, components, version=None, target_tc=None,
307               toolchain_url=None):
308     """Ensures the components of an SDK exist and are read-locked.
309
310     For a given SDK version, pulls down missing components, and provides a
311     context where the components are read-locked, which prevents the cache from
312     deleting them during its purge operations.
313
314     If both target_tc and toolchain_url arguments are provided, then this
315     does not download metadata.json for the given version. Otherwise, this
316     function requires metadata.json for the given version to exist.
317
318     Args:
319       gs_ctx: GSContext object.
320       components: A list of specific components(tarballs) to prepare.
321       version: The version to prepare.  If not set, uses the version returned by
322         GetDefaultVersion().  If there is no default version set (this is the
323         first time we are being executed), then we update the default version.
324       target_tc: Target toolchain name to use, e.g. x86_64-cros-linux-gnu
325       toolchain_url: Format pattern for path to fetch toolchain from,
326         e.g. 2014/04/%(target)s-2014.04.23.220740.tar.xz
327
328     Yields:
329       An SDKFetcher.SDKContext namedtuple object.  The attributes of the
330       object are:
331         version: The version that was prepared.
332         target_tc: Target toolchain name.
333         key_map: Dictionary that contains CacheReference objects for the SDK
334           artifacts, indexed by cache key.
335     """
336     if version is None and self.sdk_path is None:
337       version = self.GetDefaultVersion()
338       if version is None:
339         version, _ = self.UpdateDefaultVersion()
340     components = list(components)
341
342     key_map = {}
343     fetch_urls = {}
344
345     if not target_tc or not toolchain_url:
346       metadata = self._GetMetadata(version)
347       target_tc = target_tc or metadata['toolchain-tuple'][0]
348       toolchain_url = toolchain_url or metadata['toolchain-url']
349
350     # Fetch toolchains from separate location.
351     if self.TARGET_TOOLCHAIN_KEY in components:
352       fetch_urls[self.TARGET_TOOLCHAIN_KEY] = os.path.join(
353           self.toolchain_path, toolchain_url % {'target': target_tc})
354       components.remove(self.TARGET_TOOLCHAIN_KEY)
355
356     version_base = self._GetVersionGSBase(version)
357     fetch_urls.update((t, os.path.join(version_base, t)) for t in components)
358     try:
359       for key, url in fetch_urls.iteritems():
360         cache_key = self._GetCacheKeyForComponent(version, key)
361         ref = self.tarball_cache.Lookup(cache_key)
362         key_map[key] = ref
363         ref.Acquire()
364         if not ref.Exists(lock=True):
365           # TODO(rcui): Parallelize this.  Requires acquiring locks *before*
366           # generating worker processes; therefore the functionality needs to
367           # be moved into the DiskCache class itself -
368           # i.e.,DiskCache.ParallelSetDefault().
369           self._UpdateTarball(url, ref)
370
371       ctx_version = version
372       if self.sdk_path is not None:
373         ctx_version = CUSTOM_VERSION
374       yield self.SDKContext(ctx_version, target_tc, key_map)
375     finally:
376       # TODO(rcui): Move to using cros_build_lib.ContextManagerStack()
377       cros_build_lib.SafeRun([ref.Release for ref in key_map.itervalues()])
378
379
380 class GomaError(Exception):
381   """Indicates error with setting up Goma."""
382
383
384 @cros.CommandDecorator(COMMAND_NAME)
385 class ChromeSDKCommand(cros.CrosCommand):
386   """Set up an environment for building Chrome on Chrome OS.
387
388   Pulls down SDK components for building and testing Chrome for Chrome OS,
389   sets up the environment for building Chrome, and runs a command in the
390   environment, starting a bash session if no command is specified.
391
392   The bash session environment is set up by a user-configurable rc file located
393   at ~/.chromite/chrome_sdk.bashrc.
394   """
395
396   # Note, this URL is not accessible outside of corp.
397   _GOMA_URL = ('https://clients5.google.com/cxx-compiler-service/'
398                'download/goma_ctl.py')
399
400   _CLANG_DIR = 'third_party/llvm-build/Release+Asserts/bin'
401
402   EBUILD_ENV = (
403       'CXX',
404       'CC',
405       'AR',
406       'AS',
407       'LD',
408       'RANLIB',
409       'GOLD_SET',
410       'GYP_DEFINES',
411   )
412
413   SDK_GOMA_PORT_ENV = 'SDK_GOMA_PORT'
414   SDK_GOMA_DIR_ENV = 'SDK_GOMA_DIR'
415
416   GOMACC_PORT_CMD  = ['./gomacc', 'port']
417   FETCH_GOMA_CMD  = ['wget', _GOMA_URL]
418
419   # Override base class property to enable stats upload.
420   upload_stats = True
421
422   # Override base class property to use cache related commandline options.
423   use_caching_options = True
424
425   @property
426   def upload_stats_timeout(self):
427     # Give a longer timeout for interactive SDK shell invocations, since the
428     # user will not notice a longer wait because it's happening in the
429     # background.
430     if self.options.cmd:
431       return super(ChromeSDKCommand, self).upload_stats_timeout
432     else:
433       return stats.StatsUploader.UPLOAD_TIMEOUT
434
435   @staticmethod
436   def ValidateVersion(version):
437     if version.startswith('R') or len(version.split('.')) != 3:
438       raise argparse.ArgumentTypeError(
439           '--version should be in the format 3912.0.0')
440     return version
441
442   @classmethod
443   def AddParser(cls, parser):
444     super(ChromeSDKCommand, cls).AddParser(parser)
445     parser.add_argument(
446         '--board', required=True, help='The board SDK to use.')
447     parser.add_argument(
448         '--bashrc', type=osutils.ExpandPath,
449         default=constants.CHROME_SDK_BASHRC,
450         help='A bashrc file used to set up the SDK shell environment. '
451              'Defaults to %s.' % constants.CHROME_SDK_BASHRC)
452     parser.add_argument(
453         '--chroot', type=osutils.ExpandPath,
454         help='Path to a ChromeOS chroot to use.  If set, '
455              '<chroot>/build/<board> will be used as the sysroot that Chrome '
456              'is built against.  The version shown in the SDK shell prompt '
457              'will then have an asterisk prepended to it.')
458     parser.add_argument(
459         '--chrome-src', type=osutils.ExpandPath,
460         help='Specifies the location of a Chrome src/ directory.  Required if '
461              'running with --clang if not running from a Chrome checkout.')
462     parser.add_argument(
463         '--clang', action='store_true', default=False,
464         help='Sets up the environment for building with clang.')
465     parser.add_argument(
466         '--cwd', type=osutils.ExpandPath,
467         help='Specifies a directory to switch to after setting up the SDK '
468              'shell.  Defaults to the current directory.')
469     parser.add_argument(
470         '--internal', action='store_true', default=False,
471         help='Sets up SDK for building official (internal) Chrome '
472              'Chrome, rather than Chromium.')
473     parser.add_argument(
474         '--sdk-path', type='local_or_gs_path',
475         help='Provides a path, whether a local directory or a gs:// path, to '
476              'pull SDK components from.')
477     parser.add_argument(
478         '--toolchain-path', type='local_or_gs_path',
479         help='Provides a path, whether a local directory or a gs:// path, to '
480              'pull toolchain components from.')
481     parser.add_argument(
482         '--nogoma', action='store_false', default=True, dest='goma',
483         help="Disables Goma in the shell by removing it from the PATH.")
484     parser.add_argument(
485         '--version', default=None, type=cls.ValidateVersion,
486         help="Specify version of SDK to use, in the format '3912.0.0'.  "
487              "Defaults to determining version based on the type of checkout "
488              "(Chrome or ChromeOS) you are executing from.")
489     parser.add_argument(
490         'cmd', nargs='*', default=None,
491         help='The command to execute in the SDK environment.  Defaults to '
492               'starting a bash shell.')
493
494     parser.add_option_to_group(
495         parser.caching_group, '--clear-sdk-cache', action='store_true',
496         default=False,
497         help='Removes everything in the SDK cache before starting.')
498
499     group = parser.add_option_group('Metadata Overrides (Advanced)',
500         description='Provide all of these overrides in order to remove '
501                     'dependencies on metadata.json existence.')
502     parser.add_option_to_group(
503         group, '--target-tc', action='store', default=None,
504         help='Override target toolchain name, e.g. x86_64-cros-linux-gnu')
505     parser.add_option_to_group(
506         group, '--toolchain-url', action='store', default=None,
507         help='Override toolchain url format pattern, e.g. '
508              '2014/04/%%(target)s-2014.04.23.220740.tar.xz')
509
510   def __init__(self, options):
511     cros.CrosCommand.__init__(self, options)
512     self.board = options.board
513     # Lazy initialized.
514     self.sdk = None
515     # Initialized later based on options passed in.
516     self.silent = True
517
518   @staticmethod
519   def _CreatePS1(board, version, chroot=None):
520     """Returns PS1 string that sets commandline and xterm window caption.
521
522     If a chroot path is set, then indicate we are using the sysroot from there
523     instead of the stock sysroot by prepending an asterisk to the version.
524
525     Args:
526       board: The SDK board.
527       version: The SDK version.
528       chroot: The path to the chroot, if set.
529     """
530     custom = '*' if chroot else ''
531     sdk_version = '(sdk %s %s%s)' % (board, custom, version)
532     label = '\\u@\\h: \\w'
533     window_caption = "\\[\\e]0;%(sdk_version)s %(label)s \\a\\]"
534     command_line = "%(sdk_version)s \\[\\e[1;33m\\]%(label)s \\$ \\[\\e[m\\]"
535     ps1 = window_caption + command_line
536     return (ps1 % {'sdk_version': sdk_version,
537                    'label': label})
538
539   def _FixGoldPath(self, var_contents, toolchain_path):
540     """Point to the gold linker in the toolchain tarball.
541
542     Accepts an already set environment variable in the form of '<cmd>
543     -B<gold_path>', and overrides the gold_path to the correct path in the
544     extracted toolchain tarball.
545
546     Args:
547       var_contents: The contents of the environment variable.
548       toolchain_path: Path to the extracted toolchain tarball contents.
549
550     Returns:
551       Environment string that has correct gold path.
552     """
553     cmd, _, gold_path = var_contents.partition(' -B')
554     gold_path = os.path.join(toolchain_path, gold_path.lstrip('/'))
555     return '%s -B%s' % (cmd, gold_path)
556
557   def _SetupTCEnvironment(self, sdk_ctx, options, env):
558     """Sets up toolchain-related environment variables."""
559     target_tc_path = sdk_ctx.key_map[self.sdk.TARGET_TOOLCHAIN_KEY].path
560     tc_bin_path = os.path.join(target_tc_path, 'bin')
561     env['PATH'] = '%s:%s' % (tc_bin_path, os.environ['PATH'])
562
563     for var in ('CXX', 'CC', 'LD'):
564       env[var] = self._FixGoldPath(env[var], target_tc_path)
565
566     clang_path = os.path.join(options.chrome_src, self._CLANG_DIR)
567     if options.clang:
568       # Tell clang where to find the gcc headers and libraries.
569       flags = ['--gcc-toolchain=' + os.path.join(target_tc_path, 'usr'),
570                '--target=' + sdk_ctx.target_tc]
571       # TODO: It'd be nicer to inject these flags via some gyp variable.
572       # Note: It's important they're only passed to target targets, not host
573       # targets. They are intentionally added only to CC and not CC_host.
574       clang_bin = os.path.join(clang_path, 'clang')
575       env['CC'] = ' '.join([clang_bin] + flags + [env['CC'].split()[-1]])
576       clangxx_bin = os.path.join(clang_path, 'clang++')
577       env['CXX'] = ' '.join([clangxx_bin] + flags + [env['CXX'].split()[-1]])
578
579     # The host compiler intentionally doesn't use the libstdc++ from sdk_ctx,
580     # so that host binaries link against the system libstdc++ and can run
581     # without a special rpath.
582     env['CC_host'] = os.path.join(clang_path, 'clang')
583     env['CXX_host'] = os.path.join(clang_path, 'clang++')
584
585   def _SetupEnvironment(self, board, sdk_ctx, options, goma_dir=None,
586                         goma_port=None):
587     """Sets environment variables to export to the SDK shell."""
588     if options.chroot:
589       sysroot = os.path.join(options.chroot, 'build', board)
590       if not os.path.isdir(sysroot) and not options.cmd:
591         logging.warning("Because --chroot is set, expected a sysroot to be at "
592                         "%s, but couldn't find one.", sysroot)
593     else:
594       sysroot = sdk_ctx.key_map[constants.CHROME_SYSROOT_TAR].path
595
596     environment = os.path.join(sdk_ctx.key_map[constants.CHROME_ENV_TAR].path,
597                                'environment')
598     env = osutils.SourceEnvironment(environment, self.EBUILD_ENV)
599     self._SetupTCEnvironment(sdk_ctx, options, env)
600
601     # Add managed components to the PATH.
602     env['PATH'] = '%s:%s' % (constants.CHROMITE_BIN_DIR, env['PATH'])
603     env['PATH'] = '%s:%s' % (os.path.dirname(self.sdk.gs_ctx.gsutil_bin),
604                              env['PATH'])
605
606     # Export internally referenced variables.
607     os.environ[self.sdk.SDK_BOARD_ENV] = board
608     if self.options.sdk_path:
609       os.environ[self.sdk.SDK_PATH_ENV] = self.options.sdk_path
610     os.environ[self.sdk.SDK_VERSION_ENV] = sdk_ctx.version
611
612     # Export the board/version info in a more accessible way, so developers can
613     # reference them in their chrome_sdk.bashrc files, as well as within the
614     # chrome-sdk shell.
615     for var in [self.sdk.SDK_VERSION_ENV, self.sdk.SDK_BOARD_ENV]:
616       env[var.lstrip('%')] = os.environ[var]
617
618     # Export Goma information.
619     if goma_dir:
620       env[self.SDK_GOMA_DIR_ENV] = goma_dir
621       env[self.SDK_GOMA_PORT_ENV] = goma_port
622
623     # SYSROOT is necessary for Goma and the sysroot wrapper.
624     env['SYSROOT'] = sysroot
625     gyp_dict = chrome_util.ProcessGypDefines(env['GYP_DEFINES'])
626     gyp_dict['sysroot'] = sysroot
627     gyp_dict.pop('order_text_section', None)
628     gyp_dict['host_clang'] = 1
629     if options.clang:
630       gyp_dict['clang'] = 1
631     if options.internal:
632       gyp_dict['branding'] = 'Chrome'
633       gyp_dict['buildtype'] = 'Official'
634     else:
635       gyp_dict.pop('branding', None)
636       gyp_dict.pop('buildtype', None)
637       gyp_dict.pop('internal_gles2_conform_tests', None)
638
639     # Enable goma if requested.
640     if goma_dir:
641       gyp_dict['use_goma'] = 1
642       gyp_dict['gomadir'] = goma_dir
643
644     if options.clang:
645       # TODO(thakis): Remove once https://b/issue?id=16876457 is fixed.
646       gyp_dict['use_goma'] = 0
647
648     env['GYP_DEFINES'] = chrome_util.DictToGypDefines(gyp_dict)
649
650     # PS1 sets the command line prompt and xterm window caption.
651     full_version = sdk_ctx.version
652     if full_version != CUSTOM_VERSION:
653       full_version = self.sdk.GetFullVersion(sdk_ctx.version)
654     env['PS1'] = self._CreatePS1(self.board, full_version,
655                                  chroot=options.chroot)
656
657     out_dir = 'out_%s' % self.board
658     env['builddir_name'] = out_dir
659     env['GYP_GENERATOR_FLAGS'] = 'output_dir=%s' % out_dir
660     env['GYP_CROSSCOMPILE'] = '1'
661     return env
662
663   @staticmethod
664   def _VerifyGoma(user_rc):
665     """Verify that the user has no goma installations set up in user_rc.
666
667     If the user does have a goma installation set up, verify that it's for
668     ChromeOS.
669
670     Args:
671       user_rc: User-supplied rc file.
672     """
673     user_env = osutils.SourceEnvironment(user_rc, ['PATH'])
674     goma_ctl = osutils.Which('goma_ctl.py', user_env.get('PATH'))
675     if goma_ctl is not None:
676       logging.warning(
677           '%s is adding Goma to the PATH.  Using that Goma instead of the '
678           'managed Goma install.', user_rc)
679
680   @staticmethod
681   def _VerifyChromiteBin(user_rc):
682     """Verify that the user has not set a chromite bin/ dir in user_rc.
683
684     Args:
685       user_rc: User-supplied rc file.
686     """
687     user_env = osutils.SourceEnvironment(user_rc, ['PATH'])
688     chromite_bin = osutils.Which('parallel_emerge', user_env.get('PATH'))
689     if chromite_bin is not None:
690       logging.warning(
691           '%s is adding chromite/bin to the PATH.  Remove it from the PATH to '
692           'use the the default Chromite.', user_rc)
693
694   @contextlib.contextmanager
695   def _GetRCFile(self, env, user_rc):
696     """Returns path to dynamically created bashrc file.
697
698     The bashrc file sets the environment variables contained in |env|, as well
699     as sources the user-editable chrome_sdk.bashrc file in the user's home
700     directory.  That rc file is created if it doesn't already exist.
701
702     Args:
703       env: A dictionary of environment variables that will be set by the rc
704         file.
705       user_rc: User-supplied rc file.
706     """
707     if not os.path.exists(user_rc):
708       osutils.Touch(user_rc, makedirs=True)
709
710     self._VerifyGoma(user_rc)
711     self._VerifyChromiteBin(user_rc)
712
713     # We need a temporary rc file to 'wrap' the user configuration file,
714     # because running with '--rcfile' causes bash to ignore bash special
715     # variables passed through subprocess.Popen, such as PS1.  So we set them
716     # here.
717     #
718     # Having a wrapper rc file will also allow us to inject bash functions into
719     # the environment, not just variables.
720     with osutils.TempDir() as tempdir:
721       # Only source the user's ~/.bashrc if running in interactive mode.
722       contents = [
723           '[[ -e ~/.bashrc && $- == *i* ]] && . ~/.bashrc\n',
724       ]
725
726       for key, value in env.iteritems():
727         contents.append("export %s='%s'\n" % (key, value))
728       contents.append('. "%s"\n' % user_rc)
729
730       rc_file = os.path.join(tempdir, 'rcfile')
731       osutils.WriteFile(rc_file, contents)
732       yield rc_file
733
734   def _GomaPort(self, goma_dir):
735     """Returns current active Goma port."""
736     port = cros_build_lib.RunCommand(
737         self.GOMACC_PORT_CMD, cwd=goma_dir, debug_level=logging.DEBUG,
738         error_code_ok=True, capture_output=True).output.strip()
739     return port
740
741   def _FetchGoma(self):
742     """Fetch, install, and start Goma, using cached version if it exists.
743
744     Returns:
745       A tuple (dir, port) containing the path to the cached goma/ dir and the
746       Goma port.
747     """
748     common_path = os.path.join(self.options.cache_dir, constants.COMMON_CACHE)
749     common_cache = cache.DiskCache(common_path)
750
751     ref = common_cache.Lookup(('goma', '2'))
752     if not ref.Exists():
753       Log('Installing Goma.', silent=self.silent)
754       with osutils.TempDir() as tempdir:
755         goma_dir = os.path.join(tempdir, 'goma')
756         os.mkdir(goma_dir)
757         result = cros_build_lib.DebugRunCommand(
758             self.FETCH_GOMA_CMD, cwd=goma_dir, error_code_ok=True)
759         if result.returncode:
760           raise GomaError('Failed to fetch Goma')
761        # Update to latest version of goma. We choose the outside-chroot version
762        # ('goobuntu') over the chroot version ('chromeos') by supplying
763        # input='1' to the following prompt:
764        #
765        # What is your platform?
766        #  1. Goobuntu  2. Precise (32bit)  3. Lucid (32bit)  4. Debian
767        #  5. Chrome OS  6. MacOS ? -->
768         cros_build_lib.DebugRunCommand(
769             ['python', 'goma_ctl.py', 'update'], cwd=goma_dir, input='1\n')
770         ref.SetDefault(goma_dir)
771     goma_dir = ref.path
772
773     Log('Starting Goma.', silent=self.silent)
774     cros_build_lib.DebugRunCommand(
775         ['python', 'goma_ctl.py', 'ensure_start'], cwd=goma_dir)
776     port = self._GomaPort(goma_dir)
777     Log('Goma is started on port %s', port, silent=self.silent)
778     if not port:
779       raise GomaError('No Goma port detected')
780
781     return goma_dir, port
782
783   def Run(self):
784     """Perform the command."""
785     if os.environ.get(SDKFetcher.SDK_VERSION_ENV) is not None:
786       cros_build_lib.Die('Already in an SDK shell.')
787
788     if not self.options.chrome_src:
789       checkout = commandline.DetermineCheckout(os.getcwd())
790       self.options.chrome_src = checkout.chrome_src_dir
791     else:
792       checkout = commandline.DetermineCheckout(self.options.chrome_src)
793       if not checkout.chrome_src_dir:
794         cros_build_lib.Die('Chrome checkout not found at %s',
795                            self.options.chrome_src)
796       self.options.chrome_src = checkout.chrome_src_dir
797
798     if self.options.clang and not self.options.chrome_src:
799       cros_build_lib.Die('--clang requires --chrome-src to be set.')
800
801     if self.options.version and self.options.sdk_path:
802       cros_build_lib.Die('Cannot specify both --version and --sdk-path.')
803
804     self.silent = bool(self.options.cmd)
805     # Lazy initialize because SDKFetcher creates a GSContext() object in its
806     # constructor, which may block on user input.
807     self.sdk = SDKFetcher(self.options.cache_dir, self.options.board,
808                           clear_cache=self.options.clear_sdk_cache,
809                           chrome_src=self.options.chrome_src,
810                           sdk_path=self.options.sdk_path,
811                           toolchain_path=self.options.toolchain_path,
812                           silent=self.silent)
813
814     prepare_version = self.options.version
815     if not prepare_version and not self.options.sdk_path:
816       prepare_version, _ = self.sdk.UpdateDefaultVersion()
817
818     components = [self.sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR]
819     if not self.options.chroot:
820       components.append(constants.CHROME_SYSROOT_TAR)
821
822     goma_dir = None
823     goma_port = None
824     if self.options.goma:
825       try:
826         goma_dir, goma_port = self._FetchGoma()
827       except GomaError as e:
828         logging.error('Goma: %s.  Bypass by running with --nogoma.', e)
829
830     with self.sdk.Prepare(components, version=prepare_version,
831                           target_tc=self.options.target_tc,
832                           toolchain_url=self.options.toolchain_url) as ctx:
833       env = self._SetupEnvironment(self.options.board, ctx, self.options,
834                                    goma_dir=goma_dir, goma_port=goma_port)
835       with self._GetRCFile(env, self.options.bashrc) as rcfile:
836         bash_cmd = ['/bin/bash']
837
838         extra_env = None
839         if not self.options.cmd:
840           bash_cmd.extend(['--rcfile', rcfile, '-i'])
841         else:
842           # The '"$@"' expands out to the properly quoted positional args
843           # coming after the '--'.
844           bash_cmd.extend(['-c', '"$@"', '--'])
845           bash_cmd.extend(self.options.cmd)
846           # When run in noninteractive mode, bash sources the rc file set in
847           # BASH_ENV, and ignores the --rcfile flag.
848           extra_env = {'BASH_ENV': rcfile}
849
850         # Bash behaves differently when it detects that it's being launched by
851         # sshd - it ignores the BASH_ENV variable.  So prevent ssh-related
852         # environment variables from being passed through.
853         os.environ.pop('SSH_CLIENT', None)
854         os.environ.pop('SSH_CONNECTION', None)
855         os.environ.pop('SSH_TTY', None)
856
857         cmd_result = cros_build_lib.RunCommand(
858             bash_cmd, print_cmd=False, debug_level=logging.CRITICAL,
859             error_code_ok=True, extra_env=extra_env, cwd=self.options.cwd)
860         if self.options.cmd:
861           return cmd_result.returncode