Home | History | Annotate | Download | only in go
      1 #!/usr/bin/env python
      2 # Copyright 2014 The Chromium 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 # Modified from go/bootstrap.py in Chromium infrastructure's repository to patch
      7 # out everything but the core toolchain.
      8 #
      9 # https://chromium.googlesource.com/infra/infra/
     10 
     11 """Prepares a local hermetic Go installation.
     12 
     13 - Downloads and unpacks the Go toolset in ../golang.
     14 """
     15 
     16 import contextlib
     17 import logging
     18 import os
     19 import platform
     20 import shutil
     21 import stat
     22 import subprocess
     23 import sys
     24 import tarfile
     25 import tempfile
     26 import urllib
     27 import zipfile
     28 
     29 # TODO(vadimsh): Migrate to new golang.org/x/ paths once Golang moves to
     30 # git completely.
     31 
     32 LOGGER = logging.getLogger(__name__)
     33 
     34 
     35 # /path/to/util/bot
     36 ROOT = os.path.dirname(os.path.abspath(__file__))
     37 
     38 # Where to install Go toolset to. GOROOT would be <TOOLSET_ROOT>/go.
     39 TOOLSET_ROOT = os.path.join(os.path.dirname(ROOT), 'golang')
     40 
     41 # Default workspace with infra go code.
     42 WORKSPACE = os.path.join(ROOT, 'go')
     43 
     44 # Platform depended suffix for executable files.
     45 EXE_SFX = '.exe' if sys.platform == 'win32' else ''
     46 
     47 # Pinned version of Go toolset to download.
     48 TOOLSET_VERSION = 'go1.4'
     49 
     50 # Platform dependent portion of a download URL. See http://golang.org/dl/.
     51 TOOLSET_VARIANTS = {
     52   ('darwin', 'x86-32'): 'darwin-386-osx10.8.tar.gz',
     53   ('darwin', 'x86-64'): 'darwin-amd64-osx10.8.tar.gz',
     54   ('linux2', 'x86-32'): 'linux-386.tar.gz',
     55   ('linux2', 'x86-64'): 'linux-amd64.tar.gz',
     56   ('win32', 'x86-32'): 'windows-386.zip',
     57   ('win32', 'x86-64'): 'windows-amd64.zip',
     58 }
     59 
     60 # Download URL root.
     61 DOWNLOAD_URL_PREFIX = 'https://storage.googleapis.com/golang'
     62 
     63 
     64 class Failure(Exception):
     65   """Bootstrap failed."""
     66 
     67 
     68 def get_toolset_url():
     69   """URL of a platform specific Go toolset archive."""
     70   # TODO(vadimsh): Support toolset for cross-compilation.
     71   arch = {
     72     'amd64': 'x86-64',
     73     'x86_64': 'x86-64',
     74     'i386': 'x86-32',
     75     'x86': 'x86-32',
     76   }.get(platform.machine().lower())
     77   variant = TOOLSET_VARIANTS.get((sys.platform, arch))
     78   if not variant:
     79     # TODO(vadimsh): Compile go lang from source.
     80     raise Failure('Unrecognized platform')
     81   return '%s/%s.%s' % (DOWNLOAD_URL_PREFIX, TOOLSET_VERSION, variant)
     82 
     83 
     84 def read_file(path):
     85   """Returns contents of a given file or None if not readable."""
     86   assert isinstance(path, (list, tuple))
     87   try:
     88     with open(os.path.join(*path), 'r') as f:
     89       return f.read()
     90   except IOError:
     91     return None
     92 
     93 
     94 def write_file(path, data):
     95   """Writes |data| to a file."""
     96   assert isinstance(path, (list, tuple))
     97   with open(os.path.join(*path), 'w') as f:
     98     f.write(data)
     99 
    100 
    101 def remove_directory(path):
    102   """Recursively removes a directory."""
    103   assert isinstance(path, (list, tuple))
    104   p = os.path.join(*path)
    105   if not os.path.exists(p):
    106     return
    107   LOGGER.info('Removing %s', p)
    108   # Crutch to remove read-only file (.git/* in particular) on Windows.
    109   def onerror(func, path, _exc_info):
    110     if not os.access(path, os.W_OK):
    111       os.chmod(path, stat.S_IWUSR)
    112       func(path)
    113     else:
    114       raise
    115   shutil.rmtree(p, onerror=onerror if sys.platform == 'win32' else None)
    116 
    117 
    118 def install_toolset(toolset_root, url):
    119   """Downloads and installs Go toolset.
    120 
    121   GOROOT would be <toolset_root>/go/.
    122   """
    123   if not os.path.exists(toolset_root):
    124     os.makedirs(toolset_root)
    125   pkg_path = os.path.join(toolset_root, url[url.rfind('/')+1:])
    126 
    127   LOGGER.info('Downloading %s...', url)
    128   download_file(url, pkg_path)
    129 
    130   LOGGER.info('Extracting...')
    131   if pkg_path.endswith('.zip'):
    132     with zipfile.ZipFile(pkg_path, 'r') as f:
    133       f.extractall(toolset_root)
    134   elif pkg_path.endswith('.tar.gz'):
    135     with tarfile.open(pkg_path, 'r:gz') as f:
    136       f.extractall(toolset_root)
    137   else:
    138     raise Failure('Unrecognized archive format')
    139 
    140   LOGGER.info('Validating...')
    141   if not check_hello_world(toolset_root):
    142     raise Failure('Something is not right, test program doesn\'t work')
    143 
    144 
    145 def download_file(url, path):
    146   """Fetches |url| to |path|."""
    147   last_progress = [0]
    148   def report(a, b, c):
    149     progress = int(a * b * 100.0 / c)
    150     if progress != last_progress[0]:
    151       print >> sys.stderr, 'Downloading... %d%%' % progress
    152       last_progress[0] = progress
    153   # TODO(vadimsh): Use something less crippled, something that validates SSL.
    154   urllib.urlretrieve(url, path, reporthook=report)
    155 
    156 
    157 @contextlib.contextmanager
    158 def temp_dir(path):
    159   """Creates a temporary directory, then deletes it."""
    160   tmp = tempfile.mkdtemp(dir=path)
    161   try:
    162     yield tmp
    163   finally:
    164     remove_directory([tmp])
    165 
    166 
    167 def check_hello_world(toolset_root):
    168   """Compiles and runs 'hello world' program to verify that toolset works."""
    169   with temp_dir(toolset_root) as tmp:
    170     path = os.path.join(tmp, 'hello.go')
    171     write_file([path], r"""
    172         package main
    173         func main() { println("hello, world\n") }
    174     """)
    175     out = subprocess.check_output(
    176         [get_go_exe(toolset_root), 'run', path],
    177         env=get_go_environ(toolset_root, tmp),
    178         stderr=subprocess.STDOUT)
    179     if out.strip() != 'hello, world':
    180       LOGGER.error('Failed to run sample program:\n%s', out)
    181       return False
    182     return True
    183 
    184 
    185 def ensure_toolset_installed(toolset_root):
    186   """Installs or updates Go toolset if necessary.
    187 
    188   Returns True if new toolset was installed.
    189   """
    190   installed = read_file([toolset_root, 'INSTALLED_TOOLSET'])
    191   available = get_toolset_url()
    192   if installed == available:
    193     LOGGER.debug('Go toolset is up-to-date: %s', TOOLSET_VERSION)
    194     return False
    195 
    196   LOGGER.info('Installing Go toolset.')
    197   LOGGER.info('  Old toolset is %s', installed)
    198   LOGGER.info('  New toolset is %s', available)
    199   remove_directory([toolset_root])
    200   install_toolset(toolset_root, available)
    201   LOGGER.info('Go toolset installed: %s', TOOLSET_VERSION)
    202   write_file([toolset_root, 'INSTALLED_TOOLSET'], available)
    203   return True
    204 
    205 
    206 def get_go_environ(
    207     toolset_root,
    208     workspace=None):
    209   """Returns a copy of os.environ with added GO* environment variables.
    210 
    211   Overrides GOROOT, GOPATH and GOBIN. Keeps everything else. Idempotent.
    212 
    213   Args:
    214     toolset_root: GOROOT would be <toolset_root>/go.
    215     workspace: main workspace directory or None if compiling in GOROOT.
    216   """
    217   env = os.environ.copy()
    218   env['GOROOT'] = os.path.join(toolset_root, 'go')
    219   if workspace:
    220     env['GOBIN'] = os.path.join(workspace, 'bin')
    221   else:
    222     env.pop('GOBIN', None)
    223 
    224   all_go_paths = []
    225   if workspace:
    226     all_go_paths.append(workspace)
    227   env['GOPATH'] = os.pathsep.join(all_go_paths)
    228 
    229   # New PATH entries.
    230   paths_to_add = [
    231     os.path.join(env['GOROOT'], 'bin'),
    232     env.get('GOBIN'),
    233   ]
    234 
    235   # Make sure not to add duplicates entries to PATH over and over again when
    236   # get_go_environ is invoked multiple times.
    237   path = env['PATH'].split(os.pathsep)
    238   paths_to_add = [p for p in paths_to_add if p and p not in path]
    239   env['PATH'] = os.pathsep.join(paths_to_add + path)
    240 
    241   return env
    242 
    243 
    244 def get_go_exe(toolset_root):
    245   """Returns path to go executable."""
    246   return os.path.join(toolset_root, 'go', 'bin', 'go' + EXE_SFX)
    247 
    248 
    249 def bootstrap(logging_level):
    250   """Installs all dependencies in default locations.
    251 
    252   Supposed to be called at the beginning of some script (it modifies logger).
    253 
    254   Args:
    255     logging_level: logging level of bootstrap process.
    256   """
    257   logging.basicConfig()
    258   LOGGER.setLevel(logging_level)
    259   ensure_toolset_installed(TOOLSET_ROOT)
    260 
    261 
    262 def prepare_go_environ():
    263   """Returns dict with environment variables to set to use Go toolset.
    264 
    265   Installs or updates the toolset if necessary.
    266   """
    267   bootstrap(logging.INFO)
    268   return get_go_environ(TOOLSET_ROOT, WORKSPACE)
    269 
    270 
    271 def find_executable(name, workspaces):
    272   """Returns full path to an executable in some bin/ (in GOROOT or GOBIN)."""
    273   basename = name
    274   if EXE_SFX and basename.endswith(EXE_SFX):
    275     basename = basename[:-len(EXE_SFX)]
    276   roots = [os.path.join(TOOLSET_ROOT, 'go', 'bin')]
    277   for path in workspaces:
    278     roots.extend([
    279       os.path.join(path, 'bin'),
    280     ])
    281   for root in roots:
    282     full_path = os.path.join(root, basename + EXE_SFX)
    283     if os.path.exists(full_path):
    284       return full_path
    285   return name
    286 
    287 
    288 def main(args):
    289   if args:
    290     print >> sys.stderr, sys.modules[__name__].__doc__,
    291     return 2
    292   bootstrap(logging.DEBUG)
    293   return 0
    294 
    295 
    296 if __name__ == '__main__':
    297   sys.exit(main(sys.argv[1:]))
    298