View Javadoc

1   package com.jayway.maven.plugins.android.phase05compile;
2   
3   import com.jayway.maven.plugins.android.common.AetherHelper;
4   import com.jayway.maven.plugins.android.common.AndroidExtension;
5   import com.jayway.maven.plugins.android.common.JarHelper;
6   import com.jayway.maven.plugins.android.common.NativeHelper;
7   
8   import org.apache.commons.io.FileUtils;
9   import org.apache.commons.io.FilenameUtils;
10  import org.apache.commons.io.filefilter.TrueFileFilter;
11  import org.apache.maven.artifact.Artifact;
12  import org.apache.maven.artifact.DefaultArtifact;
13  import org.apache.maven.plugin.MojoExecutionException;
14  import org.apache.maven.plugin.logging.Log;
15  import org.sonatype.aether.RepositorySystem;
16  import org.sonatype.aether.RepositorySystemSession;
17  import org.sonatype.aether.repository.RemoteRepository;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.List;
24  import java.util.Set;
25  import java.util.jar.JarEntry;
26  import java.util.jar.JarFile;
27  
28  /**
29   * Various helper methods for dealing with Android Native makefiles.
30   *
31   * @author Johan Lindquist
32   */
33  public class MakefileHelper
34  {
35      public static final String MAKEFILE_CAPTURE_FILE = "ANDROID_MAVEN_PLUGIN_LOCAL_C_INCLUDES_FILE";
36      
37      public static final boolean IS_WINDOWS = System.getProperty( "os.name" ).toLowerCase().indexOf( "windows" ) >= 0;
38      public static final String WINDOWS_DRIVE_ROOT_REGEX = "[a-zA-Z]:\\\\";
39  
40      /**
41       * Holder for the result of creating a makefile.  This in particular keep tracks of all directories created
42       * for extracted header files.
43       */
44      public static class MakefileHolder
45      {
46          String makeFile;
47          List<File> includeDirectories;
48  
49          public MakefileHolder( List<File> includeDirectories, String makeFile )
50          {
51              this.includeDirectories = includeDirectories;
52              this.makeFile = makeFile;
53          }
54  
55          public List<File> getIncludeDirectories()
56          {
57              return includeDirectories;
58          }
59  
60          public String getMakeFile()
61          {
62              return makeFile;
63          }
64      }
65  
66      private Log log;
67      private final RepositorySystem repoSystem;
68      private final RepositorySystemSession repoSession;
69      private final List<RemoteRepository> projectRepos;
70      private final File unpackedApkLibsDirectory;
71      
72      /**
73       * Initialize the MakefileHelper by storing the supplied parameters to local variables.
74       * @param log
75       * @param repoSystem
76       * @param repoSession
77       * @param projectRepos
78       * @param unpackedApkLibsDirectory
79       */
80      public MakefileHelper( Log log,
81                             RepositorySystem repoSystem, RepositorySystemSession repoSession, 
82                             List<RemoteRepository> projectRepos, 
83                             File unpackedApkLibsDirectory )
84      {
85          this.log = log;
86          this.repoSystem = repoSystem;
87          this.repoSession = repoSession;
88          this.projectRepos = projectRepos;
89          this.unpackedApkLibsDirectory = unpackedApkLibsDirectory;
90      }
91      
92      /**
93       * Cleans up all include directories created in the temp directory during the build.
94       *
95       * @param makefileHolder The holder produced by the
96       * {@link MakefileHelper#createMakefileFromArtifacts(java.io.File, java.util.Set,
97       * boolean, org.sonatype.aether.RepositorySystemSession, java.util.List, org.sonatype.aether.RepositorySystem)}
98       */
99      public static void cleanupAfterBuild( MakefileHolder makefileHolder )
100     {
101 
102         if ( makefileHolder.getIncludeDirectories() != null )
103         {
104             for ( File file : makefileHolder.getIncludeDirectories() )
105             {
106                 try
107                 {
108                     FileUtils.deleteDirectory( file );
109                 }
110                 catch ( IOException e )
111                 {
112                     e.printStackTrace();
113                 }
114             }
115         }
116 
117     }
118 
119     /**
120      * Creates an Android Makefile based on the specified set of static library dependency artifacts.
121      *
122      * @param outputDir         Directory to resolve artifact locations relative to.  Makefiles contain relative paths
123      * @param artifacts         The list of (static library) dependency artifacts to create the Makefile from
124      * @param useHeaderArchives If true, the Makefile should include a LOCAL_EXPORT_C_INCLUDES statement, pointing to
125      *                          the location where the header archive was expanded
126      * @param repoSession
127      * @param projectRepos
128      * @param repoSystem
129      * @return The created Makefile
130      */
131     public MakefileHolder createMakefileFromArtifacts( File outputDir, Set<Artifact> artifacts,
132                                                               String ndkArchitecture,
133                                                               boolean useHeaderArchives )
134             throws IOException, MojoExecutionException
135     {
136 
137         final StringBuilder makeFile = new StringBuilder( "# Generated by Android Maven Plugin\n" );
138         final List<File> includeDirectories = new ArrayList<File>();
139 
140         // Add now output - allows us to somewhat intelligently determine the include paths to use for the header
141         // archive
142         makeFile.append( "$(shell echo \"LOCAL_C_INCLUDES=$(LOCAL_C_INCLUDES)\" > $(" + MAKEFILE_CAPTURE_FILE + "))" );
143         makeFile.append( '\n' );
144         makeFile.append( "$(shell echo \"LOCAL_PATH=$(LOCAL_PATH)\" >> $(" + MAKEFILE_CAPTURE_FILE + "))" );
145         makeFile.append( '\n' );
146         makeFile.append( "$(shell echo \"LOCAL_MODULE_FILENAME=$(LOCAL_MODULE_FILENAME)\" >> $("
147                 + MAKEFILE_CAPTURE_FILE + "))" );
148         makeFile.append( '\n' );
149         makeFile.append( "$(shell echo \"LOCAL_MODULE=$(LOCAL_MODULE)\" >> $(" + MAKEFILE_CAPTURE_FILE + "))" );
150         makeFile.append( '\n' );
151         makeFile.append( "$(shell echo \"LOCAL_CFLAGS=$(LOCAL_CFLAGS)\" >> $(" + MAKEFILE_CAPTURE_FILE + "))" );
152         makeFile.append( '\n' );
153 
154         if ( ! artifacts.isEmpty() )
155         {
156             for ( Artifact artifact : artifacts )
157             {
158                 boolean apklibStatic = false;
159 
160                 makeFile.append( "#\n" );
161                 makeFile.append( "# Group ID: " );
162                 makeFile.append( artifact.getGroupId() );
163                 makeFile.append( '\n' );
164                 makeFile.append( "# Artifact ID: " );
165                 makeFile.append( artifact.getArtifactId() );
166                 makeFile.append( '\n' );
167                 makeFile.append( "# Artifact Type: " );
168                 makeFile.append( artifact.getType() );
169                 makeFile.append( '\n' );
170                 makeFile.append( "# Version: " );
171                 makeFile.append( artifact.getVersion() );
172                 makeFile.append( '\n' );
173                 makeFile.append( "include $(CLEAR_VARS)" );
174                 makeFile.append( '\n' );
175                 makeFile.append( "LOCAL_MODULE    := " );
176                 makeFile.append( artifact.getArtifactId() );
177                 makeFile.append( '\n' );
178                 apklibStatic = addLibraryDetails( makeFile, outputDir, artifact, ndkArchitecture );
179                 if ( useHeaderArchives )
180                 {
181                     try
182                     {
183                         Artifact harArtifact = new DefaultArtifact( artifact.getGroupId(), artifact.getArtifactId(),
184                                 artifact.getVersion(), artifact.getScope(), "har", artifact.getClassifier(),
185                                 artifact.getArtifactHandler() );
186                         final Artifact resolvedHarArtifact = AetherHelper
187                                 .resolveArtifact( harArtifact, repoSystem, repoSession, projectRepos );
188 
189                         File includeDir = new File( System.getProperty( "java.io.tmpdir" ),
190                                 "android_maven_plugin_native_includes" + System.currentTimeMillis() + "_"
191                                         + resolvedHarArtifact.getArtifactId() );
192                         includeDir.deleteOnExit();
193                         includeDirectories.add( includeDir );
194 
195                         JarHelper.unjar( new JarFile( resolvedHarArtifact.getFile() ), includeDir,
196                                 new JarHelper.UnjarListener()
197                                 {
198                                     @Override
199                                     public boolean include( JarEntry jarEntry )
200                                     {
201                                         return ! jarEntry.getName().startsWith( "META-INF" );
202                                     }
203                                 } );
204 
205                         makeFile.append( "LOCAL_EXPORT_C_INCLUDES := " );
206                         final String str = includeDir.getAbsolutePath();
207                         makeFile.append( str );
208                         makeFile.append( '\n' );
209                         
210                         if ( log.isDebugEnabled() )
211                         {
212                             Collection<File> includes = FileUtils.listFiles( includeDir,
213                                     TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE );
214                             log.debug( "Listing LOCAL_EXPORT_C_INCLUDES for " + artifact.getId() + ": " + includes );
215                         }
216                     }
217                     catch ( Exception e )
218                     {
219                         throw new MojoExecutionException(
220                                 "Error while resolving header archive file for: " + artifact.getArtifactId(), e );
221                     }
222                 }
223                 if ( "a".equals( artifact.getType() ) || apklibStatic )
224                 {
225                     makeFile.append( "include $(PREBUILT_STATIC_LIBRARY)\n" );
226                 }
227                 else
228                 {
229                     makeFile.append( "include $(PREBUILT_SHARED_LIBRARY)\n" );
230                 }
231             }
232         }
233         
234         return new MakefileHolder( includeDirectories, makeFile.toString() );
235     }
236 
237     private boolean addLibraryDetails( StringBuilder makeFile, File outputDir,
238                                        Artifact artifact, String ndkArchitecture ) throws IOException
239     {
240         boolean apklibStatic = false;
241         if ( AndroidExtension.APKLIB.equals( artifact.getType() ) )
242         {
243             String classifier = artifact.getClassifier();
244             String architecture = ( classifier != null ) ? classifier : ndkArchitecture;
245             // 
246             // We assume that APKLIB contains a single static OR shared library
247             // that we should link against. The follow code identifies that file.
248             //
249             File[] staticLibs = NativeHelper.listNativeFiles( artifact, unpackedApkLibsDirectory, 
250                                                               architecture, true );
251             if ( staticLibs != null && staticLibs.length > 0 )
252             {
253                 int libIdx = findApklibNativeLibrary( staticLibs, artifact.getArtifactId() );
254                 apklibStatic = true;
255                 addLibraryDetails( makeFile, outputDir, staticLibs[libIdx] );
256             }
257             else
258             {
259                 File[] sharedLibs = NativeHelper.listNativeFiles( artifact, unpackedApkLibsDirectory, 
260                                                                   architecture, false );
261                 if ( sharedLibs == null )
262                 {
263                     throw new IOException( "Failed to find any library file in APKLIB" );
264                 }
265                 int libIdx = findApklibNativeLibrary( sharedLibs, artifact.getArtifactId() );
266                 addLibraryDetails( makeFile, outputDir, sharedLibs[libIdx] );
267             }
268         }
269         else
270         {
271             addLibraryDetails( makeFile, outputDir, artifact.getFile() );
272         }
273 
274         return apklibStatic;
275     }
276 
277     private void addLibraryDetails( StringBuilder makeFile, File outputDir, File libFile )
278         throws IOException
279     {
280         String localPath = resolveRelativePath( outputDir, libFile );
281         localPath = localPath.substring( 0, localPath.indexOf( libFile.getName() ) - 1 );
282 
283         makeFile.append( "LOCAL_PATH := " );
284         makeFile.append( localPath );
285         makeFile.append( '\n' );
286         makeFile.append( "LOCAL_SRC_FILES := " );
287         makeFile.append( libFile.getName() );
288         makeFile.append( '\n' );
289         makeFile.append( "LOCAL_MODULE_FILENAME := " );
290         makeFile.append( FilenameUtils.removeExtension( libFile.getName() ) );
291         makeFile.append( '\n' );
292     }
293 
294     /**
295      * @param libs the array of possible library files. Must not be null.
296      * @return the index in the array of the library to use
297      * @throws IOException if a library cannot be identified
298      */
299     private int findApklibNativeLibrary( File[] libs, String artifactName ) throws IOException
300     {
301         int libIdx = -1;
302         
303         if ( libs.length == 1 )
304         
305         {
306             libIdx = 0;
307         }
308         else
309         {
310             log.info( "Found multiple library files, looking for name match with artifact" );
311             StringBuilder sb = new StringBuilder();
312             for ( int i = 0; i < libs.length; i++ )
313             {
314                 if ( sb.length() != 0 )
315                 {
316                     sb.append( ", " );
317                 }
318                 sb.append( libs[i].getName() );
319                 if ( libs[i].getName().startsWith( "lib" + artifactName ) )
320                 {
321                     if ( libIdx != -1 )
322                     {
323                         // We have multiple matches, tell the user we can't handle this ...
324                         throw new IOException( "Found multiple libraries matching artifact name " + artifactName
325                                 + ". Please use unique artifact/library names." );
326                         
327                     }
328                     libIdx = i;
329                 }
330             }
331             if ( libIdx < 0 )
332             {
333                 throw new IOException( "Unable to determine main library from " + sb.toString()
334                         + " APKLIB should contain only 1 library or a library matching the artifact name" );
335             }
336         }
337         return libIdx;
338     }
339     
340     /**
341      * Resolves the relative path of the specified artifact
342      *
343      * @param outputDirectory typically the parent directory of the directory containing the makefile
344      * @param file
345      * @return
346      */
347     protected static String resolveRelativePath( File outputDirectory, File file ) throws IOException
348     {
349         String resolvedPath = file.getCanonicalPath();
350         
351         String strOutputDirectoryPath = outputDirectory.getCanonicalPath();
352         String strFilePath = file.getCanonicalPath();
353         //System.out.println( "Resolving " + strFilePath + " against " + strOutputDirectoryPath );
354 
355         if ( strFilePath.startsWith( strOutputDirectoryPath ) )
356         {
357             // Simple case where file is in a subdirectory of outputDirectory
358             resolvedPath =  strFilePath.substring( strOutputDirectoryPath.length() + 1 );
359         }
360         else
361         {
362             // Look for commonality in paths
363             List<String> outputDirectoryPathParts = splitPath( outputDirectory.getCanonicalFile() );
364             List<String> filePathParts = splitPath( file.getCanonicalFile() );
365             int commonDepth = 0;
366             int maxCommonDepth = Math.min( outputDirectoryPathParts.size(), filePathParts.size() );
367             for ( int i = 0; 
368                     ( i < maxCommonDepth ) 
369                     && outputDirectoryPathParts.get( i ).equals( filePathParts.get( i ) ); 
370                     i++ )
371             {
372                 commonDepth++;
373             }
374             // If there is a common root build a relative path between the common roots
375             if ( commonDepth > 0 )
376             {
377                 final StringBuilder stringBuilder = new StringBuilder();
378                 stringBuilder.append( ".." );
379                 for ( int i = 0; i < outputDirectoryPathParts.size() - commonDepth - 1; i++ )
380                 {
381                     stringBuilder.append( File.separator );
382                     stringBuilder.append( ".." );
383                 }
384                 for ( int i = commonDepth; i < filePathParts.size(); i++ )
385                 {
386                     stringBuilder.append( File.separator );
387                     stringBuilder.append( filePathParts.get( i ) );
388                 }
389                 resolvedPath = stringBuilder.toString();
390             }
391             else
392             {
393                 if ( IS_WINDOWS )
394                 {
395                     // Windows has no common root directory, cannot resolve a path
396                     // across drives so ...
397                     throw new IOException( "Unable to resolve relative path across windows drives" );
398                 }
399 
400                 // no intersection between paths so calculate a path via the root directory
401                 final StringBuilder stringBuilder = new StringBuilder();
402                 
403                 File depthCheck = outputDirectory.getParentFile();
404                 while ( depthCheck != null )
405                 {
406                     if ( stringBuilder.length() > 0 )
407                     {
408                         stringBuilder.append( File.separator );
409                     }
410                     stringBuilder.append( ".." );
411                     depthCheck = depthCheck.getParentFile();
412                 }
413     
414                 resolvedPath = stringBuilder.toString() + strFilePath;
415             }
416         }
417 
418         //System.out.println( "resolvedPath = " + resolvedPath );
419         return resolvedPath;
420     }
421 
422     /**
423      * Method to split the path components of a file into a List
424      * @param f the file to split
425      * @return a new list containing the components of the path as strings
426      */
427     protected static List<String> splitPath( File f )
428     {
429         List<String> result;
430         File parent = f.getParentFile();
431         if ( parent == null )
432         {
433             result = new ArrayList<String>();
434             if ( f.getName().length() > 0 )
435             {
436                 // We're at the root but have a name so we have a relative path
437                 // for which we need to add the first component to the list
438                 result.add( f.getName() );
439             }
440             else if ( IS_WINDOWS ) 
441             {
442                 String strF = f.toString();
443                 if ( strF.matches( WINDOWS_DRIVE_ROOT_REGEX ) )
444                 {
445                     // We're on windows and the path is <Drive>:\ so we
446                     // add the <Drive>: to the list
447                     result.add( strF.substring( 0, strF.length() - 1 ).toUpperCase() );
448                 }
449             }
450         }
451         else
452         {
453             result = splitPath( parent );
454             result.add( f.getName() );
455         }
456         return result;
457     }
458 
459     /**
460      * Creates a list of artifacts suitable for use in the LOCAL_STATIC_LIBRARIES or LOCAL_SHARED_LIBRARIES 
461      * variable in an Android makefile
462      *
463      * @param resolvedLibraryList
464      * @param staticLibrary
465      * @return a list of Ids for artifacts that include static or shared libraries
466      */
467     public String createLibraryList( Set<Artifact> resolvedLibraryList,
468                                      String ndkArchitecture,
469                                      boolean staticLibrary )
470     {
471         StringBuilder sb = new StringBuilder();
472 
473         for ( Artifact a : resolvedLibraryList )
474         {
475             if ( staticLibrary && "a".equals( a.getType() ) )
476             {
477                 sb.append( a.getArtifactId() );
478             }
479             if ( ! staticLibrary && "so".equals( a.getType() ) )
480             {
481                 sb.append( a.getArtifactId() );
482             }
483             if ( AndroidExtension.APKLIB.equals( a.getType() ) )
484             {
485                 File[] libFiles = NativeHelper.listNativeFiles( a, unpackedApkLibsDirectory, 
486                                                                 ndkArchitecture, staticLibrary );
487                 if ( libFiles != null && libFiles.length > 0 )
488                 {
489                     sb.append( a.getArtifactId() );
490                 }
491                 
492             }
493             sb.append( " " );
494         }
495 
496         return sb.toString();
497     }
498 }