Added basic implementation of ResourceTableRegistry

This commit is contained in:
Akash Yadav
2022-08-19 21:57:57 +05:30
parent 42c5ebb26c
commit fe907be8fe
122 changed files with 1109 additions and 478 deletions
+8 -5
View File
@@ -55,14 +55,17 @@ android {
dependencies {
implementation common.kotlin
implementation project (':build-tools:layoutlib-api')
implementation project (':logger')
implementation project (':build-tools:jaxp')
api 'com.android.tools:common:30.2.2'
implementation 'com.android.tools:annotations:30.2.2'
implementation 'com.android.tools.build:aapt2-proto:7.2.2-7984345'
implementation 'com.google.protobuf:protobuf-java:3.10.0'
api aapt.common
api project (':build-tools:layoutlib-api')
implementation aapt.annotations
implementation aapt.proto
implementation google.protobuf
testImplementation tests.junit
testImplementation tests.robolectric
testImplementation tests.google_truth
}
@@ -18,9 +18,9 @@
package com.android.aaptcompiler
import com.android.aaptcompiler.proto.serializeTableToPb
import com.android.utils.FileUtils
import com.itsaky.androidide.layoutlib.resources.ResourceType
import com.itsaky.androidide.layoutlib.resources.ResourceVisibility
import com.android.utils.FileUtils
import java.io.File
private const val VALUES_DIRECTORY_PREFIX = "values"
@@ -33,29 +33,30 @@ private const val PNG_EXTENSION = "png"
* The different options available while compiling resources.
*
* @param generateSymbolsPathData Path where the R.txt file should be written to. If null, symbols
* will not be generated.
* will not be generated.
* @param visibility The visibility of resources in the given file to process. If specified, then
* compilation of table files will fail if either the "public" or "public group" tags are
* encountered.
* compilation of table files will fail if either the "public" or "public group" tags are
* encountered.
* @param requirePngCrunching whether PNG file crunching is enabled
* @param pseudolocalize Whether or not the pseudolocales en-XA and ar-XB are generated for default
* string resources.
* @param partialRFile file to output symbols defined in the resource file (currently only
* supported for non-raw XML files (e.g. values or layouts) to match AAPT2's behaviour)
* string resources.
* @param partialRFile file to output symbols defined in the resource file (currently only supported
* for non-raw XML files (e.g. values or layouts) to match AAPT2's behaviour)
* @param legacyMode Whether or not to error or warn on positional args not being specified in a
* translatable string with parameters.
* translatable string with parameters.
* @param verbose Whether or not to show verbose logs.
* @param sourcePath Specified source path to be written in the compiled resource.
*/
data class ResourceCompilerOptions(
val generateSymbolsPathData: ResourcePathData?= null,
val generateSymbolsPathData: ResourcePathData? = null,
val visibility: ResourceVisibility? = null,
val requirePngCrunching: Boolean = false,
val pseudolocalize: Boolean = false,
val partialRFile: File? = null,
val legacyMode: Boolean = false,
val verbose: Boolean = false,
val sourcePath: String? = null)
val sourcePath: String? = null
)
/**
* Shows whether the ResourceCompiler can compile the given Resource File.
@@ -68,8 +69,9 @@ fun canCompileResourceInJvm(file: File, requirePngCrunching: Boolean): Boolean {
if (file.isHidden) return true
val pathData = extractPathData(file)
if (pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX
&& pathData.extension == XML_EXTENSION) {
if (
pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX && pathData.extension == XML_EXTENSION
) {
// file is a values table.
return true
} else {
@@ -91,7 +93,7 @@ fun canCompileResourceInJvm(file: File, requirePngCrunching: Boolean): Boolean {
*
* @param file file to processed. How it is processed is determined by the resource compiler.
* @param options the [ResourceCompilerOptions] that should be considered while compiling the
* resource.
* resource.
*
* Values files are compiled by extracting the all the resources and their values into a Resource
* Table to be used in Linking.
@@ -106,7 +108,11 @@ fun canCompileResourceInJvm(file: File, requirePngCrunching: Boolean): Boolean {
* See [canCompileResourceInJvm] to see what is supported.
*/
fun compileResource(
file: File, outputDirectory: File, options: ResourceCompilerOptions, logger: BlameLogger) {
file: File,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger
) {
// Skip hidden files.
if (file.isHidden) {
@@ -123,24 +129,28 @@ fun compileResource(
compileFunction(pathData, outputDirectory, options, logger)
} catch (e: Exception) {
logger?.info("Failed to compile file", blameSource(pathData.source))
val message =
"Resource compilation failed (${e.message}. Cause: ${e.cause}). " +
"Check logs for more details."
throw ResourceCompilationException(message, e)
val message =
"Resource compilation failed (${e.message}. Cause: ${e.cause}). " +
"Check logs for more details."
throw ResourceCompilationException(message, e)
}
}
private fun getCompileMethod(pathData: ResourcePathData, logger: BlameLogger):
(ResourcePathData, File, ResourceCompilerOptions, BlameLogger) -> Unit {
if (pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX &&
pathData.extension == XML_EXTENSION) {
private fun getCompileMethod(
pathData: ResourcePathData,
logger: BlameLogger
): (ResourcePathData, File, ResourceCompilerOptions, BlameLogger) -> Unit {
if (
pathData.resourceDirectory == VALUES_DIRECTORY_PREFIX && pathData.extension == XML_EXTENSION
) {
pathData.extension = RESOURCE_TABLE_EXTENSION
return ::compileTable
} else {
val type = ResourceType.fromFolderName(pathData.resourceDirectory)
if (type == null) {
val errorMsg = "Invalid resource type '${pathData.resourceDirectory}'" +
" for file ${pathData.file.absolutePath}"
val errorMsg =
"Invalid resource type '${pathData.resourceDirectory}'" +
" for file ${pathData.file.absolutePath}"
logger?.warning(errorMsg)
error(errorMsg)
}
@@ -158,41 +168,45 @@ private fun getCompileMethod(pathData: ResourcePathData, logger: BlameLogger):
/**
* Compiles a xml values file into a flattened [ResourceTable] proto.
*
* After all resources are extracted from the given xml, if pseudolocalization is set,
* pseudolocale strings are created for each default string resource in the xml. The result is
* flattened to a file in the output directory.
* After all resources are extracted from the given xml, if pseudolocalization is set, pseudolocale
* strings are created for each default string resource in the xml. The result is flattened to a
* file in the output directory.
*
* @param pathData the file to be processed.
* @param outputDirectory the directory to which the compiled file is to be placed.
* @throws IllegalStateException A failure occurred in processing this resource.
*/
private fun compileTable(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger) {
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger
) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling XML table ${pathData.file.absolutePath} to $outputFile")
val table = ResourceTable(logger = logger)
val extractorOptions = TableExtractorOptions(
translatable = !pathData.name.contains("donottranslate"),
errorOnPositionalArgs = !options.legacyMode,
visibility = options.visibility)
val extractorOptions =
TableExtractorOptions(
translatable = !pathData.name.contains("donottranslate"),
errorOnPositionalArgs = !options.legacyMode,
visibility = options.visibility
)
val tableExtractor =
TableExtractor(table, pathData.source, pathData.config, extractorOptions, logger)
pathData.file.inputStream().use {
try {
tableExtractor.extract(it)
} catch (e: Exception) {
// For merged values there's no need to re-write as we don't know which line failed. The
// actual error will be raised by the table extractor.
throw ResourceCompilationException(
"Failed to compile values resource file ${pathData.file}", e
)
}
try {
tableExtractor.extract(it)
} catch (e: Exception) {
// For merged values there's no need to re-write as we don't know which line failed. The
// actual error will be raised by the table extractor.
throw ResourceCompilationException(
"Failed to compile values resource file ${pathData.file}",
e
)
}
// Adds the fake locales: en-XA and ar-XB for each default-defined string resource. This is used
// for debugging apps with long text (en-XA) or rtl (ar-XB) language support.
@@ -218,20 +232,23 @@ private fun compileTable(
pkg.groups.forEach { group ->
// Macros do not define resources and therefore don't belong in the R.txt.
if (group.type != AaptResourceType.MACRO) {
val javaType = if (group.type == AaptResourceType.STYLEABLE) "int[]" else "int"
group.entries.forEach { entry ->
val visibility = getVisibility(entry.value.values, group.type)
builder.appendln("${visibility.getName()} $javaType ${group.type.tagName} ${entry.key}")
if (group.type == AaptResourceType.STYLEABLE) {
// We also need to write down styleable children (entries)
group.getStyleable(entry).entries.forEach {
val childPackage =
if (!it.name.pck.isNullOrEmpty()) "_${it.name.pck}" else ""
// styleables and their children are always public
builder.appendln("public int styleable ${entry.key}${childPackage}_${it.name.entry}")
}
}
val javaType = if (group.type == AaptResourceType.STYLEABLE) "int[]" else "int"
group.entries.forEach { entry ->
val visibility = getVisibility(entry.value.values, group.type)
builder.appendLine(
"${visibility.getName()} $javaType ${group.type.tagName} ${entry.key}"
)
if (group.type == AaptResourceType.STYLEABLE) {
// We also need to write down styleable children (entries)
group.getStyleable(entry).entries.forEach {
val childPackage = if (!it.name.pck.isNullOrEmpty()) "_${it.name.pck}" else ""
// styleables and their children are always public
builder.appendln(
"public int styleable ${entry.key}${childPackage}_${it.name.entry}"
)
}
}
}
}
}
}
@@ -240,21 +257,24 @@ private fun compileTable(
}
}
private fun getVisibility(values: Collection<ResourceEntry>, type: AaptResourceType): ResourceVisibility {
// all declare styleables need to be marked as public so they're visible in the R classes
if (type == AaptResourceType.STYLEABLE) return ResourceVisibility.PUBLIC
var foundPublic = false
var foundPrivate = false
values.forEach {
// We only care about public and private since only these can clash
if (it.visibility.level == ResourceVisibility.PUBLIC) foundPublic = true
else if (it.visibility.level == ResourceVisibility.PRIVATE) foundPrivate = true
}
if (foundPublic && foundPrivate)
error("Resource cannot be both public and private: ${type.tagName} ${values.first().name}")
if (foundPublic) return ResourceVisibility.PUBLIC
if (foundPrivate) return ResourceVisibility.PRIVATE
return ResourceVisibility.PRIVATE_XML_ONLY // default
private fun getVisibility(
values: Collection<ResourceEntry>,
type: AaptResourceType
): ResourceVisibility {
// all declare styleables need to be marked as public so they're visible in the R classes
if (type == AaptResourceType.STYLEABLE) return ResourceVisibility.PUBLIC
var foundPublic = false
var foundPrivate = false
values.forEach {
// We only care about public and private since only these can clash
if (it.visibility.level == ResourceVisibility.PUBLIC) foundPublic = true
else if (it.visibility.level == ResourceVisibility.PRIVATE) foundPrivate = true
}
if (foundPublic && foundPrivate)
error("Resource cannot be both public and private: ${type.tagName} ${values.first().name}")
if (foundPublic) return ResourceVisibility.PUBLIC
if (foundPrivate) return ResourceVisibility.PRIVATE
return ResourceVisibility.PRIVATE_XML_ONLY // default
}
/**
@@ -264,19 +284,21 @@ private fun getVisibility(values: Collection<ResourceEntry>, type: AaptResourceT
* @param outputDirectory the directory to which the compiled file is to be placed.
*/
private fun compileFile(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?
) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling file ${pathData.file.absolutePath} to $outputFile")
pathData.file.inputStream().use {
val resourceFile = ResourceFile(
ResourceName("", resourceTypeFromTag(pathData.resourceDirectory)!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.Unknown
)
val resourceFile =
ResourceFile(
ResourceName("", resourceTypeFromTag(pathData.resourceDirectory)!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.Unknown
)
val container = Container(outputFile.outputStream(), 1)
container.addFileEntry(it, resourceFile)
@@ -306,34 +328,34 @@ private fun compileFile(
* @throws IllegalStateException A failure occurred in processing this resource.
*/
private fun compileXml(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?
) {
val outputFile = File(outputDirectory, pathData.getIntermediateContainerFilename())
logger?.info("Compiling xml file ${pathData.file.absolutePath} to $outputFile")
val fileToProcess = ResourceFile(
ResourceName("", pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.ProtoXml
)
val fileToProcess =
ResourceFile(
ResourceName("", pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.ProtoXml
)
pathData.file.inputStream().use {
val xmlProcessor = XmlProcessor(source = pathData.source, logger = logger)
try {
xmlProcessor.process(fileToProcess, it)
} catch (e: Exception) {
throw ResourceCompilationException(
"Failed to compile resource file: " +
logger?.getOutputSource(blameSource(pathData.source)),
e
)
}
try {
xmlProcessor.process(fileToProcess, it)
} catch (e: Exception) {
throw ResourceCompilationException(
"Failed to compile resource file: " + logger?.getOutputSource(blameSource(pathData.source)),
e
)
}
val container =
Container(outputFile.outputStream(), xmlProcessor.xmlResources.size)
val container = Container(outputFile.outputStream(), xmlProcessor.xmlResources.size)
for (resource in xmlProcessor.xmlResources) {
container.addXmlEntry(resource)
@@ -341,9 +363,9 @@ private fun compileXml(
if (options.partialRFile != null) {
val builder = StringBuilder()
builder.appendln("default int ${pathData.type.tagName} ${pathData.name}")
builder.appendLine("default int ${pathData.type.tagName} ${pathData.name}")
fileToProcess.exportedSymbols.forEach { id ->
builder.appendln("default int id ${id.name.entry}")
builder.appendLine("default int id ${id.name.entry}")
}
FileUtils.writeToFile(options.partialRFile, builder.toString())
}
@@ -359,18 +381,18 @@ private fun compileXml(
*
* If we are not running in debug mode, the png will undergo additional crunching.
*
* Finally, the processed file is written to the output directory. It is possible that no
* processing is done to the png file, and if so it will just be written exactly as if it were
* a raw file.
* Finally, the processed file is written to the output directory. It is possible that no processing
* is done to the png file, and if so it will just be written exactly as if it were a raw file.
*
* @param pathData the file to process.
* @param outputDirectory the directory in which the processed file will be placed.
*/
private fun compilePng(
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?) {
pathData: ResourcePathData,
outputDirectory: File,
options: ResourceCompilerOptions,
logger: BlameLogger?
) {
logger?.info("Compiling image file ${pathData.file.absolutePath}")
if (pathData.extension == PATCH_9_EXTENSION) {
error("Patch 9 PNG processing is not supported with the JVM Android resource compiler.")
@@ -23,7 +23,7 @@ class ResourceTable(val validateResources: Boolean = false, val logger: BlameLog
val stringPool = StringPool()
/** The list of packages in this table. */
internal val packages = mutableListOf<ResourceTablePackage>()
val packages = mutableListOf<ResourceTablePackage>()
/**
* Set of dynamic packages that this table may reference. Their package names get encoded into the
@@ -638,8 +638,7 @@ class ResourceTablePackage(var name: String = "", var id: Byte? = null) {
}
fun findOrCreateGroup(type: AaptResourceType, groupId: Byte? = null): ResourceGroup {
val group = findGroup(type, groupId)
return when(group) {
return when(val group = findGroup(type, groupId)) {
null -> {
val newGroup = ResourceGroup(type)
newGroup.id = groupId
@@ -705,7 +704,7 @@ class ResourceEntry(val name : String) {
var allowNew: AllowNew? = null
var overlayable: OverlayableItem? = null
internal val values = mutableListOf<ResourceConfigValue>()
val values = mutableListOf<ResourceConfigValue>()
fun findValue(config: ConfigDescription, product: String = ""): ResourceConfigValue? {
return values.find { it.config == config && it.product == product }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.aapt.logging
import com.android.utils.ILogger
/**
* Implementation of [ILogger] which redirects all logs to AndroidIDE's logger implementation i.e.
* [com.itsaky.androidide.utils.ILogger].
*
* @author Akash Yadav
*/
object IDELogger : ILogger {
private val log = com.itsaky.androidide.utils.ILogger.newInstance("AAPTCompilerLogger")
override fun error(t: Throwable?, msgFormat: String?, vararg args: Any?) {
if (msgFormat != null) {
log.error(String.format(msgFormat, args))
}
if (t != null) {
log.error(t)
}
}
override fun warning(msgFormat: String?, vararg args: Any?) {
if (msgFormat != null) {
log.warn(String.format(msgFormat, args))
}
}
override fun info(msgFormat: String?, vararg args: Any?) {
if (msgFormat != null) {
log.info(String.format(msgFormat, args))
}
}
override fun verbose(msgFormat: String?, vararg args: Any?) {
if (msgFormat != null) {
log.verbose(String.format(msgFormat, args))
}
}
}
@@ -27,9 +27,10 @@ import kotlin.io.path.absolute
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.robolectric.RobolectricTestRunner
/** @author Akash Yadav */
@RunWith(JUnit4::class)
@RunWith(RobolectricTestRunner::class)
class CompilerTest {
@Test

Some files were not shown because too many files have changed in this diff Show More