选择 Android 库的构建变体的 Gradle 脚本
Posted
技术标签:
【中文标题】选择 Android 库的构建变体的 Gradle 脚本【英文标题】:Gradle script which selects a build variant of the Android library 【发布时间】:2019-07-18 23:21:25 【问题描述】:我正在使用 Eclipse 并处理 android 项目。由于多种原因,我无法使用 Android Studio。不幸的是,Eclipse 不能处理 AAR 档案。经过长期的努力,我们决定让 Eclipse 项目成为 NON-GRADLE(从所有 Eclipse 项目中消除 gradle 特性),并使用特殊的 gradle 脚本准备一个依赖项列表。 为了能够使用 Android,我编写了以下 gradle 脚本,它执行以下操作:
-
搜索所有项目中的所有依赖项
将依赖项从 gradle 缓存复制到一个特殊的 Eclipse 项目“jars-from-gradle”
为所有项目编写“.classpath”以仅使用找到的库
这里是 gradle 脚本:
apply plugin: "eclipse"
configurations
eclipseOnly
description = 'this is used only to build eclipse classpath'
afterEvaluate
project.tasks['eclipseProject'].dependsOn(project.tasks['cleanEclipseProject'])
project.tasks['eclipseClasspath'].dependsOn(project.tasks['cleanEclipseClasspath'])
eclipse
File f = rootProject.ext.find("explodedAarsDir");
if(f == null)
f = new File("$rootProject.projectDir/jars-from-gradle/explodedAars/");
rootProject.ext.set("explodedAarsDir", f)
f.mkdirs();
f = rootProject.ext.find("globalDependenciesRepo");
if(f == null)
f = new File("$rootProject.projectDir/jars-from-gradle/libs");
rootProject.ext.set("globalDependenciesRepo", f)
f.mkdirs();
org.gradle.plugins.ide.eclipse.model.internal.FileReferenceFactory frf = rootProject.ext.find("fileReferenceFactory");
if(frf == null)
frf = new org.gradle.plugins.ide.eclipse.model.internal.FileReferenceFactory();
rootProject.ext.set("fileReferenceFactory", frf)
if(!rootProject.ext.has("eclipseFileMapping"))
rootProject.ext.set("eclipseFileMapping", new HashMap<File, File>())
Map<File, File> eclipseFileMapping = rootProject.ext.get("eclipseFileMapping")
eclipseFileMapping.put(new File("bin/main"), new File("bin"))
eclipseFileMapping.put(new File("bin/test"), new File("bin"))
eclipseFileMapping.put(new File("$buildDir/classes/java/main"), new File("$projectDir.parentFile/$project.name/bin"))
eclipseFileMapping.put(new File("$buildDir/classes/java/test"), new File("$projectDir.parentFile/$project.name/bin"))
eclipseFileMapping.put(new File("$buildDir/resources/main"), new File("$projectDir.parentFile/$project.name/bin"))
eclipseFileMapping.put(new File("$buildDir/resources/test"), new File("$projectDir.parentFile/$project.name/bin"))
configurations
eclipsePlusConfig
description = "files to include into eclipse classpath"
transitive = false
eclipseMinusConfig
description = "files to exclude from eclipse classpath"
transitive = false
project
setupEclipseProject()
classpath
defaultOutputDir = new File("$projectDir/bin")
plusConfigurations += [project.configurations.eclipseOnly]
if(project.extensions.findByName("android") != null)
plusConfigurations += [project.configurations.compile]
plusConfigurations += [project.configurations.runtime]
project.eclipse.project
natures 'org.eclipse.andmore.AndroidNature'
buildCommands.clear()
buildCommand "org.eclipse.andmore.ResourceManagerBuilder"
buildCommand "org.eclipse.andmore.PreCompilerBuilder"
buildCommand "org.eclipse.jdt.core.javabuilder"
buildCommand "org.eclipse.andmore.ApkBuilder"
containers 'org.eclipse.andmore.DEPENDENCIES', 'org.eclipse.andmore.LIBRARIES', 'org.eclipse.andmore.ANDROID_FRAMEWORK'
else
plusConfigurations += [project.configurations.compile]
plusConfigurations += [project.configurations.runtime]
file
beforeMerged classpath ->
eclipse.classpath.sourceSets.each
println " source set "+ it.getName()
eclipse.classpath.plusConfigurations.each processConf(it, " ", "plus conf: ")
eclipse.classpath.minusConfigurations.each processConf(it, " ", "minus conf: ")
eclipse.classpath.plusConfigurations.add(project.configurations['eclipsePlusConfig'])
eclipse.classpath.minusConfigurations.add(project.configurations['eclipseMinusConfig'])
whenMerged classpath ->
List<org.gradle.plugins.ide.eclipse.model.ClasspathEntry> replacementEclipseClasspath = createEclipseReplacementClasspath(classpath);
classpath.setEntries(replacementEclipseClasspath)
withXml n ->
Set<File> existingPaths = new HashSet<File>();
def rootNode = n.asNode();
for(int nodeIndex = 0; nodeIndex<rootNode.children().size(); nodeIndex++)
def chld = rootNode.children().get(nodeIndex);
if("classpathentry".equals(chld.name()))
removeGradleAttributes(chld);
chld.attributes().remove("output");
String kind = chld.attributes().get("kind");
for(Map.Entry entry : chld.attributes().entrySet())
if("path".equals(entry.key) || "sourcepath".equals(entry.key))
f = new File(entry.value);
if(f.toPath().isAbsolute())
String relativeName = rootProject.projectDir.toPath().relativize(f.toPath()).toString();
entry.value = "/"+ relativeName.replace('\\', '/');
if("path".equals(entry.key) && existingPaths.contains(f))
rootNode.children().remove(nodeIndex--);
break;
if(entry.value.startsWith("/"))
if("src".equals(kind))
chld.attributes().put("combineacces-s-rules", "false");
existingPaths.add(f);
if("lib".equals(kind))
chld.attributes().put("exported", "true");
task prepareEclipse
doFirst
mkDirIfNotExists(new File("$projectDir/src/main/java"))
mkDirIfNotExists(new File("$projectDir/src/main/resources"))
mkDirIfNotExists(new File("$projectDir/src/test/java"))
mkDirIfNotExists(new File("$projectDir/src/test/resources"))
tasks['eclipseClasspath'].dependsOn(prepareEclipse)
List<org.gradle.plugins.ide.eclipse.model.ClasspathEntry> createEclipseReplacementClasspath(org.gradle.plugins.ide.eclipse.model.Classpath eclipseClasspath)
Map<String, org.gradle.plugins.ide.eclipse.model.ClasspathEntry> replacementEclipseClasspathAsMap = new HashMap<String, org.gradle.plugins.ide.eclipse.model.ClasspathEntry>();
eclipseClasspath.entries.each clspthentry ->
dumpClassPathEntry(clspthentry)
if (clspthentry instanceof org.gradle.plugins.ide.eclipse.model.Library)
org.gradle.plugins.ide.eclipse.model.Library library = clspthentry;
String moduleId = library.getModuleVersion().toString();
String groupId = null;
String artifactId = null;
String artifactVersion = null;
int index = moduleId.indexOf(":");
if(index >= 0)
groupId = moduleId.substring(0, index);
String tmp = moduleId.substring(++index);
index = tmp.indexOf(":")
if(index >= 0)
artifactId = tmp.substring(0, index);
artifactVersion = tmp.substring(++index);
moduleId = moduleId.replaceAll(":", "-");
println(" classpath entry found: moduleId="+ moduleId);
if (library.getPath().endsWith(".aar"))
explodeAarJarFiles(moduleId, groupId, artifactId, artifactVersion, library, replacementEclipseClasspathAsMap);
else
copyLibraryFromGradleCache(moduleId, groupId, artifactId, artifactVersion, library, replacementEclipseClasspathAsMap)
else
replacementEclipseClasspathAsMap.put(clspthentry.kind+ "_"+ clspthentry.path, clspthentry);
List<org.gradle.plugins.ide.eclipse.model.ClasspathEntry> replacementEclipseClasspath = new ArrayList<org.gradle.plugins.ide.eclipse.model.ClasspathEntry>();
replacementEclipseClasspath.addAll(replacementEclipseClasspathAsMap.values());
List<String> KINDS = new ArrayList<String>();
KINDS.add('src');
KINDS.add('con');
KINDS.add('lib');
KINDS.add('output');
Collections.sort(replacementEclipseClasspath, new Comparator<org.gradle.plugins.ide.eclipse.model.ClasspathEntry>()
private int detectKindIndex(String entryKind)
for(int i = 0; i<KINDS.size(); i++)
if(KINDS[i].equals(entryKind))
return i;
return KINDS.size();
public int compare(org.gradle.plugins.ide.eclipse.model.ClasspathEntry entry1, org.gradle.plugins.ide.eclipse.model.ClasspathEntry entry2)
int kindDiff = detectKindIndex(entry1.getKind()) - detectKindIndex(entry2.getKind());
if(kindDiff != 0)
return kindDiff;
if(entry1 instanceof org.gradle.plugins.ide.eclipse.model.ProjectDependency)
if(!(entry2 instanceof org.gradle.plugins.ide.eclipse.model.ProjectDependency))
return 11;
else if(entry2 instanceof org.gradle.plugins.ide.eclipse.model.ProjectDependency)
return -1;
return entry1.path.compareTo(entry2.path);
);
return replacementEclipseClasspath;
void copyLibraryFromGradleCache(String moduleId, String groupId, String artifactId, String artifactVersion, org.gradle.plugins.ide.eclipse.model.Library library, Map<String, org.gradle.plugins.ide.eclipse.model.Library> replacementEclipseClasspathAsMap)
String artifactIdAndVersion = artifactId + "-"+ artifactVersion;
int fileSuffixIndex = -1;
if(artifactId != null)
fileSuffixIndex = library.getPath().lastIndexOf(artifactIdAndVersion);
if(fileSuffixIndex >= 0)
fileSuffixIndex += artifactIdAndVersion.length();
else
fileSuffixIndex = library.getPath().lastIndexOf(".");
if(moduleId == null || fileSuffixIndex <= 0)
println(" non-movable library found: "+ library.getPath())
replacementEclipseClasspathAsMap.put(moduleId, library);
else
File targetGroupFolder = null;
if (groupId==null || groupId.trim().length()==0)
targetGroupFolder = new File(globalDependenciesRepo.getAbsolutePath());
else
targetGroupFolder = new File(globalDependenciesRepo.getAbsolutePath(), groupId);
if(!targetGroupFolder.exists())
targetGroupFolder.mkdirs()
String fileSuffix = library.getPath().substring(fileSuffixIndex);
String targetFileName = artifactIdAndVersion;
println(" target filename: "+ targetGroupFolder+ " -> "+ targetFileName)
java.nio.file.Path targetFile = java.nio.file.Paths.get(targetGroupFolder.getAbsolutePath(), targetFileName + fileSuffix);
java.nio.file.Path sourceFile = java.nio.file.Paths.get(library.getPath());
if(sourceFile.toFile().exists() && !sourceFile.toFile().isDirectory())
java.nio.file.Files.copy(sourceFile, targetFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES);
library.setPath(targetFile.toString());
if(library.getSourcePath() != null)
java.nio.file.Path sourceSourceFile = java.nio.file.Paths.get(library.getSourcePath().getPath());
if(sourceFile.toFile().exists() && !sourceFile.toFile().isDirectory())
java.nio.file.Path sourceTargetFile = java.nio.file.Paths.get(targetGroupFolder.getAbsolutePath(), targetFileName + "_source"+ fileSuffix);
println(" copying source file: "+ sourceSourceFile + " into "+ sourceTargetFile);
java.nio.file.Files.copy(sourceSourceFile, sourceTargetFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES);
//println( " TROLOLO "+ rootProject.projectDir.toPath().relativize(sourceTargetFile) );
library.setSourcePath(fileReferenceFactory.fromFile(sourceTargetFile.toFile()));
replacementEclipseClasspathAsMap.put(moduleId + "_"+ targetFileName + fileSuffix, library);
void explodeAarJarFiles(String moduleId, String groupId, String artifactId, String artifactVersion, org.gradle.plugins.ide.eclipse.model.Library aarLibrary, Map<String, org.gradle.plugins.ide.eclipse.model.Library> replacementEclipseClasspathAsMap)
File aarFile = new File(aarLibrary.getPath());
println(" exploding AAR dependency: "+ aarFile.getAbsolutePath());
File targetFolder = new File(explodedAarsDir, moduleId);
println(" target folder: "+ targetFolder.getAbsolutePath());
if (targetFolder.exists())
println(" target folder exists. deleting ");
project.delete(files(targetFolder))
if (!targetFolder.mkdirs())
throw new RuntimeException("Cannot create folder: $targetFolder.getAbsolutePath()");
try
if(aarLibrary.getSourcePath() != null)
java.nio.file.Path sourceSourceFile = java.nio.file.Paths.get(aarLibrary.getSourcePath().getPath());
if(sourceSourceFile.toFile().exists() && !sourceSourceFile.toFile().isDirectory())
String sourceFileExt = sourceSourceFile.toString();
int extensionIndex = sourceFileExt.lastIndexOf(".");
if(extensionIndex >= 0)
sourceFileExt = sourceFileExt.substring(extensionIndex);
else
sourceFileExt = ".jar";
java.nio.file.Path sourceTargetFile = java.nio.file.Paths.get(targetFolder.toString(), moduleId+ "_source"+ sourceFileExt);
println(" copying source file: "+ sourceSourceFile + " into "+ sourceTargetFile);
java.nio.file.Files.copy(sourceSourceFile, sourceTargetFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES);
aarLibrary.setSourcePath(fileReferenceFactory.fromFile(sourceTargetFile.toFile()));
java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(aarFile);
zipFile.entries().each fileInsideAar ->
if (fileInsideAar.getName().endsWith(".jar"))
String targetName = moduleId+ "_"+ fileInsideAar.getName().replace('/', '_').replace('\\', '_');
println(" jar inside aar: "+ fileInsideAar.getName());
println(" copying to: "+ targetName);
File targetFile = new File(targetFolder, targetName);
int index = 1;
while (targetFile.exists())
targetFile = new File(targetFolder, format("$targetName_$++index"));
try
InputStream inputStream = zipFile.getInputStream(fileInsideAar);
java.nio.file.Files.copy(inputStream, targetFile.toPath());
org.gradle.plugins.ide.eclipse.model.Library library = new org.gradle.plugins.ide.eclipse.model.Library(fileReferenceFactory.fromFile(targetFile));
library.setSourcePath(aarLibrary.getSourcePath())
replacementEclipseClasspathAsMap.put(targetFile.getName(), library);
catch (IOException e)
throw new RuntimeException(
"Cannot write entry to file: $e.getMessage(): $targetFile.getAbsolutePath()", e);
;
catch (IOException e)
throw new RuntimeException(
"Cannot explode aar: $e.getMessage(): $aarFile.getAbsolutePath()", e);
void removeGradleAttributes(Node node)
for(int i = 0; i<node.children().size(); i++)
Node attrs = node.children().get(i);
if("attributes".equals(attrs.name()))
for(int j = 0; j<attrs.children().size(); j++)
Node attr = attrs.children().get(j);
boolean isGradleAttr = false;
for(Map.Entry entry : attr.attributes().entrySet())
if(entry.key.toLowerCase().contains("gradle") || entry.value.toLowerCase().contains("gradle"))
isGradleAttr = true;
if(isGradleAttr)
attrs.remove(attr);
j--;
if(attrs.children().size()==0)
node.remove(attrs);
void mkDirIfNotExists(File file)
if(!file.exists())
file.mkdir()
void processConf(org.gradle.api.internal.artifacts.configurations.DefaultConfiguration cnf, String startIndent, String prefix)
println(startIndent + prefix + cnf.name+ ", path: "+ cnf.path)
StringBuilder indent = new StringBuilder();
for(int i = 0; i<startIndent.length(); i++)
indent.append(" ");
indent.append(" ");
cnf.dependencies.each dep ->
maskDependencyIfNeeded(indent.toString(), dep)
cnf.allDependencies.each dep ->
maskDependencyIfNeeded(indent.toString(), dep)
void maskDependencyIfNeeded(String indent, org.gradle.api.internal.artifacts.dependencies.AbstractDependency dep)
if(dep instanceof org.gradle.api.internal.artifacts.dependencies.DefaultSelfResolvingDependency)
boolean needToExcludeDep = false;
Set<File> maskedDepFiles = new HashSet<File>();
dep.files.each depFile ->
File f = findMaskedFile(depFile, null, true);
if(f != null)
maskedDepFiles.add(f)
needToExcludeDep = true;
println(indent.toString()+ " mask dep file "+ depFile+ " -> "+ f)
if(needToExcludeDep)
project.configurations['eclipseMinusConfig'].dependencies.add(dep)
if(!maskedDepFiles.isEmpty())
org.gradle.api.internal.artifacts.dependencies.DefaultSelfResolvingDependency newDep = new org.gradle.api.internal.artifacts.dependencies.DefaultSelfResolvingDependency(dep.targetComponentId, project.files(maskedDepFiles))
project.configurations['eclipsePlusConfig'].dependencies.add(newDep)
File findMaskedFile(File f, String postfix, boolean initiallyUnmasked)
if(f != null)
for(Map.Entry<File, File> efm : eclipseFileMapping)
boolean masked = false;
if(initiallyUnmasked)
if(efm.key.equals(f))
masked = true;
else
if(efm.value.equals(f))
masked = true;
if(masked)
if(postfix != null)
return new File(efm.value, postfix)
else
return efm.value;
return findMaskedFile(f.parentFile, postfix==null ? f.name : f.name + File.pathSeparator+ postfix, initiallyUnmasked);
return null;
void dumpClassPathEntry(org.gradle.plugins.ide.eclipse.model.ClasspathEntry clspthentry)
if("output".equals(clspthentry.kind))
// the clspthentry is instance of org.gradle.plugins.ide.eclipse.model.Output
println(" output: "+ clspthentry.path)
else if("src".equals(clspthentry.kind))
if(clspthentry instanceof org.gradle.plugins.ide.eclipse.model.ProjectDependency)
// the clspthentry is instance of org.gradle.plugins.ide.eclipse.model.ProjectDependency
println(" project: exported="+ clspthentry.exported+ "; path="+ clspthentry.path)
else
// the clspthentry is instance of org.gradle.plugins.ide.eclipse.model.SourceFolder
println(" src folder: "+ clspthentry.name+ " ("+ clspthentry.dir+ ") -> output: "+ clspthentry.output)
if(clspthentry.excludes != null && clspthentry.excludes.size()>0)
println(" excludes:")
clspthentry.excludes.each excl ->
println(" "+ excl)
if(clspthentry.includes != null && clspthentry.includes.size()>0)
println(" includes:")
clspthentry.includes.each incl ->
println(" "+ incl)
else if("con".equals(clspthentry.kind))
//the clspthentry is instance of org.gradle.plugins.ide.eclipse.model.Container
println(" con: exported="+ clspthentry.exported+ "; path="+ clspthentry.path)
else if("lib".equals(clspthentry.kind))
//the clspthentry is instance of org.gradle.plugins.ide.eclipse.model.Library
println(" lib: file="+ clspthentry.library.path)
else
println(" UNKNOWN "+ clspthentry.kind+ " -> "+ clspthentry.getClass().getName())
// Gradle adds all custom sourceSets to eclipse's linkedResources. We do not need them in eclipse project, but we do not understand how and when the source is linked.
// So, JUST HACK IT: clear the linked resourcces after evaluating the project!
// But gradle is such a misterious thing! just clearing does not help. We need to put something there
// so lets put the existing linked resource, but with relative path :(
void setupEclipseProject()
if(project.name.contains("-android"))
project.eclipse.project
linkedResource name: 'AndroidManifest.xml', type: '1', location: 'PROJECT_LOC/src/main/AndroidManifest.xml'
linkedResource name: 'android-java', type: '2', location: 'PARENT-1-PROJECT_LOC/assets/build/android/java'
linkedResource name: 'res', type: '2', location: 'PROJECT_LOC/src/main/res'
我知道这不是 gradle 编程的顶峰,但它确实有效。 这个脚本的主要问题是它要求所有依赖项都是“编译”类型,但对于 Android,这已经过时了。新的依赖类型是'api'和'implementation'。 现在 'compile' 已被弃用,但恐怕它会完全消失。
有一个简单而丑陋的解决方案:使用 'eclipseOnly' 类型的依赖项复制所有非标准依赖项。上面的脚本中使用了此解决方案。这可行,但真的很难看,因为我们必须在所有项目中修改 gradle 脚本。但我的目标不是触及所有项目。现在脚本包含在一个单独的文件“eclipseHelper.gradle”中,并包含在根项目中,如下所示:
subprojects
apply from: "$rootProject.projectDir/eclipseHelper.gradle"
我想要实现的是将android 的特定类型的依赖项添加到Eclipse 中。首先,我从依赖项中排除了所有“发布”变体:
def androidExtension = project.extensions.findByName("android")
if (androidExtension != null)
android.variantFilter variant ->
def names = variant.flavors*.name
def buildTypeName = variant.buildType.name
// if buildtype is required for filtering use
// the above field
if (variant.name.contains("elease"))
variant.ignore = true
其次,我尝试将 Android 变体配置添加到 Eclipse 类路径('plusConfigurations')中:
def androidExtension = project.extensions.findByName("android")
if (androidExtension != null)
boolean applicationBuild = rootProject.hasProperty("applicationBuild")
if (androidExtension.getClass().getName().contains("LibraryExtension"))
android.libraryVariants.all variant ->
eclipse.classpath.plusConfigurations += variant.compileConfiguration
eclipse.classpath.plusConfigurations += variant.runtimeConfiguration
else
android.applicationVariants.all variant ->
eclipse.classpath.plusConfigurations += variant.compileConfiguration
eclipse.classpath.plusConfigurations += variant.runtimeConfiguration
我们的项目中有一些 Android 库,这些库是用不同的风格构建的。所以我得到了以下异常:
org.gradle.internal.component.AmbiguousVariantSelectionException: More than one variant of project :proj1-android matches the consumer a
ttributes:
- Configuration ':proj1-android:debugApiElements' variant android-aidl:
- Found artifactType 'android-aidl' but wasn't required.
- Required com.android.build.api.attributes.BuildTypeAttr 'debug' and found compatible value 'debug'.
- Found com.android.build.api.attributes.VariantAttr 'debug' but wasn't required.
- Required com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' and found compatible value 'Aar'.
- Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
- ...
- ...
- Configuration ':proj1-android:debugApiElements' variant jar:
- Found artifactType 'jar' but wasn't required.
- Required com.android.build.api.attributes.BuildTypeAttr 'debug' and found compatible value 'debug'.
- Found com.android.build.api.attributes.VariantAttr 'debug' but wasn't required.
- Required com.android.build.gradle.internal.dependency.AndroidTypeAttr 'Aar' and found compatible value 'Aar'.
- Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
有人可以帮我解决任何问题吗?
-
从集合中选择任何配置(“调试”)
完全跳过子项目的所有配置,并将其作为项目依赖包含到.classpath中
提前致谢, 安德烈
【问题讨论】:
【参考方案1】:我也有同样的问题。 找到了几个解决方案,但都不是最终解决方案。
https://github.com/greensopinion/gradle-android-eclipse
https://github.com/GinVavilon/gradle-android-eclipse
Google 在 android.tools.build 3.4 中实现了数据绑定和 androidx,我正在尝试根据这些条款自定义我的项目。
【讨论】:
以上是关于选择 Android 库的构建变体的 Gradle 脚本的主要内容,如果未能解决你的问题,请参考以下文章
Android 库 - 使用 Gradle 将多个变体发布到本地 Maven 存储库
Android - 仅在发布版本变体上执行 Gradle 任务
如何在 gradle kts 中排除(忽略)android 构建变体
Android知识要点整理(19)----Gradle 之构建变体