package nxm.sys.lib;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import tools.BuildProps;

/** This class will compile NeXtMidas Java, C and Fortran files. Typically the
    {@link nxm.sys.intr.Build} class provides the front-end for this class.

    @author  Matthew Harrell (Initial version)
    @author  J. Czechowski IV (New and Improved version) / Neon Ngo
*/
public class Build {

  // primary location for "area" constants moved here in 3.9.2 to support transition towards JDK modules
  /** The name of the "area" macros are located in. <br><br>Value: {@value } */
  public static final String MACRO_AREA="mcr";

  /** The name of the "area" intrinsics are located in. <br><br>Value: {@value} */
  public static final String INTR_AREA="intr";

  /** The name of the "area" primitives are located in. <br><br>Value: {@value} */
  public static final String PRIM_AREA="prim";

  /** The name of the "area" X-Midas host primitives are located in. <br><br>Value: {@value} */
  public static final String HOST_AREA="host";

  /** The name of the "area" where tests are located in. <br><br>Value: {@value} */
  public static final String TEST_AREA="test";

  /** The name of the "area" where C/C++ libraries are located in. <br><br>Value: {@value} */
  public static final String LIBC_AREA="libc";

  /** The name of the "area" where Fortran libraries are located in. <br><br>Value: {@value} */
  public static final String LIBF_AREA="libf";

  /** The name of the "area" where math libraries are located in. <br><br>Value: {@value} */
  public static final String LIBM_AREA="libm";

   /** Types of source files to compile list. Since 3.3.0. */
  public static final String BUILD_TYPE_LIST = "Java,Native,Host";

  /** Bit mask that indicates that (Java) class files should be built (this includes any JUnit files). */
  public static final int BUILD_CLASS_FILES = 0x1;

  /** Bit mask that indicates that native files should be built. */
  public static final int BUILD_NATIVE_FILES = 0x2;

  /** Bit mask that indicates that host files should be built. */
  public static final int BUILD_HOST_FILES = 0x4;

  /** FYI: zero-length arrays are immutable. For more info see Effective Java,
      Item 27: Return zero-length arrays, not null. */
  private static final File[]   EMPTY_FILE_ARRAY   = new File[0];
  private static final String[] EMPTY_STRING_ARRAY = new String[0];

  private static final boolean  INTERNAL_JAVAH   = false; // Enable calling of javah internally.
  private static final boolean  INTERNAL_JAVADOC = false; // Enable calling of javadoc internally.

  private static Midas      defaultMidas     = null;  // The Midas context.
  private static boolean    defaultShowDebug = false; // Indicates if debugging info should be shown.
  private static boolean    defaultKeepFiles = true;  // Indicates if header files should be kept.

  private static final char SEMICOLON = ';';

  /** one or more white spaces (space,tab,return,newline) regular expression (compiled) */
  private static final Pattern WHITESPACE_REGEX = Pattern.compile("\\s+");
  /** comma (',') regular expression (compiled) */
  private static final Pattern COMMA_REGEX = StringUtil.COMMA_REGEX;
   /** semicolon (';') regular expression (compiled) */
  private static final Pattern SEMICOLON_REGEX = Pattern.compile(";");
  /** comma (',') or pipe (i.e. vertical bar, '|') regular expression (compiled) */
  private static final Pattern COMMA_OR_PIPE_REGEX = Pattern.compile("[,|]");

  // The following are valid options for initFortranParams(). These arrays must be sorted.
  // GFC support added in 3.9.3 NXM-3531
  // IFX support added in 4.1.4 NXM-4073
  private static final String[] compOptionsXBC = { "gfc", "ifc", "ifort", "ifx",        "pgf77" };
  private static final String[] compOptionsNXM = { "gfc", "ifc", "ifort", "ifx", "pfc", "pgf77" };
  private static final String[] archOptions    = { "32BIT", "64BIT", "CLASSIC" };

  private static final String defaultForCompiler = "ifort";
  private static String defaultGfcForCompiler = null;
  private static String gfcForCompiler = null;
  private static String gfcLibsDir = null;
  private static String  forCompiler;  // The Fortran compiler to use with XBC.
  private static String  xmArchType;   // The architecture type (32BIT,64BIT,CLASSIC) to use with XBC.
  /** list of key suffixes (should include dot) to look for property values to prepend to final return property */
  private static String[] prependSuffixes;
  /** list of key suffixes (should include dot) to look for property values to append to final return property */
  private static String[] appendSuffixes;

  private Midas   midas;        // The Midas context.
  private Shell   shell;        // The current shell.
  private boolean verbose;      // Be verbose while compiling.
  private boolean showDebug;    // Indicates if debugging info should be shown.
  private boolean keepFiles;    // Indicates if intermediate files should be kept.
  private boolean forPreProc;   // Run xfpp to preprocess Fortran files.
  private Vector<File>  classFiles;   // Java class files to build (NORMAL).
  private Vector<File>  junitFiles;   // Java class files to build (JUNIT).
  private Vector<File>  nativeFiles;  // Native JNI files to build.
  private Vector<File>  cpphostFiles; // XBC C/C++ files to build (primitives in host area).
  private Vector<File>  hostFiles;    // XBC Fortran files to build.
  private Vector<File>  clibFiles;    // Native files to link, explicitly declared.
  private Vector<File>  xlibFiles;    // Native files to link, implicitly declared.
  private Vector<File>  cleanFiles;   // Files to clean up at the end (set in compile()).
  private Table   bldFlags;     // Build flags to use.
  private int     compileCount; // Number of files compiled (<0 on error).
  private Map<String,Object>             primConfig;   // Primitives configuration from CFG file (primitives.cfg)
  private Map<String,Map<String,Object>> libConfigMap; // Libraries configuration Map of parsed CFG files (libraries.cfg)
  private HashSet<String> platform; // current platform (e.g. ["UNIX", "LINX", "Linux"], "NEXTMIDAS")
                                    // used for conditional directives in CFG files
  private JavaCompiler compiler = null; // invoke compiler internally

  /** File extensions for Java source files, (value=<code>{@value}</code>). */
  public static final String JAVA_SOURCE = ".java";
  /** File extensions for C source files, (value=<code>{@value}</code>). */
  public static final String C_SOURCE    = ".c";
  /** File extensions for C++ source files, (value=<code>{@value}</code>). */
  public static final String CPP_SOURCE  = ".cc";
  /** File extensions for Fortran source files, (value=<code>{@value}</code>). */
  public static final String FOR_SOURCE  = ".for";
  /** File extensions for NeXtMidas flib files, (value=<code>{@value}</code>). */
  public static final String FLIB_SOURCE = ".flib";
  /** File extensions for NeXtMidas clib files, (value=<code>{@value}</code>). */
  public static final String CLIB_SOURCE = ".clib";
  /** File extensions for NeXtMidas cpplib files, (value=<code>{@value}</code>). */
  public static final String CPPLIB_SOURCE = ".cpplib";
  /** File extensions for all source files, (value=<code>{@value}</code>). */
  public static final String ALL_SOURCE = JAVA_SOURCE+"|"+C_SOURCE+"|"+CPP_SOURCE+"|"
                                        + FOR_SOURCE+"|"+FLIB_SOURCE+"|"+CLIB_SOURCE+"|"+CPPLIB_SOURCE;
  /** File extension used for Fortran files after being run through <code>xfpp</code>,
      (value=<code>{@value}</code>). This intentionally differs from X-Midas so that
      X-Midas pre-processed files can coexist with those handled by NeXtMidas. Note that,
      by default, these files are deleted once the Fortran compiler has been run.
   */
  public static final String FPP_EXTENSION = ".fpp";

  /** File extension used for intermediate object files (value=<code>{@value}</code>). */
  public static final String OBJ_EXTENSION = ".o";

  private static final String[] LIB_EXTENSIONS = { ".so", ".o", ".a" }; // File extensions for libraries
  private static final String[] LIB_NAME_LIST  = { "lib<name>", "libAll", "libAll$<inner>", "lib<outer>$All" };  // Library names
  private static final String[] LIB_LIST_EXTS  = { CLIB_SOURCE, CPPLIB_SOURCE, FLIB_SOURCE };

  /** Filter that accepts all Java source files matching {@link #JAVA_SOURCE}. */
  public  static final BuildFileFilter JAVA_SOURCE_FILTER = new BuildFileFilter(JAVA_SOURCE);
  /** Filter that accepts all C source files matching {@link #C_SOURCE}. */
  public  static final BuildFileFilter C_SOURCE_FILTER    = new BuildFileFilter(C_SOURCE   );
  /** Filter that accepts all C++ source files matching {@link #CPP_SOURCE}. */
  public  static final BuildFileFilter CPP_SOURCE_FILTER  = new BuildFileFilter(CPP_SOURCE );
  /** Filter that accepts all Fortran source files matching {@link #FOR_SOURCE}. */
  public  static final BuildFileFilter FOR_SOURCE_FILTER  = new BuildFileFilter(FOR_SOURCE );
  /** Filter that accepts all NeXtMidas clib files matching {@link #CLIB_SOURCE}. */
  public  static final BuildFileFilter CLIB_SOURCE_FILTER = new BuildFileFilter(CLIB_SOURCE);
  /** Filter that accepts all NeXtMidas cpplib files matching {@link #CPPLIB_SOURCE}. */
  public  static final BuildFileFilter CPPLIB_SOURCE_FILTER = new BuildFileFilter(CPPLIB_SOURCE);
  /** Filter that accepts all NeXtMidas flib files matching {@link #FLIB_SOURCE}. */
  public  static final BuildFileFilter FLIB_SOURCE_FILTER = new BuildFileFilter(FLIB_SOURCE);
  /** Filter that accepts all source files matching {@link #ALL_SOURCE}. */
  public  static final BuildFileFilter ALL_SOURCE_FILTER  = new BuildFileFilter(ALL_SOURCE );

  private static final int DETECT_WARN_OR_ERROR = Midas.WARNING * 10 + Midas.ERROR;

  /** Status code associated with a successful build - matches shell standards */
  @InternalUseOnly
  public static final int BUILD_SUCCESSFUL = 0;
  /** Status code associated with a failed build - matches shell standards */
  @InternalUseOnly
  public static final int BUILD_FAILURE = 1;

  static { // static initializer since 2.9.2
    // 2011-05-24 below append and prepend suffixes were added to address BUG 2377
    String aArchSuffix = ".append."  + Shell.getDefArchType() + "bit";  // e.g. ".append.32bit" or ".append.64bit"
    String pArchSuffix = ".prepend." + Shell.getDefArchType() + "bit";  // e.g. ".prepend.32bit" or ".prepend.64bit"
    appendSuffixes    = new String[] { ".append", aArchSuffix };
    prependSuffixes   = new String[] { pArchSuffix, ".prepend" };
  }

  /** <b>INTERNAL USE ONLY:</b> Creates a new instance. */
  private Build (Midas midas, boolean verbose, boolean showDebug, boolean keepFiles,
                String forCompiler, boolean forPreProc, Table bldFlags) {
    this.midas       = midas;
    this.shell       = Shell.getCurrent();
    this.verbose     = verbose;
    this.showDebug   = showDebug;
    this.keepFiles   = keepFiles;
    Build.forCompiler= forCompiler;
    this.forPreProc  = forPreProc;
    this.classFiles  = new Vector<>();
    this.junitFiles  = new Vector<>();
    this.nativeFiles = new Vector<>();
    this.cpphostFiles= new Vector<>();
    this.hostFiles   = new Vector<>();
    this.clibFiles   = new Vector<>();
    this.xlibFiles   = new Vector<>();
    this.cleanFiles  = new Vector<>();
    this.libConfigMap = new HashMap<>();
    this.platform     = new HashSet<>();
    this.setBldFlags(bldFlags);
    this.reset();
    if (midas != null) midas.resetBuildStatus();
// NTN: 2011-05-19: BuildProps was updated to re-load build.props files that have been modified
//      after it was last loaded under BUG 2377, so we should NOT have to call clearCache() anymore
//  BuildProps.clearCache(); // Bug 1608 - clear cache of build.props for all option trees

    if (System.getProperty("nxm.sys.lib.Shell.ostag") == null) { // only need to set it once
      // the property below is used indirectly by BUILD (via reference to it in build.props's cflags.DOS and cflags.UNIX)
      System.setProperty("nxm.sys.lib.Shell.ostag", Shell.ostag); // since 2.9.3 for BUG 2377
    }
  }

  /** Resets the files to be built. */
  private void reset() {
    classFiles.clear();
    junitFiles.clear();
    nativeFiles.clear();
    cpphostFiles.clear();
    hostFiles.clear();
    clibFiles.clear();
    xlibFiles.clear();
    cleanFiles.clear();
    libConfigMap.clear();
    platform.clear();
  }

  /** Creates a new instance.
      @param midas     The Midas context to use.
      @param verbose   Show verbose output while compiling.
      @param showDebug Show debug info while compiling.
      @param keepFiles Keep intermediate files?
      @param bldFlags  Table of build flags.

      @return a new instance.
   */
  public static Build newInstance (Midas midas, boolean verbose, boolean showDebug,
                                  boolean keepFiles, Table bldFlags) {
    // JC4: This is static so that we can allow this class to be extended in the future.
    //      In which case this method may return a subclass of Build.
    boolean forPreProc  = true;
    String  forCompiler = null;

    return new Build(midas,verbose,showDebug,keepFiles,forCompiler,forPreProc,bldFlags);
  }

  /** Sets the build flags to use. */
  private void setBldFlags (Table flags) {
    bldFlags = (flags != null)? flags: new Table();
  }

  /** Compile a collection of Java files.
      @param files  The files to compile.
      @param junit  Should the files use the JUNIT flags or the NORMAL ones?
      @return The number of files compiled.
   */
  private int compileClassFiles (List<File> files, boolean junit){
    if (files == null) files = Collections.emptyList();
    if (files.isEmpty()) return 0;
    String flagsName = (junit)? "junit.flags": "java.flags";
    String opt     = getOpt(files);
    String options = getProperty(opt,flagsName);
    int numFiles   = 0;

    ArrayList<String> args = new ArrayList<>();
    args.addAll(Arrays.asList(WHITESPACE_REGEX.split(options)));/* Split on whitespace. */

    for (File file : files) {
      if (file.exists()) {
        if (verbose) {
          info("Compiling " + file.getAbsolutePath());
        }
        args.add(file.getAbsolutePath());
        numFiles++;
      } else {
        warn("File not found " + file);
      }
    } // end for loop

    if (numFiles > 0) {
      String   option    = getOpt(files.get(0).getParent());
      String   classpath = getClassPath(option);
      args.add(0, "-classpath");
      args.add(1, classpath);
      String[] argArray  = args.toArray(new String[args.size()]);
      boolean doInternalCompile = StringUtil.isFalse(BuildProps.getSysProp("compat.ext.javac"));

      if (doInternalCompile) {
        if (internalCompile(argArray) != 0) {
          numFiles = 0;
        }
      }
      else {
        String javac   = "java." + Shell.ostype + ".javac";
        String command = BuildProps.getSysPath(javac) + " " + StringUtil.join(argArray, " ");

        // JC4: Need to run internally (even on windows) since the length of
        //      the command string would exceed windows maximum line length.
        if (!runInternalOS(command)) { numFiles = -1; }
      }
    }
    return numFiles;
  }

