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