blob: 053280b97279b4eb382be6d3980d4e0d9f37b8aa [file] [log] [blame]
Brian Harringb938c782012-02-29 15:14:38 -08001#!/usr/bin/env python
2# Copyright (c) 2011-2012 The Chromium OS Authors. All rights reserved.
3# 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
9import optparse
10import os
11import sys
12import urlparse
13
14from chromite.buildbot import constants
Brian Harringcfe762a2012-02-29 13:03:53 -080015from chromite.lib import cgroups
Brian Harringb938c782012-02-29 15:14:38 -080016from chromite.lib import cros_build_lib
Brian Harringb938c782012-02-29 15:14:38 -080017from chromite.lib import locking
Brian Harringcfe762a2012-02-29 13:03:53 -080018from chromite.lib import sudo
Brian Harringb938c782012-02-29 15:14:38 -080019
20cros_build_lib.STRICT_SUDO = True
21
22
23DEFAULT_URL = 'https://commondatastorage.googleapis.com/chromiumos-sdk/'
Zdenek Behanfd0efe42012-04-13 04:36:40 +020024SDK_SUFFIXES = ['.tbz2', '.tar.xz']
25
Brian Harringb938c782012-02-29 15:14:38 -080026SRC_ROOT = os.path.realpath(constants.SOURCE_ROOT)
27SDK_DIR = os.path.join(SRC_ROOT, 'sdks')
28OVERLAY_DIR = os.path.join(SRC_ROOT, 'src/third_party/chromiumos-overlay')
29SDK_VERSION_FILE = os.path.join(OVERLAY_DIR,
30 'chromeos/binhost/host/sdk_version.conf')
31
32# TODO(zbehan): Remove the dependency on these, reimplement them in python
33MAKE_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/make_chroot.sh')]
34ENTER_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/enter_chroot.sh')]
35
36# We need these tools to run. Very common tools (tar,..) are ommited.
37NEEDED_TOOLS = ['curl']
38
39def GetHostArch():
40 """Returns a string for the host architecture"""
41 out = cros_build_lib.RunCommand(['uname', '-m'],
42 redirect_stdout=True, print_cmd=False).output
43 return out.rstrip('\n')
44
45def CheckPrerequisites(needed_tools):
46 """Verifies that the required tools are present on the system.
47
48 This is especially important as this script is intended to run
49 outside the chroot.
50
51 Arguments:
52 needed_tools: an array of string specified binaries to look for.
53
54 Returns:
55 True if all needed tools were found.
56 """
Brian Harring98b54902012-03-23 04:05:42 -070057 missing = []
Brian Harringb938c782012-02-29 15:14:38 -080058 for tool in needed_tools:
59 cmd = ['which', tool]
60 try:
61 cros_build_lib.RunCommand(cmd, print_cmd=False, redirect_stdout=True,
62 combine_stdout_stderr=True)
63 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -070064 missing.append(tool)
65 return missing
66
Brian Harringb938c782012-02-29 15:14:38 -080067
68def GetLatestVersion():
69 """Extracts latest version from chromiumos-overlay."""
70 sdk_file = open(SDK_VERSION_FILE)
71 buf = sdk_file.readline().rstrip('\n').split('=')
72 if buf[0] != 'SDK_LATEST_VERSION':
73 raise Exception('Malformed version file')
74 return buf[1].strip('"')
75
76
Zdenek Behanfd0efe42012-04-13 04:36:40 +020077def GetArchStageTarballs(tarballArch, version):
Brian Harringb938c782012-02-29 15:14:38 -080078 """Returns the URL for a given arch/version"""
79 D = { 'x86_64': 'cros-sdk-' }
80 try:
Zdenek Behanfd0efe42012-04-13 04:36:40 +020081 return [DEFAULT_URL + D[tarballArch] + version + x for x in SDK_SUFFIXES]
Brian Harringb938c782012-02-29 15:14:38 -080082 except KeyError:
Brian Harring98b54902012-03-23 04:05:42 -070083 raise SystemExit('Unsupported arch: %s' % (tarballArch,))
Brian Harringb938c782012-02-29 15:14:38 -080084
85
Zdenek Behanfd0efe42012-04-13 04:36:40 +020086def FetchRemoteTarballs(urls):
87 """Fetches a tarball given by url, and place it in sdk/.
88
89 Args:
90 urls: List of URLs to try to download. Download will stop on first success.
91
92 Returns:
93 Full path to the downloaded file
94 """
Zdenek Behan9c644dd2012-04-05 06:24:02 +020095
96 def RunCurl(args, **kwargs):
97 """Runs curl and wraps around all necessary hacks."""
98 cmd = ['curl']
99 cmd.extend(args)
100
101 result = cros_build_lib.RunCommand(cmd, error_ok=True, **kwargs)
102 if result.returncode > 0:
103 # These are the return codes of failing certs as per 'man curl'.
104 if result.returncode in (51, 58, 60):
105 print 'Download failed with certificate error? Try "sudo c_rehash".'
106 else:
107 print 'Curl failed!'
108 sys.exit(1)
109
110 return result
111
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200112 def RemoteTarballExists(url):
113 """Tests if a remote tarball exists."""
114 # We also use this for "local" tarballs using file:// urls. Those will
115 # fail the -I check, so just check the file locally instead.
116 if url.startswith('file://'):
117 return os.path.exists(url.replace('file://', ''))
118
119 result = RunCurl(['-I', url],
120 redirect_stdout=True, redirect_stderr=True,
121 print_cmd=False)
122 header = result.output.splitlines()[0]
123 return header.find('200 OK') != -1
124
125 url = None
126 for url in urls:
127 print 'Attempting download: %s' % url
128 if RemoteTarballExists(url):
129 break
130 else:
131 raise Exception('No valid URLs found!')
132
Zdenek Behanb2fa72e2012-03-16 04:49:30 +0100133 tarball_name = os.path.basename(urlparse.urlparse(url).path)
134 tarball_dest = os.path.join(SDK_DIR, tarball_name)
135
136 # Cleanup old tarballs.
137 files_to_delete = [f for f in os.listdir(SDK_DIR) if f != tarball_name]
138 if files_to_delete:
139 print 'Cleaning up old tarballs: ' + str(files_to_delete)
140 for f in files_to_delete:
141 f_path = os.path.join(SDK_DIR, f)
142 # Only delete regular files that belong to us.
143 if os.path.isfile(f_path) and os.stat(f_path).st_uid == os.getuid():
144 os.remove(f_path)
Brian Harringb938c782012-02-29 15:14:38 -0800145
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200146 curl_opts = ['-f', '--retry', '5', '-L', '-y', '30',
147 '--output', tarball_dest]
Brian Harringb938c782012-02-29 15:14:38 -0800148 if not url.startswith('file://') and os.path.exists(tarball_dest):
149 # Only resume for remote URLs. If the file is local, there's no
150 # real speedup, and using the same filename for different files
151 # locally will cause issues.
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200152 curl_opts.extend(['-C', '-'])
Brian Harringb938c782012-02-29 15:14:38 -0800153
154 # Additionally, certain versions of curl incorrectly fail if
155 # told to resume a file that is fully downloaded, thus do a
156 # check on our own.
157 # see:
158 # https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3482927&group_id=976
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200159 result = RunCurl(['-I', url],
160 redirect_stdout=True,
161 redirect_stderr=True,
162 print_cmd=False)
163
Brian Harringb938c782012-02-29 15:14:38 -0800164 for x in result.output.splitlines():
165 if x.lower().startswith("content-length:"):
166 length = int(x.split(":", 1)[-1].strip())
167 if length == os.path.getsize(tarball_dest):
168 # Fully fetched; bypass invoking curl, since it can screw up handling
169 # of this (>=7.21.4 and up).
170 return tarball_dest
171 break
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200172 curl_opts.append(url)
173 RunCurl(curl_opts)
Brian Harringb938c782012-02-29 15:14:38 -0800174 return tarball_dest
175
176
177def BootstrapChroot(chroot_path, stage_url, replace):
178 """Builds a new chroot from source"""
179 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
180 '--nousepkg']
181
182 stage = None
183 if stage_url:
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200184 stage = FetchRemoteTarballs([stage_url])
Brian Harringb938c782012-02-29 15:14:38 -0800185
186 if stage:
187 cmd.extend(['--stage3_path', stage])
188
189 if replace:
190 cmd.append('--replace')
191
192 try:
193 cros_build_lib.RunCommand(cmd, print_cmd=False)
194 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700195 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800196
197
198def CreateChroot(sdk_url, sdk_version, chroot_path, replace):
199 """Creates a new chroot from a given SDK"""
200 if not os.path.exists(SDK_DIR):
201 cros_build_lib.RunCommand(['mkdir', '-p', SDK_DIR], print_cmd=False)
202
203 # Based on selections, fetch the tarball
204 if sdk_url:
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200205 urls = [sdk_url]
Brian Harringb938c782012-02-29 15:14:38 -0800206 else:
207 arch = GetHostArch()
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200208 urls = GetArchStageTarballs(arch, sdk_version)
Brian Harringb938c782012-02-29 15:14:38 -0800209
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200210 sdk = FetchRemoteTarballs(urls)
Brian Harringb938c782012-02-29 15:14:38 -0800211
212 # TODO(zbehan): Unpack and install
213 # For now, we simply call make_chroot on the prebuilt chromeos-sdk.
214 # make_chroot provides a variety of hacks to make the chroot useable.
215 # These should all be eliminated/minimised, after which, we can change
216 # this to just unpacking the sdk.
217 cmd = MAKE_CHROOT + ['--stage3_path', sdk,
218 '--chroot', chroot_path]
219
220 if replace:
221 cmd.append('--replace')
222
223 try:
224 cros_build_lib.RunCommand(cmd, print_cmd=False)
225 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700226 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800227
228
229def DeleteChroot(chroot_path):
230 """Deletes an existing chroot"""
231 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
232 '--delete']
233 try:
234 cros_build_lib.RunCommand(cmd, print_cmd=False)
235 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700236 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800237
238
239def _CreateLockFile(path):
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200240 """Create a lockfile via sudo that is writable by current user."""
241 cros_build_lib.SudoRunCommand(['touch', path], print_cmd=False)
242 cros_build_lib.SudoRunCommand(['chown', str(os.getuid()), path],
243 print_cmd=False)
244 cros_build_lib.SudoRunCommand(['chmod', '644', path], print_cmd=False)
Brian Harringb938c782012-02-29 15:14:38 -0800245
246
247def EnterChroot(chroot_path, chrome_root, chrome_root_mount, additional_args):
248 """Enters an existing SDK chroot"""
249 cmd = ENTER_CHROOT + ['--chroot', chroot_path]
250 if chrome_root:
251 cmd.extend(['--chrome_root', chrome_root])
252 if chrome_root_mount:
253 cmd.extend(['--chrome_root_mount', chrome_root_mount])
254 if len(additional_args) > 0:
255 cmd.append('--')
256 cmd.extend(additional_args)
Brian Harring7199e7d2012-03-23 04:10:08 -0700257
258 ret = cros_build_lib.RunCommand(cmd, print_cmd=False, error_code_ok=True)
259 # If we were in interactive mode, ignore the exit code; it'll be whatever
260 # they last ran w/in the chroot and won't matter to us one way or another.
261 # Note this does allow chroot entrance to fail and be ignored during
262 # interactive; this is however a rare case and the user will immediately
263 # see it (nor will they be checking the exit code manually).
264 if ret.returncode != 0 and additional_args:
265 raise SystemExit('Running %r failed with exit code %i'
266 % (cmd, ret.returncode))
Brian Harringb938c782012-02-29 15:14:38 -0800267
268
Brian Harring6be2efc2012-03-01 05:04:00 -0800269def main(argv):
Brian Harringb938c782012-02-29 15:14:38 -0800270 # TODO(ferringb): make argv required once depot_tools is fixed.
Zdenek Behanfd0efe42012-04-13 04:36:40 +0200271 usage = """usage: %prog [options] [VAR1=val1 .. VARn=valn -- <args>]
Brian Harringb938c782012-02-29 15:14:38 -0800272
273This script manages a local CrOS SDK chroot. Depending on the flags,
274it can download, build or enter a chroot.
275
276Action taken is the following:
277--enter (default) .. Installs and enters a chroot
278--download .. Just download a chroot (enter if combined with --enter)
279--bootstrap .. Builds a chroot from source (enter if --enter)
280--delete .. Removes a chroot
281"""
282 sdk_latest_version = GetLatestVersion()
283 parser = optparse.OptionParser(usage)
284 # Actions:
285 parser.add_option('', '--bootstrap',
286 action='store_true', dest='bootstrap', default=False,
287 help=('Build a new SDK chroot from source'))
288 parser.add_option('', '--delete',
289 action='store_true', dest='delete', default=False,
290 help=('Delete the current SDK chroot'))
291 parser.add_option('', '--download',
292 action='store_true', dest='download', default=False,
293 help=('Download and install a prebuilt SDK'))
294 parser.add_option('', '--enter',
295 action='store_true', dest='enter', default=False,
296 help=('Enter the SDK chroot, possibly (re)create first'))
297
298 # Global options:
299 parser.add_option('', '--chroot',
300 dest='chroot', default=constants.DEFAULT_CHROOT_DIR,
301 help=('SDK chroot dir name [%s]' %
302 constants.DEFAULT_CHROOT_DIR))
303
304 # Additional options:
305 parser.add_option('', '--chrome_root',
306 dest='chrome_root', default='',
307 help=('Mount this chrome root into the SDK chroot'))
308 parser.add_option('', '--chrome_root_mount',
309 dest='chrome_root_mount', default='',
310 help=('Mount chrome into this path inside SDK chroot'))
311 parser.add_option('-r', '--replace',
312 action='store_true', dest='replace', default=False,
313 help=('Replace an existing SDK chroot'))
314 parser.add_option('-u', '--url',
315 dest='sdk_url', default='',
316 help=('''Use sdk tarball located at this url.
317 Use file:// for local files.'''))
318 parser.add_option('-v', '--version',
319 dest='sdk_version', default='',
320 help=('Use this sdk version [%s]' % sdk_latest_version))
321 (options, remaining_arguments) = parser.parse_args(argv)
322
323 # Some sanity checks first, before we ask for sudo credentials.
324 if cros_build_lib.IsInsideChroot():
Brian Harring98b54902012-03-23 04:05:42 -0700325 parser.error("This needs to be ran outside the chroot")
Brian Harringb938c782012-02-29 15:14:38 -0800326
Brian Harring98b54902012-03-23 04:05:42 -0700327 missing = CheckPrerequisites(NEEDED_TOOLS)
328 if missing:
329 parser.error((
330 'The tool(s) %s were not found.'
331 'Please install the appropriate package in your host.'
332 'Example(ubuntu):'
333 ' sudo apt-get install <packagename>'
334 % (', '.join(missing))))
Brian Harringb938c782012-02-29 15:14:38 -0800335
336 # Default action is --enter, if no other is selected.
337 if not (options.bootstrap or options.download or options.delete):
338 options.enter = True
339
340 # Only --enter can process additional args as passthrough commands.
341 # Warn and exit for least surprise.
342 if len(remaining_arguments) > 0 and not options.enter:
Brian Harring98b54902012-03-23 04:05:42 -0700343 parser.error("Additional arguments are not permitted, unless running "
344 "with --enter")
Brian Harringb938c782012-02-29 15:14:38 -0800345
346 # Some actions can be combined, as they merely modify how is the chroot
347 # going to be made. The only option that hates all others is --delete.
348 if options.delete and \
349 (options.enter or options.download or options.bootstrap):
Brian Harring98b54902012-03-23 04:05:42 -0700350 parser.error("--delete cannot be combined with --enter, "
351 "--download or --bootstrap")
Brian Harringb938c782012-02-29 15:14:38 -0800352 # NOTE: --delete is a true hater, it doesn't like other options either, but
353 # those will hardly lead to confusion. Nobody can expect to pass --version to
354 # delete and actually change something.
355
356 if options.bootstrap and options.download:
Brian Harring98b54902012-03-23 04:05:42 -0700357 parser.error("Either --bootstrap or --download, not both")
Brian Harringb938c782012-02-29 15:14:38 -0800358
359 # Bootstrap will start off from a non-selectable stage3 tarball. Attempts to
360 # select sdk by version are confusing. Warn and exit. We can still specify a
361 # tarball by path or URL though.
362 if options.bootstrap and options.sdk_version:
Brian Harring98b54902012-03-23 04:05:42 -0700363 parser.error("Cannot use --version when bootstrapping")
Brian Harringb938c782012-02-29 15:14:38 -0800364
365 chroot_path = os.path.join(SRC_ROOT, options.chroot)
366 chroot_path = os.path.abspath(chroot_path)
367 chroot_path = os.path.normpath(chroot_path)
368
369 if not options.sdk_version:
370 sdk_version = sdk_latest_version
371 else:
372 sdk_version = options.sdk_version
373
374 if options.delete and not os.path.exists(chroot_path):
375 print "Not doing anything. The chroot you want to remove doesn't exist."
Brian Harring98b54902012-03-23 04:05:42 -0700376 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800377
378 lock_path = os.path.dirname(chroot_path)
379 lock_path = os.path.join(lock_path,
380 '.%s_lock' % os.path.basename(chroot_path))
381 with sudo.SudoKeepAlive():
Brian Harring4e6412d2012-03-09 20:54:02 -0800382 with cgroups.SimpleContainChildren('cros_sdk'):
Brian Harringcfe762a2012-02-29 13:03:53 -0800383 _CreateLockFile(lock_path)
384 with locking.FileLock(lock_path, 'chroot lock') as lock:
385 if options.delete:
386 lock.write_lock()
387 DeleteChroot(chroot_path)
Brian Harring98b54902012-03-23 04:05:42 -0700388 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800389
Brian Harringcfe762a2012-02-29 13:03:53 -0800390 # Print a suggestion for replacement, but not if running just --enter.
391 if os.path.exists(chroot_path) and not options.replace and \
392 (options.bootstrap or options.download):
393 print "Chroot already exists. Run with --replace to re-create."
Brian Harringb938c782012-02-29 15:14:38 -0800394
Brian Harringcfe762a2012-02-29 13:03:53 -0800395 # Chroot doesn't exist or asked to replace.
396 if not os.path.exists(chroot_path) or options.replace:
397 lock.write_lock()
398 if options.bootstrap:
399 BootstrapChroot(chroot_path, options.sdk_url,
400 options.replace)
401 else:
402 CreateChroot(options.sdk_url, sdk_version,
403 chroot_path, options.replace)
404 if options.enter:
405 lock.read_lock()
406 EnterChroot(chroot_path, options.chrome_root,
407 options.chrome_root_mount, remaining_arguments)