#!/usr/bin/env python

from __future__ import print_function
import sys, optparse, subprocess, os, os.path, errno, zipfile, string, json, platform, shutil, tarfile, tempfile, multiprocessing, re, stat, copy
if sys.version_info >= (3,):
  from urllib.parse import urljoin
  from urllib.request import urlopen
  from urllib.error import HTTPError
else:
  from urlparse import urljoin
  from urllib2 import urlopen, HTTPError

# EMSDK_DEV is a developer mode flag, which, if true, the SDK is downloaded from a 'staging' online source,
# instead of the public source. New releases are first deployed to the staging source for testing, before 
# being published to the public. Don't enable this unless you develop EMSDK itself and need to access the
# staging source repository instead.
EMSDK_DEV = bool(os.getenv('EMSDK_DEV')) if os.getenv('EMSDK_DEV') != None else False

if EMSDK_DEV:
  print('EMSDK_DEV active.')
  emsdk_master_server = 'http://clb.demon.fi/emscripten_dev/'
else:
  emsdk_master_server = 'https://s3.amazonaws.com/mozilla-games/emscripten/'

emsdk_packages_url = urljoin(emsdk_master_server, 'packages/')

emscripten_git_repo = 'https://github.com/kripken/emscripten/'
binaryen_git_repo = 'https://github.com/WebAssembly/binaryen/'

# Enable this to do very verbose printing about the different steps that are being run. Useful for debugging.
VERBOSE = bool(os.getenv('EMSDK_VERBOSE')) if os.getenv('EMSDK_VERBOSE') != None else False

WINDOWS = False
if os.name == 'nt':
  WINDOWS = True
  ENVPATH_SEPARATOR = ';'

LINUX = False
if platform.system() == 'Linux':
  LINUX = True
  ENVPATH_SEPARATOR = ':'

OSX = False
if platform.mac_ver()[0] != '':
  OSX = True
  ENVPATH_SEPARATOR = ':'

CPU_CORES = max(multiprocessing.cpu_count()-1, 1) # Don't saturate all cores to not steal the whole system, but be aggressive.

CMAKE_BUILD_TYPE_OVERRIDE = None

# If true, perform a --shallow clone of git.
GIT_CLONE_SHALLOW = False

# If true, LLVM backend is built with tests enabled, and Binaryen is built with Visual Studio static analyzer enabled.
BUILD_FOR_TESTING = False

# If 'auto', assertions are decided by the build type (Release&MinSizeRel=disabled, Debug&RelWithDebInfo=enabled)
# Other valid values are 'ON' and 'OFF'
ENABLE_LLVM_ASSERTIONS = 'auto'

def to_unix_path(p):
  return p.replace('\\', '/')

def emsdk_path(): return to_unix_path(os.path.dirname(os.path.realpath(__file__)))

emscripten_config_directory = os.path.expanduser("~/")
# If .emscripten exists, we are configuring as embedded inside the emsdk directory.
if os.path.exists(os.path.join(emsdk_path(), '.emscripten')):
  emscripten_config_directory = emsdk_path()

EMSDK_SET_ENV = 'emsdk_set_env.bat' if WINDOWS else 'emsdk_set_env.sh'

# Finds the given executable 'program' in PATH. Operates like the Unix tool 'which'.
def which(program):
  def is_exe(fpath):
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

  fpath, fname = os.path.split(program)
  if fpath:
    if is_exe(program):
      return program
  else:
    for path in os.environ["PATH"].split(os.pathsep):
      path = path.strip('"')
      exe_file = os.path.join(path, program)
      if is_exe(exe_file):
        return exe_file

      if WINDOWS and not '.' in fname:
        if is_exe(exe_file + '.exe'):
          return exe_file + '.exe'
        if is_exe(exe_file + '.cmd'):
          return exe_file + '.cmd'
        if is_exe(exe_file + '.bat'):
          return exe_file + '.bat'

  return None

CMAKE_GENERATOR = 'Unix Makefiles'
if WINDOWS:
  # Detect which CMake generator to use when building on Windows
  if '--mingw' in sys.argv: CMAKE_GENERATOR = 'MinGW Makefiles'
  elif '--vs2013' in sys.argv: CMAKE_GENERATOR = 'Visual Studio 12'
  elif '--vs2015' in sys.argv: CMAKE_GENERATOR = 'Visual Studio 14'
  else:
    vs2015_exists = 'VS140COMNTOOLS' in os.environ or 'VSSDK140Install' in os.environ or os.path.isdir(os.path.join(os.environ['ProgramFiles'], 'Microsoft Visual Studio 14.0')) or ('ProgramFiles(x86)' in os.environ and os.path.isdir(os.path.join(os.environ['ProgramFiles(x86)'], 'Microsoft Visual Studio 14.0')))
    vs2013_exists = 'VS120COMNTOOLS' in os.environ or os.path.isdir(os.path.join(os.environ['ProgramFiles'], 'Microsoft Visual Studio 12.0')) or ('ProgramFiles(x86)' in os.environ and os.path.isdir(os.path.join(os.environ['ProgramFiles(x86)'], 'Microsoft Visual Studio 12.0')))
    mingw_exists = which('mingw32-make') != None and which('g++') != None
    if vs2015_exists: CMAKE_GENERATOR = 'Visual Studio 14'
    elif mingw_exists: CMAKE_GENERATOR = 'MinGW Makefiles'
    elif vs2013_exists: CMAKE_GENERATOR = 'Visual Studio 12' # VS2013 is no longer supported, so attempt it as a last resort if someone might want to insist using it.
    else: CMAKE_GENERATOR = '' # No detected generator
if VERBOSE: print('CMAKE_GENERATOR: ' + CMAKE_GENERATOR)
sys.argv = filter(lambda x: x not in ['--mingw', '--vs2013', '--vs2015'], sys.argv)

# Computes a suitable path prefix to use when building with a given generator.
def cmake_generator_prefix():
  if CMAKE_GENERATOR == 'Visual Studio 14': return '_vs2015'
  elif CMAKE_GENERATOR == 'MinGW Makefiles': return '_mingw'
  return '' # Unix Makefiles and Visual Studio 2013 do not specify a path prefix for backwards path compatibility

# Removes a directory tree even if it was readonly, and doesn't throw exception on failure.
def remove_tree(d):
  if VERBOSE: print('remove_tree(' + str(d) + ')')
  try:
    def remove_readonly_and_try_again(func, path, exc_info):
      if not (os.stat(path).st_mode & stat.S_IWRITE):
        os.chmod(path, stat.S_IWRITE)
        func(path)
      else:
        raise
    shutil.rmtree(d, onerror=remove_readonly_and_try_again)
  except Exception as e:
    if VERBOSE: print('remove_tree threw an exception, ignoring: ' + str(e))
    pass

def win_set_environment_variable_direct(key, value, system=True):
  prev_path = os.environ['PATH']
  try:
    py = find_used_python()
    if py:
      py_path = to_native_path(py.expand_vars(py.activated_path))
      os.environ['PATH'] = os.environ['PATH'] + ';' + py_path
    import win32api, win32con
    if system: # Read globally from ALL USERS section.
      folder = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 0, win32con.KEY_ALL_ACCESS)
    else: # Register locally from CURRENT USER section.
      folder = win32api.RegOpenKeyEx(win32con.HKEY_CURRENT_USER, 'Environment', 0, win32con.KEY_ALL_ACCESS)
    win32api.RegSetValueEx(folder, key, 0, win32con.REG_EXPAND_SZ, value)
    if VERBOSE: print('Set key=' + key + ' with value ' + value + ' in registry.')
  except Exception as e:
    if e[0] == 5 or e[2] == 'Access is denied.':
      print('Error! Failed to set the environment variable \'' + key + '\'! Setting environment variables permanently requires administrator access. Please rerun this command with administrative privileges. This can be done for example by holding down the Ctrl and Shift keys while opening a command prompt in start menu.')
      sys.exit(1)
    print('Failed to write environment variable ' + key + ':', file=sys.stderr)
    print(str(e), file=sys.stderr)
    win32api.RegCloseKey(folder)
    os.environ['PATH'] = prev_path
    return None

  win32api.RegCloseKey(folder)
  os.environ['PATH'] = prev_path
  win32api.SendMessage(win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, 'Environment')

def win_get_environment_variable(key, system=True):
  prev_path = os.environ['PATH']
  try:
    py = find_used_python()
    if py:
      py_path = to_native_path(py.expand_vars(py.activated_path))
      os.environ['PATH'] = os.environ['PATH'] + ';' + py_path
    import win32api, win32con
    if system: # Read globally from ALL USERS section.
      folder = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment')
    else: # Register locally from CURRENT USER section.
      folder = win32api.RegOpenKey(win32con.HKEY_CURRENT_USER, 'Environment')
    value = str(win32api.RegQueryValueEx(folder, key)[0])
  except Exception as e:
    if e[0] != 2: # 'The system cannot find the file specified.'
      print('Failed to read environment variable ' + key + ':', file=sys.stderr)
      print(str(e), file=sys.stderr)
    win32api.RegCloseKey(folder)
    os.environ['PATH'] = prev_path
    return None
  win32api.RegCloseKey(folder)
  os.environ['PATH'] = prev_path
  return value

def win_environment_variable_exists(key, system=True):
  value = win_get_environment_variable(key, system)
  return value != None and len(value) > 0

def win_get_active_environment_variable(key):
  value = win_get_environment_variable(key, False)
  if value != None:
    return value
  return win_get_environment_variable(key, True)

def win_set_environment_variable(key, value, system=True):
  if VERBOSE: print('set ' + str(key) + '=' + str(value) + ', in system=' + str(system), file=sys.stderr)
  previous_value = win_get_environment_variable(key, system)
  if previous_value == value:
    if VERBOSE: print('  no need to set, since same value already exists.')
    return # No need to elevate UAC for nothing to set the same value, skip.

  if not value:
    try:
      if system:
        cmd = ['REG', 'DELETE', 'SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', '/V', key, '/f']
      else:
        cmd = ['REG', 'DELETE', 'HKCU\\Environment', '/V', key, '/f']
      if VERBOSE: print(str(cmd))
      value = subprocess.call(cmd, stdout=subprocess.PIPE)
    except Exception as e:
      return
    return

  try:
    if system:
      win_set_environment_variable_direct(key, value, system)
      return
    value = value.replace('%', '^%') # Escape % signs so that we don't expand references to environment variables.
    if len(value) >= 1024:
      print('ERROR! The new environment variable ' + key + ' is more than 1024 characters long! A value this long cannot be set via command line: please add the environment variable specified above to system environment manually via Control Panel.', file=sys.stderr)
      sys.exit(1)
    cmd = ['SETX', key, value] 
    if VERBOSE: print(str(cmd))
    retcode = subprocess.call(cmd, stdout=subprocess.PIPE)
    if retcode is not 0:
      print('ERROR! Failed to set environment variable ' + key + '=' + value + '. You may need to set it manually.', file=sys.stderr)
  except Exception as e:
    print('ERROR! Failed to set environment variable ' + key + '=' + value + ':', file=sys.stderr)
    print(str(e), file=sys.stderr)
    print('You may need to set it manually.', file=sys.stderr)

def win_delete_environment_variable(key, system=True):
  if VERBOSE: print('win_delete_environment_variable(key=' + key + ', system=' + str(system) + ')')
  win_set_environment_variable(key, None, system)

