#!/usr/bin/env python
# -*- mode: python; coding: utf-8; indent-tabs-mode: nil; python-indent: 2 -*-

# Running bootstrap requires a minimum Python version of 2.6.

# To download this file:
# wget https://raw.githubusercontent.com/cctbx/cctbx_project/master/libtbx/auto_build/bootstrap.py
# or
# curl https://raw.githubusercontent.com/cctbx/cctbx_project/master/libtbx/auto_build/bootstrap.py > bootstrap.py

# Environment variables:
#   CCTBX_SKIP_CHEMDATA_CACHE_REBUILD - if this exists, the rotarama and cablam caches are not rebuilt

from __future__ import absolute_import, division, print_function

import ntpath
import os
import os.path
import platform
import posixpath
import re
import shutil
import socket as pysocket
import stat
import subprocess
import sys
import tarfile
import tempfile
import textwrap
import time
import traceback
try: # Python 3
    from urllib.parse import urlparse
    from urllib.request import urlopen, Request
    from urllib.error import HTTPError, URLError
except ImportError: # Python 2
    from urlparse import urlparse
    from urllib2 import urlopen, Request, HTTPError, URLError
import zipfile

try:
  import argparse
except ImportError:
  raise RuntimeError("""
The argparse module is required. If you are using Python 2.6, you may
need to install it separately. On CentOS 6, you can run

  yum install python-argpase

with administrative privileges.
""")

_BUILD_DIR = "build"  # set by arg parser further on down

windows_remove_list = []

rosetta_version_tar_bundle='rosetta_src_2018.33.60351_bundle'
rosetta_version_directory=rosetta_version_tar_bundle
# LICENSE REQUIRED
afitt_version="AFITT-2.4.0.4-redhat-RHEL7-x64" #binary specific to cci-vm-1
envs = {
  "PHENIX_ROSETTA_PATH" : ["modules", "rosetta"],
  "OE_EXE"              : ["modules", "openeye", "bin"],
  "OE_LICENSE"          : ["oe_license.txt"], # needed for license
}

# Utility function to be executed on slave machine or called directly by standalone bootstrap script
def tar_extract(workdir, archive, modulename=None):
  try:
    # delete tar target folder if it exists
    if modulename and os.path.exists(modulename):
      def remShut(*args):
        func, path, _ = args # onerror returns a tuple containing function, path and exception info
        os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
        os.remove(path)
      shutil.rmtree(modulename, onerror=remShut)
      # hack to work around possible race condition on Windows where deleted files may briefly
      # exist as phantoms and result in "access denied" error by subsequent IO operations
      cnt=0
      while os.path.exists(modulename):
        time.sleep(1)
        cnt = cnt + 1
        if cnt > 5:
          break
    # using tarfile module rather than unix tar command which is not platform independent
    tar = tarfile.open(os.path.join(workdir, archive), errorlevel=2)
    tar.extractall(path=workdir)
    tarfoldername = os.path.join(workdir, os.path.commonprefix(tar.getnames()).split('/')[0])
    tar.close()
    # take full permissions on all extracted files
    module = os.path.join(workdir, tarfoldername)
    for root, dirs, files in os.walk(module):
      for fname in files:
        full_path = os.path.join(root, fname)
        os.chmod(full_path, stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IROTH)
    # rename to expected folder name, e.g. boost_hot -> boost
    # only rename if folder names differ
    if modulename:
      if modulename != tarfoldername:
        os.rename(tarfoldername, modulename)
  except Exception as e:
    raise Exception("Extracting tar archive resulted in error: " + str(e) + "\n" \
      + traceback.format_exc())
    return 1
  return 0

# Mock commands to run standalone, without buildbot.
class ShellCommand(object):
  def __init__(self, **kwargs):
    self.kwargs = kwargs

  def get_command(self):
    return self.kwargs['command']

  def get_description(self):
    if 'description' in self.kwargs:
      return self.kwargs['description']
    return None

  def get_workdir(self):
    return self.kwargs.get('workdir', _BUILD_DIR)

  def get_environment(self):
    # gets environment from kwargs
    env = self.kwargs.get('env', None)
    if env:
      for key, item in env.items():
        if item is None:
          env[key] = ''
        else:
          env[key] = os.path.abspath(item)
      rc = os.environ
      rc.update(env)
      env=rc
    return env

  def run(self):
    t0=time.time()
    command = self.get_command()
    description = self.get_description()
    workdir = self.get_workdir()
    env = self.get_environment()
    if not self.kwargs.get("quiet", False):
      if description:
        print("===== Running in %s:"%workdir, description)
      else:
        print("===== Running in %s:"%workdir, " ".join(command))
    if workdir:
      try:
        os.makedirs(workdir)
      except OSError:
        pass
    if command[0] == 'tar':
      # don't think any builders explicitly calls tar but leave it here just in case
      modname = None
      if len(command) > 3 and command[3]:
        modname = command[3]
      return tar_extract(workdir, command[2], modname)
    if command[0] == 'rm':
      # XXX use shutil rather than rm which is not platform independent
      for directory in command[2:]:
        if os.path.exists(directory):
          print('Deleting directory : %s' % directory)
          try: shutil.rmtree(directory)
          except OSError:
            print("Strangely couldn't delete %s" % directory)
      return 0
    if 0:
      print('command',command)
      print('workdir',workdir)
      print('env',env)
      print(os.environ.get("PATH", None))
    try:
      #if not os.path.isabs(command[0]):
        # executable path isn't located relative to workdir
      #  command[0] = os.path.join(workdir, command[0])
      stderr, stdout = None, None
      if self.kwargs.get("silent", False):
        stderr = stdout = open(os.devnull, 'wb')
      p = subprocess.Popen(
        args=command,
        cwd=workdir,
        stdout=stdout,
        stderr=stderr,
        env=env,
      )
    except Exception as e: # error handling
      if not self.kwargs.get('haltOnFailure'):
        return 1
      if isinstance(e, OSError):
        if e.errno == 2:
          executable = os.path.normpath(os.path.join(workdir, command[0]))
          raise RuntimeError("Could not run %s: File not found" % executable)
      if 'child_traceback' in dir(e):
        print("Calling subprocess resulted in error; ", e.child_traceback)
      raise e

    p.wait()
    if p.returncode != 0 and self.kwargs.get('haltOnFailure'):
      print("Process failed with return code %s"%(p.returncode))
      sys.exit(1)
    if 0:
      if description:
        outl = "%s - %s" % (workdir, description)
      else:
        outl = "%s - %s" % (workdir, " ".join(command))
      print('===== Time to %s : %0.1f' % (outl, time.time()-t0))
    return p.returncode

class Toolbox(object):
  @staticmethod
  def download_to_file(url, file, log=sys.stdout, status=True, cache=True):
    """Downloads a URL to file. Returns the file size.
       Returns -1 if the downloaded file size does not match the expected file
       size
       Returns -2 if the download is skipped due to the file at the URL not
       being newer than the local copy (identified by A. matching timestamp and
       size, or B. matching etag).
    """

    # Create directory structure if necessary
    if os.path.dirname(file):
      try:
        os.makedirs(os.path.dirname(file))
      except Exception:
        pass

    localcopy = os.path.isfile(file)

    # Get existing ETag, if present
    etag = None
    tagfile = '%s/.%s.etag' % os.path.split(os.path.abspath(file))
    if cache and os.path.isfile(tagfile):
      if not localcopy:
        # Having an ETag without a file is pointless
        os.remove(tagfile)
      else:
        tf = open(tagfile, 'r')
        etag = tf.readline()
        tf.close()

    try:
      import ssl
      from ssl import SSLError
    except ImportError:
      ssl = None
      SSLError = None

    # Open connection to remote server
    try:
      if sys.platform == "win32" and 'lbl.gov' in url:
