com.jayway.maven.plugins.android.phase01generatesources.GenerateSourcesMojo Maven / Gradle / Ivy
Show all versions of android-maven-plugin Show documentation
/*
* Copyright (C) 2009 Jayway AB
* Copyright (C) 2007-2008 JVending Masa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jayway.maven.plugins.android.phase01generatesources;
import com.android.manifmerger.ManifestMerger;
import com.android.manifmerger.MergerLog;
import com.android.utils.StdLogger;
import com.jayway.maven.plugins.android.AbstractAndroidMojo;
import com.jayway.maven.plugins.android.CommandExecutor;
import com.jayway.maven.plugins.android.ExecutionException;
import com.jayway.maven.plugins.android.common.AaptCommandBuilder;
import com.jayway.maven.plugins.android.common.AaptCommandBuilder.AaptPackageCommandBuilder;
import com.jayway.maven.plugins.android.common.DependencyResolver;
import com.jayway.maven.plugins.android.common.FileRetriever;
import com.jayway.maven.plugins.android.configuration.BuildConfigConstant;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.repository.RepositorySystem;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.logging.console.ConsoleLogger;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static com.jayway.maven.plugins.android.common.AndroidExtension.AAR;
import static com.jayway.maven.plugins.android.common.AndroidExtension.APK;
import static com.jayway.maven.plugins.android.common.AndroidExtension.APKLIB;
import static com.jayway.maven.plugins.android.common.AndroidExtension.APKSOURCES;
/**
* Generates R.java
based on resources specified by the resources
configuration parameter.
* Generates java files based on aidl files.
*
* @author [email protected]
* @author Manfred Moser
* @author William Ferguson
* @author Malachi de AElfweald [email protected]
*/
@Mojo(
name = "generate-sources",
defaultPhase = LifecyclePhase.GENERATE_SOURCES,
requiresDependencyResolution = ResolutionScope.COMPILE
)
public class GenerateSourcesMojo extends AbstractAndroidMojo
{
/**
*
* Override default merging. You must have SDK Tools r20+
*
*
*
* IMPORTANT: The resource plugin needs to be disabled for the
* process-resources
phase, so the "default-resources"
* execution must be added. Without this the non-merged manifest will get
* re-copied to the build directory.
*
*
*
* The androidManifestFile
should also be configured to pull
* from the build directory so that later phases will pull the merged
* manifest file.
*
*
* Example POM Setup:
*
*
*
* <build>
* ...
* <plugins>
* ...
* <plugin>
* <artifactId>maven-resources-plugin</artifactId>
* <version>2.6</version>
* <executions>
* <execution>
* <phase>initialize</phase>
* <goals>
* <goal>resources</goal>
* </goals>
* </execution>
* <execution>
* <id>default-resources</id>
* <phase>DISABLED</phase>
* </execution>
* </executions>
* </plugin>
* <plugin>
* <groupId>com.jayway.maven.plugins.android.generation2</groupId>
* <artifactId>android-maven-plugin</artifactId>
* <configuration>
* <androidManifestFile>
* ${project.build.directory}/AndroidManifest.xml
* </androidManifestFile>
* <mergeManifests>true</mergeManifests>
* </configuration>
* <extensions>true</extensions>
* </plugin>
* ...
* </plugins>
* ...
* </build>
*
*
* You can filter the pre-merged APK manifest. One important note about Eclipse, Eclipse will
* replace the merged manifest with a filtered pre-merged version when the project is refreshed.
* If you want to review the filtered merged version then you will need to open it outside Eclipse
* without refreshing the project in Eclipse.
*
*
* <resources>
* <resource>
* <targetPath>${project.build.directory}</targetPath>
* <filtering>true</filtering>
* <directory>${basedir}</directory>
* <includes>
* <include>AndroidManifest.xml</include>
* </includes>
* </resource>
* </resources>
*
*/
@Parameter( property = "android.mergeManifests", defaultValue = "false" )
protected boolean mergeManifests;
/**
* Whether to produce a warning if there is an aar dependency that has an apklib artifact in its dependency
* tree. The combination of aar library including or depending on an apklib has been deprecated and may not be
* supported by future plugin versions. Traversing the dependency graph is done for all project dependencies
* present in build classpath.
*
* It is recommended to keep this set to true
to catch possible issues as soon as possible.
*/
@Parameter( defaultValue = "true" )
protected boolean warnOnApklibDependencies;
/**
* Whether to fail the build if one of the dependencies and/or the project duplicate a layout file.
*
* Such a scenario generally means that the build will fail with a compilation error due to missing resource files.
* This is because any Ids contained in the duplicate layout files can never be generated by aapt, as appt
* only picks up the first resource file it finds in its path.
*/
@Parameter( defaultValue = "true" )
private boolean failOnConflictingLayouts;
/**
* Whether to fail the build if one of the dependencies and/or the project have similar package in the
* AndroidManifest.
*
* Such scenario generally means that the build will fail with a compilation error due to
* missing resources in the R file generated.
*/
@Parameter( defaultValue = "true" )
private boolean failOnDuplicatePackages;
/**
* Override default generated folder containing aidl classes
*/
@Parameter(
property = "android.genDirectoryAidl",
defaultValue = "${project.build.directory}/generated-sources/aidl"
)
protected File genDirectoryAidl;
/**
* The directory containing the aidl files.
*/
@Parameter( property = "android.aidlSourceDirectory", defaultValue = "${project.build.sourceDirectory}" )
protected File aidlSourceDirectory;
/**
* Parameter designed to generate custom BuildConfig constants
*/
@Parameter( property = "android.buildConfigConstants", readonly = true )
protected BuildConfigConstant[] buildConfigConstants;
/**
*/
@Component
private RepositorySystem repositorySystem;
/**
* Pre AMP-4 AndroidManifest file.
*/
@Parameter( readonly = true, defaultValue = "${project.basedir}/AndroidManifest.xml" )
private File androidManifestFilePre4;
/**
* Pre AMP-4 android resources folder.
*/
@Parameter( readonly = true, defaultValue = "${project.basedir}/res" )
private File resourceDirectoryPre4;
/**
* Pre AMP-4 android assets directory.
*/
@Parameter( readonly = true, defaultValue = "${project.basedir}/assets" )
private File assetsDirectoryPre4;
/**
* Pre AMP-4 native libraries folder.
*/
@Parameter( readonly = true, defaultValue = "${project.basedir}/libs" )
private File nativeLibrariesDirectoryPre4;
/**
* If any non-standard files/folders exist and have NOT been explicitly configured then fail the build.
*/
@Parameter( defaultValue = "true" )
private boolean failOnNonStandardStructure;
/**
* Which dependency scopes should not be included when unpacking dependencies
*/
protected static final List EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION = Arrays.asList(
Artifact.SCOPE_SYSTEM, Artifact.SCOPE_IMPORT
);
/**
* Generates the sources.
*
* @throws MojoExecutionException if it fails.
* @throws MojoFailureException if it fails.
*/
public void execute() throws MojoExecutionException, MojoFailureException
{
// If the current POM isn't an Android-related POM, then don't do
// anything. This helps work with multi-module projects.
if ( ! isCurrentProjectAndroid() )
{
return;
}
validateStandardLocations();
try
{
targetDirectory.mkdirs();
copyManifest();
// Check for a deprecated AAR > APKLIB artifact combination
if ( warnOnApklibDependencies )
{
checkForApklibDependencies();
}
// TODO Do we really want to continue supporting APKSOURCES? How long has it been deprecated
extractSourceDependencies();
// Extract the apklib and aar dependencies into unpacked-libs so that they can be referenced in the build.
extractLibraryDependencies();
// Copy project assets to combinedAssets so that aapt has a single assets folder to load.
copyFolder( assetsDirectory, combinedAssets );
final String[] relativeAidlFileNames1 = findRelativeAidlFileNames( aidlSourceDirectory );
final String[] relativeAidlFileNames2 = findRelativeAidlFileNames( extractedDependenciesJavaSources );
final Map relativeApklibAidlFileNames = new HashMap();
if ( !isInstrumentationTest() )
{
// Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk.
for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) )
{
final File libSourceFolder = getUnpackedApkLibSourceFolder( artifact );
final String[] apklibAidlFiles = findRelativeAidlFileNames( libSourceFolder );
relativeApklibAidlFileNames.put( artifact.getId(), apklibAidlFiles );
}
}
mergeManifests();
checkPackagesForDuplicates();
checkForConflictingLayouts();
generateR();
generateBuildConfig();
// When compiling AIDL for this project,
// make sure we compile AIDL for dependencies as well.
// This is so project A, which depends on project B, can
// use AIDL info from project B in its own AIDL
final Map files = new HashMap();
files.put( aidlSourceDirectory, relativeAidlFileNames1 );
files.put( extractedDependenciesJavaSources, relativeAidlFileNames2 );
if ( !isInstrumentationTest() )
{
// Only add transitive APKLIB deps if we are building an APK and not an instrumentation test apk.
for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB ) )
{
final File unpackedLibSourceFolder = getUnpackedApkLibSourceFolder( artifact );
files.put( unpackedLibSourceFolder, relativeApklibAidlFileNames.get( artifact.getId() ) );
}
}
generateAidlFiles( files );
}
catch ( MojoExecutionException e )
{
getLog().error( "Error when generating sources.", e );
throw e;
}
}
/**
* Performs some level of validation on the configured files and folders in light of the change
* to AndroidStudio/Gradle style folder structures instead of the original Ant/Eclipse structures.
*
* Essentially we will be noisy if we see an artifact that looks like before
* Android Maven Plugin 4 and is not explicitly configured.
*/
private void validateStandardLocations() throws MojoExecutionException
{
boolean hasNonStandardStructure = false;
if ( androidManifestFilePre4.exists() && !androidManifestFilePre4.equals( androidManifestFile ) )
{
getLog().warn( "Non-standard location of AndroidManifest.xml file found, but not configured:\n "
+ androidManifestFilePre4 + "\nMove to the standard location src/main/AndroidManifest.xml\n"
+ "Or configure androidManifesteFile. \n" );
hasNonStandardStructure = true;
}
if ( resourceDirectoryPre4.exists() && !resourceDirectoryPre4.equals( resourceDirectory ) )
{
getLog().warn( "Non-standard location of Android res folder found, but not configured:\n "
+ resourceDirectoryPre4 + "\nMove to the standard location src/main/res/\n"
+ "Or configure resourceDirectory. \n" );
hasNonStandardStructure = true;
}
if ( assetsDirectoryPre4.exists() && !assetsDirectoryPre4.equals( assetsDirectory ) )
{
getLog().warn( "Non-standard location assets folder found, but not configured:\n "
+ assetsDirectoryPre4 + "\nMove to the standard location src/main/assets/\n"
+ "Or configure assetsDirectory. \n" );
hasNonStandardStructure = true;
}
if ( nativeLibrariesDirectoryPre4.exists() && !nativeLibrariesDirectoryPre4.equals( nativeLibrariesDirectory ) )
{
getLog().warn( "Non-standard location native libs folder found, but not configured:\n "
+ nativeLibrariesDirectoryPre4 + "\nMove to the standard location src/main/libs/\n"
+ "Or configure nativeLibrariesDirectory. \n" );
hasNonStandardStructure = true;
}
if ( hasNonStandardStructure && failOnNonStandardStructure )
{
throw new MojoExecutionException(
"\n\nFound files or folders in non-standard locations in the project!\n"
+ "....This might be a side-effect of a migration to Android Maven Plugin 4+.\n"
+ "....Please observe the warnings for specific files and folders above.\n"
+ "....Ideally you should restructure your project.\n"
+ "....Alternatively add explicit configuration overrides for files or folders.\n"
+ "....Finally you could set failOnNonStandardStructure to false, potentially "
+ "resulting in other failures.\n\n\n"
);
}
}
/**
* Copy the AndroidManifest.xml from sourceManifestFile to androidManifestFile
*
* @throws MojoExecutionException
*/
protected void copyManifest() throws MojoExecutionException
{
getLog().debug( "copyManifest: " + sourceManifestFile + " -> " + androidManifestFile );
if ( sourceManifestFile == null )
{
getLog().debug( "Manifest copying disabled. Using default manifest only" );
return;
}
try
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse( sourceManifestFile );
Source source = new DOMSource( doc );
TransformerFactory xfactory = TransformerFactory.newInstance();
Transformer xformer = xfactory.newTransformer();
xformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
FileWriter writer = null;
try
{
androidManifestFile.getParentFile().mkdirs();
writer = new FileWriter( androidManifestFile, false );
if ( doc.getXmlEncoding() != null && doc.getXmlVersion() != null )
{
String xmldecl = String.format( "%n",
doc.getXmlVersion(), doc.getXmlEncoding() );
writer.write( xmldecl );
}
Result result = new StreamResult( writer );
xformer.transform( source, result );
getLog().info( "Manifest copied from " + sourceManifestFile + " to " + androidManifestFile );
}
finally
{
IOUtils.closeQuietly( writer );
}
}
catch ( Exception e )
{
getLog().error( "Error during copyManifest" );
throw new MojoExecutionException( "Error during copyManifest", e );
}
}
/**
* Extract the source dependencies.
*
* @throws MojoExecutionException if it fails.
*/
protected void extractSourceDependencies() throws MojoExecutionException
{
for ( Artifact artifact : getDirectDependencyArtifacts() )
{
String type = artifact.getType();
if ( type.equals( APKSOURCES ) )
{
getLog().debug( "Detected apksources dependency " + artifact + " with file " + artifact.getFile()
+ ". Will resolve and extract..." );
final File apksourcesFile = resolveArtifactToFile( artifact );
getLog().debug( "Extracting " + apksourcesFile + "..." );
extractApksources( apksourcesFile );
}
}
if ( extractedDependenciesJavaResources.exists() )
{
projectHelper.addResource( project, extractedDependenciesJavaResources.getAbsolutePath(), null, null );
project.addCompileSourceRoot( extractedDependenciesJavaSources.getAbsolutePath() );
}
}
/**
* @deprecated Support APKSOURCES
artifacts has been deprecated. Use APKLIB instead.
*/
@Deprecated
private void extractApksources( File apksourcesFile ) throws MojoExecutionException
{
if ( apksourcesFile.isDirectory() )
{
getLog().warn( "The apksources artifact points to '" + apksourcesFile
+ "' which is a directory; skipping unpacking it." );
return;
}
final UnArchiver unArchiver = new ZipUnArchiver( apksourcesFile )
{
@Override
protected Logger getLogger()
{
return new ConsoleLogger( Logger.LEVEL_DEBUG, "dependencies-unarchiver" );
}
};
extractedDependenciesDirectory.mkdirs();
unArchiver.setDestDirectory( extractedDependenciesDirectory );
try
{
unArchiver.extract();
}
catch ( ArchiverException e )
{
throw new MojoExecutionException( "ArchiverException while extracting " + apksourcesFile.getAbsolutePath()
+ ". Message: " + e.getLocalizedMessage(), e );
}
}
private void extractLibraryDependencies() throws MojoExecutionException
{
final Collection artifacts = getTransitiveDependencyArtifacts(
EXCLUDED_DEPENDENCY_SCOPES_FOR_EXTRACTION );
getLog().info( "Extracting libs" );
// The only library dependencies we want included in an instrumentation test are APK and AARs.
// Any APKLIB classes have already been compiled into the APK.
final boolean instrumentationTest = isInstrumentationTest();
for ( Artifact artifact : artifacts )
{
final String type = artifact.getType();
if ( type.equals( APKLIB ) && !instrumentationTest )
{
getLog().info( "Extracting apklib " + artifact.getArtifactId() + "..." );
extractApklib( artifact );
}
else if ( type.equals( AAR ) )
{
getLog().info( "Extracting aar " + artifact.getArtifactId() + "..." );
extractAarLib( artifact );
}
else if ( type.equals( APK ) )
{
getLog().info( "Extracting apk " + artifact.getArtifactId() + "..." );
extractApkClassesJar( artifact );
}
else
{
getLog().debug( "Not extracting " + artifact.getArtifactId() + "..." );
}
}
}
/**
* Extracts ApkLib and adds the assets and apklib sources and resources to the build.
*/
private void extractApklib( Artifact apklibArtifact ) throws MojoExecutionException
{
getUnpackedLibHelper().extractApklib( apklibArtifact );
// Copy the assets to the the combinedAssets folder.
// Add the apklib source and resource to the compile.
// NB apklib sources are added to compileSourceRoot because we may need to compile against them.
// This means the apklib classes will be compiled into target/classes and packaged with this build.
copyFolder( getUnpackedLibAssetsFolder( apklibArtifact ), combinedAssets );
final File apklibSourceFolder = getUnpackedApkLibSourceFolder( apklibArtifact );
final List resourceExclusions = Arrays.asList( "**/*.java", "**/*.aidl" );
projectHelper.addResource( project, apklibSourceFolder.getAbsolutePath(), null, resourceExclusions );
project.addCompileSourceRoot( apklibSourceFolder.getAbsolutePath() );
}
/**
* Extracts AarLib and if this is an APK build then adds the assets and resources to the build.
*/
private void extractAarLib( Artifact aarArtifact ) throws MojoExecutionException
{
getUnpackedLibHelper().extractAarLib( aarArtifact );
// Copy the assets to the the combinedAssets folder, but only if an APK build.
// Ie we only want to package assets that we own.
// Assets should only live within their owners or the final APK.
if ( isAPKBuild() )
{
copyFolder( getUnpackedLibAssetsFolder( aarArtifact ), combinedAssets );
}
// Aar lib resources should only be included if we are building an apk.
// So we need to extract them into a folder that we then add to the resource classpath.
if ( isAPKBuild() )
{
// (If we are including SYSTEM_SCOPE artifacts when considering resources for APK packaging)
// then adding the AAR resources to the project would have them added twice.
getLog().debug( "Not adding AAR resources to resource classpath : " + aarArtifact );
}
}
/**
* Copies a dependent APK jar over the top of the placeholder created for it in AarMavenLifeCycleParticipant.
*
* This is necessary because we need the classes of the APK added to the compile classpath.
* NB APK dependencies are uncommon as they should really only be used in a project that tests an apk.
*
* @param artifact APK dependency for this project whose classes will be copied over.
*/
private void extractApkClassesJar( Artifact artifact ) throws MojoExecutionException
{
final File apkClassesJar = getUnpackedLibHelper().getJarFileForApk( artifact );
final File unpackedClassesJar = getUnpackedLibHelper().getUnpackedClassesJar( artifact );
try
{
FileUtils.copyFile( apkClassesJar, unpackedClassesJar );
}
catch ( IOException e )
{
throw new MojoExecutionException(
"Could not copy APK classes jar " + apkClassesJar + " to " + unpackedClassesJar, e );
}
getLog().debug( "Copied APK classes jar into compile classpath : " + unpackedClassesJar );
}
/**
* Traverses the list of project dependencies looking for "AAR depends on APKLIB" artifact combination
* that has been deprecated. For each occurrence of an AAR artifact with APKLIB direct or transitive dependency,
* produces a warning message to inform the user. Future plugin versions may default to skipping or not handling
* unsupported artifacts during build lifecycle.
*
* @throws MojoExecutionException
*/
private void checkForApklibDependencies() throws MojoExecutionException
{
final boolean isAarBuild = project.getPackaging().equals( AAR );
final DependencyResolver dependencyResolver = getDependencyResolver();
final Set allArtifacts = project.getArtifacts();
Set dependencyArtifacts = getArtifactResolverHelper().getFilteredArtifacts( allArtifacts );
boolean foundApklib = false;
for ( Artifact artifact : dependencyArtifacts )
{
final String type = artifact.getType();
if ( type.equals( APKLIB ) && isAarBuild )
{
getLog().warn( "Detected APKLIB transitive dependency: " + artifact.getId() );
foundApklib = true;
}
else if ( type.equals( AAR ) )
{
final Set dependencies = dependencyResolver
.getLibraryDependenciesFor( session, repositorySystem, artifact );
for ( Artifact dependency : dependencies )
{
if ( dependency.getType().equals( APKLIB ) )
{
getLog().warn( "Detected " + artifact.getId() + " that depends on APKLIB: "
+ dependency.getId() );
foundApklib = true;
}
}
}
}
if ( foundApklib )
{
getLog().warn( "AAR libraries should not depend or include APKLIB artifacts.\n"
+ "APKLIBs have been deprecated and the combination of the two may yield unexpected results.\n"
+ "Check the problematic AAR libraries for newer versions that use AAR packaging." );
}
}
/**
* Checks packages in AndroidManifest.xml file of project and all dependent libraries for duplicates.
* Generate warning if duplicates presents.
*
(in case of packages similarity R.java and BuildConfig files will be overridden)
*
* @throws MojoExecutionException
*/
private void checkPackagesForDuplicates() throws MojoExecutionException
{
Set dependencyArtifacts = getTransitiveDependencyArtifacts( AAR, APKLIB );
if ( dependencyArtifacts.isEmpty() )
{
//if no AAR or APKLIB dependencies presents than only one package presents
return;
}
Map> packageCompareMap = getPackageCompareMap( dependencyArtifacts );
List duplicatesMessageList = new ArrayList();
for ( Map.Entry> entry: packageCompareMap.entrySet() )
{
Set artifacts = entry.getValue();
if ( artifacts != null && artifacts.size() > 1 )
{
StringBuilder messageBuilder = new StringBuilder();
for ( Artifact item: artifacts )
{
messageBuilder
.append( messageBuilder.length() > 0 ? ", " : " [" )
.append( item.getArtifactId() );
}
messageBuilder
.append( "] have similar package='" )
.append( entry.getKey() )
.append( "'" );
duplicatesMessageList.add( messageBuilder.toString() );
}
}
if ( !duplicatesMessageList.isEmpty() )
{
List messageList = new ArrayList();
messageList.add( "" );
messageList.add( "Duplicate packages detected in AndroidManifest.xml files" );
messageList.add( "" );
messageList.add( "Such scenario generally means that the build will fail with a compilation error due to"
+ " missing resources in R file." );
messageList.add( "You should consider renaming some of the duplicate packages listed below"
+ " to avoid the conflict." );
messageList.add( "" );
messageList.add( "Conflicting artifacts:" );
messageList.addAll( duplicatesMessageList );
messageList.add( "" );
if ( failOnDuplicatePackages )
{
StringBuilder builder = new StringBuilder();
for ( String line : messageList )
{
builder.append( line );
builder.append( "\n" );
}
builder.append( "\n" );
builder.append( "You can downgrade the failure to a warning " );
builder.append( "by setting the 'failOnDuplicatePackages' plugin property to false." );
throw new MojoExecutionException( builder.toString() );
}
for ( String messageLine: messageList )
{
getLog().warn( messageLine );
}
}
}
/**
* Looks for dependencies that have a layout file with the same name as either one of the project layout files
* or one of the other dependencies. If such a duplicate occurs then it will fail the build or generate a warning
* depending upon the value of the failOnConflictingLayouts build parameter.
*
* @throws MojoExecutionException
*/
private void checkForConflictingLayouts() throws MojoExecutionException
{
final ConflictingLayoutDetector detector = new ConflictingLayoutDetector();
// Add layout files for this project
final FileRetriever retriever = new FileRetriever( "layout*/*.xml" );
detector.addLayoutFiles( getAndroidManifestPackageName(), retriever.getFileNames( resourceDirectory ) );
// Add layout files for all dependencies.
for ( final Artifact dependency : getTransitiveDependencyArtifacts( AAR, APKLIB ) )
{
final String packageName = extractPackageNameFromAndroidArtifact( dependency );
final String[] layoutFiles = retriever.getFileNames( getUnpackedLibResourceFolder( dependency ) );
detector.addLayoutFiles( packageName, layoutFiles );
}
final Collection conflictingLayouts = detector.getConflictingLayouts();
getLog().debug( "checkConflictingLayouts - conflicts : " + conflictingLayouts );
if ( !conflictingLayouts.isEmpty() )
{
final List sb = new ArrayList();
sb.add( "" );
sb.add( "" );
sb.add( "Duplicate layout files have been detected across more than one Android package." );
sb.add( "" );
sb.add( "Such a scenario generally means that the build will fail with a compilation error due to" );
sb.add( "missing resource files. You should consider renaming some of the layout files listed below" );
sb.add( "to avoid the conflict." );
sb.add( "" );
sb.add( "However, if you believe you know better, then you can downgrade the failure to a warning" );
sb.add( "by setting the failOnConflictingLayouts plugin property to false." );
sb.add( "But you really don't want to do that." );
sb.add( "" );
sb.add( "Conflicting Layouts:" );
for ( final ConflictingLayout layout : conflictingLayouts )
{
sb.add( " " + layout.getLayoutFileName() + " packages=" + layout.getPackageNames().toString() );
}
sb.add( "" );
if ( failOnConflictingLayouts )
{
final StringBuilder builder = new StringBuilder();
for ( final String line : sb )
{
builder.append( line );
builder.append( "\n" );
}
throw new MojoExecutionException( builder.toString() );
}
for ( final String line : sb )
{
getLog().warn( line );
}
}
}
/**
* Provides map with all provided dependencies or project itself grouped by package name
*
* @param dependencyArtifacts artifacts that should be grouped by package name
* @return map of with package names(String) and sets of artifacts (Set)
* that have similar package names
* @throws MojoExecutionException
*/
Map> getPackageCompareMap( Set dependencyArtifacts ) throws MojoExecutionException
{
if ( dependencyArtifacts == null )
{
throw new IllegalArgumentException( "dependencies must be initialized" );
}
Map> packageCompareMap = new HashMap>();
Set artifactSet = new HashSet();
artifactSet.add( project.getArtifact() );
packageCompareMap.put( getAndroidManifestPackageName(), artifactSet );
for ( Artifact artifact: dependencyArtifacts )
{
String libPackage = extractPackageNameFromAndroidArtifact( artifact );
Set artifacts = packageCompareMap.get( libPackage );
if ( artifacts == null )
{
artifacts = new HashSet();
packageCompareMap.put( libPackage, artifacts );
}
artifacts.add( artifact );
}
return packageCompareMap;
}
private void generateR() throws MojoExecutionException
{
getLog().info( "Generating R file for " + project.getArtifact() );
genDirectory.mkdirs();
final AaptPackageCommandBuilder commandBuilder = AaptCommandBuilder
.packageResources( getLog() )
.makePackageDirectories()
.setResourceConstantsFolder( genDirectory )
.forceOverwriteExistingFiles()
.disablePngCrunching()
.generateRIntoPackage( customPackage )
.setPathToAndroidManifest( androidManifestFile )
.addResourceDirectoryIfExists( resourceDirectory )
.addResourceDirectoriesIfExists( getResourceOverlayDirectories() )
// Need to include any AAR or APKLIB dependencies when generating R because if any local
// resources directly reference dependent resources then R generation will crash.
.addResourceDirectoriesIfExists( getLibraryResourceFolders() )
.autoAddOverlay()
.addRawAssetsDirectoryIfExists( combinedAssets )
.addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
.addConfigurations( configurations )
.addExtraArguments( aaptExtraArgs )
.setVerbose( aaptVerbose )
// We need to generate R.txt for all projects as it needs to be consumed when generating R class.
// It also needs to be consumed when packaging aar.
.generateRTextFile( targetDirectory )
// If a proguard file is defined then output Proguard options to it.
.setProguardOptionsOutputFile( proguardFile )
.makeResourcesNonConstant( AAR.equals( project.getArtifact().getType() ) );
getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
try
{
final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
executor.setLogger( getLog() );
executor.setCaptureStdOut( true );
final List commands = commandBuilder.build();
executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
}
catch ( ExecutionException e )
{
throw new MojoExecutionException( "", e );
}
final ResourceClassGenerator resGenerator = new ResourceClassGenerator( this, targetDirectory, genDirectory );
generateCorrectRJavaForApklibDependencies( resGenerator );
generateCorrectRJavaForAarDependencies( resGenerator );
getLog().info( "Adding R gen folder to compile classpath: " + genDirectory );
project.addCompileSourceRoot( genDirectory.getAbsolutePath() );
}
/**
* Generate correct R.java for apklibs dependencies of a current project
*
* @throws MojoExecutionException
*/
private void generateCorrectRJavaForApklibDependencies( ResourceClassGenerator resourceGenerator )
throws MojoExecutionException
{
getLog().debug( "" );
getLog().debug( "#generateCorrectRJavaFoApklibDeps" );
// Generate R.java for apklibs
// Compatibility with Apklib which isn't present in AndroidBuilder
getLog().debug( "Generating Rs for apklib deps of project " + project.getArtifact() );
final Set apklibDependencies = getTransitiveDependencyArtifacts( APKLIB );
for ( final Artifact artifact : apklibDependencies )
{
getLog().debug( "Generating apklib R.java for " + artifact.getArtifactId() + "..." );
generateRForApkLibDependency( artifact );
}
// Generate corrected R.java for APKLIB dependencies, but only if this is an APK build.
if ( !apklibDependencies.isEmpty() && APK.equals( project.getArtifact().getType() ) )
{
// Generate R.java for each APKLIB based on R.txt
getLog().debug( "" );
getLog().debug( "Rewriting R files for APKLIB dependencies : " + apklibDependencies );
resourceGenerator.generateLibraryRs( apklibDependencies, "apklib" );
}
}
/**
* Generate correct R.java for aar dependencies of a current project
*
* @throws MojoExecutionException
*/
private void generateCorrectRJavaForAarDependencies( ResourceClassGenerator resourceGenerator )
throws MojoExecutionException
{
// Generate corrected R.java for AAR dependencies.
final Set aarLibraries = getTransitiveDependencyArtifacts( AAR );
if ( !aarLibraries.isEmpty() )
{
// Generate R.java for each AAR based on R.txt
getLog().debug( "Generating R file for AAR dependencies" );
resourceGenerator.generateLibraryRs( aarLibraries, "aar" );
}
}
private List getLibraryResourceFolders()
{
final List resourceFolders = new ArrayList();
for ( Artifact artifact : getTransitiveDependencyArtifacts( AAR, APKLIB ) )
{
getLog().debug( "Considering dep artifact : " + artifact );
final File resourceFolder = getUnpackedLibResourceFolder( artifact );
if ( resourceFolder.exists() )
{
getLog().debug( "Adding apklib or aar resource folder : " + resourceFolder );
resourceFolders.add( resourceFolder );
}
}
return resourceFolders;
}
/**
* Executes aapt to generate the R class for the given apklib.
*
* @param apklibArtifact apklib for which to generate the R class.
* @throws MojoExecutionException if it fails.
*/
private void generateRForApkLibDependency( Artifact apklibArtifact ) throws MojoExecutionException
{
final File unpackDir = getUnpackedLibFolder( apklibArtifact );
getLog().debug( "Generating incomplete R file for apklib: " + apklibArtifact.getGroupId()
+ ":" + apklibArtifact.getArtifactId() );
final File apklibManifest = new File( unpackDir, "AndroidManifest.xml" );
final File apklibResDir = new File( unpackDir, "res" );
List dependenciesResDirectories = new ArrayList();
final Set apklibDeps = getDependencyResolver()
.getLibraryDependenciesFor( this.session, this.repositorySystem, apklibArtifact );
getLog().debug( "apklib=" + apklibArtifact + " dependencies=" + apklibDeps );
for ( Artifact dependency : apklibDeps )
{
// Add in the resources that are dependencies of the apklib.
final String extension = dependency.getType();
final File dependencyResDir = getUnpackedLibResourceFolder( dependency );
if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) && dependencyResDir.exists() )
{
dependenciesResDirectories.add( dependencyResDir );
}
}
// Create combinedAssets for this apklib dependency - can't have multiple -A args
final File apklibCombAssets = new File( getUnpackedLibFolder( apklibArtifact ), "combined-assets" );
for ( Artifact dependency : apklibDeps )
{
// Accumulate assets for dependencies of the apklib (if they exist).
final String extension = dependency.getType();
final File dependencyAssetsDir = getUnpackedLibAssetsFolder( dependency );
if ( ( extension.equals( APKLIB ) || extension.equals( AAR ) ) )
{
copyFolder( dependencyAssetsDir, apklibCombAssets );
}
}
// Overlay the apklib dependency assets (if they exist)
final File apkLibAssetsDir = getUnpackedLibAssetsFolder( apklibArtifact );
copyFolder( apkLibAssetsDir, apklibCombAssets );
final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
executor.setLogger( getLog() );
final AaptCommandBuilder commandBuilder = AaptCommandBuilder
.packageResources( getLog() )
.makeResourcesNonConstant()
.makePackageDirectories()
.setResourceConstantsFolder( genDirectory )
.generateRIntoPackage( extractPackageNameFromAndroidManifest( apklibManifest ) )
.setPathToAndroidManifest( apklibManifest )
.addResourceDirectoryIfExists( apklibResDir )
.addResourceDirectoriesIfExists( dependenciesResDirectories )
.autoAddOverlay()
.addRawAssetsDirectoryIfExists( apklibCombAssets )
.addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
.addConfigurations( configurations )
.addExtraArguments( aaptExtraArgs )
.setVerbose( aaptVerbose )
// We need to generate R.txt for all projects as it needs to be consumed when generating R class.
// It also needs to be consumed when packaging aar.
.generateRTextFile( unpackDir );
getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
try
{
executor.setCaptureStdOut( true );
final List commands = commandBuilder.build();
executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
}
catch ( ExecutionException e )
{
throw new MojoExecutionException( "", e );
}
}
private void mergeManifests() throws MojoExecutionException
{
getLog().debug( "mergeManifests: " + mergeManifests );
if ( !mergeManifests )
{
getLog().debug( "Manifest merging disabled. Using project manifest only" );
return;
}
getLog().info( "Getting manifests of dependent apklibs" );
List libManifests = new ArrayList();
for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
{
final File libManifest = new File( getUnpackedLibFolder( artifact ), "AndroidManifest.xml" );
if ( !libManifest.exists() )
{
throw new MojoExecutionException( artifact.getArtifactId() + " is missing AndroidManifest.xml" );
}
libManifests.add( libManifest );
}
if ( !libManifests.isEmpty() )
{
final File mergedManifest = new File( androidManifestFile.getParent(), "AndroidManifest-merged.xml" );
final StdLogger stdLogger = new StdLogger( StdLogger.Level.VERBOSE );
final ManifestMerger merger = new ManifestMerger( MergerLog.wrapSdkLog( stdLogger ), null );
getLog().info( "Merging manifests of dependent apklibs" );
final boolean mergeSuccess = merger.process( mergedManifest, androidManifestFile,
libManifests.toArray( new File[libManifests.size()] ), null, null );
if ( mergeSuccess )
{
// Replace the original manifest file with the merged one so that
// the rest of the build will pick it up.
androidManifestFile.delete();
mergedManifest.renameTo( androidManifestFile );
getLog().info( "Done Merging Manifests of APKLIBs" );
}
else
{
getLog().error( "Manifests were not merged!" );
throw new MojoExecutionException( "Manifests were not merged!" );
}
}
else
{
getLog().info( "No APKLIB manifests found. Using project manifest only." );
}
}
private void generateBuildConfig() throws MojoExecutionException
{
getLog().debug( "Generating BuildConfig file" );
// Create the BuildConfig for our package.
String packageName = extractPackageNameFromAndroidManifest( androidManifestFile );
if ( StringUtils.isNotBlank( customPackage ) )
{
packageName = customPackage;
}
generateBuildConfigForPackage( packageName );
// Skip BuildConfig generation for dependencies if this is an AAR project
if ( project.getPackaging().equals( AAR ) )
{
return;
}
// Generate the BuildConfig for any APKLIB and some AAR dependencies.
// Need to generate for AAR, because some old AARs like ActionBarSherlock do not have BuildConfig (or R)
for ( Artifact artifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
{
if ( skipBuildConfigGeneration( artifact ) )
{
getLog().info( "Skip BuildConfig.java generation for "
+ artifact.getGroupId() + " " + artifact.getArtifactId() );
continue;
}
final String depPackageName = extractPackageNameFromAndroidArtifact( artifact );
generateBuildConfigForPackage( depPackageName );
}
}
private boolean skipBuildConfigGeneration( Artifact artifact ) throws MojoExecutionException
{
if ( artifact.getType().equals( AAR ) )
{
String depPackageName = extractPackageNameFromAndroidArtifact( artifact );
if ( isBuildConfigPresent( artifact, depPackageName ) )
{
return true;
}
Set< Artifact > transitiveDep = getArtifactResolverHelper()
.getFilteredArtifacts( project.getArtifacts(), AAR );
for ( Artifact transitiveArtifact : transitiveDep )
{
if ( isBuildConfigPresent( transitiveArtifact, depPackageName ) )
{
return true;
}
}
}
return false;
}
/**
* Check if given artifact includes a matching BuildConfig class
*
* @throws MojoExecutionException
*/
private boolean isBuildConfigPresent( Artifact artifact ) throws MojoExecutionException
{
String depPackageName = extractPackageNameFromAndroidArtifact( artifact );
return isBuildConfigPresent( artifact, depPackageName );
}
/**
* Check whether the artifact includes a BuildConfig located in a given package.
*
* @param artifact an AAR artifact to look for BuildConfig in
* @param packageName BuildConfig package name
* @throws MojoExecutionException
*/
private boolean isBuildConfigPresent( Artifact artifact, String packageName ) throws MojoExecutionException
{
try
{
JarFile jar = new JarFile( getUnpackedAarClassesJar( artifact ) );
JarEntry entry = jar.getJarEntry( packageName.replace( '.', '/' ) + "/BuildConfig.class" );
return ( entry != null );
}
catch ( IOException e )
{
getLog().error( "Error generating BuildConfig ", e );
throw new MojoExecutionException( "Error generating BuildConfig", e );
}
}
private void generateBuildConfigForPackage( String packageName ) throws MojoExecutionException
{
getLog().debug( "Creating BuildConfig for " + packageName );
File outputFolder = new File( genDirectory, packageName.replace( ".", File.separator ) );
outputFolder.mkdirs();
StringBuilder buildConfig = new StringBuilder();
buildConfig.append( "package " ).append( packageName ).append( ";\n\n" );
buildConfig.append( "public final class BuildConfig {\n" );
buildConfig.append( " public static final boolean DEBUG = " ).append( !release ).append( ";\n" );
for ( BuildConfigConstant constant : buildConfigConstants )
{
String value = constant.getValue();
if ( "String".equals( constant.getType() ) )
{
value = "\"" + value + "\"";
}
buildConfig.append( " public static final " )
.append( constant.getType() )
.append( " " )
.append( constant.getName() )
.append( " = " )
.append( value )
.append( ";\n" );
}
buildConfig.append( "}\n" );
File outputFile = new File( outputFolder, "BuildConfig.java" );
try
{
FileUtils.writeStringToFile( outputFile, buildConfig.toString() );
}
catch ( IOException e )
{
getLog().error( "Error generating BuildConfig ", e );
throw new MojoExecutionException( "Error generating BuildConfig", e );
}
}
/**
* Given a map of source directories to list of AIDL (relative) filenames within each,
* runs the AIDL compiler for each, such that all source directories are available to
* the AIDL compiler.
*
* @param files Map of source directory File instances to the relative paths to all AIDL files within
* @throws MojoExecutionException If the AIDL compiler fails
*/
private void generateAidlFiles( Map files )
throws MojoExecutionException
{
List protoCommands = new ArrayList();
protoCommands.add( "-p" + getAndroidSdk().getPathForFrameworkAidl() );
genDirectoryAidl.mkdirs();
getLog().info( "Adding AIDL gen folder to compile classpath: " + genDirectoryAidl );
project.addCompileSourceRoot( genDirectoryAidl.getPath() );
Set sourceDirs = files.keySet();
for ( File sourceDir : sourceDirs )
{
protoCommands.add( "-I" + sourceDir );
}
for ( File sourceDir : sourceDirs )
{
for ( String relativeAidlFileName : files.get( sourceDir ) )
{
File targetDirectory = new File( genDirectoryAidl, new File( relativeAidlFileName ).getParent() );
targetDirectory.mkdirs();
final String shortAidlFileName = new File( relativeAidlFileName ).getName();
final String shortJavaFileName = shortAidlFileName.substring( 0, shortAidlFileName.lastIndexOf( "." ) )
+ ".java";
final File aidlFileInSourceDirectory = new File( sourceDir, relativeAidlFileName );
List commands = new ArrayList( protoCommands );
commands.add( aidlFileInSourceDirectory.getAbsolutePath() );
commands.add( new File( targetDirectory, shortJavaFileName ).getAbsolutePath() );
try
{
CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
executor.setLogger( this.getLog() );
executor.setCaptureStdOut( true );
executor.executeCommand( getAndroidSdk().getAidlPath(), commands, project.getBasedir(),
false );
}
catch ( ExecutionException e )
{
throw new MojoExecutionException( "", e );
}
}
}
}
private String[] findRelativeAidlFileNames( File sourceDirectory )
{
final FileRetriever retriever = new FileRetriever( "**/*.aidl" );
final String[] relativeAidlFileNames = retriever.getFileNames( sourceDirectory );
if ( relativeAidlFileNames.length == 0 )
{
getLog().debug( "ANDROID-904-002: No aidl files found" );
}
else
{
getLog().info( "ANDROID-904-002: Found aidl files: Count = " + relativeAidlFileNames.length );
}
return relativeAidlFileNames;
}
/**
* @return true if the pom type is APK, APKLIB, or APKSOURCES
*/
private boolean isCurrentProjectAndroid()
{
Set androidArtifacts = new HashSet()
{
{
addAll( Arrays.asList( APK, APKLIB, APKSOURCES, AAR ) );
}
};
return androidArtifacts.contains( project.getArtifact().getType() );
}
}