#!/usr/bin/env python3
# -*- coding: utf8 -*-

# Copyright (C) 2015-2016 Xyne
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# (version 2) as published by the Free Software Foundation.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import AUR.PkgList
import AUR.RPC
import calendar
import collections
import colorsysplus
import fnmatch
import json
import logging
import os
import platform
import pm2ml
import Powerpill
import pyalpm
import shlex
import sqlite3
import sys
import time
import urllib.error
import XCGF
import XCPF
import xdg.BaseDirectory



################################### Globals ####################################

BB_ARGS = {
  'aur' : 'Enable AUR support.',
  'aur-only' : 'Enable AUR support and limit some operations such as system upgrades to AUR packages.',
  'bb-config' : 'The bauerbill JSON configuration file.',
  'bb-quiet' : 'Suppress warning messages for AUR operations.',
  'build-all' : 'Build targets and dependencies.',
  'build' : 'Build target packages that can be built.',
  'build-dir' : 'The directory in which to save generated scripts.',
  'build-vcs' : 'Rebuild all rebuildable VCS packages (e.g. foo-git, bar-hg)',
  'nobqd' : '(no build queue deps) Do not install calculated sync deps for build queue directly. Use this option to allow makepkg to handle all sync deps. The advantage is that the build scripts can be generated without root. The disadvantage is that some deps may be installed and removed multiple times if they are required by multiple build targets.',
}
#   'build-world' : 'Rebuild all rebuildable installed packages.',

BB_PARAM_ARGS = {
  'bb-config' : '/etc/bauerbill/bauerbill.json',
  'build-dir' : 'build',
}

RUN_HOOK_SCRIPTS = 'run_hook_scripts'
WAIT_PIDS = 'wait_pids'
INSTALL_PACKAGE = 'pkg_install'

SCRIPT_HEADER = '#!/bin/bash\nset -e\n'

RUN_HOOK_SCRIPTS_FUNCTION = '''function {}()
{{
  scriptdir="$1"
  shift 1
  if [[ -d $scriptdir ]]
  then
    shopt_nullglob="$(shopt -p nullglob)"
    shopt -s nullglob
    for script_ in "$scriptdir"/*
    do
      if [[ -x $script_ ]]
      then
        "$script_" "$@"
      fi
    done
    $shopt_nullglob
  fi
}}
'''.format(RUN_HOOK_SCRIPTS)

WAIT_PIDS_FUNCTION = '''function {}()
{{
  for pid in "$@"
  do
    while [[ -e /proc/$pid ]]
    do
      #echo "waiting for $pid"
      sleep 0.5
    done
  done
}}
'''.format(WAIT_PIDS)

INSTALL_PACKAGE_FUNCTION = '''function {}()
{{
  target_pkgname_="$1"
  pacman_config_="$2"
  shift 2
  makepkg --packagelist | while read pkg_
  do
    pkgname_="${{pkg_%-*-*-*}}"
    if [[ $pkgname_ == $target_pkgname_ ]]
    then
      sudo pacman --config "$pacman_config_" -U ./"$pkg_"* "$@"
      pacman --config "$pacman_config_" -T "$target_pkgname_" || exit 1
    fi
    break
  done
}}
'''.format(INSTALL_PACKAGE)

PACMAN_CONFIG_PLACEHOLDER = 'PacmanConfig'




############################### Argument Parsing ###############################

def parse_args(args=None):
  if args is None:
    args = sys.argv[1:]

  pp_param_opts = Powerpill.PACMAN_PARAM_OPTS | Powerpill.POWERPILL_PARAM_OPTS

  pargs = {
    'powerpill_args' : list(),
  }
  for arg in BB_ARGS:
    try:
      if BB_PARAM_ARGS[arg] is not None:
        pargs[arg] = BB_PARAM_ARGS[arg]
    except KeyError:
      pargs[arg] = False

  argq = collections.deque(XCGF.expand_short_args(args))
  while argq:
    arg = argq.popleft()
    if arg[:2] == '--' and arg[2:] in BB_ARGS:
      if arg[2:] in BB_PARAM_ARGS:
        try:
          pargs[arg[2:]] = argq.popleft()
        except IndexError:
          raise ArgumentError('no argument given for "{}"\n'.format(arg))
      else:
        pargs[arg[2:]] = True
    else:
      pargs['powerpill_args'].append(arg)
      if arg in pp_param_opts:
        pargs['powerpill_args'].append(argq.popleft())

  if pargs['aur-only']:
    pargs['aur'] = True
  if pargs['build-all']:
    pargs['build'] = True

  return pargs



def bb_arguments_help_message(indent=0):
  msg = ''
  for k,v in sorted(BB_ARGS.items(), key=lambda x: x[0]):
    if k in BB_PARAM_ARGS:
      k = '{} <{}>'.format(k, k.upper())
    msg += '''{indent}--{k}
{indent}    {v}

'''.format(indent=(indent * '  '), k=k, v=v)
  return msg


def display_help():
  '''
  Print the help message.
  '''
  name = os.path.basename(sys.argv[0])
  title = name.title()

  print('''USAGE
  {name} [{name} options] [pacman args]

OPTIONS
  {title} should accept the same arguments as Powerpill, e.g.

      {name} -Syu

  See "pacman --help" for further help.

  The following additional arguments are supported:

{arguments}
'''.format(
      name=name,
      title=title,
      arguments=bb_arguments_help_message(indent=2)
    )
  )



