View Javadoc
1   package com.simpligility.maven.plugins.androidndk.common;
2   
3   import com.simpligility.maven.plugins.androidndk.AndroidNdk;
4   import org.apache.commons.io.FileUtils;
5   import org.apache.maven.artifact.Artifact;
6   import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
7   import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
8   import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
9   import org.apache.maven.artifact.resolver.filter.OrArtifactFilter;
10  import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
11  import org.apache.maven.model.Dependency;
12  import org.apache.maven.model.Exclusion;
13  import org.apache.maven.plugin.MojoExecutionException;
14  import org.apache.maven.plugin.logging.Log;
15  import org.apache.maven.project.MavenProject;
16  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
17  import org.apache.maven.shared.dependency.graph.DependencyNode;
18  import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor;
19  
20  import java.io.File;
21  import java.io.FileNotFoundException;
22  import java.io.FilenameFilter;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.LinkedHashSet;
26  import java.util.List;
27  import java.util.Scanner;
28  import java.util.Set;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  /**
33   * @author Johan Lindquist
34   */
35  public class NativeHelper
36  {
37  
38      public static final int NDK_REQUIRED_VERSION = 7;
39  
40      private MavenProject project;
41      private DependencyGraphBuilder dependencyGraphBuilder;
42      private Log log;
43  
44      public NativeHelper( MavenProject project, DependencyGraphBuilder dependencyGraphBuilder, Log log )
45      {
46          this.project = project;
47          this.dependencyGraphBuilder = dependencyGraphBuilder;
48          this.log = log;
49      }
50  
51      public static boolean hasStaticNativeLibraryArtifact ( Set<Artifact> resolveNativeLibraryArtifacts )
52      {
53          for ( Artifact resolveNativeLibraryArtifact : resolveNativeLibraryArtifacts )
54          {
55              if ( Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( resolveNativeLibraryArtifact.getType() ) )
56              {
57                  return true;
58              }
59          }
60          return false;
61      }
62  
63      public static boolean hasSharedNativeLibraryArtifact ( Set<Artifact> resolveNativeLibraryArtifacts )
64      {
65          for ( Artifact resolveNativeLibraryArtifact : resolveNativeLibraryArtifacts )
66          {
67              if ( Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( resolveNativeLibraryArtifact.getType() ) )
68              {
69                  return true;
70              }
71          }
72          return false;
73      }
74  
75      public Set<Artifact> getNativeDependenciesArtifacts( boolean sharedLibraries )
76              throws MojoExecutionException
77      {
78          log.debug( "Finding native dependencies. shared=" + sharedLibraries );
79          final Set<Artifact> filteredArtifacts = new LinkedHashSet<Artifact>();
80          final Set<Artifact> allArtifacts = new LinkedHashSet<Artifact>();
81  
82          // Add all dependent artifacts declared in the pom file
83          // Note: The result of project.getDependencyArtifacts() can be an UnmodifiableSet so we 
84          //       have created our own above and add to that.
85          allArtifacts.addAll( project.getDependencyArtifacts() );
86  
87          // Add all attached artifacts as well - this could come from the NDK mojo for example
88          allArtifacts.addAll( project.getAttachedArtifacts() );
89  
90          // Add all transitive artifacts as well
91          // this allows armeabi classifier -> apklib -> apklib -> apk chaining to only include armeabi in APK
92          allArtifacts.addAll( project.getArtifacts() );
93  
94          for ( Artifact artifact : allArtifacts )
95          {
96              log.debug( "Checking artifact : " + artifact );
97              // A null value in the scope indicates that the artifact has been attached
98              // as part of a previous build step (NDK mojo)
99              if ( isNativeLibrary( sharedLibraries, artifact.getType() ) && artifact.getScope() == null )
100             {
101                 // Including attached artifact
102                 log.debug( "Including attached artifact: " + artifact + ". Artifact scope is not set." );
103                 filteredArtifacts.add( artifact );
104             }
105             else
106             {
107                 if ( isNativeLibrary( sharedLibraries, artifact.getType() ) && (
108                         Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) || Artifact.SCOPE_RUNTIME
109                                 .equals( artifact.getScope() ) ) )
110                 {
111                     log.debug( "Including attached artifact: " + artifact + ". Artifact scope is Compile or Runtime." );
112                     filteredArtifacts.add( artifact );
113                 }
114                 else
115                 {
116                     final String type = artifact.getType();
117 
118                     // FIXME: These *may* contain native libraries - for now we simply add the to the list
119                     // FIXME: of artifacts and whether or not they are used is determined in the MakefileHelper
120                     // FIXME: when the makefile is generated - should really be done here but alas for now
121                     if ( AndroidExtension.APKLIB.equals( type ) || AndroidExtension.AAR.equals( type ) )
122                     {
123                         filteredArtifacts.add( artifact );
124                     }
125                     else
126                     {
127                         // Not checking any other types for now ...
128                     }
129                 }
130             }
131         }
132 
133         Set<Artifact> transitiveArtifacts = processTransitiveDependencies( project.getDependencies(), sharedLibraries );
134 
135         filteredArtifacts.addAll( transitiveArtifacts );
136 
137         return filteredArtifacts;
138     }
139 
140     private boolean isNativeLibrary( boolean sharedLibraries, String artifactType )
141     {
142         return ( sharedLibraries
143                 ? Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( artifactType )
144                 : Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( artifactType )
145         );
146     }
147 
148     private Set<Artifact> processTransitiveDependencies( List<Dependency> dependencies, boolean sharedLibraries )
149             throws MojoExecutionException
150     {
151         final Set<Artifact> transitiveArtifacts = new LinkedHashSet<Artifact>();
152         for ( Dependency dependency : dependencies )
153         {
154             if ( !Artifact.SCOPE_PROVIDED.equals( dependency.getScope() ) && !dependency.isOptional() )
155             {
156                 final Set<Artifact> transArtifactsFor = processTransitiveDependencies( dependency, sharedLibraries );
157                 log.debug( "Found transitive dependencies for : " + dependency + " transDeps : " + transArtifactsFor );
158                 transitiveArtifacts.addAll( transArtifactsFor );
159             }
160         }
161 
162         return transitiveArtifacts;
163 
164     }
165 
166     private Set<Artifact> processTransitiveDependencies( Dependency dependency, boolean sharedLibraries )
167             throws MojoExecutionException
168     {
169         log.debug( "Processing transitive dependencies for : " + dependency );
170 
171         try
172         {
173             final Set<Artifact> artifacts = new LinkedHashSet<Artifact>();
174 
175             final List<String> exclusionPatterns = new ArrayList<String>();
176             if ( dependency.getExclusions() != null && !dependency.getExclusions().isEmpty() )
177             {
178                 for ( final Exclusion exclusion : dependency.getExclusions() )
179                 {
180                     exclusionPatterns.add( exclusion.getGroupId() + ":" + exclusion.getArtifactId() );
181                 }
182             }
183             final ArtifactFilter optionalFilter = new ArtifactFilter()
184             {
185                 @Override
186                 public boolean include( Artifact artifact )
187                 {
188                     return !artifact.isOptional();
189                 }
190             };
191 
192             final AndArtifactFilter filter = new AndArtifactFilter();
193             filter.add( new ExcludesArtifactFilter( exclusionPatterns ) );
194             filter.add( new OrArtifactFilter( Arrays.<ArtifactFilter>asList( new ScopeArtifactFilter( "compile" ),
195                     new ScopeArtifactFilter( "runtime" ),
196                     new ScopeArtifactFilter( "test" ) ) ) );
197             filter.add( optionalFilter );
198 
199             final DependencyNode node = dependencyGraphBuilder.buildDependencyGraph( project, filter );
200             final CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor();
201             node.accept( collectingVisitor );
202 
203             final List<DependencyNode> dependencies = collectingVisitor.getNodes();
204             for ( final DependencyNode dep : dependencies )
205             {
206                 final boolean isNativeLibrary = isNativeLibrary( sharedLibraries, dep.getArtifact().getType() );
207                 log.debug( "Processing library : " + dep.getArtifact() + " isNative=" + isNativeLibrary );
208                 if ( isNativeLibrary )
209                 {
210                     artifacts.add( dep.getArtifact() );
211                 }
212             }
213 
214             return artifacts;
215         }
216         catch ( Exception e )
217         {
218             throw new MojoExecutionException( "Error while processing transitive dependencies", e );
219         }
220     }
221 
222     public static void validateNDKVersion( File ndkHomeDir ) throws MojoExecutionException
223     {
224         final File ndkSourcePropertiesFile = new File( ndkHomeDir, "source.properties" );
225 
226         if ( ndkSourcePropertiesFile.exists() )
227         {
228             // As of 11 this file is a sign of a good release
229             return;
230         }
231 
232         final File ndkVersionFile = new File( ndkHomeDir, "RELEASE.TXT" );
233 
234         if ( !ndkVersionFile.exists() )
235         {
236             throw new MojoExecutionException(
237                     "Could not locate RELEASE.TXT in the Android NDK base directory '" + ndkHomeDir.getAbsolutePath()
238                             + "'.  Please verify your setup! " + AndroidNdk.PROPER_NDK_HOME_DIRECTORY_MESSAGE );
239         }
240 
241         try
242         {
243             String versionStr = FileUtils.readFileToString( ndkVersionFile );
244             validateNDKVersion( NDK_REQUIRED_VERSION, versionStr );
245         }
246         catch ( Exception e )
247         {
248             throw new MojoExecutionException( "Error while extracting NDK version from '"
249                     + ndkVersionFile.getAbsolutePath() + "'. Please verify your setup! "
250                     + AndroidNdk.PROPER_NDK_HOME_DIRECTORY_MESSAGE );
251         }
252     }
253 
254     public static void validateNDKVersion( int desiredVersion, String versionStr ) throws MojoExecutionException
255     {
256 
257         int version = 0;
258 
259         if ( versionStr != null )
260         {
261             versionStr = versionStr.trim();
262             Pattern pattern = Pattern.compile( "[r]([0-9]{1,3})([a-z]{0,1}).*" );
263             Matcher m = pattern.matcher( versionStr );
264             if ( m.matches() )
265             {
266                 final String group = m.group( 1 );
267                 version = Integer.parseInt( group );
268             }
269         }
270 
271         if ( version < desiredVersion )
272         {
273             throw new MojoExecutionException( "You are running an old NDK (version " + versionStr + "), please update "
274                     + "to at least r'" + desiredVersion + "' or later" );
275         }
276     }
277 
278     public static String[] getAppAbi( File applicationMakefile )
279     {
280         Scanner scanner = null;
281         try
282         {
283             if ( applicationMakefile != null && applicationMakefile.exists() )
284             {
285                 scanner = new Scanner( applicationMakefile );
286                 try
287                 {
288                     while ( scanner.hasNextLine() )
289                     {
290                         String line = scanner.nextLine().trim();
291                         if ( line.startsWith( "APP_ABI" ) )
292                         {
293                             return line.substring( line.indexOf( ":=" ) + 2 ).trim().split( " " );
294                         }
295                     }
296                 }
297                 finally
298                 {
299                     scanner.close();
300                 }
301             }
302         }
303         catch ( FileNotFoundException e )
304         {
305             // do nothing
306         }
307         return null;
308     }
309 
310 
311     /**
312      * Extracts, if embedded correctly, the artifacts architecture from its classifier.  The format of the
313      * classifier, if including the architecture is &lt;architecture&gt;-&lt;classifier&gt;.  If no
314      * architecture is embedded in the classifier, 'armeabi' will be returned.
315      *
316      * @param artifact            The artifact to retrieve the classifier from.
317      * @param defaultArchitecture The architecture to return if can't be resolved from the classifier
318      * @return The retrieved architecture, or <code>defaulArchitecture</code> if not resolveable
319      */
320     public static String extractArchitectureFromArtifact( Artifact artifact, final String defaultArchitecture )
321     {
322         String classifier = artifact.getClassifier();
323         if ( classifier != null )
324         {
325             //
326             // We loop backwards to catch the case where the classifier is
327             // potentially armeabi-v7a - this collides with armeabi if looping
328             // through this loop in the other direction
329             //
330 
331             for ( int i = AndroidNdk.NDK_ARCHITECTURES.length - 1; i >= 0; i-- )
332             {
333                 String ndkArchitecture = AndroidNdk.NDK_ARCHITECTURES[ i ];
334                 if ( classifier.startsWith( ndkArchitecture ) )
335                 {
336                     return ndkArchitecture;
337                 }
338             }
339 
340         }
341         // Default case is to return the default architecture
342         return defaultArchitecture;
343     }
344 
345     /**
346      * Attempts to extract, from various sources, the applicable list of NDK architectures to support
347      * as part of the build.
348      * <br/>
349      * <br/>
350      * It retrieves the list from the following places:
351      * <ul>
352      * <li>ndkArchitecture parameter</li>
353      * <li>projects Application.mk - currently only a single architecture is supported by this method</li>
354      * </ul>
355      *
356      * @param ndkArchitectures    Space separated list of architectures.  This may be from configuration or otherwise
357      * @param applicationMakefile The makefile (Application.mk) to retrieve the list from.
358      * @param basedir             Directory the build is running from (to resolve files)
359      * @return List of architectures to be supported by build.
360      * @throws MojoExecutionException
361      */
362     public static String[] getNdkArchitectures( final String ndkArchitectures, final String applicationMakefile, final File basedir ) throws MojoExecutionException
363     {
364         String[] resolvedArchitectures = null;
365         // if there is a specified ndk architecture, return it
366         if ( ndkArchitectures != null )
367         {
368             resolvedArchitectures = ndkArchitectures.split( " " );
369         }
370 
371         if ( resolvedArchitectures == null )
372         {
373             // if there is no application makefile specified, let's use the default one
374             String applicationMakefileToUse = applicationMakefile;
375             if ( applicationMakefileToUse == null )
376             {
377                 applicationMakefileToUse = "jni/Application.mk";
378             }
379 
380             // now let's see if the application file exists
381             File appMK = new File ( basedir, applicationMakefileToUse );
382             if ( appMK.exists () )
383             {
384                 String[] foundNdkArchitectures = getAppAbi ( appMK );
385                 if ( foundNdkArchitectures != null )
386                 {
387                     resolvedArchitectures = foundNdkArchitectures;
388                 }
389             }
390         }
391 
392         // If still not fond, we default it to armeabi
393         if ( resolvedArchitectures == null )
394         {
395             resolvedArchitectures = new String[] { "armeabi" };
396         }
397 
398         List<String> processedResolvedArchitectures = new ArrayList<> (  );
399 
400         for ( int i = 0; i < resolvedArchitectures.length; i++ )
401         {
402             final String resolvedArchitecture = resolvedArchitectures[ i ].trim ();
403             if ( resolvedArchitecture.length () > 0 )
404             {
405                 processedResolvedArchitectures.add( resolvedArchitecture );
406             }
407         }
408 
409         // return a default ndk architecture
410         return processedResolvedArchitectures.toArray ( new String[ processedResolvedArchitectures.size () ] );
411     }
412 
413     /**
414      * Helper method for determining whether the specified architecture is a match for the
415      * artifact using its classifier.  When used for architecture matching, the classifier must be
416      * formed by &lt;architecture&gt;-&lt;classifier&gt;.
417      * If the artifact is legacy and defines no valid architecture, the artifact architecture will
418      * default to <strong>armeabi</strong>.
419      *
420      * @param ndkArchitecture Architecture to check for match
421      * @param artifact        Artifact to check the classifier match for
422      * @return True if the architecture matches, otherwise false
423      */
424     public static boolean artifactHasHardwareArchitecture( Artifact artifact, String ndkArchitecture,
425                                                            String defaultArchitecture )
426     {
427         return Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( artifact.getType() )
428                 && ndkArchitecture.equals( extractArchitectureFromArtifact( artifact, defaultArchitecture ) );
429     }
430 
431     /** Checks whether or not the specified project provides a native library artifact as its primary artifact.
432      *
433      * @param mavenProject Project to check
434      * @return True if the packaging of the project is either "a" or "so" (both defining native artifacts).
435      */
436     public static boolean isNativeArtifactProject( MavenProject mavenProject )
437     {
438         final String packaging = mavenProject.getPackaging();
439         return Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( packaging ) || Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( packaging );
440     }
441 
442     public static File[] listNativeFiles( Artifact artifact, File unpackDirectory, final boolean staticLibrary, final String architecture )
443     {
444         final List<File> acceptedFiles = new ArrayList<File> (  );
445         File libsFolder = new File( unpackDirectory, architecture );
446         if ( libsFolder.exists () )
447         {
448             // list all the files
449             return libsFolder.listFiles( new FilenameFilter ()
450             {
451                 public boolean accept( final File dir, final String name )
452                 {
453                     return name.startsWith( "lib" ) && name.endsWith( ( staticLibrary ? ".a" : ".so" ) );
454                 }
455             } );
456         }
457         return new File[0];
458     }
459 
460 
461 }