blob: 25c20d53365da1ec249e9f6688426e6ce4ca9481 [file] [log] [blame]
package org.apache.maven.plugin.patch;
/*
* 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.
*/
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Apply one or more patches to project sources.
*/
@Mojo( name = "apply", defaultPhase = LifecyclePhase.PROCESS_SOURCES )
public class ApplyMojo
extends AbstractMojo
{
public static final List PATCH_FAILURE_WATCH_PHRASES;
public static final List DEFAULT_IGNORED_PATCHES;
public static final List DEFAULT_IGNORED_PATCH_PATTERNS;
static
{
List watches = new ArrayList();
watches.add( "fail" );
watches.add( "skip" );
watches.add( "reject" );
PATCH_FAILURE_WATCH_PHRASES = watches;
List ignored = new ArrayList();
ignored.add( ".svn" );
ignored.add( "CVS" );
DEFAULT_IGNORED_PATCHES = ignored;
List ignoredPatterns = new ArrayList();
ignoredPatterns.add( ".svn/**" );
ignoredPatterns.add( "CVS/**" );
DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns;
}
/**
* Whether to exclude default ignored patch items, such as <code>.svn</code> or <code>CVS</code> directories.
*/
@Parameter( defaultValue = "true" )
private boolean useDefaultIgnores;
/**
* The list of patch file names, supplying the order in which patches should be applied. The path names in this list
* must be relative to the base directory specified by the parameter <code>patchDirectory</code>. This parameter
* is mutually exclusive with the <code>patchfile</code> parameter.
*/
@Parameter
protected List patches;
/**
* Whether to skip this goal's execution.
*/
@Parameter( alias = "patch.apply.skip", defaultValue = "false" )
private boolean skipApplication;
/**
* Flag to enable/disable optimization file from being written. This file tracks the patches that were applied the
* last time this goal actually executed. It is required for cases where project-sources optimizations are enabled,
* since project-sources will not be re-unpacked if they are at least as fresh as the source archive. If we avoid
* re-unpacking project sources, we need to make sure we don't reapply patches.<br/> <strong>Note:</strong> If the
* list of patches changes and this flag is enabled, a "<code>mvn clean</code>" must be executed before the next
* build, to remove the tracking file.
*/
@Parameter( defaultValue = "true" )
private boolean optimizations;
/**
* This is the tracking file used to maintain a list of the patches applied to the unpacked project sources which
* are currently in the target directory. If this file is present, and project-source unpacking is optimized
* (meaning it won't re-unpack unless the project-sources archive is newer), this goal will not execute and no
* patches will be applied in the current build.
*/
@Parameter( defaultValue = "${project.build.directory}/optimization-files/patches-applied.txt" )
private File patchTrackingFile;
/**
* The target directory for applying patches. Files in this directory will be modified.
*/
@Parameter( alias = "patchTargetDir", defaultValue = "${project.build.sourceDirectory}" )
private File targetDirectory;
/**
* Flag being <code>true</code> if the desired behavior is to fail the build on the first failed patch detected.
*/
@Parameter( defaultValue = "true" )
private boolean failFast;
/**
* Setting natural order processing to <code>true</code> will cause all patches in a directory to be processed in
* a natural order alleviating the need to declare patches directly in the project file.
*/
@Parameter( defaultValue = "false" )
private boolean naturalOrderProcessing;
/**
* When the <code>strictPatching</code> flag is set, this parameter is useful to mark certain contents of the
* patch-source directory that should be ignored without causing the build to fail.
*/
@Parameter
private List ignoredPatches;
/**
* Flag that, when set to <code>true</code>, will make sure that all patches included in the <code>patches</code>
* list must be present and describe the full contents of the patch directory. If <code>strictPatching</code> is
* set to <code>true</code>, and the <code>patches</code> list has a value that does not correspond to a file
* in the patch directory, the build will fail. If <code>strictPatching</code> is set to <code>true</code>, and
* the patch directory contains files not listed in the <code>patches</code> parameter, the build will fail. If
* set to <code>false</code>, only the patches listed in the <code>patches</code> parameter that have
* corresponding files will be applied; the rest will be ignored.
*/
@Parameter( defaultValue = "false" )
private boolean strictPatching;
/**
* The number of directories to be stripped from patch file paths, before applying, starting from the leftmost, or
* root-est.
*/
@Parameter( defaultValue = "0" )
private int strip;
/**
* Whether to ignore whitespaces when applying the patches.
*/
@Parameter( defaultValue = "true" )
private boolean ignoreWhitespace;
/**
* Whether to treat these patches as having reversed source and dest in the patch syntax.
*/
@Parameter( defaultValue = "false" )
private boolean reverse;
/**
* Whether to make backups of the original files before modding them.
*/
@Parameter( defaultValue = "false" )
private boolean backups;
/**
* List of phrases to watch for in the command output from the patch tool. If one is found, it will cause the build
* to fail. All phrases should be lower-case <em>only</em>. By default, the phrases <code>fail</code>,
* <code>skip</code> and <code>reject</code> are used.
*/
@Parameter
private List failurePhrases = PATCH_FAILURE_WATCH_PHRASES;
/**
* The original file which will be modified by the patch. By default, the patch tool will automatically derive the
* original file from the header of the patch file.
*/
@Parameter
private File originalFile;
/**
* The output file which is the original file, plus modifications from the patch. By default, the file(s) will be
* patched inplace.
*/
@Parameter
private File destFile;
/**
* The single patch file to apply. This parameter is mutually exclusive with the <code>patches</code> parameter.
*/
@Parameter
private File patchFile;
/**
* The base directory for the file names specified by the parameter <code>patches</code>.
*/
@Parameter( defaultValue = "src/main/patches" )
private File patchDirectory;
/**
* When set to <code>true</code>, the empty files resulting from the patching process are removed. Empty ancestor
* directories are removed as well.
*
* @since 1.1
*/
@Parameter( defaultValue = "false" )
private boolean removeEmptyFiles;
/**
* apply --binary option to patch command line
* @since 1.2
*/
@Parameter( defaultValue = "false" )
private boolean binary;
/**
* Apply the patches. Give preference to patchFile over patchSourceDir/patches, and preference to originalFile over
* workDir.
*/
public void execute()
throws MojoExecutionException, MojoFailureException
{
boolean patchDirEnabled = ( ( patches != null ) && !patches.isEmpty() ) || naturalOrderProcessing;
boolean patchFileEnabled = patchFile != null;
// if patches is null or empty, and naturalOrderProcessing is not true then disable patching
if ( !patchFileEnabled && !patchDirEnabled )
{
getLog().info( "Patching is disabled for this project." );
return;
}
if ( skipApplication )
{
getLog().info( "Skipping patch file application (per configuration)." );
return;
}
patchTrackingFile.getParentFile().mkdirs();
Map patchesToApply;
try
{
if ( patchFileEnabled )
{
patchesToApply = Collections.singletonMap( patchFile.getName(), createPatchCommand( patchFile ) );
}
else
{
if ( !patchDirectory.isDirectory() )
{
throw new FileNotFoundException( "The base directory for patch files does not exist: "
+ patchDirectory );
}
List foundPatchFiles = FileUtils.getFileNames( patchDirectory, "*", null, false );
patchesToApply = findPatchesToApply( foundPatchFiles, patchDirectory );
checkStrictPatchCompliance( foundPatchFiles );
}
String output = applyPatches( patchesToApply );
checkForWatchPhrases( output );
writeTrackingFile( patchesToApply );
}
catch ( IOException ioe )
{
throw new MojoExecutionException( "Unable to obtain list of patch files", ioe );
}
}
private Map findPatchesToApply( List foundPatchFiles, File patchSourceDir )
throws MojoFailureException
{
Map patchesApplied = new LinkedHashMap();
if ( naturalOrderProcessing )
{
patches = new ArrayList( foundPatchFiles );
Collections.sort( patches );
}
String alreadyAppliedPatches = "";
try
{
if ( optimizations && patchTrackingFile.exists() )
{
alreadyAppliedPatches = FileUtils.fileRead( patchTrackingFile );
}
}
catch ( IOException ioe )
{
throw new MojoFailureException( "unable to read patch tracking file: " + ioe.getMessage() );
}
for (Object patche : patches) {
String patch = (String) patche;
if (!alreadyAppliedPatches.contains(patch)) {
File patchFile = new File(patchSourceDir, patch);
getLog().debug("Looking for patch: " + patch + " in: " + patchFile);
if (!patchFile.exists()) {
if (strictPatching) {
throw new MojoFailureException(this, "Patch operation cannot proceed.",
"Cannot find specified patch: \'" + patch
+ "\' in patch-source directory: \'" + patchSourceDir
+ "\'.\n\nEither fix this error, "
+ "or relax strictPatching.");
} else {
getLog().info(
"Skipping patch: " + patch + " listed in the parameter \"patches\"; "
+ "it is missing.");
}
} else {
foundPatchFiles.remove(patch);
patchesApplied.put(patch, createPatchCommand(patchFile));
}
}
}
return patchesApplied;
}
private void checkStrictPatchCompliance( List foundPatchFiles )
throws MojoExecutionException
{
if ( strictPatching )
{
List ignored = new ArrayList();
if ( ignoredPatches != null )
{
ignored.addAll( ignoredPatches );
}
if ( useDefaultIgnores )
{
ignored.addAll( DEFAULT_IGNORED_PATCHES );
}
List limbo = new ArrayList( foundPatchFiles );
for (Object anIgnored : ignored) {
String ignoredFile = (String) anIgnored;
limbo.remove(ignoredFile);
}
if ( !limbo.isEmpty() )
{
StringBuilder extraFileBuffer = new StringBuilder();
extraFileBuffer.append("Found ").append(limbo.size()).append(" unlisted patch files:");
for (Object foundPatchFile : foundPatchFiles) {
String patch = (String) foundPatchFile;
extraFileBuffer.append("\n \'").append(patch).append('\'');
}
extraFileBuffer.append( "\n\nEither remove these files, "
+ "add them to the patches configuration list, " + "or relax strictPatching." );
throw new MojoExecutionException( extraFileBuffer.toString() );
}
}
}
private String applyPatches( Map patchesApplied )
throws MojoExecutionException
{
final StringWriter outputWriter = new StringWriter();
StreamConsumer consumer = new StreamConsumer()
{
public void consumeLine( String line )
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( line );
}
outputWriter.write( line + "\n" );
}
};
// used if failFast is false
List failedPatches = new ArrayList();
for (Object o : patchesApplied.entrySet()) {
Entry entry = (Entry) o;
String patchName = (String) entry.getKey();
Commandline cli = (Commandline) entry.getValue();
try {
getLog().info("Applying patch: " + patchName);
int result = executeCommandLine(cli, consumer, consumer);
if (result != 0) {
if (failFast) {
throw new MojoExecutionException("Patch command failed with exit code " + result + " for "
+ patchName + ". Please see console and debug output for more information.");
} else {
failedPatches.add(patchName);
}
}
} catch (CommandLineException e) {
throw new MojoExecutionException("Failed to apply patch: " + patchName
+ ". See debug output for more information.", e);
}
}
if ( !failedPatches.isEmpty() )
{
getLog().error( "Failed applying one or more patches:" );
for (Object failedPatche : failedPatches) {
getLog().error("* " + failedPatche);
}
throw new MojoExecutionException( "Patch command failed for one or more patches."
+ " Please see console and debug output for more information." );
}
return outputWriter.toString();
}
private int executeCommandLine( Commandline cli, StreamConsumer out, StreamConsumer err )
throws CommandLineException
{
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Executing: " + cli );
}
int result = CommandLineUtils.executeCommandLine( cli, out, err );
if ( getLog().isDebugEnabled() )
{
getLog().debug( "Exit code: " + result );
}
return result;
}
private void writeTrackingFile( Map patchesApplied )
throws MojoExecutionException
{
FileWriter writer = null;
try
{
boolean appending = patchTrackingFile.exists();
writer = new FileWriter( patchTrackingFile, appending );
for ( Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); )
{
if ( appending )
{
writer.write( System.getProperty( "line.separator" ) );
}
String patch = (String) it.next();
writer.write( patch );
if ( it.hasNext() )
{
writer.write( System.getProperty( "line.separator" ) );
}
}
writer.flush();
}
catch ( IOException e )
{
throw new MojoExecutionException( "Failed to write patch-tracking file: " + patchTrackingFile, e );
}
finally
{
IOUtil.close( writer );
}
}
private void checkForWatchPhrases( String output )
throws MojoExecutionException
{
for (Object failurePhrase : failurePhrases) {
String phrase = (String) failurePhrase;
if (output.contains(phrase)) {
throw new MojoExecutionException("Failed to apply patches (detected watch-phrase: \'" + phrase
+ "\' in output). " + "If this is in error, configure the patchFailureWatchPhrases parameter.");
}
}
}
/**
* Add a new Patch task to the Ant calling mechanism. Give preference to originalFile/destFile, then workDir, and
* finally ${basedir}.
*/
private Commandline createPatchCommand( File patchFile )
{
Commandline cli = new Commandline();
cli.setExecutable( "patch" );
cli.setWorkingDirectory( targetDirectory.getAbsolutePath() );
cli.createArg().setValue( "-p" + strip );
if ( binary )
{
cli.createArg().setValue( "--binary" );
}
if ( ignoreWhitespace )
{
cli.createArg().setValue( "-l" );
}
if ( reverse )
{
cli.createArg().setValue( "-R" );
}
if ( backups )
{
cli.createArg().setValue( "-b" );
}
if ( removeEmptyFiles )
{
cli.createArg().setValue( "-E" );
}
cli.createArg().setValue( "-i" );
cli.createArg().setFile( patchFile );
if ( destFile != null )
{
cli.createArg().setValue( "-o" );
cli.createArg().setFile( destFile );
}
if ( originalFile != null )
{
cli.createArg().setFile( originalFile );
}
return cli;
}
}