def get_architecture(powerpill):
  '''
  Return the target architecture.
  '''
  arch = powerpill.pacman_conf.options['Architecture']
  if arch is None or arch == 'auto':
    return platform.machine()
  else:
    return arch



def use_color(powerpill):
  '''
  Return the target architecture.
  '''
  color = powerpill.pacman_conf.options['Color']
  if color not in ('always', 'never', 'auto'):
    color = 'never'
  if color == 'never':
    return False
  elif color == 'always':
    return True
  else:
    return sys.stdout.isatty()



################################## Exceptions ##################################

class BauerbillError(XCPF.XcpfError):
  '''Parent class of all custom exceptions raised by this module.'''
  def __str__(self):
    return '{}: {}'.format(self.__class__.__name__, self.msg)



class ConfigError(BauerbillError):
  '''Exceptions raised by the Config class.'''
  pass



class ArgumentError(BauerbillError):
  '''Exceptions raised by the Config class.'''
  pass



class BuildablePkgError(BauerbillError):
  '''Exceptions raised by methods of the BuildablePkg class.'''
  pass




############################## Buildable Packages ##############################

class BuildablePkgSet(pm2ml.PkgSet):

  def __init__(self, pkgs=None):
    accessors = {
      'name' : lambda x: x.pkgname(),
      'version' : lambda x: x.version()
    }
    super(self.__class__, self).__init__(accessors, pkgs=pkgs)



def collect_pkgbases(pkgs):
  pkgbases = dict()
  for pkg in pkgs:
    try:
      pkgbases[pkg.pkgbase()].append(pkg)
    except KeyError:
      pkgbases[pkg.pkgbase()] = [pkg]
  return pkgbases



class BuildablePkgMapping(collections.abc.Mapping):
  '''
  Wrapper class to provide a mapping of BuildablePkg.
  '''
  def __init__(self, pkg):
    self.pkg = pkg
    super(self.__class__, self).__init__()
    # Use the AUR RPC names as they are the most likely to be encountered by
    # regular users.
    self.map = {
      'Name':         self.pkg.pkgname,
      'PackageBase':  self.pkg.pkgbase,
      'Repository':   self.pkg.repo,
      'Version':      self.pkg.version,
      'Maintainers':  self.pkg.maintainers,
      'Depends':      self.pkg.deps,
      'MakeDepends':  self.pkg.makedeps,
      'LastModified': self.pkg.last_modified
    }

  def __getitem__(self, key):
    v = self.map[key]()
    if not isinstance(v, str):
      try:
        v = ' '.join(iter(v))
      except TypeError:
        pass
    return v


  def __iter__(self):
    for k in self.map:
      yield k

  def items(self):
    for k, v in self.map.items():
      yield k, v()


  def __len__(self):
    return len(self.map)



# TODO
# Add a method to generate PKGBUILD-downloading Bash commands which can be used
# instead of the pbget commands.
class BuildablePkg(object):
  '''
  A wrapper object around different package objects to faciliate the generation
  of build scripts.
  '''

  def buildable(self):
    '''
    Return a boolean indicating if this package is buildable.
    '''
    return False

  def trusted(self):
    '''
    Return a boolean indicating if this package should be intrinsically trusted.
    '''
    return False

  def maintainers(self):
    '''
    Iterate over current maintainers.
    '''
    raise BuildablePkgError('maintainers method is not implemented')

  def pkgname(self):
    '''
    Return the package name.
    '''
    raise BuildablePkgError('pkgname method is not implemented')

  def version(self):
    '''
    Return the package version.
    '''
    raise BuildablePkgError('version method is not implemented')

  def pkgbase(self):
    '''
    Return the package base.
    '''
    raise BuildablePkgError('pkgbase method is not implemented')

  def repo(self):
    '''
    Return the package repository or a suitable origin identifier.
    '''
    raise BuildablePkgError('repo method is not implemented')

  def qualified_pkgbase(self):
    '''
    Returned the pkgbase with the repo prefix.
    '''
    return '{}/{}'.format(self.repo(), self.pkgbase())

  def qualified_pkgname(self):
    '''
    Returned the pkgname with the repo prefix.
    '''
    return '{}/{}'.format(self.repo(), self.pkgname())

  def last_modified(self):
    '''
    Return the last modification time, in UNIX format.
    '''
    raise BuildablePkgError('last_modified method is not implemented')

  def deps(self):
    '''
    Iterate over runtime dependencies.
    '''
    raise BuildablePkgError('deps method is not implemented')

  def makedeps(self):
    '''
    Iterate over build dependencies.
    '''
    raise BuildablePkgError('makedeps method is not implemented')

  def alldeps(self):
    '''
    Iterate over all dependencies.
    '''
    for d in self.deps():
      yield d
    for d in self.makedeps():
      yield d



class AurBuildablePkg(BuildablePkg):

  def __init__(self, pkg):
    self.pkg = pkg

  def buildable(self):
    return True

  def maintainers(self):
    m = self.pkg['Maintainer']
    if m:
      yield m

  def pkgname(self):
    return self.pkg['Name']

  def version(self):
    return self.pkg['Version']

  def pkgbase(self):
    return self.pkg['PackageBase']

  def repo(self):
    return 'AUR'

  def last_modified(self):
    return self.pkg['LastModified']

  def deps(self):
    for d in self.pkg['Depends']:
      yield d

  def makedeps(self):
    for d in self.pkg['MakeDepends']:
      yield d