# Returns the absolute pathname to the given path inside the Emscripten SDK.
def sdk_path(path):
  if os.path.isabs(path):
    return path
  else:
    return to_unix_path(os.path.join(os.path.dirname(os.path.realpath(__file__)), path))

# Modifies the given file in-place to contain '\r\n' line endings.
def file_to_crlf(filename):
  text = open(filename, 'r').read()
  text = text.replace('\r\n', '\n').replace('\n', '\r\n')
  open(filename, 'wb').write(text)

# Modifies the given file in-place to contain '\n' line endings.
def file_to_lf(filename):
  text = open(filename, 'r').read()
  text = text.replace('\r\n', '\n')
  open(filename, 'wb').write(text)

# Removes a single file, suppressing exceptions on failure.
def rmfile(filename):
  if VERBOSE: print('rmfile(' + filename + ')')
  try:
    os.remove(filename)
  except:
    pass

def fix_lineendings(filename):
  if WINDOWS:
    file_to_crlf(filename)
  else:
    file_to_lf(filename)

# http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
def mkdir_p(path):
  if VERBOSE: print('mkdir_p(' + path + ')')
  if os.path.exists(path):
    return
  try:
    os.makedirs(path)
  except OSError as exc: # Python >2.5
    if exc.errno == errno.EEXIST and os.path.isdir(path):
      pass
    else: raise

def num_files_in_directory(path):
  if not os.path.isdir(path):
    return 0
  return len([name for name in os.listdir(path) if os.path.exists(os.path.join(path, name))])

def run(cmd, cwd=None):
  if VERBOSE: print('run(cmd=' + str(cmd) + ', cwd=' + str(cwd) + ')')
  process = subprocess.Popen(cmd, cwd=cwd, env=os.environ.copy())
  process.communicate()
  if process.returncode != 0:
    print(str(cmd) + ' failed with error code ' + str(process.returncode) + '!')
  return process.returncode

# http://pythonicprose.blogspot.fi/2009/10/python-extract-targz-archive.html
def untargz(source_filename, dest_dir, unpack_even_if_exists=False):
  if VERBOSE: print('untargz(source_filename=' + source_filename + ', dest_dir=' + dest_dir + ')')
  if not unpack_even_if_exists and num_files_in_directory(dest_dir) > 0:
    print("File '" + source_filename + "' has already been unpacked, skipping.")
    return True
  print("Unpacking '" + source_filename + "' to '" + dest_dir + "'")
  mkdir_p(dest_dir)
  run(['tar', '-xvf', sdk_path(source_filename), '--strip', '1'], cwd=dest_dir)
  #tfile = tarfile.open(source_filename, 'r:gz')
  #tfile.extractall(dest_dir)
  return True

# http://stackoverflow.com/questions/12886768/simple-way-to-unzip-file-in-python-on-all-oses
def unzip(source_filename, dest_dir, unpack_even_if_exists=False):
  if VERBOSE: print('unzip(source_filename=' + source_filename + ', dest_dir=' + dest_dir + ')')
  if not unpack_even_if_exists and num_files_in_directory(dest_dir) > 0:
    print("File '" + source_filename + "' has already been unpacked, skipping.")
    return True
  print("Unpacking '" + source_filename + "' to '" + dest_dir + "'")
  mkdir_p(dest_dir)
  common_subdir = None
  try:
    with zipfile.ZipFile(source_filename) as zf:
      # Implement '--strip 1' behavior to unzipping by testing if all the files in the zip reside in a common subdirectory, and if so,
      # we move the output tree at the end of uncompression step.
      for member in zf.infolist():
        words = member.filename.split('/')
        if len(words) > 1: # If there is a directory component?
          if common_subdir == None:
            common_subdir = words[0]
          elif common_subdir != words[0]:
            common_subdir = None
            break
        else:
          common_subdir = None
          break

      unzip_to_dir = dest_dir
      if common_subdir:
        unzip_to_dir = os.path.join('/'.join(dest_dir.split('/')[:-1]), 'unzip_temp')

      # Now do the actual decompress.
      for member in zf.infolist():
        # Path traversal defense copied from
        # http://hg.python.org/cpython/file/tip/Lib/http/server.py#l789
        words = member.filename.split('/')
        path = unzip_to_dir
        for word in words[:-1]:
          drive, word = os.path.splitdrive(word)
          head, word = os.path.split(word)
          if word in (os.curdir, os.pardir, ''): continue
          path = os.path.join(path, word)
        zf.extract(member, unzip_to_dir)

      if common_subdir:
        try:
          if os.path.exists(dest_dir):
            remove_tree(dest_dir)
        except:
          pass
        shutil.copytree(os.path.join(unzip_to_dir, common_subdir), dest_dir)
        try:
          remove_tree(unzip_to_dir)
        except:
          pass
  except zipfile.BadZipfile as e:
    print("Unzipping file '" + source_filename + "' failed due to reason: " + str(e) + "! Removing the corrupted zip file.")
    rmfile(source_filename)
    return False
  except Exception as e:
    print("Unzipping file '" + source_filename + "' failed due to reason: " + str(e))
    return False

  return True

# This function interprets whether the given string looks like a path to a directory instead of a file, without looking at the actual filesystem.
# 'a/b/c' points to directory, so does 'a/b/c/', but 'a/b/c.x' is parsed as a filename
def path_points_to_directory(path):
  if path == '.':
     return True
  last_slash = max(path.rfind('/'), path.rfind('\\'))
  last_dot = path.rfind('.')
  no_suffix = last_dot < last_slash or last_dot == -1
  if no_suffix:
    return True
  suffix = path[last_dot:]
  if suffix == '.exe' or suffix == '.zip' or suffix == '.txt': # Very simple logic for the only file suffixes used by emsdk downloader. Other suffixes, like 'clang-3.2' are treated as dirs.
    return False
  else:
    return True

# On success, returns the filename on the disk pointing to the destination file that was produced
# On failure, returns None.
def download_file(url, dstpath, download_even_if_exists=False, filename_prefix = ''):
  if VERBOSE: print('download_file(url=' + url + ', dstpath=' + dstpath + ')')
  file_name = filename_prefix + url.split('/')[-1]
  if path_points_to_directory(dstpath):
    file_name = os.path.join(dstpath, file_name)
  else:
    file_name = dstpath

  # Treat all relative destination paths as relative to the SDK root directory, not the current working directory.
  file_name = sdk_path(file_name)

  if os.path.exists(file_name) and not download_even_if_exists:
    print("File '" + file_name + "' already downloaded, skipping.")
    return file_name
  try:
    u = urlopen(url)
    mkdir_p(os.path.dirname(file_name))
    with open(file_name, 'wb') as f: 
      meta = u.info()
      if hasattr(meta.getheaders, "Content-Length"):
        file_size = int(meta.getheaders("Content-Length")[0])
        print("Downloading: %s from %s, %s Bytes" % (file_name, url, file_size))
      else:
        file_size = 0
        print("Downloading: %s from %s" % (file_name, url))

      file_size_dl = 0
      block_sz = 8192
      while True:
          buffer = u.read(block_sz)
          if not buffer:
              break

          file_size_dl += len(buffer)
          f.write(buffer)
          if file_size:
            status = r"%10d  [%3.2f%%]" % (file_size_dl, file_size_dl * 100. / file_size)
            status = status + chr(8)*(len(status)+1)
            print(status, end=' ')
  except HTTPError as e:
    print("HTTP error with URL '" + url + "': " + str(e))
    rmfile(file_name)
    return None
#  except Exception as e:
#    print("Error downloading URL '" + url + "': " + str(e))
#    rmfile(file_name)
#    return None
  return file_name

def download_text_file(url, dstpath, download_even_if_exists=False, filename_prefix=''):
  filename = download_file(url, dstpath, download_even_if_exists, filename_prefix)
  fix_lineendings(os.path.join(emsdk_path(), filename))

def run_get_output(cmd, cwd=None):
  if VERBOSE: print('run_get_output(cmd=' + str(cmd) + ', cwd=' + str(cwd) + ')')
  process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, env=os.environ.copy(), universal_newlines=True)
  (stdout, stderr) = process.communicate()
  return (process.returncode, stdout, stderr)

# must_succeed: If false, the search is performed silently without printing out errors if not found. Empty string is returned if git is not found.
#               If true, the search is required to succeed, and the execution will terminate with sys.exit(1) if not found.
def GIT(must_succeed=True):
  # The order in the following is important, and specifies the preferred order of using the git tools.
  # Primarily use git from emsdk if installed. If not, use system git.
  gits = ['git/1.9.4/bin/git.exe', which('git')]
  for git in gits:
    try:
      (ret, stdout, stderr) = run_get_output([git, '--version'])
      if ret == 0:
        return git
    except:
      pass
  if must_succeed:
    if WINDOWS:
      print("ERROR: git executable was not found. Please install it by typing 'emsdk install git-1.9.4', or alternatively by installing it manually from http://git-scm.com/downloads . If you install git manually, remember to add it to PATH")
    elif OSX:
      print("ERROR: git executable was not found. Please install git for this operation! This can be done from http://git-scm.com/ , or by installing XCode and then the XCode Command Line Tools (see http://stackoverflow.com/questions/9329243/xcode-4-4-command-line-tools )")
    elif LINUX:
      print("ERROR: git executable was not found. Please install git for this operation! This can be probably be done using your package manager, see http://git-scm.com/book/en/Getting-Started-Installing-Git")
    else:
      print("ERROR: git executable was not found. Please install git for this operation!")
    sys.exit(1)
  return '' # Not found

def git_repo_version(repo_path):
  (returncode, stdout, stderr) = run_get_output([GIT(), 'log', '-n', '1', '--pretty="%aD %H"'], cwd=repo_path)
  if returncode == 0:
    return stdout.strip()
  else:
    return ""

def git_clone(url, dstpath):
  if VERBOSE: print('git_clone(url=' + url + ', dstpath=' + dstpath + ')')
  if os.path.isdir(os.path.join(dstpath, '.git')):
    print("Repository '" + url + "' already cloned to directory '" + dstpath + "', skipping.")
    return True
  mkdir_p(dstpath)
  git_clone_args = []
  if GIT_CLONE_SHALLOW:
    git_clone_args += ['--depth', '1']
  return run([GIT(), 'clone'] + git_clone_args + [url, dstpath]) == 0

def git_checkout_and_pull(repo_path, branch):
  if VERBOSE: print('git_checkout_and_pull(repo_path=' + repo_path + ', branch=' + branch + ')')
  ret = run([GIT(), 'fetch', 'origin'], repo_path)
  if ret != 0: return False
  try:
    print("Fetching latest changes to the branch '" + branch + "' for '" + repo_path + "'...")
    ret = run([GIT(), 'fetch', 'origin'], repo_path)
    if ret != 0: return False
#  run([GIT, 'checkout', '-b', branch, '--track', 'origin/'+branch], repo_path)
    ret = run([GIT(), 'checkout', '--quiet', branch], repo_path) # this line assumes that the user has not gone and manually messed with the repo and added new remotes to ambiguate the checkout.
    if ret != 0: return False
    ret = run([GIT(), 'merge', '--ff-only', 'origin/'+branch], repo_path) # this line assumes that the user has not gone and made local changes to the repo
    if ret != 0: return False
  except:
    print('git operation failed!')
    return False
  print("Successfully updated and checked out branch '" + branch + "' on repository '" + repo_path + "'")
  print("Current repository version: " + git_repo_version(repo_path))
  return True

