/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.apache.tools.ant.taskdefs; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Vector; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Main; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.taskdefs.Ant.TargetElement; import org.apache.tools.ant.types.DirSet; import org.apache.tools.ant.types.FileList; import org.apache.tools.ant.types.FileSet; import org.apache.tools.ant.types.Path; import org.apache.tools.ant.types.PropertySet; import org.apache.tools.ant.types.Reference; import org.apache.tools.ant.types.ResourceCollection; import org.apache.tools.ant.util.StringUtils; /** * Calls a given target for all defined sub-builds. This is an extension * of ant for bulk project execution. * <p> * <h2> Use with directories </h2> * <p> * subant can be used with directory sets to execute a build from different directories. * 2 different options are offered * </p> * <ul> * <li> * run the same build file /somepath/otherpath/mybuild.xml * with different base directories use the genericantfile attribute * </li> * <li>if you want to run directory1/build.xml, directory2/build.xml, .... * use the antfile attribute. The base directory does not get set by the subant task in this case, * because you can specify it in each build file. * </li> * </ul> * @since Ant1.6 * @ant.task name="subant" category="control" */ public class SubAnt extends Task { private Path buildpath; private Ant ant = null; private String subTarget = null; private String antfile = getDefaultBuildFile(); private File genericantfile = null; private boolean verbose = false; private boolean inheritAll = false; private boolean inheritRefs = false; private boolean failOnError = true; private String output = null; private List<Property> properties = new Vector<>(); private List<Ant.Reference> references = new Vector<>(); private List<PropertySet> propertySets = new Vector<>(); /** the targets to call on the new project */ private List<TargetElement> targets = new Vector<>(); /** * Get the default build file name to use when launching the task. * <p> * This function may be overrided by providers of custom ProjectHelper so they can implement easily their sub * launcher. * * @return the name of the default file * @since Ant 1.8.0 */ protected String getDefaultBuildFile() { return Main.DEFAULT_BUILD_FILENAME; } /** * Pass output sent to System.out to the new project. * * @param output a line of output * @since Ant 1.6.2 */ @Override public void handleOutput(String output) { if (ant != null) { ant.handleOutput(output); } else { super.handleOutput(output); } } /** * Process input into the ant task * * @param buffer the buffer into which data is to be read. * @param offset the offset into the buffer at which data is stored. * @param length the amount of data to read * * @return the number of bytes read * * @exception IOException if the data cannot be read * * @see Task#handleInput(byte[], int, int) * * @since Ant 1.6.2 */ @Override public int handleInput(byte[] buffer, int offset, int length) throws IOException { if (ant != null) { return ant.handleInput(buffer, offset, length); } else { return super.handleInput(buffer, offset, length); } } /** * Pass output sent to System.out to the new project. * * @param output The output to log. Should not be <code>null</code>. * * @since Ant 1.6.2 */ @Override public void handleFlush(String output) { if (ant != null) { ant.handleFlush(output); } else { super.handleFlush(output); } } /** * Pass output sent to System.err to the new project. * * @param output The error output to log. Should not be <code>null</code>. * * @since Ant 1.6.2 */ @Override public void handleErrorOutput(String output) { if (ant != null) { ant.handleErrorOutput(output); } else { super.handleErrorOutput(output); } } /** * Pass output sent to System.err to the new project. * * @param output The error output to log. Should not be <code>null</code>. * * @since Ant 1.6.2 */ @Override public void handleErrorFlush(String output) { if (ant != null) { ant.handleErrorFlush(output); } else { super.handleErrorFlush(output); } } /** * Runs the various sub-builds. */ @Override public void execute() { if (buildpath == null) { throw new BuildException("No buildpath specified"); } final String[] filenames = buildpath.list(); final int count = filenames.length; if (count < 1) { log("No sub-builds to iterate on", Project.MSG_WARN); return; } /* //REVISIT: there must be cleaner way of doing this, if it is merited at all if (subTarget == null) { subTarget = getOwningTarget().getName(); } */ BuildException buildException = null; for (int i = 0; i < count; ++i) { File file = null; String subdirPath = null; Throwable thrownException = null; try { File directory = null; file = new File(filenames[i]); if (file.isDirectory()) { if (verbose) { subdirPath = file.getPath(); log("Entering directory: " + subdirPath + "\n", Project.MSG_INFO); } if (genericantfile != null) { directory = file; file = genericantfile; } else { file = new File(file, antfile); } } execute(file, directory); if (verbose && subdirPath != null) { log("Leaving directory: " + subdirPath + "\n", Project.MSG_INFO); } } catch (RuntimeException ex) { if (!(getProject().isKeepGoingMode())) { if (verbose && subdirPath != null) { log("Leaving directory: " + subdirPath + "\n", Project.MSG_INFO); } throw ex; // throw further } thrownException = ex; } catch (Throwable ex) { if (!(getProject().isKeepGoingMode())) { if (verbose && subdirPath != null) { log("Leaving directory: " + subdirPath + "\n", Project.MSG_INFO); } throw new BuildException(ex); } thrownException = ex; } if (thrownException != null) { if (thrownException instanceof BuildException) { log("File '" + file + "' failed with message '" + thrownException.getMessage() + "'.", Project.MSG_ERR); // only the first build exception is reported if (buildException == null) { buildException = (BuildException) thrownException; } } else { log("Target '" + file + "' failed with message '" + thrownException.getMessage() + "'.", Project.MSG_ERR); log(StringUtils.getStackTrace(thrownException), Project.MSG_ERR); if (buildException == null) { buildException = new BuildException(thrownException); } } if (verbose && subdirPath != null) { log("Leaving directory: " + subdirPath + "\n", Project.MSG_INFO); } } } // check if one of the builds failed in keep going mode if (buildException != null) { throw buildException; } } /** * Runs the given target on the provided build file. * * @param file the build file to execute * @param directory the directory of the current iteration * @throws BuildException is the file cannot be found, read, is * a directory, or the target called failed, but only if * <code>failOnError</code> is <code>true</code>. Otherwise, * a warning log message is simply output. */ private void execute(File file, File directory) throws BuildException { if (!file.exists() || file.isDirectory() || !file.canRead()) { String msg = "Invalid file: " + file; if (failOnError) { throw new BuildException(msg); } log(msg, Project.MSG_WARN); return; } ant = createAntTask(directory); String antfilename = file.getAbsolutePath(); ant.setAntfile(antfilename); targets.forEach(ant::addConfiguredTarget); try { if (verbose) { log("Executing: " + antfilename, Project.MSG_INFO); } ant.execute(); } catch (BuildException e) { if (failOnError || isHardError(e)) { throw e; } log("Failure for target '" + subTarget + "' of: " + antfilename + "\n" + e.getMessage(), Project.MSG_WARN); } catch (Throwable e) { if (failOnError || isHardError(e)) { throw new BuildException(e); } log("Failure for target '" + subTarget + "' of: " + antfilename + "\n" + e.toString(), Project.MSG_WARN); } finally { ant = null; } } /** whether we should even try to continue after this error */ private boolean isHardError(Throwable t) { if (t instanceof BuildException) { return isHardError(t.getCause()); } if (t instanceof OutOfMemoryError) { return true; } if (t instanceof ThreadDeath) { return true; } // incl. t == null return false; } /** * This method builds the file name to use in conjunction with directories. * * <p>Defaults to "build.xml". * If <code>genericantfile</code> is set, this attribute is ignored.</p> * * @param antfile the short build file name. Defaults to "build.xml". */ public void setAntfile(String antfile) { this.antfile = antfile; } /** * This method builds a file path to use in conjunction with directories. * * <p>Use <code>genericantfile</code>, in order to run the same build file * with different basedirs.</p> * If this attribute is set, <code>antfile</code> is ignored. * * @param afile (path of the generic ant file, absolute or relative to * project base directory) * */ public void setGenericAntfile(File afile) { this.genericantfile = afile; } /** * Sets whether to fail with a build exception on error, or go on. * * @param failOnError the new value for this boolean flag. */ public void setFailonerror(boolean failOnError) { this.failOnError = failOnError; } /** * The target to call on the different sub-builds. Set to "" to execute * the default target. * @param target the target * <p> */ // REVISIT: Defaults to the target name that contains this task if not specified. public void setTarget(String target) { this.subTarget = target; } /** * Add a target to this Ant invocation. * @param t the <code>TargetElement</code> to add. * @since Ant 1.7 */ public void addConfiguredTarget(TargetElement t) { if (t.getName().isEmpty()) { throw new BuildException("target name must not be empty"); } targets.add(t); } /** * Enable/ disable verbose log messages showing when each sub-build path is entered/ exited. * The default value is "false". * @param on true to enable verbose mode, false otherwise (default). */ public void setVerbose(boolean on) { this.verbose = on; } /** * Corresponds to <code><ant></code>'s * <code>output</code> attribute. * * @param s the filename to write the output to. */ public void setOutput(String s) { this.output = s; } /** * Corresponds to <code><ant></code>'s * <code>inheritall</code> attribute. * * @param b the new value for this boolean flag. */ public void setInheritall(boolean b) { this.inheritAll = b; } /** * Corresponds to <code><ant></code>'s * <code>inheritrefs</code> attribute. * * @param b the new value for this boolean flag. */ public void setInheritrefs(boolean b) { this.inheritRefs = b; } /** * Corresponds to <code><ant></code>'s * nested <code><property></code> element. * * @param p the property to pass on explicitly to the sub-build. */ public void addProperty(Property p) { properties.add(p); } /** * Corresponds to <code><ant></code>'s * nested <code><reference></code> element. * * @param r the reference to pass on explicitly to the sub-build. */ public void addReference(Ant.Reference r) { references.add(r); } /** * Corresponds to <code><ant></code>'s * nested <code><propertyset></code> element. * @param ps the propertyset */ public void addPropertyset(PropertySet ps) { propertySets.add(ps); } /** * Adds a directory set to the implicit build path. * <p> * <em>Note that the directories will be added to the build path * in no particular order, so if order is significant, one should * use a file list instead!</em> * * @param set the directory set to add. */ public void addDirset(DirSet set) { add(set); } /** * Adds a file set to the implicit build path. * <p> * <em>Note that the directories will be added to the build path * in no particular order, so if order is significant, one should * use a file list instead!</em> * * @param set the file set to add. */ public void addFileset(FileSet set) { add(set); } /** * Adds an ordered file list to the implicit build path. * <p> * <em>Note that contrary to file and directory sets, file lists * can reference non-existent files or directories!</em> * * @param list the file list to add. */ public void addFilelist(FileList list) { add(list); } /** * Adds a resource collection to the implicit build path. * * @param rc the resource collection to add. * @since Ant 1.7 */ public void add(ResourceCollection rc) { getBuildpath().add(rc); } /** * Set the buildpath to be used to find sub-projects. * * @param s an Ant Path object containing the buildpath. */ public void setBuildpath(Path s) { getBuildpath().append(s); } /** * Creates a nested build path, and add it to the implicit build path. * * @return the newly created nested build path. */ public Path createBuildpath() { return getBuildpath().createPath(); } /** * Creates a nested <code><buildpathelement></code>, * and add it to the implicit build path. * * @return the newly created nested build path element. */ public Path.PathElement createBuildpathElement() { return getBuildpath().createPathElement(); } /** * Gets the implicit build path, creating it if <code>null</code>. * * @return the implicit build path. */ private Path getBuildpath() { if (buildpath == null) { buildpath = new Path(getProject()); } return buildpath; } /** * Buildpath to use, by reference. * * @param r a reference to an Ant Path object containing the buildpath. */ public void setBuildpathRef(Reference r) { createBuildpath().setRefid(r); } /** * Creates the <ant> task configured to run a specific target. * * @param directory : if not null the directory where the build should run * * @return the ant task, configured with the explicit properties and * references necessary to run the sub-build. */ private Ant createAntTask(File directory) { Ant antTask = new Ant(this); antTask.init(); if (subTarget != null && subTarget.length() > 0) { antTask.setTarget(subTarget); } if (output != null) { antTask.setOutput(output); } if (directory != null) { antTask.setDir(directory); } else { antTask.setUseNativeBasedir(true); } antTask.setInheritAll(inheritAll); properties.forEach(p -> copyProperty(antTask.createProperty(), p)); propertySets.forEach(antTask::addPropertyset); antTask.setInheritRefs(inheritRefs); references.forEach(antTask::addReference); return antTask; } /** * Assigns an Ant property to another. * * @param to the destination property whose content is modified. * @param from the source property whose content is copied. */ private static void copyProperty(Property to, Property from) { to.setName(from.getName()); if (from.getValue() != null) { to.setValue(from.getValue()); } if (from.getFile() != null) { to.setFile(from.getFile()); } if (from.getResource() != null) { to.setResource(from.getResource()); } if (from.getPrefix() != null) { to.setPrefix(from.getPrefix()); } if (from.getRefid() != null) { to.setRefid(from.getRefid()); } if (from.getEnvironment() != null) { to.setEnvironment(from.getEnvironment()); } if (from.getClasspath() != null) { to.setClasspath(from.getClasspath()); } } } // END class SubAnt