class OfficialBuildablePkg(BuildablePkg):

  def __init__(self, pkg, pkginfo=None, query_func=None):
    self.pkg = pkg
    self.pkginfo = pkginfo
    for repo, x, y, z in XCPF.ARCHLINUX_OFFICIAL_REPOS:
      if self.pkg.db.name == repo:
        self.official = True
        break
    else:
      self.official = False
    if query_func is None:
      self.query_func = XCPF.archlinux_org_pkg_info
    else:
      self.query_func = query_func

  # Lazy retrieval.
  def get_pkginfo(self):
    if self.pkginfo is None:
      try:
        self.pkginfo = self.query_func(
          self.pkg.db.name, self.pkg.arch, self.pkg.name
        )
      except urllib.error.HTTPError as e:
        raise BuildablePkgError(
          'failed to retrieve pkginfo for {}'.format(self.pkg.name),
          error=e
        )
    return self.pkginfo

  def buildable(self):
    return self.official

  def trusted(self):
    return True

  def maintainers(self):
    try:
      for m in self.get_pkginfo()['maintainers']:
        yield m
    except KeyError as e:
      raise BuildablePkgError(
        'KeyError for {}'.format(self.pkg.name),
        error=e
      )

  def pkgname(self):
    return self.pkg.name

  def version(self):
    try:
      info = self.get_pkginfo()
      return '{}-{}'.format(info['pkgver'], info['pkgrel'])
    except KeyError as e:
      raise BuildablePkgError(
        'KeyError for {}'.format(self.pkg.name),
        error=e
      )

  def pkgbase(self):
    try:
      return self.get_pkginfo()['pkgbase']
    except KeyError as e:
      raise BuildablePkgError(
        'KeyError for {}'.format(self.pkg.name),
        error=e
      )

  def repo(self):
    return self.pkg.db.name

  def last_modified(self):
    try:
      return calendar.timegm(time.strptime(
        self.get_pkginfo()['last_update'],
        XCPF.ARCHLINUX_ORG_JSON_TIME_FORMAT
      ))
    except KeyError as e:
      raise BuildablePkgError(
        'KeyError for {}'.format(self.pkg.name),
        error=e
      )
    except ValueError as e:
      raise BuildablePkgError(
        'failed to parse mtime for {}'.format(self.pkg.name),
        error=e
      )

  def deps(self):
    try:
      # The pkginfo will be used for building and may be newer than the database
      # information, so prefer it.
      for m in self.get_pkginfo()['depends']:
        yield m
    except KeyError as e:
      raise BuildablePkgError(
        'KeyError for {}'.format(self.pkg.name),
        error=e
      )

  def makedeps(self):
    # TODO
    # Determine a way to get the makedeps. This isn't a showstopper because the
    # repo makedeps are guaranteed to be available via makepkg -irs.
    return iter([])


################################ Build Ordering ################################

class DependencyGraphNode(object):
  '''
  Callable dependency graph node. Each call will propagate dependency levels to
  all nodes above this one in the graph.
  '''

  def __init__(self, graph, pkg):
    self.graph = graph
    self.pkg = pkg
    self.n = 0
    graph[pkg.pkgname()] = self
    self.deps = tuple(XCPF.split_version_requirement(d)[0] for d in pkg.alldeps())
    self.calling = False

  def __call__(self, n):
    # Prevent circular recursion.
    if not self.calling:
      self.calling = True
      self.n = max(self.n, n)
      for d in self.deps:
        try:
          self.graph[d](n+1)
        except KeyError:
          pass
      self.calling = False

  def __str__(self):
    return '{}:{:d}'.format(self.pkg.name(), self.n)

  def __repr__(self):
    return 'DependencyGraphNode({})'.format(self.__str__())



def determine_dependency_graph(pkgs):
  '''
  Build the dependency graph.
  '''
  graph = dict()
  for pkg in pkgs:
    DependencyGraphNode(graph, pkg)
  for v in graph.values():
    v(0)
  return graph



def determine_build_order(graph):
  # Include the name in the sort key for deterministic build order.
  for d in sorted(graph.values(), key=lambda x: (x.n, x.pkg.pkgname()), reverse=True):
    yield(d.pkg)



############################## Package Selection ###############################

# def select_installed_pkgs(pacman_conf):
#   '''
#   Iterate over all installed packages.
#   '''
#   handle = pacman_conf.initialize_alpm()
#   for pkg in handle.get_localdb().pkgcache:
#     yield pkg

def select_installed_vcs_pkgs(pacman_conf, bb_config):
  '''
  Iterate over all package matching a VCS pattern defined in the Bauerbill
  configuration file.
  '''
  vcs_patterns = XCGF.get_nested_key_or_none(bb_config, ('VCS patterns',))
  if vcs_patterns:
    handle = pacman_conf.initialize_alpm()
    for pkg in handle.get_localdb().pkgcache:
      for p in vcs_patterns:
        if fnmatch.fnmatch(pkg.name, p):
          yield pkg
          break



def memoized_archlinux_org_pkg_info_function(powerpill, data_ttl):
  '''
  Get a memoized version of `XCPF.archlinux_org_pkg_info`.
  '''
  opi = XCPF.OfficialPkgInfo(
    config=powerpill.pargs['pacman_config'],
    ttl=data_ttl
  )
  return opi.retrieve_pkginfo



################################### Scripts ####################################