  /** Compile/Create Javadocs.
      @param opt The name of the properties file (defining compilation options) to read.
      @param dir The directory to place the documentation files within.
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
   */
  public static boolean compileDocs (String opt, String dir) {
    return compileDocs(opt, dir, defaultShowDebug);
  }

  /** Compile/Create Javadocs.
      @param opt The name of the properties file (defining compilation options) to read.
      @param dir The directory to place the documentation files within.
      @param debug show debug messages
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
     @since NeXtMidas 3.5.0
   */
  public static boolean compileDocs (String opt, String dir, boolean debug) {
    Midas midas = defaultMidas;
    // load the sys and option tree build parameters
    opt = opt.toLowerCase();

    String  java_cp      = getClassPath(opt);
    String  javadoc      = getPropertyStatic(opt, "java."+Shell.ostype+".javadoc");
    String  docdir       = getPropertyStatic(opt, "javadoc."+Shell.ostype+".dir");
    String  javadocFlags = getPropertyStatic(opt, "javadoc.flags" );
    String  packages     = "";

    /* Include the tools as part of SYS */
    //Do not include tools if the java version starts with a 9 or greater. Any java that starts with 1.x is good.
    // old way java versioning = 1.6, 1.7 ,1.8 - New way 9.0, 10.0, 11.0
    // Since 3.8.1
    if (opt.equals("sys") && Integer.parseInt(Shell.getJavaVersion().split("[.]")[0])<9) {
      if (!Installation.ApplyBugFix.WORKAROUND_JDK8_JAVADOC_ISSUE) {
        packages += " " + Shell.getNmRoot() + "tools" + File.separator + "NmClassLoader.java";
      }
      packages += " tools";
    }

    if (dir!=null && dir.length()>0) docdir = dir;
    String   path = Shell.getOptPath(opt,null);
    String[] dirs = midas.io.listFiles(path, "*", "dir");

    if (dirs == null) {
      midas.warning("Can not compile docs for option "+opt);
      return false;
    } else {
      for (String area : dirs) {
        String apath = Shell.getOptPath(opt,area);
        String[] files = midas.io.listFiles(apath, "*", "java");
        if (files!=null && files.length>0) packages += " nxm."+opt+"."+area;
      }

      /* The arguments for JavaDoc. */
      String command = " -classpath " + java_cp + " " + javadocFlags
                     + " -d " + docdir + FileName.LOCAL_PATH_DELIM + opt
                     + " " + packages;

      /* If the JavaDocs for SYS have been built, make the links to them. */
      File    sysLink = new File(docdir + FileName.LOCAL_PATH_DELIM + "sys"
                               + FileName.LOCAL_PATH_DELIM + "package-list");
      if (!opt.equals("sys") && sysLink.exists()) {
        command = " -link " + sysLink.getParent()
                + command;
      }

      /* Add the overview page (overview.html) if present. */
      final File overview = new File(path + "overview.html");

      if (overview.exists()) {
        command = " -overview " + overview.getAbsolutePath()
                + command;
      }

      /* Run JavaDoc. */
      boolean success = true;
      try {
        if (packages.length() > 0) { /* Skip build if no Java packages/files. */
          if (INTERNAL_JAVADOC && StringUtil.isFalse(BuildProps.getSysProp("compat.ext.javadoc"))) {
            String doclet = "com.sun.tools.doclets.standard.Standard";
            String[] args = WHITESPACE_REGEX.split(command.trim()); /* Split on whitespace. */
            internalJavadoc(midas, doclet, args);
          } else {
            runInternalOS(midas, javadoc + " " + command, debug);
          }
        }

        /* Create the two html pages that link all of the javadoc pages
           together for each of the separate option trees.
         */
        String indexFileName = docdir+FileName.LOCAL_PATH_DELIM+"index.html";
        String optFileName   = docdir+FileName.LOCAL_PATH_DELIM+"opt.html";

        String[] options = Shell.getKnownOptionTrees();
        PrintStream indexFile = new PrintStream(new FileOutputStream(indexFileName));
        PrintStream optFile   = new PrintStream(new FileOutputStream(optFileName));

        if (options == null) {
          options = new String[] { "SYS" };
        }

        indexFile.println(
            "<html>\n"
          + "  <head>\n"
          + "    <!-- This file has been auto-generated by\n"
          + "         nxm.sys.lib.Build. DO NOT EDIT! -->\n"
          + "\n"
          + "    <title>NeXtMidas Code Documentation</title>\n"
          + "  </head>\n"
          + "  <FRAMESET rows=\"5%,95%\">\n"
          + "    <FRAME src=\"opt.html\"\n"
          + "           name=\"optListFrame\"\n"
          + "           title=\"All Option Trees\">\n"
          + "    <FRAME src=\"sys/index.html\"\n"
          + "           name=\"javadocFrame\"\n"
          + "           title=\"Generated Documentation\">\n"
          + "    <NOFRAMES>\n"
          + "      This document is designed to be viewed using frames. If\n"
          + "      you can see this message, your browser does not have frame\n"
          + "      support turned on.<br>\n"
          + "      <br>\n"
          + "      <a href=\"opt.html\">Non-Frame Version</a>\n"
          + "    </NOFRAMES>\n"
          + "  </FRAMESET>"
          + "</html>");
        indexFile.close();

        optFile.println(
            "<html>\n"
          + "  <head>\n"
          + "    <!-- This file has been auto-generated by\n"
          + "         nxm.sys.lib.Build. DO NOT EDIT! -->\n"
          + "\n"
          + "    <title>NeXtMidas Option Trees</title>\n"
          + "    <meta NAME=\"keywords\" CONTENT=\"Options\">\n"
          + "    <link REL=\"stylesheet\" TYPE=\"text/css\""
          + "          HREF=\"sys/stylesheet.css\" TITLE=\"Style\">\n"
          + "  </head>\n"
          + "  <body bgcolor=\"white\">\n"
          + "    <div style=\"text-align:center;\">\n"
          + "      <font CLASS=\"FrameItemFont\">Option Trees: ");

        for (String option : options) {
          optFile.println(
              "      &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; "
            + "<a href=\"" + option.toLowerCase() + "/index.html\" "
            + "target=\"javadocFrame\">" + option.toUpperCase() + "</a>\n");
        }

        optFile.println(
            "      </font>\n"
          + "    </div>\n"
          + "  </body>\n"
          + "</html>\n");

        optFile.close();

      } catch (IOException exception) {
        midas.warning("Build is unable to create index file for javadoc pages.");
        success = false;
      }

      return success;
    }
  }

  /** Create a JNI header file.
      @param path The path the header file should be placed in.
      @param name The name of the Java class to make the header from.
      @return The name of the header file.
   */
  private String makeNativeHeader (String path, String name) {
    final String area   = getArea(path);
    final String opt    = getOpt(path);
    final String javaCP = getClassPath(opt);
    final String javah  = getProperty(opt, "java."+Shell.ostype+".javah");
    final String javac = getProperty(opt, "java."+Shell.ostype+".javac");
    String clazz   = "nxm." + opt + "." + area + "." + name; // JC4: Change this to support libs.
    String header  = path + "nxm_" + opt + "_" + area + "_" + name + ".h";
    String command = javah + " -classpath " + javaCP + " -jni -d " + path + " " + clazz;

    //This will change the command for java versions 9+. javah is deprecated and has switched to javac -h.
    //The -h replaces the -d in javah.
    if(Integer.parseInt(Shell.getJavaVersion().split("[.]")[0])>=9){
      // The file needs to be pointed to directly. Need the filepath defined right.
      // Note from 3.9.3 - classpath does not seem to matter when compiling headers for the class, get the full path to the class.
      clazz = "nxm/" + opt + "/" + area + "/" + name + ".java";
      command = javac + " -classpath " + javaCP + " -h " + path + " " + Shell.getNmRoot() + File.separator + clazz;
    }

    if (INTERNAL_JAVAH) {
      Shell.warning("Build: CAN NOT CALL JAVAH INTERNALLY AT THIS TIME");
    } else {
      runAutoOS (command);
    }
    return header;
  }

  /** Compile a collection of native files.
      @param files    The files to compile.
      @return The number of files compiled, returns -1 on error.
   */
  private int compileNativeFiles (File[] files) {
    int num_files = 0;
    boolean hasError = false;

    for (File file : files) {
      File javaFile = getJavaFile(file);

      if (!file.exists()) {
        warn("File not found " + file);
      }
      else {
        String filename = file.getName();
        boolean isCppHostmainroutine = filename.equals("hostmainroutine.cc");
        if ((javaFile != null) || isCppHostmainroutine) {
          try {
            int     index = filename.indexOf(".");
            String  name  = filename.substring(0,index);
            String  ext   = filename.substring(index);
            String  path  = FileName.terminatePath(file.getParent());
            boolean cpp   = CPP_SOURCE_FILTER.accept(file);

            if (verbose) { info("Compiling " + file); }

            if (!compileNativeFile(path,name,ext,null)) {
              warn("Unable to compile native code for " + file);
              hasError = true; // NTN: since 2.7.2 mark error to return -1 at end, but continue compiles
              continue; // for loop
            }
            num_files++;

            // NTN: do not link C++ hostmainroutine.cc object file into library (e.g. hostmainroutine_lin.so).
            // hostmainroutine.o is use to link with C++ primitives directly, since if linked as a
            // shared library, it will get cached and always load the first C++ primitive into memory
            // and all other C++ primitives in a macro or current interactive shell will in effect run
            // the first primitive (which is NOT what we want)
            if (isCppHostmainroutine) continue;

            String liblist = getLibList(path,name); // search for optional libraries

            // link the native files into a library
            if (!linkNativeLib(path,name,liblist,false,cpp)) {
              warn("Unable to link native code for " + file);
              hasError = true; // NTN: since 2.7.2 mark error to return -1 at end, but continue compiles
              continue; // for loop
            }

            // add to cleanFiles list to remove files that are no longer needed (at end of build)
            cleanFiles.add(new File(path + name + OBJ_EXTENSION));

            // check for inner classes with native code
            String[] icfiles = Shell.getSharedMidasContext().io.listFiles(path, name+"$*", "class");
            if (icfiles!=null) for (int ic=0; ic<icfiles.length; ic++) {
              String fName = icfiles[ic];
              int i1 = fName.indexOf('$');
              int i2 = fName.lastIndexOf('.');
              String iclass = fName.substring(i1+1,i2);
              String icName = name+"$"+iclass;
              String opt = getOpt(path);
              String area = getArea(path);
              try { // make sure this class is attempting to use Native code
                Class<?> clazz = Class.forName("nxm."+opt+"."+area+"."+icName);
                clazz.getField("useNative");
                if (verbose) info("Compiling " + file + " for "+clazz);
              } catch(Exception e) { continue; }
              String ofile = path+icName+OBJ_EXTENSION;
              if (!compileNativeFile(path,name,ext,ofile,"-DINNERCLASS_"+iclass))
                warn("Unable to compile native code for " + icName);
              liblist = getLibList(path,icName); // search for optional libraries
              if (!linkNativeLib(path,icName,liblist,false,cpp))
                warn("Unable to link native code for " + icName);
              cleanFiles.add(new File(ofile));
            }

          } catch(Exception e) {
            midas.printStackTrace("Unable to compile " + file, e);
            return -1;
          }
        }
      }
    }
    if (hasError) return -1;
    return num_files;
  }

  /** Compile a collection of native files.
      @param files    The files to compile.
      @return The number of files compiled, returns -1 on error.
   */
  private int compileNativeHeaders (File[] files) {
    for (File file : files) {
      File javafile = getJavaFile(file);

      if (!file.exists()) {
        warn("File not found " + file);
      }
      else if (javafile != null) {
        try {
          int     index = file.getName().indexOf(".");
          String  name  = file.getName().substring(0,index);
          String  path  = FileName.terminatePath(file.getParent());

          makeNativeHeader(path, name);
        } catch(Exception e) {
          midas.printStackTrace("Unable to build JNI header for " + file, e);
          return -1;
        }
      }
    } // end for loop
    return 0;
  }

  /** Gets a java file corresponding to a given .c/.cc file. Returns null if not found. */
  private File getJavaFile (File file) {
    File java;

    if (file.exists() && (C_SOURCE_FILTER.accept(file) || CPP_SOURCE_FILTER.accept(file))) {
      int     index = file.getName().indexOf(".");
      String  name  = file.getName().substring(0,index);

      java  = new File(file.getParent(), name + ".java");
      if (java.exists()) return java;

      java  = new File(file.getParent(), name + ".class");
      if (java.exists()) return java;
    }
    return null;
  }

  /** Compile a collection of native files.
      @param files    The files to compile.
      @return The number of files compiled.
   */
  private int copyPrecompiledFiles (File[] files) {
    int count = 0;
    for (File file : files) {
      boolean valid = C_SOURCE_FILTER.accept(file) || CPP_SOURCE_FILTER.accept(file);

      if (!file.exists()) {
        warn("File not found " + file);
      }
      else if (valid && (getJavaFile(file) != null)) {
        int     index = file.getName().indexOf(".");
        String  name  = file.getName().substring(0,index);
        String  path  = FileName.terminatePath(file.getParent());

        String dllDir = Shell.getNmRoot() + "os\\dos\\dlls\\";
        String in     = dllDir + name + Shell.getSharedObjExt();
        String out    = path   + name + Shell.getSharedObjExt();

        try {
          FileUtil.copyBaseFile(midas, in, out);
        } catch (Exception e) {
          midas.printStackTrace("Could not copy pre-compiled DLL "+in+" to "+out+": "+e.getMessage(), e);
        }
      }
    }
    return count;
  }

  /** Compile native (C or C++) file to an object file (.o)
      @param path  Path to the library
      @param name  The name of the native file.
      @param ext     The file name extension (usually ".c").
      @param outFile output object file name
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
      @since NeXtMidas 2.7.2
  */
  private boolean compileNativeFile (String path, String name, String ext, String outFile) {
    String opt     = getOpt(path);
    String area    = getArea(path);
    String inFile  = path + name + ext;
    boolean cpp    = CPP_SOURCE_FILTER.accept(inFile);
    if (outFile==null) outFile = path + name + OBJ_EXTENSION;
    String command = getCCompilerFlags(opt,cpp) + " " // compiler (from OS ENV or build.props) + flags/options from build.props
                   + getFlagsFromConfig(opt, area, name, "cflags") + " " // compile flags from CFG file (primitives.cfg/libraries.cfg)
                   + getCCompilerFlagsFromOSEnv(cpp)  // compile flags from OS ENV (if any) since 2.9.2
                   + " -o " + outFile + " " + inFile;

    return runAutoOS(command);
  }

