blob: 848058a909069132bceb9b0e390bc48c36c370fd [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/'
24SRC_ROOT = os.path.realpath(constants.SOURCE_ROOT)
25SDK_DIR = os.path.join(SRC_ROOT, 'sdks')
26OVERLAY_DIR = os.path.join(SRC_ROOT, 'src/third_party/chromiumos-overlay')
27SDK_VERSION_FILE = os.path.join(OVERLAY_DIR,
28 'chromeos/binhost/host/sdk_version.conf')
29
30# TODO(zbehan): Remove the dependency on these, reimplement them in python
31MAKE_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/make_chroot.sh')]
32ENTER_CHROOT = [os.path.join(SRC_ROOT, 'src/scripts/sdk_lib/enter_chroot.sh')]
33
34# We need these tools to run. Very common tools (tar,..) are ommited.
35NEEDED_TOOLS = ['curl']
36
37def GetHostArch():
38 """Returns a string for the host architecture"""
39 out = cros_build_lib.RunCommand(['uname', '-m'],
40 redirect_stdout=True, print_cmd=False).output
41 return out.rstrip('\n')
42
43def CheckPrerequisites(needed_tools):
44 """Verifies that the required tools are present on the system.
45
46 This is especially important as this script is intended to run
47 outside the chroot.
48
49 Arguments:
50 needed_tools: an array of string specified binaries to look for.
51
52 Returns:
53 True if all needed tools were found.
54 """
Brian Harring98b54902012-03-23 04:05:42 -070055 missing = []
Brian Harringb938c782012-02-29 15:14:38 -080056 for tool in needed_tools:
57 cmd = ['which', tool]
58 try:
59 cros_build_lib.RunCommand(cmd, print_cmd=False, redirect_stdout=True,
60 combine_stdout_stderr=True)
61 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -070062 missing.append(tool)
63 return missing
64
Brian Harringb938c782012-02-29 15:14:38 -080065
66def GetLatestVersion():
67 """Extracts latest version from chromiumos-overlay."""
68 sdk_file = open(SDK_VERSION_FILE)
69 buf = sdk_file.readline().rstrip('\n').split('=')
70 if buf[0] != 'SDK_LATEST_VERSION':
71 raise Exception('Malformed version file')
72 return buf[1].strip('"')
73
74
Mike Frysingerf66d4b52012-05-03 23:48:39 -070075def GetArchStageTarball(tarballArch, version):
Brian Harringb938c782012-02-29 15:14:38 -080076 """Returns the URL for a given arch/version"""
77 D = { 'x86_64': 'cros-sdk-' }
78 try:
Mike Frysingerf66d4b52012-05-03 23:48:39 -070079 return DEFAULT_URL + D[tarballArch] + version + '.tbz2'
Brian Harringb938c782012-02-29 15:14:38 -080080 except KeyError:
Brian Harring98b54902012-03-23 04:05:42 -070081 raise SystemExit('Unsupported arch: %s' % (tarballArch,))
Brian Harringb938c782012-02-29 15:14:38 -080082
83
Mike Frysingerf66d4b52012-05-03 23:48:39 -070084def FetchRemoteTarball(url):
85 """Fetches a tarball given by url, and place it in sdk/."""
Zdenek Behan9c644dd2012-04-05 06:24:02 +020086
87 def RunCurl(args, **kwargs):
88 """Runs curl and wraps around all necessary hacks."""
89 cmd = ['curl']
90 cmd.extend(args)
91
92 result = cros_build_lib.RunCommand(cmd, error_ok=True, **kwargs)
93 if result.returncode > 0:
94 # These are the return codes of failing certs as per 'man curl'.
95 if result.returncode in (51, 58, 60):
96 print 'Download failed with certificate error? Try "sudo c_rehash".'
97 else:
98 print 'Curl failed!'
99 sys.exit(1)
100
101 return result
102
Zdenek Behanb2fa72e2012-03-16 04:49:30 +0100103 tarball_name = os.path.basename(urlparse.urlparse(url).path)
104 tarball_dest = os.path.join(SDK_DIR, tarball_name)
105
106 # Cleanup old tarballs.
107 files_to_delete = [f for f in os.listdir(SDK_DIR) if f != tarball_name]
108 if files_to_delete:
109 print 'Cleaning up old tarballs: ' + str(files_to_delete)
110 for f in files_to_delete:
111 f_path = os.path.join(SDK_DIR, f)
112 # Only delete regular files that belong to us.
113 if os.path.isfile(f_path) and os.stat(f_path).st_uid == os.getuid():
114 os.remove(f_path)
Brian Harringb938c782012-02-29 15:14:38 -0800115
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200116 curl_opts = ['-f', '--retry', '5', '-L', '-y', '30',
117 '--output', tarball_dest]
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700118 print 'Downloading sdk: "%s"' % url
Brian Harringb938c782012-02-29 15:14:38 -0800119 if not url.startswith('file://') and os.path.exists(tarball_dest):
120 # Only resume for remote URLs. If the file is local, there's no
121 # real speedup, and using the same filename for different files
122 # locally will cause issues.
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200123 curl_opts.extend(['-C', '-'])
Brian Harringb938c782012-02-29 15:14:38 -0800124
125 # Additionally, certain versions of curl incorrectly fail if
126 # told to resume a file that is fully downloaded, thus do a
127 # check on our own.
128 # see:
129 # https://sourceforge.net/tracker/?func=detail&atid=100976&aid=3482927&group_id=976
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200130 result = RunCurl(['-I', url],
131 redirect_stdout=True,
132 redirect_stderr=True,
133 print_cmd=False)
134
Brian Harringb938c782012-02-29 15:14:38 -0800135 for x in result.output.splitlines():
136 if x.lower().startswith("content-length:"):
137 length = int(x.split(":", 1)[-1].strip())
138 if length == os.path.getsize(tarball_dest):
139 # Fully fetched; bypass invoking curl, since it can screw up handling
140 # of this (>=7.21.4 and up).
141 return tarball_dest
142 break
Zdenek Behan9c644dd2012-04-05 06:24:02 +0200143 curl_opts.append(url)
144 RunCurl(curl_opts)
Brian Harringb938c782012-02-29 15:14:38 -0800145 return tarball_dest
146
147
148def BootstrapChroot(chroot_path, stage_url, replace):
149 """Builds a new chroot from source"""
150 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
151 '--nousepkg']
152
153 stage = None
154 if stage_url:
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700155 stage = FetchRemoteTarball(stage_url)
Brian Harringb938c782012-02-29 15:14:38 -0800156
157 if stage:
158 cmd.extend(['--stage3_path', stage])
159
160 if replace:
161 cmd.append('--replace')
162
163 try:
164 cros_build_lib.RunCommand(cmd, print_cmd=False)
165 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700166 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800167
168
169def CreateChroot(sdk_url, sdk_version, chroot_path, replace):
170 """Creates a new chroot from a given SDK"""
171 if not os.path.exists(SDK_DIR):
172 cros_build_lib.RunCommand(['mkdir', '-p', SDK_DIR], print_cmd=False)
173
174 # Based on selections, fetch the tarball
175 if sdk_url:
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700176 url = sdk_url
Brian Harringb938c782012-02-29 15:14:38 -0800177 else:
178 arch = GetHostArch()
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700179 if sdk_version:
180 url = GetArchStageTarball(arch, sdk_version)
181 else:
182 url = GetArchStageTarball(arch)
Brian Harringb938c782012-02-29 15:14:38 -0800183
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700184 sdk = FetchRemoteTarball(url)
Brian Harringb938c782012-02-29 15:14:38 -0800185
186 # TODO(zbehan): Unpack and install
187 # For now, we simply call make_chroot on the prebuilt chromeos-sdk.
188 # make_chroot provides a variety of hacks to make the chroot useable.
189 # These should all be eliminated/minimised, after which, we can change
190 # this to just unpacking the sdk.
191 cmd = MAKE_CHROOT + ['--stage3_path', sdk,
192 '--chroot', chroot_path]
193
194 if replace:
195 cmd.append('--replace')
196
197 try:
198 cros_build_lib.RunCommand(cmd, print_cmd=False)
199 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700200 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800201
202
203def DeleteChroot(chroot_path):
204 """Deletes an existing chroot"""
205 cmd = MAKE_CHROOT + ['--chroot', chroot_path,
206 '--delete']
207 try:
208 cros_build_lib.RunCommand(cmd, print_cmd=False)
209 except cros_build_lib.RunCommandError:
Brian Harring98b54902012-03-23 04:05:42 -0700210 raise SystemExit('Running %r failed!' % cmd)
Brian Harringb938c782012-02-29 15:14:38 -0800211
212
213def _CreateLockFile(path):
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700214 """Create a lockfile via sudo that is writable by current user."""
215 cros_build_lib.SudoRunCommand(['touch', path], print_cmd=False)
216 cros_build_lib.SudoRunCommand(['chown', str(os.getuid()), path],
217 print_cmd=False)
218 cros_build_lib.SudoRunCommand(['chmod', '644', path], print_cmd=False)
Brian Harringb938c782012-02-29 15:14:38 -0800219
220
221def EnterChroot(chroot_path, chrome_root, chrome_root_mount, additional_args):
222 """Enters an existing SDK chroot"""
223 cmd = ENTER_CHROOT + ['--chroot', chroot_path]
224 if chrome_root:
225 cmd.extend(['--chrome_root', chrome_root])
226 if chrome_root_mount:
227 cmd.extend(['--chrome_root_mount', chrome_root_mount])
228 if len(additional_args) > 0:
229 cmd.append('--')
230 cmd.extend(additional_args)
Brian Harring7199e7d2012-03-23 04:10:08 -0700231
232 ret = cros_build_lib.RunCommand(cmd, print_cmd=False, error_code_ok=True)
233 # If we were in interactive mode, ignore the exit code; it'll be whatever
234 # they last ran w/in the chroot and won't matter to us one way or another.
235 # Note this does allow chroot entrance to fail and be ignored during
236 # interactive; this is however a rare case and the user will immediately
237 # see it (nor will they be checking the exit code manually).
238 if ret.returncode != 0 and additional_args:
239 raise SystemExit('Running %r failed with exit code %i'
240 % (cmd, ret.returncode))
Brian Harringb938c782012-02-29 15:14:38 -0800241
242
Brian Harring6be2efc2012-03-01 05:04:00 -0800243def main(argv):
Brian Harringb938c782012-02-29 15:14:38 -0800244 # TODO(ferringb): make argv required once depot_tools is fixed.
Mike Frysingerf66d4b52012-05-03 23:48:39 -0700245 usage="""usage: %prog [options] [VAR1=val1 .. VARn=valn -- <args>]
Brian Harringb938c782012-02-29 15:14:38 -0800246
247This script manages a local CrOS SDK chroot. Depending on the flags,
248it can download, build or enter a chroot.
249
250Action taken is the following:
251--enter (default) .. Installs and enters a chroot
252--download .. Just download a chroot (enter if combined with --enter)
253--bootstrap .. Builds a chroot from source (enter if --enter)
254--delete .. Removes a chroot
255"""
256 sdk_latest_version = GetLatestVersion()
257 parser = optparse.OptionParser(usage)
258 # Actions:
259 parser.add_option('', '--bootstrap',
260 action='store_true', dest='bootstrap', default=False,
261 help=('Build a new SDK chroot from source'))
262 parser.add_option('', '--delete',
263 action='store_true', dest='delete', default=False,
264 help=('Delete the current SDK chroot'))
265 parser.add_option('', '--download',
266 action='store_true', dest='download', default=False,
267 help=('Download and install a prebuilt SDK'))
268 parser.add_option('', '--enter',
269 action='store_true', dest='enter', default=False,
270 help=('Enter the SDK chroot, possibly (re)create first'))
271
272 # Global options:
273 parser.add_option('', '--chroot',
274 dest='chroot', default=constants.DEFAULT_CHROOT_DIR,
275 help=('SDK chroot dir name [%s]' %
276 constants.DEFAULT_CHROOT_DIR))
277
278 # Additional options:
279 parser.add_option('', '--chrome_root',
280 dest='chrome_root', default='',
281 help=('Mount this chrome root into the SDK chroot'))
282 parser.add_option('', '--chrome_root_mount',
283 dest='chrome_root_mount', default='',
284 help=('Mount chrome into this path inside SDK chroot'))
285 parser.add_option('-r', '--replace',
286 action='store_true', dest='replace', default=False,
287 help=('Replace an existing SDK chroot'))
288 parser.add_option('-u', '--url',
289 dest='sdk_url', default='',
290 help=('''Use sdk tarball located at this url.
291 Use file:// for local files.'''))
292 parser.add_option('-v', '--version',
293 dest='sdk_version', default='',
294 help=('Use this sdk version [%s]' % sdk_latest_version))
295 (options, remaining_arguments) = parser.parse_args(argv)
296
297 # Some sanity checks first, before we ask for sudo credentials.
298 if cros_build_lib.IsInsideChroot():
Brian Harring98b54902012-03-23 04:05:42 -0700299 parser.error("This needs to be ran outside the chroot")
Brian Harringb938c782012-02-29 15:14:38 -0800300
Brian Harring98b54902012-03-23 04:05:42 -0700301 missing = CheckPrerequisites(NEEDED_TOOLS)
302 if missing:
303 parser.error((
304 'The tool(s) %s were not found.'
305 'Please install the appropriate package in your host.'
306 'Example(ubuntu):'
307 ' sudo apt-get install <packagename>'
308 % (', '.join(missing))))
Brian Harringb938c782012-02-29 15:14:38 -0800309
310 # Default action is --enter, if no other is selected.
311 if not (options.bootstrap or options.download or options.delete):
312 options.enter = True
313
314 # Only --enter can process additional args as passthrough commands.
315 # Warn and exit for least surprise.
316 if len(remaining_arguments) > 0 and not options.enter:
Brian Harring98b54902012-03-23 04:05:42 -0700317 parser.error("Additional arguments are not permitted, unless running "
318 "with --enter")
Brian Harringb938c782012-02-29 15:14:38 -0800319
320 # Some actions can be combined, as they merely modify how is the chroot
321 # going to be made. The only option that hates all others is --delete.
322 if options.delete and \
323 (options.enter or options.download or options.bootstrap):
Brian Harring98b54902012-03-23 04:05:42 -0700324 parser.error("--delete cannot be combined with --enter, "
325 "--download or --bootstrap")
Brian Harringb938c782012-02-29 15:14:38 -0800326 # NOTE: --delete is a true hater, it doesn't like other options either, but
327 # those will hardly lead to confusion. Nobody can expect to pass --version to
328 # delete and actually change something.
329
330 if options.bootstrap and options.download:
Brian Harring98b54902012-03-23 04:05:42 -0700331 parser.error("Either --bootstrap or --download, not both")
Brian Harringb938c782012-02-29 15:14:38 -0800332
333 # Bootstrap will start off from a non-selectable stage3 tarball. Attempts to
334 # select sdk by version are confusing. Warn and exit. We can still specify a
335 # tarball by path or URL though.
336 if options.bootstrap and options.sdk_version:
Brian Harring98b54902012-03-23 04:05:42 -0700337 parser.error("Cannot use --version when bootstrapping")
Brian Harringb938c782012-02-29 15:14:38 -0800338
339 chroot_path = os.path.join(SRC_ROOT, options.chroot)
340 chroot_path = os.path.abspath(chroot_path)
341 chroot_path = os.path.normpath(chroot_path)
342
343 if not options.sdk_version:
344 sdk_version = sdk_latest_version
345 else:
346 sdk_version = options.sdk_version
347
348 if options.delete and not os.path.exists(chroot_path):
349 print "Not doing anything. The chroot you want to remove doesn't exist."
Brian Harring98b54902012-03-23 04:05:42 -0700350 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800351
352 lock_path = os.path.dirname(chroot_path)
353 lock_path = os.path.join(lock_path,
354 '.%s_lock' % os.path.basename(chroot_path))
355 with sudo.SudoKeepAlive():
Brian Harring4e6412d2012-03-09 20:54:02 -0800356 with cgroups.SimpleContainChildren('cros_sdk'):
Brian Harringcfe762a2012-02-29 13:03:53 -0800357 _CreateLockFile(lock_path)
358 with locking.FileLock(lock_path, 'chroot lock') as lock:
359 if options.delete:
360 lock.write_lock()
361 DeleteChroot(chroot_path)
Brian Harring98b54902012-03-23 04:05:42 -0700362 return 0
Brian Harringb938c782012-02-29 15:14:38 -0800363
Brian Harringcfe762a2012-02-29 13:03:53 -0800364 # Print a suggestion for replacement, but not if running just --enter.
365 if os.path.exists(chroot_path) and not options.replace and \
366 (options.bootstrap or options.download):
367 print "Chroot already exists. Run with --replace to re-create."
Brian Harringb938c782012-02-29 15:14:38 -0800368
Brian Harringcfe762a2012-02-29 13:03:53 -0800369 # Chroot doesn't exist or asked to replace.
370 if not os.path.exists(chroot_path) or options.replace:
371 lock.write_lock()
372 if options.bootstrap:
373 BootstrapChroot(chroot_path, options.sdk_url,
374 options.replace)
375 else:
376 CreateChroot(options.sdk_url, sdk_version,
377 chroot_path, options.replace)
378 if options.enter:
379 lock.read_lock()
380 EnterChroot(chroot_path, options.chrome_root,
381 options.chrome_root_mount, remaining_arguments)