blob: 50f4871723f1730099cc14e726d214206e04d2b5 [file] [log] [blame]
Brian Harringb938c782012-02-29 15:14:38 -08001#!/usr/bin/env python
Mike Frysinger2de7f042012-07-10 04:45:03 -04002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Brian Harringb938c782012-02-29 15:14:38 -08003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This script fetches and prepares an SDK chroot.
7"""
8
Brian Harring0ebc33c2012-05-25 13:36:19 -07009import logging
Brian Harringb938c782012-02-29 15:14:38 -080010import optparse
11import os
12import sys
13import urlparse
14
15from chromite.buildbot import constants
Brian Harringcfe762a2012-02-29 13:03:53 -080016from chromite.lib import cgroups
Brian Harringb938c782012-02-29 15:14:38 -080017from chromite.lib import cros_build_lib
Brian Harringb938c782012-02-29 15:14:38 -080018from chromite.lib import locking
Brian Harringcfe762a2012-02-29 13:03:53 -080019from chromite.lib import sudo
Brian Harringb938c782012-02-29 15:14:38 -080020
21cros_build_lib.STRICT_SUDO = True
22
23
24DEFAULT_URL = 'https://commondatastorage.googleapis.com/chromiumos-sdk/'
Zdenek Behanfd0efe42012-04-13 04:36:40 +020025SDK_SUFFIXES = ['.tbz2', '.tar.xz']
26
Brian Harringb938c782012-02-29 15:14:38 -080027SRC_ROOT = os.path.realpath(constants.SOURCE_ROOT)
28SDK_DIR = os.path.join(SRC_ROOT, 'sdks')
29OVERLAY_DIR = os.path.join(SRC_ROOT, 'src/third_party/chromiumos-overlay')
30SDK_VERSION_FILE = os.path.join(OVERLAY_DIR,
31 'chromeos/binhost/host/sdk_version.conf')
32
33# TODO(zbehan): Remove the dependency on these, reimplement them in python
34MAKE_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/make_chroot.sh')]
35ENTER_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/enter_chroot.sh')]
36
37# We need these tools to run. Very common tools (tar,..) are ommited.
38NEEDED_TOOLS = ['curl']
39
40def GetHostArch():
41 """Returns a string for the host architecture"""
42 out = cros_build_lib.RunCommand(['uname', '-m'],
43 redirect_stdout=True, print_cmd=False).output
44 return out.rstrip('\n')
45
46def CheckPrerequisites(needed_tools):
47 """Verifies that the required tools are present on the system.
48
49 This is especially important as this script is intended to run
50 outside the chroot.
51
52 Arguments:
53 needed_tools: an array of string specified binaries to look for.
54
55 Returns:
56 True if all needed tools were found.
57 """
Brian Harring98b54902012-03-23 04:05:42 -070058 missing = []
Brian Harringb938c782012-02-29 15:14:38 -080059 for tool in needed_tools:
60 cmd = ['which', tool]
61 try:
62 cros_build_lib.RunCommand(cmd, print_cmd=False, redirect_stdout=True,
63 combine_stdout_stderr=True)
64 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -070065 missing.append(tool)
66 return missing
67
Brian Harringb938c782012-02-29 15:14:38 -080068
69def GetLatestVersion():
70 """Extracts latest version from chromiumos-overlay."""
71 sdk_file = open(SDK_VERSION_FILE)
72 buf = sdk_file.readline().rstrip('\n').split('=')
73 if buf[0] != 'SDK_LATEST_VERSION':
74 raise Exception('Malformed version file')
75 return buf[1].strip('"')
76
77
Zdenek Behanfd0efe42012-04-13 04:36:40 +020078def GetArchStageTarballs(tarballArch, version):
Brian Harringb938c782012-02-29 15:14:38 -080079 """Returns the URL for a given arch/version"""
80 D = { 'x86_64': 'cros-sdk-' }
81 try:
Zdenek Behanfd0efe42012-04-13 04:36:40 +020082 return [DEFAULT_URL + D[tarballArch] + version + x for x in SDK_SUFFIXES]
Brian Harringb938c782012-02-29 15:14:38 -080083 except KeyError:
Brian Harring98b54902012-03-23 04:05:42 -070084 raise SystemExit('Unsupported arch: %s' % (tarballArch,))
Brian Harringb938c782012-02-29 15:14:38 -080085
86
Zdenek Behanfd0efe42012-04-13 04:36:40 +020087def FetchRemoteTarballs(urls):
88 """Fetches a tarball given by url, and place it in sdk/.
89
90 Args:
91 urls: List of URLs to try to download. Download will stop on first success.
92
93 Returns:
94 Full path to the downloaded file
95 """
Zdenek Behan9c644dd2012-04-05 06:24:02 +020096
97 def RunCurl(args, **kwargs):
98 """Runs curl and wraps around all necessary hacks."""
99 cmd = ['curl']
100 cmd.extend(args)
101
Brian Harringb45afea2012-05-17 08:10:53 -0700102 # These values were discerned via scraping the curl manpage; they're all
103 # retry related (dns failed, timeout occurred, etc, see the manpage for
104 # exact specifics of each).
105 # Note we allow 22 to deal w/ 500's- they're thrown by google storage
106 # occasionally.
107 # Finally, we do not use curl's --retry option since it generally doesn't
108 # actually retry anything; code 18 for example, it will not retry on.
Brian Harring06d2b272012-05-22 18:42:02 -0700109 retriable_exits = frozenset([5, 6, 7, 15, 18, 22, 26, 28, 52, 56])
Brian Harringb45afea2012-05-17 08:10:53 -0700110 try:
111 return cros_build_lib.RunCommandWithRetries(
112 5, cmd, sleep=3, retry_on=retriable_exits, **kwargs)
113 except cros_build_lib.RunCommandError, e:
114 code = e.result.returncode
Brian Harring06d2b272012-05-22 18:42:02 -0700115 if code in (51, 58, 60):
Brian Harringb45afea2012-05-17 08:10:53 -0700116 # These are the return codes of failing certs as per 'man curl'.
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200117 print 'Download failed with certificate error? Try "sudo c_rehash".'
118 else:
Brian Harring83108e12012-07-18 17:21:17 -0700119 try:
120 return cros_build_lib.RunCommandWithRetries(
121 5, cmd, sleep=60, retry_on=retriable_exits, **kwargs)
122 except cros_build_lib.RunCommandError, e:
123 print "Curl failed w/ exit code %i" % code
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200124 sys.exit(1)
125
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200126 def RemoteTarballExists(url):
127 """Tests if a remote tarball exists."""
128 # We also use this for "local" tarballs using file:// urls. Those will
129 # fail the -I check, so just check the file locally instead.
130 if url.startswith('file://'):
131 return os.path.exists(url.replace('file://', ''))
132
133 result = RunCurl(['-I', url],
134 redirect_stdout=True, redirect_stderr=True,
135 print_cmd=False)
Bernie Thompsone522add2012-05-29 12:47:12 -0700136 # We must walk the output to find the string '200 OK' for use cases where
137 # a proxy is involved and may have pushed down the actual header.
138 for header in result.output.splitlines():
139 if header.find('200 OK') != -1:
140 return 1
141 return 0
142
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200143
144 url = None
145 for url in urls:
146 print 'Attempting download: %s' % url
147 if RemoteTarballExists(url):
148 break
149 else:
150 raise Exception('No valid URLs found!')
151
Brian Harringb45afea2012-05-17 08:10:53 -0700152 # pylint: disable=E1101
Zdenek Behanb2fa72e2012-03-16 04:49:30 +0100153 tarball_name = os.path.basename(urlparse.urlparse(url).path)
154 tarball_dest = os.path.join(SDK_DIR, tarball_name)
155
156 # Cleanup old tarballs.
157 files_to_delete = [f for f in os.listdir(SDK_DIR) if f != tarball_name]
158 if files_to_delete:
159 print 'Cleaning up old tarballs: ' + str(files_to_delete)
160 for f in files_to_delete:
161 f_path = os.path.join(SDK_DIR, f)
162 # Only delete regular files that belong to us.
163 if os.path.isfile(f_path) and os.stat(f_path).st_uid == os.getuid():
164 os.remove(f_path)
Brian Harringb938c782012-02-29 15:14:38 -0800165
Brian Harringb45afea2012-05-17 08:10:53 -0700166 curl_opts = ['-f', '-L', '-y', '30', '--output', tarball_dest]
Brian Harringb938c782012-02-29 15:14:38 -0800167 if not url.startswith('file://') and os.path.exists(tarball_dest):
168 # Only resume for remote URLs. If the file is local, there's no
169 # real speedup, and using the same filename for different files
170 # locally will cause issues.
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200171 curl_opts.extend(['-C', '-'])
Brian Harringb938c782012-02-29 15:14:38 -0800172
173 # Additionally, certain versions of curl incorrectly fail if
174 # told to resume a file that is fully downloaded, thus do a
175 # check on our own.
176 # see:
Brian Harringb45afea2012-05-17 08:10:53 -0700177 # pylint: disable=C0301
Brian Harringb938c782012-02-29 15:14:38 -0800178 # https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3482927&group_id=976
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200179 result = RunCurl(['-I', url],
180 redirect_stdout=True,
181 redirect_stderr=True,
182 print_cmd=False)
183
Brian Harringb938c782012-02-29 15:14:38 -0800184 for x in result.output.splitlines():
185 if x.lower().startswith("content-length:"):
186 length = int(x.split(":", 1)[-1].strip())
187 if length == os.path.getsize(tarball_dest):
188 # Fully fetched; bypass invoking curl, since it can screw up handling
189 # of this (>=7.21.4 and up).
190 return tarball_dest
191 break
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200192 curl_opts.append(url)
193 RunCurl(curl_opts)
Brian Harringb938c782012-02-29 15:14:38 -0800194 return tarball_dest
195
196
197def BootstrapChroot(chroot_path, stage_url, replace):
198 """Builds a new chroot from source"""
199 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
200 '--nousepkg']
201
202 stage = None
203 if stage_url:
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200204 stage = FetchRemoteTarballs([stage_url])
Brian Harringb938c782012-02-29 15:14:38 -0800205
206 if stage:
207 cmd.extend(['--stage3_path', stage])
208
209 if replace:
210 cmd.append('--replace')
211
212 try:
213 cros_build_lib.RunCommand(cmd, print_cmd=False)
214 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700215 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800216
217
Mike Frysinger2de7f042012-07-10 04:45:03 -0400218def CreateChroot(sdk_url, sdk_version, chroot_path, replace, nousepkg):
Brian Harringb938c782012-02-29 15:14:38 -0800219 """Creates a new chroot from a given SDK"""
220 if not os.path.exists(SDK_DIR):
221 cros_build_lib.RunCommand(['mkdir', '-p', SDK_DIR], print_cmd=False)
222
223 # Based on selections, fetch the tarball
224 if sdk_url:
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200225 urls = [sdk_url]
Brian Harringb938c782012-02-29 15:14:38 -0800226 else:
227 arch = GetHostArch()
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200228 urls = GetArchStageTarballs(arch, sdk_version)
Brian Harringb938c782012-02-29 15:14:38 -0800229
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200230 sdk = FetchRemoteTarballs(urls)
Brian Harringb938c782012-02-29 15:14:38 -0800231
232 # TODO(zbehan): Unpack and install
233 # For now, we simply call make_chroot on the prebuilt chromeos-sdk.
234 # make_chroot provides a variety of hacks to make the chroot useable.
235 # These should all be eliminated/minimised, after which, we can change
236 # this to just unpacking the sdk.
237 cmd = MAKE_CHROOT + ['--stage3_path', sdk,
238 '--chroot', chroot_path]
Mike Frysinger2de7f042012-07-10 04:45:03 -0400239 if nousepkg:
240 cmd.append('--nousepkg')
Brian Harringb938c782012-02-29 15:14:38 -0800241 if replace:
242 cmd.append('--replace')
243
244 try:
245 cros_build_lib.RunCommand(cmd, print_cmd=False)
246 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700247 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800248
249
250def DeleteChroot(chroot_path):
251 """Deletes an existing chroot"""
252 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
253 '--delete']
254 try:
255 cros_build_lib.RunCommand(cmd, print_cmd=False)
256 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700257 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800258
259
260def _CreateLockFile(path):
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200261 """Create a lockfile via sudo that is writable by current user."""
262 cros_build_lib.SudoRunCommand(['touch', path], print_cmd=False)
263 cros_build_lib.SudoRunCommand(['chown', str(os.getuid()), path],
264 print_cmd=False)
265 cros_build_lib.SudoRunCommand(['chmod', '644', path], print_cmd=False)
Brian Harringb938c782012-02-29 15:14:38 -0800266
267
268def EnterChroot(chroot_path, chrome_root, chrome_root_mount, additional_args):
269 """Enters an existing SDK chroot"""
270 cmd = ENTER_CHROOT + ['--chroot', chroot_path]
271 if chrome_root:
272 cmd.extend(['--chrome_root', chrome_root])
273 if chrome_root_mount:
274 cmd.extend(['--chrome_root_mount', chrome_root_mount])
275 if len(additional_args) > 0:
276 cmd.append('--')
277 cmd.extend(additional_args)
Brian Harring7199e7d2012-03-23 04:10:08 -0700278
279 ret = cros_build_lib.RunCommand(cmd, print_cmd=False, error_code_ok=True)
280 # If we were in interactive mode, ignore the exit code; it'll be whatever
281 # they last ran w/in the chroot and won't matter to us one way or another.
282 # Note this does allow chroot entrance to fail and be ignored during
283 # interactive; this is however a rare case and the user will immediately
284 # see it (nor will they be checking the exit code manually).
285 if ret.returncode != 0 and additional_args:
286 raise SystemExit('Running %r failed with exit code %i'
287 % (cmd, ret.returncode))
Brian Harringb938c782012-02-29 15:14:38 -0800288
289
Brian Harring6be2efc2012-03-01 05:04:00 -0800290def main(argv):
Brian Harringb938c782012-02-29 15:14:38 -0800291 # TODO(ferringb): make argv required once depot_tools is fixed.
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200292 usage = """usage: %prog [options] [VAR1=val1 .. VARn=valn -- <args>]
Brian Harringb938c782012-02-29 15:14:38 -0800293
294This script manages a local CrOS SDK chroot. Depending on the flags,
295it can download, build or enter a chroot.
296
297Action taken is the following:
298--enter (default) .. Installs and enters a chroot
299--download .. Just download a chroot (enter if combined with --enter)
300--bootstrap .. Builds a chroot from source (enter if --enter)
301--delete .. Removes a chroot
302"""
303 sdk_latest_version = GetLatestVersion()
304 parser = optparse.OptionParser(usage)
305 # Actions:
Brian Harring0ebc33c2012-05-25 13:36:19 -0700306 parser.add_option('--bootstrap',
Brian Harringb938c782012-02-29 15:14:38 -0800307 action='store_true', dest='bootstrap', default=False,
308 help=('Build a new SDK chroot from source'))
Brian Harring0ebc33c2012-05-25 13:36:19 -0700309 parser.add_option('--delete',
Brian Harringb938c782012-02-29 15:14:38 -0800310 action='store_true', dest='delete', default=False,
311 help=('Delete the current SDK chroot'))
Brian Harring0ebc33c2012-05-25 13:36:19 -0700312 parser.add_option('--download',
Brian Harringb938c782012-02-29 15:14:38 -0800313 action='store_true', dest='download', default=False,
314 help=('Download and install a prebuilt SDK'))
Brian Harring0ebc33c2012-05-25 13:36:19 -0700315 parser.add_option('--enter',
Brian Harringb938c782012-02-29 15:14:38 -0800316 action='store_true', dest='enter', default=False,
317 help=('Enter the SDK chroot, possibly (re)create first'))
318
319 # Global options:
Brian Harring0ebc33c2012-05-25 13:36:19 -0700320 parser.add_option('--chroot',
Brian Harringb938c782012-02-29 15:14:38 -0800321 dest='chroot', default=constants.DEFAULT_CHROOT_DIR,
322 help=('SDK chroot dir name [%s]' %
323 constants.DEFAULT_CHROOT_DIR))
324
325 # Additional options:
Brian Harring0ebc33c2012-05-25 13:36:19 -0700326 parser.add_option('--chrome_root',
Brian Harringb938c782012-02-29 15:14:38 -0800327 dest='chrome_root', default='',
328 help=('Mount this chrome root into the SDK chroot'))
Brian Harring0ebc33c2012-05-25 13:36:19 -0700329 parser.add_option('--chrome_root_mount',
Brian Harringb938c782012-02-29 15:14:38 -0800330 dest='chrome_root_mount', default='',
331 help=('Mount chrome into this path inside SDK chroot'))
332 parser.add_option('-r', '--replace',
333 action='store_true', dest='replace', default=False,
334 help=('Replace an existing SDK chroot'))
Mike Frysinger2de7f042012-07-10 04:45:03 -0400335 parser.add_option('--nousepkg',
336 action='store_true', dest='nousepkg', default=False,
337 help=('Do not use binary packages when creating a chroot'))
Brian Harringb938c782012-02-29 15:14:38 -0800338 parser.add_option('-u', '--url',
339 dest='sdk_url', default='',
340 help=('''Use sdk tarball located at this url.
341 Use file:// for local files.'''))
342 parser.add_option('-v', '--version',
343 dest='sdk_version', default='',
344 help=('Use this sdk version [%s]' % sdk_latest_version))
Brian Harring0ebc33c2012-05-25 13:36:19 -0700345 parser.add_option('--debug', action='store_true', default=False,
346 help="Show debugging messages.")
Brian Harringb938c782012-02-29 15:14:38 -0800347 (options, remaining_arguments) = parser.parse_args(argv)
348
Brian Harring0ebc33c2012-05-25 13:36:19 -0700349 # Setup logging levels first so any parsing triggered log messages
350 # are appropriately filtered.
351 logging.getLogger().setLevel(
352 logging.DEBUG if options.debug else logging.INFO)
353
Brian Harringb938c782012-02-29 15:14:38 -0800354 # Some sanity checks first, before we ask for sudo credentials.
355 if cros_build_lib.IsInsideChroot():
Brian Harring98b54902012-03-23 04:05:42 -0700356 parser.error("This needs to be ran outside the chroot")
Brian Harringb938c782012-02-29 15:14:38 -0800357
Brian Harring98b54902012-03-23 04:05:42 -0700358 missing = CheckPrerequisites(NEEDED_TOOLS)
359 if missing:
360 parser.error((
361 'The tool(s) %s were not found.'
362 'Please install the appropriate package in your host.'
363 'Example(ubuntu):'
364 ' sudo apt-get install <packagename>'
365 % (', '.join(missing))))
Brian Harringb938c782012-02-29 15:14:38 -0800366
367 # Default action is --enter, if no other is selected.
368 if not (options.bootstrap or options.download or options.delete):
369 options.enter = True
370
371 # Only --enter can process additional args as passthrough commands.
372 # Warn and exit for least surprise.
373 if len(remaining_arguments) > 0 and not options.enter:
Brian Harring98b54902012-03-23 04:05:42 -0700374 parser.error("Additional arguments are not permitted, unless running "
375 "with --enter")
Brian Harringb938c782012-02-29 15:14:38 -0800376
377 # Some actions can be combined, as they merely modify how is the chroot
378 # going to be made. The only option that hates all others is --delete.
379 if options.delete and \
380 (options.enter or options.download or options.bootstrap):
Brian Harring98b54902012-03-23 04:05:42 -0700381 parser.error("--delete cannot be combined with --enter, "
382 "--download or --bootstrap")
Brian Harringb938c782012-02-29 15:14:38 -0800383 # NOTE: --delete is a true hater, it doesn't like other options either, but
384 # those will hardly lead to confusion. Nobody can expect to pass --version to
385 # delete and actually change something.
386
387 if options.bootstrap and options.download:
Brian Harring98b54902012-03-23 04:05:42 -0700388 parser.error("Either --bootstrap or --download, not both")
Brian Harringb938c782012-02-29 15:14:38 -0800389
390 # Bootstrap will start off from a non-selectable stage3 tarball. Attempts to
391 # select sdk by version are confusing. Warn and exit. We can still specify a
392 # tarball by path or URL though.
393 if options.bootstrap and options.sdk_version:
Brian Harring98b54902012-03-23 04:05:42 -0700394 parser.error("Cannot use --version when bootstrapping")
Brian Harringb938c782012-02-29 15:14:38 -0800395
396 chroot_path = os.path.join(SRC_ROOT, options.chroot)
397 chroot_path = os.path.abspath(chroot_path)
398 chroot_path = os.path.normpath(chroot_path)
399
400 if not options.sdk_version:
401 sdk_version = sdk_latest_version
402 else:
403 sdk_version = options.sdk_version
404
405 if options.delete and not os.path.exists(chroot_path):
406 print "Not doing anything. The chroot you want to remove doesn't exist."
Brian Harring98b54902012-03-23 04:05:42 -0700407 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800408
409 lock_path = os.path.dirname(chroot_path)
410 lock_path = os.path.join(lock_path,
411 '.%s_lock' % os.path.basename(chroot_path))
412 with sudo.SudoKeepAlive():
Brian Harring4e6412d2012-03-09 20:54:02 -0800413 with cgroups.SimpleContainChildren('cros_sdk'):
Brian Harringcfe762a2012-02-29 13:03:53 -0800414 _CreateLockFile(lock_path)
415 with locking.FileLock(lock_path, 'chroot lock') as lock:
416 if options.delete:
417 lock.write_lock()
418 DeleteChroot(chroot_path)
Brian Harring98b54902012-03-23 04:05:42 -0700419 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800420
Brian Harringcfe762a2012-02-29 13:03:53 -0800421 # Print a suggestion for replacement, but not if running just --enter.
422 if os.path.exists(chroot_path) and not options.replace and \
423 (options.bootstrap or options.download):
424 print "Chroot already exists. Run with --replace to re-create."
Brian Harringb938c782012-02-29 15:14:38 -0800425
Brian Harringcfe762a2012-02-29 13:03:53 -0800426 # Chroot doesn't exist or asked to replace.
427 if not os.path.exists(chroot_path) or options.replace:
428 lock.write_lock()
429 if options.bootstrap:
430 BootstrapChroot(chroot_path, options.sdk_url,
431 options.replace)
432 else:
433 CreateChroot(options.sdk_url, sdk_version,
Mike Frysinger2de7f042012-07-10 04:45:03 -0400434 chroot_path, options.replace, options.nousepkg)
Brian Harringcfe762a2012-02-29 13:03:53 -0800435 if options.enter:
436 lock.read_lock()
437 EnterChroot(chroot_path, options.chrome_root,
438 options.chrome_root_mount, remaining_arguments)