  /** Compile native (C or C++) file to an object file (.o)
      @param path  Path to the library
      @param name  The name of the native file.
      @param ext     The file name extension (usually ".c").
      @param outFile output object file name
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
      @since NeXtMidas 2.7.2
  */
  private boolean compileNativeFile (String path, String name, String ext, String outFile, String cflags) {
    String opt     = getOpt(path);
    String area    = getArea(path);
    String inFile  = path + name + ext;
    boolean cpp    = CPP_SOURCE_FILTER.accept(inFile);
    if (outFile==null) outFile = path + name + OBJ_EXTENSION;
    String command = getCCompilerFlags(opt,cpp) + " " // compiler (from OS ENV or build.props) + flags/options from build.props
                   + getFlagsFromConfig(opt, area, name, "cflags") + " " // compile flags from CFG file (primitives.cfg/libraries.cfg)
                   + getCCompilerFlagsFromOSEnv(cpp) + " " // compile flags from OS ENV (if any) since 2.9.2
                   + cflags + " -o " + outFile + " " + inFile;

    return runAutoOS(command);
  }

  /** Link native library.
      @param path    Path to the library (must end with a path separator).
      @param name    The name of the library.
      @param liblist List of other library files to link in.
      @param independent Indicates that the library has no "dependent" file (if
                         this is <code>false</code> it is assumed that an object
                         file with the same name as the library is to be added).
      @param cpp     Indicates that a C++ compiler should be used.
      @return <code>true</code> if the linking is successful, <code>false</code> otherwise.
  */
  private boolean linkNativeLib (String path, String name, String liblist,
                                 boolean independent, boolean cpp) {
    String opt      = getOpt(path);
    String area     = getArea(path);
    StringBuilder cFiles = new StringBuilder();
    String libName  = path + name + Shell.getSharedObjExt();

    if (!independent) {
      cFiles.append(path + name + OBJ_EXTENSION + " ");
    }

    if (cpp && area.equals("host")) { // for C++ host primitives, link against libHPrimitive and hostmainroutine.o
      String xbclibpath = Shell.getOptPath("xbc","lib");
      String hprim        = xbclibpath + "libHPrimitive" + Shell.getSharedObjExt();
      // NTN: do not link C++ hostmainroutine.cc as a shared object, otherwise system will
      // cache and always load the first C++ primitive into memory and all other (unique) C++ primitives
      // executed in a macro or current interactive shell will in effect run the first primitive
      // (which is NOT what we want)
      String hmainroutine = xbclibpath + "hostmainroutine" + OBJ_EXTENSION;
      cFiles.append(hmainroutine + " " + hprim + " ");
    }

    cFiles.append(liblist);

    // since 3.5.0: moved linker flags to end for BUG 2566
    String command = getCCompiler(opt,cpp)
                   + " -o " + libName
                   + " " + cFiles
                   + " " + getCLinkFlags(opt,cpp)
                   + " " + getFlagsFromConfig(opt, area, name, "lflags");  // link flags from CFG file (if any)

    return runAutoOS(command);
  }

  /** Compile native Fortran library.
      @param path Path to the library
      @param name The name of the library.
      @param ext  The file name extension (usually ".for").
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
  */
  private boolean compileFortran (String path, String name, String ext) {
    String  area     = getArea(path);
    String  opt      = getOpt(path);
    String  inName   = path + name + ext;
    String  fppName  = null;
    String  outName  = path + name + OBJ_EXTENSION;
    boolean status   = true;

    // Compiler with flags
    String compFlags = getFortranCompiler(opt) + " "
                     + getFlagsFromConfig(opt, area, name, "cflags") + " " // Compiler flags from CFG file
                     + getFortranFlags(opt,area,path);

    if (forPreProc) {
      if (preprocessFortran(path, name, ext, FPP_EXTENSION, compFlags)) {
        fppName = path + name + FPP_EXTENSION;
        inName  = fppName;
      } else {
        warn("Error while trying to preprocess "+inName);
        status = false;
      }
    }

    if (status) {
      status = runAutoOS(compFlags+" "+getFortranHostFlags(opt,area)+" -o "+outName+" "+inName);

      if ((fppName != null) && status) {
        cleanFiles.add(new File(fppName));
      }
    }

    return status;
  }

  /** <b>INTERNAL USE ONLY</b>.
      @deprecated This should never be called directly, it is used internally by the XBC option tree.
                  This method is subject to change or removal without notice. Do not use!!!

      @param path     Path to the library
      @param nameList The name of the libraries.
      @param ext      The file name extension of the source file (usually ".for").
      @param newExt   The file name extension of the pre-processed file (usually ".fpp").
      @param ref      Should always be a {@link Macro} from the XBC option tree.
      @param verbose  True to enable verbose output, false to disable.
      @param debug    True to enable debug output, false to disable.
      @param bldFlags The build flags being used.
      @return true if completed with no errors, false otherwise.
   */
  @InternalUseOnly
  @Deprecated public static boolean preprocessFortran (String path, String nameList, String ext, String newExt,
                                          Object ref, boolean verbose, boolean debug, Table bldFlags) {

    Midas midas = Convert.ref2Midas(ref);
    if (!(ref instanceof Macro) || ((Macro)ref).option.equals("XBC")) {
      throw new MidasException("Error: nxm.sys.lib.Build.preprocessFortran is for internal use only.");
    }

    Build    build  = newInstance(midas,verbose,bldFlags);
    build.showDebug = debug;
    String[] names  = COMMA_OR_PIPE_REGEX.split(nameList); /* Split on a ',' or '|' */
    String   area   = build.getArea(path);
    String   opt    = build.getOpt(path);
    boolean  ok     = true;

    String flags = build.getFortranFlags(opt,area,path); // Compiler flags

    for (int i = 0; ok && (i < names.length); i++) {
      String inName = path + names[i] + ext;

      if (verbose) build.info("Preprocessing " + inName);
      if (debug  ) build.info("      using flags: " + flags);

      ok = build.preprocessFortran(path, names[i], ext, newExt, flags);

      if (!ok) build.warn("Error while trying to preprocess "+inName);
    }
    return ok;
  }

  /** Preprocess native Fortran library.
      @param path   Path to the library
      @param name   The name of the library.
      @param oldExt The file name extension of the source file (usually ".for").
      @param newExt The file name extension of the pre-processed file (usually ".fpp").
      @param flags  The build flags being used.
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
  */
  private boolean preprocessFortran (String path, String name, String oldExt, String newExt,
                                     String flags) {
    String oldName = FileName.terminatePath(path) + name + oldExt;
    String newName = FileName.terminatePath(path) + name + newExt;

    try {
      // Using reflection here b/c this is calling FortranPreprocessor in the XBC option tree
      Class<?>[]     ctypes = new Class<?>[]  { Midas.class, Boolean.TYPE };
      Object[]       cparam = new Object[] { midas, Boolean.valueOf(showDebug) };
      Class<?>[]     types  = new Class<?>[]  { String.class, String.class, String.class };
      Object[]       params = new Object[] { oldName, newName, flags };
      Class<?>       clazz  = Class.forName("nxm.xbc.lib.FortranPreprocessor");
      Constructor<?> cons   = clazz.getConstructor(ctypes);
      Object         obj    = cons.newInstance(cparam);
      Method         meth   = clazz.getMethod("preprocess", types);

      Boolean  status = (Boolean)meth.invoke(obj, params);

      return status.booleanValue();

    } catch (Exception e) {
      Throwable cause = e.getCause();
      if (cause instanceof UnsatisfiedLinkError) {
        // Commonly seen if (1) XBC was not built yet or (2) running with native
        // code disabled, since FortranPreprocessor uses XFPP which is C++ code.
        // (When '$ nm make ...' is used, native code is disabled since the make
        // outside of the NeXtMidas shell is for the boot-strap build and it
        // disables all native code).
        BaseFile bf = new BaseFile(midas, "nxm.xbc.lib.FortranPreprocessor.class");
        if (!bf.exists()) { // Could not find compiled classes for XBC
          midas.warning(">>>> ");
          midas.warning(">>>> WARNING:");
          midas.warning(">>>>   Can not preprocess Fortran file, XBC does not appear to have");
          midas.warning(">>>>   been built yet. Please build XBC with 'nM> MAKE ALL XBC' first. ");
          midas.warning(">>>> ");

        } else {            // Found classes for XBC, but still not able to run...
          midas.warning(">>>> ");
          midas.warning(">>>> WARNING:");
          midas.warning(">>>>   Can not preprocess Fortran file, try building in the");
          midas.warning(">>>>   NeXtMidas shell with Native code enabled. ");
          midas.warning(">>>> ");
        }
      } else {
        // Unknown exception
        midas.printStackTrace("Error building "+oldName,e);
      }
      return false;
    }
  }

  /** Compile native Fortran library.
      @param path        Path to the library
      @param name        The name of the library.
      @param libList     List of other library files to link in.
      @param independent Indicates that the library has no "dependent" file (if
                         this is <code>false</code> it is assumed that an object
                         file with the same name as the library is to be added).
      @return <code>true</code> if the compilation is successful, <code>false</code> otherwise.
  */
  private boolean linkFortran (String path, String name, String libList, boolean independent) {
    String area    = getArea(path);
    String opt     = getOpt(path);
    String outName = path + name + Shell.getSharedObjExt();

    if (area.equals("host")) { // Fortran Host Primitive
      String hprim   = Shell.getOptPath("xbc","lib") + "libHPrimitive" + Shell.getSharedObjExt();
      libList = hprim + " " + libList;
    }

    if (!independent) { // NTN: we should include the output target before the libraries
      libList = (path + name + OBJ_EXTENSION) + " " + libList;
    }

    String linkCmd = getFortranLinker(opt) + " "
                   + getFlagsFromConfig(opt, area, name, "lflags") + " " // link flags from CFG file
                   + getFortranFlags(opt,area,path)
                   + getFortranHostFlags(opt,area)
                   + " -o " + outName + " " + libList;

    return runAutoOS(linkCmd);
  }

  /** Compile a collection of X-Midas Fortran/C++/C host primitive files.
      @param files The files to compile.
      @return The number of files compiled.
   */
  private int compileHostFiles (File[] files) {
    int    numFiles = 0;

    // loop through all the files given and attempt to compile them
    for (File file : files) {
      if (!file.exists()) {
        warn("File not found " + file);
      }
      else {
        boolean isFortran = FOR_SOURCE_FILTER.accept(file);
        boolean isCpp     = CPP_SOURCE_FILTER.accept(file);
        boolean isC       = C_SOURCE_FILTER.accept(file);
        if (isFortran || isCpp || isC) {
        try {
          String path    = FileName.terminatePath(file.getParent());
          int    index   = file.getName().indexOf('.');
          String name    = file.getName().substring(0,index);
          String ext     = file.getName().substring(index);
          String liblist = getLibList(path,name); // search for optional libraries

          if (isFortran || isCpp) {
            // compile optional companion C++ and/or C library with same filename to include in link stage
            File cFile   =  new File(path, name+C_SOURCE);
            File cppFile =  new File(path, name+CPP_SOURCE);
            if (cFile.exists())   { // have matching .c file for either Fortran or C++ primitive
              if (verbose) { info("Compiling " + cFile); }
              String outputFile = path + name + "_c" + OBJ_EXTENSION;
              if (!compileNativeFile(path,name,C_SOURCE, outputFile)) {
                warn("Unable to compile native C code for " + cFile);
                return -1;
              }
              liblist = outputFile + " " + liblist; // add C object file to beginning of link list
              cleanFiles.add(new File(outputFile)); // add temporary object file to cleanup list
            }
            if (!isCpp && cppFile.exists()) { // have matching .cc file for Fortran primitive
              if (verbose) { info("Compiling " + cppFile); }
              String outputFile = path + name + "_cc" + OBJ_EXTENSION;
              if (!compileNativeFile(path,name,CPP_SOURCE, outputFile)) {
                warn("Unable to compile native C++ code for " + cppFile);
                return -1;
              }
              liblist = outputFile + " " + liblist; // add C++ object file to beginning of link list
              cleanFiles.add(new File(outputFile)); // add temporary object file to cleanup list
            }
          }

          if (verbose) { info("Compiling " + file); }
          if (isFortran) { // Fortran primitive
            if (!compileFortran(path,name,ext) || !linkFortran(path,name,liblist,false)) {
              warn("Unable to compile native Fortran code for " + file);
              return -1;
            }
          }
          else {           // C++ or C primitive
            if (!compileNativeFile(path,name,ext,null) || !linkNativeLib(path,name,liblist,false,isCpp)) {
              warn("Unable to compile native C/C++ code for " + file);
              return -1;
            }
          }
          cleanFiles.add(new File(path + name + OBJ_EXTENSION));
          numFiles++;
        } catch(Exception e) {
          midas.printStackTrace("Unable to compile " + file + ": "+e.getMessage(), e);
        }
        }
      }
    }
    return numFiles;
  }

  /** Compile a collection of X-Midas host primitive files.
      @param files The files to compile.
      @return The number of files compiled.
   */
  private int buildSharedObjects (File[] files) {
    int numCompiled = 0;
    ArrayList<File> librariesAlreadyBuilt = new ArrayList<>();
    for (File libfile : files) {
      if (!librariesAlreadyBuilt.contains(libfile)) {
        numCompiled += buildShareObject(libfile, librariesAlreadyBuilt);
      }
    }

    return numCompiled;
  }