def git_clone_checkout_and_pull(url, dstpath, branch):
  if VERBOSE: print('git_clone_checkout_and_pull(url=' + url + ', dstpath=' + dstpath + ', branch=' + branch + ')')
  success = git_clone(url, dstpath)
  if not success:
    return False
  success = git_checkout_and_pull(dstpath, branch)
  return success

# Each tool can have its own build type, or it can be overridden on the command line.
def decide_cmake_build_type(tool):
  global CMAKE_BUILD_TYPE_OVERRIDE
  if CMAKE_BUILD_TYPE_OVERRIDE:
    return CMAKE_BUILD_TYPE_OVERRIDE
  else:
    return tool.cmake_build_type

# The root directory of the build.
def fastcomp_build_dir(tool):
  generator_suffix = ''
  if CMAKE_GENERATOR == 'Visual Studio 10': generator_suffix = '_vs2010'
  elif CMAKE_GENERATOR == 'Visual Studio 11': generator_suffix = '_vs2012'
  elif CMAKE_GENERATOR == 'Visual Studio 12': generator_suffix = '_vs2013'
  elif CMAKE_GENERATOR == 'Visual Studio 14': generator_suffix = '_vs2015'
  elif CMAKE_GENERATOR == 'MinGW Makefiles': generator_suffix = '_mingw'

  bitness_suffix = '_32' if tool.bitness == 32 else '_64'

  if hasattr(tool, 'git_branch'):
    build_dir = 'build_' + tool.git_branch.replace(os.sep, '-') + generator_suffix + bitness_suffix
  else:
    build_dir = 'build_' + tool.version + generator_suffix + bitness_suffix
  return build_dir

def exe_suffix(filename):
  if WINDOWS and not filename.endswith('.exe'): return filename + '.exe'
  else: return filename

# The directory where the binaries are produced. (relative to the installation root directory of the tool)
def fastcomp_build_bin_dir(tool):
  build_dir = fastcomp_build_dir(tool)
  if WINDOWS and 'Visual Studio' in CMAKE_GENERATOR:
    old_llvm_bin_dir = os.path.join(build_dir, 'bin', decide_cmake_build_type(tool))

    new_llvm_bin_dir = None
    default_cmake_build_type = decide_cmake_build_type(tool)
    cmake_build_types = [default_cmake_build_type, 'Release', 'RelWithDebInfo', 'MinSizeRel', 'Debug']
    for build_type in cmake_build_types:
      d = os.path.join(build_dir, build_type, 'bin')
      if os.path.isfile(os.path.join(tool.installation_path(), d, exe_suffix('clang'))):
        new_llvm_bin_dir = d
        break

    if new_llvm_bin_dir and os.path.exists(os.path.join(tool.installation_path(), new_llvm_bin_dir)): return new_llvm_bin_dir
    elif os.path.exists(os.path.join(tool.installation_path(), old_llvm_bin_dir)): return old_llvm_bin_dir
    return os.path.join(build_dir, default_cmake_build_type, 'bin')
  else:
    return os.path.join(build_dir, 'bin')

def build_env(generator):
  build_env = os.environ.copy()

  # To work around a build issue with older Mac OS X builds, add -stdlib=libc++ to all builds.
  # See https://groups.google.com/forum/#!topic/emscripten-discuss/5Or6QIzkqf0
  if OSX: build_env['CXXFLAGS'] = ((build_env['CXXFLAGS'] + ' ') if hasattr(build_env, 'CXXFLAGS') else '') + '-stdlib=libc++'

  if 'Visual Studio 14' in generator or 'Visual Studio 2015' in generator:
    build_env['VCTargetsPath'] = os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/Microsoft.Cpp/v4.0/V140')

    # CMake and VS2015 cl.exe needs to have mspdb140.dll et al. in its PATH.
    vc_bin_paths = [os.path.join(os.environ['ProgramFiles'], 'Microsoft Visual Studio 14.0\\VC\\bin'),
                    os.path.join(os.environ['ProgramFiles(x86)'], 'Microsoft Visual Studio 14.0\\VC\\bin')]
    for path in vc_bin_paths:
      if os.path.isdir(path):
        build_env['PATH'] = build_env['PATH'] + ';' + path

  elif 'Visual Studio 12' in generator or 'Visual Studio 2013' in generator:
    build_env['VCTargetsPath'] = os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/Microsoft.Cpp/v4.0/V120')

  return build_env

def get_generator_for_sln_file(sln_file):
  contents = open(sln_file, 'r').read()
  if '# Visual Studio Express 2015' in contents or '# Visual Studio 2015' in contents or '# Visual Studio 14' in contents:
    return 'Visual Studio 14'
  if '# Visual Studio Express 2013' in contents or '# Visual Studio 2013' in contents or '# Visual Studio 12' in contents:
    return 'Visual Studio 12'
  raise Exception('Unknown generator used to build solution file ' + sln_file)