# Downloading from http://cci.lbl.gov/cctbx_dependencies caused
# SSL: CERTIFICATE_VERIFY_FAILED error on Windows only as of today (why?).
# Quick and dirty hack to disable ssl certificate verification.
        try:
          _create_unverified_https_context = ssl._create_unverified_context
        except AttributeError:
          # Legacy Python that doesn't verify HTTPS certificates by default
          pass
        except NameError:
          # ssl module was not loaded
          pass
        else:
          # Handle target environment that doesn't support HTTPS verification
          ssl._create_default_https_context = _create_unverified_https_context
      url_request = Request(url)
      if etag:
        url_request.add_header("If-None-Match", etag)
      if localcopy:
        # Shorten timeout to 7 seconds if a copy of the file is already present
        socket = urlopen(url_request, None, 7)
      else:
        socket = urlopen(url_request)
    except SSLError as e:
      # This could be a timeout
      if localcopy:
        # Download failed for some reason, but a valid local copy of
        # the file exists, so use that one instead.
        log.write("%s\n" % str(e))
        return -2
      # otherwise pass on the error message
      raise
    except (pysocket.timeout, HTTPError) as e:
      if isinstance(e, HTTPError) and etag and e.code == 304:
        # When using ETag. a 304 error means everything is fine
        log.write("local copy is current (etag)\n")
        return -2
      if localcopy:
        # Download failed for some reason, but a valid local copy of
        # the file exists, so use that one instead.
        log.write("%s\n" % str(e))
        return -2
      # otherwise pass on the error message
      raise
    except URLError as e:
      if localcopy:
        # Download failed for some reason, but a valid local copy of
        # the file exists, so use that one instead.
        log.write("%s\n" % str(e))
        return -2
      # if url fails to open, try using curl
      # temporary fix for old OpenSSL in system Python on macOS
      # https://github.com/cctbx/cctbx_project/issues/33
      command = ['/usr/bin/curl', '--http1.0', '-fLo', file, '--retry', '5', url]
      subprocess.call(command, shell=False)
      socket = None     # prevent later socket code from being run
      try:
        received = os.path.getsize(file)
      except OSError:
        raise RuntimeError("Download failed")

    if (socket is not None):
      try:
        file_size = int(socket.info().get('Content-Length'))
      except Exception:
        file_size = 0

      if os.path.isfile(tagfile):
        # ETag did not match, so delete any existing ETag.
        os.remove(tagfile)

      remote_mtime = 0
      try:
        remote_mtime = time.mktime(socket.info().getdate('last-modified'))
      except Exception:
        pass

      if (file_size > 0):
        if (remote_mtime > 0):
          # check if existing file matches remote size and timestamp
          try:
            (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(file)
            if (size == file_size) and (remote_mtime == mtime):
              log.write("local copy is current\n")
              socket.close()
              return -2
          except Exception:
            # proceed with download if timestamp/size check fails for any reason
            pass

        hr_size = (file_size, "B")
        if (hr_size[0] > 500): hr_size = (hr_size[0] / 1024, "kB")
        if (hr_size[0] > 500): hr_size = (hr_size[0] / 1024, "MB")
        log.write("%.1f %s\n" % hr_size)
        if status:
          log.write("    [0%")
          log.flush()

      received = 0
      block_size = 8192
      progress = 1
      # Allow for writing the file immediately so we can empty the buffer
      tmpfile = file + '.tmp'

      f = open(tmpfile, 'wb')
      while True:
        block = socket.read(block_size)
        received += len(block)
        f.write(block)
        if status and (file_size > 0):
          while (100 * received / file_size) > progress:
            progress += 1
            if (progress % 20) == 0:
              log.write("%d%%" % progress)
            elif (progress % 2) == 0:
              log.write(".")
            log.flush()

        if not block: break
      f.close()
      socket.close()

      if status and (file_size > 0):
        log.write("]\n")
      else:
        log.write("%d kB\n" % (received / 1024))
      log.flush()

      # Do not overwrite file during the download. If a download temporarily fails we
      # may still have a clean, working (yet older) copy of the file.
      shutil.move(tmpfile, file)

      if (file_size > 0) and (file_size != received):
        return -1

      if remote_mtime > 0:
        # set file timestamp if timestamp information is available
        from stat import ST_ATIME
        st = os.stat(file)
        atime = st[ST_ATIME] # current access time
        os.utime(file,(atime,remote_mtime))

      if cache and socket.info().get('ETag'):
        # If the server sent an ETAG, then keep it alongside the file
        open(tagfile, 'w').write(socket.info().get('ETag'))

    return received

  @staticmethod
  def unzip(archive, directory, trim_directory=0, verbose=False):
    '''unzip a file into a directory.'''
    if verbose:
      print("===== Installing %s into %s" % (archive, directory))
    if not zipfile.is_zipfile(archive):
      raise Exception("%s is not a valid .zip file" % archive)
    z = zipfile.ZipFile(archive, 'r')
    for member in z.infolist():
      is_directory = member.filename.endswith('/')
      filename = os.path.join(*member.filename.split('/')[trim_directory:])
      if filename != '':
        filename = os.path.normpath(filename)
        if '../' in filename:
          raise Exception('Archive %s contains invalid filename %s' % (archive, filename))
        filename = os.path.join(directory, filename)
        upperdirs = os.path.dirname(filename)
        try:
          if is_directory and not os.path.exists(filename):
            os.makedirs(filename)
          elif upperdirs and not os.path.exists(upperdirs):
            os.makedirs(upperdirs)
        except Exception: pass
        if not is_directory:
          source = z.open(member)
          target = open(filename, "wb")
          shutil.copyfileobj(source, target)
          target.close()
          source.close()

          # Preserve executable permission, if set
          unix_executable = member.external_attr >> 16 & 0o111
          # rwxrwxrwx => --x--x--x => 0o111
          if unix_executable:
            mode = os.stat(filename).st_mode
            mode |= (mode & 0o444) >> 2 # copy R bits to X
             # r--r--r-- => 0o444
            os.chmod(filename, mode)
    z.close()

  @staticmethod
  def set_git_repository_config_to_rebase(config):
    with open(config, 'r') as fh:
      cfg = fh.readlines()

    branch, remote, rebase = False, False, False
    insertions = []
    for n, line in enumerate(cfg):
      if line.startswith('['):
        if branch and remote and not rebase:
          insertions.insert(0, (n, branch))
        if line.startswith('[branch'):
          branch = line.split('"')[1]
        else:
          branch = False
        remote, rebase = False, False
      if re.match('remote\s*=', line.strip()):
        remote = True
      if re.match('rebase\s*=', line.strip()):
        rebase = True
    if branch and remote and not rebase:
      insertions.insert(0, (n + 1, branch))
    for n, branch in insertions:
      print("  setting branch %s to rebase" % branch)
      cfg.insert(n, '\trebase = true\n')
    with open(config, 'w') as fh:
      fh.write("".join(cfg))

  @staticmethod
  def git(module, parameters, destination=None, use_ssh=False, verbose=False, reference=None):
    '''Retrieve a git repository, either by running git directly
       or by downloading and unpacking an archive.'''
    git_available = True
    try:
      subprocess.call(['git', '--version'], stdout=open(os.devnull, 'wb'), stderr=open(os.devnull, 'wb'))
    except OSError:
      git_available = False

    if destination is None:
      destination = os.path.join('modules', module)
    destpath, destdir = os.path.split(destination)

    # default to using ssh for private phenix repositories
    if module in ['phenix', 'solve_resolve', 'phenix_pathwalker', 'labelit']:
      use_ssh = True

    if os.path.exists(destination):
      if git_available and os.path.exists(os.path.join(destination, '.git')):
        if not open(os.path.join(destination, '.git', 'HEAD'), 'r').read().startswith('ref:'):
          print("WARNING: Can not update existing git repository! You are not on a branch.")
          print("This may be legitimate when run eg. via Jenkins, but be aware that you cannot commit any changes")
          return

        else:
          # This may fail for unclean trees and merge problems. In this case manual
          # user intervention will be required.
          # For the record, you can clean up the tree and *discard ALL changes* with
          #   git reset --hard origin/master
          #   git clean -dffx
          return ShellCommand(
            command=['git', 'pull', '--rebase'], workdir=destination, silent=False, haltOnFailure=True).run()

      print("Existing non-git directory -- don't know what to do. skipping: %s" % module)
      if ('cctbx_project.git' in parameters[0]):
        print('\n' + '=' * 80 + '\nCCTBX moved to git on November 22, 2016.\n\nTo update cctbx_project to the last available subversion revision please run "svn update" while in the cctbx_project directory.\n' + '*'*80 + '\n')
      return
    if isinstance(parameters, str):
      parameters = [ parameters ]
    git_parameters = []
    for source_candidate in parameters:
      if source_candidate.startswith('-'):
        git_parameters = source_candidate.split(' ')
        continue
      if (not source_candidate.lower().startswith('http') and not use_ssh):
        continue
      if source_candidate.lower().endswith('.git'):
        if not git_available:
          continue
        reference_parameters = []
        if reference is not None:
          if os.path.exists(reference) and os.path.exists(os.path.join(reference, '.git')):
            reference_parameters = [ '--reference', reference ]
        cmd = [ 'git', 'clone', '--recursive' ] + git_parameters + [ source_candidate, destdir ] + reference_parameters
        if verbose:
          cmd = cmd + [ '--progress', '--verbose' ]
        returncode = ShellCommand(
          command=cmd, workdir=destpath, silent=False
        ).run()
        if returncode:
          return returncode # no point trying to continue on error
        if reference_parameters:
          # Sever the link between checked out and reference repository
          cmd = [ 'git', 'repack', '-a', '-d' ]
          returncode = ShellCommand(
            command=cmd, workdir=destination, silent=False
          ).run()
          try:
            os.remove(os.path.join(destination, '.git', 'objects', 'info', 'alternates'))
          except OSError:
            returncode = 1
        Toolbox.set_git_repository_config_to_rebase(os.path.join(destination, '.git', 'config'))
        if returncode:
          return returncode # no point trying to continue on error
        # Show the hash for the checked out commit for debugging purposes, ignore any failures.
        ShellCommand(
          command=[ 'git', 'rev-parse', 'HEAD' ], workdir=destination, silent=False
        ).run()
        return returncode
      filename = "%s-%s" % (module,
                            urlparse(source_candidate)[2].split('/')[-1])
      filename = os.path.join(destpath, filename)
      if verbose:
        print("===== Downloading %s: " % source_candidate, end=' ')
      Toolbox.download_to_file(source_candidate, filename)
      Toolbox.unzip(filename, destination, trim_directory=1, verbose=verbose)
      return

    error = "Cannot satisfy git dependency for module %s: None of the sources are available." % module
    if not git_available:
      print(error)
      error = "A git installation has not been found."
    raise Exception(error)

class cleanup_ext_class(object):
  def __init__(self, filename_ext, workdir=None, walk=True):
    self.filename_ext = filename_ext
    self.workdir = workdir
    self.walk = walk

  def get_command(self):
    return "delete *%s in %s" % (self.filename_ext, self.workdir).split()

  def remove_ext_files(self):
    cwd=os.getcwd()
    if self.workdir is not None:
      if os.path.exists(self.workdir):
        os.chdir(self.workdir)
      else:
        return
    print("\n  removing %s files in %s, walk? %s" % (self.filename_ext,
                                                     os.getcwd(),
                                                     self.walk,
      ))
    i=0
    if self.walk:
      for root, dirs, files in os.walk(".", topdown=False):
        for name in files:
          if name.endswith(self.filename_ext):
            os.remove(os.path.join(root, name))
            i+=1
    else:
      for name in os.listdir(os.getcwd()):
        if name.endswith(self.filename_ext):
          os.remove(os.path.join(name))
          i+=1
    os.chdir(cwd)
    print("  removed %d files" % i)

  def run(self):
    self.remove_ext_files()

class cleanup_dirs(object):
  """Command to remove unwanted subdirectories"""

  def __init__(self, dirs, workdir=None):
    """
    :param dirs:    List of subdirectories to remove from workdir
    :param workdir: The root directory for everything in dirs. If None, then
                    the command will be run in the current working directory.
    """
    self.dirs = dirs
    self.workdir = workdir

  def get_command(self):
    return "cleanup dirs in %s" % (self.workdir).split()

  def remove_dirs(self):
    cwd=os.getcwd()
    try:
      # Move to the workdir
      if self.workdir is not None:
        if os.path.exists(self.workdir):
          os.chdir(self.workdir)
        else:
          return

      # Don't notify the user if we aren't doing anything
      if any(os.path.exists(d) for d in self.dirs):
        print("===== Removing directories in %s" % (os.getcwd()))

        for d in self.dirs:
          if os.path.exists(d):
            print("      removing %s" % (os.path.join(os.getcwd(),d)))
            shutil.rmtree(d)
    finally:
      # Leave the directory untouched even if we failed
      os.chdir(cwd)

  def run(self):
    self.remove_dirs()

##### Modules #####
class SourceModule(object):
  _modules = {}
  module = None
  authenticated = None
  authentarfile = None
  anonymous = None
  def __init__(self):
    if not self._modules:
      self.update_subclasses()

  def items(self):
    return list(self._modules.items())

  @classmethod
  def update_subclasses(cls):
    for i in cls.__subclasses__():
      cls._modules[i.module] = i

  def get_module(self, module):
    if module in self._modules:
      return self._modules[module]
    raise KeyError("Unknown module: %s"%module)

  def get_url(self, auth=None):
    repo = None
    try:
      repo = self.get_authenticated(auth=auth)
    except KeyError as e:
      repo = self.get_anonymous()
      if not repo:
        raise Exception('No anonymous access method defined for module: %s. Try with --%s'%(self.module, e.args[0]))
    repo = repo or self.get_anonymous()
    if not repo:
      raise Exception('No access method defined for module: %s'%self.module)
    return repo

  def get_authenticated(self, auth=None):
    auth = auth or {}
    if not self.authenticated:
      return None
    return [self.authenticated[0], self.authenticated[1]%auth]

  def get_tarauthenticated(self, auth=None):
    auth = auth or {}
    if self.authentarfile:
      return [self.authentarfile[0]%auth, self.authentarfile[1], self.authentarfile[2]]
    return None, None, None

  def get_anonymous(self):
    return self.anonymous

# Core external repositories
# The trailing slashes ARE significant.
# These must all provide anonymous access.
# On Windows due to absence of rsync we use pscp from the Putty programs.
class ccp4io_module(SourceModule):
  module = 'ccp4io'
  anonymous = ['git',
               'git@github.com:cctbx/ccp4io.git',
               'https://github.com/cctbx/ccp4io.git',
               'https://github.com/cctbx/ccp4io/archive/master.zip']

class annlib_module(SourceModule):
  module = 'annlib'
  anonymous = ['git',
               'git@github.com:cctbx/annlib.git',
               'https://github.com/cctbx/annlib.git',
               'https://github.com/cctbx/annlib/archive/master.zip']

class scons_module(SourceModule):
  module = 'scons'
  anonymous = ['git', '-b 3.1.1',
               'https://github.com/SCons/scons/archive/3.1.1.zip']

# external modules
class rosetta_class(SourceModule):
  module = 'rosetta'
  authenticated = [
    'rsync',
    '%(cciuser)s@cci.lbl.gov:/net/cci/auto_build/externals/'+rosetta_version_tar_bundle+'/',
  ]
  authenticated = [
    'scp',
    '%(cciuser)s@cci.lbl.gov:/net/cci-filer2/raid1/auto_build/externals/'+rosetta_version_tar_bundle+'.tgz']

class afitt_class(SourceModule):
  module = 'afitt'
  authenticated = [
    'scp',
    '%(cciuser)s@cci.lbl.gov:/net/cci-filer2/raid1/auto_build/externals/'+afitt_version+'.gz']

# Core CCTBX repositories
# These must all provide anonymous access.
class cctbx_module(SourceModule):
  module = 'cctbx_project'
  anonymous = ['git',
               'git@github.com:cctbx/cctbx_project.git',
               'https://github.com/cctbx/cctbx_project.git',
               'https://github.com/cctbx/cctbx_project/archive/master.zip']

class amber_adaptbx_module(SourceModule):
  module = 'amber_adaptbx'
  anonymous = ['git',
               'git@github.com:phenix-project/amber_adaptbx.git',
               'https://github.com/phenix-project/amber_adaptbx.git',
               ]

class amber_library_module(SourceModule):
  module = 'amber_library'
  anonymous = ['git',
               'git@github.com:phenix-project/amber_library.git',
               'https://github.com/phenix-project/amber_library.git',
               ]

class qrefine_module(SourceModule):
  module = 'qrefine'
  anonymous = ['git',
               'git@github.com:qrefine/qrefine.git',
               'https://github.com/qrefine/qrefine.git',
               ]

class mon_lib_module(SourceModule):
  module = 'mon_lib'
  anonymous = ['curl', 'http://cci.lbl.gov/repositories/mon_lib.gz']
  authentarfile = ['%(cciuser)s@cci.lbl.gov', 'mon_lib.tar.gz', '/net/cci/auto_build/repositories/mon_lib']
  #authenticated = ['rsync', '%(cciuser)s@cci.lbl.gov:/net/cci/auto_build/repositories/annlib/']

class geostd_module(SourceModule):
  module = 'geostd'
  anonymous = ['git',
               'git@github.com:phenix-project/geostd.git',
               'https://github.com/phenix-project/geostd.git'
               ]

class boost_module(SourceModule):
  module = 'boost'
  anonymous = ['git',
               'git@github.com:cctbx/boost.git',
               'https://github.com/cctbx/boost.git',
               'https://github.com/cctbx/boost/archive/master.zip']

class cbflib_module(SourceModule):
  module = 'cbflib'
  anonymous = ['git',
               'git@github.com:yayahjb/cbflib.git',
               'https://github.com/yayahjb/cbflib.git',
               'https://github.com/yayahjb/cbflib/archive/main.zip']

class ccp4io_adaptbx(SourceModule):
  module = 'ccp4io_adaptbx'
  anonymous = ['git',
               'git@github.com:cctbx/ccp4io_adaptbx.git',
               'https://github.com/cctbx/ccp4io_adaptbx.git',
               'https://github.com/cctbx/ccp4io_adaptbx/archive/master.zip']

class annlib_adaptbx(SourceModule):
  module = 'annlib_adaptbx'
  anonymous = ['git',
               'git@github.com:cctbx/annlib_adaptbx.git',
               'https://github.com/cctbx/annlib_adaptbx.git',
               'https://github.com/cctbx/annlib_adaptbx/archive/master.zip']

class tntbx_module(SourceModule):
  module = 'tntbx'
  anonymous = ['git',
               'git@github.com:cctbx/tntbx.git',
               'https://github.com/cctbx/tntbx.git',
               'https://github.com/cctbx/tntbx/archive/master.zip']

class clipper_module(SourceModule):
  module = 'clipper'
  anonymous = ['git',
               'git@github.com:cctbx/clipper.git',
               'https://github.com/cctbx/clipper.git',
               'https://github.com/cctbx/clipper/archive/master.zip']

class gui_resources_module(SourceModule):
  module = 'gui_resources'
  anonymous = ['git',
               'git@github.com:cctbx/gui_resources.git',
               'https://github.com/cctbx/gui_resources.git',
               'https://github.com/cctbx/gui_resources/archive/master.zip']

class opt_resources_module(SourceModule):
  module = 'opt_resources'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/opt_resources/trunk']

class eigen_module(SourceModule):
  module = 'eigen'
  anonymous = ['git', '-b 3.4.0',
               'https://gitlab.com/libeigen/eigen.git']

# Phenix repositories
class phenix_module(SourceModule):
  module = 'phenix'
  anonymous = ['git', 'git@github.com:phenix-project/phenix.git']

class phenix_html(SourceModule):
  module = 'phenix_html'
  anonymous = ['git',
               'git@github.com:phenix-project/phenix_html.git',
               'https://github.com/phenix-project/phenix_html.git']

class phenix_dev_doc(SourceModule):
  module = 'phenix_dev_doc'
  anonymous = ['git',
               'git@github.com:phenix-project/phenix_dev_doc.git',
               'https://github.com/phenix-project/phenix_dev_doc.git']

class phenix_examples(SourceModule):
  module = 'phenix_examples'
  anonymous = ['git',
               'git@gitlab.com:phenix_project/phenix_examples.git',
               'https://gitlab.com/phenix_project/phenix_examples.git']

class phenix_regression(SourceModule):
  module = 'phenix_regression'
  anonymous = ['git',
               'git@gitlab.com:phenix_project/phenix_regression.git',
               'https://gitlab.com/phenix_project/phenix_regression.git']

class phenix_colabs(SourceModule):
  module = 'Colabs'
  anonymous = ['git',
               'git@github.com:phenix-project/Colabs.git',
               'https://github.com/phenix-project/Colabs.git']

class plex_module(SourceModule):
  module = 'Plex'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/Plex/trunk']

class pyquante_module(SourceModule):
  module = 'PyQuante'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/PyQuante/trunk']

class chem_data_module(SourceModule):
  module = 'chem_data'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/chem_data/trunk']

class elbow_module(SourceModule):
  module = 'elbow'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/elbow/trunk']

class ksdssp_module(SourceModule):
  module = 'ksdssp'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/ksdssp/trunk']

class pulchra_module(SourceModule):
  module = 'pulchra'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/pulchra/trunk']

class solve_resolve_module(SourceModule):
  module = 'solve_resolve'
  anonymous = ['git', 'git@github.com:phenix-project/solve_resolve.git']

class reel_module(SourceModule):
  module = 'reel'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/reel/trunk']

class muscle_module(SourceModule):
  module = 'muscle'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/muscle/trunk']

class cxi_xdr_xes_module(SourceModule):
  module = 'cxi_xdr_xes'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/cxi_xdr_xes/trunk']

class buildbot_module(SourceModule):
  module = 'buildbot'
  authenticated = ['svn', 'svn+ssh://%(cciuser)s@cci.lbl.gov/buildbot/trunk']

class phenix_pathwalker_module(SourceModule):
  module = 'phenix_pathwalker'
  anonymous = ['git', 'git@github.com:phenix-project/phenix_pathwalker.git']

# Phaser repositories
class phaser_module(SourceModule):
  module = 'phaser'
  anonymous = ['git',
               'git@gitlab.developers.cam.ac.uk:scm/haematology/readgroup/phaser.git',
               'https://gitlab.developers.cam.ac.uk/scm/haematology/readgroup/phaser.git']

class phasertng_module(SourceModule):
  module = 'phasertng'
  anonymous = ['git',
               'git@gitlab.developers.cam.ac.uk:scm/haematology/readgroup/phasertng.git',
               'https://gitlab.developers.cam.ac.uk/scm/haematology/readgroup/phasertng.git']

class phaser_voyager_module(SourceModule):
  module = 'phaser_voyager'
  anonymous = ['git',
               'git@gitlab.developers.cam.ac.uk:scm/haematology/readgroup/phaser_voyager.git',
               'https://gitlab.developers.cam.ac.uk/scm/haematology/readgroup/phaser_voyager.git']

class phaser_regression_module(SourceModule):
  module = 'phaser_regression'
  anonymous = ['git',
               'git@gitlab.developers.cam.ac.uk:scm/haematology/readgroup/phaser_regression.git',
               'https://gitlab.developers.cam.ac.uk/scm/haematology/readgroup/phaser_regression.git']

class voyager_regression_module(SourceModule):
  module = 'voyager_regression'
  anonymous = ['git',
               'git@gitlab.developers.cam.ac.uk:scm/haematology/readgroup/voyager_regression.git',
               'https://gitlab.developers.cam.ac.uk/scm/haematology/readgroup/voyager_regression.git']

# DIALS repositories
class labelit_module(SourceModule):
  module = 'labelit'
  anonymous = ['git', 'git@github.com:phenix-project/labelit.git']

class labelit_regression_module(SourceModule):
  module = 'labelit_regression'
  anonymous = ['git',
               'git@gitlab.com:phenix_project/labelit_regression.git',
               'https://gitlab.com/phenix_project/labelit_regression.git']

class dials_module(SourceModule):
  module = 'dials'
  anonymous = ['git',
               'git@github.com:dials/dials.git',
               'https://github.com/dials/dials.git',
               'https://github.com/dials/dials/archive/main.zip']

class dxtbx_module(SourceModule):
  module = 'dxtbx'
  anonymous = ['git',
               'git@github.com:cctbx/dxtbx.git',
               'https://github.com/cctbx/dxtbx.git',
               'https://github.com/cctbx/dxtbx/archive/main.zip']

class dials_regression_module(SourceModule):
  module = 'dials_regression'
  authenticated = ['svn',
                   'svn+ssh://%(cciuser)s@cci.lbl.gov/dials_regression/trunk']

class iota_module(SourceModule):
  module = 'iota'
  anonymous = ['git',
               'git@github.com:ssrl-px/iota.git',
               'https://github.com/ssrl-px/iota.git',
               'https://github.com/ssrl-px/iota/archive/master.zip']

class msgpack_module(SourceModule):
  module = 'msgpack'
  anonymous = ['curl', [
    "https://gitcdn.link/repo/dials/dependencies/dials-1.13/msgpack-3.1.1.tar.gz",
    "https://github.com/dials/dependencies/raw/dials-1.13/msgpack-3.1.1.tar.gz",
  ]]

class xfel_regression_module(SourceModule):
  module = 'xfel_regression'
  authenticated = ['svn',
                   'svn+ssh://%(cciuser)s@cci.lbl.gov/xfel_regression/trunk']

class xia2_module(SourceModule):
  module = 'xia2'
  anonymous = ['git',
               'git@github.com:xia2/xia2.git',
               'https://github.com/xia2/xia2.git',
               'https://github.com/xia2/xia2/archive/main.zip']

class kokkos_module(SourceModule):
  module = 'kokkos'
  anonymous = ['git', '-b 3.7.01',
               'git@github.com:kokkos/kokkos.git',
               'https://github.com/kokkos/kokkos.git',
               'https://github.com/kokkos/kokkos/archive/master.zip']

class kokkos_kernels_module(SourceModule):
  module = 'kokkos-kernels'
  anonymous = ['git', '-b 3.7.01',
               'git@github.com:kokkos/kokkos-kernels.git',
               'https://github.com/kokkos/kokkos-kernels.git',
               'https://github.com/kokkos/kokkos-kernels/archive/master.zip']

# Duke repositories
class probe_module(SourceModule):
  module = 'probe'
  anonymous = ['svn', 'https://github.com/rlabduke/probe.git/trunk']

class reduce_module(SourceModule):
  module = 'reduce'
  anonymous = ['git', '-b v3.7.2',
               'https://github.com/rlabduke/reduce.git']

class king_module(SourceModule):
  module = 'king'
  anonymous = ['svn',
               'https://github.com/rlabduke/phenix_king_binaries.git/trunk']

class molprobity_moodule(SourceModule):
  module = 'molprobity'
  anonymous = ['svn', 'https://github.com/rlabduke/MolProbity.git/trunk']

class uc_metrics_module(SourceModule):
  module = 'uc_metrics'
  anonymous = ['git',
               'git@gitlab.com:cctbx/uc_metrics.git',
               'https://gitlab.com/cctbx/uc_metrics.git']

class ncdist_module(SourceModule):
  module = 'ncdist'
  anonymous = ['git',
               'git@github.com:yayahjb/ncdist.git',
               'https://github.com/yayahjb/ncdist.git',
               'https://github.com/yayahjb/ncdist/archive/master.zip']

MODULES = SourceModule()

###################################
##### Base Configuration      #####
###################################

class Builder(object):
  """Create buildbot configurations for CCI and CCTBX-like software."""
  # Base packages
  BASE_PACKAGES = 'all'
  # Checkout these codebases
  CODEBASES = ['cctbx_project']
  CODEBASES_EXTRA = []
  # Copy these sources from cci.lbl.gov
  HOT = []
  HOT_EXTRA = []
  # Configure for these cctbx packages
  LIBTBX = ['cctbx']
  LIBTBX_EXTRA = []

  def __init__(
      self,
      category=None,
      subcategory=None,
      platform=None,
      sep=None,
      python_base=None,
      cleanup=False,
      hot=True,
      update=True,
      revert=None,
      base=True,
      build=True,
      tests=True,
      doc=True,
      distribute=False,
      auth=None,
      with_python=None,
      nproc=1,
      verbose=False,
      download_only=False,
      skip_base="",
      force_base_build=False,
      enable_shared=False,
      mpi_build=False,
      python3=False,
      wxpython4=False,
      config_flags=[],
      use_conda=None,
      python='27',
      no_boost_src=False,
    ):
    if nproc is None:
      self.nproc=1
    else:
      self.nproc=nproc
    """Create and add all the steps."""
    # self.cciuser = cciuser or getpass.getuser()
    self.set_auth(auth)
    self.steps = []
    self.category = category
    self.subcategory = subcategory
    if self.subcategory: self.EXTERNAL_CODEBASES = [self.subcategory]
    self.platform = platform
    if self.isPlatformWindows():
      self.op = ntpath
    else:
      self.op = os.path
    self.name = '%s-%s'%(self.category, self.platform)
    # Platform configuration.
    python_executable = 'python'
    self.python3 = python.startswith('3')
    if python3:
      python_executable = 'python3'
    self.wxpython4 = wxpython4
    if self.platform and ('windows' in self.platform or self.platform == 'win32'):
      python_executable = python_executable + '.exe'
    if self.platform and 'windows' in self.platform:
      self.python_base = self.opjoin(*['..', 'base', 'bin', 'python', python_executable])
    elif sys.platform == "win32": # assuming we run standalone without buildbot
      self.python_base = self.opjoin(*[os.getcwd(), 'base', 'bin', 'python', python_executable])
    else:
      self.python_base = self.opjoin(*['..', 'base', 'bin', python_executable])
    self.with_python = with_python
    if self.with_python:
      self.python_base = with_python
    self.verbose = verbose
    self.download_only = download_only
    self.skip_base = skip_base
    self.force_base_build = force_base_build
    # self.config_flags are only from the command line
    # get_libtbx_configure can still be used to always set flags specific to a
    # builder
    self.config_flags = config_flags
    self.use_conda = use_conda
    self.python = python
    self.no_boost_src = no_boost_src
    self.add_init()

    # Cleanup
    if cleanup:
      self.cleanup(['dist', 'tests', 'doc', 'tmp', 'base', 'base_tmp', _BUILD_DIR,
                    'conda_base'])
    else:
      self.cleanup(['dist', 'tests', 'tmp'])

    if self.platform and 'windows' in self.platform: # only executed by buildbot master
      from buildbot.steps.transfer import FileDownload
      # download us to folder above modules on slave so we can run the utility functions defined above
      self.add_step(FileDownload(mastersrc="bootstrap.py", slavedest="../bootstrap.py"))

    # Add 'hot' sources
    if hot:
      # conda builds do not need eigen (disabled), scons
      hot = self.get_hot()
      if self.use_conda is not None:
        for module in ['scons']:
          # SCons conda package may cause issues with procrunner on Python 2.7
          # https://stackoverflow.com/questions/24453387/scons-attributeerror-builtin-function-or-method-object-has-no-attribute-disp
          if module == 'scons' and self.python == '27':
            continue
          try:
            hot.remove(module)
          except ValueError:
            pass
      list(map(self.add_module, hot))

    # Add svn sources.
    self.revert=revert
    if update:
      # check if boost needs to be downloaded
      codebases = self.get_codebases()
      if self.no_boost_src:
        try:
          codebases.remove('boost')
        except ValueError:
          pass
      list(map(self.add_module, codebases))

    # always remove .pyc files
    self.remove_pyc()

    # Build base packages
    if base:
      extra_opts = ["--nproc=%s" % str(self.nproc)]
      if enable_shared:
        extra_opts.append("--python-shared")
      if mpi_build:
        extra_opts.append("--mpi-build")
      self.add_base(extra_opts=extra_opts)

    # Configure, make, get revision numbers
    if build and not self.download_only:
      self.add_configure()
      self.add_make()
      self.add_install()

    # Tests, tests
    if tests and not self.download_only:
      self.add_tests()

    # docs
    if doc:
      self.rebuild_docs()

    # Distribute
    if distribute and not self.download_only:
      self.add_distribute()

    # Distribute does this but uses correct PHENIX_VERSION
    if build and not self.download_only:
      self.add_dispatchers()
      self.add_refresh()

    if self.platform and 'windows' in self.platform: # only executed by buildbot master
      self.add_rm_bootstrap_on_slave()

  def isPlatformWindows(self):
    if self.platform and 'windows' in self.platform:
        return True
    else:
      if self.platform == "dev" and sys.platform == "win32":
        return True
    return False

  def isPlatformLinux(self):
    if self.platform and 'linux' in self.platform:
        return True
    else:
      if self.platform == "dev" and sys.platform.startswith("linux"):
        return True
    return False

  def isPlatformMacOSX(self):
    if self.platform and 'mac' in self.platform:
        return True
    else:
      if self.platform == "dev" and sys.platform.startswith("darwin"):
        return True
    return False

  def add_auth(self, account, username):
    self.auth[account] = username

  def set_auth(self, auth):
    self.auth = auth or {}

  def get_auth(self):
    return self.auth

  def remove_pyc(self):
    self.add_step(cleanup_ext_class(".pyc", "modules"))

  def shell(self, **kwargs):
    # Convenience for ShellCommand
    kwargs['haltOnFailure'] = kwargs.pop('haltOnFailure', True)
    kwargs['description'] = kwargs.get('description') or kwargs.get('name')
    kwargs['timeout'] = 60*60*2 # 2 hours
    if 'workdir' in kwargs:
      kwargs['workdir'] = self.opjoin(*kwargs['workdir'])
    return ShellCommand(**kwargs)

  def run(self):
    for i in self.steps:
      i.run()

  def opjoin(self, *args):
    return self.op.join(*args)

  def get_codebases(self):
    if self.isPlatformWindows():
      rc = set(self.CODEBASES+self.CODEBASES_EXTRA)
      for r in windows_remove_list: rc = rc - set([r])
      return list(rc)
    rc = self.CODEBASES + self.CODEBASES_EXTRA
    if hasattr(self, "EXTERNAL_CODEBASES"):
      rc = self.EXTERNAL_CODEBASES + rc
    return rc

  def get_hot(self):
    return self.HOT + self.HOT_EXTRA

  def get_libtbx_configure(self): # modified in derived class PhenixBuilder
    return self.LIBTBX + self.LIBTBX_EXTRA

  def add_init(self):
    pass

  def cleanup(self, dirs=None):
    dirs = dirs or []
    if self.isPlatformWindows():
      # Delete folders by copying an empty folder with ROBOCOPY is more reliable on Windows
      # If ROBOCOPY fails i.e. ERRORLEVEL>0 then bail to stop bootstrap. Start cmd.exe with
      cmd = ['cmd', '/C', 'mkdir', 'empty', '&',
         '(FOR', '%b', 'IN', '('] + dirs + [')', 'DO',
              '((ROBOCOPY', 'empty', '%b', '/MIR', '/COPYALL', '>', 'nul)',
                 '&', 'rmdir', '/S', '/Q', '%b\\', # remove directory after robocopy
                 '&', '@IF', 'EXIST', '%b\\', # backslash indicates it's a directory and not a file
                         '(echo.', '&', 'echo', 'Error', 'deleting', '%b',
                          '&', 'echo.', '&', 'exit', '/B', '42', ')))',
          '&', 'rmdir', 'empty'
       ]
      self.add_step(self.shell(
        name='Removing directories ' + ', '.join(dirs),
        command =cmd,
        workdir=['.'],
        description="deleting " + ", ".join(dirs),
      ))
    else:
      self.add_step(cleanup_dirs(dirs, "modules"))

  def add_rm_bootstrap_on_slave(self):
    # if file is not found error flag is set. Mask it with cmd shell
    cmd=['cmd', '/c', 'del', '/Q', "bootstrap.py*", '&', 'set', 'ERRORLEVEL=0']
    self.add_step(self.shell(
      name='removing bootstrap utilities',
      command =cmd,
      workdir=['.'],
      description="remove temporary bootstrap.py*",
    ))

  def add_step(self, step):
    """Add a step."""
    self.steps.append(step)
    if 0:
      print("commands "*8)
      for step in self.steps:
        print(step)
        #try:    print " ".join(step.get_command())
        #except: print '????'
      print("commands "*8)

  def add_module(self, module, workdir=None, module_directory=None):
    action = MODULES.get_module(module)().get_url(auth=self.get_auth())
    method, parameters = action[0], action[1:]
    if len(parameters) == 1: parameters = parameters[0]
    tarurl, arxname, dirpath = None, None, None
    if self.isPlatformWindows() and (method == "authenticated" or method == "rsync"):
      tarurl, arxname, dirpath = MODULES.get_module(module)().get_tarauthenticated(auth=self.get_auth())
    if self.isPlatformWindows():
      if module in windows_remove_list:
        return
    if method == 'rsync' and not self.isPlatformWindows():
      self._add_rsync(module,
                      parameters, # really the url
                      workdir=workdir,
                      module_directory=module_directory)
    elif self.isPlatformWindows() and tarurl:
      # if more bootstraps are running avoid potential race condition on
      # remote server by using unique random filenames
      randarxname = next(tempfile._get_candidate_names()) + "_" + arxname
      self._add_remote_make_tar(module, tarurl, randarxname, dirpath)
      self._add_scp(module, tarurl + ':' + randarxname)
      self._add_remote_rm_tar(module, tarurl, randarxname)
    elif method == 'scp':
      self._add_scp(module, parameters)
    elif method == 'curl':
      self._add_curl(module, parameters)
    elif method == 'svn':
      self._add_svn(module, parameters)
    elif method == 'git':
      self._add_git(module, parameters)
    else:
      raise Exception('Unknown access method: %s %s'%(method, str(parameters)))

  def _add_rsync(self, module, url, workdir=None, module_directory=None):
    """Add packages not in source control."""
    # rsync the hot packages.
    if not workdir: workdir=["modules"]
    if not module_directory: module_directory=module
    self.add_step(self.shell(
      name='hot %s'%module,
      command=[
        'rsync',
        '-rptgoDLK', #'-aL',
        '--delete',
        url,
        module_directory,
      ],
      workdir=workdir,
    ))

  def _add_remote_make_tar(self, module, tarurl, arxname, dirpath):
    """Windows: tar up hot packages for quick file transfer since there's no rsync and pscp is painfully slow"""
    if dirpath[-1] == '/':
      dirpath = dirpath[:-1]
    basename = posixpath.basename(dirpath)
    cmd=[
        'ssh',
        tarurl,
        '"' + 'cd',
        posixpath.split(dirpath)[0],
        '&&',
        'tar',
        'cfzh',
        '~/' + arxname,
        basename + '"'
      ]
    mstr= " ".join(cmd)
    self.add_step(self.shell( # pack directory with tar on remote system
      name='hot %s'%module,
      command=mstr,
      workdir=['modules'],
      description="create remote temporary archive %s:%s" %(tarurl, arxname),
    ))

  def _add_remote_rm_tar(self, module, tarurl, arxname):
    """Windows: Delete tar file on remote system, unpack tar file locally, then delete tar file locally"""
    self.add_step(self.shell( # delete the tarfile on remote system
      name='hot %s'%module,
      command=[
        'ssh',
        tarurl,
        'rm ',
        arxname
      ],
      workdir=['modules'],
      description="delete remote temporary archive of %s" %module,
    ))
    self.add_step(self.shell(command=[
      sys.executable,"-c","import sys; sys.path.append('..'); import bootstrap; \
      bootstrap.tar_extract('','%s', '%s')" %(arxname, module) ],
      workdir=['modules'],
      description="extracting archive files to %s" %module,
    ))
    self.add_step(self.shell( # delete the tarfile locally
      # use 'cmd', '/c' as a substitute for shell=True in the subprocess.Popen call
      command=['cmd', '/c', 'del', arxname],
      workdir=['modules'],
      description="delete local temporary archive of %s" %module,
    ))

  def _add_scp(self, module, url):
    self.add_step(self.shell(
      name='hot %s'%module,
      command=[
        'scp',
        '-r',
        url,
        '.',
      ],
      workdir=['modules'],
      description="getting remote file %s" %url.split("/")[-1],
    ))

  def _add_download(self, url, to_file):
    if not isinstance(url, list):
      url = [url]
    class _download(object):
      def run(self):
        for _url in url:
          for retry in (3,3,0):
            print("===== Downloading %s: " % _url, end=' ')
            try:
              Toolbox().download_to_file(_url, to_file)
              return
            except Exception as e:
              print("Download failed with", e)
              if retry:
                print("Retrying in %d seconds" % retry)
                time.sleep(retry)
        raise RuntimeError("Could not download " + to_file)
    self.add_step(_download())

  def _add_curl(self, module, url):
    if isinstance(url, list):
      filename = urlparse(url[0])[2].split('/')[-1]
    else:
      filename = urlparse(url)[2].split('/')[-1]
    # Google Drive URL does not contain the module name
    if filename == 'uc':
      filename = module + '.gz'
    self._add_download(url, os.path.join('modules', filename))
    self.add_step(self.shell(
      name="extracting files from %s" %filename,
      command=[
       sys.executable,"-c","import sys; sys.path.append('..'); import bootstrap; \
       bootstrap.tar_extract('','%s')" %filename],
      workdir=['modules'],
      description="extracting files from %s" %filename,
    ))

  def _add_unzip(self, archive, directory, trim_directory=0):
    class _indirection(object):
      def run(self):
        print("===== Installing %s into %s" % (archive, directory))
        Toolbox().unzip(archive, directory, trim_directory)
    self.add_step(_indirection())

  def _add_svn(self, module, url):
    update_list = ['update']
    if module in ["reduce", "probe", "king"]:
      pass
    elif self.revert:
      update_list = ['update', '-r', self.revert]
    thisworkdir = 'modules'
    if module == 'molprobity' : thisworkdir = '.'
    # avoid stalling bootstrap with prompts
    # or when encountering unknown server certificates
    svnflags = ['--non-interactive', '--trust-server-cert']
    if module == 'chem_data':  # stop pulling geostd from SourceForge
      svnflags.append('--ignore-externals')
    if os.path.exists(self.opjoin(*[thisworkdir, module, '.svn'])):
      self.add_step(self.shell(
          command=['svn'] + update_list +[module] + svnflags,
          workdir=[thisworkdir]
      ))
      self.add_step(self.shell(
          command=['svn', 'status', module] + svnflags,
          workdir=[thisworkdir],
          quiet=True,
      ))
    elif os.path.exists(self.opjoin(*[thisworkdir, module])):
      print("Existing non-svn directory -- don't know what to do. skipping: %s"%module)
    else:
      # print "fresh checkout..."
      self.add_step(self.shell(
          command=['svn', 'co', url, module] + svnflags,
          workdir=[thisworkdir]
      ))
    # replace geostd (replace this once chem_data is moved)
    if module == 'chem_data':
      if not os.path.exists(self.opjoin(thisworkdir, module)) \
        or os.path.exists(self.opjoin(thisworkdir, module, 'geostd', '.svn')):
          self.add_step(cleanup_dirs(['geostd'], self.opjoin(thisworkdir, module)))
      action = MODULES.get_module('geostd')().get_url(auth=self.get_auth())
      method, parameters = action[0], action[1:]
      self._add_git('geostd', parameters, destination=self.opjoin(thisworkdir, 'chem_data', 'geostd'))

  def _add_git(self, module, parameters, destination=None):
    use_git_ssh = self.auth.get('git_ssh', False)
    reference_repository_path = self.auth.get('git_reference', None)
    if reference_repository_path is None:
      if os.name == 'posix' and 'diamond.ac.uk' in pysocket.gethostname():
        reference_repository_path = '/dls/science/groups/scisoft/DIALS/repositories/git-reference'
    if reference_repository_path:
      reference_repository_path = os.path.expanduser(os.path.join(reference_repository_path, module))
    class _indirection(object):
      def run(self):
        Toolbox().git(module, parameters, destination=destination,
            use_ssh=use_git_ssh, verbose=True, reference=reference_repository_path)
    self.add_step(_indirection())

    # Update version information
    if module == 'cctbx_project':
      workdir = ['modules', module]
      self.add_step(self.shell(command=[sys.executable, os.path.join('libtbx', 'version.py')], workdir=workdir))

    # Use dials-2.2 branches for Python 2
    if (module == 'dials' or module == 'dxtbx' or module == 'xia2') and not self.python3:
      workdir = ['modules', module]
      if module == 'dxtbx':
        self.add_step(self.shell(command=['git', 'remote', 'set-url', 'origin', 'https://github.com/dials/dxtbx.git'], workdir=workdir))
        self.add_step(self.shell(command=['git', 'fetch', 'origin'], workdir=workdir))
      self.add_step(self.shell(command=['git', 'checkout', 'dials-2.2'], workdir=workdir))
      self.add_step(self.shell(
        command=['git', 'branch', '--set-upstream-to=origin/dials-2.2', 'dials-2.2'],
        workdir=workdir))

  def _check_for_Windows_prerequisites(self):
    if self.isPlatformWindows():
      # platform specific checks cannot run on buildbot master so add to build steps to run on slaves
      self.add_step(self.shell(command=[
         sys.executable,"-c","import sys; sys.path.append('..'); import bootstrap; \
          bootstrap.CheckWindowsPrerequisites()"],
        workdir=['modules'],
        description="Checking Windows prerequisites",
      ))

  def _get_conda_manager(self):
    """
    Helper function for determining the location of the conda environment
    """
    if __package__ is None:
      root_path = os.path.dirname(os.path.abspath(__file__))
      paths = [root_path,
               os.path.join(root_path, 'modules', 'cctbx_project', 'libtbx',
                            'auto_build')]
      for path in paths:
        if os.path.isfile(os.path.join(path, 'install_conda.py')):
          if path not in sys.path:
            sys.path.append(path)
            break
      from install_conda import conda_manager
    else:
      from .install_conda import conda_manager

    # drop output
    log = open(os.devnull, 'w')

    # environment is provided, so do check that it exists
    if self.use_conda is not None and os.path.isdir(self.use_conda):
      check_file = True
      self.use_conda = os.path.abspath(self.use_conda)
    # no path provided or file provided
    else:
      check_file = False
      # base step has not run yet, so do not check if files exist
      self.use_conda = os.path.join('..', 'conda_base')
      if self.isPlatformWindows():
        self.use_conda = os.path.join(os.getcwd(), 'conda_base')
    # basic checks for python and conda
    m = conda_manager(root_dir=os.getcwd(), conda_env=self.use_conda,
                      check_file=check_file, log=log)

    return m

  def _get_conda_python(self):
    """
    Helper function for determining the location of Python for the base
    and build actions.
    """
    try:
      m = self._get_conda_manager()
      return m.get_conda_python()
    except ImportError:  # modules directory is not available

      # -----------------------------------------------------------------------
      # duplicate logic from get_conda_python function in install_conda.py
      # since install_conda.py may not be available
      def m_get_conda_python(self):
        m_conda_python = os.path.join('bin', 'python')
        if self.isPlatformWindows():
          m_conda_python = self.op.join('python.exe')
        elif self.isPlatformMacOSX():
          m_conda_python = os.path.join('python.app', 'Contents',
                                        'MacOS', 'python')
        return m_conda_python
      # -----------------------------------------------------------------------

      conda_python = None

      # (case 1)
      # use default location or file provided to --use-conda
      if self.use_conda == '' or os.path.isfile(self.use_conda):
        conda_python = self.op.join('..', 'conda_base',
                                    m_get_conda_python(self))
        if self.isPlatformWindows():
          conda_python = self.op.join(os.getcwd(), 'conda_base', m_get_conda_python(self))
      # (case 2)
      # use path provided to --use-conda
      elif os.path.isdir(self.use_conda):
        self.use_conda = os.path.abspath(self.use_conda)
        conda_python = os.path.join(self.use_conda, m_get_conda_python(self))
      else:
        raise RuntimeError("""
The --use-conda flag can accept a directory to a conda environment or a
file that defines a conda environment. Please make sure a valid conda
environment exists in or is defined by {conda_env}.
""".format(conda_env=self.use_conda))

      if conda_python is None:
        raise RuntimeError('A conda version of python could not be found.')

    return conda_python

  def add_command(self, command, name=None, workdir=None, args=None, **kwargs):
    if self.isPlatformWindows():
      command = command + '.bat'
    # Relative path to workdir.
    workdir = workdir or [_BUILD_DIR]
    dots = [".."]*len(workdir)
    if workdir[0] == '.':
      dots = []
    if sys.platform == "win32": # assuming we run standalone without buildbot
      dots.extend([os.getcwd(), _BUILD_DIR, 'bin', command])
    else:
      dots.extend([_BUILD_DIR, 'bin', command])
    self.add_step(self.shell(
      name=name or command,
      command=[self.opjoin(*dots)] + (args or []),
      workdir=workdir,
      **kwargs
    ))

  def add_test_command(self,
                       command,
                       name=None,
                       workdir=None,
                       args=None,
                       haltOnFailure=False,
                       **kwargs
                       ):
    if name is None: name='test %s'%command
    self.add_command(
      command,
      name=name,
      workdir=(workdir or ['tests', command]),
      args=args,
      haltOnFailure=haltOnFailure,
      **kwargs
    )

  def add_test_parallel(self, module=None, nproc=None, slow_tests=False, **kwargs):
    if nproc is None:
      nprocstr = 'nproc=auto'
    else:
      nprocstr = 'nproc=%d'%nproc
    args=['module=%s'%module, nprocstr, 'verbosity=1']
    if slow_tests:
      args.append('slow_tests=True')
    self.add_command(
      'libtbx.run_tests_parallel',
      name='test %s'%module,
      workdir=['tests', module],
      args=args,
      haltOnFailure=False,
      **kwargs
    )

  def add_refresh(self):
    self.add_command(
      'libtbx.refresh',
      name='libtbx.refresh',
      workdir=['.'],
    )

  # Override these methods.
  def add_base(self, extra_opts=[]):
    """Build the base dependencies, e.g. Python, HDF5, etc."""
    if self.with_python:
      extra_opts = ['--with-python', self.with_python]
    if self.verbose:
      extra_opts.append('-v')
    if self.download_only:
      extra_opts.append('--download-only')
    if self.auth.get('git_ssh',False):
      extra_opts.append('--git-ssh')
    if self.skip_base:
      extra_opts.append('--skip-base=%s' % self.skip_base)
    if self.python3:
      extra_opts.append('--python3')
    if self.wxpython4:
      extra_opts.append('--wxpython4')
    if not self.force_base_build:
      if "--skip-if-exists" not in extra_opts:
        extra_opts.append("--skip-if-exists")
    command=[
      sys.executable,
      self.opjoin('modules', 'cctbx_project', 'libtbx', 'auto_build', 'install_base_packages.py'),
      '--python-shared',
      '--%s'%self.BASE_PACKAGES
    ] + extra_opts

    # Override base with conda
    #
    # The use of conda is focused on 2 main groups
    #   1) Developers who do not actively use conda
    #      A basic conda installation will be created at the same level as the
    #      "modules" and "build" directories. The default environment for the
    #      builder will be created in the "conda_base" directory at the same
    #      level.
    #   2) Developers who do
    #      A path to a conda environment should be provided. No checks are done
    #      on the environment. The environment files for the build should be
    #      used to construct the starting environment and the developer is
    #      responsible for maintaining it.
    if self.use_conda is not None:  # --use-conda flag is set
      # reset command
      command = []

      # file or no path provided (case 1), case 2 handled in _get_conda_python
      if self.use_conda == '' or os.path.isfile(self.use_conda):
        flags = ['--builder={builder}'.format(builder=self.category)]
        # check if a file was an argument
        if os.path.isfile(self.use_conda):
          filename = os.path.abspath(self.use_conda)
          flags.append('--install_env={filename}'.format(filename=filename))
        # check for existing miniconda3 installation
        if not os.path.isdir('mc3'):
          flags.append('--install_conda')
        flags.append('--python={python}'.format(python=self.python))
        command = [
          sys.executable,
          self.opjoin('modules', 'cctbx_project', 'libtbx', 'auto_build',
                      'install_conda.py',)] + flags

    if len(command) > 0:
      print("Installing base packages using:\n  " + " ".join(command))
      self.add_step(self.shell(name='base', command=command, workdir=['.']))

  def add_dispatchers(self, product_name="phenix"):
    """Write dispatcher_include file."""
    """Generating Phenix environment additions for dispatchers..."""
    envcmd = "export"
    dispatcher = os.path.join("build",
                              "dispatcher_include_%s.sh" %
                              product_name)
    if self.isPlatformWindows():
      envcmd = "set"
      dispatcher = os.path.join("build",
                                "dispatcher_include_%s.bat" %
                                product_name)
    if (os.path.isfile(dispatcher)): os.remove(dispatcher)
    env_prefix = product_name.upper() # e.g. "Phenix" -> "PHENIX"
    prologue = "\n".join([
      "%s %s=\"%s\"" % (envcmd, env_prefix, os.getcwd()),
      "%s %s_VERSION=%s" % (envcmd, env_prefix, "dev-svn"),
      "%s %s_ENVIRONMENT=1" % (envcmd, env_prefix),
      #"%s %s_MTYPE=%s" % (envcmd, env_prefix, "none"),
    ] #+ self.product_specific_dispatcher_prologue())
                           )
    #epilogue = "\n".join(self.product_specific_dispatcher_epilogue())
    dispatcher_opts = [
      "--build_dir=%s" % ".",
      "--suffix=%s"    % "phenix",
    ]
    if self.use_conda is None:
      dispatcher_opts += [
        "--base_dir=%s"  % "../base",
        "--gtk_version=2.10.0", # XXX this can change!
        #"--quiet",
      ]
    else:
      # default
      base_dir = self.op.join('..', 'conda_base')
      # use path from --use-conda flag
      # error-checking done in _get_conda_python function
      if os.path.isdir(self.use_conda):
        base_dir = self.use_conda

      dispatcher_opts += [
      "--base_dir=%s" % base_dir,
      "--use_conda",
      "--ignore_missing_dirs"
      ]
    #if (not self.flag_build_gui):
    #  dispatcher_opts.append("--ignore_missing_dirs")
    # FIXME this will happen regardless of whether the GUI modules are being
    # distributed or not - will this be problematic?
    self.add_step(self.shell(
      name='gui dispatcher',
      command=[
        self.python_base, #'python',
        self.opjoin("..",
                    'modules',
                    'cctbx_project',
                    'libtbx',
                    'auto_build',
                    'write_gui_dispatcher_include.py'),
        '--prologue=%s' % prologue,
        #"--epilogue=%s"
      ] + dispatcher_opts,
      workdir=[_BUILD_DIR]
    ))

  def add_configure(self):

    env = None

    if self.use_conda is not None:
      if '--use_conda' not in self.config_flags:
        self.config_flags.append('--use_conda')
      self.python_base = self._get_conda_python()
      # conda python prefers no environment customizations
      # the get_environment function in ShellCommand updates the environment
      if os.environ.get('CCTBX_CONDA_USE_ENVIRONMENT_VARIABLES', None):
        env = {
          'PYTHONPATH': None,
          'LD_LIBRARY_PATH': None,
          'DYLD_LIBRARY_PATH': None,
          'DYLD_FALLBACK_LIBRARY_PATH': None
        }

    configcmd =[
        self.python_base, # default to using our python rather than system python
        self.opjoin('..', 'modules', 'cctbx_project', 'libtbx', 'configure.py')
        ] + self.get_libtbx_configure() + self.config_flags
    self.add_step(self.shell(command=configcmd, workdir=[_BUILD_DIR],
      description="run configure.py", env=env))
    # Prepare saving configure.py command to file should user want to manually recompile Phenix
    fname = self.opjoin("config_modules.cmd")
    ldlibpath = ''
    if self.isPlatformLinux() and self.use_conda is None:
      ldlibpath = 'export LD_LIBRARY_PATH=../base/lib\n'
      # because that was the environment when python and base components were built during bootstrap
    confstr = ldlibpath + subprocess.list2cmdline(configcmd)
    if not self.isPlatformWindows():
      fname = self.opjoin("config_modules.sh")
      confstr = '#!/bin/sh\n\n' + confstr
    # klonky way of writing file later on, but it works
    self.add_step(self.shell(command=[
         sys.executable,'-c','open(r\"%s\",\"w\").write(r\"\"\"%s\"\"\" + \"\\n\")' %(fname, confstr)
         ],
      workdir=[_BUILD_DIR],
      description="save configure command",
    ))
    if not self.isPlatformWindows():
      self.add_step(self.shell(command=[ 'chmod', '+x', fname ],
        workdir=[_BUILD_DIR],
        description="permit execution of config_modules.sh",
      ))

    # write extra setpaths script for conda
    if self.use_conda is not None:
      self.add_command('libtbx.install_conda', args=['--write_setpaths'],
                       description='Writing additional setup scripts for conda.')

  def add_make(self):
    self.add_command('libtbx.scons', args=['-j',
                                           str(self.nproc),
#                                          #"--skip-version", # for Phaser
                                           ])
    # run build again to make sure everything is built
    self.add_command('libtbx.scons', args=['-j',
                                           str(self.nproc),
#                                          #"--skip-version", # for Phaser
                                           ])

  def add_install(self):
    """Run after compile, before tests."""
    if os.getenv('CCTBX_SKIP_CHEMDATA_CACHE_REBUILD') is None:
      self.add_command('mmtbx.rebuild_rotarama_cache',
                      name="rebuild rotarama",
      )
      self.add_command('mmtbx.rebuild_cablam_cache',
                      name="rebuild cablam",
      )

  def add_tests(self):
    """Run the unit tests."""
    pass

  def rebuild_docs(self):
    self.add_command('phenix_html.rebuild_docs')

  def add_distribute(self):
    pass

##### Specific Configurations ######

class CCIBuilder(Builder):
  """Base class for packages that include CCTBX as a dependency."""
  # Base packages
  BASE_PACKAGES = 'all'
  # Checkout these codebases
  CODEBASES = [
    'boost',
    'cbflib',
    'cctbx_project',
    'dxtbx',
    'gui_resources',
    'ccp4io',
    'ccp4io_adaptbx',
    'annlib',
    'annlib_adaptbx',
    'tntbx',
    'clipper',
    'eigen',
    'reduce',
  ]
  CODEBASES_EXTRA = []
  # Copy these sources from cci.lbl.gov
  HOT = [
    'scons',
  ]
  HOT_EXTRA = []
  # Configure for these cctbx packages
  LIBTBX = [
    'cctbx',
    'cctbx_website',
    'cbflib',
    'dxtbx',
    'scitbx',
    'crys3d',
    'libtbx',
    'iotbx',
    'mmtbx',
    'smtbx',
    'gltbx',
    'wxtbx',
  ]
  LIBTBX_EXTRA = []

##### CCTBX-derived packages #####

class MOLPROBITYBuilder(Builder):
  BASE_PACKAGES = 'molprobity'
  # Checkout these codebases
  CODEBASES = [
    'boost',
    'cbflib',
    'cctbx_project',
    'ccp4io',
    'ccp4io_adaptbx',
    'annlib',
    'annlib_adaptbx',
    'tntbx',
  ]
  CODEBASES_EXTRA = [
    'molprobity',
    #'chem_data', #chem_data removed from molprobity builder until accessible outside cci, -CJW
    'reduce',
    'probe'
  ]
  # Copy these sources from cci.lbl.gov
  HOT = [
    'scons',
    #"libsvm",
  ]
  HOT_EXTRA = []
  # Configure for these cctbx packages
  LIBTBX = [
    'mmtbx',
  ]
  LIBTBX_EXTRA = [
  ]

  def add_tests(self):
    pass

# def add_base(self, extra_opts=[]):
#   super(MOLPROBITYBuilder, self).add_base(
#     extra_opts=['--molprobity',
#                ] + extra_opts)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

class PhaserBuilder(CCIBuilder):
  BASE_PACKAGES = 'cctbx'
    # Checkout these codebases
  CODEBASES = [
    'boost',
    'cctbx_project',
    'ccp4io',
    'ccp4io_adaptbx',
    'annlib',
    'annlib_adaptbx',
    'eigen',
    'tntbx',
    'phaser_regression',
    'phaser',
    'reduce',
  ]
  # Configure for these cctbx packages
  LIBTBX = [
    'cctbx',
    'scitbx',
    'crys3d',
    'libtbx',
    'iotbx',
    'mmtbx',
    'smtbx',
    'phaser_regression',
    'phaser',
  ]

  def add_tests(self):
    self.add_test_parallel(module='phaser_regression') # run phaser_regression/run_tests.py file
    """
    self.add_test_command('phaser_regression.regression', # run Gabors tests
                          args=['all',
                                '-o',
                                'terse_failed',
                                '-n',
                                '%s' %self.nproc,
                                ],
    )
    """

  def add_base(self, extra_opts=[]):
    # skip unnecessary base packages when building phaser only
    if self.skip_base is None or len(self.skip_base) == 0:
      self.skip_base = "hdf5,lz4_plugin,py2app,wxpython,docutils,pyopengl,pillow,tiff," + \
        "cairo,fonts,render,fontconfig,pixman,png,sphinx,freetype,gtk,matplotlib," + \
        "cython,h5py,gettext,numpy,pythonextra,pytest,junitxml,libsvm,pyrtf,six,send2trash," + \
         "jinja2,orderedset,procrunner,tabulate,scipy,scikit_learn,biopython,expat,glib,mrcfile"
    else:
      self.skip_base = ','.join(self.skip_base.split(',') + ['hdf5','lz4_plugin','py2app',
         'wxpython','docutils','pyopengl','pillow','tiff','cairo','fonts','pyrtf','six','send2trash',
         'fontconfig','render','pixman','png','sphinx','freetype','gtk', 'matplotlib',
         'cython', 'h5py', 'gettext', 'numpy', 'pythonextra', 'pytest', 'junitxml','libsvm',
         'jinja2', 'orderedset', 'procrunner', 'tabulate', 'scipy', 'scikit_learn', 'biopython',
         'expat', 'glib', 'mrcfile'
         ])
    super(PhaserBuilder, self).add_base(
      extra_opts=['--cctbx',
                 ] + extra_opts)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

  def get_libtbx_configure(self):
    configlst = super(PhaserBuilder, self).get_libtbx_configure()
    if not self.isPlatformMacOSX():
      configlst.append("--enable_openmp_if_possible=True")
    return configlst

class PhaserTNGBuilder(PhaserBuilder):
  CODEBASES = PhaserBuilder.CODEBASES + ['phasertng', 'phaser_voyager', 'voyager_regression']
  LIBTBX = PhaserBuilder.LIBTBX + ['phasertng', 'phaser_voyager', 'voyager_regression']

  def add_tests(self):
    #self.add_test_parallel(module='phaser_regression') # run phaser_regression/run_tests.py file
    self.add_test_parallel(module='voyager_regression') # run voyager_regression/run_tests.py file

  def get_libtbx_configure(self):
    configlst = super(PhaserTNGBuilder, self).get_libtbx_configure()
    configlst.append('--enable_cxx11')
    return configlst

  def get_codebases(self):
    """
    Phaser uses Boost in the conda environment for Python 3 and Windows
    """
    codebases = super(PhaserTNGBuilder, self).get_codebases()
    if (self.python3 and self.use_conda is not None):
      try:
        codebases.remove('boost')
      except ValueError:
        pass
    return codebases

class CCTBXLiteBuilder(CCIBuilder):
  BASE_PACKAGES = 'cctbx'
    # Checkout these codebases
  CODEBASES = [
    'boost',
    'cctbx_project',
    'gui_resources',
    'ccp4io',
    'ccp4io_adaptbx',
    'annlib',
    'annlib_adaptbx',
    'tntbx',
    'clipper',
    'eigen'
  ]
  # Configure for these cctbx packages
  LIBTBX = [
    'cctbx',
    'cctbx_website',
    'scitbx',
    'libtbx',
    'iotbx',
    'mmtbx',
    'smtbx',
    'gltbx',
    'wxtbx',
  ]

  def add_tests(self):
    self.add_test_command('libtbx.import_all_python', workdir=['modules', 'cctbx_project'])
    self.add_test_command('cctbx_regression.test_nightly')

  def add_base(self, extra_opts=[]):
    if self.skip_base is None or len(self.skip_base) == 0:
      self.skip_base = "hdf5,lz4_plugin,h5py"
    else:
      self.skip_base = ','.join(self.skip_base.split(',') + ['hdf5','lz4_plugin','h5py'])
    super(CCTBXLiteBuilder, self).add_base(
      extra_opts=['--cctbx',
                 ] + extra_opts)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

class CCTBXBuilder(CCIBuilder):
  BASE_PACKAGES = 'cctbx'
  def add_tests(self):
    self.add_test_command('libtbx.import_all_python', workdir=['modules', 'cctbx_project'])
    self.add_test_command('cctbx_regression.test_nightly')

  def add_base(self, extra_opts=[]):
    super(CCTBXBuilder, self).add_base(
      extra_opts=['--cctbx',
                 ] + extra_opts)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

  def _add_git(self, module, parameters, destination=None):
    super(CCTBXBuilder, self)._add_git(module, parameters, destination)
    # select dials-3.5 branch
    if (module == 'dials' or module == 'dxtbx' or module == 'xia2') and self.python3:
      workdir = ['modules', module]
      if module == 'dxtbx':
        self.add_step(self.shell(command=['git', 'remote', 'set-url', 'origin', 'https://github.com/dials/dxtbx.git'], workdir=workdir))
        self.add_step(self.shell(command=['git', 'fetch', 'origin'], workdir=workdir))
      self.add_step(self.shell(command=['git', 'checkout', 'dials-3.5'], workdir=workdir))
      self.add_step(self.shell(
        command=['git', 'branch', '--set-upstream-to=origin/dials-3.5', 'dials-3.5'],
        workdir=workdir))
    # switch eigen to 3.3.9 for CentOS 6
    if module == 'eigen':
      if sys.platform.startswith('linux') and '.el6.' in platform.platform():
        workdir = ['modules', module]
        self.add_step(self.shell(command=['git', 'checkout', '3.3.9'], workdir=workdir))

class DIALSBuilder(CCIBuilder):
  CODEBASES_EXTRA = ['dials', 'iota', 'xia2', 'kokkos', 'kokkos-kernels']
  LIBTBX_EXTRA = ['dials', 'xia2', 'prime', 'iota', '--skip_phenix_dispatchers']
  HOT_EXTRA = ['msgpack']
  def add_tests(self):
    self.add_test_command('libtbx.pytest',
                          args=['--regression', '-n', 'auto'],
                          workdir=['modules', 'dxtbx'],
                          haltOnFailure=True)
    self.add_test_command('libtbx.pytest',
                          args=['--regression', '-n', 'auto'],
                          workdir=['modules', 'dials'],
                          haltOnFailure=True)

  def add_base(self, extra_opts=[]):
    super(DIALSBuilder, self).add_base(
      extra_opts=['--dials', '--xia2',
                 ] + extra_opts)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

  def get_libtbx_configure(self):
    configlst = super(DIALSBuilder, self).get_libtbx_configure()
    if self.python != "27":
      # Do not enable C++11 for Python 2.7 builds, cf. https://github.com/cctbx/cctbx_project/pull/497
      configlst.append('--enable_cxx11')
    return configlst

class LABELITBuilder(CCIBuilder):
  CODEBASES_EXTRA = ['labelit', 'dials']
  LIBTBX_EXTRA = ['labelit', 'dials']

  def add_base(self, extra_opts=[]):
    super(LABELITBuilder, self).add_base(
      extra_opts=['--labelit', 'dials'] + extra_opts)

  def add_tests(self):
    self.add_test_parallel('labelit', flunkOnFailure=False, warnOnFailure=True)

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

class XFELLegacyBuilder(CCIBuilder):
  CODEBASES_EXTRA = [
    'dials',
    'iota',
    'labelit',
    'cxi_xdr_xes'
  ]
  LIBTBX_EXTRA = [
    'dials',
    'labelit',
    'xfel',
    'cxi_xdr_xes',
    'prime',
    'iota'
  ]
  HOT_EXTRA = ['msgpack']

  def add_base(self, extra_opts=[]):
    super(XFELLegacyBuilder, self).add_base(
      extra_opts=['--labelit', '--dials'] + extra_opts)

  def add_tests(self):
    self.add_test_command('cctbx_regression.test_nightly')

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

class XFELBuilder(CCIBuilder):
  CODEBASES_EXTRA = [
    'dials',
    'iota',
    'uc_metrics',
    'ncdist',
    'kokkos',
    'kokkos-kernels',
  ]
  LIBTBX_EXTRA = [
    'dials',
    'xfel',
    'prime',
    'iota',
    'uc_metrics',
  ]
  HOT_EXTRA = ['msgpack']

  def add_base(self, extra_opts=[]):
    super(XFELBuilder, self).add_base(
      extra_opts=['--dials'] + extra_opts)

  def get_libtbx_configure(self):
    configlst = super(XFELBuilder, self).get_libtbx_configure()
    if '--enable_cxx11' in configlst: configlst.remove('--enable_cxx11')
    configlst.append('--cxxstd=c++14')
    return configlst

  def add_tests(self):
    self.add_test_command('cctbx_regression.test_nightly')
    self.add_test_parallel(module='uc_metrics')

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

class PhenixBuilder(CCIBuilder):
  HOT = []
  CODEBASES_EXTRA = [
    'chem_data',
    'phenix',
    'phenix_dev_doc',
    'phenix_regression',
    'phenix_html',
    'phenix_examples',
    'phenix_pathwalker',
    'Colabs',
    'labelit',
    'Plex',
    'PyQuante',
    'elbow',
    'amber_adaptbx',
    'amber_library',
    'ksdssp',
    'pulchra',
    'solve_resolve',
    'reel',
    'gui_resources',
    'opt_resources',
    'muscle',
    'reduce',
    'probe',
    'king',
    'phaser',
    'phasertng',
    'phaser_regression',
    'voyager_regression',
    'phaser_voyager',
    # 'dials',
    # 'xia2',
    # 'iota',
  ]
  LIBTBX_EXTRA = [
    'chem_data',
    'phenix',
    'phenix_dev_doc',
    'phenix_regression',
    'phenix_examples',
    'phenix_pathwalker',
    'Colabs',
    'solve_resolve',
    'reel',
    'phaser',
    'phasertng',
    'phaser_regression',
    'voyager_regression',
    'phaser_voyager',
    'labelit',
    'elbow',
    'amber_adaptbx',
    'reduce',
    'probe',
    'cootbx',
    'qttbx',
    # 'dials',
    # 'xia2',
    # 'prime',
  ]

  # select dials-3.8 branch
  def _add_git(self, module, parameters, destination=None):
    super(PhenixBuilder, self)._add_git(module, parameters, destination)
    if module == 'boost':
      workdir = ['modules', module]
      self.add_step(self.shell(command=['git', 'checkout', '1.74'], workdir=workdir))
    elif (module == 'dials' or module == 'dxtbx' or module == 'xia2') and self.python3:
      workdir = ['modules', module]
      if module == 'dxtbx':
        self.add_step(self.shell(command=['git', 'remote', 'set-url', 'origin', 'https://github.com/dials/dxtbx.git'], workdir=workdir))
        self.add_step(self.shell(command=['git', 'fetch', 'origin'], workdir=workdir))
      self.add_step(self.shell(command=['git', 'checkout', 'dials-3.8'], workdir=workdir))
      self.add_step(self.shell(
        command=['git', 'branch', '--set-upstream-to=origin/dials-3.8', 'dials-3.8'],
        workdir=workdir))

  def add_module(self, module, workdir=None, module_directory=None):
    """
    Add git-lfs command for phenix_examples and phenix_regression
    If the dev_env directory already exists, it is assumed that git-lfs
    is available in that directory
    """
    super(PhenixBuilder, self).add_module(module, workdir, module_directory)

    # update phenix_regression and phenix_examples with git-lfs
    if module == 'phenix_examples' or module == 'phenix_regression':
      # prepend path for check
      dev_env = os.path.join('.', 'dev_env', 'bin')
      if sys.platform == 'win32':
        dev_env = os.path.join('.', 'dev_env', 'Library', 'bin')
        os.environ['PATH'] = os.path.abspath(dev_env) + ';'  + os.environ['PATH']
      else:
        os.environ['PATH'] = os.path.abspath(dev_env) + ':'  + os.environ['PATH']

      svn_is_available = False
      git_lfs_is_available = False

      # check if git-lfs and svn are available
      log = open(os.devnull, 'w')

      try:
        returncode = subprocess.call(['svn', '--version'], stdout=log, stderr=log)
        if returncode == 0:
          svn_is_available = True
      except Exception:
        pass

      try:
        returncode = subprocess.call(['git', 'lfs', '--version'], stdout=log, stderr=log)
        if returncode == 0:
          git_lfs_is_available = True
      except Exception:
        pass

      log.close()

      # set if dev_env will be created in base step
      self.install_dev_env = False
      if not svn_is_available or not git_lfs_is_available:
        self.install_dev_env = True

      # get lfs files
      if self.install_dev_env:
        print('*'*79)
        print("""\
An environment containing git-lfs and/or svn will be installed during the "base"
step. Pleaser re-run the "update" step after "base" completes, so that git-lfs
files for {module} will be downloaded.""".format(module=module))
        print('*'*79)
      else:
        workdir = ['modules', module]
        self.add_step(self.shell(command=['git', 'lfs', 'install', '--local'], workdir=workdir))
        self.add_step(self.shell(command=['git', 'lfs', 'pull'], workdir=workdir))

  def add_base(self, extra_opts=[]):
    super(PhenixBuilder, self).add_base(
      extra_opts=['--phenix',
                  '--labelit',
                  '--dials',
                  '--xia2',
                 ] + extra_opts)

    # install extra development environment if necessary
    if hasattr(self, 'install_dev_env') and self.install_dev_env:
      if self.use_conda is None:
        raise RuntimeError("""
Conda is needed for creating the extra environment with git-lfs. Please add
"--use-conda" to your bootstrap.py command or make sure git-lfs is available
in your path. """)
      self.python_base = self._get_conda_python()
      if self.python_base.startswith('../conda_base'):
        self.python_base = self.python_base[1:]  # keep current directory
      env = {
        'PYTHONPATH': None,
        'LD_LIBRARY_PATH': None,
        'DYLD_LIBRARY_PATH': None
      }
      command = [self.python_base,
                 os.path.join('modules', 'cctbx_project', 'libtbx',
                              'auto_build', 'install_conda.py'),
                 '--install_dev_env', '--verbose']
      self.add_step(self.shell(command=command, workdir=['.']))

  def add_install(self):
    Builder.add_install(self)

  def get_libtbx_configure(self):
    configlst = super(PhenixBuilder, self).get_libtbx_configure()
    if '--enable_cxx11' in configlst:
      configlst.remove('--enable_cxx11')
    set_std = ['cxxstd' in conf for conf in configlst]
    if set_std.count(True) == 0:
      if platform.mac_ver()[-1] == 'arm64':
        configlst.append('--cxxstd=c++14')
      else:
        configlst.append('--cxxstd=c++11')
    if not self.isPlatformMacOSX():
      configlst.append("--enable_openmp_if_possible=True")
    return configlst

  def rebuild_docs(self):
    self.add_command('phenix_html.rebuild_docs')

  def add_tests(self):
    # Include cctbx tests.
    self.add_test_command('libtbx.import_all_ext')
    self.add_test_command('cctbx_regression.test_nightly')
    # Windows convenience hack.
    if self.isPlatformWindows():
      self.add_test_command('phenix_regression.test_nightly_windows')
    else:
      self.add_test_command('phenix_regression.test_nightly')
    # Other Phenix tests.
    self.add_test_parallel(module='elbow')
    self.rebuild_docs()
    self.add_test_command('phenix_regression.run_p9_sad_benchmark',
                          name="test p9 sad",
                         )
    self.add_test_command('phenix_regression.run_hipip_refine_benchmark',
                          name="test hipip",
                         )
    self.add_test_command('phenix_regression.wizards.test_all_parallel',
      args = ['nproc=8'],
      name="test wizards",
                         )
    run_dials_tests=True
    if self.isPlatformWindows():
      if 'dials' in windows_remove_list:
        run_dials_tests=False
    if run_dials_tests:
      self.add_test_parallel('dials', flunkOnFailure=False, warnOnFailure=True)

class PhenixExternalRegression(PhenixBuilder):
  EXTERNAL_CODEBASES = [
    "afitt",
    "rosetta",
    ]

  def cleanup(self, dirs=None):
    self.add_step(cleanup_ext_class(".bz2", "modules", walk=False))
    lt = time.localtime()
    cleaning = ['dist', 'tests', 'doc', 'tmp', 'base_tmp']
    if lt.tm_wday==5: # do a completer build on Saturday night
      cleaning += ['base', 'base_tmp', _BUILD_DIR, 'conda_base']
    # Preparation
    # AFITT
    if self.subcategory in [None, "afitt"]:
      self.add_step(cleanup_dirs(['openeye'], 'modules'))
    PhenixBuilder.cleanup(self, cleaning)

  def get_environment(self, add_build_python_to_path=True):
    environment = {}
    for env, dirs in envs.items():
      environment[env] = os.path.join(*dirs)
    if add_build_python_to_path:
      old_path = os.environ.get("PATH", "") # this is just another now
                                            # universal hack to get Amber to
                                            # compile...
      environment["PATH"] = '%s:%s' % (os.path.join(#os.getcwd(),
                                                    "build",
                                                    "bin",
                                                  ),
                                       old_path,
                                       )
    return environment

  def write_environment(self,
                        env,
                        filename="setpaths_externals",
                       ):
    # called by add_make which is called in build
    # this is a little funky as it seems to be very often in the wrong remote dir
    outl = ""
    for key, path in env.items():
      if key in ["PATH"]: continue
      outl += 'setenv %(key)s "%%(PWD)s/../%(path)s"\n' % locals()
    fname="%s.csh" % filename
    self.add_step(self.shell(command=[
      sys.executable,
      '-c',
      'import os; open("%s","w").write("""%s""" %% os.environ)' %(fname, outl)
      ],
      workdir=[_BUILD_DIR],
      description="save csh external paths",
    ))
    outl = ""
    for key, path in env.items():
      if key in ["PATH"]: continue
      outl += 'export %(key)s="%%(PWD)s/../%(path)s"\n' % locals()
    fname="%s.sh" % filename
    self.add_step(self.shell(command=[
      sys.executable,
      '-c',
      'import os; open("%s","w").write("""%s""" %% os.environ)' %(fname, outl)
      ],
      workdir=[_BUILD_DIR],
      description="save sh external paths",
    ))

  def add_make(self):
    # Phenix compile
    PhenixBuilder.add_make(self)
    # need to use the Phenix python for building
    # Rosetta
    # AFITT
    env = self.get_environment()
    self.write_environment(env)
    # not universal but works because only slave running this is same as master
    for name, command, workdir in [
        ['AFITT - untar',
         ['tar', 'xvf', '%s.gz' % afitt_version],
         ['modules']],
        ['Rosetta - untar',
         ['tar', 'xvf', '%s.tgz' % rosetta_version_tar_bundle],
         ['modules']],
        ['Rosetta - rm link',
         # not windows compatible
         ['rm', '-f', "rosetta"],
         ['modules']],
        ['Rosetta - link',
         # not windows compatible
         ['ln', '-sf', '%s' % rosetta_version_directory, "rosetta"],
         ['modules']],
        #['Rosetta compile', # not really needed
        # ["./scons.py",
        #  "-j",
        #  self.nproc,
        #  #"mode=release",
        # ],
        # ["modules", 'rosetta', "main", "source"]],
        ]:
      if self.subcategory:
        if name.lower().find(self.subcategory)==-1: continue
      haltOnFailure=True
      self.add_step(self.shell(
        name       = name,
        command    = command,
        workdir    = workdir,
        description= "",
        env        = env,
        haltOnFailure = haltOnFailure,
        ))

    self.add_refresh()
    # Rosetta
    if self.subcategory in [None, "rosetta"]:
      self.add_command(
        'rosetta.build_phenix_interface',
        args = ["nproc=%s" % self.nproc],
        name='rosetta.build_phenix_interface',
        workdir=['.'],
        env=env,
      )

  def add_tests(self):
    # timings
    if self.subcategory in [None, 'timings']:
      self.add_test_command(
        'mmtbx.python',
        args=[os.path.join('..',
                           '..',
                           'modules',
                           'phenix_regression',
                           'development',
                           'runtime_speed_regression_test.py',
                           )],
        name="timings test",
        )
    # amber
    if self.subcategory in [None, "amber"]:
      self.add_test_command('amber.run_tests',
                            env = self.get_environment(),
                            haltOnFailure=False,
                           )
    # rosetta refine
    if self.subcategory in [None, "rosetta"]:
      self.add_test_command('rosetta.run_tests',
                            env = self.get_environment()
                           )
    # MR rosetta
    if self.subcategory in [None, "rosetta"]:
      self.add_test_command(
        'phenix_regression.wizards.run_wizard_test',
        args=['test_prerefine'],
        name="test MR rosetta quick",
        env = self.get_environment()
      )
    # afitt
    if self.subcategory in [None, "afitt"]:
      self.add_test_command('afitt.run_tests',
                            env = self.get_environment()
                           )
    # GLR
    if self.subcategory in [None, "glr"]:
      self.add_test_command('elbow.run_glr_tests',
                            haltOnFailure=False,
                            )

class QRBuilder(PhenixBuilder):
  #
  # Designed to be run in Phenix build to add Q|R
  # and the entire PhenixBuilder if user is builder
  #
  EXTERNAL_CODEBASES = ["qrefine"]
  user = os.environ.get('USER', None)

  def add_make(self):
    if self.user=='builder': PhenixBuilder.add_make(self)
    #
    # XXX Use older ASE (the new one is only Python3)
    # XXX Do not get JPype1 as it fails. This makes QR work only with
    # XXX fast_interaction=True (=False won't work)
    #
    pip_installs = ['ase', 'pymongo']
    instructions = []
    # versioning
    cmd = [os.path.join('..', self.python_base),
           os.path.join('utils', 'make_version.py'),
           ]
    instructions.append(['Versioning', cmd, ['modules/qrefine']])
    for pi in pip_installs:
      instructions.append(['Q|R pip %s' % pi,
                           [self.python_base,
                            '-m',
                            'pip',
                            'install',
                            pi
                          ],
                           ['modules']])

    for name, command, workdir in instructions:
      self.add_step(self.shell(
        name       = name,
        command    = command,
        workdir    = workdir,
        description= "",
        haltOnFailure = 1, #haltOnFailure,
        ))
    self.add_refresh()

  def get_hot(self):
    if self.user=='builder': return PhenixBuilder.get_hot(self)
    return [] # don't have any HOT downloads and the difference between
              # anonymous and cciuser is making a mess

  def get_libtbx_configure(self): # modified in derived class PhenixBuilder
    return self.LIBTBX + self.LIBTBX_EXTRA + self.EXTERNAL_CODEBASES

  def get_codebases(self):
    if self.isPlatformWindows(): assert 0, 'not supported'
    if self.user=='builder':
      rc = PhenixBuilder.get_codebases(self)
    else:
      rc = self.EXTERNAL_CODEBASES #+ ['cctbx_project']
    return rc

  def add_tests(self):
    self.add_test_command('qr.build_interfaces',
                          haltOnFailure=False,
                          env = self.get_environment()
                          )
    self.add_test_command('qr.test',
                          # args=['--non_mopac_only'],
                          haltOnFailure=True,
                          env = self.get_environment()
                          )

  def add_dispatchers(self):
    pass

  def rebuild_docs(self):
    pass

  def get_environment(self, add_build_python_to_path=True):
    environment = {}
    mopac_envs = {
      "MOPAC_LICENSE"  : "/home/builder/software/mopac",
      "MOPAC_COMMAND"  : "/home/builder/software/mopac/mopac.csh",
    }
    for env, dirs in mopac_envs.items():
      environment[env] = dirs
    return environment

class PhenixReleaseBuilder(PhenixBuilder):
  '''
  Phenix with DIALS
  '''
  extra_codebases = ['dials', 'iota', 'xia2']
  extra_libtbx = extra_codebases + ['prime']
  CODEBASES_EXTRA = PhenixBuilder.CODEBASES_EXTRA + extra_codebases
  LIBTBX_EXTRA = PhenixBuilder.LIBTBX_EXTRA + extra_libtbx

def set_builder_defaults(options):
  '''
  Updates defaults for specific builders
  '''
  if options.builder == 'phenix_voyager':
    if not options.no_boost_src:
      options.no_boost_src = True
      # restore default for CentOS 7
      if sys.platform.startswith('linux') and '.el7.' in platform.platform():
        options.no_boost_src = False
  if options.builder == 'phenix_voyager' or options.builder == 'phenix':
    # Apple Silicon uses Boost 1.78 in environment, Python 3.9
    if platform.mac_ver()[-1] == 'arm64':
      options.no_boost_src = True
      options.python = '39'
    if not options.no_boost_src:
      options.no_boost_src = True
      if sys.platform.startswith('linux') and '.el7.' in platform.platform():
        options.no_boost_src = False
    if options.use_conda is None:
      options.use_conda = ''

  return options

def run(root=None):
  builders = {
    'cctbxlite': CCTBXLiteBuilder,
    'cctbx': CCTBXBuilder,
    'phenix': PhenixBuilder,
    'phenix_voyager': PhenixBuilder,
    'phenix_release': PhenixReleaseBuilder,
    'xfellegacy': XFELLegacyBuilder,
    'xfel': XFELBuilder,
    'labelit': LABELITBuilder,
    'dials': DIALSBuilder,
    'external': PhenixExternalRegression,
    'molprobity':MOLPROBITYBuilder,
    'qrefine': QRBuilder,
    'phaser': PhaserBuilder,
    'voyager': PhaserTNGBuilder
  }

  wrapper = textwrap.TextWrapper(width=80, initial_indent='  ',
                                 subsequent_indent='    ')
  builders_text = ', '.join(sorted(builders.keys()))
  builders_text = '\n'.join(wrapper.wrap(builders_text))

  prog = os.environ.get('LIBTBX_DISPATCHER_NAME')
  if prog is None or prog.startswith('python') or prog.endswith('python'):
    prog = os.path.basename(sys.argv[0])

  description = """
  You may specify one or more actions:
    hot - Update static sources (scons, etc.)
    update - Update source repositories (cctbx, cbflib, etc.)
    base - Build base dependencies (python, hdf5, wxWidgets, etc.)
    build - Build
    tests - Run tests
    doc - Build documentation

  The default action is to run: hot, update, base, build

  You can specify which package will be downloaded, configured,
  and built with "--builder". Current builders:
  {builders}

  You can provide your SourceForge username with "--sfuser", and
  your CCI SVN username with "--cciuser". These will checkout
  and update repositories with your credentials. Some builders,
  like phenix, require this argument for access to certain
  repositories.

  You can run the compilation step in parallel by providing a
  the number of processes using "--nproc".
  Complete build output is shown with "-v" or "--verbose".

  Finally, you may specify a specific Python interpreter
  using "--with-python".

  Example:

    python bootstrap.py --builder=cctbx --sfuser=metalheadd hot update build tests
  """.format(builders=builders_text)

  parser = argparse.ArgumentParser(
    prog=prog, description=description,
    formatter_class=argparse.RawDescriptionHelpFormatter)
  # parser.add_argument("--root", help="Root directory; this will contain base, modules, build, etc.")
  parser.add_argument('action', nargs='*', help="Actions for building")
  parser.add_argument(
    "--builder",
    help="Builder: " + ",".join(list(builders.keys())),
    default="cctbx")
  parser.add_argument("--cciuser", help="CCI SVN username.")
  parser.add_argument("--sfuser", help="SourceForge SVN username.")
  parser.add_argument("--revert", help="SVN string to revert all SVN trees")
  parser.add_argument("--sfmethod",
                    help="SourceForge SVN checkout method.",
                    default="svn+ssh")
  parser.add_argument(
    "--git-ssh",
    dest="git_ssh",
    action="store_true",
    help="Use ssh connections for git. This allows you to commit changes without changing remotes and use reference repositories.",
    default=False)
  parser.add_argument(
    "--git-reference",
    dest="git_reference",
    help="Path to a directory containing reference copies of repositories for faster checkouts.")
  parser.add_argument("--with-python",
                    dest="with_python",
                    help="Use specified Python interpreter")
  parser.add_argument("--nproc",
                    help="number of parallel processes in compile step.")
  parser.add_argument("--download-only",
                    dest="download_only",
                    action="store_true",
                    help="Do not build, only download prerequisites",
                    default=False)
  parser.add_argument("-v",
                    "--verbose",
                    dest="verbose",
                    action="store_true",
                    help="Verbose output",
                    default=False)
  parser.add_argument("--skip-base-packages",
                    dest="skip_base",
                    action="store",
                    default="")
  parser.add_argument("--force-base-build",
                    dest="force_base_build",
                    action="store_true",
                    default=False)
  parser.add_argument("--enable-shared",
                    dest="enable_shared",
                    action="store_true",
                    default=False)
  parser.add_argument("--mpi-build",
                    dest="mpi_build",
                    help="Builds software with mpi functionality",
                    action="store_true",
                    default=False)
  python_args = parser.add_mutually_exclusive_group(required=False)
  python_args.add_argument('--python',
                    default='37', type=str, nargs='?', const='37',
                    choices=['27', '37', '38', '39', '310'],
                    help="""When set, a specific Python version of the
conda environment will be used. This only affects environments selected with
the --builder flag. For non-conda dependencies, any Python 3 implies
Python 3.7.""")
  parser.add_argument("--config-flags", "--config_flags",
                    dest="config_flags",
                    help="""Pass flags to the configuration step. Flags should
be passed separately with quotes to avoid confusion (e.g
--config_flags="--build=debug" --config_flags="--enable_cxx11")""",
                    action="append",
                    default=[])
  parser.add_argument("--use-conda", "--use_conda", metavar="ENVIRONMENT",
                    dest="use_conda",
                    help="""Use conda for dependencies. The directory to an
existing conda environment or a file defining a conda environment can be
provided. The build will use that environment instead of creating a default one
for the builder. If the currently active conda environment is to be used for
building, $CONDA_PREFIX should be the argument for this flag. Otherwise, a new
environment will be created. The --python flag will be ignored when there is
an argument for this flag. Specifying an environment is for developers that
maintain their own conda environment.""",
                    default=None, nargs='?', const='')
  parser.add_argument("--no-boost-src", "--no_boost_src",
                      dest="no_boost_src",
                      help="""When set, the reduced Boost source code is not
downloaded into the modules directory. This enables the usage of an existing
installation of Boost in the same directory as the Python for configuration.
For example, this flag should be used if the conda package for Boost is
available. This flag only affects the "update" step.""",
                      action="store_true",
                      default=False)
  parser.add_argument("--build-dir",
                     dest="build_dir",
                     help="directory where the build will be. Should be at the same level as modules! default is 'build'",
                     default="build", type=str)

  options = parser.parse_args()
  args = options.action

  global _BUILD_DIR
  _BUILD_DIR = options.build_dir  # TODO: this is probably ok way to go with globalvar, but check and see

  # process external
  options.specific_external_builder=None
  if options.builder.lower() in ["afitt",
                                 "rosetta",
                                 ]:
    options.specific_external_builder=options.builder.lower()
    options.builder="external"

  # Root dir
  # options.root = options.root or root

  # Check actions
  allowedargs = ['cleanup', 'hot', 'update', 'base', 'build', 'tests', 'doc']
  args = args or ['hot', 'update', 'base', 'build']
  actions = []
  for arg in args:
    if arg not in allowedargs:
      raise ValueError("Unknown action: %s"%arg)
  for arg in allowedargs:
    if arg in args:
      actions.append(arg)

  # Check if an action was an argument to --use-conda
  if options.use_conda in allowedargs:
    if len(options.action) == 0:
      actions = [options.use_conda]
    else:
      actions.append(options.use_conda)
    options.use_conda = ''

  # Check if the argument to --use-conda starts with '~'
  if options.use_conda is not None and options.use_conda.startswith('~'):
    options.use_conda = os.path.expanduser(options.use_conda)

  print("Performing actions:", " ".join(actions))

  # Check builder
  if options.builder not in builders:
    raise ValueError("Unknown builder: %s"%options.builder)

  auth = { 'git_ssh': options.git_ssh, 'git_reference': options.git_reference }
  if options.cciuser:
    auth['cciuser'] = options.cciuser
  if options.sfuser:
    auth['sfuser'] = options.sfuser
  if options.sfmethod:
    auth['sfmethod'] = options.sfmethod

  # Apply defaults for specific builders
  options = set_builder_defaults(options)

  # Build
  builder = builders[options.builder]
  builder(
    category=options.builder,
    subcategory=options.specific_external_builder,
    platform='dev',
    with_python=options.with_python,
    auth=auth,
    hot=('hot' in actions),
    update=('update' in actions),
    revert=options.revert,
    base=('base' in actions),
    build=('build' in actions),
    tests=('tests' in actions),
    doc=('doc' in actions),
    cleanup=("cleanup" in actions),
    nproc=options.nproc,
    verbose=options.verbose,
    download_only=options.download_only,
    skip_base=options.skip_base,
    force_base_build=options.force_base_build,
    enable_shared=options.enable_shared,
    mpi_build=options.mpi_build,
    python3=False,
    wxpython4=False,
    config_flags=options.config_flags,
    use_conda=options.use_conda,
    python=options.python,
    no_boost_src=options.no_boost_src,
  ).run()
  print("\nBootstrap success: %s" % ", ".join(actions))

if __name__ == "__main__":
  run()
