python/mozboot/mozboot/osx.py

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 # You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 from __future__ import print_function, unicode_literals
michael@0 6
michael@0 7 import os
michael@0 8 import re
michael@0 9 import subprocess
michael@0 10 import sys
michael@0 11 import tempfile
michael@0 12 try:
michael@0 13 from urllib2 import urlopen
michael@0 14 except ImportError:
michael@0 15 from urllib.request import urlopen
michael@0 16
michael@0 17 from distutils.version import StrictVersion
michael@0 18
michael@0 19 from mozboot.base import BaseBootstrapper
michael@0 20
michael@0 21 HOMEBREW_BOOTSTRAP = 'https://raw.github.com/Homebrew/homebrew/go/install'
michael@0 22 XCODE_APP_STORE = 'macappstore://itunes.apple.com/app/id497799835?mt=12'
michael@0 23 XCODE_LEGACY = 'https://developer.apple.com/downloads/download.action?path=Developer_Tools/xcode_3.2.6_and_ios_sdk_4.3__final/xcode_3.2.6_and_ios_sdk_4.3.dmg'
michael@0 24 HOMEBREW_AUTOCONF213 = 'https://raw.github.com/Homebrew/homebrew-versions/master/autoconf213.rb'
michael@0 25
michael@0 26 MACPORTS_URL = {'9': 'https://distfiles.macports.org/MacPorts/MacPorts-2.2.1-10.9-Mavericks.pkg',
michael@0 27 '8': 'https://distfiles.macports.org/MacPorts/MacPorts-2.1.3-10.8-MountainLion.pkg',
michael@0 28 '7': 'https://distfiles.macports.org/MacPorts/MacPorts-2.1.3-10.7-Lion.pkg',
michael@0 29 '6': 'https://distfiles.macports.org/MacPorts/MacPorts-2.1.3-10.6-SnowLeopard.pkg',}
michael@0 30
michael@0 31 MACPORTS_CLANG_PACKAGE = 'clang-3.3'
michael@0 32
michael@0 33 RE_CLANG_VERSION = re.compile('Apple (?:clang|LLVM) version (\d+\.\d+)')
michael@0 34
michael@0 35 APPLE_CLANG_MINIMUM_VERSION = StrictVersion('4.2')
michael@0 36
michael@0 37 XCODE_REQUIRED = '''
michael@0 38 Xcode is required to build Firefox. Please complete the install of Xcode
michael@0 39 through the App Store.
michael@0 40
michael@0 41 It's possible Xcode is already installed on this machine but it isn't being
michael@0 42 detected. This is possible with developer preview releases of Xcode, for
michael@0 43 example. To correct this problem, run:
michael@0 44
michael@0 45 `xcode-select --switch /path/to/Xcode.app`.
michael@0 46
michael@0 47 e.g. `sudo xcode-select --switch /Applications/Xcode.app`.
michael@0 48 '''
michael@0 49
michael@0 50 XCODE_REQUIRED_LEGACY = '''
michael@0 51 You will need to download and install Xcode to build Firefox.
michael@0 52
michael@0 53 Please complete the Xcode download and then relaunch this script.
michael@0 54 '''
michael@0 55
michael@0 56 XCODE_NO_DEVELOPER_DIRECTORY = '''
michael@0 57 xcode-select says you don't have a developer directory configured. We think
michael@0 58 this is due to you not having Xcode installed (properly). We're going to
michael@0 59 attempt to install Xcode through the App Store. If the App Store thinks you
michael@0 60 have Xcode installed, please run xcode-select by hand until it stops
michael@0 61 complaining and then re-run this script.
michael@0 62 '''
michael@0 63
michael@0 64 XCODE_COMMAND_LINE_TOOLS_MISSING = '''
michael@0 65 The Xcode command line tools are required to build Firefox.
michael@0 66 '''
michael@0 67
michael@0 68 INSTALL_XCODE_COMMAND_LINE_TOOLS_STEPS = '''
michael@0 69 Perform the following steps to install the Xcode command line tools:
michael@0 70
michael@0 71 1) Open Xcode.app
michael@0 72 2) Click through any first-run prompts
michael@0 73 3) From the main Xcode menu, select Preferences (Command ,)
michael@0 74 4) Go to the Download tab (near the right)
michael@0 75 5) Install the "Command Line Tools"
michael@0 76
michael@0 77 When that has finished installing, please relaunch this script.
michael@0 78 '''
michael@0 79
michael@0 80 UPGRADE_XCODE_COMMAND_LINE_TOOLS = '''
michael@0 81 An old version of the Xcode command line tools is installed. You will need to
michael@0 82 install a newer version in order to compile Firefox. If Xcode itself is old,
michael@0 83 its command line tools may be too old even if it claims there are no updates
michael@0 84 available, so if you are seeing this message multiple times, please update
michael@0 85 Xcode first.
michael@0 86 '''
michael@0 87
michael@0 88 PACKAGE_MANAGER_INSTALL = '''
michael@0 89 We will install the %s package manager to install required packages.
michael@0 90
michael@0 91 You will be prompted to install %s with its default settings. If you
michael@0 92 would prefer to do this manually, hit CTRL+c, install %s yourself, ensure
michael@0 93 "%s" is in your $PATH, and relaunch bootstrap.
michael@0 94 '''
michael@0 95
michael@0 96 PACKAGE_MANAGER_PACKAGES = '''
michael@0 97 We are now installing all required packages via %s. You will see a lot of
michael@0 98 output as packages are built.
michael@0 99 '''
michael@0 100
michael@0 101 PACKAGE_MANAGER_OLD_CLANG = '''
michael@0 102 We require a newer compiler than what is provided by your version of Xcode.
michael@0 103
michael@0 104 We will install a modern version of Clang through %s.
michael@0 105 '''
michael@0 106
michael@0 107 PACKAGE_MANAGER_CHOICE = '''
michael@0 108 Please choose a package manager you'd like:
michael@0 109 1. Homebrew
michael@0 110 2. MacPorts
michael@0 111 Your choice:
michael@0 112 '''
michael@0 113
michael@0 114 NO_PACKAGE_MANAGER_WARNING = '''
michael@0 115 It seems you don't have any supported package manager installed.
michael@0 116 '''
michael@0 117
michael@0 118 PACKAGE_MANAGER_EXISTS = '''
michael@0 119 Looks like you have %s installed. We will install all required packages via %s.
michael@0 120 '''
michael@0 121
michael@0 122 MULTI_PACKAGE_MANAGER_EXISTS = '''
michael@0 123 It looks like you have multiple package managers installed.
michael@0 124 '''
michael@0 125
michael@0 126 # May add support for other package manager on os x.
michael@0 127 PACKAGE_MANAGER = {'Homebrew': 'brew',
michael@0 128 'MacPorts': 'port'}
michael@0 129
michael@0 130 PACKAGE_MANAGER_CHOICES = ['Homebrew', 'MacPorts']
michael@0 131
michael@0 132 PACKAGE_MANAGER_BIN_MISSING = '''
michael@0 133 A package manager is installed. However, your current shell does
michael@0 134 not know where to find '%s' yet. You'll need to start a new shell
michael@0 135 to pick up the environment changes so it can be found.
michael@0 136
michael@0 137 Please start a new shell or terminal window and run this
michael@0 138 bootstrapper again.
michael@0 139
michael@0 140 If this problem persists, you will likely want to adjust your
michael@0 141 shell's init script (e.g. ~/.bash_profile) to export a PATH
michael@0 142 environment variable containing the location of your package
michael@0 143 manager binary. e.g.
michael@0 144
michael@0 145 export PATH=/usr/local/bin:$PATH
michael@0 146 '''
michael@0 147
michael@0 148 BAD_PATH_ORDER = '''
michael@0 149 Your environment's PATH variable lists a system path directory (%s)
michael@0 150 before the path to your package manager's binaries (%s).
michael@0 151 This means that the package manager's binaries likely won't be
michael@0 152 detected properly.
michael@0 153
michael@0 154 Modify your shell's configuration (e.g. ~/.profile or
michael@0 155 ~/.bash_profile) to have %s appear in $PATH before %s. e.g.
michael@0 156
michael@0 157 export PATH=%s:$PATH
michael@0 158
michael@0 159 Once this is done, start a new shell (likely Command+T) and run
michael@0 160 this bootstrap again.
michael@0 161 '''
michael@0 162
michael@0 163
michael@0 164 class OSXBootstrapper(BaseBootstrapper):
michael@0 165 def __init__(self, version):
michael@0 166 BaseBootstrapper.__init__(self)
michael@0 167
michael@0 168 self.os_version = StrictVersion(version)
michael@0 169
michael@0 170 if self.os_version < StrictVersion('10.6'):
michael@0 171 raise Exception('OS X 10.6 or above is required.')
michael@0 172
michael@0 173 self.minor_version = version.split('.')[1]
michael@0 174
michael@0 175 def install_system_packages(self):
michael@0 176 self.ensure_xcode()
michael@0 177
michael@0 178 choice = self.ensure_package_manager()
michael@0 179 self.package_manager = choice
michael@0 180 getattr(self, 'ensure_%s_packages' % choice)()
michael@0 181
michael@0 182 def ensure_xcode(self):
michael@0 183 if self.os_version < StrictVersion('10.7'):
michael@0 184 if not os.path.exists('/Developer/Applications/Xcode.app'):
michael@0 185 print(XCODE_REQUIRED_LEGACY)
michael@0 186
michael@0 187 subprocess.check_call(['open', XCODE_LEGACY])
michael@0 188 sys.exit(1)
michael@0 189
michael@0 190 # OS X 10.7 have Xcode come from the app store. However, users can
michael@0 191 # still install Xcode into any arbitrary location. We honor the
michael@0 192 # location of Xcode as set by xcode-select. This should also pick up
michael@0 193 # developer preview releases of Xcode, which can be installed into
michael@0 194 # paths like /Applications/Xcode5-DP6.app.
michael@0 195 elif self.os_version >= StrictVersion('10.7'):
michael@0 196 select = self.which('xcode-select')
michael@0 197 try:
michael@0 198 output = self.check_output([select, '--print-path'],
michael@0 199 stderr=subprocess.STDOUT)
michael@0 200 except subprocess.CalledProcessError as e:
michael@0 201 # This seems to appear on fresh OS X machines before any Xcode
michael@0 202 # has been installed. It may only occur on OS X 10.9 and later.
michael@0 203 if 'unable to get active developer directory' in e.output:
michael@0 204 print(XCODE_NO_DEVELOPER_DIRECTORY)
michael@0 205 self._install_xcode_app_store()
michael@0 206 assert False # Above should exit.
michael@0 207
michael@0 208 # This isn't the most robust check in the world. It relies on the
michael@0 209 # default value not being in an application bundle, which seems to
michael@0 210 # hold on at least Mavericks.
michael@0 211 if '.app/' not in output:
michael@0 212 print(XCODE_REQUIRED)
michael@0 213 self._install_xcode_app_store()
michael@0 214 assert False # Above should exit.
michael@0 215
michael@0 216 # Once Xcode is installed, you need to agree to the license before you can
michael@0 217 # use it.
michael@0 218 try:
michael@0 219 output = self.check_output(['/usr/bin/xcrun', 'clang'],
michael@0 220 stderr=subprocess.STDOUT)
michael@0 221 except subprocess.CalledProcessError as e:
michael@0 222 if 'license' in e.output:
michael@0 223 xcodebuild = self.which('xcodebuild')
michael@0 224 try:
michael@0 225 subprocess.check_call([xcodebuild, '-license'],
michael@0 226 stderr=subprocess.STDOUT)
michael@0 227 except subprocess.CalledProcessError as e:
michael@0 228 if 'requires admin privileges' in e.output:
michael@0 229 self.run_as_root([xcodebuild, '-license'])
michael@0 230
michael@0 231 # Even then we're not done! We need to install the Xcode command line tools.
michael@0 232 # As of Mountain Lion, apparently the only way to do this is to go through a
michael@0 233 # menu dialog inside Xcode itself. We're not making this up.
michael@0 234 if self.os_version >= StrictVersion('10.7'):
michael@0 235 if not os.path.exists('/usr/bin/clang'):
michael@0 236 print(XCODE_COMMAND_LINE_TOOLS_MISSING)
michael@0 237 print(INSTALL_XCODE_COMMAND_LINE_TOOLS_STEPS)
michael@0 238 sys.exit(1)
michael@0 239
michael@0 240 output = self.check_output(['/usr/bin/clang', '--version'])
michael@0 241 match = RE_CLANG_VERSION.search(output)
michael@0 242 if match is None:
michael@0 243 raise Exception('Could not determine Clang version.')
michael@0 244
michael@0 245 version = StrictVersion(match.group(1))
michael@0 246
michael@0 247 if version < APPLE_CLANG_MINIMUM_VERSION:
michael@0 248 print(UPGRADE_XCODE_COMMAND_LINE_TOOLS)
michael@0 249 print(INSTALL_XCODE_COMMAND_LINE_TOOLS_STEPS)
michael@0 250 sys.exit(1)
michael@0 251
michael@0 252 def _install_xcode_app_store(self):
michael@0 253 subprocess.check_call(['open', XCODE_APP_STORE])
michael@0 254 print('Once the install has finished, please relaunch this script.')
michael@0 255 sys.exit(1)
michael@0 256
michael@0 257 def ensure_homebrew_packages(self):
michael@0 258 self.brew = self.which('brew')
michael@0 259 assert self.brew is not None
michael@0 260
michael@0 261 installed = self.check_output([self.brew, 'list']).split()
michael@0 262
michael@0 263 packages = [
michael@0 264 # We need to install Python because Mercurial requires the Python
michael@0 265 # development headers which are missing from OS X (at least on
michael@0 266 # 10.8) and because the build system wants a version newer than
michael@0 267 # what Apple ships.
michael@0 268 ('python', 'python'),
michael@0 269 ('mercurial', 'mercurial'),
michael@0 270 ('git', 'git'),
michael@0 271 ('yasm', 'yasm'),
michael@0 272 ('autoconf213', HOMEBREW_AUTOCONF213),
michael@0 273 ]
michael@0 274
michael@0 275 # terminal-notifier is only available in Mountain Lion or newer.
michael@0 276 if self.os_version >= StrictVersion('10.8'):
michael@0 277 packages.append(('terminal-notifier', 'terminal-notifier'))
michael@0 278
michael@0 279 printed = False
michael@0 280
michael@0 281 for name, package in packages:
michael@0 282 if name in installed:
michael@0 283 continue
michael@0 284
michael@0 285 if not printed:
michael@0 286 print(PACKAGE_MANAGER_PACKAGES % ('Homebrew',))
michael@0 287 printed = True
michael@0 288
michael@0 289 subprocess.check_call([self.brew, '-v', 'install', package])
michael@0 290
michael@0 291 if self.os_version < StrictVersion('10.7') and 'llvm' not in installed:
michael@0 292 print(PACKAGE_MANAGER_OLD_CLANG % ('Homebrew',))
michael@0 293
michael@0 294 subprocess.check_call([self.brew, '-v', 'install', 'llvm',
michael@0 295 '--with-clang', '--all-targets'])
michael@0 296
michael@0 297 def ensure_macports_packages(self):
michael@0 298 self.port = self.which('port')
michael@0 299 assert self.port is not None
michael@0 300
michael@0 301 installed = set(self.check_output([self.port, 'installed']).split())
michael@0 302
michael@0 303 packages = ['python27',
michael@0 304 'mercurial',
michael@0 305 'yasm',
michael@0 306 'libidl',
michael@0 307 'autoconf213']
michael@0 308
michael@0 309 missing = [package for package in packages if package not in installed]
michael@0 310 if missing:
michael@0 311 print(PACKAGE_MANAGER_PACKAGES % ('MacPorts',))
michael@0 312 self.run_as_root([self.port, '-v', 'install'] + missing)
michael@0 313
michael@0 314 if self.os_version < StrictVersion('10.7') and MACPORTS_CLANG_PACKAGE not in installed:
michael@0 315 print(PACKAGE_MANAGER_OLD_CLANG % ('MacPorts',))
michael@0 316 self.run_as_root([self.port, '-v', 'install', MACPORTS_CLANG_PACKAGE])
michael@0 317
michael@0 318 self.run_as_root([self.port, 'select', '--set', 'python', 'python27'])
michael@0 319 self.run_as_root([self.port, 'select', '--set', 'clang', 'mp-' + MACPORTS_CLANG_PACKAGE])
michael@0 320
michael@0 321 def ensure_package_manager(self):
michael@0 322 '''
michael@0 323 Search package mgr in sys.path, if none is found, prompt the user to install one.
michael@0 324 If only one is found, use that one. If both are found, prompt the user to choose
michael@0 325 one.
michael@0 326 '''
michael@0 327 installed = []
michael@0 328 for name, cmd in PACKAGE_MANAGER.iteritems():
michael@0 329 if self.which(cmd) is not None:
michael@0 330 installed.append(name)
michael@0 331
michael@0 332 active_name, active_cmd = None, None
michael@0 333
michael@0 334 if not installed:
michael@0 335 print(NO_PACKAGE_MANAGER_WARNING)
michael@0 336 choice = self.prompt_int(prompt=PACKAGE_MANAGER_CHOICE, low=1, high=2)
michael@0 337 active_name = PACKAGE_MANAGER_CHOICES[choice - 1]
michael@0 338 active_cmd = PACKAGE_MANAGER[active_name]
michael@0 339 getattr(self, 'install_%s' % active_name.lower())()
michael@0 340 elif len(installed) == 1:
michael@0 341 print(PACKAGE_MANAGER_EXISTS % (installed[0], installed[0]))
michael@0 342 active_name = installed[0]
michael@0 343 active_cmd = PACKAGE_MANAGER[active_name]
michael@0 344 else:
michael@0 345 print(MULTI_PACKAGE_MANAGER_EXISTS)
michael@0 346 choice = self.prompt_int(prompt=PACKAGE_MANAGER_CHOICE, low=1, high=2)
michael@0 347
michael@0 348 active_name = PACKAGE_MANAGER_CHOICES[choice - 1]
michael@0 349 active_cmd = PACKAGE_MANAGER[active_name]
michael@0 350
michael@0 351 # Ensure the active package manager is in $PATH and it comes before
michael@0 352 # /usr/bin. If it doesn't come before /usr/bin, we'll pick up system
michael@0 353 # packages before package manager installed packages and the build may
michael@0 354 # break.
michael@0 355 p = self.which(active_cmd)
michael@0 356 if not p:
michael@0 357 print(PACKAGE_MANAGER_BIN_MISSING % active_cmd)
michael@0 358 sys.exit(1)
michael@0 359
michael@0 360 p_dir = os.path.dirname(p)
michael@0 361 for path in os.environ['PATH'].split(os.pathsep):
michael@0 362 if path == p_dir:
michael@0 363 break
michael@0 364
michael@0 365 for check in ('/bin', '/usr/bin'):
michael@0 366 if path == check:
michael@0 367 print(BAD_PATH_ORDER % (check, p_dir, p_dir, check, p_dir))
michael@0 368 sys.exit(1)
michael@0 369
michael@0 370 return active_name.lower()
michael@0 371
michael@0 372 def install_homebrew(self):
michael@0 373 print(PACKAGE_MANAGER_INSTALL % ('Homebrew', 'Homebrew', 'Homebrew', 'brew'))
michael@0 374 bootstrap = urlopen(url=HOMEBREW_BOOTSTRAP, timeout=20).read()
michael@0 375 with tempfile.NamedTemporaryFile() as tf:
michael@0 376 tf.write(bootstrap)
michael@0 377 tf.flush()
michael@0 378
michael@0 379 subprocess.check_call(['ruby', tf.name])
michael@0 380
michael@0 381 def install_macports(self):
michael@0 382 url = MACPORTS_URL.get(self.minor_version, None)
michael@0 383 if not url:
michael@0 384 raise Exception('We do not have a MacPorts install URL for your '
michael@0 385 'OS X version. You will need to install MacPorts manually.')
michael@0 386
michael@0 387 print(PACKAGE_MANAGER_INSTALL % ('MacPorts', 'MacPorts', 'MacPorts', 'port'))
michael@0 388 pkg = urlopen(url=url, timeout=300).read()
michael@0 389 with tempfile.NamedTemporaryFile(suffix='.pkg') as tf:
michael@0 390 tf.write(pkg)
michael@0 391 tf.flush()
michael@0 392
michael@0 393 self.run_as_root(['installer', '-pkg', tf.name, '-target', '/'])
michael@0 394
michael@0 395 def _update_package_manager(self):
michael@0 396 if self.package_manager == 'homebrew':
michael@0 397 subprocess.check_call([self.brew, '-v', 'update'])
michael@0 398 else:
michael@0 399 assert self.package_manager == 'macports'
michael@0 400 self.run_as_root([self.port, 'selfupdate'])
michael@0 401
michael@0 402 def _upgrade_package(self, package):
michael@0 403 self._ensure_package_manager_updated()
michael@0 404
michael@0 405 if self.package_manager == 'homebrew':
michael@0 406 try:
michael@0 407 subprocess.check_output([self.brew, '-v', 'upgrade', package],
michael@0 408 stderr=subprocess.STDOUT)
michael@0 409 except subprocess.CalledProcessError as e:
michael@0 410 if 'already installed' not in e.output:
michael@0 411 raise
michael@0 412 else:
michael@0 413 assert self.package_manager == 'macports'
michael@0 414
michael@0 415 self.run_as_root([self.port, 'upgrade', package])
michael@0 416
michael@0 417 def upgrade_mercurial(self, current):
michael@0 418 self._upgrade_package('mercurial')
michael@0 419
michael@0 420 def upgrade_python(self, current):
michael@0 421 if self.package_manager == 'homebrew':
michael@0 422 self._upgrade_package('python')
michael@0 423 else:
michael@0 424 self._upgrade_package('python27')
michael@0 425

mercurial