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