/*******************************************************************************
* Copyright (c) 2012 Arapiki Solutions Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* psmith - initial API and
* implementation and/or initial documentation
*******************************************************************************/
package com.buildml.main;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import com.buildml.config.PerTreeConfigFile;
import com.buildml.model.BuildStoreFactory;
import com.buildml.model.BuildStoreVersionException;
import com.buildml.model.IBuildStore;
import com.buildml.model.IFileMgr;
import com.buildml.model.IPackageMgr;
import com.buildml.model.IPackageRootMgr;
import com.buildml.utils.errors.ErrorCode;
import com.buildml.utils.print.PrintUtils;
import com.buildml.utils.string.StringArray;
/**
* Main entry point for the "bml" command, used to invoke a build operation.
*
* @author Peter Smith <psmith@arapiki.com>
*/
public class BMLMain {
/*=====================================================================================*
* FIELDS/TYPES
*=====================================================================================*/
/** name of the build.bml file to search for */
private static final String DATABASE_FILE_NAME = "build.bml";
/** name of the per-tree configuration file to search for (or create) */
private static final String CONFIG_FILE_NAME = ".bmlconf";
/** The BuildStore used when performing the build */
private IBuildStore buildStore = null;
/** The per-tree configuration file - contains aliases and root paths, etc. */
private PerTreeConfigFile configFile = null;
/*=====================================================================================*
* PUBLIC METHODS
*=====================================================================================*/
/**
* Starting point for execution.
* @param args The standard command line array.
*/
public static void main(String[] args) {
new BMLMain().invokeCommand(args);
}
/*=====================================================================================*
* PRIVATE METHODS
*=====================================================================================*/
/**
* Display an error message, then completely exit from the program.
* @param message The error message to be displayed (possibly null).
*/
private void showUsageAndExit(String message) {
if (message != null) {
System.err.println(message);
}
System.err.println();
System.err.println("Usage: bml <alias> | { <pkg-name> ... } - Build the specified packages.");
System.err.println(" bml -h - Show this help page.");
System.err.println(" bml -l - List available packages and aliases.");
System.err.println(" bml -r - Show file system root path mappings.");
System.err.println(" bml -s <root> <path> - Set a root path mapping.");
System.err.println(" bml -d <root> - Delete a root path mapping.");
System.err.println(" bml -a <alias> <pkg-name> ... - Define a package alias.");
System.err.println(" bml -u <alias> - Undefine a package alias.");
System.err.println("\nIn addition, the following global options can be used in combination with");
System.err.println("the options shown above:\n");
System.err.println(" -f <bml-file> - Specify path to .bml file");
System.err.println();
System.exit(-1);
}
/*-------------------------------------------------------------------------------------*/
/**
* Display an error message to the stderr, and exit the program.
*
* @param message The fatal error message to display.
*/
private void fatal(String message) {
System.err.println(message);
System.exit(-1);
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoke the "bml" with the specified arguments.
*
* @param args The command line argument string.
*/
private void invokeCommand(String[] args) {
int argPos = 0;
if ((args.length > 0) && (args[0].equals("-h"))) {
showUsageAndExit(null);
}
/* open the BuildStore and the per-tree configuration */
argPos = openDatabases(args, argPos);
/* now parse the remaining arguments */
if (args.length > argPos) {
String option = args[argPos];
/* if the option starts with '-', it's likely to be a flag */
if (option.startsWith("-")) {
if (option.length() == 2) {
argPos++;
switch (option.charAt(1)) {
case 'l':
listPackages(args, argPos);
break;
case 'a':
doAddAlias(args, argPos);
break;
case 'u':
doUnAlias(args, argPos);
break;
case 'r':
listRoots(args, argPos);
break;
case 's':
setRoot(args, argPos);
break;
case 'd':
removeRoot(args, argPos);
break;
default:
showUsageAndExit("Invalid option: " + option);
break;
}
} else {
showUsageAndExit("Invalid option: " + option);
}
}
/* else, invoke a build with the user-supplied package names */
else {
doBuild(args, argPos);
}
}
/* else perform "default" build */
else {
doBuild(new String[] { "default" }, 0);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Open the BuildStore database, and the per-tree configuration file. This method
* reports errors (and terminates the program) if there are any errors.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
* @return The index into args of the next arguments to be processed.
*/
private int openDatabases(String[] args, int firstArg) {
File pathToDatabase = null;
File pathToConfiguration = null;
/* if the -f option is given that's the location of the build.bml file */
if ((args.length > 0) && args[firstArg].equals("-f")) {
if (args.length == 1) {
showUsageAndExit("The -f option requires a path name to the build.bml file");
} else {
pathToDatabase = new File(args[firstArg + 1]).getAbsoluteFile();
if (!pathToDatabase.exists()) {
fatal("File \"" + args[firstArg + 1] + "\" does not exist.");
}
firstArg += 2;
}
}
/* else, search for the build.bml file in this directory or its ancestors */
else {
pathToDatabase = searchForBMLFile();
if (pathToDatabase == null) {
fatal("No build.bml file could be found in this directory, or a parent directory.");
}
}
/* attempt to open the build.bml file */
try {
buildStore = BuildStoreFactory.openBuildStore(pathToDatabase.toString());
} catch (FileNotFoundException e) {
fatal("File \"" + pathToDatabase + "\" can't be opened as a BuildML database.");
} catch (IOException e) {
fatal("File \"" + pathToDatabase + "\" has I/O problems.");
} catch (BuildStoreVersionException e) {
fatal(e.getMessage());
}
/* now we have an open BuildStore, let's open the configuration file */
File configDir = pathToDatabase.getParentFile();
if (configDir == null) {
fatal("Unable to determine directory to contain " + CONFIG_FILE_NAME);
}
pathToConfiguration = new File(configDir, CONFIG_FILE_NAME);
try {
configFile = new PerTreeConfigFile(buildStore, pathToConfiguration);
} catch (IOException e) {
fatal("Unable to open configuration file: " + pathToConfiguration +
". " + e.getMessage());
}
/* load some of the content from the configuration file into the BuildStore */
loadMappingsIntoBuildStore(configFile, buildStore);
/* all is OK... return the position of the next command line argument */
return firstArg;
}
/*-------------------------------------------------------------------------------------*/
/**
* Load the package root mappings that are stored in the configuration file and set
* them into the BuildStore. The BuildStore doesn't persist these mappings (they're
* per-user settings), so they need to be explicitly loaded each time we run "bml".
*
* @param configFile The configuration file to load the mappings from.
* @param buildStore The BuildStore to load them into.
*/
private void loadMappingsIntoBuildStore(PerTreeConfigFile configFile,
IBuildStore buildStore) {
IPackageMgr pkgMgr = buildStore.getPackageMgr();
/*
* For each package (except for <import>), check if the config file contains a
* mapping for each of the package roots. If so, copy the mapping to the BuildStore.
*/
String packages[] = pkgMgr.getPackages();
for (int i = 0; i < packages.length; i++) {
int pkgId = pkgMgr.getId(packages[i]);
if (pkgId != pkgMgr.getImportPackage()) {
loadOneMapping(packages[i] + "_src", pkgId,
IPackageRootMgr.SOURCE_ROOT, configFile, buildStore);
loadOneMapping(packages[i] + "_gen", pkgId,
IPackageRootMgr.GENERATED_ROOT, configFile, buildStore);
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper function for loading a single package root mapping from the configuration
* file into the BuildStore.
*
* @param rootName Name of the root to be mapped.
* @param pkgId Package ID associated with this root.
* @param type SOURCE_ROOT or GENERATED_ROOT.
* @param configFile Configuration file to load from.
* @param buildStore BuildStore to load into.
*/
private void loadOneMapping(String rootName, int pkgId, int type,
PerTreeConfigFile configFile, IBuildStore buildStore) {
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
String nativePath = configFile.getNativeRootMapping(rootName);
if (nativePath != null) {
/*
* Assuming that the config file object has already validated the content
* of the configuration file, there's no need to check for errors.
*/
pkgRootMgr.setPackageRootNative(pkgId, type, nativePath);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* @return The file system path of the nearest enclosing build.bml file, or null if
* none could be found.
*/
private File searchForBMLFile() {
/* loop upwards from CWD to /, until we find a directory containing build.bml */
File thisDir = new File(".").getAbsoluteFile();
do {
File bmlFile = new File(thisDir, DATABASE_FILE_NAME);
if (bmlFile.exists()) {
return bmlFile;
}
thisDir = thisDir.getParentFile();
} while (thisDir != null);
/* reach top of file system without finding the file */
return null;
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -l (list packages) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void listPackages(String args[], int firstArg) {
IPackageMgr pkgMgr = buildStore.getPackageMgr();
String packages[] = pkgMgr.getPackages();
if (args.length != firstArg) {
showUsageAndExit("Excessive arguments after -l option.");
}
System.out.println("\nValid package names:\n");
/* don't print the <import> package, since we can't build that */
int importPkgId = pkgMgr.getImportPackage();
String importPkgName = pkgMgr.getName(importPkgId);
/* print all the remaining package names */
for (int i = 0; i < packages.length; i++) {
String thisName = packages[i];
if (!thisName.equals(importPkgName)) {
System.out.println(" " + thisName);
}
}
String aliases[] = configFile.getAliases();
if (aliases.length == 0) {
System.out.println("\nNo aliases defined.");
}
else {
System.out.println("\nAliases defined:\n");
int maxLength = StringArray.maxStringLength(aliases) + 3;
for (int i = 0; i < aliases.length; i++) {
System.out.print(" " + aliases[i]);
PrintUtils.indent(System.out, maxLength - aliases[i].length());
String pkgNames[] = configFile.getAlias(aliases[i]);
for (int j = 0; j < pkgNames.length; j++) {
System.out.print(pkgNames[j] + " ");
}
System.out.println();
}
}
System.out.println("\nTo build a package, use: \"bml <pkg-name>\" or \"bml <alias>\"\n");
}
/*-------------------------------------------------------------------------------------*/
/**
* Invoke a build operation by building the packages that are specified on the command line.
* Any alias names that are mentioned will first be expanded to the corresponding package
* names.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void doBuild(String[] args, int firstArg) {
IPackageMgr pkgMgr = buildStore.getPackageMgr();
/*
* For each argument, determine if it's an alias or a package name.
* Aliases are first expanded to their definitions.
*/
SortedSet<String> pkgSet = new TreeSet<String>();
for (int i = firstArg; i < args.length; i++) {
/* expand aliases - expPkgs is a list of one or more package names */
String[] expPkgs = configFile.getAlias(args[i]);
if (expPkgs == null) {
expPkgs = new String[] { args[i] };
}
/* validate package names */
for (int j = 0; j < expPkgs.length; j++) {
int pkgId = pkgMgr.getId(expPkgs[j]);
if ((pkgId == ErrorCode.NOT_FOUND) || pkgMgr.isFolder(pkgId)) {
fatal("Invalid package or alias name: \"" + expPkgs[j] +
"\". Use \"bml -l\" to see valid choices, or \"bml -h\" for more help.");
}
}
/*
* Add packages names to our set of packages to build. Using a set
* will eliminate duplicates.
*/
for (int j = 0; j < expPkgs.length; j++) {
pkgSet.add(expPkgs[j]);
}
}
/*
* Invoke the build (for now, display the packages).
*/
for (Iterator<String> iterator = pkgSet.iterator(); iterator.hasNext();) {
String pkgName = (String) iterator.next();
System.out.println("Building: " + pkgName);
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -a (alias) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void doAddAlias(String[] args, int firstArg) {
if (args.length < firstArg + 2) {
showUsageAndExit("Insufficient arguments to -a option.");
}
String aliasName = args[firstArg];
String packages[] = Arrays.copyOfRange(args, firstArg + 1, args.length);
/* perform the add operation, handling errors as necessary */
int rc = configFile.addAlias(aliasName, packages);
if (rc == ErrorCode.INVALID_NAME) {
fatal("Invalid alias name: " + aliasName);
}
if (rc == ErrorCode.BAD_VALUE) {
fatal("One or more of the package names is invalid.");
}
/* success - save the configuration */
try {
configFile.save();
} catch (IOException e) {
fatal("Problem saving configuration: " + e.getMessage());
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -u (unalias) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void doUnAlias(String[] args, int firstArg) {
if (args.length != firstArg + 1) {
showUsageAndExit("Incorrect number of arguments for -u option.");
}
/* perform the remove operation */
String aliasName = args[firstArg];
if (configFile.removeAlias(aliasName) == ErrorCode.NOT_FOUND) {
fatal("Alias not defined: " + aliasName);
}
/* success - save the configuration */
try {
configFile.save();
} catch (IOException e) {
fatal("Problem saving configuration: " + e.getMessage());
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -r (list roots) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void listRoots(String[] args, int firstArg) {
if (args.length != firstArg) {
showUsageAndExit("No arguments expected for -r option.");
}
IFileMgr fileMgr = buildStore.getFileMgr();
IPackageMgr pkgMgr = buildStore.getPackageMgr();
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
/* Display details about the @workspace root */
String workspaceVFSPath;
int workspaceRootId = pkgRootMgr.getWorkspaceRoot();
if (workspaceRootId == ErrorCode.NOT_FOUND) {
workspaceVFSPath = "<invalid>";
} else {
workspaceVFSPath = fileMgr.getPathName(workspaceRootId);
}
System.out.println("@workspace root:");
System.out.println(" VFS path : " + workspaceVFSPath);
System.out.println(" Native path : " + pkgRootMgr.getWorkspaceRootNative());
System.out.println();
/*
* For each package, displaying the _src and _gen root details. For each root,
* we show the name, the VFS path, and the native path.
*/
String[] packageNames = pkgMgr.getPackages();
for (int i = 0; i < packageNames.length; i++) {
int pkgId = pkgMgr.getId(packageNames[i]);
if (pkgId != pkgMgr.getImportPackage()) {
listRootsHelper(pkgId, packageNames[i] + "_src", IPackageRootMgr.SOURCE_ROOT);
listRootsHelper(pkgId, packageNames[i] + "_gen", IPackageRootMgr.GENERATED_ROOT);
System.out.println();
}
}
}
/*-------------------------------------------------------------------------------------*/
/**
* Helper method (for listRoots()) that displays the VFS/native paths for a single root.
*
* @param pkgId The ID of the package the root belongs to.
* @param rootName The name of the root.
* @param type The type of root (SOURCE_ROOT or GENERATED_ROOT).
*/
private void listRootsHelper(int pkgId, String rootName, int type) {
IFileMgr fileMgr = buildStore.getFileMgr();
IPackageRootMgr pkgRootMgr = buildStore.getPackageRootMgr();
String vfsPathName;
int pathId = pkgRootMgr.getRootPath(rootName);
if (pathId == ErrorCode.NOT_FOUND) {
vfsPathName = "<invalid>";
} else {
vfsPathName = fileMgr.getPathName(pathId);
}
String nativePathName = pkgRootMgr.getPackageRootNative(pkgId, type);
System.out.println("@" + rootName + " root:");
System.out.println(" VFS path : " + vfsPathName);
System.out.println(" Native path : " + nativePathName);
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -s (set root) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void setRoot(String[] args, int firstArg) {
if (args.length != firstArg + 2) {
showUsageAndExit("Incorrect number of arguments for -s option.");
}
String rootName = args[firstArg];
String nativePath = args[firstArg + 1];
/*
* Set the mapping in the configuration file. It won't impact the BuildStore
* at all (for now), but the whole configuration will be read into the
* BuildStore when the "bml" script is next invoked.
*/
int rc = configFile.addNativeRootMapping(rootName, nativePath);
if (rc == ErrorCode.NOT_FOUND) {
fatal("Invalid root name: " + rootName);
} else if (rc == ErrorCode.BAD_PATH) {
fatal("Native path is not a valid directory: " + nativePath);
}
/* success - save the configuration */
try {
configFile.save();
} catch (IOException e) {
fatal("Problem saving configuration: " + e.getMessage());
}
System.out.println("Root \"" + rootName + "\" now refers to native path: " + nativePath);
}
/*-------------------------------------------------------------------------------------*/
/**
* Perform the -d (remove root) command.
*
* @param args The command line arguments.
* @param firstArg The index of the first argument to be used.
*/
private void removeRoot(String[] args, int firstArg) {
if (args.length != firstArg + 1) {
showUsageAndExit("Incorrect number of arguments for -d option.");
}
String rootName = args[firstArg];
/*
* Start by checking whether the mapping is already in place. This doesn't
* affect the result of this command, other than giving a meaningful error message.
*/
String oldMapping = configFile.getNativeRootMapping(rootName);
/*
* Make the change in the configuration file. When the BuildStore is next loaded (and
* the configuration file loaded into the BuildStore), the change will actually take place.
*/
int rc = configFile.clearNativeRootMapping(rootName);
if (rc == ErrorCode.NOT_FOUND) {
fatal("Invalid root name: " + rootName);
}
if (oldMapping == null) {
fatal("Root is not currently mapped: " + rootName);
}
/* success - save the configuration */
try {
configFile.save();
} catch (IOException e) {
fatal("Problem saving configuration: " + e.getMessage());
}
System.out.println("Root \"" + rootName + "\" has been unmapped.");
}
/*-------------------------------------------------------------------------------------*/
}