def find_msbuild(sln_file):
  search_paths_vs2015 = [os.path.join(os.environ['ProgramFiles'], 'MSBuild/14.0/Bin/amd64'),
                        os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/14.0/Bin/amd64'),
                        os.path.join(os.environ['ProgramFiles'], 'MSBuild/14.0/Bin'),
                        os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/14.0/Bin')]
  search_paths_vs2013 = [os.path.join(os.environ['ProgramFiles'], 'MSBuild/12.0/Bin/amd64'),
                        os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/12.0/Bin/amd64'),
                        os.path.join(os.environ['ProgramFiles'], 'MSBuild/12.0/Bin'),
                        os.path.join(os.environ['ProgramFiles(x86)'], 'MSBuild/12.0/Bin')]
  search_paths_old = [os.path.join(os.environ["WINDIR"], 'Microsoft.NET/Framework/v4.0.30319')]
  generator = get_generator_for_sln_file(sln_file)
  if generator == 'Visual Studio 14':
    search_paths = search_paths_vs2015
  elif generator == 'Visual Studio 12':
    search_paths = search_paths_vs2013 + search_paths_old
  else:
    raise Exception('Unknown generator!')

  for path in search_paths:
    p = os.path.join(path, 'MSBuild.exe')
    if os.path.isfile(p): return p

def make_build(build_root, build_type, build_target_platform='x64'):
  if VERBOSE: print('make_build(build_root=' + build_root + ', build_type=' + build_type + ', build_target_platform=' + build_target_platform + ')')
  global CPU_CORES
  if CPU_CORES > 1:
    print('Performing a parallel build with ' + str(CPU_CORES) + ' cores.')
  else:
    print('Performing a singlethreaded build.')

  generator_to_use = CMAKE_GENERATOR

  if WINDOWS:
    if 'Visual Studio' in CMAKE_GENERATOR:
      solution_name = subprocess.check_output(['dir', '/b', '*.sln'], shell=True, cwd=build_root).strip()
      generator_to_use = get_generator_for_sln_file(os.path.join(build_root, solution_name))
# Disabled for now: Don't pass /maxcpucount argument to msbuild, since it looks like when building, msbuild already automatically spawns the full amount of logical
# cores the system has, and passing the number of logical cores here has been observed to give a quadratic N*N explosion on the number of spawned processes
# (e.g. on a Core i7 5960X with 16 logical cores, it would spawn 16*16=256 cl.exe processes, which would start crashing when running out of system memory)
#      make = [find_msbuild(os.path.join(build_root, solution_name)), '/maxcpucount:' + str(CPU_CORES), '/t:Build', '/p:Configuration='+build_type, '/nologo', '/verbosity:minimal', solution_name]
      make = [find_msbuild(os.path.join(build_root, solution_name)), '/t:Build', '/p:Configuration='+build_type, '/p:Platform='+build_target_platform, '/nologo', '/verbosity:minimal', solution_name]
    else:
      make = ['mingw32-make', '-j' + str(CPU_CORES)]
  else:
    make = ['make', '-j' + str(CPU_CORES)]

  # Build
  try:
    print('Running build: ' + str(make))
    ret = subprocess.check_call(make, cwd=build_root, env=build_env(generator_to_use))
    if ret != 0:
      print('Build failed with exit code ' + ret + '!', file=sys.stderr)
      print('Working directory: ' + build_root, file=sys.stderr)
      return False
  except Exception as e:
    print('Build failed due to exception!', file=sys.stderr)
    print('Working directory: ' + build_root, file=sys.stderr)
    print(str(e), file=sys.stderr)
    return False

  return True

def cmake_configure(generator, build_root, src_root, build_type, extra_cmake_args = []):
  if VERBOSE: print('cmake_configure(generator=' + str(generator) + ', build_root=' + str(build_root) + ', src_root=' + str(src_root) + ', build_type=' + str(build_type) + ', extra_cmake_args=' + str(extra_cmake_args) + ')')
  # Configure
  if not os.path.isdir(build_root): os.mkdir(build_root) # Create build output directory if it doesn't yet exist.
  try:
    if generator: generator = ['-G', generator]
    else: generator = []
    cmdline = ['cmake'] + generator +  ['-DCMAKE_BUILD_TYPE='+build_type, '-DPYTHON_EXECUTABLE='+sys.executable] + extra_cmake_args + [src_root]
    print('Running CMake: ' + str(cmdline))
    def quote_parens(x):
      if ' ' in x:
        return '"' + x.replace('"', '\\"') + '"'
      else:
        return x
    open(os.path.join(build_root, 'recmake.' + ('bat' if WINDOWS else 'sh')), 'w').write(' '.join(map(quote_parens, cmdline))) # Create a file 'recmake.bat/sh' in the build root that user can call to manually recmake the build tree with the previous build params
    ret = subprocess.check_call(cmdline, cwd=build_root, env=build_env(CMAKE_GENERATOR))
    if ret != 0:
      print('CMake invocation failed with exit code ' + ret + '!', file=sys.stderr)
      print('Working directory: ' + build_root, file=sys.stderr)
      return False  
  except OSError as e:
    if e.errno == errno.ENOENT:
      print(str(e), file=sys.stderr)
      print('Could not run CMake, perhaps it has not been installed?', file=sys.stderr)
      if WINDOWS:
        print('Installing this package requires CMake. Get it from http://www.cmake.org/', file=sys.stderr)
      elif LINUX:
        print('Installing this package requires CMake. Get it via your system package manager (e.g. sudo apt-get install cmake), or from http://www.cmake.org/', file=sys.stderr)
      elif OSX:
        print('Installing this package requires CMake. Get it via a OSX package manager (Homebrew: "brew install cmake", or MacPorts: "sudo port install cmake"), or from http://www.cmake.org/', file=sys.stderr)        
      return False
    raise
  except Exception as e:
    print('CMake invocation failed due to exception!', file=sys.stderr)
    print('Working directory: ' + build_root, file=sys.stderr)
    print(str(e), file=sys.stderr)
    return False

  return True

def build_fastcomp_tool(tool):
  if VERBOSE: print('build_fastcomp_tool(' + str(tool) + ')')
  fastcomp_root = tool.installation_path()
  fastcomp_src_root = os.path.join(fastcomp_root, 'src')
  if hasattr(tool, 'git_branch'): # Does this tool want to be git cloned from github?
    success = git_clone_checkout_and_pull(tool.download_url(), fastcomp_src_root, tool.git_branch)
    if not success: return False
    clang_root = os.path.join(fastcomp_src_root, 'tools/clang')
    success = git_clone_checkout_and_pull(tool.clang_url, clang_root, tool.git_branch)
    if not success: return False
  else: # Not a git cloned tool, so instead download from git tagged releases
    success = download_and_unzip(tool.download_url(), fastcomp_src_root, filename_prefix='llvm-e')
    if not success: return False
    success = download_and_unzip(tool.windows_clang_url if WINDOWS else tool.unix_clang_url, os.path.join(fastcomp_src_root, 'tools/clang'), filename_prefix='clang-e')
    if not success: return False

  cmake_generator = CMAKE_GENERATOR
  if 'Visual Studio' in CMAKE_GENERATOR and tool.bitness == 64:
    cmake_generator += ' Win64'

  build_dir = fastcomp_build_dir(tool)
  build_root = os.path.join(fastcomp_root, build_dir)

  build_type = decide_cmake_build_type(tool)

  # Configure
  global BUILD_FOR_TESTING, ENABLE_LLVM_ASSERTIONS
  tests_arg = 'ON' if BUILD_FOR_TESTING else 'OFF'

  enable_assertions = ENABLE_LLVM_ASSERTIONS.lower() == 'on' or (ENABLE_LLVM_ASSERTIONS == 'auto' and build_type.lower() != 'release' and build_type.lower() != 'minsizerel')

  args = ['-DLLVM_TARGETS_TO_BUILD=X86;JSBackend', '-DLLVM_INCLUDE_EXAMPLES=OFF', '-DCLANG_INCLUDE_EXAMPLES=OFF', '-DLLVM_INCLUDE_TESTS=' + tests_arg, '-DCLANG_INCLUDE_TESTS=' + tests_arg, '-DLLVM_ENABLE_ASSERTIONS=' + ('ON' if enable_assertions else 'OFF')]
  success = cmake_configure(cmake_generator, build_root, fastcomp_src_root, build_type, args)
  if not success: return False

  # Make
  success = make_build(build_root, build_type, 'x64' if tool.bitness == 64 else 'Win32')
  return success

# Emscripten asm.js optimizer build scripts:
def optimizer_build_root(tool):
  build_root = tool.installation_path().strip()
  if build_root.endswith('/') or build_root.endswith('\\'): build_root = build_root[:-1]
  generator_prefix = cmake_generator_prefix()
  build_root = build_root + generator_prefix + '_' + str(tool.bitness) + 'bit_optimizer'
  return build_root

def uninstall_optimizer(tool):
  if VERBOSE: print('uninstall_optimizer(' + str(tool) + ')')
  build_root = optimizer_build_root(tool)
  print("Deleting path '" + build_root + "'")
  try:
    remove_tree(build_root)
    os.remove(build_root)
  except:
    pass

def is_optimizer_installed(tool):
  build_root = optimizer_build_root(tool)
  return os.path.exists(build_root)

def build_optimizer_tool(tool):
  if VERBOSE: print('build_optimizer_tool(' + str(tool) + ')')
  src_root = os.path.join(tool.installation_path(), 'tools', 'optimizer')
  build_root = optimizer_build_root(tool)
  build_type = decide_cmake_build_type(tool)

  # Configure
  cmake_generator = CMAKE_GENERATOR
  if 'Visual Studio' in CMAKE_GENERATOR and tool.bitness == 64:
    cmake_generator += ' Win64'
  success = cmake_configure(cmake_generator, build_root, src_root, build_type)
  if not success: return False

  # Make
  success = make_build(build_root, build_type, 'x64' if tool.bitness == 64 else 'Win32')
  return success


# Binaryen build scripts:
def binaryen_build_root(tool):
  build_root = tool.installation_path().strip()
  if build_root.endswith('/') or build_root.endswith('\\'): build_root = build_root[:-1]
  generator_prefix = cmake_generator_prefix()
  build_root = build_root + generator_prefix + '_' + str(tool.bitness) + 'bit_binaryen'
  return build_root

def uninstall_binaryen(tool):
  if VERBOSE: print('uninstall_binaryen(' + str(tool) + ')')
  build_root = binaryen_build_root(tool)
  print("Deleting path '" + build_root + "'")
  try:
    remove_tree(build_root)
    os.remove(build_root)
  except:
    pass

def is_binaryen_installed(tool):
  build_root = binaryen_build_root(tool)
  return os.path.exists(build_root)

def build_binaryen_tool(tool):
  if VERBOSE: print('build_binaryen_tool(' + str(tool) + ')')
  src_root = tool.installation_path()
  build_root = binaryen_build_root(tool)
  build_type = decide_cmake_build_type(tool)

  # Configure
  args = []

  cmake_generator = CMAKE_GENERATOR
  if 'Visual Studio' in CMAKE_GENERATOR:
    if tool.bitness == 64: cmake_generator += ' Win64'
    if BUILD_FOR_TESTING: args += ['-DRUN_STATIC_ANALYZER=1']

  success = cmake_configure(cmake_generator, build_root, src_root, build_type, args)
  if not success: return False

  # Make
  success = make_build(build_root, build_type, 'x64' if tool.bitness == 64 else 'Win32')

  # Deploy scripts needed from source repository to build directory
  remove_tree(os.path.join(build_root, 'scripts'))
  shutil.copytree(os.path.join(src_root, 'scripts'), os.path.join(build_root, 'scripts'))
  remove_tree(os.path.join(build_root, 'src', 'js'))
  shutil.copytree(os.path.join(src_root, 'src', 'js'), os.path.join(build_root, 'src', 'js'))
  shutil.copyfile(os.path.join(src_root, 'bin', 'binaryen.js'), os.path.join(build_root, 'bin', 'binaryen.js'))
  shutil.copyfile(os.path.join(src_root, 'bin', 'wasm.js'), os.path.join(build_root, 'bin', 'wasm.js'))

  return success


def download_and_unzip(zipfile, dest_dir, download_even_if_exists=False, filename_prefix = ''):
  if VERBOSE: print('download_and_unzip(zipfile=' + zipfile + ', dest_dir=' + dest_dir + ')')
  if not download_even_if_exists and num_files_in_directory(dest_dir) > 0:
    print("The contents of file '" + zipfile + "' already exist in destination '" + dest_dir + "', skipping.")
    return True
  dst_file = download_file(urljoin(emsdk_packages_url, zipfile), 'zips/', download_even_if_exists, filename_prefix)
  if not dst_file:
    return False
  if zipfile.endswith('.zip'):
    return unzip(dst_file, dest_dir, unpack_even_if_exists=download_even_if_exists)
  else:
    return untargz(dst_file, dest_dir, unpack_even_if_exists=download_even_if_exists)

def to_native_path(p):
  if WINDOWS:
    return to_unix_path(p).replace('/', '\\')
  else:
    return to_unix_path(p)

# Finds and returns a list of the directories that need to be added to PATH for the given set of tools.
def get_required_path(active_tools):
  path_add = [to_native_path(emsdk_path())]
  for tool in active_tools:
    if hasattr(tool, 'activated_path'):
      path_add += [to_native_path(tool.expand_vars(tool.activated_path))]
  return path_add

# Returns the absolute path to the file '.emscripten' for the current user on this system.
def dot_emscripten_path():
  return os.path.join(emscripten_config_directory, ".emscripten")

dot_emscripten = {}

def parse_key_value(line):
  if not line:
    return ('', '')
  eq = line.find('=')
  if eq != -1:
    key = line[0:eq].strip()
    value = line[eq+1:].strip()
    return (key, value)
  else:
    return (key, '')

def load_dot_emscripten():
  global dot_emscripten
  dot_emscripten = {}
  lines = []
  try:
    lines = open(dot_emscripten_path(), "r").read().split('\n')
  except:
    pass
  for line in lines:
    try:
      (key, value) = parse_key_value(line)
      if value != '':
        dot_emscripten[key] = value
#        print("Got '" + key + "' = '" + value + "'")
    except:
      pass

def generate_dot_emscripten(active_tools):
  global emscripten_config_directory
  if emscripten_config_directory == emsdk_path():
    temp_dir = sdk_path('tmp')
    mkdir_p(temp_dir)
    embedded = True
  else:
    temp_dir = tempfile.gettempdir().replace('\\', '/')
    embedded = False

  has_spidermonkey = False
  has_node = False

  cfg = 'import os\n'
  
  if embedded:
    cfg += "emsdk_path=os.path.dirname(os.environ.get('EM_CONFIG')).replace('\\\\', '/')\n"

  for tool in active_tools:
    tool_cfg = tool.activated_config()
    if tool_cfg:
      tool_cfg = tool_cfg.replace(';', '\n')
      if 'SPIDERMONKEY_ENGINE=' in tool_cfg: has_spidermonkey = True
      if 'NODE_JS=' in tool_cfg: has_node = True
      cfg += tool_cfg + '\n'

  # These two vars must always be defined, even though they might not exist.
  if not has_spidermonkey: cfg += "SPIDERMONKEY_ENGINE = ''\n"
  if not has_node:
    node_fallback = which('nodejs')
    if not node_fallback:
      node_fallback = 'node'
    cfg += "NODE_JS = '" + node_fallback + "'\n"

  cfg += '''V8_ENGINE = ''
TEMP_DIR = ''' + "'" + temp_dir + "'" + '''
COMPILER_ENGINE = NODE_JS
JS_ENGINES = [NODE_JS]
'''

  if embedded:
    cfg = cfg.replace(emscripten_config_directory, "' + emsdk_path + '")

  with open(dot_emscripten_path(), "w") as text_file: text_file.write(cfg)
  
  # Clear old cached emscripten content.
  try:
    remove_tree(os.path.join(emscripten_config_directory, ".emscripten_cache"))
    os.remove(os.path.join(emscripten_config_directory, ".emscripten_sanity"))
    os.remove(os.path.join(emscripten_config_directory, ".emscripten_cache__last_clear"))
  except:
    pass

  print("The Emscripten configuration file " + os.path.normpath(dot_emscripten_path()) + " has been rewritten with the following contents:")
  print('')
  print(cfg.strip())
  print('')
  
  path_add = get_required_path(active_tools)
  if not WINDOWS:
    emsdk_env = os.path.relpath(sdk_path('emsdk_env.sh'))
    if not '/' in emsdk_env:
      emsdk_env = './emsdk_env.sh'
    print("To conveniently access the selected set of tools from the command line, consider adding the following directories to PATH, or call 'source " + emsdk_env + "' to do this for you.")
    print('')
    print('   ' + ENVPATH_SEPARATOR.join(path_add))

def find_msbuild_dir():
  if 'ProgramFiles' in os.environ and os.environ['ProgramFiles']:
    program_files = os.environ['ProgramFiles']
  else:
    program_files = 'C:/Program Files'
  if 'ProgramFiles(x86)' in os.environ and os.environ['ProgramFiles(x86)']:
    program_files_x86 = os.environ['ProgramFiles(x86)']
  else:
    program_files_x86 = 'C:/Program Files (x86)'
  MSBUILDX86_DIR = os.path.join(program_files_x86, "MSBuild/Microsoft.Cpp/v4.0/Platforms")
  MSBUILD_DIR = os.path.join(program_files, "MSBuild/Microsoft.Cpp/v4.0/Platforms")
  if os.path.exists(MSBUILDX86_DIR):
    return MSBUILDX86_DIR
  elif os.path.exists(MSBUILD_DIR):
    return MSBUILD_DIR
  else:
    return '' # No MSbuild installed.

def get_installed_vstool_version(installed_path):
  try:
    return open(installed_path + "/version.txt", "r").read()
  except:
    return None

class Tool:
  def __init__(self, data):
    # Convert the dictionary representation of the tool in 'data' to members of this class for convenience.
    for key in data:
      if sys.version_info < (3,) and isinstance(data[key], unicode):
        setattr(self, key, data[key].encode('Latin-1'))
      else:
        setattr(self, key, data[key])

  def __str__(self):
    s = self.id + '-' + self.version
    if hasattr(self, 'bitness'):
      s += '-' + str(self.bitness) + 'bit'
    return s

  def expand_vars(self, str):
    if WINDOWS and '%MSBuildPlatformsDir%' in str:
      str = str.replace('%MSBuildPlatformsDir%', find_msbuild_dir())
    if '%cmake_build_type_on_win%' in str:
      str = str.replace('%cmake_build_type_on_win%', (decide_cmake_build_type(self) + '/') if WINDOWS else '')
    if '%installation_dir%' in str:
      str = str.replace('%installation_dir%', sdk_path(self.installation_dir()))
    if '%generator_prefix%' in str:
      str = str.replace('%generator_prefix%', cmake_generator_prefix())
    str = str.replace('%.exe%', '.exe' if WINDOWS else '')
    if '%fastcomp_build_dir%' in str:
      str = str.replace('%fastcomp_build_dir%', fastcomp_build_dir(self))
    if '%fastcomp_build_bin_dir%' in str:
      str = str.replace('%fastcomp_build_bin_dir%', fastcomp_build_bin_dir(self))
    return str

  # Return true if this tool requires building from source, and false if this is a precompiled tool.
  def needs_compilation(self):
    if hasattr(self, 'cmake_build_type'): return True

    if hasattr(self, 'uses'):
      for tool_name in self.uses:
        tool = find_tool(tool_name)
        if not tool:
          if VERBOSE: print('Tool ' + str(self) + ' depends on ' + tool_name + ' which does not exist!')
          continue
        if tool.needs_compilation(): return True

    return False

  # Specifies the target path where this tool will be installed to. This could either be a directory or a filename (e.g. in case of node.js)
  def installation_path(self):
    if WINDOWS and hasattr(self, 'windows_install_path'):
      pth = self.expand_vars(self.windows_install_path)
      return sdk_path(pth)
    if hasattr(self, 'install_path'):
      pth = self.expand_vars(self.install_path)
      return sdk_path(pth)
    p = self.version
    if hasattr(self, 'bitness') and (not hasattr(self, 'append_bitness') or self.append_bitness):
      p += '_' + str(self.bitness) + 'bit'
    return sdk_path(os.path.join(self.id, p))

  # Specifies the target directory this tool will be installed to.
  def installation_dir(self):
    dir = self.installation_path()
    if path_points_to_directory(dir):
      return dir
    else:
      return os.path.dirname(dir)

  # Returns the configuration item that needs to be added to .emscripten to make this Tool active for the current user.
  def activated_config(self):
    if hasattr(self, 'activated_cfg'):
      return to_unix_path(self.expand_vars(self.activated_cfg))
    else:
      return ''

  def activated_environment(self):
    if hasattr(self, 'activated_env'):
      return self.expand_vars(self.activated_env)
    else:
      return ''
  
  def compatible_with_this_os(self):
    if hasattr(self, 'os'):
      if self.os == 'all':
        return True
      if (WINDOWS and 'win' in self.os) or (LINUX and ('linux' in self.os or 'unix' in self.os)) or (OSX and ('osx' in self.os or 'unix' in self.os)):
        return True
      else:
        return False
    else:
      if not hasattr(self, 'osx_url') and not hasattr(self, 'windows_url') and not hasattr(self, 'unix_url') and not hasattr(self, 'linux_url'):
        return True

    if OSX and hasattr(self, 'osx_url'):
      return True

    if LINUX and hasattr(self, 'linux_url'):
      return True

    if WINDOWS and (hasattr(self, 'windows_url') or hasattr(self, 'windows_install_path')):
      return True

    if (LINUX or OSX) and hasattr(self, 'unix_url'):
      return True

    return hasattr(self, 'url')

  def is_installed(self):
    # If this tool/sdk depends on other tools, require that all dependencies are installed for this tool to count as being installed.
    if hasattr(self, 'uses'):
      for tool_name in self.uses:
        tool = find_tool(tool_name)
        if tool == None:
          print("Manifest error: No tool by name '" + tool_name + "' found! This may indicate an internal SDK error!")
          return False
        if not tool.is_installed():
          return False

    if self.download_url() != None:
      # For e.g. fastcomp clang from git repo, the activated PATH is the directory where the compiler is built to, and installation_path is
      # the directory where the source tree exists. To distinguish between multiple packages sharing the same source
      # (clang-master-32bit, clang-master-64bit, clang-incoming-32bit and clang-incoming-64bit each share the same git repo), require
      # that in addition to the installation directory, each item in the activated PATH must exist.
      activated_path = self.expand_vars(self.activated_path).split(';') if hasattr(self, 'activated_path') else [self.installation_path()]
      def each_path_exists(pathlist):
        for path in pathlist:
          if not os.path.exists(path): return False
        return True
      content_exists = os.path.exists(self.installation_path()) and each_path_exists(activated_path) and (os.path.isfile(self.installation_path()) or num_files_in_directory(self.installation_path()) > 0)
      
      if self.id == 'vs-tool': # vs-tool is a special tool since all versions must be installed to the same dir, so dir name will not differentiate the version.
        return content_exists and get_installed_vstool_version(self.installation_path()) == self.version
      elif hasattr(self, 'custom_is_installed_script'):
        if self.custom_is_installed_script == 'is_optimizer_installed': return is_optimizer_installed(self)
        elif self.custom_is_installed_script == 'is_binaryen_installed': return is_binaryen_installed(self)
        else: raise Exception('Unknown custom_is_installed_script directive "' + self.custom_is_installed_script + '"!')
      else:
        return content_exists
    else:
      return True # This tool does not contain downloadable elements, so it is installed by default.

  def is_active(self):
    if not self.is_installed():
      return False

    if self.id == 'vs-tool': 
      return True # vs-tool is a special tool since all versions must be installed to the same dir, which means that if this tool is installed, it is also active.
      
    # All dependencies of this tool must be active as well.
    deps = self.dependencies()
    for tool in deps:
      if not tool.is_active():
        return False

    activated_cfg = self.activated_config()
    if activated_cfg == '':
      return len(deps) > 0

    activated_cfg = activated_cfg.split(';')
    for cfg in activated_cfg:
      cfg = cfg.strip()
      (key, value) = parse_key_value(cfg)
      if not key in dot_emscripten: return False

      # If running in embedded mode, all paths are stored dynamically relative to the emsdk root, so normalize those first.
      dot_emscripten_key = dot_emscripten[key].replace("' + emsdk_path + '", emsdk_path())
      if dot_emscripten_key != value:
        return False
    return True

  # Returns true if the system environment variables requires by this tool are currently active.
  def is_env_active(self):
    if hasattr(self, 'activated_env'):
      (key, value) = parse_key_value(self.activated_environment())
      if not key in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value):
        return False