def sh_info_comments(pkgs):
  '''
  Comments containing information about the package.
  '''
  if isinstance(pkgs, list):
    pkg = pkgs[0]
  else:
    pkg = pkgs
    pkgs = None

  info = '''# Maintainer(s): {maintainers}
# Last modified: {mtime}
# Repository: {repo}'''.format(
    maintainers=' '.join(pkg.maintainers()),
    mtime=time.strftime(XCGF.DISPLAY_TIME_FORMAT, time.localtime(pkg.last_modified())),
    repo=pkg.repo()
  )

  if pkgs:
    info += '''
# Packages: {pkgnames}'''.format(
      pkgnames=' '.join(p.pkgname() for p in pkgs)
    )
  else:
    info += '''
# Package Base: {pkgbase}'''.format(
      pkgbase=pkg.pkgbase(),
    )

  return info



def iterate_hook_targets(pbs, as_dict=False):
  '''
  Convenience function for iterating over runscript targets.
  '''
  # This originally accepted Aur packages which are just dictionaries so
  # checking for isinstance(pbs, dict) did not work. I'm leaving this as-is for
  # now.
  if as_dict:
    for pkgbase, pkgs in sorted(pbs.items(), key=lambda x: x[0]):
      pkg = pkgs[0]
      yield pkg
  else:
    if not isinstance(pbs, list):
      pbs = [pbs]

    for pkg in sorted(pbs):
      yield pkg



def sh_hooks_block(bb_config, pacman_config, typ, pbs, as_dict=False):
  '''
  Get runscripts block.
  '''
  block = ''

  cpath = ('hooks', 'commands', typ)
  scriptdir = XCGF.get_nested_key_or_none(bb_config, ('hooks', 'directory'))
  hook_cmds = XCGF.get_nested_key_or_none(bb_config, cpath)
  if not (scriptdir or hook_cmds):
    return block

  for pkg in iterate_hook_targets(pbs, as_dict=as_dict):
    kwargs = dict(BuildablePkgMapping(pkg))
    kwargs[PACMAN_CONFIG_PLACEHOLDER] = pacman_config

    if scriptdir:
      dpath = os.path.join(scriptdir, pkg.pkgbase(), typ)
      cmd = [RUN_HOOK_SCRIPTS, dpath]

      args = get_matching_cmd(bb_config, ('hooks', 'arguments'), pkg, required=False)
      if args:
        args = XCGF.sh_quote_words(args, kwargs=kwargs)
        cmd.extend(args)
      block += XCGF.sh_quote_words(cmd) + '\n'

    if hook_cmds:
      cmds = get_matching_cmd(bb_config, cpath, pkg, required=False)
      if cmds:
        for cmd in cmds:
          if cmd:
            cmd = XCGF.sh_quote_words(cmd, kwargs=kwargs)
            if cmd:
              block += cmd + '\n'
  return block



def get_matching_cmd(bb_config, cpath, pkg, required=True):
  '''
  Match package against custom and VCS pattern to determine which value to
  return.
  '''
  cpath = list(cpath)
  value = None
  pkgbase = pkg.pkgbase()
  qualified_pkgbase = pkg.qualified_pkgbase()

  custom_cmds = XCGF.get_nested_key_or_none(bb_config, cpath + ['custom'])
  if custom_cmds:
    for pattern, c in custom_cmds:
      if fnmatch.fnmatch(qualified_pkgbase, pattern):
        value = c

  if not value:
    vcs_cmd = XCGF.get_nested_key_or_none(bb_config, cpath + ['VCS'])
    if vcs_cmd:
      vcs_patterns = XCGF.get_nested_key_or_none(bb_config, ['VCS patterns'])
      if vcs_patterns:
        for pattern in vcs_patterns:
          if fnmatch.fnmatch(pkgbase, pattern):
            value = vcs_cmd

  if not value:
    value = XCGF.get_nested_key_or_none(bb_config, cpath + ['default'])

  if not value:
    if required:
      raise ConfigError('missing default: {}'.format(cpath))

  value = value.copy()
  common_args = XCGF.get_nested_key_or_none(bb_config, cpath + ['common arguments'])
  if common_args:
    value.extend(common_args)

  return value



def get_bin(bb_config, cmd):
  try:
    return bb_config['bin'][cmd].copy()
  except KeyError:
    return [cmd]



def get_query_trust_cmd(bb_config, pkg, subdir=False):
  '''
  Get the query trust command to prompt the user if necessary.
  '''
  qt_cmd = get_bin(bb_config, 'bb-query_trust')
  path = '.'
  if subdir:
    path = os.path.join(path, pkg.pkgbase())
  qt_cmd.extend((
    '-l', path,
    pkg.qualified_pkgbase(),
    pkg.last_modified(),
    *pkg.maintainers()
  ))
  return '{} || exit 1'.format(XCGF.sh_quote_words(qt_cmd))



