Skip to content

Commit

Permalink
Implement resource merging for flavored source sets (#89)
Browse files Browse the repository at this point in the history
* Implement resource merging for flavored source sets

* Push missed file

* Ensure outputs are present and handle cases like empty resource files
  • Loading branch information
arunkumar9t2 authored Apr 25, 2023
1 parent 0206168 commit f7277a3
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 9 deletions.
1 change: 1 addition & 0 deletions android/initialize.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def bazel_common_initialize(
"com.squareup.moshi:moshi:1.11.0",
"org.jetbrains.kotlin:kotlin-parcelize-compiler:1.6.10",
"org.jetbrains.kotlin:kotlin-parcelize-runtime:1.6.10",
"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4",
"com.github.tschuchortdev:kotlin-compile-testing:1.3.1",
"com.google.android.material:material:1.2.1",
"javax.inject:javax.inject:1",
Expand Down
58 changes: 56 additions & 2 deletions bazel_common_maven_install.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"dependency_tree": {
"__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL",
"__INPUT_ARTIFACTS_HASH": -824821778,
"__RESOLVED_ARTIFACTS_HASH": 1298305614,
"__INPUT_ARTIFACTS_HASH": 182772807,
"__RESOLVED_ARTIFACTS_HASH": -450453989,
"conflict_resolution": {
"com.squareup.moshi:moshi:1.11.0": "com.squareup.moshi:moshi:1.14.0"
},
Expand Down Expand Up @@ -2990,6 +2990,60 @@
"sha256": "aa88e9625577957f3249a46cb6e166ee09b369e600f7a11d148d16b0a6d87f05",
"url": "https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.7.0/kotlin-stdlib-1.7.0.jar"
},
{
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib-common:1.7.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0"
],
"directDependencies": [
"org.jetbrains.kotlin:kotlin-stdlib-common:1.7.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar",
"mirror_urls": [
"https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar",
"https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar",
"https://jcenter.bintray.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar",
"https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar"
],
"packages": [
"kotlinx.coroutines",
"kotlinx.coroutines.channels",
"kotlinx.coroutines.debug",
"kotlinx.coroutines.debug.internal",
"kotlinx.coroutines.flow",
"kotlinx.coroutines.flow.internal",
"kotlinx.coroutines.internal",
"kotlinx.coroutines.intrinsics",
"kotlinx.coroutines.scheduling",
"kotlinx.coroutines.selects",
"kotlinx.coroutines.sync"
],
"sha256": "c24c8bb27bb320c4a93871501a7e5e0c61607638907b197aef675513d4c820be",
"url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.6.4/kotlinx-coroutines-core-jvm-1.6.4.jar"
},
{
"coord": "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4",
"dependencies": [
"org.jetbrains.kotlin:kotlin-stdlib-common:1.7.0",
"org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0",
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4"
],
"directDependencies": [
"org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4"
],
"file": "v1/https/repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar",
"mirror_urls": [
"https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar",
"https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar",
"https://jcenter.bintray.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar",
"https://maven.google.com/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar"
],
"packages": [],
"sha256": "778851e73851b502e8366434bc9ec58371431890fb12b89e7edbf1732962c030",
"url": "https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/kotlinx-coroutines-core-1.6.4.jar"
},
{
"coord": "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.3.0",
"dependencies": [
Expand Down
1 change: 1 addition & 0 deletions rules/android/android_binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def android_binary(
resource_files = build_resources(
name = name,
resource_files = attrs.get("resource_files", default = []),
resources = attrs.get("resources", default = {}),
res_values = res_values,
)

Expand Down
1 change: 1 addition & 0 deletions rules/android/android_library.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def android_library(
resource_files = build_resources(
name = name,
resource_files = attrs.get("resource_files", default = []),
resources = attrs.get("resources", default = {}),
res_values = res_values,
)

Expand Down
Empty file.
61 changes: 61 additions & 0 deletions rules/android/private/resource_merger.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
Rule to merge android variant specific resource folders and account for overrides.
"""

def _to_path(f):
return f.path

def _resource_merger_impl(ctx):
outputs = ctx.outputs.merged_resources
label = ctx.label.name

# Args for compiler
args = ctx.actions.args()
args.set_param_file_format("multiline")
args.use_param_file("--flagfile=%s", use_always = True)
args.add("RESOURCE_MERGER")
args.add("--target", ctx.label.package)
args.add_joined(
"--source-sets",
ctx.attr.source_sets,
join_with = ",",
)
args.add_joined(
"--output",
outputs,
join_with = ",",
map_each = _to_path,
)

mnemonic = "MergeSourceSets"
ctx.actions.run(
mnemonic = mnemonic,
inputs = depset(ctx.files.resources + ctx.files.manifests),
outputs = outputs,
executable = ctx.executable._compiler,
arguments = [args],
progress_message = "%s %s" % (mnemonic, ctx.label),
execution_requirements = {
"supports-workers": "0",
"supports-multiplex-workers": "0",
"requires-worker-protocol": "json",
"worker-key-mnemonic": "MergeSourceSets",
},
)

return [DefaultInfo(files = depset(outputs))]

resource_merger = rule(
implementation = _resource_merger_impl,
attrs = {
"source_sets": attr.string_list(),
"resources": attr.label_list(allow_files = True, mandatory = True),
"manifests": attr.label_list(allow_files = True, mandatory = True),
"merged_resources": attr.output_list(mandatory = True),
"_compiler": attr.label(
default = Label("@grab_bazel_common//tools/aapt_lite:aapt_lite"),
executable = True,
cfg = "exec",
),
},
)
81 changes: 78 additions & 3 deletions rules/android/resources.bzl
Original file line number Diff line number Diff line change
@@ -1,10 +1,85 @@
load("@grab_bazel_common//tools/res_value:res_value.bzl", "res_value")
load("@grab_bazel_common//rules/android/private:resource_merger.bzl", "resource_merger")

def build_resources(name, resource_files, res_values):
def _calculate_output_files(name, all_resources):
"""
Returns list of source `resource_files` and generated `resource_files` from the `res_value` macro
Resource merger would merge source resource files and write to a merged directory. Bazel needs to know output files in advance, so this
method tries to predict the output files so we can register them as predeclared outputs.
Args:
all_resources: All resource files sorted based on priority with higher priority appearing first.
"""
outputs = []

# Multiple res folders root can contain same file name of resource, prevent creating same outputs by storing normalized resource paths
# eg: `res/values/strings.xml`
normalized_res_paths = {}

for file in all_resources:
res_name_and_dir = file.split("/")[-2:] # ["values", "values.xml"] etc
res_dir = res_name_and_dir[0]
res_name = res_name_and_dir[1]
if "values" in res_dir:
# Resource merging merges all values files into single values.xml file.
normalized_res_path = "%s/out/res/%s/values.xml" % (name, res_dir)
else:
normalized_res_path = "%s/out/res/%s/%s" % (name, res_dir, res_name)

if normalized_res_path not in normalized_res_paths:
normalized_res_paths[normalized_res_path] = normalized_res_path
outputs.append(normalized_res_path)
return outputs

def build_resources(
name,
resource_files,
resources,
res_values):
"""
return resource_files + res_value(
Calculates and returns resource_files either generated, merged or just the source ones based on parameters given. When `resources` are
declared and it has multiple resource roots then all those roots are merged into single directory and contents of the directory are returned.
Conversely if resource_files are used then sources are returned as is. In both cases, generated resources passed via res_values are
accounted for.
"""
generated_resources = res_value(
name = name + "_res_value",
strings = res_values.get("strings", default = {}),
)

if (len(resources) != 0 and len(resource_files) != 0):
fail("Either resources or resource_files should be specified but not both")

if (len(resources) != 0):
# Resources are passed with the new format
# Merge sources and return the merged result

if (len(resources) == 1):
resource_dir = resources.keys()[0]
return native.glob([resource_dir + "/**"]) + generated_resources

source_sets = [] # Source sets in the format res_dir::manifest
all_resources = [] # All resources
all_manifests = []

for resource_dir in resources.keys():
resource_dict = resources.get(resource_dir)
all_resources.extend(native.glob([resource_dir + "/**"]))

manifest = resource_dict.get("manifest", "")
if manifest != "":
all_manifests.append(manifest)

source_sets.append("%s::%s" % (resource_dir, manifest))

merge_target_name = name + "_res"
merged_resources = _calculate_output_files(merge_target_name, all_resources)
resource_merger(
name = merge_target_name,
source_sets = source_sets,
resources = all_resources,
manifests = all_manifests,
merged_resources = merged_resources,
)
return merged_resources + generated_resources
else:
return resource_files + generated_resources
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library")

kt_jvm_library(
name = "source_set",
srcs = [
"SourceSet.kt",
],
)

java_library(
name = "merger",
srcs = glob([
"*.java",
]),
deps = [
":source_set",
"//tools/android:android_tools",
],
)

kt_jvm_library(
name = "resource",
srcs = [
"OutputFixer.kt",
"ResourceMergerCommand.kt",
],
visibility = [
"//visibility:public",
],
deps = [
":merger",
":source_set",
"//tools/aapt_lite/src/main/java/com/grab/aapt/databinding/util",
"@bazel_common_maven//:com_github_ajalt_clikt",
"@bazel_common_maven//:org_jetbrains_kotlinx_kotlinx_coroutines_core",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.google.devtools.build.android

import java.io.File

object OutputFixer {
const val EMPTY_RES_CONTENT = """<?xml version="1.0" encoding="UTF-8" standalone="no"?><resources/>"""
fun process(outputDir: File, declaredOutputs: List<String>) {
val outputDirPath = outputDir.toPath()

// Merged directories will have qualifiers added by bazel which will not match the path specified in declaredOutputs, manually
// walk and remove the suffixes like v4, v13 etc from the resource bucket directories.
outputDir.walk()
.filter { it != outputDirPath }
.filter { it.parentFile?.parentFile?.toPath() == outputDirPath }
.filter { it.isDirectory && it.name.matches(Regex(".*-v\\d+$")) }
.forEach { resBucket ->
val newName = resBucket.name.split("-").dropLast(1).joinToString(separator = "-")
resBucket.renameTo(File(resBucket.parent, newName))
}

// Empty resource files especially xmls are skipped in the merged directory, in order to satisfy bazel action output requirements
// manually add empty resource files here.
declaredOutputs
.asSequence()
.filter { it.endsWith(".xml") }
.map { File(it) }
.filter { !it.exists() }
.forEach { file ->
file.run {
parentFile?.mkdirs()
writeText(EMPTY_RES_CONTENT)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.google.devtools.build.android;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.MoreExecutors;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class ResourceMerger {

public static ParsedAndroidData emptyAndroidData() {
return ParsedAndroidData.of(
ImmutableSet.of(),
ImmutableMap.of(),
ImmutableMap.of(),
ImmutableMap.of());
}

public static void merge(final List<SourceSet> sourceSets, final File outputDir) throws IOException {
final Path target = Paths.get(outputDir.getAbsolutePath());
Collections.reverse(sourceSets);
final ImmutableList<DependencyAndroidData> deps = ImmutableList.copyOf(sourceSets
.stream()
.map(sourceSet -> new DependencyAndroidData(
/*resourceDirs*/ ImmutableList.copyOf(sourceSet.getResourceDirs()),
/*assetDirs*/ ImmutableList.copyOf(sourceSet.getAssetDirs()),
/*manifest*/ sourceSet.getManifest().toPath(),
/*rTxt*/ null,
/*symbols*/ null,
/*compiledSymbols*/ null
)).collect(Collectors.toList()));

final ParsedAndroidData androidData = ParsedAndroidData.from(deps);
final AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

final UnwrittenMergedAndroidData unwrittenMergedAndroidData = merger.doMerge(
/*transitive*/ emptyAndroidData(),
/*direct*/ emptyAndroidData(),
/*parsedPrimary*/ androidData,
/*primaryManifest*/ null,
/*primaryOverrideAll*/ true,
/*throwOnResourceConflict*/ false
);
final MergedAndroidData result = unwrittenMergedAndroidData.write(
AndroidDataWriter.createWith(
/*manifestDirectory*/ target,
/*resourceDirectory*/ target.resolve("res"),
/*assertsDirectory*/ target.resolve("assets"),
/*executorService*/ MoreExecutors.newDirectExecutorService())
);
}
}
Loading

0 comments on commit f7277a3

Please sign in to comment.