#      if WINDOWS:
#        env_var = win_get_active_environment_variable(key)
#        if env_var != value:
#          return False
    if hasattr(self, 'activated_path'):
      path = self.expand_vars(self.activated_path).replace('\\', '/')
      path = path.split(ENVPATH_SEPARATOR)
      for p in path:
        path_items = os.environ['PATH'].replace('\\', '/').split(ENVPATH_SEPARATOR)
        if not normalized_contains(path_items, p):
          return False
    return True

  def win_activate_env_vars(self, permanently_activate):
    if WINDOWS and hasattr(self, 'activated_env'):
      (key, value) = parse_key_value(self.activated_environment())
      if permanently_activate:
        win_delete_environment_variable(key, False) # If there is an env var for the LOCAL USER with same name, it will hide the system var, so must remove that first.

      win_set_environment_variable(key, value, permanently_activate)
    
  # If this tool can be installed on this system, this function returns True.
  # Otherwise, this function returns a string that describes the reason why this tool is not available.
  def can_be_installed(self):
    if self.id == 'vs-tool':
      msbuild_dir = find_msbuild_dir()
      if len(msbuild_dir) > 0:
        return True
      else:
        return "Visual Studio 2010 was not found!"
    else:
      return True

  def download_url(self):
    if WINDOWS and hasattr(self, 'windows_url'):
      return self.windows_url
    elif OSX and hasattr(self, 'osx_url'):
      return self.osx_url
    elif LINUX and hasattr(self, 'linux_url'):
      return self.linux_url
    elif (OSX or LINUX) and hasattr(self, 'unix_url'):
      return self.unix_url
    elif hasattr(self, 'url'):
      return self.url
    else:
      return None

  def install(self):
    if self.can_be_installed() != True:
      print("The tool '" + str(self) + "' is not available due to the reason: " + self.can_be_installed())
      return False

    if self.id == 'sdk':
      print("Installing SDK '" + str(self) + "'..")
      for tool_name in self.uses:
        tool = find_tool(tool_name)
        if tool == None:
          print("Manifest error: No tool by name '" + tool_name + "' found! This may indicate an internal SDK error!")
        success = tool.install()
        if not success:
          return False
      print("Done installing SDK '" + str(self) + "'.")
      return True
    else:
      print("Installing tool '" + str(self) + "'..")
      url = self.download_url()

      if hasattr(self, 'custom_install_script') and self.custom_install_script == 'build_fastcomp':
        success = build_fastcomp_tool(self)
      elif hasattr(self, 'git_branch'):
        success = git_clone_checkout_and_pull(url, self.installation_path(), self.git_branch)
      elif url.endswith('zip') or url.endswith('.tar') or url.endswith('.gz'):
        download_even_if_exists = (self.id == 'vs-tool')
        success = download_and_unzip(url, self.installation_path(), download_even_if_exists, filename_prefix=self.zipfile_prefix if hasattr(self, 'zipfile_prefix') else '')
      else:
        dst_file = download_file(urljoin(emsdk_packages_url, self.download_url()), self.installation_path())
        if dst_file: success = True
        else: success = False

      if success and hasattr(self, 'custom_install_script'):
        if self.custom_install_script == 'build_optimizer': success = build_optimizer_tool(self)
        elif self.custom_install_script == 'build_fastcomp': pass # 'build_fastcomp' is a special one that does the download on its own, others do the download manually.
        elif self.custom_install_script == 'build_binaryen': success = build_binaryen_tool(self)
        else: raise Exception('Unknown custom_install_script command "' + self.custom_install_script + '"!')

      if not success:
        print("Installation failed!")
        return False
    print("Done installing tool '" + str(self) + "'.")

    # Sanity check that the installation succeeded, and if so, remove unneeded leftover installation files.
    if self.is_installed():
      self.cleanup_temp_install_files()
    else:
      print("Warning: The installation of '" + str(self) + "' seems to have failed, but no error was detected. Either something went wrong with the installation, or this may indicate an internal emsdk error.")
    return True

  def cleanup_temp_install_files(self):
    url = self.download_url()
    if url.endswith('zip') or url.endswith('.tar') or url.endswith('.gz'):
      file_name = url.split('/')[-1]
      tempzipfile = os.path.join('zips/', file_name)
      rmfile(tempzipfile)

  def uninstall(self):
    if not self.is_installed():
      print("Tool '" + str(self) + "' was not installed. No need to uninstall.")
      return
    print("Uninstalling tool '" + str(self) + "'..")
    if hasattr(self, 'custom_uninstall_script'):
      if self.custom_uninstall_script == 'uninstall_optimizer': uninstall_optimizer(self)
      elif self.custom_uninstall_script == 'uninstall_binaryen': uninstall_binaryen(self)
      else: raise Exception('Unknown custom_uninstall_script directive "' + custom_uninstall_script + '"!')
    try:
      print("Deleting path '" + self.installation_path() + "'")
      remove_tree(self.installation_path())
      os.remove(self.installation_path())
    except:
      pass
    print("Done uninstalling '" + str(self) + "'.")

  def dependencies(self):
    if not hasattr(self, 'uses'):
      return []
    deps = []
    
    for tool_name in self.uses:
      tool = find_tool(tool_name)
      if tool:
        deps += [tool]
    return deps

  def recursive_dependencies(self):
    if not hasattr(self, 'uses'):
      return []
    deps = []
    for tool_name in self.uses:
      tool = find_tool(tool_name)
      if tool:
        deps += [tool]
        deps += tool.recursive_dependencies()
    return deps