  /** Compile a collection files for a Shared Object (e.g. library).
      @param libfile library file to build
      @param librariesAlreadyBuilt List of files that have already been built
      @return number of files or libraries built (normally 1)
      @since NeXtMidas 2.7.0 (extracted from {@link #buildSharedObjects(File[])}
   */
  private int buildShareObject (File libfile, List<File> librariesAlreadyBuilt) {
    int numCompiled = 0;
    info("Building Shared Object " + libfile);

    File[] libs = prepareLibraries(libfile,true);

    if (libs.length == 0) {
      throw new MidasException("Unable to build shared object " + libfile); // 2.7.2?
    }
    else {
      String path  = libfile.getParentFile().getAbsolutePath() + File.separator;
      String fname = libfile.getName();
      String name  = fname.substring(0, fname.lastIndexOf('.'));
      StringBuilder liblist = new StringBuilder();

      for (File file : libs) {
        liblist.append(' ');
        String fullpathname;
        try {
          fullpathname = file.getCanonicalPath(); // use canonical path
        } catch (IOException ioe) {
          fullpathname = file.getAbsolutePath(); // fall back to using absolute path
        }

        if (FLIB_SOURCE_FILTER.accept(file) || CLIB_SOURCE_FILTER.accept(file) || CPPLIB_SOURCE_FILTER.accept(file)) {
          // convert mylib.[f|c|cpp]lib to mylib<sharedObjExt>
          String libname  = fullpathname.substring(0, fullpathname.lastIndexOf('.')) + Shell.getSharedObjExt();
          if (!librariesAlreadyBuilt.contains(libfile)) {
            if (!new File(libname).exists()) {
              // NTN: build included shared object ONLY if it does NOT exist
              //      this is to reduce the compile time, otherwise it is safe
              //      to always build included .?lib files
              numCompiled += buildShareObject(file, librariesAlreadyBuilt);
            }
          }
          liblist.append(libname);
        } else {
          liblist.append(fullpathname);
        }
      } // end for

      if (FLIB_SOURCE_FILTER.accept(libfile)) {
//if (showDebug) System.out.println("  calling linkFortran: name="+name+" path="+path+" liblist="+liblist);
        if (linkFortran(path, name, liblist.toString(), true)) {
          numCompiled++;
        } else {
          // warn("Unable to link libraries for " + libfile); // 2.7.0
          throw new MidasException("Unable to link libraries for " + libfile); // 2.7.2
        }
      } else {
        boolean cpp = CPPLIB_SOURCE_FILTER.accept(libfile); // Use cpp linker rather than just c linker
//if (showDebug) System.out.println("  calling linkNativeLib: name="+name+" path="+path+" liblist="+liblist);
        if (linkNativeLib(path, name, liblist.toString(), true, cpp)) {
          numCompiled++;
        } else {
          // warn("Unable to link libraries for " + libfile); // 2.7.0
          throw new MidasException("Unable to link libraries for " + libfile); // 2.7.2
        }
      }
    }

    librariesAlreadyBuilt.add(libfile);
    return numCompiled;
  }

  /** Compile a collection of X-Midas host primitive files.
      @param file     The file to compile.
      @param compile  Indicates if the dependent files should be compiled.
      @return The number of files compiled.
   */
  private File[] prepareLibraries (File file, boolean compile) {
    File[]  returnValue = EMPTY_FILE_ARRAY;
    boolean verbose = this.verbose && compile;
    boolean error   = false;

    if (file.exists()) {
      try {
        String[]        libs   = readLibListFile(file);
        ArrayList<File> toLink = new ArrayList<>();

        for (String libName : libs) {
          String fileName = libName.replace('\\', File.separatorChar)
                                   .replace('/',  File.separatorChar);
          File src = new File(fileName);

          if (!src.exists()) {
            if (verbose) { warn("    "+src+" (MISSING)"); }
            error = true;

          } else if (C_SOURCE_FILTER.accept(src) || CPP_SOURCE_FILTER.accept(src)
                                                 || FOR_SOURCE_FILTER.accept(src)) {
            int     index = src.getName().lastIndexOf('.');
            String  path  = src.getParentFile().getAbsolutePath() + File.separator;
            String  name  = src.getName().substring(0,index);
            String  ext   = src.getName().substring(index);
            boolean cpp   = CPP_SOURCE_FILTER.accept(src);
            File    lib   = new File(src.getParentFile(), name+OBJ_EXTENSION);

            if (verbose) { info("    "+src+" (BUILDING)"); }

            toLink.add(lib);
            cleanFiles.add(lib);

            if (compile) {
              if (FOR_SOURCE_FILTER.accept(src)) {
                if (!compileFortran(path,name,ext)) {
                  warn("Unable to compile " + src);
                  error = true;
                }
              } else {
                if (!compileNativeFile(path,name,ext,null)) {
                  warn("Unable to compile " + src);
                  error = true;
                }
              }
            }
          } else {
            // Assume it's an object file.
            if (verbose) { info("    "+src+" (FOUND)"); }
            toLink.add(src);
          }
        }

        // Build any dependencies.
        if (!error) {
          returnValue = toLink.toArray(EMPTY_FILE_ARRAY);
        }
      } catch(Exception e) {
        midas.printStackTrace("Unable to compile " + file + ": " + e.getMessage(), e);
      }
    } else {
      warn("File not found " + file);
    }

    return returnValue;
  }

  /** Reads a LibList file (.[c,cpp,f]lib). */
  private String[] readLibListFile (File file) throws IOException {
    final ArrayList<String> libs = new ArrayList<>();
    final BufferedReader    in   = new BufferedReader(new FileReader(file));

    String line;
    while ((line = in.readLine()) != null) {
      line = line.trim();
      if ((line.length() == 0) || line.startsWith("#") || line.startsWith("!")) {
        continue; // ignore empty lines and lines starting with # or !
      }
      else if (StringUtil.isWildcardString(line)) {
        File root = file.getParentFile();

        if (line.startsWith("/") || line.startsWith("\\")) {
          root = new File(File.pathSeparator);
          line = line.substring(1);
        }
        listFiles(libs, root, line);
      }
      else {
        String filename;
        if (FileUtil.startsWithRootDir(line)) filename = line;    // file points to an absolute path
        else filename = file.getParent() + File.separator + line; // otherwise file is relative to current area of option tree
        libs.add(filename);
      }
    }
    in.close();
    return libs.toArray(EMPTY_STRING_ARRAY);
  }

  /** Lists files in a directory matching a wildcard name. This is used only by readLibListFile() */
  private void listFiles (List<String> fileList, File root, String name) {
    name = name.replace('\\', '/'); // Make all paths unix-based for the moment.

    while (name.indexOf('/') == 0) {
      name = name.substring(1); // Remove any "//" directory separators.
    }

    if (!root.isDirectory()) {
      throw new MidasException("Can not list files in "+root+": not a directory.");
    }
    else if (name.indexOf('/') > 0) {
      String item = name.substring(0, name.indexOf('/'));
      name = name.substring(name.indexOf('/')+1);

      if (StringUtil.isWildcardString(item)) {
        /* Go down several single directories */
        File[] files = root.listFiles();

        for (File file : files) {
          if (file.isDirectory() && StringUtil.isWildcardMatch(item, file.getName())) {
            listFiles(fileList, file, name);
          }
        }
      }
      else {
        /* Go down a single directory */
        listFiles(fileList, new File(root,item), name);
      }
    }
    else if (StringUtil.isWildcardString(name)) {
      File[] files = root.listFiles();

      for (File file : files) {
        if (!file.isDirectory() && StringUtil.isWildcardMatch(name, file.getName())) {
          fileList.add(file.toString());
        }
      }
    }
    else {
      fileList.add(new File(root, name).toString());
    }
  }

  /** Check for any .clib, .cpplib, or .flib dependencies.
      @param file The native file.
   */
  private void checkForLibs (File file) {
    int     index = file.getName().indexOf(".");
    String  name  = file.getName().substring(0,index);
    String  path  = FileName.terminatePath(file.getParent());

    checkForLibs(path,name);
  }

  private String formName (String s, String name) {
    int i=s.indexOf('$'), j=name.indexOf('$'), k;
    if (i>0 && j<0) return null;
    k = s.indexOf("<name>");
    if (k>=0) s = s.substring(0,k)+name+s.substring(k+6);
    k = s.indexOf("<inner>");
    if (k>=0) s = s.substring(0,k)+name.substring(j+1)+s.substring(k+7);
    k = s.indexOf("<outer>");
    if (k>=0) s = s.substring(0,k)+name.substring(0,j)+s.substring(k+7);
    return s;
  }

  /** Check for any .clib, .cpplib, or .flib, dependencies.
      @param path The path to the file.
      @param name The file name.
  */
  private void checkForLibs (String path, String name) {
    for (String libName : LIB_NAME_LIST) {
      if ((libName=formName(libName,name))==null) continue;
      for (String libExt : LIB_LIST_EXTS) {
        File file = new File(path + libName + libExt);
        if (file.exists() && !xlibFiles.contains(file) && !clibFiles.contains(file)) {
          xlibFiles.add(file); break; // found extension match, so stop looking
        }
      }
    }
  }

  /** Check for any lib<name> or libAll with extensions .so, .o, or .a, to autolink.
      @param path The path to the file.
      @param name The file name.
  */
  private String getLibList (String path, String name) {
    String liblist = "";
    // iterate through list of libraries that match name to be include by linker
    for (String libName : LIB_NAME_LIST) {
      if ((libName=formName(libName,name))==null) continue;
      for (String libExtension : LIB_EXTENSIONS) {
        File file = new File(path+libName+Shell.getLibrarySuffix()+libExtension);
        if (file.exists()) { liblist += " "+file; break; } // found extension match, so stop looking
      }
    }
    return liblist;
  }

  /** Indicates if we can run the build commands internally. We need to run things external by
      default on windows. This is because windows locks the files we are trying to rewrite (e.g.
      DLL files) causing GCC to fail with little warning.
   */
  private boolean isRunInternalOk () {
    return !Shell.ostag.equals("win");
  }

  /** Run OS build command.
      @param command The OS command to run
      @return Always returns <code>true</code>.
  */
  private boolean runAutoOS (String command) {
    return (isRunInternalOk())? runInternalOS(command): runExternalOS(command);
  }

  /** Run OS build command.
      @param command The OS command to run
      @return Always returns <code>true</code>.
  */
  private boolean runInternalOS (String command) {
    return runInternalOS(midas,command,showDebug);
  }

  /** Run OS build command.
      @param command The OS command to run
      @return Always returns <code>true</code>.
  */
  private boolean runExternalOS (String command) {
    return runExternalOS(midas,command,showDebug);
  }

  /** Run OS build command.
      @param command The OS command to run
      @return Always returns <code>true</code>.
  */
  private static boolean runInternalOS (Midas midas, String command, boolean showDebug) {
    if (showDebug) {
      midas.info("Running Command: " + command);
    }
    return (Foreign.runInternal(midas,command,true) == 0);
  }

  /** Run OS build command.
      @param command The OS command to run
      @return Always returns <code>true</code>.
  */
  private static boolean runExternalOS (Midas midas, String command, boolean showDebug) {
    if (showDebug) {
      midas.info("Running Command: " + command);
    }
    Foreign.runExternal(midas,command);
    return true;
  }

  /** A method that gets a system property. Note that this will look into the bldFlags
      Table and prepend the value at the matching propName key (with dots replaces with underscores)
      to the values from the properties file.
      @param opt      The option tree.
      @param propName The property name.
      @return The property value associated with the property name or "" if it does not exist.
  */
  private String getProperty (String opt, String propName) {
    String name = propName.replace('.','_').toUpperCase();
    String prop = (bldFlags == null)? null: bldFlags.getS(name, null);
    String val  = getPropertyStatic(opt, propName);

    if (val == null) { val = ""; }

    if (prop == null) {
      prop = val;
    } else {
      prop = prop + " " + val;
    }

    return prop;
  }

  /** A method that gets a system property for specified option tree and property name.
      This uses <code>BuildProps.getPropsFor(opt, Shell.getCurrent())</code>
      @param opt      The option tree name (cannot be null).
      @param propName The property name.
      @return The property value associated with the property name or "" if it does not exist.
  */
  public static String getPropertyStatic (String opt, String propName) {
    String value = BuildProps.getPropsFor(opt, Shell.getCurrent()).getProperty(propName, prependSuffixes, appendSuffixes);
    return (value == null)? "": value;
  }

  /** Invoked to compile all of the files within a given area of the specified option tree. <br>
      <br>
      Examples:
      <pre>
        1. Build all of the Java files in the $NMROOT/sys/prim directory:
             compile("sys", "prim", BUILD_CLASS_FILES);

        2. Build all of the Java and Native files in $NMROOT/sys/libg and
           $NMROOT/sys/lib:
             compile("sys", "lib,libg", BUILD_CLASS_FILES|BUILD_NATIVE_FILES);
      </pre>
      @param opt      The option tree containing the files.
      @param areaList The list of areas within the option tree containing the
                      files to compile. The list should be either comma-
                      (<code>','</code>) or pipe- (<code>'|'</code>) separated.
      @param types    The types of files to compile. This should be the or'ing
                      of type masks (see {@link #BUILD_CLASS_FILES},
                      {@link #BUILD_NATIVE_FILES}, and {@link #BUILD_HOST_FILES}).
      @return The number of files that are compiled or <code>-1</code> on error.
  */
  public int compile (String opt, String areaList, int types) {
    String[] areas   = StringUtil.split(areaList); // Split on a ',', '|', or whitespace(s)
    String   optPath = "," + midas.getResults().get("OPT.PATH") + ",";

    for (String area : areas) {
      File path = new File(midas.io.name(area, "", "", opt));

      if (path.exists()) {
        if ((types & BUILD_HOST_FILES) != 0) {
          types |= BUILD_NATIVE_FILES; // HOST requires NATIVE
        }

        if ((types & BUILD_CLASS_FILES) != 0) {
          classFiles.addAll(Arrays.asList(listFiles(path,JAVA_SOURCE_FILTER)));
        }

        if ((types & BUILD_NATIVE_FILES) != 0) {
          nativeFiles.addAll(Arrays.asList(listFiles(path,C_SOURCE_FILTER)));
          nativeFiles.addAll(Arrays.asList(listFiles(path,CPP_SOURCE_FILTER)));
          clibFiles.addAll(Arrays.asList(listFiles(path,CLIB_SOURCE_FILTER)));
          clibFiles.addAll(Arrays.asList(listFiles(path,CPPLIB_SOURCE_FILTER)));
        }

        if ((types & BUILD_HOST_FILES) != 0) {
          clibFiles.addAll(Arrays.asList(listFiles(path,FLIB_SOURCE_FILTER)));

          if (area.equalsIgnoreCase("host")) {
            if (optPath.indexOf(",XBC,") < 0) {
              midas.warning("Must add XBC to the path before compiling host primitives.");
            }
            else if (optPath.indexOf("," + opt + ",") >= 0) {
              String[] hostCommands = Command.getMenuCommands(midas, null, "H", opt, false, false);

              for (String hostCmd : hostCommands) {
                hostCmd        = hostCmd.toLowerCase();
                File forFile   = new File(path, hostCmd + FOR_SOURCE);
                File cppFile   = new File(path, hostCmd + CPP_SOURCE);
                File cFile     = new File(path, hostCmd + C_SOURCE);
                if      (forFile.exists()) hostFiles.add(forFile); // Fortran  primitive takes 1st precedence
                else if (cppFile.exists()) hostFiles.add(cppFile); // then C++ primitive next
                else if (cFile.exists())   hostFiles.add(cFile);   // then C   primitive last
              }
            }
            else {
              midas.warning("Must add " + opt + " to the path before compiling host primitives.");
            }
          }
        }

        if (area.equalsIgnoreCase(TEST_AREA) && ((types & BUILD_CLASS_FILES) != 0)) {
          File junit = new File(path, "junit");

          if (junit.exists()) {
            junitFiles.addAll(Arrays.asList(listFiles(junit,JAVA_SOURCE_FILTER)));

            File[] junitDirs = junit.listFiles();

            for (File junitDir : junitDirs) {
              if (junitDir.exists() && junitDir.isDirectory()) {
                junitFiles.addAll(Arrays.asList(listFiles(junitDir,JAVA_SOURCE_FILTER)));

                // NXM-3897 Checks for a second layer of directories under test/junit containing .java files
                File [] junitDirSubDirs = junitDir.listFiles();
                for (File junitDirSubDir : junitDirSubDirs) {
                  if (junitDirSubDir.exists() && junitDirSubDir.isDirectory()) {
                    junitFiles.addAll(Arrays.asList(listFiles(junitDirSubDir,JAVA_SOURCE_FILTER)));
                  }
                }
              }
            } // end for loop
          }
        }
      }
    }
    return compile(opt);
  }

