/* ******************************************************************* * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC), * 2003 Contributors. * 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: * PARC initial implementation * ******************************************************************/ package org.aspectj.internal.tools.build; import java.io.File; import java.io.FileFilter; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Properties; import java.util.StringTokenizer; import org.apache.tools.ant.BuildException; import org.aspectj.internal.tools.build.Result.Kind; /** * Template class to build (eclipse) modules (and, weakly, products), including * any required modules. When building modules, this assumes: * * This currently provides no control over the compile or assembly process, but * clients can harvest {moduleDir}/bin directories to re-use the * results of eclipse compiles. *

* When building products, this assumes: *

*

* When run using main(String[]), all relevant Ant libraries and properties must * be defined. *

* Written to compile standalone. Refactor if using utils, bridge, etc. */ public abstract class Builder { /** * This has only weak forms for build instructions needed: - resource * pattern - compiler selection and control * * Both assumed and generated paths are scattered; see XXXNameLiteral and * XXXFileLiteral. * * Builder is supposed to be thread-safe, but currently caches build * properties to tunnel for filters. hmm. */ public static final String RESOURCE_PATTERN; public static final String BINARY_SOURCE_PATTERN; public static final String ALL_PATTERN; /** enable copy filter semantics */ protected static final boolean FILTER_ON = true; /** disable copy filter semantics */ protected static final boolean FILTER_OFF = false; /** define libraries to skip as comma-delimited values for this key */ private static final String SKIP_LIBRARIES_KEY = "skip.libraries"; /** List (String) names of libraries to skip during assembly */ private static final List SKIP_LIBRARIES; private static final String ERROR_KEY = "error loading properties"; private static final Properties PROPS; static { PROPS = new Properties(); List skips = Collections.emptyList(); String resourcePattern = "**/*.txt,**/*.rsc,**/*.gif,**/*.properties"; String allPattern = "**/*"; String binarySourcePattern = "**/*.rsc,**/*.gif,**/*.jar,**/*.zip"; String name = Builder.class.getName().replace('.', '/') + ".properties"; try { InputStream in = Builder.class.getClassLoader() .getResourceAsStream(name); PROPS.load(in); allPattern = PROPS.getProperty("all.pattern"); resourcePattern = PROPS.getProperty("resource.pattern"); binarySourcePattern = PROPS.getProperty("binarySource.pattern"); skips = commaStrings(PROPS.getProperty(SKIP_LIBRARIES_KEY)); } catch (Throwable t) { if (t instanceof ThreadDeath) { throw (ThreadDeath) t; } String m = "error loading " + name + ": " + t.getClass() + " " + t; PROPS.setProperty(ERROR_KEY, m); } SKIP_LIBRARIES = skips; ALL_PATTERN = allPattern; BINARY_SOURCE_PATTERN = binarySourcePattern; RESOURCE_PATTERN = resourcePattern; } /** * Splits strings into an unmodifable List of String using * comma as the delimiter and trimming whitespace from the result. * * @param text * String to split. * @return unmodifiable List (String) of String delimited by comma in text */ public static List commaStrings(String text) { if ((null == text) || (0 == text.length())) { return Collections.EMPTY_LIST; } List strings = new ArrayList(); StringTokenizer tok = new StringTokenizer(text, ","); while (tok.hasMoreTokens()) { String token = tok.nextToken().trim(); if (0 < token.length()) { strings.add(token); } } return Collections.unmodifiableList(strings); } /** * Map delivered-jar name to created-module name * * @param jarName * the String (lowercased) of the jar/zip to map */ private String moduleAliasFor(String jarName) { String result = PROPS.getProperty("alias." + jarName, jarName); if (verbose && result.equals(jarName)) { String m = "expected alias for " + jarName; handler.error(m + PROPS.getProperty(ERROR_KEY, "")); } return result; } protected final Messager handler; protected boolean buildingEnabled; private final File tempDir; private final ArrayList tempFiles; private final boolean useEclipseCompiles; protected boolean verbose; protected Builder(File tempDir, boolean useEclipseCompiles, Messager handler) { Util.iaxIfNull(handler, "handler"); this.useEclipseCompiles = useEclipseCompiles; this.handler = handler; this.tempFiles = new ArrayList(); if ((null == tempDir) || !tempDir.canWrite() || !tempDir.isDirectory()) { this.tempDir = Util.makeTempDir("Builder"); } else { this.tempDir = tempDir; } buildingEnabled = true; } /** tell builder to stop or that it's ok to run */ public void setBuildingEnabled(boolean enabled) { buildingEnabled = enabled; } public void setVerbose(boolean verbose) { this.verbose = verbose; } private void verifyBuildSpec(BuildSpec buildSpec) { if (null == buildSpec.productDir) { // ensure module properties // derive moduleDir from baseDir + module if (null == buildSpec.moduleDir) { if (null == buildSpec.baseDir) { throw new BuildException("require baseDir or moduleDir"); } else if (null == buildSpec.module) { throw new BuildException("require module with baseDir"); } else { if (null == buildSpec.baseDir) { buildSpec.baseDir = new File("."); // user.home? } buildSpec.moduleDir = new File(buildSpec.baseDir, buildSpec.module); } } else if (null == buildSpec.baseDir) { // derive baseDir from moduleDir parent buildSpec.baseDir = buildSpec.moduleDir.getParentFile(); // rule: base is parent if (null == buildSpec.baseDir) { buildSpec.baseDir = new File("."); // user.home? } handler.log("Builder using derived baseDir: " + buildSpec.baseDir); } Util.iaxIfNotCanReadDir(buildSpec.moduleDir, "moduleDir"); if (null == buildSpec.module) { // derive module name from directory buildSpec.module = buildSpec.moduleDir.getName(); if (null == buildSpec.module) { throw new BuildException("no name, even from " + buildSpec.moduleDir); } } } } /** * Find the Result (and hence Module and Modules) for this BuildSpec. */ protected Result specifyResultFor(BuildSpec buildSpec) { if (buildSpec.trimTesting && (-1 != buildSpec.module.indexOf("testing"))) { // XXXNameLiteral String warning = "Warning - cannot trimTesting for testing modules: "; handler.log(warning + buildSpec.module); } Messager handler = new Messager(); Modules modules = new Modules(buildSpec.baseDir, buildSpec.jarDir, handler); final Module moduleToBuild = modules.getModule(buildSpec.module); Kind kind = Result.kind(buildSpec.trimTesting, buildSpec.assembleAll); return moduleToBuild.getResult(kind); } public final boolean build(BuildSpec buildSpec) { if (!buildingEnabled) { return false; } verifyBuildSpec(buildSpec); if (null != buildSpec.productDir) { return buildProduct(buildSpec); } Result result = specifyResultFor(buildSpec); ArrayList errors = new ArrayList(); try { return buildAll(result, errors); } finally { if (0 < errors.size()) { String label = "error building " + buildSpec + ": "; for (Iterator iter = errors.iterator(); iter.hasNext();) { String m = label + iter.next(); handler.error(m); } } } } /** * Clean up any temporary files, etc. after build completes */ public boolean cleanup() { boolean noErr = true; for (ListIterator iter = tempFiles.listIterator(); iter.hasNext();) { File file = (File) iter.next(); if (!Util.deleteContents(file) || !file.delete()) { if (noErr) { noErr = false; } handler.log("unable to clean up " + file); } } return noErr; } protected final boolean isLogging() { return (verbose && (null != this.handler)); } protected Result[] skipUptodate(Result[] results) { if (null == results) { return new Result[0]; } Result[] done = new Result[results.length]; int to = 0; for (int i = 0; i < done.length; i++) { if ((null != results[i]) && results[i].outOfDate()) { done[to++] = results[i]; } } if (to < results.length) { Result[] newdone = new Result[to]; System.arraycopy(done, 0, newdone, 0, newdone.length); done = newdone; } return done; } /** * Build a result with all antecedants. * * @param result * the Result to build * @param errors * the List sink for errors, if any * @return false after successful build, when module jar should exist */ protected final boolean buildAll(Result result, List errors) { Result[] buildList = skipUptodate(getAntecedantResults(result)); ArrayList doneList = new ArrayList(); if ((null != buildList) && (0 < buildList.length)) { if (isLogging()) { handler.log("modules to build: " + Arrays.asList(buildList)); } for (int i = 0; i < buildList.length; i++) { Result required = buildList[i]; if (!buildingEnabled) { return false; } String requiredName = required.getName(); if (!doneList.contains(requiredName)) { doneList.add(requiredName); if (!buildOnly(required, errors)) { return false; } } } } return true; } /** * Build a module but no antecedants. * * @param module * the Module to build * @param errors * the List sink for errors, if any * @return false after successful build, when module jar should exist */ protected final boolean buildOnly(Result result, List errors) { if (!result.outOfDate()) { return true; } if (isLogging()) { handler.log("building " + result); } if (!buildingEnabled) { return false; } if (result.getKind().assemble) { return assembleAll(result, handler); } Module module = result.getModule(); final File classesDir; if (useEclipseCompiles) { classesDir = new File(module.moduleDir, "bin"); // FileLiteral } else { String name = "classes-" + System.currentTimeMillis(); classesDir = new File(tempDir, name); } if (verbose) { handler.log("buildOnly " + module); } try { return (compile(result, classesDir,useEclipseCompiles, errors)) && assemble(result, classesDir, errors); } finally { if (!useEclipseCompiles && !Util.delete(classesDir)) { errors.add("buildOnly unable to delete " + classesDir); } } } /** * Register temporary file or directory to be deleted when the build is * complete, even if an Exception is thrown. */ protected void addTempFile(File tempFile) { if (null != tempFile) { tempFiles.add(tempFile); } } /** * Build product by discovering any modules to build, building those, * assembling the product distribution, and optionally creating an installer * for it. * * @return true on success */ protected final boolean buildProduct(BuildSpec buildSpec) throws BuildException { Util.iaxIfNull(buildSpec, "buildSpec"); if (!buildSpec.trimTesting) { buildSpec.trimTesting = true; handler.log("testing trimmed for " + buildSpec); } Util.iaxIfNotCanReadDir(buildSpec.productDir, "productDir"); Util.iaxIfNotCanReadDir(buildSpec.baseDir, "baseDir"); Util.iaxIfNotCanWriteDir(buildSpec.distDir, "distDir"); // ---- discover modules to build, and build them Modules modules = new Modules(buildSpec.baseDir, buildSpec.jarDir, handler); ProductModule[] productModules = discoverModules(buildSpec.productDir, modules); for (int i = 0; i < productModules.length; i++) { if (buildSpec.verbose) { handler.log("building product module " + productModules[i]); } if (!buildProductModule(productModules[i])) { return false; } } if (buildSpec.verbose) { handler.log("assembling product module for " + buildSpec); } // ---- assemble product distribution final String productName = buildSpec.productDir.getName(); final File targDir = new File(buildSpec.distDir, productName); final String targDirPath = targDir.getPath(); if (targDir.canWrite()) { Util.deleteContents(targDir); } if (!targDir.canWrite() && !targDir.mkdirs()) { if (buildSpec.verbose) { handler.log("buildProduct unable to create " + targDir); } return false; } // copy non-binaries (with filter) File distDir = new File(buildSpec.productDir, "dist"); if (!copyNonBinaries(buildSpec, distDir, targDir)) { return false; } // copy binaries (but not module flag files) String excludes = null; { StringBuffer buf = new StringBuffer(); for (int i = 0; i < productModules.length; i++) { if (0 < buf.length()) { buf.append(","); } buf.append(productModules[i].relativePath); } if (0 < buf.length()) { excludes = buf.toString(); } } if (!copyBinaries(buildSpec, distDir, targDir, excludes)) { return false; } // copy binaries associated with module flag files for (int i = 0; i < productModules.length; i++) { final ProductModule product = productModules[i]; final Kind kind = Result.kind(Result.NORMAL, product.assembleAll); Result result = product.module.getResult(kind); String targPath = Util.path(targDirPath, product.relativePath); File jarFile = result.getOutputFile(); copyFile(jarFile, new File(targPath), FILTER_OFF); } handler.log("created product in " + targDir); // ---- create installer if (buildSpec.createInstaller) { return buildInstaller(buildSpec, targDirPath); } else { return true; } } protected boolean copyBinaries(BuildSpec buildSpec, File distDir, File targDir, String excludes) { String includes = Builder.BINARY_SOURCE_PATTERN; return copyFiles(distDir, targDir, includes, excludes, FILTER_OFF); } /** * filter-copy everything but the binaries */ protected boolean copyNonBinaries(BuildSpec buildSpec, File distDir, File targDir) { String excludes = Builder.BINARY_SOURCE_PATTERN; String includes = Builder.ALL_PATTERN; return copyFiles(distDir, targDir, includes, excludes, FILTER_ON); } protected final boolean buildProductModule(ProductModule module) { ArrayList errors = new ArrayList(); try { Kind productKind = Result.kind(Result.NORMAL, Result.ASSEMBLE); Result result = module.module.getResult(productKind); return buildAll(result, errors); } finally { for (Iterator iter = errors.iterator(); iter.hasNext();) { handler.error("error building " + module + ": " + iter.next()); } } } /** * Discover any modules that might need to be built in order to assemble the * product distribution. This interprets empty .jar files as module * deliverables. */ protected ProductModule[] discoverModules(File productDir, Modules modules) { final ArrayList found = new ArrayList(); FileFilter filter = new FileFilter() {// empty jar files public boolean accept(File file) { if ((null != file) && file.canRead() && file.getPath().endsWith(".jar") // XXXFileLiteral && (0l == file.length())) { found.add(file); } return true; } }; Util.visitFiles(productDir, filter); ArrayList results = new ArrayList(); for (File file: found) { String jarName = moduleAliasFor(file.getName().toLowerCase()); if (jarName.endsWith(".jar") || jarName.endsWith(".zip")) { // XXXFileLiteral jarName = jarName.substring(0, jarName.length() - 4); } else { handler.log("can only replace .[jar|zip]: " + file); // XXX error? } boolean assembleAll = jarName.endsWith("-all"); // XXXFileLiteral String name = (!assembleAll ? jarName : jarName.substring(0, jarName.length() - 4)); Module module = modules.getModule(name); if (null == module) { handler.log("unable to find module for " + file); } else { results.add(new ProductModule(productDir, file, module, assembleAll)); } } return (ProductModule[]) results.toArray(new ProductModule[0]); } /** * Subclasses should query whether to include library files in the assembly. * * @param module * the Module being built * @param libraries * the List of File path to the jar to consider assembling * @return true if the jar should be included, false otherwise. */ protected void removeLibraryFilesToSkip(Module module, List libraries) { for (ListIterator liter = libraries.listIterator(); liter.hasNext();) { File library = (File) liter.next(); final String fname = library.getName(); if (null != fname) { for (Iterator iter = SKIP_LIBRARIES.iterator(); iter.hasNext();) { String name = (String) iter.next(); if (fname.equals(name)) { liter.remove(); break; } } } } } /** * @return String[] names of results to build for this module */ abstract protected Result[] getAntecedantResults(Result toBuild); /** * Compile module classes to classesDir, saving String errors. * * @param module * the Module to compile * @param classesDir * the File directory to compile to * @param useExistingClasses * if true, don't recompile and ensure classes are available * @param errors * the List to add error messages to */ abstract protected boolean compile(Result result, File classesDir, boolean useExistingClasses, List errors); /** * Assemble the module distribution from the classesDir, saving String * errors. * * @see #removeLibraryFilesToSkip(Module, File) */ abstract protected boolean assemble(Result result, File classesDir, List errors); /** * Assemble the module distribution from the classesDir and all * antecendants, saving String errors. * * @see #removeLibraryFilesToSkip(Module, File) */ abstract protected boolean assembleAll(Result result, Messager handler); /** * Generate the installer for this product to targDirPath */ abstract protected boolean buildInstaller(BuildSpec buildSpec, String targDirPath); /** * Copy fromFile to toFile, optionally filtering contents */ abstract protected boolean copyFile(File fromFile, File toFile, boolean filter); /** * Copy toDir any fromDir included files without any exluded files, * optionally filtering contents. * * @param fromDir * File dir to read from - error if not readable * @param toDir * File dir to write to - error if not writable * @param included * String Ant pattern of included files (if null, include all) * @param excluded * String Ant pattern of excluded files (if null, exclude none) * @param filter * if FILTER_ON, then filter file contents using global * token/value pairs */ abstract protected boolean copyFiles(File fromDir, File toDir, String included, String excluded, boolean filter); }