# A global registry of all known Emscripten SDK tools available in the SDK manifest.
tools = []

# A global registry of all known SDK toolsets.
sdks = []

# N.B. In both tools and sdks list above, we take the convention that the newest items are at the back of the list (ascending chronological order)

def find_tool(name):
  for tool in tools:
    if str(tool) == name:
      return tool
  return None

def find_sdk(name):
  for sdk in sdks:
    if str(sdk) == name:
      return sdk
  return None

def is_os_64bit(): # http://stackoverflow.com/questions/2208828/detect-64bit-os-windows-in-python
  return platform.machine().endswith('64')

def find_latest_32bit_sdk():
  for sdk in reversed(sdks): # Newest SDK is always at the end of the list.
    if not hasattr(sdk, 'bitness') or sdk.bitness == 32: # If no 'bitness' field, it means the SDK is universal.
      if not 'nightly' in sdk.version:
        return sdk
  return None

def find_latest_64bit_sdk():
  for sdk in reversed(sdks): # Newest SDK is always at the end of the list.
    if not hasattr(sdk, 'bitness') or sdk.bitness == 64: # If no 'bitness' field, it means the SDK is universal.
      if not 'nightly' in sdk.version:
        return sdk
  return None

def find_latest_sdk():
  if is_os_64bit():
    return find_latest_64bit_sdk()
  else:
    return find_latest_32bit_sdk()

def find_latest_nightly_32bit_sdk():
  for sdk in reversed(sdks): # Newest SDK is always at the end of the list.
    if not hasattr(sdk, 'bitness') or sdk.bitness == 32: # If no 'bitness' field, it means the SDK is universal.
      if 'nightly' in sdk.version:
        return sdk
  return None

def find_latest_nightly_64bit_sdk():
  for sdk in reversed(sdks): # Newest SDK is always at the end of the list.
    if not hasattr(sdk, 'bitness') or sdk.bitness == 64: # If no 'bitness' field, it means the SDK is universal.
      if 'nightly' in sdk.version:
        return sdk
  return None

def find_latest_nightly_sdk():
  if is_os_64bit():
    return find_latest_nightly_64bit_sdk()
  else:
    return find_latest_nightly_32bit_sdk()

# Finds the best-matching python tool for use.
def find_used_python():
  for t in reversed(tools): # Find newest tool first - those are always at the end of the list.
    if t.id == 'python' and t.is_installed() and t.is_active() and t.is_env_active():
      return t
  for t in reversed(tools):
    if t.id == 'python' and t.is_installed() and t.is_active():
      return t
  for t in reversed(tools):
    if t.id == 'python' and t.is_installed():
      return t
  return None

def is_version_at_least(ver, target):
  ver = ver.split('.')
  target = target.split('.')
  while len(ver) < len(target):
    ver += ['0']
  for i in range(len(ver)):
    if int(ver[i]) < int(target[i]):
      return False
    elif int(ver[i]) > int(target[i]):
      return True
  return True

def version_compare(x, y):
  a = 1 if is_version_at_least(x, y) else 0
  b = 1 if is_version_at_least(y, x) else 0
  return a - b

def fetch_emscripten_tags():
  git = GIT(must_succeed=False)
  if not git:
    print('Update complete, however skipped fetching the Emscripten tags, since git was not found.')
    if WINDOWS:
      print("If you want to compile one of the tagged releases from source, please install git by typing 'emsdk install git-1.9.4', or alternatively by installing it manually from http://git-scm.com/downloads . If you install git manually, remember to add it to PATH.")
    elif OSX:
      print("If you want to compile one of the tagged releases from source, please install git from http://git-scm.com/ , or by installing XCode and then the XCode Command Line Tools (see http://stackoverflow.com/questions/9329243/xcode-4-4-command-line-tools ).")
    elif LINUX:
      print("If you want to compile one of the tagged releases from source, please install git using your package manager, see http://git-scm.com/book/en/Getting-Started-Installing-Git .")
    else:
      print("If you want to compile one of the tagged releases from source, please install git.")
    print("If you are not looking to build Emscripten from source, you can safely ignore this message.")
    return

  print('Fetching all tags from Emscripten Github repository...')
  tags = run_get_output([GIT(), 'ls-remote', '--tags', emscripten_git_repo])
  if tags[0] != 0: return False # failed to get tags?
  tags = tags[1]
  all_tags = []
  for t in tags.split('\n'):
    try:
      t = t[t.index('refs/tags/') + len('refs/tags/'):].strip()
      if is_version_at_least(t, '1.28.2'):
        all_tags += [t]
    except:
      pass
  all_tags = sorted(all_tags, cmp=version_compare)
  open(sdk_path('emscripten-tags.txt'), 'w').write('\n'.join(all_tags))
  if len(all_tags) > 0:
    print('Done. ' + str(len(all_tags)) + ' tagged releases available, latest is ' + all_tags[-1] + '.') 
  else:
    print('Done. No tagged releases available.')

  print('Fetching all tags from Binaryen Github repository...')
  tags = run_get_output([GIT(), 'ls-remote', '--tags', binaryen_git_repo])
  if tags[0] != 0: return False # failed to get tags?
  tags = tags[1]
  binaryen_tags = []
  for t in tags.split('\n'):
    try:
      t = t[t.index('refs/tags/') + len('refs/tags/'):].strip()
      if '.' in t:
        binaryen_tags += [t]
    except:
      pass
  binaryen_tags = sorted(binaryen_tags, cmp=version_compare)
  open(sdk_path('binaryen-tags.txt'), 'w').write('\n'.join(binaryen_tags))
  if len(binaryen_tags) > 0:
    print('Done. ' + str(len(binaryen_tags)) + ' tagged Binaryen releases available, latest is ' + binaryen_tags[-1] + '.')
  else:
    print('Done. No tagged Binaryen releases available.')

  def os_name_for_llvm_location():
    if WINDOWS: return 'win'
    if LINUX: return 'linux'
    if OSX: return 'osx'
  def os_name_for_emscripten_location():
    if WINDOWS: return 'win'
    else: return 'linux'

  print('Fetching all precompiled Nightly versions..')
  download_file('https://s3.amazonaws.com/mozilla-games/emscripten/packages/llvm/nightly/' + os_name_for_llvm_location() + '_32bit/index.txt', 'llvm-nightlies-32bit.txt', download_even_if_exists=True)
  download_file('https://s3.amazonaws.com/mozilla-games/emscripten/packages/llvm/nightly/' + os_name_for_llvm_location() + '_64bit/index.txt', 'llvm-nightlies-64bit.txt', download_even_if_exists=True)
  download_file('https://s3.amazonaws.com/mozilla-games/emscripten/packages/emscripten/nightly/' + os_name_for_emscripten_location() + '/index.txt', 'emscripten-nightlies.txt', download_even_if_exists=True)

def update_emsdk():
  if WINDOWS:
    download_and_unzip(urljoin(emsdk_packages_url, 'emsdk_windows_update.zip'), emsdk_path(), download_even_if_exists=True)
    rmfile('zips/emsdk_windows_update.zip')
  elif OSX or LINUX:
    download_and_unzip(urljoin(emsdk_packages_url, 'emsdk_unix_update.tar.gz'), emsdk_path(), download_even_if_exists=True)
    rmfile('zips/emsdk_unix_update.tar.gz')
  else:
    print('Unsupported OS, cannot update!')
  fetch_emscripten_tags()

def load_emscripten_tags():
  try:
    return open(sdk_path('emscripten-tags.txt'), 'r').read().split('\n')
  except:
    return []

def load_binaryen_tags():
  try:
    return open(sdk_path('binaryen-tags.txt'), 'r').read().split('\n')
  except:
    return []

def remove_prefix(s, prefix):
  if s.startswith(prefix): return s[len(prefix):]
  else: return s

def remove_suffix(s, suffix):
  if s.endswith(suffix): return s[:len(s)-len(suffix)]
  else: return s

# Kind should be one of: 'llvm-nightlies-32bit.txt', 'llvm-nightlies-64bit.txt', 'emscripten-nightlies.txt'
def load_nightlies_list(kind):
  try:
    items = open(sdk_path(kind), 'r').read().split('\n')
    items = map(lambda x: remove_suffix(remove_suffix(remove_prefix(remove_prefix(x, 'emscripten-llvm-e'), 'emscripten-nightly-'), '.tar.gz'), '.zip').strip(), items)
    items = filter(lambda x: 'latest' not in x and len(x) > 0, items)
    return items
  except:
    return []

def load_llvm_32bit_nightlies():
  return load_nightlies_list('llvm-nightlies-32bit.txt')

def load_llvm_64bit_nightlies():
  return load_nightlies_list('llvm-nightlies-64bit.txt')

def load_emscripten_nightlies():
  return load_nightlies_list('emscripten-nightlies.txt')

def load_sdk_manifest():
  global tools, sdks
  try:
    manifest = json.loads(open(sdk_path("emsdk_manifest.json"), "r").read())
  except Exception as e:
    print('Error parsing emsdk_manifest.json!')
    print(str(e))
    return

  emscripten_tags = load_emscripten_tags()
  binaryen_tags = load_binaryen_tags()
  llvm_32bit_nightlies = list(reversed(load_llvm_32bit_nightlies()))
  llvm_64bit_nightlies = list(reversed(load_llvm_64bit_nightlies()))
  emscripten_nightlies = list(reversed(load_emscripten_nightlies()))

  def dependencies_exist(sdk):
    for tool_name in sdk.uses:
      tool = find_tool(tool_name)
      if not tool: return False
    return True

  # A 'category parameter' is a %foo%-encoded identifier that specifies
  # a class of tools instead of just one tool, e.g. %tag% or %nightly..%
  def expand_category_param(param, category_list, t, is_sdk):
    global tools, sdks
    for i in range(len(category_list)):
      ver = category_list[i]
      t2 = copy.copy(t)
      for p, v in vars(t2).iteritems():
        if isinstance(v, basestring) and param in v:
          t2.__dict__[p] = v.replace(param, ver)
      t2.is_old = i < len(category_list) - 2
      if hasattr(t2, 'uses'):
        t2.uses = map((lambda x: x.replace(param, ver)), t2.uses)
      if is_sdk:
        if dependencies_exist(t2):
          sdks.append(t2)
      else:
        tools.append(t2)

  for tool in manifest['tools']:
    t = Tool(tool)
    if t.compatible_with_this_os():
      if not hasattr(t, 'is_old'):
        t.is_old = False

      # Expand the metapackages that refer to tags or nightlies.
      if '%tag%' in t.version:
        expand_category_param('%tag%', emscripten_tags, t, is_sdk=False)
      elif '%binaryen_tag%' in t.version:
        expand_category_param('%binaryen_tag%', binaryen_tags, t, is_sdk=False)
      elif '%nightly-llvm-64bit%' in t.version:
        expand_category_param('%nightly-llvm-64bit%', llvm_64bit_nightlies, t, is_sdk=False)
      elif '%nightly-llvm-32bit%' in t.version:
        expand_category_param('%nightly-llvm-32bit%', llvm_32bit_nightlies, t, is_sdk=False)
      elif '%nightly-emscripten%' in t.version:
        expand_category_param('%nightly-emscripten%', emscripten_nightlies, t, is_sdk=False)
      else:
        tools.append(t)

  for sdk_str in manifest['sdks']:
    sdk = Tool(sdk_str)
    sdk.id = "sdk"
    if sdk.compatible_with_this_os():
      if not hasattr(sdk, 'is_old'):
        sdk.is_old = False

      if '%tag%' in sdk.version:
        expand_category_param('%tag%', emscripten_tags, sdk, is_sdk=True)
      elif '%nightly-llvm-64bit%' in sdk.version:
        expand_category_param('%nightly-llvm-64bit%', llvm_64bit_nightlies, sdk, is_sdk=True)
      elif '%nightly-llvm-32bit%' in sdk.version:
        expand_category_param('%nightly-llvm-32bit%', llvm_32bit_nightlies, sdk, is_sdk=True)
      elif '%nightly-emscripten%' in sdk.version:
        expand_category_param('%nightly-emscripten%', emscripten_nightlies, sdk, is_sdk=True)
      else:
        sdks.append(sdk)

