python/mozboot/mozboot/osx.py

Thu, 15 Jan 2015 21:03:48 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 21:03:48 +0100
branch
TOR_BUG_9701
changeset 11
deefc01c0e14
permissions
-rw-r--r--

Integrate friendly tips from Tor colleagues to make (or not) 4.5 alpha 3;
This includes removal of overloaded (but unused) methods, and addition of
a overlooked call to DataStruct::SetData(nsISupports, uint32_t, bool.)

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

mercurial