View Javadoc
1   package com.simpligility.maven.plugins.android.common;
2   
3   import com.google.common.io.PatternFilenameFilter;
4   import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
5   import com.simpligility.maven.plugins.android.AndroidNdk;
6   
7   import org.apache.maven.artifact.Artifact;
8   import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
9   import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
10  import org.apache.maven.artifact.resolver.filter.ExcludesArtifactFilter;
11  import org.apache.maven.artifact.resolver.filter.OrArtifactFilter;
12  import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
13  import org.apache.maven.model.Dependency;
14  import org.apache.maven.model.Exclusion;
15  import org.apache.maven.plugin.MojoExecutionException;
16  import org.apache.maven.plugin.logging.Log;
17  import org.apache.maven.project.MavenProject;
18  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
19  import org.apache.maven.shared.dependency.graph.DependencyNode;
20  import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor;
21  
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.LinkedHashSet;
27  import java.util.List;
28  import java.util.Scanner;
29  import java.util.Set;
30  
31  import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
32  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;
33  
34  /**
35   * @author Johan Lindquist
36   */
37  public class NativeHelper
38  {
39  
40      public static final int NDK_REQUIRED_VERSION = 7;
41  
42      private MavenProject project;
43      private DependencyGraphBuilder dependencyGraphBuilder;
44      private Log log;
45  
46      public NativeHelper( MavenProject project, DependencyGraphBuilder dependencyGraphBuilder, Log log )
47      {
48          this.project = project;
49          this.dependencyGraphBuilder = dependencyGraphBuilder;
50          this.log = log;
51      }
52  
53      public Set<Artifact> getNativeDependenciesArtifacts(
54              AbstractAndroidMojo mojo, File unpackDirectory, boolean sharedLibraries )
55              throws MojoExecutionException
56      {
57          log.debug( "Finding native dependencies. UnpackFolder=" + unpackDirectory + " shared=" + sharedLibraries );
58          final Set<Artifact> filteredArtifacts = new LinkedHashSet<Artifact>();
59          final Set<Artifact> allArtifacts = new LinkedHashSet<Artifact>();
60          
61          // Add all dependent artifacts declared in the pom file
62          // Note: The result of project.getDependencyArtifacts() can be an UnmodifiableSet so we 
63          //       have created our own above and add to that.
64          allArtifacts.addAll( project.getDependencyArtifacts() );
65  
66          // Add all transitive artifacts as well
67          // this allows armeabi classifier -> apklib -> apklib -> apk chaining to only include armeabi in APK
68          allArtifacts.addAll( project.getArtifacts() );
69  
70          for ( Artifact artifact : allArtifacts )
71          {
72              final boolean isNativeLibrary = isNativeLibrary( sharedLibraries, artifact.getType() );
73  
74              log.debug( "Checking artifact : " + artifact );
75              // A null value in the scope indicates that the artifact has been attached
76              // as part of a previous build step (NDK mojo)
77              if ( isNativeLibrary && artifact.getScope() == null )
78              {
79                  // Including attached artifact
80                  log.debug( "Including attached artifact: " + artifact + ". Artifact scope is not set." );
81                  filteredArtifacts.add( artifact );
82              }
83              else
84              {
85                  if ( isNativeLibrary && (
86                          Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) || Artifact.SCOPE_RUNTIME
87                                  .equals( artifact.getScope() ) ) )
88                  {
89                      log.debug( "Including attached artifact: " + artifact + ". Artifact scope is Compile or Runtime." );
90                      filteredArtifacts.add( artifact );
91                  }
92                  else
93                  {
94                      final String type = artifact.getType();
95  
96                      if ( APKLIB.equals( type ) || AAR.equals( type ) )
97                      {
98                          // Check if the artifact contains a libs folder - if so, include it in the list
99                          final File libsFolder;
100                         if ( mojo != null )
101                         {
102                             libsFolder = mojo.getUnpackedLibNativesFolder( artifact );
103                         }
104                         else
105                         {
106                             // This is used from NativeHelperTest since we have no AbstractAndroidMojo there
107                             libsFolder = new File(
108                                 AbstractAndroidMojo.getLibraryUnpackDirectory( unpackDirectory, artifact ),
109                                 AAR.equals( type ) ? "jni" : "libs" );
110                         }
111 
112                         if ( !libsFolder.exists() )
113                         {
114                             log.debug( "Skipping " + libsFolder.getAbsolutePath() + " for native artifacts" );
115                             continue;
116                         }
117 
118                         if ( !libsFolder.isDirectory() )
119                         {
120                             continue;
121                         }
122 
123                         log.debug( "Checking " + libsFolder.getAbsolutePath() + " for native artifacts" );
124                         // make sure we ignore libs folders that only contain JARs
125                         // The regular expression filters out all file paths ending with '.jar' or '.JAR',
126                         // so all native libs remain
127                         if ( libsFolder.list( new PatternFilenameFilter( "^.*(?<!(?i)\\.jar)$" ) ).length > 0 )
128                         {
129                             log.debug( "Including attached artifact: " + artifact + ". Artifact is "
130                                     + artifact.getType() );
131                             filteredArtifacts.add( artifact );
132                         }
133                     }
134                     else if ( ! "jar".equals( type ) )
135                     {
136                         log.debug( "Not checking " + type + " for native artifacts" );
137                     }
138                 }
139             }
140         }
141 
142         Set<Artifact> transitiveArtifacts = processTransitiveDependencies( project.getDependencies(), sharedLibraries );
143 
144         filteredArtifacts.addAll( transitiveArtifacts );
145 
146         return filteredArtifacts;
147     }
148 
149     private boolean isNativeLibrary( boolean sharedLibraries, String artifactType )
150     {
151         return ( sharedLibraries
152                 ? Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( artifactType )
153                 : Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( artifactType )
154         );
155     }
156 
157     private Set<Artifact> processTransitiveDependencies( List<Dependency> dependencies, boolean sharedLibraries )
158             throws MojoExecutionException
159     {
160         final Set<Artifact> transitiveArtifacts = new LinkedHashSet<Artifact>();
161         for ( Dependency dependency : dependencies )
162         {
163             if ( ! Artifact.SCOPE_PROVIDED.equals( dependency.getScope() ) && ! dependency.isOptional() )
164             {
165                 final Set<Artifact> transArtifactsFor = processTransitiveDependencies( dependency, sharedLibraries );
166                 log.debug( "Found transitive dependencies for : " + dependency + " transDeps : " + transArtifactsFor );
167                 transitiveArtifacts.addAll( transArtifactsFor );
168             }
169         }
170 
171         return transitiveArtifacts;
172 
173     }
174 
175     private Set<Artifact> processTransitiveDependencies( Dependency dependency, boolean sharedLibraries )
176             throws MojoExecutionException
177     {
178         try
179         {
180             final Set<Artifact> artifacts = new LinkedHashSet<Artifact>();
181 
182             final List<String> exclusionPatterns = new ArrayList<String>();
183             if ( dependency.getExclusions() != null && ! dependency.getExclusions().isEmpty() )
184             {
185                 for ( final Exclusion exclusion : dependency.getExclusions() )
186                 {
187                     exclusionPatterns.add( exclusion.getGroupId() + ":" + exclusion.getArtifactId() );
188                 }
189             }
190             final ArtifactFilter optionalFilter = new ArtifactFilter()
191             {
192                 @Override
193                 public boolean include( Artifact artifact )
194                 {
195                     return !artifact.isOptional();
196                 }
197             };
198 
199             final AndArtifactFilter filter = new AndArtifactFilter();
200             filter.add( new ExcludesArtifactFilter( exclusionPatterns ) );
201             filter.add( new OrArtifactFilter( Arrays.<ArtifactFilter>asList( new ScopeArtifactFilter( "compile" ),
202                                                                             new ScopeArtifactFilter( "runtime" ),
203                                                                             new ScopeArtifactFilter( "test" ) ) ) );
204             filter.add( optionalFilter );
205 
206             final DependencyNode node = dependencyGraphBuilder.buildDependencyGraph( project, filter );
207             final CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor();
208             node.accept( collectingVisitor );
209 
210             final List<DependencyNode> dependencies = collectingVisitor.getNodes();
211             for ( final DependencyNode dep : dependencies )
212             {
213                 final boolean isNativeLibrary = isNativeLibrary( sharedLibraries, dep.getArtifact().getType() );
214                 if ( isNativeLibrary )
215                 {
216                     artifacts.add( dep.getArtifact() );
217                 }
218             }
219 
220             return artifacts;
221         }
222         catch ( Exception e )
223         {
224             throw new MojoExecutionException( "Error while processing transitive dependencies", e );
225         }
226     }
227 
228 
229     public static String[] getAppAbi( File applicationMakefile )
230     {
231         Scanner scanner = null;
232         try
233         {
234             if ( applicationMakefile != null && applicationMakefile.exists() )
235             {
236                 scanner = new Scanner( applicationMakefile );
237                 while ( scanner.hasNextLine( ) )
238                 {
239                     String line = scanner.nextLine().trim();
240                     if ( line.startsWith( "APP_ABI" ) )
241                     {
242                         return line.substring( line.indexOf( ":=" ) + 2 ).trim().split( " " );
243                     }
244                 }
245             }
246         }
247         catch ( FileNotFoundException e )
248         {
249             // do nothing
250         }
251         finally
252         {
253             if ( scanner != null )
254             {
255                 scanner.close();
256             }
257         }
258         return null;
259     }
260 
261 
262     /** Extracts, if embedded correctly, the artifacts architecture from its classifier.  The format of the
263      * classifier, if including the architecture is &lt;architecture&gt;-&lt;classifier&gt;.  If no
264      * architecture is embedded in the classifier, 'armeabi' will be returned.
265      *
266      *
267      * @param artifact The artifact to retrieve the classifier from.
268      * @param defaultArchitecture The architecture to return if can't be resolved from the classifier
269      * @return The retrieved architecture, or <code>defaulArchitecture</code> if not resolveable
270      */
271     public static String extractArchitectureFromArtifact( Artifact artifact, final String defaultArchitecture )
272     {
273         String classifier = artifact.getClassifier();
274         if ( classifier != null )
275         {
276             //
277             // We loop backwards to catch the case where the classifier is
278             // potentially armeabi-v7a - this collides with armeabi if looping
279             // through this loop in the other direction
280             //
281 
282             for ( int i = AndroidNdk.NDK_ARCHITECTURES.length - 1; i >= 0; i-- )
283             {
284                 String ndkArchitecture = AndroidNdk.NDK_ARCHITECTURES[i];
285                 if ( classifier.startsWith( ndkArchitecture ) )
286                 {
287                     return ndkArchitecture;
288                 }
289             }
290 
291         }
292         // Default case is to return the default architecture
293         return defaultArchitecture;
294     }
295 
296     /** Attempts to extract, from various sources, the applicable list of NDK architectures to support
297      * as part of the build.
298      * <br>
299      * <br>
300      * It retrieves the list from the following places:
301      * <ul>
302      *     <li>ndkArchitecture parameter</li>
303      *     <li>projects Application.mk - currently only a single architecture is supported by this method</li>
304      * </ul>
305      *
306      *
307      * @param ndkArchitectures Space separated list of architectures.  This may be from configuration or otherwise
308      * @param applicationMakefile The makefile (Application.mk) to retrieve the list from.
309      * @param basedir Directory the build is running from (to resolve files)
310      *
311      * @return List of architectures to be supported by build.
312      *
313      * @throws MojoExecutionException
314      */
315     public static String[] getNdkArchitectures( final String ndkArchitectures, final String applicationMakefile,
316                                                 final File basedir )
317         throws MojoExecutionException
318     {
319         // if there is a specified ndk architecture, return it
320         if ( ndkArchitectures != null )
321         {
322             return ndkArchitectures.split( " " );
323         }
324 
325         // if there is no application makefile specified, let's use the default one
326         String applicationMakefileToUse = applicationMakefile;
327         if ( applicationMakefileToUse == null )
328         {
329             applicationMakefileToUse = "jni/Application.mk";
330         }
331 
332         // now let's see if the application file exists
333         File appMK = new File( basedir, applicationMakefileToUse );
334         if ( appMK.exists() )
335         {
336             String[] foundNdkArchitectures = getAppAbi( appMK );
337             if ( foundNdkArchitectures != null )
338             {
339                 return foundNdkArchitectures;
340             }
341         }
342 
343         // return a default ndk architecture
344         return new String[] { "armeabi" };
345     }
346 
347     /** Helper method for determining whether the specified architecture is a match for the
348      * artifact using its classifier.  When used for architecture matching, the classifier must be
349      * formed by &lt;architecture&gt;-&lt;classifier&gt;.
350      * If the artifact is legacy and defines no valid architecture, the artifact architecture will
351      * default to <strong>armeabi</strong>.
352      *
353      * @param ndkArchitecture Architecture to check for match
354      * @param artifact Artifact to check the classifier match for
355      * @return True if the architecture matches, otherwise false
356      */
357     public static boolean artifactHasHardwareArchitecture( Artifact artifact, String ndkArchitecture,
358                                                            String defaultArchitecture )
359     {
360         return Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( artifact.getType() )
361                 && ndkArchitecture.equals( extractArchitectureFromArtifact( artifact, defaultArchitecture ) );
362     }
363 
364 }