  /** Invoked to compile a file whose type is determined by extension.
      This is identical to <code>compile(new&nbsp;File[]{file})</code>.
      @param file The file to compile.
      @param optname The option tree name
      @return The number of files that are compiled or <code>-1</code> on error.
  */
  public final int compile (File file, String optname) {
    return compile(new File[] { file }, optname);
  }

  /** Invoked to compile a set of files whose type is determined by extension. See {@link #C_SOURCE},
      {@link #CPP_SOURCE}, {@link #FOR_SOURCE}, {@link #CLIB_SOURCE}, {@link #CPPLIB_SOURCE},
      and {@link #FLIB_SOURCE}.
      Note: most of this logic have been moved into {@link Build#compile(File[], String, String)}
      @param files The file to compile.
      @param optname The option tree name where the files should be
      @return The number of files that are compiled or <code>-1</code> on error.
  */
  public int compile (File[] files, String optname) {
    String area = "";
    return compile(files, optname, area);
  }

  /** Invoked to compile a set of files whose type is determined by extension. See {@link #C_SOURCE},
      {@link #CPP_SOURCE}, {@link #FOR_SOURCE}, {@link #CLIB_SOURCE}, {@link #CPPLIB_SOURCE},
      and {@link #FLIB_SOURCE}.
      @param files The files to compile.
      @param optname The option tree name where the files should be
      @param area The area within the option to search (e.g. <code>prim</code>) (cannot be null).
      @return The number of files that are compiled or <code>-1</code> on error.
      @since NeXtMidas 2.7.2
  */
  public int compile (File[] files, String optname, String area) {
// if (showDebug) System.out.println("  compile(File[], opt, area): opt="+optname+" area="+area +" files="+Arrays.toString(files));
    for (File file : files) {
      if (JAVA_SOURCE_FILTER.accept(file)) {
        classFiles.add(file);
      }
      else if (C_SOURCE_FILTER.accept(file) || CPP_SOURCE_FILTER.accept(file)) {
        if (area.equalsIgnoreCase("host")) {
          hostFiles.add(file);
        } else {
          nativeFiles.add(file);
        }
        checkForLibs(file);
      }
      else if (FOR_SOURCE_FILTER.accept(file)) {
        hostFiles.add(file);
        checkForLibs(file);
      }
      else if (CLIB_SOURCE_FILTER.accept(file)
              || CPPLIB_SOURCE_FILTER.accept(file)
              || FLIB_SOURCE_FILTER.accept(file)) {
        clibFiles.add(file);
      }
      else {
        warn("Unknown file extension for "+file+", skipping.");
      }
    }
    return compile(optname);
  }

  /** Indicates if precompiled libraries should be used rather than compiling native code. */
  private boolean usesPrecompiledLibs (String opt) {
    return getCCompiler(opt,false).equals("none");
  }

  /** Verifies that a set of libraries is empty. Used ONLY by compile(). */
  private boolean checkEmpty (List<?> list, String descr) {
    boolean ok = (list.size() == 0);
    if (!ok) { warn("Can not compile "+descr+" when C/C++ compiler is set to 'none'."); }
    return ok;
  }

  /** Compiles the files for the specified option tree. */
  private synchronized int compile (String opt) {
    if (opt == null) opt = getOpt(nativeFiles); // if opt is null, revert to old behavior
    boolean precompiled = usesPrecompiledLibs(opt);
    boolean ok          = true;
    compileCount = 0;

    // Note: The order of the following is important!!!
    ok = ok && count(compileClassFiles(classFiles, false));
    ok = ok && count(compileClassFiles(junitFiles, true));

    if (precompiled) {
      ok = ok && count(copyPrecompiledFiles(asArray(nativeFiles)));
      ok = ok && checkEmpty(hostFiles, "host primitives");
      ok = ok && checkEmpty(clibFiles, ".clib and .cpplib and .flib files");
      // It is ok for xlibFiles to be non-empty since those files were automatically
      // picked up, and would already be part of any pre-compiled library.

    } else {
      // NTN: parse primitives.cfg & libraries.cfg in specified option tree
      if (primConfig == null || !opt.equalsIgnoreCase((String)primConfig.get("optname"))) {
        // 1. only read CFG file if we haven't already read & parse it before.
        // 2. make primitives implicitly depend on "HPrimitive;XBC" for BUG 2298
        Map<String,Object> defaults = new HashMap<>();
        ArrayList<String> default_libdepend = new ArrayList<>();
        default_libdepend.add("HPrimitive;XBC");
        defaults.put("libdepend", default_libdepend);
        primConfig = read_cfg_file(opt, "primitives.cfg", defaults);
      }
//      if (libConfig == null || !opt.equalsIgnoreCase((String)libConfig.get("optname"))) {
//        // only read CFG file if we haven't already read & parse it before
//        libConfig = read_cfg_file(opt, "libraries.cfg", null);
//      }

      // JC4: Separated header generation from compilation to handle this case:
      //    (1) Build header           myprim.java -> myprim.h
      //    (2) Build shared object    libmyprim.clib (depends on myprim.h) -> libmyprim.so
      //    (3) Build native library   myprim.c (depends on libmyprim.so) -> myprim.so
      ok = ok && count(compileNativeHeaders(asArray(nativeFiles)));
      ok = ok && count(buildSharedObjects(asArray(clibFiles)));
      ok = ok && count(buildSharedObjects(asArray(xlibFiles)));
      ok = ok && count(compileNativeFiles(asArray(nativeFiles)));
      ok = ok && count(compileHostFiles(asArray(hostFiles)));
    }

    if (ok) {
      delete(asArray(cleanFiles));
    }
    reset();
    return compileCount;
  }

  /** Called only by compile(). */
  private boolean count (int numCompiled) {
    compileCount = (numCompiled < 0)? numCompiled: (compileCount + numCompiled);
    return (compileCount >= 0);
  }

  /** Used only by compile(). */
  private static File[] asArray (List<File> list) {
    return list.toArray(EMPTY_FILE_ARRAY);
  }

  /** Invoked to build the specified macro within the given option tree. Since there is no real
      translation of the script files, this method simply attempts to parse the script to ensure
      that it compiles properly.
      @param opt  The option tree that the macro resides within.
      @param name The base name of the script to be built.
  */
  public static void compileMacro (String opt, String name) {
    Midas midas = defaultMidas;
    if (name.endsWith(".mm")) {
      name = name.substring(0, name.length() - 3);
    }

    Args targs = Shell.parseCommand(midas,name);
    Command command = Shell.loadCommand(midas,targs);
    ((Macro)command).loadText(); // try translation
  }

  /** Gets the option tree from a set of files. This should only be used when getting a compile
      property. If the option trees in the set of files don't match, a warning will be issued and
      "SYS" will be returned.
      @param files The set of files.
      @return The option tree (or "SYS" if files is empty).
   */
  private String getOpt (List<File> files) {
    String opt = "SYS";

    if ((files != null) && (files.size() > 0)) {
      opt = getOpt(files.get(0).getAbsolutePath());

      for (File f: files) {
        String option = getOpt(f.getAbsolutePath());
        if (!opt.equals(option)) {
          midas.warning("BUILD: Given set of files that span multiple option trees, using "
              + "build.props in SYS.");
          opt = "SYS";
          break;
        }
      } // end for loop
    }

    return opt;
  }

  /** This will get the area out of a path.
      @param path The path to a file.
      @return The area from that path.
      @see #getOpt(String)
   */
  private String getArea (String path) {
    return getOptArea(path)[1];
  }

  /** This will get the area out of a path.
      @param path The path to a file.
      @return The area from that path.
      @see #getArea(String)
  */
  private String getOpt (String path) {
    return getOptArea(path)[0];
  }

  /** This will get the "option" tree name and "area" out of a path. */
  private String[] getOptArea (String path) {
    String opt;
    String area;
    if (path.endsWith(File.separator)) {
      path = path.substring(0,path.length()-1);
    }
    int i = path.lastIndexOf(File.separator+"nxm"+File.separator);
    if (i == -1) { // "/nxm/" NOT found in path
      // TODO: return 1. empty string or 2. null for opt and area?
    }
    int j = path.indexOf(File.separator,i+5);
    if (j == -1) {
      opt = path.substring(i+5);
      area = ""; // no AREA found in path
    }
    else {
      opt = path.substring(i+5,j);
      area = path.substring(j+1);
    }
    return new String[] { opt, area };
  }

  /** Gets the classpath to use.
      @param  opt The option tree to compile (empty for none).
      @return The classpath.
  */
  private static String getClassPath (String opt) {
    StringBuilder classPath = new StringBuilder();

    /* Add the JAR files and directories for the options in the PATH. */
    classPath.append(getPropertyStatic(opt, "java.class.path"));

    /* Add the JAR files and directories for the options not in the PATH but in $NMROOT/nxm. */
    if (opt != null) {
      classPath.append(File.pathSeparator);
      classPath.append(StringUtil.join(Shell.getClasspathFor(opt),File.pathSeparator));
    }

    return classPath.toString();
  }

  /** This gets a print writer that will write to {@link Midas#message(CharSequence, int, Object)}
      This is useful when calling javac and javadoc.
      @param msgType The Midas message type.
      @param midas   The Midas context to use.
      @return The appropriate PrintWriter.
  */
  private static PrintWriter getMidasPrintWriter (int msgType, Midas midas) {
    return new PrintWriter(getOutputStreamToMidas(msgType,midas));
  }

  /** This gets a OutputStream that will write to {@link Midas#message(CharSequence, int, Object)}
      This is useful when calling javac and javadoc.
      @param msgType The Midas message type or combined Build DETECT_WARN_OR_ERROR
      @param midas   The Midas context to use.
      @return The appropriate OutputStream.
      @since NeXtMidas 3.3.0 (extracted originally from {@link #getMidasPrintWriter(int, Midas)})
  */
  private static OutputStream getOutputStreamToMidas (final int msgType, final Midas midas) {
    final String lineSeparator = Shell.getProperty("line.separator");
    boolean warnUnlessAutodetectError = Build.DETECT_WARN_OR_ERROR == msgType;

    return new OutputStream() {
      private StringBuffer buffer = new StringBuffer();

      public synchronized void write(int b) { buffer.append((char)b); }
      public synchronized void flush()      { flush(false); }
      public synchronized void close()      { flush(true); }


      public synchronized void flush(boolean forceWrite) {
        if (forceWrite || (buffer.length() > 0)) {
          String text = buffer.toString();

          if (text.endsWith(lineSeparator)) {
            text = text.substring(0, text.length() - lineSeparator.length());
          }

          // In 4.0.0 added ability to detect error conditions and
          // adjust error type from WARN to ERROR NXM-3661
          int midasMsgType = msgType;
          if (warnUnlessAutodetectError) {
            if (text.toLowerCase().contains("error:")) {
              midasMsgType = Midas.ERROR;
              // add build error to a build status table
              if (midas != null) {
                midas.setBuildStatus(BUILD_FAILURE);
                midas.addBuildError(text);
              }
            }
            else {
              midasMsgType = Midas.WARNING;
            }
          }

          midas.message(text, midasMsgType, null);

          buffer = new StringBuffer();
        }
      }
    };
  }

  /** Gets the name of the Fortran compiler to use.
      @return the name of the Fortran compiler in use
   */
  public static String getFortranCompiler () {
    if (forCompiler == null) {
      forCompiler = getPropertyStatic("SYS", "fflags.compiler");
      if (xmArchType == null) getXmArchType();

      if ((forCompiler == null) || (forCompiler.trim().length() == 0)) {
        forCompiler = "auto";
      } else {
        forCompiler = forCompiler.trim().toLowerCase();
      }

      // Get Midas context and results
      Midas sharedMidas = Shell.getSharedMidasContext();
      Table sharedMidasResults = sharedMidas.getResults();

      // SET OPTIONS
      String  path      = "," + sharedMidasResults.getS("OPT.PATH") + ",";
      boolean xbcOnPath = path.contains(",XBC,");

      // INIT THE FORTRAN COMPILER
      if (forCompiler.equals("auto")) {
        String envVar = Import.getEnv("NM_FORTRAN");

        if (envVar == null) envVar = Import.getEnv("XM_FORTRAN");
        if (envVar != null) envVar = envVar.trim().toLowerCase();

        if ((envVar != null) && (envVar.length() > 0)) {
          forCompiler = envVar;
        } else {
          forCompiler = defaultForCompiler; // since 3.5.3 default to Intel Fortran compiler (instead of unsupported pgf77)
        }
      }
      forCompiler = forCompiler.trim().replace('\t',' ');
      if (forCompiler.indexOf(' ') > 0) {
        forCompiler = forCompiler.substring(0,forCompiler.indexOf(' '));
      }

      boolean forCompilerEqGfc         = forCompiler.equals("gfc");
      boolean forCompilerEqGfcGfortran = forCompiler.equals("gfc-gfortran");
      boolean forCompilerEqGfortran    = forCompiler.equals("gfortran");
      if (forCompilerEqGfc || forCompilerEqGfcGfortran || forCompilerEqGfortran) {
        defaultGfcForCompiler = getPropertyStatic("SYS","gfc.bin");
        Shell.runCommand(sharedMidas,"os/stdout=GFCSCRIPT ^{env.nmroot}os/unix/nmgfcinfo.sh");
        String[] gfcScript = sharedMidasResults.get("GFCSCRIPT").toString().trim().split("\n");
        gfcForCompiler = gfcScript[0];
        if ((gfcForCompiler != null) && !gfcForCompiler.equals("-1")) {
          gfcLibsDir = gfcScript[1];
          if (forCompilerEqGfcGfortran && !gfcForCompiler.equals("gfc-gfortran")) {
            Shell.info("'gfc-gfortran' specified, but switching to using the 'gfortran' gfc Fortran compiler.");
          }
          else if (forCompilerEqGfortran && !gfcForCompiler.equals("gfortran")) {
            Shell.warning(">>>> ");
            Shell.warning(">>>> WARNING:");
            Shell.warning(">>>>   The 'gfortran' gfc Fortran compiler either is NOT installed or does NOT take DEC extensions.");
            Shell.warning(">>>>   Switching to using the 'gfc-gfortran' gfc Fortran compiler.");
            Shell.warning(">>>> ");
          }
        }
        else {
          gfcForCompiler = (forCompilerEqGfc) ? defaultGfcForCompiler : forCompiler;
          Shell.warning(">>>> ");
          Shell.warning(">>>> WARNING:");
          Shell.warning(">>>>   Neither the 'gfc-gfortran' nor 'gfortran' gfc Fortran compilers seem to take DEC extensions or be installed.");
          if (forCompilerEqGfc) Shell.warning(">>>> Switching to "+defaultGfcForCompiler+" by default since 'gfc' was specified.");
          Shell.warning(">>>> ");
        }
        forCompiler = "gfc";
      }
      else if (xbcOnPath && forCompiler.equals("pgf77")) { // since 3.5.3 warn that pgf77 does NOT work with XBC
        Shell.warning(">>>> ");                            // using pgf77 for XBC on 64-bit will probably crash JVM
        Shell.warning(">>>> WARNING:");
        Shell.warning(">>>>   NeXtMidas XBC does not support the 'pgf77' Fortran compiler.");
        Shell.warning(">>>>   Your host primitives will have undefined behaviors and/or core dump.");
        Shell.warning(">>>> ");
      }
      else {
        String[] compOptions = (xbcOnPath) ? compOptionsXBC : compOptionsNXM;
        if (Arrays.binarySearch(compOptions,forCompiler) < 0) {
          Shell.warning(">>>> ");
          Shell.warning(">>>> WARNING:");
          Shell.warning(">>>>   NeXtMidas does not support the '"+forCompiler+"' Fortran compiler for use.");
          Shell.warning(">>>>   Please set the environment variable NM_FORTRAN to indicate your compiler.");
          Shell.warning(">>>>   Using default compiler '"+defaultForCompiler+"'.");
          Shell.warning(">>>> ");
          forCompiler = defaultForCompiler; // since 3.5.3 default to Intel Fortran compiler (instead of unsupported pgf77)
        }
      }
    }
    return forCompiler;
  }

