View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.patch;
20  
21  /*
22   * Licensed to the Apache Software Foundation (ASF) under one
23   * or more contributor license agreements.  See the NOTICE file
24   * distributed with this work for additional information
25   * regarding copyright ownership.  The ASF licenses this file
26   * to you under the Apache License, Version 2.0 (the
27   * "License"); you may not use this file except in compliance
28   * with the License.  You may obtain a copy of the License at
29   *
30   *   http://www.apache.org/licenses/LICENSE-2.0
31   *
32   * Unless required by applicable law or agreed to in writing,
33   * software distributed under the License is distributed on an
34   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
35   * KIND, either express or implied.  See the License for the
36   * specific language governing permissions and limitations
37   * under the License.
38   */
39  
40  import java.io.File;
41  import java.io.FileNotFoundException;
42  import java.io.FileWriter;
43  import java.io.IOException;
44  import java.io.StringWriter;
45  import java.io.Writer;
46  import java.util.ArrayList;
47  import java.util.Collections;
48  import java.util.Iterator;
49  import java.util.LinkedHashMap;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  
54  import org.apache.maven.plugin.AbstractMojo;
55  import org.apache.maven.plugin.MojoExecutionException;
56  import org.apache.maven.plugin.MojoFailureException;
57  import org.apache.maven.plugins.annotations.LifecyclePhase;
58  import org.apache.maven.plugins.annotations.Mojo;
59  import org.apache.maven.plugins.annotations.Parameter;
60  import org.codehaus.plexus.util.FileUtils;
61  import org.codehaus.plexus.util.StringUtils;
62  import org.codehaus.plexus.util.cli.CommandLineException;
63  import org.codehaus.plexus.util.cli.CommandLineUtils;
64  import org.codehaus.plexus.util.cli.Commandline;
65  import org.codehaus.plexus.util.cli.StreamConsumer;
66  
67  /**
68   * Apply one or more patches to project sources.
69   */
70  @Mojo(name = "apply", defaultPhase = LifecyclePhase.PROCESS_SOURCES)
71  public class ApplyMojo extends AbstractMojo {
72  
73      public static final List<String> PATCH_FAILURE_WATCH_PHRASES;
74  
75      public static final List<String> DEFAULT_IGNORED_PATCHES;
76  
77      public static final List<String> DEFAULT_IGNORED_PATCH_PATTERNS;
78  
79      static {
80          List<String> watches = new ArrayList<>();
81  
82          watches.add("fail");
83          watches.add("skip");
84          watches.add("reject");
85  
86          PATCH_FAILURE_WATCH_PHRASES = watches;
87  
88          List<String> ignored = new ArrayList<>();
89  
90          ignored.add(".svn");
91          ignored.add("CVS");
92  
93          DEFAULT_IGNORED_PATCHES = ignored;
94  
95          List<String> ignoredPatterns = new ArrayList<>();
96  
97          ignoredPatterns.add(".svn/**");
98          ignoredPatterns.add("CVS/**");
99  
100         DEFAULT_IGNORED_PATCH_PATTERNS = ignoredPatterns;
101     }
102 
103     /**
104      * Whether to exclude default ignored patch items, such as <code>.svn</code> or <code>CVS</code> directories.
105      */
106     @Parameter(defaultValue = "true")
107     private boolean useDefaultIgnores;
108 
109     /**
110      * The list of patch file names, supplying the order in which patches should be applied. The path names in this list
111      * must be relative to the base directory specified by the parameter <code>patchDirectory</code>. This parameter
112      * is mutually exclusive with the <code>patchfile</code> parameter.
113      */
114     @Parameter
115     protected List<String> patches;
116 
117     /**
118      * Whether to skip this goal's execution.
119      */
120     @Parameter(alias = "patch.apply.skip", defaultValue = "false")
121     private boolean skipApplication;
122 
123     /**
124      * Flag to enable/disable optimization file from being written. This file tracks the patches that were applied the
125      * last time this goal actually executed. It is required for cases where project-sources optimizations are enabled,
126      * since project-sources will not be re-unpacked if they are at least as fresh as the source archive. If we avoid
127      * re-unpacking project sources, we need to make sure we don't reapply patches.<br/> <strong>Note:</strong> If the
128      * list of patches changes and this flag is enabled, a "<code>mvn clean</code>" must be executed before the next
129      * build, to remove the tracking file.
130      */
131     @Parameter(defaultValue = "true")
132     private boolean optimizations;
133 
134     /**
135      * This is the tracking file used to maintain a list of the patches applied to the unpacked project sources which
136      * are currently in the target directory. If this file is present, and project-source unpacking is optimized
137      * (meaning it won't re-unpack unless the project-sources archive is newer), this goal will not execute and no
138      * patches will be applied in the current build.
139      */
140     @Parameter(defaultValue = "${project.build.directory}/optimization-files/patches-applied.txt")
141     private File patchTrackingFile;
142 
143     /**
144      * The target directory for applying patches. Files in this directory will be modified.
145      */
146     @Parameter(alias = "patchTargetDir", defaultValue = "${project.build.sourceDirectory}")
147     private File targetDirectory;
148 
149     /**
150      * Flag being <code>true</code> if the desired behavior is to fail the build on the first failed patch detected.
151      */
152     @Parameter(defaultValue = "true")
153     private boolean failFast;
154 
155     /**
156      * Setting natural order processing to <code>true</code> will cause all patches in a directory to be processed in
157      * a natural order alleviating the need to declare patches directly in the project file.
158      */
159     @Parameter(defaultValue = "false")
160     private boolean naturalOrderProcessing;
161 
162     /**
163      * When the <code>strictPatching</code> flag is set, this parameter is useful to mark certain contents of the
164      * patch-source directory that should be ignored without causing the build to fail.
165      */
166     @Parameter
167     private List ignoredPatches;
168 
169     /**
170      * Flag that, when set to <code>true</code>, will make sure that all patches included in the <code>patches</code>
171      * list must be present and describe the full contents of the patch directory. If <code>strictPatching</code> is
172      * set to <code>true</code>, and the <code>patches</code> list has a value that does not correspond to a file
173      * in the patch directory, the build will fail. If <code>strictPatching</code> is set to <code>true</code>, and
174      * the patch directory contains files not listed in the <code>patches</code> parameter, the build will fail. If
175      * set to <code>false</code>, only the patches listed in the <code>patches</code> parameter that have
176      * corresponding files will be applied; the rest will be ignored.
177      */
178     @Parameter(defaultValue = "false")
179     private boolean strictPatching;
180 
181     /**
182      * The number of directories to be stripped from patch file paths, before applying, starting from the leftmost, or
183      * root-est.
184      */
185     @Parameter(defaultValue = "0")
186     private int strip;
187 
188     /**
189      * Whether to ignore whitespaces when applying the patches.
190      */
191     @Parameter(defaultValue = "true")
192     private boolean ignoreWhitespace;
193 
194     /**
195      * Whether to treat these patches as having reversed source and dest in the patch syntax.
196      */
197     @Parameter(defaultValue = "false")
198     private boolean reverse;
199 
200     /**
201      * Whether to make backups of the original files before modding them.
202      */
203     @Parameter(defaultValue = "false")
204     private boolean backups;
205 
206     /**
207      * List of phrases to watch for in the command output from the patch tool. If one is found, it will cause the build
208      * to fail. All phrases should be lower-case <em>only</em>. By default, the phrases <code>fail</code>,
209      * <code>skip</code> and <code>reject</code> are used.
210      */
211     @Parameter
212     private List<String> failurePhrases = PATCH_FAILURE_WATCH_PHRASES;
213 
214     /**
215      * The original file which will be modified by the patch. By default, the patch tool will automatically derive the
216      * original file from the header of the patch file.
217      */
218     @Parameter
219     private File originalFile;
220 
221     /**
222      * The output file which is the original file, plus modifications from the patch. By default, the file(s) will be
223      * patched inplace.
224      */
225     @Parameter
226     private File destFile;
227 
228     /**
229      * The single patch file to apply. This parameter is mutually exclusive with the <code>patches</code> parameter.
230      */
231     @Parameter
232     private File patchFile;
233 
234     /**
235      * The base directory for the file names specified by the parameter <code>patches</code>.
236      */
237     @Parameter(defaultValue = "src/main/patches")
238     private File patchDirectory;
239 
240     /**
241      * When set to <code>true</code>, the empty files resulting from the patching process are removed. Empty ancestor
242      * directories are removed as well.
243      *
244      * @since 1.1
245      */
246     @Parameter(defaultValue = "false")
247     private boolean removeEmptyFiles;
248 
249     /**
250      * apply --binary option to patch command line
251      * @since 1.2
252      */
253     @Parameter(defaultValue = "false")
254     private boolean binary;
255 
256     /**
257      * List of ANT-style exclude patterns. These patterns are used to exclude patch files found
258      * in the {@link ApplyMojo#patchDirectory}. These exclude patterns does not apply to a static defined list of
259      * {@link ApplyMojo#patches}.
260      *
261      * @since 1.3
262      */
263     @Parameter
264     private List<String> excludes;
265 
266     /**
267      * Apply the patches. Give preference to patchFile over patchSourceDir/patches, and preference to originalFile over
268      * workDir.
269      */
270     public void execute() throws MojoExecutionException, MojoFailureException {
271         boolean patchDirEnabled = ((patches != null) && !patches.isEmpty()) || naturalOrderProcessing;
272         boolean patchFileEnabled = patchFile != null;
273 
274         // if patches is null or empty, and naturalOrderProcessing is not true then disable patching
275         if (!patchFileEnabled && !patchDirEnabled) {
276             getLog().info("Patching is disabled for this project.");
277             return;
278         }
279 
280         if (skipApplication) {
281             getLog().info("Skipping patch file application (per configuration).");
282             return;
283         }
284 
285         patchTrackingFile.getParentFile().mkdirs();
286 
287         Map patchesToApply;
288 
289         try {
290             if (patchFileEnabled) {
291                 patchesToApply = Collections.singletonMap(patchFile.getName(), createPatchCommand(patchFile));
292             } else {
293                 if (!patchDirectory.isDirectory()) {
294                     throw new FileNotFoundException(
295                             "The base directory for patch files does not exist: " + patchDirectory);
296                 }
297 
298                 String excludePatterns = null;
299                 if (excludes != null) {
300                     excludePatterns = StringUtils.join(excludes.iterator(), ",");
301                     getLog().info("Exclude pattern: " + excludePatterns);
302                 }
303 
304                 List<String> foundPatchFiles = FileUtils.getFileNames(patchDirectory, "*", excludePatterns, false);
305 
306                 patchesToApply = findPatchesToApply(foundPatchFiles, patchDirectory);
307 
308                 checkStrictPatchCompliance(foundPatchFiles);
309             }
310 
311             String output = applyPatches(patchesToApply);
312 
313             checkForWatchPhrases(output);
314 
315             writeTrackingFile(patchesToApply);
316         } catch (IOException ioe) {
317             throw new MojoExecutionException("Unable to obtain list of patch files", ioe);
318         }
319     }
320 
321     private Map<String, Commandline> findPatchesToApply(List foundPatchFiles, File patchSourceDir)
322             throws MojoFailureException {
323         Map<String, Commandline> patchesApplied = new LinkedHashMap<>();
324 
325         if (naturalOrderProcessing) {
326             patches = new ArrayList(foundPatchFiles);
327             Collections.sort(patches);
328         }
329 
330         String alreadyAppliedPatches = "";
331 
332         try {
333             if (optimizations && patchTrackingFile.exists()) {
334                 alreadyAppliedPatches = FileUtils.fileRead(patchTrackingFile);
335             }
336         } catch (IOException ioe) {
337             throw new MojoFailureException("unable to read patch tracking file: " + ioe.getMessage());
338         }
339 
340         for (String patch : patches) {
341             if (!alreadyAppliedPatches.contains(patch)) {
342                 File patchFile = new File(patchSourceDir, patch);
343                 getLog().debug("Looking for patch: " + patch + " in: " + patchFile);
344 
345                 if (!patchFile.exists()) {
346                     if (strictPatching) {
347                         throw new MojoFailureException(
348                                 this,
349                                 "Patch operation cannot proceed.",
350                                 "Cannot find specified patch: \'" + patch
351                                         + "\' in patch-source directory: \'" + patchSourceDir
352                                         + "\'.\n\nEither fix this error, "
353                                         + "or relax strictPatching.");
354                     } else {
355                         getLog().info("Skipping patch: " + patch + " listed in the parameter \"patches\"; "
356                                 + "it is missing.");
357                     }
358                 } else {
359                     foundPatchFiles.remove(patch);
360 
361                     patchesApplied.put(patch, createPatchCommand(patchFile));
362                 }
363             }
364         }
365 
366         return patchesApplied;
367     }
368 
369     private void checkStrictPatchCompliance(List<String> foundPatchFiles) throws MojoExecutionException {
370         if (strictPatching) {
371             List<String> ignored = new ArrayList<>();
372 
373             if (ignoredPatches != null) {
374                 ignored.addAll(ignoredPatches);
375             }
376 
377             if (useDefaultIgnores) {
378                 ignored.addAll(DEFAULT_IGNORED_PATCHES);
379             }
380 
381             List<String> limbo = new ArrayList(foundPatchFiles);
382             limbo.removeAll(ignored);
383 
384             if (!limbo.isEmpty()) {
385                 StringBuilder extraFileBuffer = new StringBuilder();
386 
387                 extraFileBuffer.append("Found ").append(limbo.size()).append(" unlisted patch files:");
388 
389                 for (String patch : foundPatchFiles) {
390                     extraFileBuffer.append("\n  \'").append(patch).append('\'');
391                 }
392 
393                 extraFileBuffer.append("\n\nEither remove these files, "
394                         + "add them to the patches configuration list, " + "or relax strictPatching.");
395 
396                 throw new MojoExecutionException(extraFileBuffer.toString());
397             }
398         }
399     }
400 
401     private String applyPatches(Map<String, Commandline> patchesApplied) throws MojoExecutionException {
402         final StringWriter outputWriter = new StringWriter();
403 
404         StreamConsumer consumer = new StreamConsumer() {
405             public void consumeLine(String line) {
406                 if (getLog().isDebugEnabled()) {
407                     getLog().debug(line);
408                 }
409 
410                 outputWriter.write(line + "\n");
411             }
412         };
413 
414         // used if failFast is false
415         List<String> failedPatches = new ArrayList<>();
416 
417         for (Entry<String, Commandline> entry : patchesApplied.entrySet()) {
418             String patchName = entry.getKey();
419             Commandline cli = entry.getValue();
420 
421             try {
422                 getLog().info("Applying patch: " + patchName);
423 
424                 int result = executeCommandLine(cli, consumer, consumer);
425 
426                 if (result != 0) {
427                     if (failFast) {
428                         throw new MojoExecutionException("Patch command failed with exit code " + result + " for "
429                                 + patchName + ". Please see console and debug output for more information.");
430                     } else {
431                         failedPatches.add(patchName);
432                     }
433                 }
434             } catch (CommandLineException e) {
435                 throw new MojoExecutionException(
436                         "Failed to apply patch: " + patchName + ". See debug output for more information.", e);
437             }
438         }
439 
440         if (!failedPatches.isEmpty()) {
441             getLog().error("Failed applying one or more patches:");
442             for (String failedPatch : failedPatches) {
443                 getLog().error("* " + failedPatch);
444             }
445             throw new MojoExecutionException("Patch command failed for one or more patches."
446                     + " See console and debug output for more information.");
447         }
448 
449         return outputWriter.toString();
450     }
451 
452     private int executeCommandLine(Commandline cli, StreamConsumer out, StreamConsumer err)
453             throws CommandLineException {
454         if (getLog().isDebugEnabled()) {
455             getLog().debug("Executing: " + cli);
456         }
457 
458         int result = CommandLineUtils.executeCommandLine(cli, out, err);
459 
460         if (getLog().isDebugEnabled()) {
461             getLog().debug("Exit code: " + result);
462         }
463 
464         return result;
465     }
466 
467     private void writeTrackingFile(Map patchesApplied) throws MojoExecutionException {
468         boolean appending = patchTrackingFile.exists();
469 
470         try (Writer writer = new FileWriter(patchTrackingFile, appending)) {
471             for (Iterator it = patchesApplied.keySet().iterator(); it.hasNext(); ) {
472                 if (appending) {
473                     writer.write(System.getProperty("line.separator"));
474                 }
475 
476                 String patch = (String) it.next();
477                 writer.write(patch);
478 
479                 if (it.hasNext()) {
480                     writer.write(System.getProperty("line.separator"));
481                 }
482             }
483         } catch (IOException e) {
484             throw new MojoExecutionException("Failed to write patch-tracking file: " + patchTrackingFile, e);
485         }
486     }
487 
488     private void checkForWatchPhrases(String output) throws MojoExecutionException {
489         for (String phrase : failurePhrases) {
490             if (output.contains(phrase)) {
491                 throw new MojoExecutionException("Failed to apply patches (detected watch-phrase: \'" + phrase
492                         + "\' in output). " + "If this is in error, configure the patchFailureWatchPhrases parameter.");
493             }
494         }
495     }
496 
497     /**
498      * Add a new Patch task to the calling mechanism. Give preference to originalFile/destFile, then workDir, and
499      * finally ${basedir}.
500      */
501     private Commandline createPatchCommand(File patchFile) {
502         Commandline cli = new Commandline();
503 
504         cli.setExecutable("patch");
505 
506         cli.setWorkingDirectory(targetDirectory.getAbsolutePath());
507 
508         cli.createArg().setValue("-p" + strip);
509 
510         if (binary) {
511             cli.createArg().setValue("--binary");
512         }
513 
514         if (ignoreWhitespace) {
515             cli.createArg().setValue("-l");
516         }
517 
518         if (reverse) {
519             cli.createArg().setValue("-R");
520         }
521 
522         if (backups) {
523             cli.createArg().setValue("-b");
524         }
525 
526         if (removeEmptyFiles) {
527             cli.createArg().setValue("-E");
528         }
529 
530         cli.createArg().setValue("-i");
531         cli.createArg().setFile(patchFile);
532 
533         if (destFile != null) {
534             cli.createArg().setValue("-o");
535             cli.createArg().setFile(destFile);
536         }
537 
538         if (originalFile != null) {
539             cli.createArg().setFile(originalFile);
540         }
541 
542         return cli;
543     }
544 }