# Tests if the two given tools can be active at the same time.
# Currently only a simple check for name for same tool with different versions,
# possibly adds more logic in the future.
def can_simultaneously_activate(tool1, tool2):
  return tool1.id != tool2.id

def remove_nonexisting_tools(tool_list, log_errors=True):
  i = 0
  while i < len(tool_list):
    tool = tool_list[i]
    if not tool.is_installed():
      if log_errors:
        print("The SDK/tool '" + str(tool) + "' cannot be activated since it is not installed! Skipping this tool...")
      tool_list.pop(i)
      continue
    i += 1
  return tool_list

# Expands dependencies for each tool, and removes ones that don't exist.
def process_tool_list(tools_to_activate, log_errors=True):
  i = 0
  # Gather dependencies for each tool
  while i < len(tools_to_activate):
    tool = tools_to_activate[i]
    deps = tool.recursive_dependencies()
    tools_to_activate = tools_to_activate[:i] + deps + tools_to_activate[i:]
    i += len(deps)+1

  tools_to_activate = remove_nonexisting_tools(tools_to_activate, log_errors=log_errors)

  # Remove conflicting tools
  i = 0
  while i < len(tools_to_activate):
    j = 0
    while j < i:
      secondary_tool = tools_to_activate[j]
      primary_tool = tools_to_activate[i]
      if not can_simultaneously_activate(primary_tool, secondary_tool):
        tools_to_activate.pop(j)
        j -= 1
        i -= 1
      j += 1
    i += 1
  return tools_to_activate

# Reconfigure .emscripten to choose the currently activated toolset, set PATH and other environment variables.
# Returns the full list of deduced tools that are now active.
def set_active_tools(tools_to_activate, permanently_activate):
  tools_to_activate = process_tool_list(tools_to_activate, log_errors=True)

  generate_dot_emscripten(tools_to_activate)

  # Construct a .bat script that will be invoked to set env. vars and PATH
  if WINDOWS:
    env_string = construct_env(tools_to_activate, False)
    open(EMSDK_SET_ENV, 'w').write(env_string)

  # Apply environment variables to global all users section.
  if WINDOWS and permanently_activate:
    # Individual env. vars
    for tool in tools_to_activate:
      tool.win_activate_env_vars(permanently_activate=True)

    # PATH variable
    newpath, added_items = adjusted_path(tools_to_activate, system_path_only=True)
    if newpath != os.environ['PATH']: # Are there any actual changes?
      win_set_environment_variable('PATH', newpath, system=True)

  return tools_to_activate

def currently_active_sdk():
  for sdk in reversed(sdks):
    if sdk.is_active():
      return sdk
  return None

def currently_active_tools():
  active_tools = []
  for tool in tools:
    if tool.is_active():
      active_tools += [tool]
  return active_tools

# http://stackoverflow.com/questions/480214/how-do-you-remove-duplicates-from-a-list-in-python-whilst-preserving-order
def unique_items(seq):
  seen = set()
  seen_add = seen.add
  return [ x for x in seq if x not in seen and not seen_add(x)]

# Tests if a path is contained in the given list, but with separators normalized.
def normalized_contains(lst, elem):
  elem = to_unix_path(elem)
  for e in lst:
    if elem == to_unix_path(e):
      return True
  return False

# Looks at the current PATH and adds and removes entries so that the PATH reflects
# the set of given active tools.
def adjusted_path(tools_to_activate, log_additions=False, system_path_only=False):
  # These directories should be added to PATH
  path_add = get_required_path(tools_to_activate)
  # These already exist.
  if WINDOWS:
    existing_path = win_get_environment_variable('PATH', system=True)
    if not system_path_only:
      current_user_path = win_get_environment_variable('PATH', system=False)
      if current_user_path: existing_path += ENVPATH_SEPARATOR + current_user_path
    existing_path = existing_path.split(ENVPATH_SEPARATOR)

    # Fix up after potential changes made by bug https://github.com/kripken/emscripten/issues/4121
    system_root = os.environ['SystemRoot'].lower()
    for i in range(len(existing_path)):
      p = existing_path[i]
      if p.lower() == system_root: p = '%SystemRoot%'
      elif (system_root + '\\system32') in p.lower(): p = '%SystemRoot%\\system32'
      elif (system_root + '\\system32\\wbem') in p.lower(): p = '%SystemRoot%\\System32\\Wbem'
      elif (system_root + '\\system32\\windowspowershell\v1.0') in p.lower(): p = '%SystemRoot%\\System32\\WindowsPowerShell\v1.0\\'
      existing_path[i] = p

  else:
    existing_path = os.environ['PATH'].split(ENVPATH_SEPARATOR)
  emsdk_root_path = to_unix_path(emsdk_path())

  existing_emsdk_tools = [item for item in existing_path if to_unix_path(item).startswith(emsdk_root_path)]
  new_emsdk_tools = [item for item in path_add if not normalized_contains(existing_emsdk_tools, item)]
  # Existing non-emsdk tools
  existing_path = [item for item in existing_path if not to_unix_path(item).startswith(emsdk_root_path)]
  new_path = [item for item in path_add if not normalized_contains(existing_path, item)]
  return (ENVPATH_SEPARATOR.join(unique_items(new_path + existing_path)), new_emsdk_tools)

def construct_env(tools_to_activate, permanent):
  global emscripten_config_directory
  env_string = ''
  newpath, added_path = adjusted_path(tools_to_activate)

# Dont permanently add to PATH, since this will break the whole system if there are more than 1024 chars in PATH.
# (SETX truncates to set only 1024 chars)
#  if permanent:
#    print('SETX PATH "' + newpath + '"')
#  else:

  if os.environ['PATH'] != newpath: # Don't bother setting the path if there are no changes.
    if WINDOWS: env_string += 'SET PATH=' + newpath + '\n'
    else: env_string += 'export PATH="' + newpath + '"\n'
    if len(added_path) > 0:
      print('Adding directories to PATH:')
      for item in added_path:
        print('PATH += ' + item)
      print('')

  env_vars_to_add = []

  # A core variable EMSDK points to the root of Emscripten SDK directory.
  env_vars_to_add += [('EMSDK', to_unix_path(emsdk_path()))]

  em_config_path = os.path.normpath(os.path.join(emscripten_config_directory, '.emscripten'))
  if not 'EM_CONFIG' in os.environ or to_unix_path(os.environ['EM_CONFIG']) != to_unix_path(em_config_path):
    env_vars_to_add += [('EM_CONFIG', em_config_path)]
  if emscripten_config_directory == emsdk_path():
    em_cache_dir = sdk_path('.emscripten_cache')
    if not 'EM_CACHE' in os.environ or to_unix_path(os.environ['EM_CACHE']) != to_unix_path(em_cache_dir):
      env_vars_to_add += [('EM_CACHE', em_cache_dir)]
    mkdir_p(em_cache_dir)

  for tool in tools_to_activate:
    if hasattr(tool, 'activated_env'):
      (key, value) = parse_key_value(tool.activated_env)
      value = to_native_path(tool.expand_vars(value))
      if not key in os.environ or to_unix_path(os.environ[key]) != to_unix_path(value): # Don't set env. vars which are already set to the correct value.
        env_vars_to_add += [(key, value)]

  if len(env_vars_to_add) > 0:
    print('Setting environment variables:')
    for key, value in env_vars_to_add:
      if WINDOWS:
        if permanent:
          env_string += 'SETX ' + key + ' "' + value + '"\n'
        else:
          env_string += 'SET ' + key + '=' + value + '\n'
      else:
        env_string += 'export ' + key + '="' + value + '"\n'
      print(key + ' = ' + value)
    print('')
  return env_string

def silentremove(filename):
  try:
    os.remove(filename)
  except OSError as e:
    if e.errno != errno.ENOENT: raise