def download_script(bb_config, pacman_config, pargs, build_pkgs):
  '''
  Create a script to download the PKGBUILDs and source files.
  '''
  pbs = collect_pkgbases(build_pkgs)
  pkgbases = sorted(pbs)
  pbget_cmd = get_bin(bb_config, 'pbget')

  if pargs['aur-only'] or not pargs['pbget repo packages']:
    pbget_cmd.append('--aur-only')
  elif pargs['aur']:
    pbget_cmd.append('--aur')
  pbget_cmd.extend(pkgbases)

  script = '{}\n{}\n{}\n'.format(
    SCRIPT_HEADER,
    RUN_HOOK_SCRIPTS_FUNCTION,
    WAIT_PIDS_FUNCTION
  )

  preget_block = sh_hooks_block(
    bb_config, pacman_config, 'preget', pbs, as_dict=True
  )
  if preget_block:
    script += '''{preget_header}
{preget_block}
'''.format(
    preget_header=XCGF.sh_comment_header('Run preget scripts, if any.'),
    preget_block=preget_block
  )

  script += '''{pbget_header}
{pbget_cmd}

pids=()
'''.format(
    pbget_header=XCGF.sh_comment_header('Get PKGBUILDS and related files.'),
    pbget_cmd=XCGF.sh_quote_words(pbget_cmd),
  )

  entries = list(
    get_query_trust_cmd(
      bb_config,
      pbs[pkgbase][0],
      subdir=True
    ) for pkgbase in pkgbases if not pbs[pkgbase][0].trusted()
  )
  if entries:
    script += '''
{}
{}
'''.format(
      XCGF.sh_comment_header('Query trust before starting downloads.'),
      '\n'.join(entries)
    )

  for pkgbase in pkgbases:
    pkgs = pbs[pkgbase]
    pkg = pkgs[0]
    kwargs = dict(BuildablePkgMapping(pkg))
    kwargs[PACMAN_CONFIG_PLACEHOLDER] = pacman_config

    kwargs = {
      'header' : XCGF.sh_comment_header('Download sources for package base {}.'.format(pkgbase)),
      'info' : sh_info_comments(pkgs),
      'quoted_pkgbase' : shlex.quote(pkgbase),
      'predownload_block' : sh_hooks_block(
        bb_config, pacman_config, 'predownload', pbs[pkgbase]
      ),
      'download_cmd' : XCGF.sh_quote_words(
        get_matching_cmd(bb_config, ('makepkg commands', 'download'), pkg),
        kwargs=kwargs
      )
    }

    script += '''
{header}
{info}
pushd {quoted_pkgbase}
{predownload_block}{download_cmd} &
pids+=($!)
echo {quoted_pkgbase} ": $!"
popd
'''.format(**kwargs)

  script += '''
{header}
{wait_pids} "${{pids[@]}}"
'''.format(
      header=XCGF.sh_comment_header('Wait for downloads and verifications to finish.'),
      wait_pids=WAIT_PIDS
    )
  return script



def build_script(bb_config, pacman_config, build_pkgs, build_deps):
  '''
  Create a script to build and install the target packages.
  '''
  build_graph = determine_dependency_graph(build_pkgs)
  script = '{}\n{}'.format(
    SCRIPT_HEADER,
    RUN_HOOK_SCRIPTS_FUNCTION
  )
  for pkg in determine_build_order(build_graph):
    if pkg in build_deps:
      qualifier = ' --asdeps'
    else:
      qualifier = ''
    if pkg.trusted():
      query_trust_cmd = ''
    else:
      query_trust_cmd = get_query_trust_cmd(bb_config, pkg) + '\n'
    kwargs = dict(BuildablePkgMapping(pkg))
    kwargs[PACMAN_CONFIG_PLACEHOLDER] = pacman_config

    script += '''
{header}
{info}
pushd {pkgbase}
{query_trust_cmd}{prebuild_block}{build_cmd}{qualifier}
pacman --config {pconfig_path} -T {safename} || exit 1
popd
'''.format(
      header=XCGF.sh_comment_header(
        'Build package {}'.format(pkg.pkgname())
      ),
      safename=shlex.quote(pkg.pkgname()),
      pkgbase=shlex.quote(pkg.pkgbase()),
      info=sh_info_comments(pkg),
      query_trust_cmd=query_trust_cmd,
      prebuild_block=sh_hooks_block(
        bb_config, pacman_config, 'prebuild', pkg
      ),
      build_cmd=XCGF.sh_quote_words(
        get_matching_cmd(bb_config, ('makepkg commands', 'build'), pkg),
        kwargs=kwargs
      ),
      pconfig_path=shlex.quote(pacman_config),
      qualifier=qualifier
    )
  return script



def clean_script(bb_config, pacman_config, remaining_deps):
  '''
  Create a script to remove leftover build dependencies.
  '''
  sudo_cmd = get_bin(bb_config, 'sudo')
  pacman_cmd = get_bin(bb_config, 'pacman')
  rm_cmd = sudo_cmd + pacman_cmd + ['-Runs', '--config', pacman_config]
  lst_cmd = pacman_cmd + ['-Qq', '--config', pacman_config]
  return '''{}
target_pkgs_=(
  {}
)
installed_pkgs_=($({} "${{target_pkgs_[@]}}"))
{} "${{installed_pkgs_[@]}}"
'''.format(
    SCRIPT_HEADER,
    '\n  '.join(shlex.quote(rd) for rd in sorted(remaining_deps)),
    XCGF.sh_quote_words(lst_cmd),
    XCGF.sh_quote_words(rm_cmd)
  )



def save_scripts(dpath, scripts, print_msg=False):
  '''
  Save the scripts to the output directory and chown them as necessary.
  '''
  paths = [dpath]
  try:
    os.mkdir(dpath)
  except FileExistsError:
    pass
  for name, script in scripts.items():
    path = os.path.join(dpath, '{}.sh'.format(name))
    paths.append(path)
    with open(path, 'w') as f:
      f.write(script)
    msg = 'created {}'.format(path)
    logging.info('created {}'.format(path))
    if print_msg:
      print(msg)

  name, uid, gid = XCGF.get_sudo_user_info()

  for p in paths:
    os.chmod(p, 0o755)
    if uid is not None:
      os.chown(p, uid, gid)



