View Javadoc

1   /*
2    * #%L
3    * Native ARchive plugin for Maven
4    * %%
5    * Copyright (C) 2002 - 2014 NAR Maven Plugin developers.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   * http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package com.github.maven_nar;
21  
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import java.util.*;
31  import java.util.regex.Pattern;
32  
33  import org.apache.bcel.classfile.ClassParser;
34  import org.apache.bcel.classfile.JavaClass;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugin.logging.Log;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.codehaus.plexus.util.FileUtils;
41  import org.codehaus.plexus.util.cli.Commandline;
42  
43  /**
44   * @author Mark Donszelmann
45   */
46  public final class NarUtil {
47    private static final class StreamGobbler extends Thread {
48      private final InputStream is;
49  
50      private final TextStream ts;
51  
52      private StreamGobbler(final InputStream is, final TextStream ts) {
53        this.is = is;
54        this.ts = ts;
55      }
56  
57      @Override
58      public void run() {
59        try {
60          final BufferedReader reader = new BufferedReader(new InputStreamReader(this.is));
61          String line = null;
62          while ((line = reader.readLine()) != null) {
63            this.ts.println(line);
64          }
65          reader.close();
66        } catch (final IOException e) {
67          // e.printStackTrace()
68          final StackTraceElement[] stackTrace = e.getStackTrace();
69          for (final StackTraceElement element : stackTrace) {
70            this.ts.println(element.toString());
71          }
72        }
73      }
74    }
75  
76    public static final String DEFAULT_EXCLUDES = "**/*~,**/#*#,**/.#*,**/%*%,**/._*,"
77        + "**/CVS,**/CVS/**,**/.cvsignore," + "**/SCCS,**/SCCS/**,**/vssver.scc," + "**/.svn,**/.svn/**,**/.DS_Store";
78  
79    public static String addLibraryPathToEnv(final String path, final Map environment, final String os) {
80      String pathName = null;
81      char separator = ' ';
82      switch (os) {
83        case OS.WINDOWS:
84          pathName = "PATH";
85          separator = ';';
86          break;
87        case OS.MACOSX:
88          pathName = "DYLD_LIBRARY_PATH";
89          separator = ':';
90          break;
91        case OS.AIX:
92          pathName = "LIBPATH";
93          separator = ':';
94          break;
95        default:
96          pathName = "LD_LIBRARY_PATH";
97          separator = ':';
98          break;
99      }
100 
101     String value = environment != null ? (String) environment.get(pathName) : null;
102     if (value == null) {
103       value = NarUtil.getEnv(pathName, pathName, null);
104     }
105 
106     String libPath = path;
107     libPath = libPath.replace(File.pathSeparatorChar, separator);
108     if (value != null) {
109       value += separator + libPath;
110     } else {
111       value = libPath;
112     }
113     if (environment != null) {
114       environment.put(pathName, value);
115     }
116     return pathName + "=" + value;
117   }
118 
119   /**
120    * (Darren) this code lifted from mvn help:active-profiles plugin Recurses
121    * into the project's parent poms to find the active profiles of the
122    * specified project and all its parents.
123    * 
124    * @param project
125    *          The project to start with
126    * @return A list of active profiles
127    */
128   static List collectActiveProfiles(final MavenProject project) {
129     final List profiles = project.getActiveProfiles();
130 
131     if (project.hasParent()) {
132       profiles.addAll(collectActiveProfiles(project.getParent()));
133     }
134 
135     return profiles;
136   }
137 
138   public static int copyDirectoryStructure(final File sourceDirectory, final File destinationDirectory,
139       final String includes, final String excludes) throws IOException {
140     if (!sourceDirectory.exists()) {
141       throw new IOException("Source directory doesn't exists (" + sourceDirectory.getAbsolutePath() + ").");
142     }
143 
144     final List files = FileUtils.getFiles(sourceDirectory, includes, excludes);
145     final String sourcePath = sourceDirectory.getAbsolutePath();
146 
147     int copied = 0;
148     for (final Object file1 : files) {
149       final File file = (File) file1;
150       String dest = file.getAbsolutePath();
151       dest = dest.substring(sourcePath.length() + 1);
152       final File destination = new File(destinationDirectory, dest);
153       if (file.isFile()) {
154         // destination = destination.getParentFile();
155         // use FileUtils from commons-io, because it preserves timestamps
156         org.apache.commons.io.FileUtils.copyFile(file, destination);
157         copied++;
158 
159         // copy executable bit
160         try {
161           // 1.6 only so coded using introspection
162           // destination.setExecutable( file.canExecute(), false );
163           final Method canExecute = file.getClass().getDeclaredMethod("canExecute");
164           final Method setExecutable = destination.getClass()
165               .getDeclaredMethod("setExecutable", boolean.class, boolean.class);
166           setExecutable
167               .invoke(destination, canExecute.invoke(file), Boolean.FALSE);
168         } catch (final SecurityException | InvocationTargetException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException e) {
169           // ignored
170         }
171       } else if (file.isDirectory()) {
172         if (!destination.exists() && !destination.mkdirs()) {
173           throw new IOException("Could not create destination directory '" + destination.getAbsolutePath() + "'.");
174         }
175         copied += copyDirectoryStructure(file, destination, includes, excludes);
176       } else {
177         throw new IOException("Unknown file type: " + file.getAbsolutePath());
178       }
179     }
180     return copied;
181   }
182 
183   public static void deleteDirectory(final File dir) throws MojoExecutionException {
184     int retries = OS.WINDOWS.equalsIgnoreCase(System.getProperty("os.name")) ? 3 : 1;  // Windows file locking (such as due to virus scanners) and sometimes deleting slowly, or slow to report completion.
185     while (retries > 0) {
186       retries--;
187       try {
188         FileUtils.deleteDirectory(dir);
189         retries = 0;
190       } catch (final IOException e) {
191         if (retries > 0) {
192           Thread.yield();
193         } else {
194           throw new MojoExecutionException("Could not delete directory: " + dir, e);
195         }
196       }
197       if (retries > 0) {
198 //      getLog().info("Could not delete directory: " + dir + " : Retrying");
199         try {
200           Thread.sleep(200);
201         } catch (InterruptedException e) {
202         }
203       //TODO: if( windows and interactive ) prompt for retry?
204       //@Component(role=org.codehaus.plexus.components.interactivity.Prompter.class, hint="archetype")
205       //public class ArchetypePrompter
206       }
207     }
208   }
209   
210   static Set findInstallNameToolCandidates(final File[] files, final Log log)
211       throws MojoExecutionException, MojoFailureException {
212     final HashSet candidates = new HashSet();
213 
214     for (final File file2 : files) {
215       final File file = file2;
216 
217       if (!file.exists()) {
218         continue;
219       }
220 
221       if (file.isDirectory()) {
222         candidates.addAll(findInstallNameToolCandidates(file.listFiles(), log));
223       }
224 
225       final String fileName = file.getName();
226       if (file.isFile() && file.canWrite()
227           && (fileName.endsWith(".so") || fileName.endsWith(".dylib") || fileName.endsWith(".jnilib"))) {
228         candidates.add(file);
229       }
230     }
231 
232     return candidates;
233   }
234 
235   // FIXME, should go to AOL.
236   /*
237    * NOT USED ?
238    * public static String getAOLKey( String architecture, String os, Linker
239    * linker )
240    * throws MojoFailureException, MojoExecutionException
241    * {
242    * // construct AOL key prefix
243    * return getArchitecture( architecture ) + "." + getOS( os ) + "." +
244    * getLinkerName( architecture, os, linker )
245    * + ".";
246    * }
247    */
248 
249   public static AOL getAOL(final MavenProject project, final String architecture, final String os, final Linker linker,
250       final String aol, final Log log) throws MojoFailureException, MojoExecutionException {
251     // adjust aol
252     return aol == null ? new AOL(getArchitecture(architecture), getOS(os), getLinkerName(project, architecture, os,
253         linker, log)) : new AOL(aol);
254   }
255 
256   public static String getAOLKey(final String aol) {
257     // FIXME, this may not always work correctly
258     return replace("-", ".", aol);
259   }
260 
261   public static String getArchitecture(final String architecture) {
262     if (architecture == null) {
263       return System.getProperty("os.arch");
264     }
265     return architecture;
266   }
267 
268   /**
269    * Returns the Bcel Class corresponding to the given class filename
270    * 
271    * @param filename
272    *          the absolute file name of the class
273    * @return the Bcel Class.
274    * @throws IOException
275    */
276   public static JavaClass getBcelClass(final String filename) throws IOException {
277     final ClassParser parser = new ClassParser(filename);
278     return parser.parse();
279   }
280 
281   public static String getEnv(final String envKey, final String alternateSystemProperty, final String defaultValue) {
282     String envValue = null;
283     try {
284       envValue = System.getenv(envKey);
285       if (envValue == null && alternateSystemProperty != null) {
286         envValue = System.getProperty(alternateSystemProperty);
287       }
288     } catch (final Error e) {
289       // JDK 1.4?
290       if (alternateSystemProperty != null) {
291         envValue = System.getProperty(alternateSystemProperty);
292       }
293     }
294 
295     if (envValue == null) {
296       envValue = defaultValue;
297     }
298 
299     return envValue;
300   }
301 
302   /**
303    * Returns the header file name (javah) corresponding to the given class file
304    * name
305    * 
306    * @param filename
307    *          the absolute file name of the class
308    * @return the header file name.
309    */
310   public static String getHeaderName(final String basename, final String filename) {
311     final String base = basename.replaceAll("\\\\", "/");
312     final String file = filename.replaceAll("\\\\", "/");
313     if (!file.startsWith(base)) {
314       throw new IllegalArgumentException("Error " + file + " does not start with " + base);
315     }
316     String header = file.substring(base.length() + 1);
317     header = header.replaceAll("/", "_");
318     header = header.replaceAll("\\.class", ".h");
319     return header;
320   }
321 
322   public static File getJavaHome(final File javaHome, final String os) {
323     File home = javaHome;
324     // adjust JavaHome
325     if (home == null) {
326       home = new File(System.getProperty("java.home"));
327       if (home.getName().equals("jre")) {
328         // we want the JDK base directory, not the JRE subfolder
329         home = home.getParentFile();
330       }
331     }
332     return home;
333   }
334 
335   public static Linker getLinker(final Linker linker, final Log log) {
336     Linker link = linker;
337     if (link == null) {
338       link = new Linker(log);
339     }
340     return link;
341   }
342 
343   public static String getLinkerName(final MavenProject project, final String architecture, final String os,
344       final Linker linker, final Log log) throws MojoFailureException, MojoExecutionException {
345     return getLinker(linker, log).getName(NarProperties.getInstance(project),
346         getArchitecture(architecture) + "." + getOS(os) + ".");
347   }
348 
349   public static String getOS(final String defaultOs) {
350     String os = defaultOs;
351     // adjust OS if not given
352     if (os == null) {
353       os = System.getProperty("os.name");
354       final String name = os.toLowerCase();
355       if (name.startsWith("windows")) {
356         os = OS.WINDOWS;
357       }
358       if (name.startsWith("linux")) {
359         os = OS.LINUX;
360       }
361       if (name.startsWith("freebsd")) {
362         os = OS.FREEBSD;
363       }
364       if (name.equals("mac os x")) {
365         os = OS.MACOSX;
366       }
367     }
368     return os;
369   }
370 
371   public static boolean isWindows() {
372     return Objects.equals(getOS(null), OS.WINDOWS);
373   }
374 
375   public static void makeExecutable(final File file, final Log log) throws MojoExecutionException, MojoFailureException {
376     if (!file.exists()) {
377       return;
378     }
379 
380     if (file.isDirectory()) {
381       final File[] files = file.listFiles();
382       for (final File file2 : files) {
383         makeExecutable(file2, log);
384       }
385     }
386     if (file.isFile() && file.canRead() && file.canWrite() && !file.isHidden()) {
387       // chmod +x file
388       final int result = runCommand("chmod", new String[] {
389           "+x", file.getPath()
390       }, null, null, log);
391       if (result != 0) {
392         throw new MojoExecutionException("Failed to execute 'chmod +x " + file.getPath() + "'" + " return code: \'"
393             + result + "\'.");
394       }
395     }
396   }
397 
398   public static void makeLink(final File file, final Log log) throws MojoExecutionException, MojoFailureException {
399     if (!file.exists()) {
400       return;
401     }
402 
403     if (file.isDirectory()) {
404       final File[] files = file.listFiles();
405       for (final File file2 : files) {
406         makeLink(file2, log);
407       }
408     }
409     if (file.isFile() && file.canRead() && file.canWrite() && !file.isHidden()
410         && file.getName().matches(".*\\.so(\\.\\d+)+$")) {
411       final File sofile = new File(file.getParent(), file.getName().substring(0, file.getName().indexOf(".so") + 3));
412       if (!sofile.exists()) {
413         // ln -s lib.so.xx lib.so
414         final int result = runCommand("ln", new String[] {
415             "-s", file.getName(), sofile.getPath()
416         }, null, null, log);
417         if (result != 0) {
418           throw new MojoExecutionException("Failed to execute 'ln -s " + file.getName() + " " + sofile.getPath() + "'"
419               + " return code: \'" + result + "\'.");
420         }
421       }
422     }
423   }
424 
425   /* for jdk 1.4 */
426   private static String quote(final String s) {
427     final String escQ = "\\Q";
428     final String escE = "\\E";
429 
430     int slashEIndex = s.indexOf(escE);
431     if (slashEIndex == -1) {
432       return escQ + s + escE;
433     }
434 
435     final StringBuffer sb = new StringBuffer(s.length() * 2);
436     sb.append(escQ);
437     slashEIndex = 0;
438     int current = 0;
439     while ((slashEIndex = s.indexOf(escE, current)) != -1) {
440       sb.append(s.substring(current, slashEIndex));
441       current = slashEIndex + 2;
442       sb.append(escE);
443       sb.append("\\");
444       sb.append(escE);
445       sb.append(escQ);
446     }
447     sb.append(s.substring(current, s.length()));
448     sb.append(escE);
449     return sb.toString();
450   }
451 
452   /* for jdk 1.4 */
453   private static String quoteReplacement(final String s) {
454     if (s.indexOf('\\') == -1 && s.indexOf('$') == -1) {
455       return s;
456     }
457     final StringBuffer sb = new StringBuffer();
458     for (int i = 0; i < s.length(); i++) {
459       final char c = s.charAt(i);
460       if (c == '\\') {
461         sb.append('\\');
462         sb.append('\\');
463       } else if (c == '$') {
464         sb.append('\\');
465         sb.append('$');
466       } else {
467         sb.append(c);
468       }
469     }
470     return sb.toString();
471   }
472 
473   static void removeNulls(final Collection<?> collection) {
474     for (final Iterator<?> iter = collection.iterator(); iter.hasNext();) {
475       if (iter.next() == null) {
476         iter.remove();
477       }
478     }
479   }
480 
481   /**
482    * Replaces target with replacement in string. For jdk 1.4 compatiblity.
483    * 
484    * @param target
485    * @param replacement
486    * @param string
487    * @return
488    */
489   public static String replace(final CharSequence target, final CharSequence replacement, final String string) {
490     return Pattern.compile(quote(target.toString())/*
491                                                     * , Pattern.LITERAL jdk 1.4
492                                                     */).matcher(string).replaceAll(
493     /* Matcher. jdk 1.4 */quoteReplacement(replacement.toString()));
494   }
495 
496   public static int runCommand(final String cmd, final String[] args, final File workingDirectory, final String[] env,
497       final Log log) throws MojoExecutionException, MojoFailureException {
498     if (log.isInfoEnabled()) {
499       final StringBuilder argLine = new StringBuilder();
500       if (args != null) {
501         for (final String arg : args) {
502           argLine.append(" ").append(arg);
503         }
504       }
505       if (workingDirectory != null) {
506         log.info("+ cd " + workingDirectory.getAbsolutePath());
507       }
508       log.info("+ " + cmd + argLine);
509     }
510     return runCommand(cmd, args, workingDirectory, env, new TextStream() {
511       @Override
512       public void println(final String text) {
513         log.info(text);
514       }
515     }, new TextStream() {
516       @Override
517       public void println(final String text) {
518         log.error(text);
519       }
520 
521     }, new TextStream() {
522       @Override
523       public void println(final String text) {
524         log.debug(text);
525       }
526     }, log);
527   }
528 
529   public static int runCommand(final String cmd, final String[] args, final File workingDirectory, final String[] env,
530       final TextStream out, final TextStream err, final TextStream dbg, final Log log)
531       throws MojoExecutionException, MojoFailureException {
532     return runCommand(cmd, args, workingDirectory, env, out, err, dbg, log, false);
533   }
534 
535   public static int runCommand(final String cmd, final String[] args, final File workingDirectory, final String[] env,
536       final TextStream out, final TextStream err, final TextStream dbg, final Log log, final boolean expectFailure)
537       throws MojoExecutionException, MojoFailureException {
538     final Commandline cmdLine = new Commandline();
539 
540     try {
541       dbg.println("RunCommand: " + cmd);
542       cmdLine.setExecutable(cmd);
543       if (args != null) {
544         for (final String arg : args) {
545           dbg.println("  '" + arg + "'");
546         }
547         cmdLine.addArguments(args);
548       }
549       if (workingDirectory != null) {
550         dbg.println("in: " + workingDirectory.getPath());
551         cmdLine.setWorkingDirectory(workingDirectory);
552       }
553 
554       if (env != null) {
555         dbg.println("with Env:");
556         for (final String element : env) {
557           final String[] nameValue = element.split("=", 2);
558           if (nameValue.length < 2) {
559             throw new MojoFailureException("   Misformed env: '" + element + "'");
560           }
561           dbg.println("   '" + nameValue[0] + "=" + nameValue[1] + "'");
562           cmdLine.addEnvironment(nameValue[0], nameValue[1]);
563         }
564       }
565 
566       final Process process = cmdLine.execute();
567       final StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), err);
568       final StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), out);
569 
570       errorGobbler.start();
571       outputGobbler.start();
572       process.waitFor();
573       final int exitValue = process.exitValue();
574       dbg.println("ExitValue: " + exitValue);
575       final int timeout = 5000;
576       errorGobbler.join(timeout);
577       outputGobbler.join(timeout);
578       if (exitValue != 0 ^ expectFailure) {
579         if (log == null) {
580           System.err.println(err.toString());
581           System.err.println(out.toString());
582           System.err.println(dbg.toString());
583         } else {
584           log.warn(err.toString());
585           log.warn(out.toString());
586           log.warn(dbg.toString());
587         }
588         throw new MojoExecutionException("exit code: " + exitValue);
589       }
590       return exitValue;
591     } catch (final MojoExecutionException e) {
592       throw e;
593     } catch (final Exception e) {
594       throw new MojoExecutionException("Could not launch " + cmdLine, e);
595     }
596   }
597 
598   static void runInstallNameTool(final File[] files, final Log log) throws MojoExecutionException, MojoFailureException {
599     final Set libs = findInstallNameToolCandidates(files, log);
600 
601     for (final Object lib1 : libs) {
602       final File subjectFile = (File) lib1;
603       final String subjectName = subjectFile.getName();
604       final String subjectPath = subjectFile.getPath();
605 
606       final int idResult = runCommand("install_name_tool", new String[] { "-id", subjectPath, subjectPath
607       }, null, null, log);
608 
609       if (idResult != 0) {
610         throw new MojoExecutionException(
611             "Failed to execute 'install_name_tool -id " + subjectPath + " " + subjectPath + "'" + " return code: \'"
612                 + idResult + "\'.");
613       }
614 
615       for (final Object lib : libs) {
616         final File dependentFile = (File) lib;
617         final String dependentPath = dependentFile.getPath();
618 
619         if (Objects.equals(dependentPath, subjectPath)) {
620           continue;
621         }
622 
623         final int changeResult = runCommand("install_name_tool",
624             new String[] { "-change", subjectName, subjectPath, dependentPath
625             }, null, null, log);
626 
627         if (changeResult != 0) {
628           throw new MojoExecutionException(
629               "Failed to execute 'install_name_tool -change " + subjectName + " " + subjectPath + " " + dependentPath
630                   + "'" + " return code: \'" + changeResult + "\'.");
631         }
632       }
633     }
634   }
635 
636   public static void runRanlib(final File file, final Log log) throws MojoExecutionException, MojoFailureException {
637     if (!file.exists()) {
638       return;
639     }
640 
641     if (file.isDirectory()) {
642       final File[] files = file.listFiles();
643       for (final File file2 : files) {
644         runRanlib(file2, log);
645       }
646     }
647     if (file.isFile() && file.canWrite() && !file.isHidden() && file.getName().endsWith(".a")) {
648       // ranlib file
649       final int result = runCommand("ranlib", new String[] {
650         file.getPath()
651       }, null, null, log);
652       if (result != 0) {
653         throw new MojoExecutionException("Failed to execute 'ranlib " + file.getPath() + "'" + " return code: \'"
654             + result + "\'.");
655       }
656     }
657   }
658 
659   /**
660    * Produces a human-readable string of the given object which has fields
661    * annotated with the Maven {@link Parameter} annotation.
662    * 
663    * @param o The object for which a human-readable string is desired.
664    * @return A human-readable string, with each {@code @Parameter} field on a
665    *         separate line rendered as a key/value pair.
666    */
667   public static String prettyMavenString(final Object o) {
668     final StringBuilder sb = new StringBuilder();
669     sb.append(o.getClass().getName()).append(":\n");
670     for (final Field f : o.getClass().getDeclaredFields()) {
671       if (f.getAnnotation(Parameter.class) == null) continue;
672       sb.append("\t").append(f.getName()).append("=").append(fieldValue(f, o)).append("\n");
673     }
674     return sb.toString();
675   }
676 
677   private static Object fieldValue(final Field f, final Object o) {
678     try {
679       return f.get(o);
680     }
681     catch (final IllegalArgumentException | IllegalAccessException exc) {
682       return "<ERROR>";
683     }
684   }
685 
686   private NarUtil() {
687     // never instantiate
688   }
689 }