Coverage Report - com.simpligility.maven.plugins.androidndk.common.NativeHelper
 
Classes in this File Line Coverage Branch Coverage Complexity
NativeHelper
17%
24/141
13%
12/92
4.529
NativeHelper$1
0%
0/2
0%
0/2
4.529
NativeHelper$2
0%
0/2
0%
0/6
4.529
 
 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  9
     {
 46  9
         this.project = project;
 47  9
         this.dependencyGraphBuilder = dependencyGraphBuilder;
 48  9
         this.log = log;
 49  9
     }
 50  
 
 51  
     public static boolean hasStaticNativeLibraryArtifact ( Set<Artifact> resolveNativeLibraryArtifacts )
 52  
     {
 53  0
         for ( Artifact resolveNativeLibraryArtifact : resolveNativeLibraryArtifacts )
 54  
         {
 55  0
             if ( Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( resolveNativeLibraryArtifact.getType() ) )
 56  
             {
 57  0
                 return true;
 58  
             }
 59  0
         }
 60  0
         return false;
 61  
     }
 62  
 
 63  
     public static boolean hasSharedNativeLibraryArtifact ( Set<Artifact> resolveNativeLibraryArtifacts )
 64  
     {
 65  0
         for ( Artifact resolveNativeLibraryArtifact : resolveNativeLibraryArtifacts )
 66  
         {
 67  0
             if ( Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( resolveNativeLibraryArtifact.getType() ) )
 68  
             {
 69  0
                 return true;
 70  
             }
 71  0
         }
 72  0
         return false;
 73  
     }
 74  
 
 75  
     public Set<Artifact> getNativeDependenciesArtifacts( boolean sharedLibraries )
 76  
             throws MojoExecutionException
 77  
     {
 78  0
         log.debug( "Finding native dependencies. shared=" + sharedLibraries );
 79  0
         final Set<Artifact> filteredArtifacts = new LinkedHashSet<Artifact>();
 80  0
         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  0
         allArtifacts.addAll( project.getDependencyArtifacts() );
 86  
 
 87  
         // Add all attached artifacts as well - this could come from the NDK mojo for example
 88  0
         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  0
         allArtifacts.addAll( project.getArtifacts() );
 93  
 
 94  0
         for ( Artifact artifact : allArtifacts )
 95  
         {
 96  0
             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  0
             if ( isNativeLibrary( sharedLibraries, artifact.getType() ) && artifact.getScope() == null )
 100  
             {
 101  
                 // Including attached artifact
 102  0
                 log.debug( "Including attached artifact: " + artifact + ". Artifact scope is not set." );
 103  0
                 filteredArtifacts.add( artifact );
 104  
             }
 105  
             else
 106  
             {
 107  0
                 if ( isNativeLibrary( sharedLibraries, artifact.getType() ) && (
 108  
                         Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) || Artifact.SCOPE_RUNTIME
 109  
                                 .equals( artifact.getScope() ) ) )
 110  
                 {
 111  0
                     log.debug( "Including attached artifact: " + artifact + ". Artifact scope is Compile or Runtime." );
 112  0
                     filteredArtifacts.add( artifact );
 113  
                 }
 114  
                 else
 115  
                 {
 116  0
                     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  0
                     if ( AndroidExtension.APKLIB.equals( type ) || AndroidExtension.AAR.equals( type ) )
 122  
                     {
 123  0
                         filteredArtifacts.add( artifact );
 124  
                     }
 125  
                     else
 126  
                     {
 127  
                         // Not checking any other types for now ...
 128  
                     }
 129  
                 }
 130  
             }
 131  0
         }
 132  
 
 133  0
         Set<Artifact> transitiveArtifacts = processTransitiveDependencies( project.getDependencies(), sharedLibraries );
 134  
 
 135  0
         filteredArtifacts.addAll( transitiveArtifacts );
 136  
 
 137  0
         return filteredArtifacts;
 138  
     }
 139  
 
 140  
     private boolean isNativeLibrary( boolean sharedLibraries, String artifactType )
 141  
     {
 142  0
         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  0
         final Set<Artifact> transitiveArtifacts = new LinkedHashSet<Artifact>();
 152  0
         for ( Dependency dependency : dependencies )
 153  
         {
 154  0
             if ( !Artifact.SCOPE_PROVIDED.equals( dependency.getScope() ) && !dependency.isOptional() )
 155  
             {
 156  0
                 final Set<Artifact> transArtifactsFor = processTransitiveDependencies( dependency, sharedLibraries );
 157  0
                 log.debug( "Found transitive dependencies for : " + dependency + " transDeps : " + transArtifactsFor );
 158  0
                 transitiveArtifacts.addAll( transArtifactsFor );
 159  
             }
 160  0
         }
 161  
 
 162  0
         return transitiveArtifacts;
 163  
 
 164  
     }
 165  
 
 166  
     private Set<Artifact> processTransitiveDependencies( Dependency dependency, boolean sharedLibraries )
 167  
             throws MojoExecutionException
 168  
     {
 169  0
         log.debug( "Processing transitive dependencies for : " + dependency );
 170  
 
 171  
         try
 172  
         {
 173  0
             final Set<Artifact> artifacts = new LinkedHashSet<Artifact>();
 174  
 
 175  0
             final List<String> exclusionPatterns = new ArrayList<String>();
 176  0
             if ( dependency.getExclusions() != null && !dependency.getExclusions().isEmpty() )
 177  
             {
 178  0
                 for ( final Exclusion exclusion : dependency.getExclusions() )
 179  
                 {
 180  0
                     exclusionPatterns.add( exclusion.getGroupId() + ":" + exclusion.getArtifactId() );
 181  0
                 }
 182  
             }
 183  0
             final ArtifactFilter optionalFilter = new ArtifactFilter()
 184  0
             {
 185  
                 @Override
 186  
                 public boolean include( Artifact artifact )
 187  
                 {
 188  0
                     return !artifact.isOptional();
 189  
                 }
 190  
             };
 191  
 
 192  0
             final AndArtifactFilter filter = new AndArtifactFilter();
 193  0
             filter.add( new ExcludesArtifactFilter( exclusionPatterns ) );
 194  0
             filter.add( new OrArtifactFilter( Arrays.<ArtifactFilter>asList( new ScopeArtifactFilter( "compile" ),
 195  
                     new ScopeArtifactFilter( "runtime" ),
 196  
                     new ScopeArtifactFilter( "test" ) ) ) );
 197  0
             filter.add( optionalFilter );
 198  
 
 199  0
             final DependencyNode node = dependencyGraphBuilder.buildDependencyGraph( project, filter );
 200  0
             final CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor();
 201  0
             node.accept( collectingVisitor );
 202  
 
 203  0
             final List<DependencyNode> dependencies = collectingVisitor.getNodes();
 204  0
             for ( final DependencyNode dep : dependencies )
 205  
             {
 206  0
                 final boolean isNativeLibrary = isNativeLibrary( sharedLibraries, dep.getArtifact().getType() );
 207  0
                 log.debug( "Processing library : " + dep.getArtifact() + " isNative=" + isNativeLibrary );
 208  0
                 if ( isNativeLibrary )
 209  
                 {
 210  0
                     artifacts.add( dep.getArtifact() );
 211  
                 }
 212  0
             }
 213  
 
 214  0
             return artifacts;
 215  
         }
 216  0
         catch ( Exception e )
 217  
         {
 218  0
             throw new MojoExecutionException( "Error while processing transitive dependencies", e );
 219  
         }
 220  
     }
 221  
 
 222  
     public static void validateNDKVersion( File ndkHomeDir ) throws MojoExecutionException
 223  
     {
 224  0
         final File ndkSourcePropertiesFile = new File( ndkHomeDir, "source.properties" );
 225  
 
 226  0
         if ( ndkSourcePropertiesFile.exists() )
 227  
         {
 228  
             // As of 11 this file is a sign of a good release
 229  0
             return;
 230  
         }
 231  
 
 232  0
         final File ndkVersionFile = new File( ndkHomeDir, "RELEASE.TXT" );
 233  
 
 234  0
         if ( !ndkVersionFile.exists() )
 235  
         {
 236  0
             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  0
             String versionStr = FileUtils.readFileToString( ndkVersionFile );
 244  0
             validateNDKVersion( NDK_REQUIRED_VERSION, versionStr );
 245  
         }
 246  0
         catch ( Exception e )
 247  
         {
 248  0
             throw new MojoExecutionException( "Error while extracting NDK version from '"
 249  
                     + ndkVersionFile.getAbsolutePath() + "'. Please verify your setup! "
 250  
                     + AndroidNdk.PROPER_NDK_HOME_DIRECTORY_MESSAGE );
 251  0
         }
 252  0
     }
 253  
 
 254  
     public static void validateNDKVersion( int desiredVersion, String versionStr ) throws MojoExecutionException
 255  
     {
 256  
 
 257  14
         int version = 0;
 258  
 
 259  14
         if ( versionStr != null )
 260  
         {
 261  14
             versionStr = versionStr.trim();
 262  14
             Pattern pattern = Pattern.compile( "[r]([0-9]{1,3})([a-z]{0,1}).*" );
 263  14
             Matcher m = pattern.matcher( versionStr );
 264  14
             if ( m.matches() )
 265  
             {
 266  14
                 final String group = m.group( 1 );
 267  14
                 version = Integer.parseInt( group );
 268  
             }
 269  
         }
 270  
 
 271  14
         if ( version < desiredVersion )
 272  
         {
 273  6
             throw new MojoExecutionException( "You are running an old NDK (version " + versionStr + "), please update "
 274  
                     + "to at least r'" + desiredVersion + "' or later" );
 275  
         }
 276  8
     }
 277  
 
 278  
     public static String[] getAppAbi( File applicationMakefile )
 279  
     {
 280  0
         Scanner scanner = null;
 281  
         try
 282  
         {
 283  0
             if ( applicationMakefile != null && applicationMakefile.exists() )
 284  
             {
 285  0
                 scanner = new Scanner( applicationMakefile );
 286  
                 try
 287  
                 {
 288  0
                     while ( scanner.hasNextLine() )
 289  
                     {
 290  0
                         String line = scanner.nextLine().trim();
 291  0
                         if ( line.startsWith( "APP_ABI" ) )
 292  
                         {
 293  0
                             return line.substring( line.indexOf( ":=" ) + 2 ).trim().split( " " );
 294  
                         }
 295  0
                     }
 296  
                 }
 297  
                 finally
 298  
                 {
 299  0
                     scanner.close();
 300  0
                 }
 301  
             }
 302  
         }
 303  0
         catch ( FileNotFoundException e )
 304  
         {
 305  
             // do nothing
 306  0
         }
 307  0
         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  30
         String classifier = artifact.getClassifier();
 323  30
         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  128
             for ( int i = AndroidNdk.NDK_ARCHITECTURES.length - 1; i >= 0; i-- )
 332  
             {
 333  126
                 String ndkArchitecture = AndroidNdk.NDK_ARCHITECTURES[ i ];
 334  126
                 if ( classifier.startsWith( ndkArchitecture ) )
 335  
                 {
 336  28
                     return ndkArchitecture;
 337  
                 }
 338  
             }
 339  
 
 340  
         }
 341  
         // Default case is to return the default architecture
 342  2
         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  0
         String[] resolvedArchitectures = null;
 365  
         // if there is a specified ndk architecture, return it
 366  0
         if ( ndkArchitectures != null )
 367  
         {
 368  0
             resolvedArchitectures = ndkArchitectures.split( " " );
 369  
         }
 370  
 
 371  0
         if ( resolvedArchitectures == null )
 372  
         {
 373  
             // if there is no application makefile specified, let's use the default one
 374  0
             String applicationMakefileToUse = applicationMakefile;
 375  0
             if ( applicationMakefileToUse == null )
 376  
             {
 377  0
                 applicationMakefileToUse = "jni/Application.mk";
 378  
             }
 379  
 
 380  
             // now let's see if the application file exists
 381  0
             File appMK = new File ( basedir, applicationMakefileToUse );
 382  0
             if ( appMK.exists () )
 383  
             {
 384  0
                 String[] foundNdkArchitectures = getAppAbi ( appMK );
 385  0
                 if ( foundNdkArchitectures != null )
 386  
                 {
 387  0
                     resolvedArchitectures = foundNdkArchitectures;
 388  
                 }
 389  
             }
 390  
         }
 391  
 
 392  
         // If still not fond, we default it to armeabi
 393  0
         if ( resolvedArchitectures == null )
 394  
         {
 395  0
             resolvedArchitectures = new String[] { "armeabi" };
 396  
         }
 397  
 
 398  0
         List<String> processedResolvedArchitectures = new ArrayList<> (  );
 399  
 
 400  0
         for ( int i = 0; i < resolvedArchitectures.length; i++ )
 401  
         {
 402  0
             final String resolvedArchitecture = resolvedArchitectures[ i ].trim ();
 403  0
             if ( resolvedArchitecture.length () > 0 )
 404  
             {
 405  0
                 processedResolvedArchitectures.add( resolvedArchitecture );
 406  
             }
 407  
         }
 408  
 
 409  
         // return a default ndk architecture
 410  0
         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  16
         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  0
         final String packaging = mavenProject.getPackaging();
 439  0
         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  0
         final List<File> acceptedFiles = new ArrayList<File> (  );
 445  0
         File libsFolder = new File( unpackDirectory, architecture );
 446  0
         if ( libsFolder.exists () )
 447  
         {
 448  
             // list all the files
 449  0
             return libsFolder.listFiles( new FilenameFilter ()
 450  0
             {
 451  
                 public boolean accept( final File dir, final String name )
 452  
                 {
 453  0
                     return name.startsWith( "lib" ) && name.endsWith( ( staticLibrary ? ".a" : ".so" ) );
 454  
                 }
 455  
             } );
 456  
         }
 457  0
         return new File[0];
 458  
     }
 459  
 
 460  
 
 461  
 }