def main():
  global emscripten_config_directory, BUILD_FOR_TESTING, ENABLE_LLVM_ASSERTIONS
  load_dot_emscripten()
  load_sdk_manifest()

  if len(sys.argv) <= 1 or sys.argv[1] == 'help' or sys.argv[1] == '--help':
    if len(sys.argv) <= 1:
      print(' emsdk: No command given. Please call one of the following:')
    else:
      print(' emsdk: Available commands:')

    print('''
   emsdk list [--old]           - Lists all available SDKs and tools and their
                                  current installation status. With the --old
                                  parameter, also historical versions are
                                  shown.

   emsdk update                 - Updates emsdk to the newest version, and also
                                  runs 'update-tags' (below).

   emsdk update-tags            - Fetches the most up to date list of available
                                  Emscripten tagged and nightly releases.

   emsdk install [options] <tool 1> <tool 2> <tool 3> ...
                                - Downloads and installs given tools or SDKs.
                                  Options can contain:

                         -j<num>: Specifies the number of cores to use when
                                  building the tool. Default: use one less
                                  than the # of detected cores.

                  --build=<type>: Controls what kind of build of LLVM to
                                  perform. Pass either 'Debug', 'Release',
                                  'MinSizeRel' or 'RelWithDebInfo'. Default:
                                  'Release' for LLVM master branch, and
                                  'RelWithDebInfo' for LLVM incoming branch.

                       --shallow: When installing tools from one of the git
                                  development branches 'master' or 'incoming',
                                  this parameter can be passed to perform a
                                  shallow git clone instead of a full one.
                                  This reduces the amount of network transfer
                                  that is needed. This option should only be
                                  used when you are interested in downloading
                                  one of the development branches, but are not
                                  looking to develop Emscripten yourself.
                                  Default: disabled, i.e. do a full clone.

                   --build-tests: If enabled, LLVM is built with internal tests
                                  included. Pass this to enable running test
                                  other.test_llvm_lit in the Emscripten test
                                  suite. Default: disabled.
             --enable-assertions: If specified, LLVM is built with assert()
                                  checks enabled. Useful for development
                                  purposes. Default: Enabled for 'incoming'
                                  branch, disabled for 'master' branch.
            --disable-assertions: Forces assertions off during the build.

               --vs2013/--vs2015: If building from source, overrides to build
                                  using the specified compiler. When installing
                                  precompiled packages, this has no effect.
                                  Note: The same compiler specifier must be
                                  passed to the emsdk activate command to
                                  activate the desired version.

   emsdk uninstall <tool/sdk>   - Removes the given tool or SDK from disk.''')

    if WINDOWS:
      print('''
   emsdk activate [--global] [--embedded] [--build=type] [--vs2013/--vs2015] <tool/sdk> 

                                - Activates the given tool or SDK in the
                                  environment of the current shell. If the
                                  --global option is passed, the registration
                                  is done globally to all users in the system
                                  environment. If the --embedded option is
                                  passed, all Emcripten configuration files as
                                  well as the temp, cache and ports directories
                                  are located inside the Emscripten SDK
                                  directory rather than the user home
                                  directory. If a custom compiler version was
                                  used to override the compiler to use, pass
                                  the same --vs2013/--vs2015 parameter here to
                                  choose which version to activate.

   emcmdprompt.bat              - Spawns a new command prompt window with the
                                  Emscripten environment active.''')
    else:
      print('''   emsdk activate [--embedded] [--build=type] <tool/sdk>

                                - Activates the given tool or SDK in the
                                  environment of the current shell. If the
                                  --embedded option is passed, all Emcripten
                                  configuration files as well as the temp, cache
                                  and ports directories are located inside the
                                  Emscripten SDK directory rather than the user
                                  home directory.''')

    print('''
       Both commands 'install' and 'activate' accept an optional parameter
       '--build=type', which can be used to override what kind of installation
       or activation to perform. Possible values for type are Debug, Release,
       MinSizeRel or RelWithDebInfo. Note: When overriding a custom build type,
       be sure to match the same --build= option to both 'install' and
       'activate' commands and the invocation of 'emsdk_env', or otherwise
       these commands will default to operating on the default build types,
       which are Release for the 'master' SDK, and RelWithDebInfo for the
       'incoming' SDK.''')
    return 1
  cmd = sys.argv[1]

  # Process global args
  for i in range(2, len(sys.argv)):
    if sys.argv[i].startswith('--build='):
      build_type = re.match(r'^--build=(.+)$', sys.argv[i])
      if build_type:
        global CMAKE_BUILD_TYPE_OVERRIDE
        build_type = build_type.group(1)
        build_types = ['Debug', 'MinSizeRel', 'RelWithDebInfo', 'Release']
        try:
          build_type_index = [x.lower() for x in build_types].index(build_type.lower())
          CMAKE_BUILD_TYPE_OVERRIDE = build_types[build_type_index]
          sys.argv[i] = ''
        except:
          print('Unknown CMake build type "' + build_type + '" specified! Please specify one of ' + str(build_types), file=sys.stderr)
          return 1
      else:
        print("Invalid command line parameter " + sys.argv[i] + ' specified!', file=sys.stderr)
        return 1
  sys.argv = [x for x in sys.argv if not len(x) == 0]

  # Replace meta-packages with the real package names.
  if (cmd == 'update' or cmd == 'install' or cmd == 'activate'):
    for i in range(2, len(sys.argv)):
      if sys.argv[i] == 'latest' or sys.argv[i] == 'sdk-latest':
        sys.argv[i] = str(find_latest_sdk())
      elif sys.argv[i] == 'latest-32bit' or sys.argv[i] == 'sdk-latest-32bit':
        sys.argv[i] = str(find_latest_32bit_sdk())
      elif sys.argv[i] == 'latest-64bit' or sys.argv[i] == 'sdk-latest-64bit':
        sys.argv[i] = str(find_latest_64bit_sdk())
      elif sys.argv[i] == 'sdk-nightly-latest':
        sys.argv[i] = str(find_latest_nightly_sdk())
      elif sys.argv[i] == 'sdk-nightly-latest-32bit':
        sys.argv[i] = str(find_latest_nightly_32bit_sdk())
      elif sys.argv[i] == 'sdk-nightly-latest-64bit':
        sys.argv[i] = str(find_latest_nightly_64bit_sdk())

  if cmd == 'list':
    print('')
    show_old = len(sys.argv) >= 3 and sys.argv[2] == '--old'
    has_partially_active_tools = [False] # Use array to work around the lack of being able to mutate from enclosing function.
    if len(tools) > 0:
      def find_tools(needs_compilation):
        t = []
        for tool in tools:
          if tool.is_old and not show_old:
            continue
          if tool.needs_compilation() != needs_compilation:
            continue
          t += [tool]
        return t

      def print_tools(t):
        for tool in t:
          if tool.is_old and not show_old:
            continue
          if tool.can_be_installed() == True:
            installed = '\tINSTALLED' if tool.is_installed() else ''
          else:
            installed = '\tNot available: ' + tool.can_be_installed()
          tool_is_active = tool.is_active()
          tool_is_env_active = tool_is_active and tool.is_env_active()
          if tool_is_env_active: active = ' * '
          elif tool_is_active:
            active = '(*)'
            has_partially_active_tools[0] = has_partially_active_tools[0] or True
          else: active = '   '
          print('    ' + active + '    {0: <25}'.format(str(tool)) + installed)
        print('')

      print('The following precompiled tool packages are available for download:')
      print_tools(find_tools(False))
      print('The following tools can be compiled from source:')
      print_tools(find_tools(True))
    else:
      print("There are no tools available. Run 'emsdk update' to fetch the latest set of tools.")
      print('')

    if len(sdks) > 0:
      def find_sdks(needs_compilation):
        s = []
        for sdk in sdks:
          if sdk.is_old and not show_old:
            continue
          if sdk.needs_compilation() == needs_compilation:
            s += [sdk]
        return s

      def print_sdks(s):
        for sdk in s:
          installed = '\tINSTALLED' if sdk.is_installed() else ''
          active = '*' if sdk.is_active() else ' '
          print('    ' + active + '    {0: <25}'.format(str(sdk)) + installed)
        print('')
      print('The following precompiled SDKs are available for download: (Run "./emsdk update" to pull in the latest list)')
      print_sdks(find_sdks(False))
      print('The following SDKs can be compiled from source:')
      print_sdks(find_sdks(True))

      print('Items marked with * are activated for the current user.')
      if has_partially_active_tools[0]:
        env_cmd = 'emsdk_env.bat' if WINDOWS else 'source ./emsdk_env.sh'
        print('Items marked with (*) are selected for use, but your current shell environment is not configured to use them. Type "' + env_cmd + '" to set up your current shell to use them' + (', or call "emsdk activate --global <name_of_sdk>" to permanently activate them.' if WINDOWS else '.'))
      if not show_old:
        print('')
        print("To access the historical archived versions, type 'emsdk list --old'")

    return 0
  elif cmd == 'construct_env':
    silentremove(EMSDK_SET_ENV) # Clean up old temp file up front, in case of failure later before we get to write out the new one.
    tools_to_activate = currently_active_tools()
    tools_to_activate = process_tool_list(tools_to_activate, log_errors=True)
    env_string = construct_env(tools_to_activate, len(sys.argv) >= 3 and 'perm' in sys.argv[2])
    open(EMSDK_SET_ENV, 'w').write(env_string)
    if LINUX or OSX: os.chmod(EMSDK_SET_ENV, 0o755)
    return 0
  elif cmd == 'update':
    update_emsdk()
    silentremove(sdk_path(EMSDK_SET_ENV)) # Clean up litter after old emsdk update which may have left this temp file around.
    return 0
  elif cmd == 'update-tags':
    fetch_emscripten_tags()
    return 0
  elif cmd == 'activate':
    permanently_activate = '--global' in sys.argv
    if permanently_activate:
      print('Registering active Emscripten environment globally for all users.')
      print('')
    embed_activation_to_sdk_dir = '--embedded' in sys.argv
    if embed_activation_to_sdk_dir:
      # Activating the emsdk tools locally relative to Emscripten SDK directory.
      emscripten_config_directory = emsdk_path()
      print('Writing .emscripten configuration file to Emscripten SDK directory ' + emscripten_config_directory)
    else:
      print('Writing .emscripten configuration file to user home directory ' + emscripten_config_directory)
      # Remove .emscripten from emsdk dir, since its presence is used to detect whether emsdk is activate in embedded mode or not.
      try:
        os.remove(os.path.join(emsdk_path(), ".emscripten"))
      except:
        pass

    sys.argv = [x for x in sys.argv if not x.startswith('--')]
    # If called without arguments, activate latest versions of all tools
    if len(sys.argv) <= 2:
      tools_to_activate = list(tools)
      tools_to_activate = remove_nonexisting_tools(tools_to_activate, log_errors=False)
    else:
      tools_to_activate = currently_active_tools()
      for i in range(2, len(sys.argv)):
        tool = find_tool(sys.argv[i])
        if tool == None:
          tool = find_sdk(sys.argv[i])
        if tool == None:
          print("Error: No tool or SDK found by name '" + sys.argv[i] + "'.")
          return 1
        tools_to_activate += [tool]
    tools_to_activate = set_active_tools(tools_to_activate, permanently_activate=permanently_activate)
    if WINDOWS and not permanently_activate:
      print('The changes made to environment variables only apply to the currently running shell instance. Use the \'emsdk_env.bat\' to re-enter this environment later, or if you\'d like to permanently register this environment globally to all users in Windows Registry, rerun this command with the option --global.')
    return 0
  elif cmd == 'install':
    # Process args
    for i in range(2, len(sys.argv)):
      if sys.argv[i].startswith('-j'):
        multicore = re.match(r'^-j(\d+)$', sys.argv[i])
        if multicore:
          global CPU_CORES
          CPU_CORES = int(multicore.group(1))
          sys.argv[i] = ''
        else:
          print("Invalid command line parameter " + sys.argv[i] + ' specified!', file=sys.stderr)
          return 1
      elif sys.argv[i] == '--shallow':
        global GIT_CLONE_SHALLOW
        GIT_CLONE_SHALLOW = True
        sys.argv[i] = ''
      elif sys.argv[i] == '--build-tests':
        BUILD_FOR_TESTING = True
        sys.argv[i] = ''
      elif sys.argv[i] == '--enable-assertions':
        ENABLE_LLVM_ASSERTIONS = 'ON'
        sys.argv[i] = ''
      elif sys.argv[i] == '--disable-assertions':
        ENABLE_LLVM_ASSERTIONS = 'OFF'
        sys.argv[i] = ''
    sys.argv = [x for x in sys.argv if not len(x) == 0]
    if len(sys.argv) <= 2:
      print("Missing parameter. Type 'emsdk install <tool name>' to install a tool or an SDK. Type 'emsdk list' to obtain a list of available tools. Type 'emsdk install latest' to automatically install the newest version of the SDK.")
      return 1
    for t in sys.argv[2:]:
      tool = find_tool(t)
      if tool == None:
        tool = find_sdk(t)
      if tool == None:
        print("Error: No tool or SDK found by name '" + t + "'.")
        return 1
      success = tool.install()
      if not success:
        return 1
  elif cmd == 'uninstall':
    if len(sys.argv) <= 2:
      print("Syntax error. Call 'emsdk uninstall <tool name>'. Call 'emsdk list' to obtain a list of available tools.")
      return 1
    tool = find_tool(sys.argv[2])
    if tool == None:
      print("Error: Tool by name '" + sys.argv[2] + "' was not found.")
      return 1
    tool.uninstall()
  else:
    print("Unknown command '" + cmd + "' given! Type 'emsdk help' to get a list of commands.")
    return 1

if __name__ == '__main__':
  sys.exit(main())
