#
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
#
#
# gen_make.py -- generate makefiles and dependencies
#

import os
import stat
import sys
try:
  # Python >=3.0
  import configparser
except ImportError:
  # Python <3.0
  import ConfigParser as configparser

if sys.version_info[0] >= 3:
  # Python >=3.0
  from io import StringIO
else:
  # Python <3.0
  try:
    from cStringIO import StringIO
  except ImportError:
    from StringIO import StringIO

import ezt

import gen_base
import generator.swig.header_wrappers
import generator.swig.checkout_swig_header
import generator.swig.external_runtime

from gen_base import build_path_join, build_path_strip, build_path_splitfile, \
      build_path_basename, build_path_dirname, build_path_retreat, unique


def _normstr(x):
  if os.sep == '/':
    return os.path.normpath(str(x))
  else:
    return os.path.normpath(str(x).replace('/', os.sep)).replace(os.sep, '/')

class Generator(gen_base.GeneratorBase):

  _extension_map = {
    ('exe', 'target'): '$(EXEEXT)',
    ('exe', 'object'): '.lo',
    ('lib', 'target'): '.la',
    ('lib', 'object'): '.lo',
    ('pyd', 'target'): '.la',
    ('pyd', 'object'): '.lo',
    ('so', 'target'): '.la',
    ('so', 'object'): '.lo',
    }

  def __init__(self, fname, verfname, options=None):
    gen_base.GeneratorBase.__init__(self, fname, verfname, options)
    self.assume_shared_libs = False
    if ('--assume-shared-libs', '') in options:
      self.assume_shared_libs = True

  def write(self):
    install_deps = self.graph.get_deps(gen_base.DT_INSTALL)
    install_sources = self.graph.get_all_sources(gen_base.DT_INSTALL)

    cp = configparser.ConfigParser()
    cp.read('gen-make.opts')
    if cp.has_option('options', '--installed-libs'):
      self.installed_libs = cp.get('options', '--installed-libs').split(',')
    else:
      self.installed_libs = []

    # ensure consistency between runs
    install_deps.sort()
    install_sources.sort(key = lambda s: s.name)

    class _eztdata(object):
      def __init__(self, **kw):
        vars(self).update(kw)

    data = _eztdata(
      modules=[ ],
      swig_langs=[ ],
      swig_c=[ ],
      target=[ ],
      itargets=[ ],
      areas=[ ],
      isources=[ ],
      deps=[ ],
      sql=[],
      )

    ########################################

    for target in install_sources:
      if isinstance(target, gen_base.TargetRaModule) or \
         isinstance(target, gen_base.TargetFsModule):
        # name of the module: strip 'libsvn_' and upper-case it
        name = target.name[7:].upper()

        # construct a list of the other .la libs to link against
        retreat = build_path_retreat(target.path)
        if target.name in self.installed_libs:
          deps = []
          link = [ '-l%s-%s' % (target.name[3:], self.version) ]
        else:
          deps = [ target.filename ]
          link = [ build_path_join(retreat, target.filename) ]
        for source in self.graph.get_sources(gen_base.DT_LINK, target.name):
          if not isinstance(source, gen_base.TargetLib) or source.external_lib:
            continue
          elif source.name in self.installed_libs:
            continue
          deps.append(source.filename)
          link.append(build_path_join(retreat, source.filename))

        data.modules.append(_eztdata(name=name, deps=deps, link=link))

    # write a list of directories in which things are built
    #   get all the test scripts' directories
    script_dirs = list(map(build_path_dirname, self.scripts + self.bdb_scripts))

    #   remove duplicate directories between targets and tests
    build_dirs = unique(self.target_dirs + script_dirs + self.swig_dirs)
    data.build_dirs = build_dirs

    # write lists of test files
    # deps = all, progs = not including those marked "testing = skip"
    data.bdb_test_deps = self.bdb_test_deps + self.bdb_scripts
    data.bdb_test_progs = self.bdb_test_progs + self.bdb_scripts
    data.test_deps = self.test_deps + self.scripts
    data.test_progs = self.test_progs + self.scripts
    data.test_helpers = self.test_helpers

    # write list of all manpages
    data.manpages = self.manpages

    # write a list of files to remove during "make clean"
    cfiles = [ ]
    for target in install_sources:
      # .la files are handled by the standard 'clean' rule; clean all the
      # other targets
      if not isinstance(target, gen_base.TargetScript) \
         and not isinstance(target, gen_base.TargetProject) \
         and not isinstance(target, gen_base.TargetI18N) \
         and not isinstance(target, gen_base.TargetJava) \
         and not target.external_lib \
         and target.filename[-3:] != '.la':
        cfiles.append(target.filename)
    for script in self.scripts:
      if script.endswith('.py'):
        cfiles.append(script + 'c')
    data.cfiles = sorted(cfiles)

    # here are all the SQL files and their generated headers. the Makefile
    # has an implicit rule for generating these, so there isn't much to do
    # except to clean them out. we only do that for 'make extraclean' since
    # these are included as part of the tarball. the files are transformed
    # by gen-make, and developers also get a Make rule to keep them updated.
    for hdrfile, sqlfile in sorted(self.graph.get_deps(gen_base.DT_SQLHDR),
                                   key=lambda t: t[0]):
      data.sql.append(_eztdata(header=hdrfile, source=sqlfile[0]))

    data.release_mode = ezt.boolean(self.release_mode)

    ########################################

    if not self.release_mode:
      swig_rules = StringIO()
      for swig in (generator.swig.header_wrappers,
                   generator.swig.checkout_swig_header,
                   generator.swig.external_runtime):
        gen = swig.Generator(self.conf, "swig")
        gen.write_makefile_rules(swig_rules)

      data.swig_rules = swig_rules.getvalue()

    ########################################

    # write dependencies and build rules for generated .c files
    swig_c_deps = sorted(self.graph.get_deps(gen_base.DT_SWIG_C),
                         key=lambda t: t[0].filename)

    swig_lang_deps = {}
    for lang in self.swig.langs:
      swig_lang_deps[lang] = []

    for objname, sources in swig_c_deps:
      swig_lang_deps[objname.lang].append(str(objname))

    for lang in self.swig.langs:
      data.swig_langs.append(_eztdata(name=lang,
                                      short=self.swig.short[lang],
                                      short_upper=self.swig.short[lang].upper(),
                                      deps=swig_lang_deps[lang]))

    ########################################

    if not self.release_mode:
      for objname, sources in swig_c_deps:
        data.swig_c.append(_eztdata(c_file=str(objname),
                                    deps=list(map(str, sources)),
                                    opts=self.swig.opts[objname.lang],
                                    source=str(sources[0])))

    ########################################

    for target_ob in install_sources:

      if isinstance(target_ob, gen_base.TargetScript):
        # there is nothing to build
        continue

      target = target_ob.name
      if isinstance(target_ob, gen_base.TargetJava):
        path = target_ob.output_dir
      else:
        path = target_ob.path

      retreat = build_path_retreat(path)

      # get the source items (.o and .la) for the link unit
      objects = [ ]
      objdeps = [ ]
      object_srcs = [ ]
      headers = [ ]
      header_classes = [ ]
      header_class_filenames = [ ]
      deps = [ ]
      libs = [ ]
      add_deps = target_ob.add_deps.split()

      for link_dep in self.graph.get_sources(gen_base.DT_LINK, target_ob.name):
        if isinstance(link_dep, gen_base.TargetJava):
          deps.append(link_dep.name)
        elif isinstance(link_dep, gen_base.TargetLinked):
          if link_dep.external_lib:
            libs.append(link_dep.external_lib)
          elif link_dep.external_project:
            # FIXME: This is a temporary workaround to fix build breakage
            # expeditiously.  It is of questionable validity for a build
            # node to have external_project but not have external_lib.
            pass
          elif link_dep.name in self.installed_libs:
            libs.append('-l%s-%s' % (link_dep.name[3:], self.version))
          else:
            # append the output of the target to our stated dependencies
            if not self.assume_shared_libs:
              deps.append(link_dep.filename)

            # link against the library
            libs.append(build_path_join(retreat, link_dep.filename))
        elif isinstance(link_dep, gen_base.ObjectFile):
          # link in the object file
          objects.append(link_dep.filename)
          objdeps.append(_normstr(link_dep.filename))
          for dep in self.graph.get_sources(gen_base.DT_OBJECT, link_dep, gen_base.SourceFile):
            object_srcs.append(
              build_path_join('$(abs_srcdir)', dep.filename))
        elif isinstance(link_dep, gen_base.HeaderFile):
          # link in the header file
          # N.B. that filename_win contains the '_'-escaped class name
          headers.append(link_dep.filename_win)
          header_classes.append(link_dep.classname)
          for dep in self.graph.get_sources(gen_base.DT_OBJECT, link_dep, gen_base.ObjectFile):
            header_class_filenames.append(dep.filename)
        else:
          ### we don't know what this is, so we don't know what to do with it
          raise UnknownDependency

      for nonlib in self.graph.get_sources(gen_base.DT_NONLIB, target_ob.name):
        if isinstance(nonlib, gen_base.TargetLinked):
          if not nonlib.external_lib:
            deps.append(nonlib.filename)

      targ_varname = target.replace('-', '_')
      objnames = build_path_strip(path, objects)

      ezt_target = _eztdata(name=target_ob.name,
                            varname=targ_varname,
                            path=path,
                            install=None,
                            add_deps=add_deps,
                            objects=objects,
                            objdeps=objdeps,
                            deps=deps,
                            when=target_ob.when,
                            )
      data.target.append(ezt_target)

      if hasattr(target_ob, 'link_cmd'):
        ezt_target.link_cmd = target_ob.link_cmd
      if hasattr(target_ob, 'output_dir'):
        ezt_target.output_dir = target_ob.output_dir
      if hasattr(target_ob, 'headers_dir'):
        ezt_target.headers_dir = target_ob.headers_dir

      # Add additional install dependencies if necessary
      if target_ob.add_install_deps:
        ezt_target.install = target_ob.install
        ezt_target.install_deps = target_ob.add_install_deps

      if isinstance(target_ob, gen_base.TargetJava):
        ezt_target.type = 'java'
        ezt_target.headers = headers
        ezt_target.sources = None
        ezt_target.jar = None
        ezt_target.classes = target_ob.classes

        # Build the headers from the header_classes with one 'javah' call
        if headers:
          ezt_target.header_class_filenames = header_class_filenames
          ezt_target.header_classes = header_classes

        # Build the objects from the object_srcs with one 'javac' call
        if object_srcs:
          ezt_target.sources = object_srcs

        # Once the bytecodes have been compiled up, we produce the
        # JAR.
        if target_ob.jar:
          ezt_target.jar_path = build_path_join(target_ob.classes,
                                                target_ob.jar)
          ezt_target.packages = target_ob.packages

      elif isinstance(target_ob, gen_base.TargetI18N):
        ezt_target.type = 'i18n'
      else:
        ezt_target.type = 'n/a'
        ezt_target.filename = target_ob.filename
        ezt_target.path = path
        if (isinstance(target_ob, gen_base.TargetLib)
            and not target_ob.undefined_lib_symbols):
          ezt_target.undefined_flag = '$(LT_NO_UNDEFINED)'
        else:
          ezt_target.undefined_flag = ''
        ezt_target.libs = gen_base.unique(libs)
        ezt_target.objnames = objnames
        ezt_target.basename = build_path_basename(target_ob.filename)

    ########################################

    for itype, i_targets in install_deps:

      # perl bindings do their own thing, "swig-pl" target is
      # already specified in Makefile.in
      if itype == "swig-pl":
        continue

      outputs = [ ]

      for t in i_targets:
        if hasattr(t, 'filename'):
          outputs.append(t.filename)

      data.itargets.append(_eztdata(type=itype, outputs=outputs))

    ########################################

    # for each install group, write a rule to install its outputs
    for area, inst_targets in install_deps:

      # perl bindings do their own thing, "install-swig-pl" target is
      # already specified in Makefile.in
      if area == "swig-pl":
        continue

      # get the output files for these targets, sorted in dependency order
      files = gen_base._sorted_files(self.graph, area)

      ezt_area_type = (area == 'apache-mod' and 'mods-shared' or area)
      ezt_area = _eztdata(type=ezt_area_type, files=[], extra_install=None)

      def file_to_eztdata(file):
          # cd to dirname before install to work around libtool 1.4.2 bug.
          dirname, fname = build_path_splitfile(file.filename)
          return _eztdata(mode=None,
                          dirname=dirname, fullname=file.filename,
                          filename=fname, when=file.when,
                          pc_fullname=None,
                          pc_installdir=None,
                          pc_install_fname=None,)

      def apache_file_to_eztdata(file):
          # cd to dirname before install to work around libtool 1.4.2 bug.
          dirname, fname = build_path_splitfile(file.filename)
          base, ext = os.path.splitext(fname)
          name = base.replace('mod_', '')
          return _eztdata(mode='apache-mod',
                          fullname=file.filename, dirname=dirname,
                          name=name, filename=fname, when=file.when)

      if area != 'test' and area != 'bdb-test':
        data.areas.append(ezt_area)

        area_var = area.replace('-', '_')
        upper_var = area_var.upper()
        ezt_area.varname = area_var
        ezt_area.uppervar = upper_var

        for file in files:
          if isinstance(file.target, gen_base.TargetApacheMod):
            ezt_file = apache_file_to_eztdata(file)
          else:
            ezt_file = file_to_eztdata(file)
            if area == 'locale':
              lang, objext = os.path.splitext(ezt_file.filename)
              installdir = ('$(DESTDIR)$(%sdir)/%s/LC_MESSAGES'
                            % (area_var, lang))
              ezt_file.installdir = installdir
              ezt_file.objext = objext
            else:
              ezt_file.install_fname = build_path_join('$(%sdir)' % area_var,
                                                       ezt_file.filename)

          # Install pkg-config files
          if (isinstance(file.target, gen_base.TargetLib) and
              ezt_file.fullname.startswith('subversion/libsvn_')):
            ezt_file.pc_fullname = ezt_file.fullname.replace('-1.la', '.pc')
            ezt_file.pc_installdir = '$(pkgconfig_dir)'
            pc_install_fname = ezt_file.filename.replace('-1.la', '.pc')
            ezt_file.pc_install_fname = build_path_join(ezt_file.pc_installdir,
                                                        pc_install_fname)
          ezt_area.files.append(ezt_file)

        # certain areas require hooks for extra install rules defined
        # in Makefile.in
        ### we should turn AREA into an object, then test it instead of this
        if area[:5] == 'swig-' and area[-4:] != '-lib' \
           or area[:7] == 'javahl-' \
           or area[:6] == 'svnxx-' \
           or area == 'tools':
          ezt_area.extra_install = 'yes'

    ########################################

    includedir = build_path_join('$(includedir)',
                                 'subversion-%s' % self.version)
    data.includes = [_eztdata(file=file,
                              src=build_path_join('$(abs_srcdir)', file),
                              dst=build_path_join(includedir,
                                                  build_path_basename(file)))
                      for file in self.includes]
    data.includedir = includedir

    ########################################

    for target in install_sources:
      if not isinstance(target, gen_base.TargetScript) and \
         not isinstance(target, gen_base.TargetJava) and \
         not isinstance(target, gen_base.TargetI18N):
        data.isources.append(_eztdata(name=target.name,
                                      filename=target.filename))

    ########################################

    # write dependencies and build rules (when not using suffix rules)
    # for all other generated files which will not be installed
    # (or will be installed, but not by the main generated build)
    obj_deps = sorted(self.graph.get_deps(gen_base.DT_OBJECT),
                      key=lambda t: t[0].filename)

    for objname, sources in obj_deps:
      dep = _eztdata(name=_normstr(objname),
                     when=objname.when,
                     deps=list(map(_normstr, sources)),
                     cmd=objname.compile_cmd,
                     source=_normstr(sources[0]))
      data.deps.append(dep)
      dep.generated = ezt.boolean(getattr(objname, 'source_generated', 0))

    template = ezt.Template(os.path.join('build', 'generator', 'templates',
                                         'build-outputs.mk.ezt'),
                            compress_whitespace=False)
    template.generate(open('build-outputs.mk', 'w'), data)

    self.write_standalone()

    self.write_transform_libtool_scripts(install_sources)

    self.write_pkg_config_dot_in_files(install_sources)

  def write_standalone(self):
    """Write autogen-standalone.mk"""

    standalone = open("autogen-standalone.mk", "w")
    standalone.write('# DO NOT EDIT -- AUTOMATICALLY GENERATED '
                     'BY build/generator/gen_make.py\n')
    standalone.write('# FROM build-outputs.mk\n')
    standalone.write('abs_srcdir = %s\n' % os.getcwd())
    standalone.write('abs_builddir = %s\n' % os.getcwd())
    standalone.write('top_srcdir = .\n')
    standalone.write('top_builddir = .\n')
    standalone.write('SWIG = swig\n')
    standalone.write('SWIG_PY_OPTS = -python -py3 -nofastunpack -modern\n')
    standalone.write('PYTHON = ' + sys.executable + '\n')
    standalone.write('\n')
    standalone.write(open("build-outputs.mk","r").read())
    standalone.close()

  def write_transform_libtool_scripts(self, install_sources):
    """Write build/transform_libtool_scripts.sh"""
    script = 'build/transform_libtool_scripts.sh'
    fd = open(script, 'w')
    fd.write('''#!/bin/sh
# DO NOT EDIT -- AUTOMATICALLY GENERATED BY build/generator/gen_make.py

transform()
{
  SCRIPT="$1"
  LIBS="$2"
  if [ -f $SCRIPT ]; then
    if grep LD_PRELOAD "$SCRIPT" > /dev/null; then
      :
    elif grep LD_LIBRARY_PATH "$SCRIPT" > /dev/null; then
      echo "Transforming $SCRIPT"
      EXISTINGLIBS=""
      for LIB in $LIBS; do
        # exclude libsvn_test since the undefined test_funcs breaks libtool
        case $LIB in
          *libsvn_test-*) continue ;;
        esac
        if [ ! -f $LIB ]; then
          continue
        fi
        if [ -z "$EXISTINGLIBS" ]; then
          EXISTINGLIBS="$LIB"
        else
          EXISTINGLIBS="$EXISTINGLIBS $LIB"
        fi
      done
      if [ ! -z "$EXISTINGLIBS" ]; then
        cat "$SCRIPT" |
        (
          read LINE
          echo "$LINE"
          read LINE
          echo "$LINE"
          read LINE
          echo "$LINE"
          read LINE
          echo "$LINE"
          echo "LD_PRELOAD=\\"$EXISTINGLIBS\\""
          echo "export LD_PRELOAD"
          cat
        ) < "$SCRIPT" > "$SCRIPT.new"
        mv -f "$SCRIPT.new" "$SCRIPT"
        chmod +x "$SCRIPT"
      fi
    fi
  fi
}

DIR=`pwd`

''')
    libdep_cache = {}
    paths = {}
    for lib in ('libsvn_auth_gnome_keyring', 'libsvn_auth_kwallet'):
      paths[lib] = self.sections[lib].options.get('path')
    for target_ob in install_sources:
      if not isinstance(target_ob, gen_base.TargetExe):
        continue
      name = target_ob.name
      libs = self._get_all_lib_deps(target_ob.name, libdep_cache, paths)
      path = paths[name]
      for i in range(0, len(libs)):
        lib = libs[i]
        libpath = paths[libs[i]]
        libs[i] = '$DIR/%s/.libs/%s-%s.so' % (libpath, lib, self.version)
      fd.write('transform %s/%s "%s"\n' % (path, name, " ".join(libs)))
    fd.close()
    mode = stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH
    os.chmod(script, mode)

  def _get_all_lib_deps(self, target_name, libdep_cache, paths):
    if not target_name in libdep_cache:
      libs = set()
      path = None
      if target_name in self.sections:
        section = self.sections[target_name]
        opt_libs = self.sections[target_name].options.get('libs')
        paths[target_name] = section.options.get('path')
        if opt_libs:
          for lib_name in opt_libs.split():
            if lib_name.startswith('libsvn_'):
              libs.add(lib_name)
            for lib in self._get_all_lib_deps(lib_name, libdep_cache, paths):
              libs.add(lib)
      if target_name == 'libsvn_subr':
        libs.update(('libsvn_auth_gnome_keyring', 'libsvn_auth_kwallet'))
      libdep_cache[target_name] = sorted(libs)
    return libdep_cache[target_name]

  def write_pkg_config_dot_in_files(self, install_sources):
    """Write pkg-config .pc.in files for Subversion libraries."""
    for target_ob in install_sources:
      if not (isinstance(target_ob, gen_base.TargetLib) and
              target_ob.path.startswith('subversion/libsvn_')):
        continue

      lib_name = target_ob.name
      lib_path = self.sections[lib_name].options.get('path')
      lib_deps = self.sections[lib_name].options.get('libs')
      lib_desc = self.sections[lib_name].options.get('description')
      output_path = build_path_join(lib_path, lib_name + '.pc.in')
      template = ezt.Template(os.path.join('build', 'generator', 'templates',
                                           'pkg-config.in.ezt'),
                              compress_whitespace=False)
      class _eztdata(object):
        def __init__(self, **kw):
          vars(self).update(kw)

      data = _eztdata(
        lib_name=lib_name,
        lib_desc=lib_desc,
        lib_deps=[],
        lib_required=[],
        lib_required_private=[],
        version=self.version,
        )
      # libsvn_foo -> -lsvn_foo-1
      data.lib_deps.append('-l%s-%s' % (lib_name.replace('lib', '', 1), data.version))
      for lib_dep in lib_deps.split():
        if lib_dep == 'apriconv':
          # apriconv is part of apr-util, skip it
          continue
        external_lib = self.sections[lib_dep].options.get('external-lib')
        if external_lib:
          ### Some of Subversion's internal libraries can appear as external
          ### libs to handle conditional compilation. Skip these for now.
          if external_lib in ['$(SVN_RA_LIB_LINK)', '$(SVN_FS_LIB_LINK)']:
            continue
          # If the external library is known to support pkg-config,
          # add it to the Required: or Required.private: section.
          # Otherwise, add the external library to linker flags.
          pkg_config = self.sections[lib_dep].options.get('pkg-config')
          if pkg_config:
            private = self.sections[lib_dep].options.get('pkg-config-private')
            if private:
              data.lib_required_private.append(pkg_config)
            else:
              data.lib_required.append(pkg_config)
          else:
            # $(EXTERNAL_LIB) -> @EXTERNAL_LIB@
            data.lib_deps.append('@%s@' % external_lib[2:-1])
        else:
          data.lib_required_private.append(lib_dep)

      template.generate(open(output_path, 'w'), data)

class UnknownDependency(Exception):
  "We don't know how to deal with the dependent to link it in."
  pass

### End of file.
