View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *      http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package com.simpligility.maven.plugins.androidndk.phase05compile;
15  
16  import com.simpligility.maven.plugins.androidndk.AndroidNdk;
17  import com.simpligility.maven.plugins.androidndk.CommandExecutor;
18  import com.simpligility.maven.plugins.androidndk.common.ArtifactResolverHelper;
19  import com.simpligility.maven.plugins.androidndk.common.Const;
20  import com.simpligility.maven.plugins.androidndk.common.MavenToPlexusLogAdapter;
21  import com.simpligility.maven.plugins.androidndk.common.NativeHelper;
22  import com.simpligility.maven.plugins.androidndk.configuration.AdditionallyBuiltModule;
23  import com.simpligility.maven.plugins.androidndk.configuration.HeaderFilesDirective;
24  import com.simpligility.maven.plugins.androidndk.configuration.ArchitectureToolchainMappings;
25  import com.simpligility.maven.plugins.androidndk.configuration.IgnoreHeaderFilesArchive;
26  import org.apache.commons.lang3.StringUtils;
27  import org.apache.maven.archiver.MavenArchiveConfiguration;
28  import org.apache.maven.archiver.MavenArchiver;
29  import org.apache.maven.artifact.Artifact;
30  import org.apache.maven.artifact.handler.ArtifactHandler;
31  import org.apache.maven.artifact.resolver.ArtifactResolver;
32  import org.apache.maven.plugin.AbstractMojo;
33  import org.apache.maven.plugin.MojoExecutionException;
34  import org.apache.maven.plugin.MojoFailureException;
35  import org.apache.maven.plugins.annotations.Component;
36  import org.apache.maven.plugins.annotations.LifecyclePhase;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.project.MavenProjectHelper;
41  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
42  import org.codehaus.plexus.archiver.jar.JarArchiver;
43  import org.codehaus.plexus.util.IOUtil;
44  
45  import java.io.File;
46  import java.io.FileInputStream;
47  import java.io.FileOutputStream;
48  import java.io.FilenameFilter;
49  import java.io.IOException;
50  import java.util.ArrayList;
51  import java.util.Arrays;
52  import java.util.LinkedHashSet;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.Properties;
56  import java.util.Set;
57  import java.util.regex.Matcher;
58  import java.util.regex.Pattern;
59  
60  /**
61   * @author Johan Lindquist <johanlindquist@gmail.com>
62   */
63  @Mojo( name = "ndk-build", defaultPhase = LifecyclePhase.COMPILE )
64  public class NdkBuildMojo extends AbstractMojo
65  {
66      /**
67       * The <code>ANDROID_NDK_HOME</code> environment variable name.
68       */
69      public static final String ENV_ANDROID_NDK_HOME = "ANDROID_NDK_HOME";
70  
71      /**
72       * <p>Parameter designed to pick up <code>-Dandroid.ndk.ndkPath</code> in case there is no pom with an
73       * <code>&lt;ndk&gt;</code> configuration tag.</p>
74       */
75      @Parameter( property = "android.ndk.ndkPath", readonly = true )
76      private File ndkPath;
77  
78      /**
79       * Allows for overriding the default ndk-build executable.
80       */
81      @Parameter( property = "android.ndk.ndkBuildExecutable" )
82      private String ndkBuildExecutable;
83  
84      /**
85       * Folder in which the NDK makefiles are constructed.
86       */
87      @Parameter( property = "android.ndk.buildDirectory", defaultValue = "${project.build.directory}/android-ndk-maven-plugin", readonly = true )
88      private File buildDirectory;
89  
90      /** Folder in which the ndk-build command is executed.  This is using the -C command line flag.
91       */
92      @Parameter( property = "android.ndk.workingDirectory", defaultValue = "${project.basedir}", readonly = true )
93      private File workingDirectory;
94  
95      /** Specifies the classifier with which the artifact should be stored in the repository
96       */
97      @Parameter( property = "android.ndk.classifier" )
98      private String classifier;
99  
100     /** Specifies additional command line parameters to pass to ndk-build
101      */
102     @Parameter( property = "android.ndk.additionalCommandline" )
103     protected String additionalCommandline;
104 
105     /**
106      * <p>Folder containing native, static libraries compiled and linked by the NDK.</p>
107      * <p/>
108      */
109     @Parameter( property = "android.ndk.objectsOutputDirectory", defaultValue = "${project.build.directory}/obj" )
110     private File objectsOutputDirectory;
111 
112     /**
113      * <p>Folder containing native, static libraries compiled and linked by the NDK.</p>
114      * <p/>
115      */
116     @Parameter( property = "android.ndk.librariesOutputDirectory", defaultValue = "${project.build.directory}/ndk-libs" )
117     private File librariesOutputDirectory;
118 
119     /**
120      * Folder in which AAR/APKLIB library dependencies will be unpacked.
121      */
122     @Parameter( property = "unpackedLibsFolder", defaultValue = "${project.build.directory}/unpacked-libs" )
123     private File unpackedLibsFolder;
124 
125     /**
126      * <p>Target to invoke on the native makefile.</p>
127      */
128     @Parameter( property = "android.ndk.target" )
129     private String target;
130 
131     /**
132      * Defines the architectures for the NDK build - this is a space separated list (i.e x86 armeabi)
133      */
134     @Parameter( property = "android.ndk.architectures" )
135     private String architectures;
136 
137     /**
138      * Defines the architecture to toolchain mappings for the NDK build
139      * &lt;architectureToolchainMappings&gt;
140      * &lt;x86&gt;x86-4.7&lt;/x86&gt;
141      * &lt;armeabi&gt;arm-linux-androideabi-4.7&lt;/armeabi&gt;
142      * &lt;/architectureToolchainMappings&gt;
143      */
144     @Parameter
145     private ArchitectureToolchainMappings architectureToolchainMappings;
146 
147     /**
148      * Flag indicating whether the header files used in the build should be included and attached to the build as
149      * an additional artifact.
150      */
151     @Parameter( property = "android.ndk.attachHeaderFiles", defaultValue = "true" )
152     private Boolean attachHeaderFiles;
153 
154     /**
155      * Flag indicating whether the final artifacts should be included and attached to the build as an artifact.
156      */
157     @Parameter( property = "android.ndk.attachLibrariesArtifacts", defaultValue = "true" )
158     private Boolean attachLibrariesArtifacts;
159 
160     /**
161      * Flag indicating whether temporary build artifacts are removed after a build
162      */
163     @Parameter( property = "android.ndk.leaveTemporaryBuildArtifacts", defaultValue = "false" )
164     private Boolean leaveTemporaryBuildArtifacts;
165 
166     /**
167      * Flag indicating whether the make files last LOCAL_SRC_INCLUDES should be used for determining what header
168      * files to include.  Setting this flag to true, overrides any defined header files directives.
169      * <strong>Note: </strong> By setting this flag to true, all header files used in the project will be
170      * added to the resulting header archive.  This may be undesirable in most cases and is therefore turned off by
171      * default.
172      */
173     @Parameter( property = "android.ndk.useLocalSrcIncludePaths", defaultValue = "false" )
174     private Boolean useLocalSrcIncludePaths;
175 
176     /**
177      * Specifies the set of header files includes/excludes which should be used for bundling the exported header
178      * files.  The below shows an example of how this can be used.
179      * <p/>
180      * <pre>
181      * &lt;headerFilesDirectives&gt;
182      *   &lt;headerFilesDirective&gt;
183      *     &lt;directory&gt;${basedir}/jni/include&lt;/directory&gt;
184      *     &lt;includes&gt;
185      *       &lt;includes&gt;**\/*.h&lt;/include&gt;
186      *     &lt;/includes&gt;
187      *   &lt;headerFilesDirective&gt;
188      * &lt;/headerFilesDirectives&gt;
189      * </pre>
190      * <br/>
191      * If no <code>headerFilesDirectives</code> is specified, the default includes will be defined as shown below:
192      * <br/>
193      * <pre>
194      * &lt;headerFilesDirectives&gt;
195      *   &lt;headerFilesDirective&gt;
196      *     &lt;directory&gt;${basedir}/jni&lt;/directory&gt;
197      *     &lt;includes&gt;
198      *       &lt;includes&gt;**\/*.h&lt;/include&gt;
199      *     &lt;/includes&gt;
200      *     &lt;excludes&gt;
201      *       &lt;exclude&gt;**\/*.c&lt;/exclude&gt;
202      *     &lt;/excludes&gt;
203      *   &lt;headerFilesDirective&gt;
204      *   [..]
205      * &lt;/headerFilesDirectives&gt;
206      * </pre>
207      */
208     @Parameter
209     private List<HeaderFilesDirective> headerFilesDirectives;
210 
211     /**
212      * Flag indicating whether the header files for native, static library dependencies should be used.  If true,
213      * the header archive for each statically linked dependency will be resolved.
214      */
215     @Parameter( property = "android.ndk.build.use-header-archive", defaultValue = "true" )
216     private Boolean useHeaderArchives;
217 
218     /** Specifies a set of group/artifact identifiers for which header archives should not be attempted to be resolved.
219      * This is useful when a static library dependcy on other static libraries but the headers of those libraries are not necessarily
220      * available.  This allows the plugin to exclude the retrieval of those header archives
221      * <p/>
222      * <pre>
223      *   &lt;ignoreHeaderFilesArchives&gt;
224      *     &lt;ignoreHeaderFilesArchive&gt;
225      *       &lt;groupId&gt;com.insidesecure.drm.agent.android&lt;/groupId&gt;
226      *       &lt;artifactId&gt;expat-static-lib&lt;/artifactId&gt;
227      *     &lt;/ignoreHeaderFilesArchive&gt;
228      *     &lt;ignoreHeaderFilesArchive&gt;
229      *       &lt;groupId&gt;com.insidesecure.drm.agent.android&lt;/groupId&gt;
230      *       &lt;artifactId&gt;pcre-static-&lt;/artifactId&gt;
231      *     &lt;/ignoreHeaderFilesArchive&gt;
232      *   &lt;/ignoreHeaderFilesArchives&gt;
233      * </pre>
234      *
235      */
236     @Parameter
237     private List<IgnoreHeaderFilesArchive> ignoreHeaderFilesArchives;
238 
239     /**
240      * Defines additional system properties which should be exported to the ndk-build script.  This
241      * <br/>
242      * <pre>
243      * &lt;systemProperties&gt;
244      *   &lt;propertyName&gt;propertyValue&lt;/propertyName&gt;
245      *   &lt;build-target&gt;android&lt;/build-target&gt;
246      *   [..]
247      * &lt;/systemProperties&gt;
248      * </pre>     *
249      */
250     @Parameter
251     private Map<String, String> systemProperties;
252 
253     /**
254      * Flag indicating whether warnings should be ignored while compiling.  If true,
255      * the build will not fail if warning are found during compile.
256      */
257     @Parameter( property = "android.ndk.ignoreBuildWarnings", defaultValue = "true" )
258     private Boolean ignoreBuildWarnings;
259 
260     /**
261      * Defines the regular expression used to detect whether error/warning output from ndk-build is a minor compile
262      * warning or is actually an error which should cause the build to fail.
263      * <p/>
264      * If the pattern matches, the output from the compiler will <strong>not</strong> be considered an error and compile
265      * will be successful.
266      */
267     @Parameter( property = "android.ndk.buildWarningsRegularExpression", defaultValue = ".*[warning|note]: .*" )
268     private String buildWarningsRegularExpression;
269 
270     /** Specifies the NDK toolchain to use for the build.  This will be using the NDK_TOOLCHAIN define on the ndk-build commandline.
271      */
272     @Parameter( property = "android.ndk.build.ndk-toolchain" )
273     private String ndkToolchain;
274 
275     /**
276      * Specifies the final name of the library output by the build (this allows the pom to override the default artifact name).
277      * The value should not include the 'lib' prefix or filename extension (e.g. '.so').
278      */
279     @Parameter( property = "android.ndk.finalLibraryName" )
280     private String finalLibraryName;
281 
282     /**
283      * Specifies the makefile to use for the build (if other than the default Android.mk).
284      */
285     @Parameter( property = "android.ndk.makefile" )
286     private String makefile;
287 
288     /**
289      * Specifies the application makefile to use for the build (if other than the default Application.mk).
290      */
291     @Parameter( property = "android.ndk.applicationMakefile" )
292     private String applicationMakefile;
293 
294     /**
295      * Flag indicating whether to use the max available jobs for the host machine
296      */
297     @Parameter( property = "android.ndk.maxJobs", defaultValue = "false" )
298     private Boolean maxJobs;
299 
300     /**
301      *
302      */
303     @Parameter()
304     private List<AdditionallyBuiltModule> additionallyBuiltModules;
305 
306     /**
307      * Flag indicating whether or not the build should be skipped entirely
308      */
309     @Parameter( defaultValue = "false" )
310     private boolean skip;
311 
312     /**
313      * Flag indicating whether or not multiple number of native libraries should be included
314      */
315     @Parameter( defaultValue = "false" )
316     private boolean allowMultiArtifacts;
317 
318     /**
319      * The Jar archiver.
320      */
321     @Component( role = org.codehaus.plexus.archiver.Archiver.class, hint = "jar" )
322     private JarArchiver jarArchiver;
323 
324     @Component( role = org.apache.maven.artifact.handler.ArtifactHandler.class, hint = "har" )
325     private ArtifactHandler harArtifactHandler;
326 
327     /**
328      * The maven project.
329      */
330     @Parameter( defaultValue = "${project}", readonly = true, required = true )
331     protected MavenProject project;
332 
333     /**
334      * Maven ProjectHelper.
335      */
336     @Component
337     protected MavenProjectHelper projectHelper;
338 
339     @Component
340     private ArtifactResolver artifactResolver;
341 
342     /**
343      * Dependency graph builder component.
344      */
345     @Component( hint = "default" )
346     protected DependencyGraphBuilder dependencyGraphBuilder;
347 
348     private ArtifactResolverHelper artifactResolverHelper;
349     private NativeHelper nativeHelper;
350 
351     /**
352      * @parameter expression="${mojoExecution}"
353      */
354     @Component
355     private org.apache.maven.plugin.MojoExecution execution;
356 
357     /**
358      * @throws MojoExecutionException
359      * @throws MojoFailureException
360      */
361     public void execute() throws MojoExecutionException, MojoFailureException
362     {
363         if ( skip )
364         {
365             getLog().info( "Skipping execution as per configuration" );
366             return;
367         }
368 
369         if ( !attachLibrariesArtifacts && NativeHelper.isNativeArtifactProject( project ) )
370         {
371             getLog().warn( "Configured to not attach artifacts, this may cause an error at install/deploy time" );
372         }
373 
374         // Validate the NDK
375         final File ndkBuildFile = new File( getAndroidNdk().getNdkBuildPath() );
376         NativeHelper.validateNDKVersion( ndkBuildFile.getParentFile() );
377 
378         validateMakefile( project, makefile );
379 
380         final String[] resolvedNDKArchitectures = NativeHelper.getNdkArchitectures( architectures, applicationMakefile, project.getBasedir() );
381 
382         // Resolve all dependencies
383 
384         final Set<Artifact> nativeLibraryArtifacts = findNativeLibraryDependencies();
385 
386         // If there are any static libraries the code needs to link to, include those in the make file
387         final Set<Artifact> resolvedNativeLibraryArtifacts = getArtifactResolverHelper().resolveArtifacts( nativeLibraryArtifacts );
388 
389         getLog().debug( "resolveArtifacts found " + resolvedNativeLibraryArtifacts.size() + ": " + resolvedNativeLibraryArtifacts.toString() );
390 
391         CompileCommand compileCommand = new CompileCommand ();
392         compileCommand.nativeLibraryDepedencies = resolvedNativeLibraryArtifacts;
393         compileCommand.resolvedArchitectures = resolvedNDKArchitectures;
394 
395         setupOutputDirectories( compileCommand );
396 
397         compile ( compileCommand );
398 
399     }
400 
401     private void setupOutputDirectories ( final CompileCommand compileCommand )
402     {
403         compileCommand.librariesOutputDirectory = librariesOutputDirectory;
404         compileCommand.objectsOutputDirectory = objectsOutputDirectory;
405 
406         // This indicates that the build is either the default build (by extension)
407         // or an execution within an (for example) APKLIB build.
408         // The execution can currently not be named since it would differ
409         // for APKLIB once that is executed.
410         //
411         // TODO: Once the AAR/APKLIB can pull the attached artifacts from the
412         // TODO: Maven session, this can be sorted out better.
413         //
414         if ( ! "default-ndk-build".equals( execution.getExecutionId () ) && ! "default".equals ( execution.getExecutionId () ) )
415         {
416             String libsOut = librariesOutputDirectory.getAbsolutePath();
417             String out = objectsOutputDirectory.getAbsolutePath();
418 
419             libsOut = libsOut + "/" + execution.getExecutionId ();
420             out = out + "/" + execution.getExecutionId ();
421 
422             // FIXME: Will this actually work - what happens if the execution is the single one & it has an ID?
423             // compileCommand.librariesOutputDirectory = new File( libsOut );
424             // compileCommand.objectsOutputDirectory = new File( out );
425 
426         }
427 
428         getLog ().debug ( "Setting library out to " + compileCommand.librariesOutputDirectory.getAbsolutePath () );
429         getLog ().debug ( "Setting out to " + compileCommand.objectsOutputDirectory.getAbsolutePath () );
430 
431     }
432 
433     private class CompileCommand
434     {
435         private File objectsOutputDirectory;
436         private File librariesOutputDirectory;
437 
438         private Set<Artifact> nativeLibraryDepedencies;
439         private String[] resolvedArchitectures;
440 
441         public Set<Artifact> getNativeLibraryDepedencies ()
442         {
443             return nativeLibraryDepedencies;
444         }
445 
446         public String[] getResolvedArchitectures ()
447         {
448             return resolvedArchitectures;
449         }
450     }
451 
452     private void compile ( CompileCommand compileCommand ) throws MojoExecutionException
453     {
454         MakefileHelper.MakefileResponse makefileResponse = null;
455         try
456         {
457             // Start setting up the command line to be executed
458             final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor ();
459 
460             // Add an error listener to the build - this allows the build to conditionally fail
461             // depending on a) the output of the build b) whether or not build errors (output on stderr) should be
462             // ignored and c) whether the pattern matches or not
463             executor.setErrorListener ( getNdkErrorListener () );
464 
465             final Set<Artifact> nativeLibraryArtifacts = compileCommand.getNativeLibraryDepedencies ();
466             // If there are any static libraries the code needs to link to, include those in the make file
467             final Set<Artifact> resolvedNativeLibraryArtifacts = getArtifactResolverHelper ().resolveArtifacts ( nativeLibraryArtifacts );
468 
469             getLog ().debug ( "resolveArtifacts found " + resolvedNativeLibraryArtifacts.size () + ": " + resolvedNativeLibraryArtifacts.toString () );
470 
471             final File buildFolder = new File ( buildDirectory, "makefile" );
472             buildFolder.mkdirs ();
473 
474             final File androidMavenMakefile = new File ( buildFolder, "android_maven_plugin_makefile.mk" );
475             final MakefileHelper makefileHelper = new MakefileHelper ( project, getLog (), getArtifactResolverHelper (), harArtifactHandler, unpackedLibsFolder,
476                     buildDirectory );
477 
478             MakefileHelper.MakefileRequest makefileRequest = new MakefileHelper.MakefileRequest ();
479             makefileRequest.artifacts = resolvedNativeLibraryArtifacts;
480             makefileRequest.defaultNDKArchitecture = "armeabi";
481             makefileRequest.useHeaderArchives = useHeaderArchives;
482             makefileRequest.ignoreHeaderFilesArchives = ignoreHeaderFilesArchives;
483             makefileRequest.leaveTemporaryBuildArtifacts = leaveTemporaryBuildArtifacts;
484             makefileRequest.architectures = compileCommand.getResolvedArchitectures ();
485 
486             makefileResponse = makefileHelper.createMakefileFromArtifacts ( makefileRequest );
487 
488             final FileOutputStream output = new FileOutputStream ( androidMavenMakefile );
489             try
490             {
491                 IOUtil.copy ( makefileResponse.getMakeFile (), output );
492             }
493             finally
494             {
495                 output.close ();
496             }
497 
498             // Add the path to the generated makefile - this is picked up by the build (by an include from the user)
499             executor.addEnvironment ( "ANDROID_MAVEN_PLUGIN_MAKEFILE", androidMavenMakefile.getAbsolutePath () );
500 
501             setupNativeLibraryEnvironment ( executor, makefileResponse );
502 
503             // Adds the location of the Makefile capturer file - this file will after the build include
504             // things like header files, flags etc.  It is processed after the build to retrieve the headers
505             // and also capture flags etc ...
506             final File makefileCaptureFile = File.createTempFile ( "android_maven_plugin_makefile_captures", ".tmp", buildDirectory );
507 
508             if ( !leaveTemporaryBuildArtifacts )
509             {
510                 makefileCaptureFile.deleteOnExit ();
511             }
512 
513             executor.addEnvironment ( MakefileHelper.MAKEFILE_CAPTURE_FILE, makefileCaptureFile.getAbsolutePath () );
514 
515             // Add any defined system properties
516             if ( systemProperties != null && !systemProperties.isEmpty() )
517             {
518                 for ( Map.Entry<String, String> entry : systemProperties.entrySet() )
519                 {
520                     executor.addEnvironment( entry.getKey(), entry.getValue() );
521                 }
522             }
523 
524             executor.setLogger( this.getLog() );
525 
526             // Setup the command line for the make
527             final List<String> commands = new ArrayList<String>();
528 
529             configureArchitectures( commands, compileCommand.getResolvedArchitectures () );
530 
531             configureBuildDirectory( compileCommand, commands );
532 
533             configureMakefile( commands );
534 
535             configureApplicationMakefile( commands );
536 
537             configureMaxJobs( commands );
538 
539             // Only allow configuration of the toolchain if the architecture being built is a single one!
540             if ( compileCommand.getResolvedArchitectures ().length == 1 )
541             {
542                 configureNdkToolchain ( compileCommand.getResolvedArchitectures ()[0], commands );
543             }
544 
545             configureAdditionalCommands( commands );
546 
547             // If a build target is specified, tag that onto the command line as the very last of the parameters
548             commands.add ( target != null ? target : "all" );
549 
550             final String ndkBuildPath = resolveNdkBuildExecutable ();
551             getLog ().debug ( ndkBuildPath + " " + commands.toString () );
552             getLog ().info ( "Executing NDK make at : " + buildDirectory );
553 
554             executor.setCaptureStdOut ( true );
555             executor.executeCommand ( ndkBuildPath, commands, buildDirectory, true );
556             getLog ().debug ( "Executed NDK  make at : " + buildDirectory );
557 
558             if ( attachLibrariesArtifacts )
559             {
560                 // Attempt to attach the native libraries (shared only)
561                 for ( int i = 0; i < compileCommand.getResolvedArchitectures ().length; i++ )
562                 {
563                     String architecture = compileCommand.getResolvedArchitectures ()[ i ];
564                     processCompiledArtifacts ( compileCommand, architecture, makefileCaptureFile );
565                 }
566             }
567             else
568             {
569                 getLog ().info ( "Will skip attaching compiled libraries as per configuration" );
570             }
571 
572         }
573         catch ( Exception e )
574         {
575             throw new MojoExecutionException ( "Failure during build: " + e.getMessage (), e );
576         }
577         finally
578         {
579             cleanupAfterBuild( makefileResponse );
580         }
581 
582     }
583 
584     private void configureArchitectures ( final List<String> commands, final String[] resolvedArchitectures )
585     {
586         StringBuilder sb = new StringBuilder ( );
587 
588         for ( int i = 0; i < resolvedArchitectures.length; i++ )
589         {
590             sb.append ( resolvedArchitectures[i] );
591             if ( ( i + 1 ) < resolvedArchitectures.length )
592             {
593                 sb.append ( " " );
594             }
595         }
596 
597         // We always for the APP_ABI onto the command line
598         commands.add ( "APP_ABI=" + sb.toString () );
599 
600     }
601 
602 
603     private void configureBuildDirectory( final CompileCommand compileCommand, final List<String> commands )
604     {
605         // Setup the build directory (defaults to the current directory) but may be different depending
606         // on user configuration
607         commands.add( "-C" );
608         commands.add( workingDirectory.getAbsolutePath() );
609 
610         // Next, configure the output directories
611         commands.add( "NDK_LIBS_OUT=" + compileCommand.librariesOutputDirectory.getAbsolutePath () );
612         commands.add( "NDK_OUT=" + compileCommand.objectsOutputDirectory.getAbsolutePath ()  );
613 
614     }
615 
616     private void configureMakefile( final List<String> commands ) throws MojoExecutionException
617     {
618         // If the build should use a custom makefile or not - some validation is done to ensure
619         // this exists and all
620         if ( makefile != null )
621         {
622             File makeFile = new File( project.getBasedir(), makefile );
623             if ( !makeFile.exists() )
624             {
625                 getLog().error( "Specified makefile " + makeFile + " does not exist" );
626                 throw new MojoExecutionException( "Specified makefile " + makeFile + " does not exist" );
627             }
628             commands.add( "APP_BUILD_SCRIPT=" + makefile );
629         }
630     }
631 
632     private void cleanupAfterBuild ( final MakefileHelper.MakefileResponse makefileResponse )
633     {
634         // directories after we're done
635         if ( makefileResponse != null )
636         {
637             getLog().info( "Cleaning up extracted include directories used for build" );
638             MakefileHelper.cleanupAfterBuild( makefileResponse );
639         }
640     }
641 
642     private void configureAdditionalCommands( final List<String> commands )
643     {
644         // Anything else on the command line the user wants to add - simply splice it up and
645         // add it one by one to the command line
646         if ( additionalCommandline != null )
647         {
648             final String[] additionalCommands = additionalCommandline.split( " " );
649             commands.addAll( Arrays.asList( additionalCommands ) );
650         }
651     }
652 
653     private void configureApplicationMakefile( List<String> commands )
654             throws MojoExecutionException
655     {
656         if ( applicationMakefile != null )
657         {
658             File appMK = new File( project.getBasedir(), applicationMakefile );
659             if ( !appMK.exists() )
660             {
661                 getLog().error( "Specified application makefile " + appMK + " does not exist" );
662                 throw new MojoExecutionException( "Specified application makefile " + appMK + " does not exist" );
663             }
664             commands.add( "NDK_APPLICATION_MK=" + applicationMakefile );
665         }
666     }
667 
668     private void configureMaxJobs( List<String> commands )
669     {
670         if ( maxJobs )
671         {
672             String jobs = String.valueOf( Runtime.getRuntime().availableProcessors() );
673             getLog().info( "executing " + jobs + " parallel jobs" );
674             commands.add( "-j" );
675             commands.add( jobs );
676         }
677     }
678 
679     private void configureNdkToolchain( String architecture, List<String> commands )
680             throws MojoExecutionException
681     {
682         if ( ndkToolchain != null )
683         {
684             // Setup the correct toolchain to use
685             // FIXME: perform a validation that this toolchain exists in the NDK and is valid for the specified
686             // FIXME: architecture!
687             commands.add( "NDK_TOOLCHAIN=" + ndkToolchain );
688             commands.add( "APP_ABI=" + architecture );
689         }
690         else
691         {
692             // Resolve the toolchain from the architecture
693             // <architectures>
694             //   <x86>x86-4.6</x86>
695             //   <armeabi>x86-4.6</armeabi>
696             // </architectures>
697             final String toolchainFromArchitecture = getAndroidNdk().getToolchainFromArchitecture( architecture, architectureToolchainMappings );
698             getLog().debug( "Resolved toolchain for " + architecture + " to " + toolchainFromArchitecture );
699             commands.add( "NDK_TOOLCHAIN=" + toolchainFromArchitecture );
700             commands.add( "APP_ABI=" + architecture );
701         }
702     }
703 
704     /**
705      * Attaches native libs to project.
706      */
707     private void processCompiledArtifacts ( final CompileCommand compileCommand, String architecture, final File makefileCaptureFile ) throws IOException, MojoExecutionException
708     {
709         // Where the NDK build creates the libs.
710         final File nativeLibraryDirectory = new File( compileCommand.librariesOutputDirectory, architecture );
711 
712         // Where the NDK build creates the object files - static files end up here
713         final File nativeObjDirectory = new File( new File( compileCommand.objectsOutputDirectory, "local" ), architecture );
714 
715         final List<String> classifiers = new ArrayList<String>();
716         if ( allowMultiArtifacts )
717         {
718             attachManyArtifacts( nativeLibraryDirectory, architecture, nativeObjDirectory, classifiers );
719         }
720         else
721         {
722             attachOneArtifact( nativeLibraryDirectory, architecture, nativeObjDirectory, classifiers );
723         }
724 
725         if ( additionallyBuiltModules != null && !additionallyBuiltModules.isEmpty() )
726         {
727             for ( AdditionallyBuiltModule additionallyBuiltModule : additionallyBuiltModules )
728             {
729                 File additionalBuiltModuleFile = nativeLibraryFromName( true, nativeLibraryDirectory, additionallyBuiltModule.getName() );
730 
731                 // If it doesnt exist, check the object directory
732                 if ( !additionalBuiltModuleFile.exists() )
733                 {
734                     additionalBuiltModuleFile = nativeLibraryFromName( true, nativeObjDirectory, additionallyBuiltModule.getName() );
735                 }
736 
737                 // FIMXE: This should be validated
738                 final String additionallyBuiltArtifactType = resolveArtifactType( additionalBuiltModuleFile );
739 
740                 String additionallyBuiltClassifier = architecture + "-" + additionallyBuiltModule.getClassifier();
741                 projectHelper.attachArtifact( this.project, additionallyBuiltArtifactType, additionallyBuiltClassifier, additionalBuiltModuleFile );
742                 classifiers.add( additionallyBuiltClassifier );
743             }
744         }
745 
746         // Process conditionally any of the headers to include into the header archive file
747         if ( attachHeaderFiles )
748         {
749             attachHeaderFiles( compileCommand, makefileCaptureFile, classifiers );
750         }
751 
752 
753     }
754 
755     private void attachManyArtifacts( File nativeLibraryDirectory, String architecture, File nativeObjDirectory, List<String> classifiers ) throws MojoExecutionException
756     {
757         List<File> artifacts = Arrays.asList( findNativeLibrary( nativeLibraryDirectory, nativeObjDirectory ) );
758         for ( File file : artifacts )
759         {
760             attachArtifactFile( architecture, classifiers, file );
761         }
762     }
763 
764     private void attachOneArtifact( File nativeLibraryDirectory, String architecture, File nativeObjDirectory, List<String> classifiers ) throws MojoExecutionException
765     {
766         final File nativeArtifactFile;
767         if ( finalLibraryName == null )
768         {
769             nativeArtifactFile = findNativeLibrary( nativeLibraryDirectory, nativeObjDirectory )[0];
770         }
771         else
772         {
773             nativeArtifactFile = nativeLibraryFromName( nativeLibraryDirectory, nativeObjDirectory, finalLibraryName );
774         }
775 
776         attachArtifactFile( architecture, classifiers, nativeArtifactFile );
777     }
778 
779     private void attachArtifactFile( String architecture, List<String> classifiers, File nativeArtifactFile )
780     {
781         final String artifactType = resolveArtifactType( nativeArtifactFile );
782         getLog().debug( "Adding native compiled artifact: " + nativeArtifactFile );
783 
784         final String actualClassifier = ( classifier == null ) ? architecture : architecture + "-" + classifier;
785         projectHelper.attachArtifact( this.project, artifactType, actualClassifier, nativeArtifactFile );
786         classifiers.add( actualClassifier );
787     }
788 
789     /**
790      * Search the specified directory for native artifacts that match the artifact Id
791      */
792     private File[] findNativeLibrary( File nativeLibDirectory, final File nativeObjDirectory ) throws MojoExecutionException
793     {
794         getLog().info( "Searching " + nativeLibDirectory + " for built shared library" );
795         // FIXME: Should really just look for shared libraries in here really ....
796         File[] files = nativeLibDirectory.listFiles( new FilenameFilter()
797         {
798             public boolean accept( final File dir, final String name )
799             {
800                 String libraryName = finalLibraryName;
801 
802                 if ( libraryName == null || libraryName.isEmpty() )
803                 {
804                     libraryName = project.getArtifactId();
805                 }
806 
807                 // FIXME: The following logic won't work for an APKLIB building a static library
808                 final String extension = Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( project.getPackaging() ) ? ".a" : ".so";
809                 boolean found = name.startsWith( "lib" + libraryName ) && name.endsWith( extension );
810                 if ( !found )
811                 {
812                     // Issue #14 : Work-around issue where the project is actually called "lib" something
813                     if ( libraryName.startsWith( "lib" ) )
814                     {
815                         found = name.startsWith( libraryName ) && name.endsWith( extension );
816                     }
817                 }
818                 return found;
819             }
820         } );
821 
822         // Check the object output directory as well
823         // FIXME: Should really just look for static libraries in here really ....
824 
825         if ( files == null || files.length == 0 )
826         {
827             getLog().info( "Searching " + nativeObjDirectory + " for built static library" );
828             files = nativeObjDirectory.listFiles( new FilenameFilter()
829             {
830                 public boolean accept( final File dir, final String name )
831                 {
832                     String libraryName = finalLibraryName;
833 
834                     if ( libraryName == null || libraryName.isEmpty() )
835                     {
836                         libraryName = project.getArtifactId();
837                     }
838 
839                     // FIXME: The following logic won't work for an APKLIB building a static library
840                     if ( Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( project.getPackaging() ) )
841                     {
842                         return name.startsWith( "lib" + libraryName ) && name.endsWith( ".a" );
843                     }
844                     else
845                     {
846                         return name.startsWith( "lib" + libraryName ) && name.endsWith( ".so" );
847                     }
848                 }
849             } );
850 
851         }
852 
853         // slight limitation at this stage - we only handle a single .so artifact
854         if ( ( files == null || files.length != 1 )  && !allowMultiArtifacts )
855         {
856             getLog().warn( "Error while detecting native compile artifacts: " + ( files == null || files.length == 0 ? "None found" : "Found more than 1 artifact" ) );
857             if ( target != null )
858             {
859                 getLog().warn( "Using the 'target' configuration option to specify the output file name is no longer supported, use 'finalLibraryName' instead." );
860             }
861 
862             if ( files != null && files.length > 1 )
863             {
864                 getLog().debug( "List of files found: " + Arrays.asList( files ) );
865                 getLog().error( "Currently, only a single, final native library is supported by the build" );
866                 throw new MojoExecutionException( "Currently, only a single, final native library is supported by the build" );
867             }
868             else
869             {
870                 getLog().error( "No native compiled library found, did the native compile complete successfully?" );
871                 throw new MojoExecutionException( "No native compiled library found, did the native compile complete successfully?" );
872             }
873         }
874         return files;
875     }
876 
877     private File nativeLibraryFromName( File nativeLibDirectory, final File nativeObjDirectory, final String libraryName ) throws MojoExecutionException
878     {
879         try
880         {
881             return nativeLibraryFromName( false, nativeLibDirectory, libraryName );
882         }
883         catch ( MojoExecutionException e )
884         {
885             // Try the obj directory
886             return nativeLibraryFromName( true, nativeObjDirectory, libraryName );
887         }
888     }
889 
890     private File nativeLibraryFromName( boolean logErrors, File directory, final String libraryName ) throws MojoExecutionException
891     {
892         final File libraryFile;
893         // Find the nativeArtifactFile in the nativeLibDirectory/finalLibraryName
894         if ( Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( project.getPackaging() ) || Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( project.getPackaging() ) )
895         {
896             libraryFile = new File( directory, "lib" + libraryName + "." + project.getPackaging() );
897         }
898         else
899         {
900             final File staticLib = new File( directory, "lib" + libraryName + ".a" );
901             if ( staticLib.exists() )
902             {
903                 libraryFile = staticLib;
904             }
905             else
906             {
907                 libraryFile = new File( directory, "lib" + libraryName + ".so" );
908             }
909         }
910         if ( !libraryFile.exists() )
911         {
912             if ( logErrors )
913             {
914                 getLog().error( "Could not locate final native library using the provided finalLibraryName " + libraryName + " (tried " + libraryFile.getAbsolutePath() + ")" );
915             }
916             throw new MojoExecutionException( "Could not locate final native library using the provided finalLibraryName " + libraryName + " (tried " + libraryFile.getAbsolutePath() + ")" );
917         }
918 
919         return libraryFile;
920     }
921 
922 
923     private CommandExecutor.ErrorListener getNdkErrorListener()
924     {
925         return new CommandExecutor.ErrorListener()
926         {
927             @Override
928             public boolean isError( String error )
929             {
930 
931                 // Unconditionally ignore *All* build warning if configured to
932                 if ( ignoreBuildWarnings )
933                 {
934                     return false;
935                 }
936 
937                 final Pattern pattern = Pattern.compile( buildWarningsRegularExpression );
938                 final Matcher matcher = pattern.matcher( error );
939 
940                 // If the the reg.exp actually matches, we can safely say this is not an error
941                 // since in theory the user told us so
942                 if ( matcher.matches() )
943                 {
944                     return false;
945                 }
946 
947                 // Otherwise, it is just another error
948                 return true;
949             }
950         };
951     }
952 
953     /**
954      * Validate the makefile - if our packaging type is so (for example) and there are
955      * dependencies on .a files (or shared files for that matter) the makefile should include
956      * the include of our Android Maven plugin generated makefile.
957      */
958     private void validateMakefile( MavenProject project, String file )
959     {
960         // TODO: actually perform validation
961     }
962 
963     private String resolveNdkBuildExecutable() throws MojoExecutionException
964     {
965         if ( ndkBuildExecutable != null )
966         {
967             getLog().debug( "ndk-build overriden, using " + ndkBuildExecutable );
968             return ndkBuildExecutable;
969         }
970         return getAndroidNdk().getNdkBuildPath();
971     }
972 
973     private void attachHeaderFiles ( final CompileCommand compileCommand, final File localCIncludesFile, final List<String> classifiers ) throws MojoExecutionException, IOException
974     {
975 
976         final List<HeaderFilesDirective> finalHeaderFilesDirectives = new ArrayList<HeaderFilesDirective>();
977 
978         if ( useLocalSrcIncludePaths )
979         {
980             Properties props = new Properties();
981             props.load( new FileInputStream( localCIncludesFile ) );
982             String localCIncludes = props.getProperty( "LOCAL_C_INCLUDES" );
983             if ( localCIncludes != null && !localCIncludes.trim().isEmpty() )
984             {
985                 String[] includes = localCIncludes.split( " " );
986                 for ( String include : includes )
987                 {
988                     final HeaderFilesDirective headerFilesDirective = new HeaderFilesDirective();
989                     File includeDir = new File( project.getBasedir(), include );
990                     headerFilesDirective.setDirectory( includeDir.getAbsolutePath() );
991                     headerFilesDirective.setIncludes( new String[]{ "**/*.h", "**/*.hpp" } );
992                     finalHeaderFilesDirectives.add( headerFilesDirective );
993                 }
994             }
995         }
996         else
997         {
998             if ( headerFilesDirectives != null )
999             {
1000                 finalHeaderFilesDirectives.addAll( headerFilesDirectives );
1001             }
1002         }
1003         if ( finalHeaderFilesDirectives.isEmpty() )
1004         {
1005             getLog().debug( "No header files included, will add default set" );
1006             final HeaderFilesDirective e = new HeaderFilesDirective();
1007             final File folder = new File( project.getBasedir() + "/jni" );
1008             if ( folder.exists() )
1009             {
1010                 e.setDirectory( folder.getAbsolutePath() );
1011                 e.setIncludes( new String[] { "**/*.h", "**/*.hpp" } );
1012                 finalHeaderFilesDirectives.add( e );
1013             }
1014         }
1015         createHeaderArchive( compileCommand, finalHeaderFilesDirectives, classifiers );
1016     }
1017 
1018     private void createHeaderArchive ( final CompileCommand compileCommand, final List<HeaderFilesDirective> finalHeaderFilesDirectives, final List<String> classifiers ) throws MojoExecutionException
1019     {
1020         try
1021         {
1022             MavenArchiver mavenArchiver = new MavenArchiver();
1023             mavenArchiver.setArchiver( jarArchiver );
1024 
1025             final File jarFile = File.createTempFile( "tmp", ".har", buildDirectory );
1026 
1027             mavenArchiver.setOutputFile( jarFile );
1028 
1029             for ( HeaderFilesDirective headerFilesDirective : finalHeaderFilesDirectives )
1030             {
1031                 mavenArchiver.getArchiver().addDirectory( new File( headerFilesDirective.getDirectory() ), headerFilesDirective.getIncludes(), headerFilesDirective.getExcludes() );
1032             }
1033 
1034             final MavenArchiveConfiguration mavenArchiveConfiguration = new MavenArchiveConfiguration();
1035             mavenArchiveConfiguration.setAddMavenDescriptor( false );
1036 
1037             mavenArchiver.createArchive( project, mavenArchiveConfiguration );
1038 
1039             for ( String classifier : classifiers )
1040             {
1041                 getLog().debug( "Attaching 'har' classifier=" + classifier + " file=" + jarFile );
1042                 projectHelper.attachArtifact( project, Const.ArtifactType.NATIVE_HEADER_ARCHIVE, classifier, jarFile );
1043             }
1044 
1045         }
1046         catch ( Exception e )
1047         {
1048             throw new MojoExecutionException( e.getMessage() );
1049         }
1050     }
1051 
1052     private void setupNativeLibraryEnvironment ( final CommandExecutor executor,
1053                                                  final MakefileHelper.MakefileResponse makefileResponse )
1054     {
1055         if ( makefileResponse.hasStaticLibraryDepdendencies() )
1056         {
1057             String staticlibs = makefileResponse.getStaticLibraryList();
1058             executor.addEnvironment( "ANDROID_MAVEN_PLUGIN_LOCAL_STATIC_LIBRARIES", staticlibs );
1059             getLog().debug( "Set ANDROID_MAVEN_PLUGIN_LOCAL_STATIC_LIBRARIES = " + staticlibs );
1060         }
1061 
1062         if ( makefileResponse.hasSharedLibraryDepdendencies() )
1063         {
1064             String staticlibs = makefileResponse.getSharedLibraryList();
1065             executor.addEnvironment( "ANDROID_MAVEN_PLUGIN_LOCAL_SHARED_LIBRARIES", staticlibs );
1066             getLog().debug( "Set ANDROID_MAVEN_PLUGIN_LOCAL_SHARED_LIBRARIES = " + staticlibs );
1067         }
1068     }
1069 
1070     private Set<Artifact> findNativeLibraryDependencies() throws MojoExecutionException
1071     {
1072         final NativeHelper nativeHelper = getNativeHelper();
1073         final Set<Artifact> staticLibraryArtifacts = nativeHelper.getNativeDependenciesArtifacts( false );
1074         final Set<Artifact> sharedLibraryArtifacts = nativeHelper.getNativeDependenciesArtifacts( true );
1075 
1076         final Set<Artifact> mergedArtifacts = new LinkedHashSet<Artifact>();
1077         filterNativeDependencies( mergedArtifacts, staticLibraryArtifacts );
1078         filterNativeDependencies( mergedArtifacts, sharedLibraryArtifacts );
1079 
1080         getLog().debug( "findNativeLibraryDependencies found " + mergedArtifacts.size() + ": " + mergedArtifacts.toString() );
1081 
1082         return mergedArtifacts;
1083     }
1084 
1085     /**
1086      * Selectively add artifacts from source to target excluding any whose groupId and artifactId match
1087      * the current build.
1088      * <p/>
1089      * Introduced to work around an issue when the ndk-build is executed twice by maven for example when
1090      * invoking maven 'install site'. In this case the artifacts attached by the first invocation are
1091      * found but are not valid dependencies and must be excluded.
1092      *
1093      * @param targetSet artifact Set to copy in to
1094      * @param source    artifact Set to filter
1095      */
1096     private void filterNativeDependencies( Set<Artifact> targetSet, Set<Artifact> source )
1097     {
1098         for ( Artifact a : source )
1099         {
1100             if ( project.getGroupId().equals( a.getGroupId() ) && project.getArtifactId().equals( a.getArtifactId() ) )
1101             {
1102                 getLog().warn( "Excluding native dependency attached by this build" );
1103             }
1104             else
1105             {
1106                 targetSet.add( a );
1107             }
1108         }
1109     }
1110 
1111     /**
1112      * Resolve the artifact type from the current project and the specified file.  If the project packaging is
1113      * either 'a' or 'so' it will use the packaging, otherwise it checks the file for the extension
1114      *
1115      * @param file The file being added as an artifact
1116      * @return The artifact type (so or a)
1117      */
1118     private String resolveArtifactType( File file )
1119     {
1120         if ( Const.ArtifactType.NATIVE_SYMBOL_OBJECT.equals( project.getPackaging() ) || Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE.equals( project.getPackaging() ) )
1121         {
1122             return project.getPackaging();
1123         }
1124         else
1125         {
1126             // At this point, the file (as found by our filtering previously will end with either 'so' or 'a'
1127             return file.getName().endsWith( Const.ArtifactType.NATIVE_SYMBOL_OBJECT ) ? Const.ArtifactType.NATIVE_SYMBOL_OBJECT : Const.ArtifactType.NATIVE_IMPLEMENTATION_ARCHIVE;
1128         }
1129     }
1130 
1131     /**
1132      * <p>Returns the Android NDK to use.</p>
1133      * <p/>
1134      * <p>Current implementation looks for <code>&lt;ndk&gt;&lt;path&gt;</code> configuration in pom, then System
1135      * property <code>android.ndk.path</code>, then environment variable <code>ANDROID_NDK_HOME</code>.
1136      * <p/>
1137      * <p>This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is
1138      * based on available parameters. This method should be the only one you should need to look at to understand how
1139      * the Android NDK is chosen, and from where on disk.</p>
1140      *
1141      * @return the Android NDK to use.
1142      * @throws org.apache.maven.plugin.MojoExecutionException if no Android NDK path configuration is available at all.
1143      */
1144     protected AndroidNdk getAndroidNdk() throws MojoExecutionException
1145     {
1146         final File chosenNdkPath;
1147 
1148         if ( ndkPath != null )
1149         {
1150             // -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
1151             chosenNdkPath = ndkPath;
1152         }
1153         else
1154         {
1155             // No -Dandroid.ndk.path is set on command line, or via <properties><ndk.path>...
1156             chosenNdkPath = new File( getAndroidNdkHomeOrThrow() );
1157         }
1158 
1159         return new AndroidNdk( chosenNdkPath );
1160     }
1161 
1162 
1163     /**
1164      * @return
1165      * @throws MojoExecutionException
1166      */
1167     private String getAndroidNdkHomeOrThrow() throws MojoExecutionException
1168     {
1169         final String androidHome = System.getenv( ENV_ANDROID_NDK_HOME );
1170         if ( StringUtils.isBlank( androidHome ) )
1171         {
1172             throw new MojoExecutionException( "No Android NDK path could be found. You may configure it in the pom using <ndkPath>...</ndkPath> or "
1173                     + "<properties><android.ndk.path>...</android.ndk.path></properties> or on command-line using -Dandroid.ndk.path=... "
1174                     + "or by setting environment variable " + ENV_ANDROID_NDK_HOME );
1175         }
1176         return androidHome;
1177     }
1178 
1179     protected final ArtifactResolverHelper getArtifactResolverHelper()
1180     {
1181         if ( artifactResolverHelper == null )
1182         {
1183             artifactResolverHelper = new ArtifactResolverHelper( artifactResolver, new MavenToPlexusLogAdapter( getLog() ), project.getRemoteArtifactRepositories() );
1184         }
1185         return artifactResolverHelper;
1186     }
1187 
1188     protected final NativeHelper getNativeHelper()
1189     {
1190         if ( nativeHelper == null )
1191         {
1192             nativeHelper = new NativeHelper( project, dependencyGraphBuilder, getLog() );
1193         }
1194         return nativeHelper;
1195     }
1196 
1197 
1198 }