################################### Display ####################################

def display_sync_info(pargs, pacman_conf, aur, pkgs):
  '''
  Emulate "pacman -Si".
  '''
  pm2ml_args = ['--nodeps'] + pkgs
  if pargs['aur-only']:
    pm2ml_args.append('--aur-only')
  elif pargs['aur']:
    pm2ml_args.append('--aur')
  pm2ml_pargs = pm2ml.parse_args(pm2ml_args)

  handle = pacman_conf.initialize_alpm()

  sync_pkgs, sync_deps, \
  aur_pkgs, aur_deps, \
  not_found, unknown_deps, orphans = pm2ml.resolve_targets_from_arguments(
    handle, pacman_conf, pm2ml_pargs, aur=aur
  )

  for p in sync_pkgs:
    print(XCPF.format_pkginfo(p))

  if aur_pkgs:
    for i in AUR.RPC.format_pkginfo(aur, aur_pkgs):
      print(i)

  if not_found:
    msg = 'not found: {}'.format(' '.join(not_found))
    logging.error(msg)
    return 1
  else:
    return 0


# community/i3-wm 4.11-1 (i3) [installed]
#     An improved dynamic tiling window manager
def display_sync_search(pargs, powerpill, aur):
  '''
  Emulate "pacman -Ss"
  '''
  if pargs['aur-only']:
    e = 0
  else:
    e = powerpill.run_pacman()
  if aur is None:
    return e

  found_all = None
  for term in powerpill.pargs['args']:
    found = pm2ml.AurPkgSet(aur.search(term))
    if found_all is None:
      found_all = found
    else:
      found_all &= found

  if found_all:
    if not powerpill.pargs['quiet']:
      handle = powerpill.pacman_conf.initialize_alpm()
      local_db = handle.get_localdb()
      color = use_color(powerpill)
    else:
      local_db = None
      color = False

    # *_ce: color escape
    if color:
      repo_ce = colorsysplus.ansi_sgr(fg=13)
      version_ce = colorsysplus.ansi_sgr(fg=10)
      installed_ce = colorsysplus.ansi_sgr(fg=14)
      reset_ce = colorsysplus.ansi_sgr(reset=True)
    else:
      repo_ce = ''
      version_ce = ''
      installed_ce = ''
      reset_ce = ''

    for pkg in sorted(found_all, key=lambda p: p['Name']):
      if local_db is None:
        print(pkg['Name'])

      else:
        if local_db.get_pkg(pkg['Name']):
          tag = ' {}[installed]'.format(installed_ce)
        else:
          tag = ''
        print('''{repo_ce}AUR{reset_ce}/{name}{tag}{reset_ce}
    {desc}'''.format(
          name=pkg['Name'],
          tag=tag,
          desc=pkg['Description'],
          repo_ce=repo_ce,
          reset_ce=reset_ce
        ))
  return e


def display_aur_list(powerpill, aur_pkglist, local_db=None):
  # <repo> <pkgname> <pkgver> [installed]
  # Version is missing and not worth retrieving via RPC.
  # *_ce: color escape
  if use_color(powerpill):
    repo_ce = colorsysplus.ansi_sgr(fg=13)
    version_ce = colorsysplus.ansi_sgr(fg=10)
    installed_ce = colorsysplus.ansi_sgr(fg=14)
    reset_ce = colorsysplus.ansi_sgr(reset=True)
  else:
    repo_ce = ''
    version_ce = ''
    installed_ce = ''
    reset_ce = ''
  if aur_pkglist:
    for p in sorted(aur_pkglist):
      if local_db is None:
        print(p)
      else:
        if local_db.get_pkg(p):
          tag = ' {}[installed]'.format(installed_ce)
        else:
          tag = ''
        print('{repo_ce}AUR{reset_ce} {pkg}{tag}{repo_ce}'.format(
          pkg=p,
          tag=tag,
          repo_ce=repo_ce,
          reset_ce=reset_ce
        ))


def display_sync_list(powerpill, aur_pkglist, aur_only=False):
  '''
  Emulate "pacman -Sl".
  '''
  if not powerpill.pargs['quiet']:
    handle = powerpill.pacman_conf.initialize_alpm()
    local_db = handle.get_localdb()
  else:
    local_db = None
  if not powerpill.pargs['args']:
    if not aur_only:
      e = powerpill.run_pacman()
    else:
      e = None
    if not e:
      display_aur_list(powerpill, aur_pkglist, local_db)
    return e
  # TODO
  # Improve thise.
  else:
    e = None
    if 'AUR' in powerpill.pargs['args']:
      for a in list(powerpill.pargs['args']):
        if e:
          break
        elif a == 'AUR':
          display_aur_list(powerpill, aur_pkglist, local_db)
        else:
          powerpill.pargs['args'] = [a]
          e = powerpill.run_pacman()
    else:
      e = powerpill.run_pacman()
    return e



