diff --git a/README.md b/README.md
index 6831ecbd3..845f9f54f 100644
--- a/README.md
+++ b/README.md
@@ -108,3 +108,21 @@ android_library(
...
)
```
+
+## Available Rules
+
+- **`android_binary`** - Builds an Android APK
+- **`android_library`** - Builds an Android library
+- **`android_application`** - Builds an Android application bundle (AAB)
+- **`fat_aar`** - Bundles multiple Android libraries into a single AAR file (see [rules/fat_aar](rules/fat_aar/))
+- **`fat_aar_pom`** - Generates a Maven POM from a fat_aar's excluded dependencies
+
+For a complete list of rules, see the [rules.bzl](rules/rules.bzl) file.
+
+## Examples
+
+See the [examples](examples/) directory for sample projects:
+
+- **[basicapp](examples/basicapp/)** - Basic Android application
+- **[bundle](examples/bundle/)** - Android application bundle (AAB)
+- **[fat_aar](examples/fat_aar/)** - Fat AAR with resources, assets, and native libraries
diff --git a/examples/fat_aar/AndroidManifest.xml b/examples/fat_aar/AndroidManifest.xml
new file mode 100644
index 000000000..fe8cfeb89
--- /dev/null
+++ b/examples/fat_aar/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/examples/fat_aar/BUILD b/examples/fat_aar/BUILD
new file mode 100644
index 000000000..ceca40246
--- /dev/null
+++ b/examples/fat_aar/BUILD
@@ -0,0 +1,44 @@
+load("//rules/android_library:rule.bzl", "android_library")
+load("//rules/fat_aar:rule.bzl", "fat_aar")
+
+# Library with resources and assets
+android_library(
+ name = "lib1",
+ srcs = ["Lib1.java"],
+ manifest = "AndroidManifest.xml",
+ resource_files = glob(["res/**"]),
+ assets = glob(["assets/**"]),
+ assets_dir = "assets",
+)
+
+# Library depending on lib1 (transitive dependency test)
+android_library(
+ name = "lib2",
+ srcs = ["Lib2.java"],
+ manifest = "AndroidManifest.xml",
+ deps = [":lib1"],
+)
+
+# Native library
+cc_library(
+ name = "native_lib",
+ srcs = ["jni/arm64-v8a/libnative.so"],
+)
+
+# Fat AAR bundling all transitive dependencies
+fat_aar(
+ name = "bundled",
+ min_sdk_version = "21",
+ deps = [
+ ":lib2",
+ ":native_lib",
+ ],
+)
+
+# Fat AAR with exclusions (e.g., exclude external dependencies)
+fat_aar(
+ name = "bundled_filtered",
+ min_sdk_version = "21",
+ exclude = ["@maven//"],
+ deps = [":lib2"],
+)
diff --git a/examples/fat_aar/Lib1.java b/examples/fat_aar/Lib1.java
new file mode 100644
index 000000000..42a3d73b3
--- /dev/null
+++ b/examples/fat_aar/Lib1.java
@@ -0,0 +1,7 @@
+package com.example.fat_aar;
+
+public class Lib1 {
+ public String getMessage() {
+ return "Hello from Lib1";
+ }
+}
diff --git a/examples/fat_aar/Lib2.java b/examples/fat_aar/Lib2.java
new file mode 100644
index 000000000..b06f3134b
--- /dev/null
+++ b/examples/fat_aar/Lib2.java
@@ -0,0 +1,9 @@
+package com.example.fat_aar;
+
+public class Lib2 {
+ private Lib1 lib1 = new Lib1();
+
+ public String getCombinedMessage() {
+ return "Lib2 says: " + lib1.getMessage();
+ }
+}
diff --git a/examples/fat_aar/README.md b/examples/fat_aar/README.md
new file mode 100644
index 000000000..10be480e0
--- /dev/null
+++ b/examples/fat_aar/README.md
@@ -0,0 +1,60 @@
+# Fat AAR Example
+
+This example demonstrates how to use the `fat_aar` rule to bundle multiple Android libraries and their transitive dependencies into a single AAR file.
+
+## Structure
+
+- **lib1**: Android library with resources, layouts, and assets
+- **lib2**: Android library that depends on lib1 (demonstrates transitive bundling)
+- **native_lib**: Native library (demonstrates native library bundling)
+
+## Targets
+
+### `bundled`
+A fat AAR that bundles all transitive dependencies including:
+- Java/Kotlin classes from lib1 and lib2
+- Resources from all libraries
+- Assets from all libraries
+- Native libraries
+- Merged AndroidManifest.xml
+
+```bash
+bazel build //examples/fat_aar:bundled
+```
+
+### `bundled_filtered`
+A fat AAR with exclusions - demonstrates filtering out external dependencies:
+```bash
+bazel build //examples/fat_aar:bundled_filtered
+```
+
+## What Gets Bundled
+
+The `fat_aar` rule automatically collects and bundles:
+
+1. **Classes (classes.jar)**: All Java/Kotlin bytecode from transitive dependencies
+2. **Resources (res/)**: All resources from transitive android_library targets
+3. **Assets (assets/)**: All assets from transitive dependencies
+4. **Native Libraries (jni/)**: Native .so files for all architectures
+5. **AndroidManifest.xml**: Merged manifest from all libraries
+6. **R.txt**: Combined R.txt for resource IDs
+7. **proguard.txt**: Merged ProGuard rules
+
+## Excluding Dependencies
+
+Use the `exclude` attribute to filter out dependencies (e.g., external Maven dependencies):
+
+```python
+fat_aar(
+ name = "my_aar",
+ exclude = ["@maven//"], # Exclude all Maven dependencies
+ deps = [":my_lib"],
+)
+```
+
+## Output
+
+The fat AAR is a standard Android AAR file that can be:
+- Published to Maven/Artifactory
+- Consumed by other Android projects (Gradle or Bazel)
+- Distributed as a reusable component
diff --git a/examples/fat_aar/assets/sample_asset.txt b/examples/fat_aar/assets/sample_asset.txt
new file mode 100644
index 000000000..ebe542907
--- /dev/null
+++ b/examples/fat_aar/assets/sample_asset.txt
@@ -0,0 +1 @@
+This is a sample asset file to test asset bundling in fat_aar.
diff --git a/examples/fat_aar/jni/arm64-v8a/libnative.so b/examples/fat_aar/jni/arm64-v8a/libnative.so
new file mode 100644
index 000000000..9bd98f285
--- /dev/null
+++ b/examples/fat_aar/jni/arm64-v8a/libnative.so
@@ -0,0 +1 @@
+FAKE_NATIVE_LIB_FOR_TESTING
\ No newline at end of file
diff --git a/examples/fat_aar/res/layout/lib_layout.xml b/examples/fat_aar/res/layout/lib_layout.xml
new file mode 100644
index 000000000..d3b7dfc5b
--- /dev/null
+++ b/examples/fat_aar/res/layout/lib_layout.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/examples/fat_aar/res/values/strings.xml b/examples/fat_aar/res/values/strings.xml
new file mode 100644
index 000000000..ccae96da0
--- /dev/null
+++ b/examples/fat_aar/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Fat AAR Example
+ Hello from resources
+
diff --git a/rules/BUILD b/rules/BUILD
index 4cb95be0b..d47e255aa 100644
--- a/rules/BUILD
+++ b/rules/BUILD
@@ -109,6 +109,7 @@ bzl_library(
],
visibility = [
"//mobile_install:__pkg__",
+ "//rules/fat_aar:__pkg__",
"//stardoc:__pkg__",
"//test/rules/android_binary/r8_integration:__pkg__",
"//test/rules/android_sdk_repository:__pkg__",
diff --git a/rules/fat_aar/BUILD b/rules/fat_aar/BUILD
new file mode 100644
index 000000000..5542fed8b
--- /dev/null
+++ b/rules/fat_aar/BUILD
@@ -0,0 +1,18 @@
+load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files([
+ "add_native_libs.sh",
+ "rule.bzl",
+ "aspect.bzl",
+])
+
+bzl_library(
+ name = "bzl",
+ srcs = glob(["*.bzl"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//rules:bzl",
+ ],
+)
diff --git a/rules/fat_aar/README.md b/rules/fat_aar/README.md
new file mode 100644
index 000000000..aec12d4c9
--- /dev/null
+++ b/rules/fat_aar/README.md
@@ -0,0 +1,207 @@
+# Fat AAR Rules
+
+This directory contains the implementation of the `fat_aar` rule and supporting utilities for bundling multiple Android libraries into a single AAR file.
+
+## Overview
+
+The `fat_aar` rule consolidates multiple `android_library` targets and their transitive dependencies into a unified AAR package. This is useful for:
+
+- **Simplified distribution**: Publish a single AAR instead of managing multiple library dependencies
+- **Dependency encapsulation**: Hide internal module structure from external consumers
+- **Selective bundling**: Exclude external dependencies (e.g., Maven artifacts) while tracking them for POM generation
+
+## Rules
+
+### `fat_aar`
+
+Bundles transitive `android_library` dependencies into a single AAR file.
+
+**Defined in**: `rule.bzl`
+
+**Key features**:
+- Collects all transitive dependencies using aspects
+- Bundles Java/Kotlin classes, resources, assets, manifests, native libraries, and ProGuard rules
+- Filters dependencies based on exclusion patterns
+- Generates excluded dependencies file as output group
+
+**Examples**:
+
+Basic usage:
+```python
+fat_aar(
+ name = "my_fat_aar",
+ deps = [":my_library"],
+ exclude = ["@maven//", "@@maven//", "@@com_github_jetbrains_kotlin"],
+ min_sdk_version = "23",
+)
+```
+
+With R8 optimization:
+```python
+fat_aar(
+ name = "my_fat_aar_optimized",
+ deps = [":my_library"],
+ exclude = ["@maven//", "@@maven//", "@@com_github_jetbrains_kotlin"],
+ r8_config = "proguard.pro",
+ min_sdk_version = "23",
+)
+```
+
+**Attributes**:
+- `deps` (required): List of `android_library` targets to bundle. All transitive dependencies will be included unless excluded via the `exclude` attribute
+- `exclude` (optional, default: `[]`): List of label patterns to exclude from bundling. Commonly used to exclude external dependencies that consumers are expected to provide (e.g., `["@maven//", "@@maven//"]` to exclude all Maven dependencies, `"@@com_github_jetbrains_kotlin"` to exclude Kotlin stdlib)
+- `min_sdk_version` (optional, default: `"23"`): Minimum SDK version for the primary manifest. This is used when merging multiple AndroidManifest.xml files
+- `r8_config` (optional): ProGuard configuration file (`.pro` or `.txt`) for R8 optimization. If provided, R8 will optimize and shrink the bundled code. The configuration is combined with transitive ProGuard specs from all dependencies
+
+**Outputs**:
+- Default output: The bundled AAR file containing merged classes, resources, assets, manifests, native libraries, and ProGuard rules
+- Output group `excluded_deps`: Text file listing all dependencies that were excluded (useful for POM generation)
+- Output group `manifest`: The merged AndroidManifest.xml file
+- Output group `class_jar`: The classes.jar file (optionally R8-optimized if `r8_config` is provided)
+
+### `fat_aar_pom`
+
+Generates a Maven POM file from a fat_aar's excluded dependencies.
+
+**Defined in**: `pom_from_fat_aar.bzl`
+
+**Key features**:
+- Looks up excluded Bazel labels in Maven coordinates list
+- Generates POM with proper dependency declarations
+- Supports both 3-part and 4-part Maven coordinates
+
+**Example**:
+```python
+fat_aar_pom(
+ name = "my_pom",
+ fat_aar = ":my_fat_aar",
+ maven_coords = MAVEN_ARTIFACTS,
+ group_id = "com.example",
+ artifact_id = "my-library",
+ version = "1.0.0",
+)
+```
+
+**Attributes**:
+- `fat_aar`: The `fat_aar` target to generate POM for
+- `maven_coords`: List of all Maven coordinates (format: `"group:artifact:type:version"`)
+- `group_id`: Maven group ID
+- `artifact_id`: Maven artifact ID
+- `version`: Maven version
+
+## Supporting Files
+
+### `aspect.bzl`
+
+Defines the `fat_aar_aspect` and providers:
+
+- **`FatAarInfo`**: Collects Android providers (resources, assets, manifests, native libs, ProGuard) as `(label, provider)` tuples for filtering
+- **`FatAarDependenciesInfo`**: Tracks labels of excluded dependencies for POM generation
+
+The aspect traverses the dependency graph and collects all Android-related providers from transitive dependencies.
+
+### `add_native_libs.sh`
+
+Shell script that adds native libraries to the AAR in the correct format.
+
+**Why it's needed**:
+- Android native libraries are distributed in ZIP files with `lib/ARCH/*.so` structure
+- The AAR format requires `jni/ARCH/*.so` structure
+- This script extracts native libs from ZIP files and converts them to AAR format
+
+**Usage**: Called automatically by the `fat_aar` rule implementation.
+
+## R8 Integration
+
+When `r8_config` is provided, R8 runs with the following behavior:
+
+- **Output format**: `.class` files (not `.dex`) using the `--classfile` flag
+- **Library classpath**: Android SDK jar is provided via `--lib`
+- **ProGuard configs**: Combines the user-provided `r8_config` with all transitive ProGuard specifications from dependencies
+- **Optimization mode**: `--release` mode for production optimization
+- **API level**: Note that `--min-api` is not supported when using `--classfile` mode
+
+### R8 Configuration Best Practices
+
+Create a ProGuard configuration file (e.g., `proguard.pro`):
+
+```proguard
+# Keep all public API
+-keep public class * { public *; }
+
+# Keep attributes for debugging
+-keepattributes Exceptions,InnerClasses,Signature,SourceFile,LineNumberTable,EnclosingMethod
+
+# Don't obfuscate (optional - use if you want readable class names)
+-dontobfuscate
+
+# Allow R8 to optimize
+-allowaccessmodification
+
+# Don't warn about excluded dependencies
+-dontwarn kotlin.**
+-dontwarn org.jetbrains.annotations.**
+-dontwarn java.lang.invoke.LambdaMetafactory
+```
+
+**Key points**:
+1. **Keep public API**: Use `-keep public class * { public *; }` to preserve your SDK's public interface
+2. **Exclude warnings**: Add `-dontwarn` rules for dependencies you've excluded (e.g., Kotlin stdlib)
+3. **Keep attributes**: Include `EnclosingMethod` attribute when keeping `InnerClasses`
+4. **Don't obfuscate (optional)**: Use `-dontobfuscate` if you want readable class names in your SDK
+5. **Allow optimization**: Use `-allowaccessmodification` to let R8 optimize more aggressively
+
+## How It Works
+
+1. **Dependency Collection**:
+ - The `fat_aar_aspect` traverses the dependency graph
+ - Collects Android providers as `(label, provider)` tuples
+ - Allows filtering based on label patterns
+
+2. **Filtering**:
+ - Labels and files are checked against `exclude` patterns
+ - Excluded labels are tracked in `FatAarDependenciesInfo` provider
+ - Excluded dependencies list is generated as output group
+
+3. **Bundling**:
+ - Resources are merged from all included libraries
+ - Manifests are merged using Android's manifest merger tool
+ - Classes are combined into a single `classes.jar`
+ - **R8 Optimization** (optional): If `r8_config` is provided, R8 optimizes and shrinks the merged classes
+ - Native libraries are converted to AAR format
+ - R.txt and ProGuard rules are merged
+
+4. **POM Generation**:
+ - Excluded labels are looked up in Maven coordinates list
+ - Matching dependencies are added to POM
+ - Generated POM can be used for Maven publishing
+
+## Integration with Uber Repository
+
+In the Uber Android monorepo, these rules are wrapped by the `uber_fat_aar` macro in `android/defs.bzl`, which:
+
+- Creates the fat AAR with standard exclusions
+- Wraps it with `aar_import` for consumption
+- Generates POM file automatically
+- Creates publish target for Maven/Artifactory
+
+See `android/experimental/sample/simple/app_fat_aar/` for a complete example.
+
+## File Structure
+
+```
+rules/fat_aar/
+├── README.md # This file
+├── BUILD # Exports scripts and bzl files
+├── rule.bzl # fat_aar rule implementation
+├── aspect.bzl # Aspect and providers
+├── pom_from_fat_aar.bzl # fat_aar_pom rule
+└── add_native_libs.sh # Native library conversion script
+```
+
+## See Also
+
+- **Example**: `bazel_rules_android_legacy/examples/fat_aar/`
+- **Uber integration**: `android/defs.bzl` (`uber_fat_aar` macro)
+- **Publishing**: `android/rules/publish_aar/aar_publish.bzl`
+- **Usage example**: `android/experimental/sample/simple/app_fat_aar/`
diff --git a/rules/fat_aar/add_native_libs.sh b/rules/fat_aar/add_native_libs.sh
new file mode 100644
index 000000000..4678bc934
--- /dev/null
+++ b/rules/fat_aar/add_native_libs.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+# Adds native libraries to AAR in the correct jni/ARCH/*.so format.
+#
+# Why this script is needed:
+# - Android native libraries are distributed in ZIP files with lib/ARCH/*.so structure
+# (e.g., lib/arm64-v8a/libnative.so, lib/armeabi-v7a/libnative.so)
+# - The AAR format requires native libraries in jni/ARCH/*.so structure
+# (e.g., jni/arm64-v8a/libnative.so, jni/armeabi-v7a/libnative.so)
+# - This script extracts native libs from ZIP files and converts them to the correct format
+#
+# The conversion is necessary for aar_import to properly recognize and use the native
+# libraries when the fat AAR is consumed by other projects.
+#
+# Usage: add_native_libs.sh BASE_AAR FINAL_AAR TEMP_DIR NATIVE_ZIP1 [NATIVE_ZIP2 ...]
+
+set -e
+
+BASE_AAR="$(cd "$(dirname "$1")"; pwd)/$(basename "$1")"
+FINAL_AAR="$(cd "$(dirname "$2")"; pwd)/$(basename "$2")"
+TEMP_DIR="$3"
+shift 3
+
+ORIG_DIR="$(pwd)"
+
+mkdir -p "$TEMP_DIR"
+unzip -q "$BASE_AAR" -d "$TEMP_DIR"
+
+cd "$TEMP_DIR"
+mkdir -p jni
+
+for native_zip in "$@"; do
+ if [ -f "$ORIG_DIR/$native_zip" ] && [ -s "$ORIG_DIR/$native_zip" ]; then
+ TEMP_EXTRACT=$(mktemp -d)
+ unzip -q -o "$ORIG_DIR/$native_zip" -d "$TEMP_EXTRACT" 2>/dev/null || true
+ # Convert lib/ARCH/*.so to jni/ARCH/*.so
+ if [ -d "$TEMP_EXTRACT/lib" ]; then
+ cp -r "$TEMP_EXTRACT/lib/"* jni/ 2>/dev/null || true
+ fi
+ rm -rf "$TEMP_EXTRACT"
+ fi
+done
+
+zip -q -r "$FINAL_AAR" .
diff --git a/rules/fat_aar/aspect.bzl b/rules/fat_aar/aspect.bzl
new file mode 100644
index 000000000..0ba61d97c
--- /dev/null
+++ b/rules/fat_aar/aspect.bzl
@@ -0,0 +1,112 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""Aspect for collecting transitive Android providers."""
+
+load("@rules_java//java/common:proguard_spec_info.bzl", "ProguardSpecInfo")
+load("//providers:providers.bzl", "AndroidAssetsInfo", "AndroidNativeLibsInfo", "AndroidResourcesInfo", "StarlarkAndroidResourcesInfo")
+load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
+
+visibility(PROJECT_VISIBILITY)
+
+FatAarInfo = provider(
+ "Collects Android providers from transitive dependencies",
+ fields = {
+ "resource_infos": "Depset of (label, AndroidResourcesInfo) tuples",
+ "assets_infos": "Depset of (label, AndroidAssetsInfo) tuples",
+ "native_libs_infos": "Depset of (label, AndroidNativeLibsInfo) tuples",
+ "manifest_infos": "Depset of (label, manifest_file) tuples",
+ "proguard_infos": "Depset of (label, ProguardSpecInfo) tuples",
+ },
+)
+
+FatAarDependenciesInfo = provider(
+ "Tracks dependencies that were excluded during fat_aar bundling",
+ fields = {
+ "excluded_labels": "Depset of labels that were excluded",
+ },
+)
+
+def _fat_aar_aspect_impl(target, ctx):
+ """Collects Android providers transitively.
+
+ Args:
+ target: The target being visited
+ ctx: The aspect context
+
+ Returns:
+ List containing FatAarInfo provider
+ """
+ resource_infos = []
+ assets_infos = []
+ native_libs_infos = []
+ manifest_infos = []
+ proguard_infos = []
+
+ # Collect providers with their source label
+ label = ctx.label
+
+ # Collect both AndroidResourcesInfo and StarlarkAndroidResourcesInfo
+ if AndroidResourcesInfo != None and AndroidResourcesInfo in target:
+ resource_infos.append((label, target[AndroidResourcesInfo]))
+ if StarlarkAndroidResourcesInfo in target:
+ resource_infos.append((label, target[StarlarkAndroidResourcesInfo]))
+ if AndroidAssetsInfo != None and AndroidAssetsInfo in target:
+ assets_infos.append((label, target[AndroidAssetsInfo]))
+ if AndroidNativeLibsInfo in target:
+ native_libs_infos.append((label, target[AndroidNativeLibsInfo]))
+ if ProguardSpecInfo in target:
+ proguard_infos.append((label, target[ProguardSpecInfo]))
+
+ # Collect manifest if available
+ if hasattr(ctx.rule.attr, "manifest") and ctx.rule.attr.manifest:
+ if hasattr(ctx.rule.attr.manifest, "files"):
+ for f in ctx.rule.attr.manifest.files.to_list():
+ manifest_infos.append((label, f))
+
+ transitive_resource_infos = []
+ transitive_assets_infos = []
+ transitive_native_libs_infos = []
+ transitive_manifest_infos = []
+ transitive_proguard_infos = []
+
+ # Collect from deps and exports attributes (declared in attr_aspects)
+ for dep in ctx.rule.attr.deps:
+ transitive_resource_infos.append(dep[FatAarInfo].resource_infos)
+ transitive_assets_infos.append(dep[FatAarInfo].assets_infos)
+ transitive_native_libs_infos.append(dep[FatAarInfo].native_libs_infos)
+ transitive_manifest_infos.append(dep[FatAarInfo].manifest_infos)
+ transitive_proguard_infos.append(dep[FatAarInfo].proguard_infos)
+
+ if hasattr(ctx.rule.attr, "exports"):
+ for dep in ctx.rule.attr.exports:
+ transitive_resource_infos.append(dep[FatAarInfo].resource_infos)
+ transitive_assets_infos.append(dep[FatAarInfo].assets_infos)
+ transitive_native_libs_infos.append(dep[FatAarInfo].native_libs_infos)
+ transitive_manifest_infos.append(dep[FatAarInfo].manifest_infos)
+ transitive_proguard_infos.append(dep[FatAarInfo].proguard_infos)
+
+ return [FatAarInfo(
+ resource_infos = depset(resource_infos, transitive = transitive_resource_infos),
+ assets_infos = depset(assets_infos, transitive = transitive_assets_infos),
+ native_libs_infos = depset(native_libs_infos, transitive = transitive_native_libs_infos),
+ manifest_infos = depset(manifest_infos, transitive = transitive_manifest_infos),
+ proguard_infos = depset(proguard_infos, transitive = transitive_proguard_infos),
+ )]
+
+fat_aar_aspect = aspect(
+ implementation = _fat_aar_aspect_impl,
+ attr_aspects = ["deps", "exports"],
+ provides = [FatAarInfo],
+)
diff --git a/rules/fat_aar/pom_from_fat_aar.bzl b/rules/fat_aar/pom_from_fat_aar.bzl
new file mode 100644
index 000000000..41151af24
--- /dev/null
+++ b/rules/fat_aar/pom_from_fat_aar.bzl
@@ -0,0 +1,179 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""POM generation for fat_aar targets.
+
+This rule generates a Maven POM file from a fat_aar's excluded dependencies.
+"""
+
+load("//rules/fat_aar:aspect.bzl", "FatAarDependenciesInfo")
+load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
+
+visibility(PROJECT_VISIBILITY)
+
+def _pom_from_fat_aar_impl(ctx):
+ """Generates a Maven POM file from a fat_aar's excluded dependencies.
+
+ Args:
+ ctx: The context.
+
+ Returns:
+ DefaultInfo with the generated POM file.
+ """
+ pom_file = ctx.actions.declare_file(ctx.label.name + ".pom")
+
+ excluded_labels = ctx.attr.fat_aar[FatAarDependenciesInfo].excluded_labels.to_list()
+
+ # Build a mapping from Bazel label to Maven coordinate
+ # maven_coords format: "group:artifact:type:version"
+ label_to_coord = {}
+ for coord in ctx.attr.maven_coords:
+ parts = coord.split(":")
+ if len(parts) >= 2:
+ group = parts[0]
+ artifact = parts[1]
+ # Convert to Bazel label format
+ label_name = "{}_{}".format(group, artifact).replace(".", "_").replace("-", "_")
+ maven_label = "@maven//:{}".format(label_name)
+ label_to_coord[maven_label] = coord
+
+ # Match excluded labels to Maven coordinates
+ matched_coords = {}
+ for label in excluded_labels:
+ label_str = str(label)
+ # Handle both @maven// and @@maven// formats
+ if "@maven//" not in label_str:
+ continue
+
+ # Extract the target name after the last ':'
+ if ":" not in label_str:
+ continue
+ target = label_str.split(":")[-1]
+
+ # Skip special targets
+ if target.startswith("jarinfer_") or target.startswith("proguard_") or target.startswith("v1"):
+ continue
+
+ # Try to find matching coordinate
+ check_label = "@maven//:{}".format(target)
+ if check_label in label_to_coord:
+ coord = label_to_coord[check_label]
+ matched_coords[coord] = True
+
+ # Generate POM dependencies XML
+ dependencies_xml = ""
+ for coord in sorted(matched_coords.keys()):
+ parts = coord.split(":")
+ classifier = None
+ if len(parts) == 3:
+ # Format: group:artifact:version or group:artifact:version@type
+ group_id = parts[0]
+ artifact_id = parts[1]
+ version_part = parts[2]
+ if "@" in version_part:
+ version, packaging = version_part.split("@", 1)
+ else:
+ version = version_part
+ packaging = None
+ elif len(parts) == 4:
+ # Format: group:artifact:type:version
+ group_id = parts[0]
+ artifact_id = parts[1]
+ packaging = parts[2]
+ version = parts[3]
+ elif len(parts) == 5:
+ # Format: group:artifact:type:classifier:version
+ group_id = parts[0]
+ artifact_id = parts[1]
+ packaging = parts[2]
+ classifier = parts[3]
+ version = parts[4]
+ else:
+ continue
+
+ classifier_xml = ""
+ if classifier:
+ classifier_xml = " {classifier}\n".format(classifier = classifier)
+
+ packaging_xml = ""
+ if packaging:
+ packaging_xml = " {packaging}\n".format(packaging = packaging)
+
+ dependencies_xml += """
+ {group_id}
+ {artifact_id}
+ {version}
+{packaging_xml}{classifier_xml} compile
+
+""".format(group_id = group_id, artifact_id = artifact_id, version = version, packaging_xml = packaging_xml, classifier_xml = classifier_xml)
+
+ pom_content = """
+
+ 4.0.0
+
+ {group_id}
+ {artifact_id}
+ {version}
+ aar
+
+
+{dependencies}
+
+""".format(
+ group_id = ctx.attr.group_id,
+ artifact_id = ctx.attr.artifact_id,
+ version = ctx.attr.version,
+ dependencies = dependencies_xml,
+ )
+
+ ctx.actions.write(
+ output = pom_file,
+ content = pom_content,
+ )
+
+ return [
+ DefaultInfo(
+ files = depset([pom_file]),
+ ),
+ ]
+
+fat_aar_pom = rule(
+ implementation = _pom_from_fat_aar_impl,
+ attrs = {
+ "fat_aar": attr.label(
+ mandatory = True,
+ providers = [FatAarDependenciesInfo],
+ doc = "The fat_aar target to generate POM for",
+ ),
+ "maven_coords": attr.string_list(
+ mandatory = True,
+ doc = "List of all Maven coordinates to match against (e.g., from maven_artifacts.bzl)",
+ ),
+ "group_id": attr.string(
+ mandatory = True,
+ doc = "Maven group ID for the POM",
+ ),
+ "artifact_id": attr.string(
+ mandatory = True,
+ doc = "Maven artifact ID for the POM",
+ ),
+ "version": attr.string(
+ mandatory = True,
+ doc = "Maven version for the POM",
+ ),
+ },
+ doc = "Generates a Maven POM file from a fat_aar's excluded dependencies by looking them up in the provided maven_coords list.",
+)
diff --git a/rules/fat_aar/rule.bzl b/rules/fat_aar/rule.bzl
new file mode 100644
index 000000000..e7afefa8a
--- /dev/null
+++ b/rules/fat_aar/rule.bzl
@@ -0,0 +1,458 @@
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# 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.
+
+"""fat_aar rule implementation."""
+
+load("@rules_java//java/common:java_common.bzl", "java_common")
+load("@rules_java//java/common:java_info.bzl", "JavaInfo")
+load("@rules_java//java/common:proguard_spec_info.bzl", "ProguardSpecInfo")
+load("//rules:busybox.bzl", _busybox = "busybox")
+load("//rules:common.bzl", _common = "common")
+load("//rules:java.bzl", "java")
+load("//providers:providers.bzl", "AndroidAssetsInfo", "AndroidNativeLibsInfo", "AndroidResourcesInfo", "StarlarkAndroidResourcesInfo")
+load("//rules:resources.bzl", _resources = "resources")
+load("//rules:utils.bzl", "get_android_sdk", "get_android_toolchain", "utils")
+load("//rules:visibility.bzl", "PROJECT_VISIBILITY")
+load("//rules/fat_aar:aspect.bzl", "FatAarDependenciesInfo", "FatAarInfo", "fat_aar_aspect")
+
+visibility(PROJECT_VISIBILITY)
+
+def _fat_aar_impl(ctx):
+ """Bundles transitive android_library deps into a single AAR.
+
+ When working with modular Android projects, publishing individual libraries can become
+ unwieldy as the number of modules grows. The fat_aar rule consolidates multiple internal
+ android_library targets into a unified AAR package, providing several advantages:
+
+ - Dependency management: Consumers depend on a single AAR instead of managing multiple
+ library versions, reducing integration complexity
+ - Size optimization: Bundling libraries together can enable better code shrinking and
+ result in a more compact final artifact
+ - Encapsulation: Internal module structure remains hidden, giving library authors greater
+ flexibility to refactor without impacting external consumers
+
+ Args:
+ ctx: The context.
+
+ Returns:
+ A list of providers.
+ """
+
+ # Collect and merge JavaInfo providers
+ all_java_infos = utils.collect_providers(JavaInfo, ctx.attr.deps)
+ merged_java_info = java_common.merge(all_java_infos)
+
+ # Helper function to check if label or file should be excluded
+ def should_exclude_label(label):
+ label_str = str(label)
+ for exclude_pattern in ctx.attr.exclude:
+ if exclude_pattern in label_str:
+ return True
+ return False
+
+ def should_exclude_file(file):
+ # Check file path for external repo markers
+ if not hasattr(file, "path"):
+ return False
+ path = file.path
+ for exclude_pattern in ctx.attr.exclude:
+ # Fast check: external files have paths like "external/maven/..."
+ if exclude_pattern.startswith("@"):
+ # Strip all leading @ (handles both @repo and @@repo bzlmod syntax)
+ repo_name = exclude_pattern.lstrip("@").rstrip("/")
+ # WORKSPACE format: external//
+ if "external/" + repo_name + "/" in path:
+ return True
+ # Old bzlmod format: ~/
+ if "~" + repo_name + "/" in path:
+ return True
+ # Bzlmod canonical format: ++/ (e.g. +maven_repos+maven/)
+ if "+" + repo_name + "/" in path:
+ return True
+ return False
+
+ # Collect Android providers from aspect (now as (label, provider) tuples)
+ # Track excluded labels for POM generation
+ all_resource_infos = []
+ all_assets_infos = []
+ all_native_libs_infos = []
+ all_manifest_infos = []
+ all_proguard_infos = []
+ excluded_labels = []
+
+ for dep in ctx.attr.deps:
+ if FatAarInfo not in dep:
+ continue
+ # Filter based on exclude patterns
+ for label, info in dep[FatAarInfo].resource_infos.to_list():
+ if should_exclude_label(label):
+ excluded_labels.append(label)
+ else:
+ all_resource_infos.append(info)
+ for label, info in dep[FatAarInfo].assets_infos.to_list():
+ if should_exclude_label(label):
+ excluded_labels.append(label)
+ else:
+ all_assets_infos.append(info)
+ for label, info in dep[FatAarInfo].native_libs_infos.to_list():
+ if should_exclude_label(label):
+ excluded_labels.append(label)
+ else:
+ all_native_libs_infos.append(info)
+ for label, manifest in dep[FatAarInfo].manifest_infos.to_list():
+ if should_exclude_label(label):
+ excluded_labels.append(label)
+ else:
+ all_manifest_infos.append(manifest)
+ for label, info in dep[FatAarInfo].proguard_infos.to_list():
+ if should_exclude_label(label):
+ excluded_labels.append(label)
+ else:
+ all_proguard_infos.append(info)
+
+ # Extract transitive data from collected providers
+ # Handle both AndroidResourcesInfo and StarlarkAndroidResourcesInfo
+ transitive_resource_files = []
+ transitive_assets = []
+ transitive_manifests = []
+ transitive_r_txts = []
+
+ for info in all_resource_infos:
+ if type(info) == "StarlarkAndroidResourcesInfo" or hasattr(info, "transitive_resource_files"):
+ transitive_resource_files.append(info.transitive_resource_files)
+ transitive_manifests.append(info.transitive_manifests)
+ transitive_r_txts.append(info.transitive_r_txts)
+ if hasattr(info, "transitive_assets"):
+ transitive_assets.append(info.transitive_assets)
+ elif hasattr(info, "transitive_resources"):
+ transitive_resource_files.append(info.transitive_resources)
+ transitive_manifests.append(info.transitive_manifests)
+ transitive_r_txts.append(info.transitive_aapt2_r_txt)
+
+ for info in all_assets_infos:
+ transitive_assets.append(info.assets)
+
+ # Use depset filtering - only convert to list at the end if needed
+ if ctx.attr.exclude:
+ # Filter files lazily
+ all_resource_files = depset(transitive = transitive_resource_files).to_list()
+ resource_files = [f for f in all_resource_files if not should_exclude_file(f)]
+
+ all_assets = depset(transitive = transitive_assets).to_list()
+ assets = [f for f in all_assets if not should_exclude_file(f)]
+
+ all_manifests_from_providers = depset(transitive = transitive_manifests).to_list()
+ filtered_manifests = [f for f in all_manifests_from_providers if not should_exclude_file(f)]
+ manifests = filtered_manifests + all_manifest_infos
+
+ all_r_txts = depset(transitive = transitive_r_txts).to_list()
+ r_txts = [f for f in all_r_txts if not should_exclude_file(f)]
+ else:
+ # No filtering - use depsets directly
+ resource_files = depset(transitive = transitive_resource_files).to_list()
+ assets = depset(transitive = transitive_assets).to_list()
+ manifests = depset(transitive = transitive_manifests).to_list() + all_manifest_infos
+ r_txts = depset(transitive = transitive_r_txts).to_list()
+
+ assets_dir = "assets" if assets else None
+
+ # Merge transitive manifests
+ merged_manifest = ctx.actions.declare_file(ctx.label.name + "_merged/AndroidManifest.xml")
+
+ if not manifests:
+ ctx.actions.write(
+ merged_manifest,
+ content = """
+
+
+""",
+ )
+ elif len(manifests) == 1:
+ ctx.actions.run_shell(
+ inputs = manifests,
+ outputs = [merged_manifest],
+ command = "cp $1 $2",
+ arguments = [manifests[0].path, merged_manifest.path],
+ mnemonic = "CopyManifest",
+ )
+ else:
+ primary_manifest = ctx.actions.declare_file(ctx.label.name + "_primary/AndroidManifest.xml")
+ ctx.actions.write(
+ primary_manifest,
+ content = """
+
+
+
+
+""".format(min_sdk = ctx.attr.min_sdk_version),
+ )
+
+ merge_log = ctx.actions.declare_file(ctx.label.name + "_merged/manifest_merger_log.txt")
+
+ # Use APPLICATION merge type (same as android_binary)
+ _busybox.merge_manifests(
+ ctx,
+ out_file = merged_manifest,
+ out_log_file = merge_log,
+ merge_type = "APPLICATION",
+ manifest = primary_manifest,
+ mergee_manifests = depset(manifests),
+ manifest_merge_order = "dependency",
+ manifest_values = {},
+ java_package = None,
+ busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
+ host_javabase = _common.get_host_javabase(ctx),
+ )
+
+ # Merge transitive runtime JARs into single classes.jar
+ # Filter out excluded dependencies (e.g., external Maven dependencies)
+ merged_class_jar = ctx.actions.declare_file(ctx.label.name + "_classes.jar")
+
+ all_jars = merged_java_info.transitive_runtime_jars.to_list()
+ filtered_jars = []
+
+ for jar in all_jars:
+ # Check if jar should be excluded based on exclude patterns
+ owner_str = str(jar.owner) if jar.owner else ""
+ should_exclude = False
+ for exclude_pattern in ctx.attr.exclude:
+ if exclude_pattern in owner_str:
+ should_exclude = True
+ excluded_labels.append(jar.owner)
+ break
+ if should_exclude:
+ continue
+ filtered_jars.append(jar)
+
+ args = ctx.actions.args()
+ args.add("--output", merged_class_jar)
+ args.add("--dont_change_compression")
+ args.add("--normalize")
+
+ for jar in filtered_jars:
+ args.add("--sources", jar)
+
+ java_toolchain = _common.get_java_toolchain(ctx)
+ ctx.actions.run(
+ executable = java_toolchain[java_common.JavaToolchainInfo].single_jar,
+ arguments = [args],
+ inputs = depset(filtered_jars),
+ outputs = [merged_class_jar],
+ mnemonic = "MergeLibraryJars",
+ progress_message = "Merging transitive library jars",
+ )
+
+ r_txt = r_txts[0] if r_txts else None
+ if not r_txt:
+ r_txt = ctx.actions.declare_file(ctx.label.name + "_R.txt")
+ ctx.actions.write(r_txt, content = "")
+
+ proguard_specs = []
+ for spec_info in all_proguard_infos:
+ all_specs = spec_info.specs.to_list()
+ proguard_specs.extend([s for s in all_specs if not should_exclude_file(s)])
+
+ # Run R8 optimization if r8_config is provided
+ if ctx.file.r8_config:
+ class_jar = ctx.actions.declare_file(ctx.label.name + "_optimized_classes.jar")
+
+ # Collect all ProGuard configs: user-provided + transitive deps
+ all_proguard_configs = [ctx.file.r8_config] + proguard_specs
+
+ # Get android_jar from SDK (same as android_binary)
+ android_jar = get_android_sdk(ctx).android_jar
+
+ # R8 command line arguments
+ # Use --classfile to output .class files instead of .dex
+ # Note: --min-api is not supported with --classfile
+ r8_args = ctx.actions.args()
+ r8_args.add("--classfile")
+ r8_args.add("--lib", android_jar)
+ r8_args.add("--release")
+ r8_args.add("--output", class_jar)
+
+ # Add all ProGuard config files
+ for config in all_proguard_configs:
+ r8_args.add("--pg-conf", config)
+
+ r8_args.add(merged_class_jar)
+
+ java.run(
+ ctx = ctx,
+ host_javabase = _common.get_host_javabase(ctx),
+ executable = get_android_toolchain(ctx).r8.files_to_run,
+ arguments = [r8_args],
+ inputs = [merged_class_jar, android_jar] + all_proguard_configs,
+ outputs = [class_jar],
+ mnemonic = "R8Optimize",
+ progress_message = "Optimizing classes with R8",
+ )
+ else:
+ class_jar = merged_class_jar
+
+ fat_aar_java_info = JavaInfo(
+ output_jar = class_jar,
+ compile_jar = class_jar,
+ deps = all_java_infos,
+ )
+
+ all_native_libs = []
+ for info in all_native_libs_infos:
+ all_native_libs.extend(info.native_libs.to_list())
+ native_libs_files = [f for f in all_native_libs if not should_exclude_file(f)]
+
+ aar = ctx.actions.declare_file(ctx.label.name + ".aar")
+ if native_libs_files:
+ # Create base AAR then add native libs via script
+ base_aar = ctx.actions.declare_file(ctx.label.name + "_base.aar")
+ _busybox.make_aar(
+ ctx,
+ out_aar = base_aar,
+ assets = assets,
+ assets_dir = assets_dir,
+ resource_files = resource_files,
+ class_jar = class_jar,
+ r_txt = r_txt,
+ manifest = merged_manifest,
+ proguard_specs = proguard_specs,
+ busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
+ host_javabase = _common.get_host_javabase(ctx),
+ )
+
+ temp_dir = ctx.actions.declare_directory(ctx.label.name + "_temp_native_libs")
+
+ args = ctx.actions.args()
+ args.add(base_aar)
+ args.add(aar)
+ args.add(temp_dir.path)
+ args.add_all(native_libs_files)
+
+ ctx.actions.run_shell(
+ inputs = [base_aar, ctx.file._add_native_libs_script] + native_libs_files,
+ outputs = [aar, temp_dir],
+ command = "bash $1 ${@:2}",
+ arguments = [ctx.file._add_native_libs_script.path, args],
+ mnemonic = "AddNativeLibsToAAR",
+ progress_message = "Adding native libraries to AAR",
+ )
+ else:
+ aar = _resources.make_aar(
+ ctx,
+ assets = assets,
+ assets_dir = assets_dir,
+ resource_files = resource_files,
+ class_jar = class_jar,
+ r_txt = r_txt,
+ manifest = merged_manifest,
+ proguard_specs = proguard_specs,
+ busybox = get_android_toolchain(ctx).android_resources_busybox.files_to_run,
+ host_javabase = _common.get_host_javabase(ctx),
+ )
+
+ # Generate excluded dependencies file
+ # Just output the raw excluded labels - consumers can filter/transform as needed
+ excluded_deps_file = ctx.actions.declare_file(ctx.label.name + "_excluded_deps.txt")
+
+ # Deduplicate labels
+ unique_labels = {}
+ for label in excluded_labels:
+ label_str = str(label)
+ if label_str not in unique_labels:
+ unique_labels[label_str] = True
+
+ content = ""
+ for label_str in sorted(unique_labels.keys()):
+ content += label_str + "\n"
+
+ ctx.actions.write(
+ output = excluded_deps_file,
+ content = content,
+ )
+
+ return [
+ DefaultInfo(
+ files = depset([aar]),
+ ),
+ OutputGroupInfo(
+ aar = depset([aar]),
+ class_jar = depset([class_jar]),
+ manifest = depset([merged_manifest]),
+ excluded_deps = depset([excluded_deps_file]),
+ ),
+ fat_aar_java_info,
+ StarlarkAndroidResourcesInfo(
+ direct_resources_nodes = depset(),
+ transitive_resources_nodes = depset(),
+ transitive_assets = depset(assets),
+ transitive_assets_symbols = depset(),
+ transitive_compiled_assets = depset(),
+ direct_compiled_resources = depset(),
+ transitive_compiled_resources = depset(),
+ transitive_manifests = depset([merged_manifest]),
+ transitive_r_txts = depset([r_txt]),
+ transitive_resource_files = depset(resource_files),
+ packages_to_r_txts = {},
+ transitive_resource_apks = depset(),
+ ),
+ AndroidNativeLibsInfo(
+ native_libs = depset(native_libs_files),
+ ),
+ FatAarDependenciesInfo(
+ excluded_labels = depset(excluded_labels),
+ ),
+ ]
+
+fat_aar = rule(
+ implementation = _fat_aar_impl,
+ attrs = {
+ "deps": attr.label_list(
+ aspects = [fat_aar_aspect],
+ doc = "The list of android_library targets to bundle",
+ ),
+ "min_sdk_version": attr.string(
+ default = "23",
+ doc = "Minimum SDK version for the primary manifest",
+ ),
+ "exclude": attr.string_list(
+ default = [],
+ doc = "List of patterns to exclude from bundling (e.g., ['@maven//'])",
+ ),
+ "r8_config": attr.label(
+ allow_single_file = [".pro", ".txt"],
+ doc = "ProGuard configuration file for R8. If provided, R8 optimization is enabled. Combined with transitive ProGuard specs from dependencies.",
+ ),
+ "_add_native_libs_script": attr.label(
+ default = Label("//rules/fat_aar:add_native_libs.sh"),
+ allow_single_file = True,
+ ),
+ "_java_toolchain": attr.label(
+ default = Label("@bazel_tools//tools/jdk:current_java_toolchain"),
+ ),
+ "_host_javabase": attr.label(
+ default = Label("@bazel_tools//tools/jdk:current_host_java_runtime"),
+ cfg = "exec",
+ ),
+ },
+ toolchains = [
+ config_common.toolchain_type("@rules_android//toolchains/android:toolchain_type", mandatory = False),
+ config_common.toolchain_type("//toolchains/android:toolchain_type", mandatory = False),
+ config_common.toolchain_type("@rules_android//toolchains/android_sdk:toolchain_type", mandatory = False),
+ config_common.toolchain_type("//toolchains/android_sdk:toolchain_type", mandatory = False),
+ ],
+ fragments = ["android", "bazel_android", "java"],
+ doc = "Bundles transitive android_library dependencies into a single AAR file.",
+)
diff --git a/rules/rules.bzl b/rules/rules.bzl
index af06e80e2..5a8245cc1 100644
--- a/rules/rules.bzl
+++ b/rules/rules.bzl
@@ -69,6 +69,14 @@ load(
"//rules/android_sdk_repository:rule.bzl",
_android_sdk_repository = "android_sdk_repository",
)
+load(
+ "//rules/fat_aar:rule.bzl",
+ _fat_aar = "fat_aar",
+)
+load(
+ "//rules/fat_aar:pom_from_fat_aar.bzl",
+ _fat_aar_pom = "fat_aar_pom",
+)
# Current version. Tools may check this to determine compatibility.
RULES_ANDROID_VERSION = "0.1.0"
@@ -84,6 +92,8 @@ android_sdk = _android_sdk
android_sdk_repository = _android_sdk_repository
android_tools_defaults_jar = _android_tools_defaults_jar
asar_import = _asar_import
+fat_aar = _fat_aar
+fat_aar_pom = _fat_aar_pom
instrumented_app_info_aspect = _instrumented_app_info_aspect
StarlarkApkInfo = _StarlarkApkInfo
ApkInfo = _ApkInfo