  /** Gets the architecture type to use.
      @return the architecture type in use.
   */
  public static String getXmArchType () {
    if (xmArchType == null) {
      xmArchType = getPropertyStatic("SYS", "fflags.xmarch");

      if ((xmArchType == null) || (xmArchType.trim().length() == 0)) {
        xmArchType = "auto";
      } else {
        xmArchType = xmArchType.trim().toUpperCase();
      }

      if (Arrays.binarySearch(archOptions,xmArchType) < 0) {
        String def = Shell.getDefArchType()+"BIT";
        if (!xmArchType.equals("AUTO")) {
          Shell.warning(">>>> ");
          Shell.warning(">>>> WARNING:");
          Shell.warning(">>>>   NeXtMidas does not support the '"+xmArchType+"' arch type. Expected one of");
          Shell.warning(">>>>   '32Bit,64Bit,Classic'. Please set fflags.xmarch in nxm/sys/cfg/build.props.");
          Shell.warning(">>>>   Using default fflags.xmarch='"+def+"'.");
          Shell.warning(">>>> ");
        }
        xmArchType = def;
      }
    }
    return xmArchType;
  }

  /** Compile code internally by invoking com.sun.tools.javac.Main.compile(..).
      @return 0 for success, non-zero otherwise
    */
  private int internalCompile (String[] args) {
    int status = -1;
    OutputStream warnOrError = getOutputStreamToMidas(Build.DETECT_WARN_OR_ERROR, midas);
    OutputStream info = getOutputStreamToMidas(Midas.INFO,    midas);

    if (showDebug)
      midas.info("Running internal compile: " + java.util.Arrays.toString(args));

    status = getJavaCompiler().run(null, info, warnOrError, args); // since 3.3.0 - use JDK 6 API

    if (showDebug)
      midas.type("Build.internalCompile(..) args.length = "+args.length+" result = "+status);

    return status;
  }

  /** Compile javadocs internally by invoking com.sun.tools.javadoc.Main.execute(..). */
  private static void internalJavadoc (Midas midas, String doclet, String[] args) {
    PrintWriter warn = getMidasPrintWriter(Midas.WARNING, midas);
    PrintWriter info = getMidasPrintWriter(Midas.INFO,    midas);

    try {
      Class<?>   main    = Class.forName("com.sun.tools.javadoc.Main");
      Object[]   params  = { "NeXtMidas", warn, warn, info, doclet, args };
      Class<?>[] types   = { String.class, PrintWriter.class, PrintWriter.class, PrintWriter.class,
                           String.class, String[].class,  };
      Method   execute   = main.getMethod("execute", types);

      execute.invoke(null, params);

    } catch(Exception e) {
      midas.printStackTrace("Build: Error running javadoc internally",e);
    }
  }

  /** Deletes a given set of files. */
  private void delete (File[] files) {
    if (keepFiles) {
      // Don't delete
    } else if (isRunInternalOk()) {
      for (File file : files) {
        if (file.exists()) {
          if (showDebug) {
            info("Deleting " + file);
          }
          if (!file.delete()) {
            warn("Unable to delete " + file);
          }
        }
      }
    } else {
      String rm = getProperty("SYS", "remove."+Shell.ostype);

      Arrays.sort(files);
      for (int i = 0; i < files.length; i++) {
        if ((i > 0) && (files[i-1].equals(files[i]))) {
          // skip duplicate entry
        } else {
          runExternalOS(rm+" "+files[i].getAbsolutePath());
        }
      }
    }
  }

  /** Prints an info message. */
  private void info (String str) {
    midas.message(str, Midas.INFO, null);
  }

  /** Prints a warning message. */
  private void warn (String str) {
    midas.message(str, Midas.WARNING, null);
  }

  /** This is used to avoid ambiguity when both FilenameFilter and FileFilter are implemented. */
  private static File[] listFiles (File path, FilenameFilter filter) {
    return path.listFiles(filter);
  }

  /** Gets the OS name (e.g. "lin", "sol", "win") */
  private String getOsName (String opt) {
    return getProperty(opt, "native."+Shell.ostag+".osname");
  }

  /** Get the C/C++ compiler flags from OS environment variables $NM_CC_OPTIONS
      or $NM_CXX_OPTIONS (if set) for the C or the C++ compilers, respectively.
      @param cpp true to get C++ compiler options, otherwise gets C compiler options.
      @since NeXtMidas 2.9.2
   */
  private String getCCompilerFlagsFromOSEnv (boolean cpp) {
    String flags_from_env = (cpp)? Import.getEnv("NM_CXX_OPTIONS") : Import.getEnv("NM_CC_OPTIONS");
    if (flags_from_env == null) flags_from_env = "";
    return flags_from_env;
  }

  /** Get the C/C++ compiler with flags/options.
      @param opt option tree name to get the C or C++ compiler + options.
      @param cpp true to get C++ compiler + options, otherwise gets C compiler + options.
      @see #getCCompiler(String, boolean)
      @see #getCBuildFlags(String, boolean)
   */
  private String getCCompilerFlags (String opt, boolean cpp) {
    return getCCompiler(opt,cpp) + " " + getCBuildFlags(opt,cpp);
  }

  /** Get the C/C++ linker with flags (excluding appended flags). */
  private String getCLinkerFlags (String opt, boolean cpp) {
    return getCCompiler(opt,cpp) + " " + getCLinkFlags(opt,cpp);
  }

  /** Get the C/C++ compiler (executable to run).
      Since NeXtMidas 2.9.2, the $NM_CC and $NM_CXX OS environment variables (if set)
      will override the "native.&lt;ostag&gt;.compiler" and "native.&lt;ostag;gt;.cppcomp"
      values in build.props.
      @param opt option tree name to get the C or C++ compiler.
      @param cpp true to get C++ compiler, otherwise gets C compiler.
   */
  private String getCCompiler (String opt, boolean cpp) {
    String compiler;
    if (cpp) {
      compiler = Import.getEnv("NM_CXX");
      if (compiler == null) compiler = getProperty(opt, "native."+Shell.ostag+".cppcomp");
    } else {
      compiler = Import.getEnv("NM_CC");
      if (compiler == null) compiler = getProperty(opt, "native."+Shell.ostag+".compiler");
    }

    return compiler;
  }

  /** Get the C/C++ build flags.
   *  @param opt name of option tree (MUST be in lower-case)
   *  @param cpp true to get C++ build flags, otherwise gets C build flags
   */
  private CharSequence getCBuildFlags (String opt, boolean cpp) {
    String flagsOsType  = getProperty(opt, "flags."+Shell.ostype);
    String cflagsOsType = getProperty(opt, "cflags."+Shell.ostype);
    String osname       = getProperty(opt, "native."+Shell.ostag+".osname");
    String processor    = Shell.getProperty("os.arch");

    String cCompilerLower = getCCompiler(opt, cpp).toLowerCase();
    StringBuilder cflagsNative = new StringBuilder();

    // Add support for grabbing extra Intel ICX cflags (NXM-4011)
    if (cpp) {
      cflagsNative.append(getProperty(opt, "native."+Shell.ostag+".cpp.comp"));
      if (cCompilerLower.equals("icpx")) {
        cflagsNative.append(" "+getProperty(opt, "native."+Shell.ostag+".cpp.comp.llvm")); // Extra flags for Intel LLVM C++ compiler
      }
      if (Shell.iscentos7) {
        cflagsNative.append(" "+getProperty(opt, "native."+Shell.ostag+".cpp.comp.el7")); // Extra flags for C++ compiler on RHEL/CentOS 7
      }
    } else {
      cflagsNative.append(getProperty(opt, "native."+Shell.ostag+".cflags"));
      if (cCompilerLower.equals("icx")) {
        cflagsNative.append(" "+getProperty(opt, "native."+Shell.ostag+".cflags.llvm")); // Extra flags for Intel LLVM C compiler
      }
    }

    // Add extra flags... these are explained in build.props
    StringBuilder retflags = new StringBuilder(512); // use a larger capacity to reduce resizes

    if (processor.equals("i386") || processor.equals("x86")) {
      // Allow unaligned access to 8-byte values (double,long) in native casts (Native.c).
      // Currently only the Intel i386 and x86 processors allow this. Others, like the
      // Sun/SPARC will cause a core dump if 8-byte values are not aligned to 8-byte boundaries.
      retflags.append("-D_ALLOW_64BIT_UNALIGNED ");
    }
    retflags.append("-D_"+osname+" -D_"+shell.getOSRep()+" -D_"+getXmArchType());
    if (!opt.equals("sys")) {
      retflags.append(" -I" + Shell.getOptPath(opt, "inc")); // add option tree's inc area to include path
    }
    // since 3.5.0: put rest of build flags after option tree's include path for BUG 2815
    retflags.append(" " + flagsOsType + " " + cflagsOsType+ " " + cflagsNative);

    return retflags;
  }

  /** Get the C/C++ link flags. */
  private String getCLinkFlags (String opt, boolean cpp) {
    String lflags = getProperty(opt, "lflags."+Shell.ostype);
    String nlflags; // native link flags

    if (cpp) {
      nlflags = getProperty(opt, "native."+Shell.ostag+".cpp.link");
    } else {
      nlflags = getProperty(opt, "native."+Shell.ostag+".lflags");
    }

    return lflags + " " + nlflags;
  }

  /** Gets the Fortran Compiler (with flags). */
  private String getFortranCompiler (String opt) {
    getFortranCompiler();
    String forComp = getProperty(opt,forCompiler+".comp") + " "
        + getProperty(opt, "native."+Shell.ostag+".for.comp");
    if (forCompiler.equals("gfc") && ((gfcForCompiler != null) && !gfcForCompiler.equals(defaultGfcForCompiler))) {
      forComp = (" "+forComp).replaceFirst(" "+defaultGfcForCompiler+" ",gfcForCompiler+" ");
    }
    return forComp;
  }

  /** Gets the Fortran linker (with flags). */
  private String getFortranLinker (String opt) {
    getFortranCompiler();
    String forLinker = getProperty(opt,forCompiler+".link") + " "
        + getProperty(opt, "native."+Shell.ostag+".for.link");
    if (forCompiler.equals("gfc") && ((gfcForCompiler != null) && !gfcForCompiler.equals(defaultGfcForCompiler))) {
      forLinker = (" "+forLinker).replaceFirst(" "+defaultGfcForCompiler+" ",gfcForCompiler+" ");
      if (gfcLibsDir != null) forLinker = forLinker.replaceAll(getProperty(opt,"gfc.libs.dir"),gfcLibsDir);
    }
    return forLinker;
  }

  /** Gets the Fortran build flags. */
  private String getFortranFlags (String opt, String area, String path) {
    String fflags = getProperty(opt, "flags."+Shell.ostype) + " "
                  + getProperty(opt, "fflags."+Shell.ostype)
                  + " -D_"+getFortranCompiler().toUpperCase() // fix for Bug 1682
                  + " -D_"+getOsName(opt)+" -D_"+shell.getOSRep()+" -D_"+getXmArchType()+" ";
    String local  = "-I" + Shell.getOptPath(opt,"inc") + " " + " -I" + path;

    if (!opt.equalsIgnoreCase("sys")) {

      if (opt.equalsIgnoreCase("xm")) {
        fflags = fflags + " " + local; // Always override X-Midas definitions
      } else {
        fflags = local + " " + fflags; // Local messages override XBC's messages
      }
    }

    return fflags;
  }

  /** Gets the Fortran build flags. */
  private String getFortranHostFlags (String opt, String area) {
    return (!area.equals("host"))? "": getProperty(opt, "fflags.host");
  }

  /** Reads in X-Midas CFG file (libraries.cfg or primitives.cfg) under the
      "cfg" area of the specified option tree if it exists. This mirrors the
      functionality of X-Midas's 'xbhost' command (i.e. xbh.py script).

      Note: The flags (cflags, lflags) will be will be appended to the
            compile and link flags from build.props. The remove directives
            will only remove them if they exist in the CFG file and
            will NOT effect the build.props properties.
            Additionally, OS environment variables (i.e. "$") are NOT supported
            because they increases the build complexity and makes it more
            platform specific.
     @param opt       Name of option tree of CFG file to read/load
     @param cfg_fname filename of the CFG (config) file (either "primitives.cfg"
                      or "libraries.cfg"
     @param defaults  default values to use to populate initial build top level
                      configurations.
     @return Map containing the build configuration from the specified CFG file
             or a default set of configuration if CFG does not exist.
     @since NeXtMidas 2.7.0
   */
  private Map<String,Object> read_cfg_file (String opt, String cfg_fname, Map<String,Object> defaults) {
    Map<String,Object> cfg = getDefaultConfig();
    if (defaults != null) cfg.putAll(defaults);

    String filename = Shell.getOptPath(opt, "CFG") + cfg_fname;
    TextFile tf = new TextFile(midas, filename);
    // if (showDebug) System.out.println("read_cfg_file() opt="+opt+" cfgfile="+tf); // DEBUG
    if (!tf.exists()) return cfg;

    cfg.put("optname", opt.toLowerCase()); // save optname if we have CFG file
    cfg.put("cfgfile", filename);

    // platform set (e.g. ["UNIX", "Linux", "LINX", ])
    if (platform.isEmpty()) {
      platform.add(getOsName(opt));    // OS Short Name (e.g. "LINX", "HPUX", "SOL", "OSF")
      platform.add(shell.getOSType()); // OS Type (e.g. "UNIX", "DOS")
      platform.add(shell.getOSName()); // OS Name (e.g. "Linux", "SunOS", "Windows XP")
      platform.add("NEXTMIDAS");       // allows conditional directive for NEXTMIDAS
    }

    tf.open(TextFile.INPUT|TextFile.FORCEABORT);

    parseCfgFile(opt, cfg, defaults, tf, platform);
    resolve_libdepend(cfg);
    // if (showDebug) System.out.println("read_cfg_file() cfg="+ cfg); // DEBUG

    tf.close();

    return cfg;
  }