def display_query_upgrades(pargs, powerpill, aur):
  '''
  Emulate "pacman -Qu".
  '''
  if pargs['aur-only']:
    e = 0
  else:
    e = powerpill.run_pacman()
  if aur is None:
    return e

  if pargs['aur-only']:
    pm2ml_args = ['--aur-only']
  else:
    pm2ml_args = ['--aur']

  pm2ml_args.extend(powerpill.get_pm2ml_pkg_download_args(ignore=False))
  pm2ml_pargs = pm2ml.parse_args(pm2ml_args)
  handle = powerpill.pacman_conf.initialize_alpm()

  sync_pkgs, sync_deps, \
  aur_pkgs, aur_deps, \
  not_found, unknown_deps, orphans = pm2ml.resolve_targets_from_arguments(
    handle, powerpill.pacman_conf, pm2ml_pargs, aur=aur,
    ignore_ignored=True,
  )

  if aur_pkgs:
    # *_ce: color escape
    if use_color(powerpill):
      version_ce = colorsysplus.ansi_sgr(fg=10)
      reset_ce = colorsysplus.ansi_sgr(reset=True)
    else:
      version_ce = ''
      reset_ce = ''

    show = set(powerpill.pargs['args'])
    local_db = handle.get_localdb()
    for pkgname, pkg in sorted(aur_pkgs.pkgs.items()):
      if show and pkgname not in show:
        continue

      if powerpill.pargs['quiet']:
        print(pkgname)

      else:
        local_pkg = local_db.get_pkg(pkgname)
        if local_pkg:
          if pkgname in powerpill.pacman_conf.options['IgnorePkg']:
            ignored = ' [ignored]'
          else:
            ignored = ''
          print('{pkgname} {vce}{locver}{rce} -> {vce}{aurver}{rce}{ignored}'.format(
            pkgname=pkgname,
            vce=version_ce,
            rce=reset_ce,
            aurver=pkg['Version'],
            locver=local_pkg.version,
            ignored=ignored
          ))
  return e





##################################### Main #####################################

# This should follow the main function of Powerpill.
def main(args=None):
  pargs = parse_args(args)

  pp_pargs = Powerpill.parse_args(pargs['powerpill_args'])
  if pp_pargs['help']:
    display_help()
    return 0

  try:
    bb_config = XCGF.load_json(pargs['bb-config'])
  except (ValueError, FileNotFoundError) as e:
    raise ConfigError(
      'failed to load configuration file [{}]'.format(pargs['bb-config']),
      error=e
    )

  Powerpill.configure_logging(pp_pargs, pargs['bb-quiet'])

  powerpill_conf = Powerpill.Config(pp_pargs['powerpill_config'])
  pacman_conf = Powerpill.get_pacman_conf(pp_pargs, powerpill_conf)
  powerpill = Powerpill.Powerpill(powerpill_conf, pacman_conf, pp_pargs)

  exit_code = 0
  data_ttl = pargs.get('data ttl', AUR.common.DEFAULT_TTL)
  pkginfo_dbpath = XCPF.pkginfo_dbpath()

  # Clean up before doing anything else.
  if pp_pargs['powerpill_clean']:
    Powerpill.clean(Powerpill.get_cleaning_targets(pacman_conf))
    if Powerpill.no_operation(pp_pargs):
      return 0

  if not pp_pargs['sync']:
    # -Qu --aur
    if pargs['aur'] and Powerpill.query_upgrades(pp_pargs):
      aur = AUR.RPC.AUR(ttl=data_ttl)
      return display_query_upgrades(pargs, powerpill, aur)
    else:
      return powerpill.run_pacman()



  if pargs['aur']:
    aur = AUR.RPC.AUR(ttl=data_ttl)
    aur_pkglist = AUR.PkgList.PkgList(ttl=data_ttl, auto_refresh=True)
    if pp_pargs['refresh'] > 1:
      aur.db_clean(wipe=True)
      aur_pkglist.refresh(0)
    elif pp_pargs['refresh']:
      aur.db_clean()
      aur_pkglist.refresh(0)
  else:
    aur = None
    aur_pkglist = None

  if pp_pargs['refresh'] > 0:
    if not pargs['aur-only']:
      Powerpill.refresh_databases(powerpill)
      try:
        os.unlink(pkginfo_dbpath)
      except FileNotFoundError:
        pass
    if pargs['aur']:
      if pp_pargs['refresh'] > 1:
        ct = 0
      else:
        ct = -1
      aur_pkglist.refresh(ttl=ct)
      # TODO
      # Remove the local RPC caching database here.
      # aur.ttl = 0
    pp_pargs['refresh'] = 0



