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