    /** Get a default build configuration Map for parsing CFG file. */
  private Map<String,Object> getDefaultConfig () {
    Map<String,Object> cfg = new LinkedHashMap<>();
    cfg.put("cfgfile",    null);                     // cfg filename (String)
    cfg.put("cflags",     new ArrayList<>());  // compiler flags
    cfg.put("lflags",     new ArrayList<>());  // linker flags
    cfg.put("libdepend",  new ArrayList<>());  // libraries from option trees
    cfg.put("linklibs",   "");                       // libraries to link (String)
    cfg.put("searchpath", "");                       // Search path (String)
    cfg.put("targets",    new LinkedHashMap<>()); // map of build targets configs
    cfg.put("targetlist", new ArrayList<>());  // list of build targets
    return cfg;
  }

  /** Create a new CFG Config Map for the specified target, based on the
      given top level configuration (cfg). Then add the target to the
      cfg's "targetlist" and "target" map.
      @param target name of target
      @param cfg top level CFG configuration to get settings from.
      @since NeXtMidas 2.7.0
   */
  @SuppressWarnings("unchecked") // NTN: matches type in getDefaultConfig()
  private Map<String,Object> newTargetConfig (String target, Map<String,Object> cfg) {
    Map<String,Object> c = new HashMap<>();
    c.put("cfgfile",    cfg.get("cfgfile"));
    c.put("cflags",     ((ArrayList<String>)cfg.get("cflags")).clone());
    c.put("lflags",     ((ArrayList<String>)cfg.get("lflags")).clone());
    c.put("libdepend",  ((ArrayList<String>)cfg.get("libdepend")).clone());
    c.put("libdefault", ((ArrayList<String>)cfg.get("libdefault")).clone());
    c.put("linklibs",   cfg.get("linklibs"));
    c.put("searchpath", cfg.get("searchpath"));
    c.put("modules",    new ArrayList<>());
    c.put("headers",    new ArrayList<>());
    c.put("target",     target); // save target name

    ArrayList<String> targetlist = (ArrayList<String>) cfg.get("targetlist");
    Map<String,Map<String,Object>> targets = (Map<String,Map<String,Object>>) cfg.get("targets");
    targetlist.add(target); // add to cfg's target list
    targets.put(target, c); // add to cfg's targets map
    return c;
  }

  /** Parse CFG file line by line and put results into cfg config Map.
      @param cfg      Map<String,Object> to hold the top level build configuration
      @param defaults Map<String,Object> with default values to use to populate
                      initial configurations Maps (TBD)
      @param tf       TextFile to CFG file to parse
      @param platform Set<String> with the current platform for this machine used
                      for conditional directives (via "->") in CFG files.
      @since NeXtMidas 2.7.0
   */
  @SuppressWarnings("unchecked")
  private void parseCfgFile (String optname, Map<String,Object> cfg, Map<String,Object> defaults, TextFile tf, Set<String> platform) {
    String line;
    int line_no = 0;
    int indent = 0;
    Map<String,Object> c = cfg; // start current config at top level cfg
    String directive;
    String target;
    Object ignoreTarget = null;
    int endIndex;
    int mindent = 0;
    ArrayList<String> modules = new ArrayList<>();

    // Keep track of the default library dependencies, so that users may ask
    // to turn them off explicitly (e.g. 'libdepend - XMIDAS;SYS'), but they
    // will otherwise remain in the libdepend list.
    cfg.put("libdefault", ((ArrayList<String>)cfg.get("libdepend")).clone());

    while ( (line=tf.read()) != null ) {
      line_no++;
      String trimmedline = line.trim();
      int trimlen = trimmedline.length();
      if (trimlen == 0 || trimmedline.startsWith("!") ) continue; // ignore comment lines or empty lines
      indent = StringUtil.rtrim(line).length() - trimlen;         // get indent level
      if (indent == 0) { c = cfg;  }                              // back to top level configuration
      if (indent <= mindent) {
        // For module-specific cflags, (cflags directives following a modules
        // directive that ends in a ':' that are indented further than that
        // modules directive) mindent will be set to the indent level of that
        // last modules statement. Once any line is no longer indented further
        // than it, we are no longer gathering module-specific cflags for its modules.
        mindent = 0;
      }

      // System.out.println(line_no+": "+line); // DEBUG
      String[] tokens = WHITESPACE_REGEX.split(trimmedline);
      endIndex = tokens.length;

      // if we have a non-target directive with a conditional
      if (tokens.length >= 3 && tokens[tokens.length-2].equals("->")
          && !tokens[0].endsWith(":")) {
        if (inExcludeDirective(tokens[tokens.length-1], platform)) continue;
        endIndex -= 2; // ignore last two conditional tokens
      }
      directive = tokens[0].toLowerCase();
      if (indent == 0 && directive.endsWith(":")) {   // have target directive
        target = directive;
        // if we have a target directive with a conditional
        if (tokens.length >= 3 && tokens[tokens.length-2].equals("->") &&
            inExcludeDirective(tokens[tokens.length-1], platform) ) {
          ignoreTarget = target; // tag to ignore all directives indented under this target directive
          continue;
        } else {
          ignoreTarget = null;
          c = newTargetConfig(target.substring(0, target.length()-1), cfg);
        }
      } else if (ignoreTarget != null) {
        continue;
      } else if (directive.equals("optdepend")) {
        // This directive has NO EFFECT on the build.
        // Document the OPTION tree(s) that should be on the path when GENMSGINC
        // generates the configured messages.
        c.put("optdepnd", parseDirectiveList(tokens, 1, endIndex, false));
      } else if (directive.equals("libdepend")) {
        // Builds up a list of Midas option tree and 3rd party libraries to link
        // the target against. The "inc" directory of each option tree
        // represented in the list is also added to the search path used during
        // the compilation pahse (if it exists).
        // libdepend [+/-] [[[<lib>|*];]<opt> ... ] [+/-]
        // <target>:
        //   [libdepend [+/-] [[<lib>|*];]<opt> ... ] [+/-]
        ArrayList<String> dependlist = parseDirectiveList(tokens, 1, endIndex, false);
        ArrayList<String> libdepend  = new ArrayList<>();
        for (String arg : dependlist) {
          // Accept ;<opt> <lib>;<opt>, *;<opt> and <opt> in the file
          // but store it always as [<lib>];<opt>.
          String lib = null;
          String opt = null;
          if ("+".equals(arg) || "-".equals(arg)) {
            libdepend.add(arg);
            continue;
          } else if (arg.indexOf(";") >= 0) {
            String[] tmp= SEMICOLON_REGEX.split(arg);
            lib = tmp[0];
            if (tmp.length < 2 ) opt = optname;
            else opt = tmp[1].toUpperCase();
          } else {
            lib = "*";
            opt = arg.toUpperCase();
          }

          if (!lib.equals("*")) {
            libdepend.add(lib+";"+opt);
          } else {
            // for *;<opt> then expand out all libraries target for specified option tree
            Map<String,Object> optcfg = getLibrariesConfig(opt, defaults);
            ArrayList<String> targetlist = (ArrayList<String>) optcfg.get("targetlist");
            int count = 0;
            if (targetlist != null) {
              for (String targetname : targetlist) {
                libdepend.add(targetname + ";" + opt);
                count++;
              }
            }
            if (count == 0) libdepend.add(";"+opt); // if no libraries, still add ";<opt>" to pick up include dir
          }
        }

        ArrayList<String> cfg_libdepend  = (ArrayList<String>) c.get("libdepend");
        ArrayList<String> cfg_libdefault = (ArrayList<String>) c.get("libdefault");
        if (libdepend.isEmpty()) { // clear the library dependecies
          cfg_libdepend.clear();
          cfg_libdefault.clear(); // assume that user does not want the default libraries either
        } else if (libdepend.contains("-")) {
          // Explicitly removing library dependencies; remove from the default
          // library list. Assume that the user is doing this intentionally
          for (String libname : libdepend) {
            if (cfg_libdefault.contains(libname)) {
              cfg_libdefault.remove(libname);
            }
          }
        } else if (! libdepend.contains("+")) {
          // Unconditionally set library dependencies; make sure that
          // the default libraries do not get wiped out.
          for (String libname : cfg_libdepend) {
            if (! libdepend.contains(libname)) {
              libdepend.remove(libname);
            }
          }
        }
        modList(cfg_libdepend, libdepend.toArray(EMPTY_STRING_ARRAY), 0, libdepend.size());
        warnDuplicateLibs(c);
      } else if (directive.equals("lflags")) {
        modList((ArrayList<String>)c.get("lflags"), tokens, 1, endIndex);
        warnDuplicateLibs(c);
      } else if (directive.equals("cflags")) {
        // If a library or primitive is being defined and cflags directives are
        // interspersed with modules directives it indicates that different
        // modules need to be compiled with different compile flags.  Add an
        // entry for each module indicating what its cflags should be.
        if (c.containsKey("modules") || mindent > 0) {
          HashMap<String,List<String>> module_cflags = (HashMap<String,List<String>>) c.get("module_cflags");
          if (module_cflags == null) {
            module_cflags = new HashMap<>();
            c.put("module_cflags", module_cflags);
          }
          if (mindent > 0) {
            // A modules directive was added with module-specific
            // cflags, these cflags apply only to those entries.
            for (String m : modules) {
              ArrayList<String> cflaglist = (ArrayList<String>) module_cflags.get(m);
              if (cflaglist == null) {
                cflaglist = (ArrayList<String>) ((ArrayList<String>)c.get("cflags")).clone();
                module_cflags.put(m, cflaglist);
              }
              modList(cflaglist, tokens, 1, endIndex);
            }
            continue;
          }
          ArrayList<String> c_modules = (ArrayList<String>) c.get("modules");
          for (String m : c_modules) {
            if (! module_cflags.containsKey(m)) {
              ArrayList<String> arrayList = (ArrayList<String>)c.get("cflags");
              module_cflags.put(m, (ArrayList<String>)arrayList.clone());
            }
          }
        }
        modList((ArrayList<String>)c.get("cflags"), tokens, 1, endIndex);
      } else if (directive.equals("libname") && c.containsKey("modules")) {
        // libraries.cfg only - Indicates the desired root name of the resulting
        // library file. Only needed if it does not match the standard
        // "lib<libname>_<ostag>.so" format. Libraries that are renamed in
        // this way are **NOT** available to other Midas options to link against
        // via the "libdepend" directive (though you can still link to against
        // them by adding explicit "lflags" directives).
        c.put("libname", tokens[1]);
      } else if (directive.equals("modules") && c.containsKey("modules")) {
        // Associates code files with a particular target.
        modules = parseDirectiveList(tokens, 1, endIndex, false);
        int lastItemIndex = modules.size()-1;
        String lastItem = (lastItemIndex >= 0)? modules.get(lastItemIndex) : "";
        if (lastItem.endsWith(":")) {
          // If a modules directive ends in a ':' then it may be followed by
          // module-specific cflags directives.  Record the indent level of the
          // modules directive and any cflags directives that occur at a greater
          // indent level before the next modules statement will apply only to
          // the modules in this directive.
          mindent = indent;
          // Get rid of the ':' from the modules list we just got
          // Could be by itself, could be attached to the last module name.
          if (lastItem.equals(":")) modules.remove(lastItemIndex);
          else modules.set(lastItemIndex, lastItem.substring(0, lastItem.length()-1));
        } else {
          mindent = 0;
        }
        ((ArrayList<String>)c.get("modules")).addAll(modules);
      } else if (directive.equals("headers") && c.containsKey("modules")) {
        // This directive has NO EFFECT on the build.
        // Document associated header files with a particular target.
        ((ArrayList<String>)c.get("headers")).addAll(parseDirectiveList(tokens, 1, endIndex, false));
      } else {
        midas.warning("Unrecognized/unexpected directive '"+directive
            +"' at line "+line_no+" of file "+tf.getName()+ " indent="+indent+" mindent="+mindent);
      }
    } // end read CFG file loop
  }

  /** Process conditional targets and directives in primitives.cfg and libraries.cfg.
      Conditional targets and directives are based on OSNAME or OSTYPE, or the
      presence of an OS environment variable using the '->' arrow syntax.
      NOTE: conditionals are comma separated (w/o whitespace).
      <pre>E.g.:

      cflags + -O1 -> LINX,SOL
      lflags + -a  -> !SOL
      lflags + -f  -> $PRESENCE_OF_OS_ENV_VAR   (since 3.1.2)
      </pre>
      @param dlist    directive and conditional targets list
      @param platform Set containing current system's platforms (ostype, osname)
      @return true if the conditional evaluates to true, i.e. the directive
              should be NOT excluded, or false otherwise.
      @since NeXtMidas 2.7.0
   */
  private boolean inExcludeDirective (String dlist, Set<String> platform) {
    boolean result = true;
    if (dlist.startsWith("!")) {
      // logical negation; evaluate the rest of the directive first
      return ! inExcludeDirective(dlist.substring(1), platform);
    }

    String[] items = COMMA_REGEX.split(dlist);
    for (String item : items) {
      if (item.startsWith("$")) { // since 3.1.2: evaulate for existence of OS Environment variable
        if (Import.getEnv(item.substring(1)) != null) return false;
      }
      else if (platform.contains(item)) return false;
    }
    return result;
  }