#   if pargs['build-world']:
#     pp_pargs['args'] += list(pkg.name for pkg in select_installed_pkgs(pacman_conf))
#   el
  if pargs['build-vcs']:
    force_build_pkgs = pm2ml.PyalpmPkgSet(
      p for p in select_installed_vcs_pkgs(pacman_conf, bb_config)
    )
    pp_pargs['args'] += list(p.name for p in force_build_pkgs)
  else:
    force_build_pkgs = pm2ml.PyalpmPkgSet()

  if Powerpill.no_download(pp_pargs):
    if pp_pargs['other_operation']:

      if Powerpill.info_operation(pp_pargs):
        return display_sync_info(pargs, pacman_conf, aur, pp_pargs['args'])

      elif Powerpill.search_operation(pp_pargs):
        return display_sync_search(pargs, powerpill, aur)

      elif Powerpill.list_operation(pp_pargs):
        return display_sync_list(powerpill, aur_pkglist, aur_only=pargs['aur-only'])

      else:
        return powerpill.run_pacman()

    return 0

  build_pkgs = BuildablePkgSet()
  build_deps = BuildablePkgSet()
  sync_pkgs = pm2ml.PyalpmPkgSet()
  sync_deps = pm2ml.PyalpmPkgSet()

  if pp_pargs['sysupgrade'] > 0 or pp_pargs['args']:
    if pargs['build'] or pargs['aur']:

      if pargs['aur-only']:
        pm2ml_args = ['--aur-only']
      elif pargs['aur']:
        pm2ml_args = ['--aur']
      else:
        pm2ml_args = list()

      pm2ml_args.extend(powerpill.get_pm2ml_pkg_download_args())
      pm2ml_pargs = pm2ml.parse_args(pm2ml_args)
      handle = pacman_conf.initialize_alpm()

      sync_pkgs, sync_deps, \
      aur_pkgs, aur_deps, \
      not_found, unknown_deps, orphans = pm2ml.resolve_targets_from_arguments(
        handle, pacman_conf, pm2ml_pargs, aur=aur
      )
      pp_pargs['args'] = list(x.name for x in sync_pkgs)

      orphan_names = set(p.name for p in orphans)
      for label, xs, is_error in (
        ('Not found', not_found, False),
        ('Unresolved dependencies', unknown_deps, True),
        ('Installed orphans', orphan_names, False)
      ):
        if xs:
          msg = '{}: {}'.format(label, ' '.join(sorted(xs)))
          if is_error:
            logging.error(msg)
          else:
            logging.warn(msg)

      if pargs['aur']:
        build_pkgs = BuildablePkgSet(
          AurBuildablePkg(p) for p in (aur_pkgs | aur_deps)
        )
        build_deps = BuildablePkgSet(
          AurBuildablePkg(p) for p in aur_deps
        )


  # This can be used to avoid querying the GIT interface if there are only AUR
  # packages for the download.
  pargs['pbget repo packages'] = False
  # TODO
  # Carefully consider if naming conflicts can arise when unioning packages
  # from different sources in the BuildablePkgSets.
  if pargs['build']:
    retriever =  memoized_archlinux_org_pkg_info_function(powerpill, data_ttl)

    if sync_pkgs:
    # This can be used to avoid querying the GIT interface if there are only
    # AUR packages for the download.
      pargs['pbget repo packages'] = True
      build_pkgs |= BuildablePkgSet(
        OfficialBuildablePkg(p,query_func=retriever) for p in sync_pkgs
      )
    if pargs['build-all']:
      if sync_deps:
        pargs['pbget repo packages'] = True
      build_pkgs |= BuildablePkgSet(
        OfficialBuildablePkg(p,query_func=retriever) for p in sync_deps
      )
      build_deps |= BuildablePkgSet(
        OfficialBuildablePkg(p,query_func=retriever) for p in sync_deps
      )
      pp_pargs['args'].clear()

    else:
      if pargs['nobqd']:
        pp_pargs['args'].clear()
      else:
        pp_pargs['options'].append('--asdeps')
        pp_pargs['args'] = list(x.name for x in sync_deps)

  # Build packages selected by user.
  elif force_build_pkgs:
    if sync_pkgs:
      force_build_sync_pkgs = sync_pkgs & force_build_pkgs
      if force_build_sync_pkgs:
        retriever =  memoized_archlinux_org_pkg_info_function(powerpill, data_ttl)
        build_pkgs |= BuildablePkgSet(
          OfficialBuildablePkg(p,query_func=retriever) for p in force_build_sync_pkgs
        )
        pargs['pbget repo packages'] = True
        sync_pkgs -= force_build_pkgs
    pp_pargs['args'] = list(p for p in pp_pargs['args'] if p not in force_build_pkgs)




  if pargs['build'] or pargs['aur-only']:
    pp_pargs['sysupgrade'] = 0


  if not Powerpill.no_download(pp_pargs):
    Powerpill.download_packages(powerpill)

  if Powerpill.proceed_to_installation(pp_pargs):
    exit_code = powerpill.run_pacman()

  if build_pkgs:
    unbuildable_pkgs = BuildablePkgSet(
      p for p in build_pkgs if not p.buildable()
    )
    build_pkgs -= unbuildable_pkgs
    # The build_deps are a subset of build_pkgs.
    build_deps -= unbuildable_pkgs

    # Anything unbuildable must be available in a binary repo and can therefore
    # be installed by makepkg so display warnings instead of errors.
    if unbuildable_pkgs:
      logging.warn('unbuildable: {}'.format(
        ' '.join(sorted(p.pkgname() for p in unbuildable_pkgs))
      ))

    pacman_config = powerpill.pargs['pacman_config']
    scripts = collections.OrderedDict()
    scripts['download'] = download_script(
      bb_config, pacman_config, pargs, build_pkgs
    )
    scripts['build'] = build_script(
      bb_config, pacman_config, build_pkgs, build_deps
    )


#     if not pargs['nobqd']:
    remaining_deps = set(d.pkgname() for d in build_deps)
    if remaining_deps:
      # Don't remove anything that's already installed.
      handle = pacman_conf.initialize_alpm()
      local_db = handle.get_localdb()
      remaining_deps = set(d for d in remaining_deps if not local_db.get_pkg(d))
      if remaining_deps:
        scripts['clean'] = clean_script(
          bb_config, powerpill.pargs['pacman_config'], remaining_deps
        )

    save_scripts(pargs['build-dir'], scripts, print_msg=True)

  return exit_code










def run_main(args=None):
  try:
    return main(args)
  except (KeyboardInterrupt, BrokenPipeError):
    pass
  except (
    XCGF.LockError,
    XCPF.XcpfError,
    Powerpill.PowerpillError,
    pyalpm.error,
    PermissionError
  ) as e:
    logging.error(str(e))
    return 1



if __name__ == '__main__':
  try:
    run_main()
  except KeyboardInterrupt:
    pass