  /** cflags, lflags, or libdepend mod list to process + (add), - (remove),
      otherwise defaults to replace. if mods is null, then just clear list.
      @param list list to update
      @param mods String array of modifications to make to list
      @param start starting index in mods array. normally 0 or 1 (to skip
                   directive element in mods array)
      @param end   ending index in mods array usually the ending index of the
                   mods array (or two less if there is a conditional)
      @since NeXtMidas 2.7.0
   */
  private void modList (ArrayList<String> list, String[] mods, int start, int end) {
    if (mods == null || mods.length == 0) { // clear
      list.clear();
    } else if (mods[start].equals("+")) {  // prepend (skip leading "+")
      for (int i=(start+1); i<end; i++) {
        list.add(0, mods[i]);
      }
    } else if (mods[end-1].equals("+")) {  // append (skip ending "+")
      for (int i=start; i<end-1; i++) {
        list.add(mods[i]);
      }
    } else if (mods[start].equals("-")) {  // delete (skip leading "-")
      for (int i=(start+1); i<end; i++) {
        list.remove(mods[i]);
      }
    } else if (mods[end-1].equals("-")) {  // delete (skip ending "-")
      for (int i=start; i<end-1; i++) {
        list.remove(mods[i]);
      }
    } else {                               // replace
      list.clear();
      for (int i=start; i<end; i++) {
        list.add(mods[i]);
      }
    }
  }

    /** Parses a line that is meant to be a list and allows comma
        separation OR space separation or both.
        @param tokens String array of tokens in line to parse
        @param start  start index into tokens to parse
        @param end    end index into tokens array to parse
        @param upcase true to upper case items before adding to return list
        @return ArrayList of the items in tokens separated by spaces and/or commas
        @since NeXtMidas 2.7.0
   */
  private ArrayList<String> parseDirectiveList (String[] tokens, int start, int end, boolean upcase) {
    ArrayList<String> list = new ArrayList<>(tokens.length);
    for (int ii=start; ii<end; ii++) {
      String[] strarr = COMMA_REGEX.split(tokens[ii]);
      // String[] strarr = StringUtil.COMMA_OR_WHITESPACE_REGEX.split(tokens[ii]); // TODO: for future?
      for (String item : strarr) {
        if (upcase) item = item.toUpperCase();
        list.add(item);
      }
    }
    return list;
  }

  /** Search for duplicate archive names that may be present in all libdepend
      and lflags directives.
      @param cfg build configuration Map to check against.
      @since NeXtMidas 2.7.0
   */
  @SuppressWarnings("unchecked")
  private void warnDuplicateLibs (Map<String,Object> cfg) {
    HashSet<String> warnset   = new HashSet<>();
    ArrayList<String> libdepend = (ArrayList<String>) cfg.get("libdepend");
    ArrayList<String> lflags    = (ArrayList<String>) cfg.get("lflags");
    HashSet<String> libs = new HashSet<>();
    for (String str : libdepend) {
      int iSemiColon = str.indexOf(SEMICOLON);
      if (iSemiColon > 1) {       // has libname (before semi-colon)
        String libname = str.substring(0,iSemiColon).toLowerCase(); // get libname and lower case it
        if (!libs.add(libname)) { // add to libs set, but if already in libs
          warnset.add(libname);   // then add to warnset
        }
      }
    }
    for (String curflag : lflags) {
      if (curflag.startsWith("-l")) {  // if library flag
        String libname = curflag.substring(2).toLowerCase(); // skip pass "-l" and lower case
        if (!libs.add(libname)) {      // add to libs set, but if already in libs
          warnset.add(libname);        // then add to warning set
        }
      }
    }

    String cfgfile = (String) cfg.get("cfgfile");
    String target  = (String) cfg.get("target");
    HashSet<String> warned = (HashSet<String>) cfg.get("warned");
    if (warned == null) {
      warned = new HashSet<>();
      cfg.put("warned", warned);
    }
    for (String libname : warnset) {
      if (!warned.contains(libname)) { // only warn once for same lib name
        midas.warning("Multiple \"-l"+libname+"\" flags for target '"+target+"' in: "+cfgfile);
        warned.add(libname);
      }
    }
  }

  /** Given a CFG configuration, go through each item in the "libdepend" list
      and uniquely add:
        -L<optpath>/lib -l<lib><library_suffix> to the "linklibs" string list <br />
        <optpath>/inc to the "searchpath" string list
      then recursively do so for all contained targets.
      @param cfg Configuration map to resolve the "linklibs" and "searchpath"
                 and store into.
      @since NeXtMidas 2.7.0
   */
  @SuppressWarnings("unchecked")
  private void resolve_libdepend (Map<String,Object> cfg) {
    ArrayList<String> libdependList = (ArrayList<String>) cfg.get("libdepend");
    LinkedHashSet<String> linklibs   = new LinkedHashSet<>(); // using LinkedHashSet to keep insertion order
    LinkedHashSet<String> searchpath = new LinkedHashSet<>(); // using LinkedHashSet to keep insertion order

    for (String lib : libdependList) {
      int iSemicolon = lib.indexOf(SEMICOLON);
      if (iSemicolon >= 0) {
        String optname = lib.substring(iSemicolon+1);   // +1 to skip pass semi-colon
        String libname = lib.substring(0, iSemicolon);
        String libpath, incDir, includeDir;
        boolean is3rdPartyLib = false;

        if (optname.startsWith("$")) { // since 3.1.2 for BUG 2429
          // when an OS environment substitution is given for the option tree name (e.g. $TMS)
          // then assume this is a non Midas package (aka it is a third party library).
          // This is similar to X-Midas's xbh.py resolve_third_party_paths function
          is3rdPartyLib = true;
          String envName = optname.substring(1);
          String pathFromOsEnv = Import.getEnv(envName, envName + "__OS_ENV_NOT_FOUND__");
          libpath    = FileName.addPathDir(pathFromOsEnv, "lib");
          incDir     = FileName.addPathDir(pathFromOsEnv, "inc");
          includeDir = FileName.addPathDir(pathFromOsEnv, "include");

          if (!CoreIO.fexists(includeDir,true) || !CoreIO.fexists(incDir,true)) { // if neither $OSENV/include nor $OSENV/inc exists
            if (!CoreIO.fexists(libpath,true)) { // and if $OSENV/lib does not exists
              File file = new File(pathFromOsEnv);
              if (file.isDirectory()) {          // finally, if $OSENV directory exits check for Python, Tkl like 3rd party libraries
                File parent = file.getParentFile();
                String grandParentDir = parent.getParent();
                String productDir = parent.getName();
                if (productDir.equals("include") || productDir.equals("lib")) {
                  includeDir = FileName.addPathDir(grandParentDir, "include" + File.separator + productDir);
                  includeDir = FileName.addPathDir(grandParentDir, "lib"     + File.separator + productDir);
                }
              }
            }
          }
          // end since 3.1.2 for BUG 2429
        } else {
          libpath    = Shell.getOptPath(optname, "lib");
          incDir     = Shell.getOptPath(optname, "inc");
          includeDir = Shell.getOptPath(optname, "include");
        }

        linklibs.add("-L"+ libpath);
        if (libname.length() > 0 && libname.indexOf("*") < 0 ) {
          if (is3rdPartyLib) linklibs.add("-l"+ libname); // since 3.1.2 for BUG 2429
          else               linklibs.add("-l"+ libname+Shell.getLibrarySuffix());
        }
        File incFile      = new File(incDir);
        File includeFile  = new File(includeDir);
        if (incFile.isDirectory())     searchpath.add("-I"+ incDir);
        if (includeFile.isDirectory()) searchpath.add("-I"+ includeDir);
      } else {
        midas.warning("CFG configuration for libdepend has not be normalized! ignoring " + lib);
      }
    } // end for loop over libdepend list

    ArrayList<String> lflagsList = (ArrayList<String>) cfg.get("lflags");
    for (Iterator<String> lflagsIter = lflagsList.iterator(); lflagsIter.hasNext();) {
      // Move all the -l and -L flags from "lflags" to the end of "linklibs".
      // It is ok to do this here b/c we know we're done reading the "lflags" directives.
      String lflag = lflagsIter.next();
      if (lflag.startsWith("-l") || lflag.startsWith("-L")) {
        linklibs.add(lflag);      // add to "linklibs" list and
        lflagsIter.remove();      // remove from original "lflags" list
      }
    }
    cfg.put("linklibs", converToString(linklibs));
    cfg.put("searchpath", converToString(searchpath));

    // Recursively resolve our "targets" libdepends
    Map<String,Map<String,Object>> targets = (Map<String,Map<String,Object>>) cfg.get("targets");
    if (targets != null) {
      for (Iterator<Map<String,Object>> iter = targets.values().iterator(); iter.hasNext();) {
        resolve_libdepend(iter.next());
      }
    }
  }

  /** Converts a Collection of flags/linklibs/searchpath into a space separated
      String with a leading space.
      @return Space separated string of the specified input collection
      @since NeXtMidas 2.7.0
   */
  private String converToString (Object collection) {
    StringBuilder sb = new StringBuilder();
    if (collection instanceof Collection) {
      Collection<?> list = (Collection<?>) collection;
      for (Iterator<?> iter = list.iterator(); iter.hasNext();) {
        sb.append(" ").append(iter.next());
      }
    } else if (collection != null) {
      sb.append(" ").append(collection);
    }
    return Import.expandEnvVars(sb.toString()); // since 3.1.2: expand OS env variables for BUG 2429
  }

  /** Get compile or link flags from specified CFG config for the specified
      target. If flagname is "cflags", will also include the target's "searchpath"
      (i.e. "-I<optpath>/inc ..."). If flagname is "lflags", will  also include
      the target's "linklibs" (i.e. "-L<optpath>/lib -l<libname> ..").
      @param opt        option tree name
      @param area       area of target name (e.g. host, lib)
      @param targetname target name to find in config
      @param flagname   "cflag" for compile flags or "lflag" for link flags
      @return Compile or Link flags for the specified target (based on input config)
              with a leading space. If no matching target is found or config is null,
              then returns an empty string.
      @since NeXtMidas 2.7.0
   */
  @SuppressWarnings("unchecked")
  private String getFlagsFromConfig (String opt, String area, String targetname, String flagname) {
    StringBuilder sb = new StringBuilder(256);
    Map<String,Object> topLevelConfig; // top level CFG config map

    if (area.equals("host")) { topLevelConfig = primConfig; }
    else                     { topLevelConfig = getLibrariesConfig(opt, null); }

    if (topLevelConfig != null ) {
      Map<String,Object> cfg;
      Map<String,?> targetsMap = (Map<String,?>) topLevelConfig.get("targets");
      if (targetsMap == null) {
        cfg = topLevelConfig;  // don't have targetsMap, use top level config
      }
      else {
        cfg = (Map<String, Object>) targetsMap.get(targetname);                // 1st. try with targetname
        if (cfg == null) {
          cfg = (Map<String, Object>) targetsMap.get(targetname.toLowerCase());// 1.b  try with targetname (lower case)
        }
        if ((cfg == null) && targetname.startsWith("lib")) {
          cfg = (Map<String, Object>) targetsMap.get(targetname.substring(3)); // 2nd. try without "lib" prefix
        }
        if (cfg == null) cfg = topLevelConfig;                                 // 3rd. fall back to top level config
      }
      sb.append(converToString(cfg.get(flagname)));
      if (flagname.equals("cflags")) {
        Object searchpath = cfg.get("searchpath");
        if (searchpath != null) sb.append(" ").append(searchpath);
      } else if (flagname.equals("lflags")) {
        String rpath_option = getProperty(opt, "rpath_option");
        String linklibs = (String) cfg.get("linklibs");
        sb.append(expand_rpath_opts(linklibs, rpath_option));
        if (linklibs != null) sb.append(" ").append(linklibs);
      }
    }
    return sb.toString();
  }

  /** Expands the rpath entry for the specified "linklibs" by prepending a
      a space followed by the specified rpath_option then the path (extracted
      from each "linklib" entry that begins with "-L".
      @param linklibs     the "linklibs" options (e.g. -l<libname> -L<path_to_lib> ..."
      @param rpath_option the rpath_option to specify rpath entry to linker
      @return StringBuilder of the expanded rpath entries. An empty StringBuilder
              is returned when the rpath_option is null or is empty or if the
              "linklibs" has no "-L" entries.
      @since NeXtMidas 2.7.1
   */
  private StringBuilder expand_rpath_opts (String linklibs, String rpath_option) {
    StringBuilder sb = new StringBuilder(linklibs.length());
    if (rpath_option==null || rpath_option.trim().length()==0) return sb;
    StringTokenizer st = new StringTokenizer(linklibs);
    while (st.hasMoreTokens()) {
      String token = st.nextToken();
      if (token.startsWith("-L") && token.length()>2) {
        sb.append(" " + rpath_option + token.substring(2));
      }
    }
    return sb;
  }

  /** Gets libraries CFG configuration for specified option tree.
      Will attempt to get CFG from libConfigMap (cache of previously read and
      parsed and configuration). Otherwise read and parse the "libraries.cfg"
      for the specified option tree and return it.
      @param optname The option tree where the libraries
      @since NeXtMidas 2.7.0
   */
  private Map<String,Object> getLibrariesConfig(String optname, Map<String,Object> defaults) {
    Map<String,Object> cfg = libConfigMap.get(optname);
    if (cfg == null) {
      cfg = read_cfg_file(optname, "libraries.cfg", defaults);
      libConfigMap.put(optname, cfg);
    }
    return cfg;
  }

  /** @return JavaCompiler instance
      @throws MidasException if enable to obtain system java compiler (i.e. not running with JDK)
      @since NeXtMidas 3.3.0 use JDK6 API
   */
  private synchronized JavaCompiler getJavaCompiler () throws MidasException {
    if (compiler == null) {
      compiler = ToolProvider.getSystemJavaCompiler();
      if (compiler == null) {
        throw new MidasException("Unable to get Java compiler! Not using JDK?");
      }
    }
    return compiler;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Filters files based on a list of accepted file name extensions. */
  public static final class BuildFileFilter implements FilenameFilter, FileFilter {
    private final String[] extList; // List of allowable extensions

    /** Creates a new filter with the allowable extensions. For example ext=".for|.fpp|.f". */
    private BuildFileFilter(String ext) {
      extList = ext.split("[|]");
    }

    public boolean accept(File path) {
      return accept(path.getName());
    }

    public boolean accept(File dir, String name) {
      return accept(name);
    }

    /** Accepts all file names ending with an allowable file extension.
        @param name the file name.
        @return true if file ends in an allowable file extension, false otherwise.
     */
    public boolean accept(String name) {
      boolean accept = false;
      for (int i = 0; !accept && (i < extList.length); i++) {
        accept = name.endsWith(extList[i]);
      }
      return accept;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Deprecated Methods
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Sets the default midas context to use in BUILD.
      @param midas The Midas context to use.
   */
  @InternalUseOnly
  public static void setMidasContext (Midas midas) {
    defaultMidas = midas;
  }

  /** DEPRECATED: Only used by deprecated methods. */
  @InternalUseOnly
  private static Build newInstance (Midas midas, boolean verbose, Table bldFlags) {
    return newInstance(midas,verbose,defaultShowDebug,defaultKeepFiles,bldFlags);
  }

}
