diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1efc5d9..7d75f5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,3 @@ -# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions -# Renaming ? Change the README badge. name: Build on: push: @@ -11,9 +9,14 @@ jobs: name: Base Checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 1.8 - - name: Perform base checks - run: ./gradlew build publishToDirectory \ No newline at end of file + java-version: 17 + distribution: temurin + cache: gradle + - uses: gradle/actions/wrapper-validation@v4 + - name: Check local deployment + run: ./gradlew build deployLocal + - name: Check sample app + run: cd tests && ../gradlew sample-library:assembleDebug \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 436687d..ed70656 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,17 +3,24 @@ on: release: types: [published] jobs: - BINTRAY_UPLOAD: - name: Bintray Upload + DEPLOY: + name: GitHub and Maven Central publication runs-on: ubuntu-latest env: - BINTRAY_USER: ${{ secrets.BINTRAY_USER }} - BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }} - BINTRAY_REPO: ${{ secrets.BINTRAY_REPO }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GHUB_USER: ${{ secrets.GHUB_USER }} + GHUB_PERSONAL_ACCESS_TOKEN: ${{ secrets.GHUB_PERSONAL_ACCESS_TOKEN }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: 1.8 - - name: Publish to Bintray - run: ./gradlew publishToBintray + java-version: 17 + distribution: temurin + cache: gradle + - name: Publish to Maven Central + run: ./gradlew deployNexus + - name: Publish to GitHub Packages + run: ./gradlew deployGithub \ No newline at end of file diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 0000000..24603f3 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,23 @@ +name: Snapshot +on: + push: + branches: + - main +jobs: + SNAPSHOT: + name: Publish Snapshot + runs-on: ubuntu-latest + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_USER: ${{ secrets.SONATYPE_USER }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Publish Nexus Snapshot + run: ./gradlew deployNexusSnapshot \ No newline at end of file diff --git a/.gitignore b/.gitignore index 603b140..cbef6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +**/local.properties +**/.idea/ .DS_Store /build /captures diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 976bc20..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 61a9130..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 02cb845..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 1d6cae2..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinScripting.xml b/.idea/kotlinScripting.xml deleted file mode 100644 index a6fe551..0000000 --- a/.idea/kotlinScripting.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml deleted file mode 100644 index 12fb99d..0000000 --- a/.idea/markdown-navigator-enh.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml deleted file mode 100644 index 4463382..0000000 --- a/.idea/markdown-navigator.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 3e91cc6..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - 1.8 - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Grease_grease__publishAllDirectory_.xml b/.idea/runConfigurations/Grease_grease__publishAllDirectory_.xml deleted file mode 100644 index 373b61a..0000000 --- a/.idea/runConfigurations/Grease_grease__publishAllDirectory_.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - true - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e57478 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 DeepMedia Srl + + 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. \ No newline at end of file diff --git a/README.md b/README.md index 2c1486f..66c3381 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,30 @@ -[![Build Status](https://github.com/deepmedia/Grease/workflows/Build/badge.svg?event=push)](https://github.com/deepmedia/Grease/actions) +[![Build Status](https://github.com/deepmedia/Grease/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/deepmedia/Grease/actions) [![Release](https://img.shields.io/github/release/deepmedia/Grease.svg)](https://github.com/deepmedia/Grease/releases) [![Issues](https://img.shields.io/github/issues-raw/deepmedia/Grease.svg)](https://github.com/deepmedia/Grease/issues) # Grease A Gradle plugin for creating fat AARs, useful for distributing multiple modules in a single file. -To install the plugin, you must configure the build script classpath: +To install the plugin, apply it to your Android project: ```kotlin -buildscript { +// settings.gradle.kts +pluginManagement { repositories { - jcenter() - google() - } - dependencies { - classpath("io.deepmedia.tools:grease:0.2.0") + mavenCentral() } } -``` -## Usage +// build.gradle.kts +plugins { + id("com.android.library") + id("io.deepmedia.tools.grease") version "0.3.0" +} +``` -To apply the plugin, declare it in your build script with the `io.deepmedia.tools.grease` id. -This must be done after the com.android.library plugin: +Note: it is important that Grease is applied *after* the Android library plugin. -```groovy -apply plugin: 'com.android.library' -apply plugin: 'io.deepmedia.tools.grease' -``` +## Usage Once applied, Grease will create Gradle configurations that you can use to select which of your dependencies should be bundled in the final fat AAR. @@ -61,19 +58,21 @@ dependencies that you own, to be sure that they won't be present in the classpat ### Transitivity -When you add a grease dependency, by default all transitive dependencies are greased as well, so -they will become part of the fat AARs. To avoid this, you can mark the configuration as non transitive: +By default, declaring a specific grease dependency **will not** include transitive dependencies. +To do so, you can use grease configurations ending in `Tree`: ```kotlin -configurations["grease"].isTransitive = false -configurations["greaseRelease"].isTransitive = false -configurations["greaseDebug"].isTransitive = false - -// Variant specific configurations are created lazily so you must wait for them to be -// available before modifying them. -configurations.configureEach { - if (name == "greaseBlueCircleDebug") { - isTransitive = false - } +dependencies { + grease("my-package:artifact:1.0.0") // not transitive + greaseTree("my-other-package:other-artifact:1.0.0") // transitive } -``` \ No newline at end of file +``` + +### Relocations + +We support relocations through the popular [Shadow](https://github.com/GradleUp/shadow) plugin. Inside the `grease` +extension, you can use: + +- `relocate(prefix: String)` to relocate all included packages by prefixing them with the given prefix. Defaults to `"grease"`. +- `relocate(from: String, to: String, configure: Action)` to register a package-specific relocator, + as described in the [shadow plugin docs](https://gradleup.com/shadow/configuration/relocation/#relocating-packages) diff --git a/build.gradle.kts b/build.gradle.kts index d664f12..e20df22 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,31 +1,4 @@ -buildscript { - repositories { - maven("build/maven") - maven("https://dl.bintray.com/deepmedia/tools/") - google() - jcenter() - } - - configurations.configureEach { - resolutionStrategy.cacheChangingModulesFor(10, TimeUnit.SECONDS) - } - - dependencies { - classpath("io.deepmedia.tools:publisher:0.4.0") - classpath("io.deepmedia.tools:grease:0.2.0") { - isChanging = true - } - } -} - -allprojects { - repositories { - mavenCentral() - google() - jcenter() - } -} - -tasks.register("clean", Delete::class) { - delete(buildDir) -} +plugins { + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.jvm) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 23339e0..83df5e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..6d8fa58 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,23 @@ +[versions] +agp = "8.1.4" +asm-commons = "9.6" +android-tools = "31.1.4" +kotlin = "2.0.0" +shadow = "8.3.0" +publisher = "0.14.0" + +[libraries] +asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm-commons" } +gradle-android-common = { module = "com.android.tools:common", version.ref = "android-tools" } +gradle-android-sdk-common = { module = "com.android.tools:sdk-common", version.ref = "android-tools" } +gradle-android-layoutlib = { module = "com.android.tools.layoutlib:layoutlib-api", version.ref = "android-tools" } +gradle-android-build = { module = "com.android.tools.build:gradle", version.ref = "agp" } +gradle-shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" } + +[bundles] +gradle-android = ["gradle-android-sdk-common", "gradle-android-build", "gradle-android-common", "gradle-android-layoutlib"] + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +publisher = { id = "io.deepmedia.tools.deployer", version.ref = "publisher" } +android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961f..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e47231f..8838ba9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Mar 18 11:56:36 BRT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip diff --git a/gradlew b/gradlew index cccdd3d..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,130 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +133,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,22 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,25 +27,29 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -35,48 +57,36 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/grease/build.gradle.kts b/grease/build.gradle.kts index bc2aacc..1f7f6ab 100644 --- a/grease/build.gradle.kts +++ b/grease/build.gradle.kts @@ -1,39 +1,78 @@ -import io.deepmedia.tools.publisher.common.* - plugins { `kotlin-dsl` - id("io.deepmedia.tools.publisher") + alias(libs.plugins.publisher) +} + +group = "io.deepmedia.tools" +version = "0.3.0" + +gradlePlugin { + plugins { + create("grease") { + id = "io.deepmedia.tools.grease" + implementationClass = "io.deepmedia.tools.grease.GreasePlugin" + } + } } dependencies { - api("com.android.tools.build:gradle:4.1.1") // android gradle plugin - api(gradleApi()) // gradle - api(gradleKotlinDsl()) // not sure if needed - api(localGroovy()) // groovy + implementation(libs.asm.commons) + implementation(libs.gradle.shadow) + implementation(libs.bundles.gradle.android) } -publisher { - project.name = "Grease" - project.artifact = "grease" - project.description = "Fat AARs for Android." - project.group = "io.deepmedia.tools" - project.url = "https://github.com/deepmedia/Grease" - project.vcsUrl = "https://github.com/deepmedia/Grease.git" - release.version = "0.2.0" - release.sources = Release.SOURCES_AUTO - release.docs = Release.DOCS_AUTO +deployer { + content { + gradlePluginComponents { + kotlinSources() + emptyDocs() + } + } + + projectInfo { + description = "Fat AARs for Android, to distribute multiple library modules in a single file with no dependencies, with relocation support." + url = "https://github.com/deepmedia/Grease" + scm.fromGithub("deepmedia", "Grease") + developer("Mattia Iavarone", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io") + license(apache2) + } + + signing { + key = secret("SIGNING_KEY") + password = secret("SIGNING_PASSWORD") + } + + // use "deployLocal" to deploy to local maven repository + localSpec { + directory.set(rootProject.layout.buildDirectory.get().dir("inspect")) + signing { + key = absent() + password = absent() + } + } - bintray { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" + // use "deployNexus" to deploy to OSSRH / maven central + nexusSpec { + auth.user = secret("SONATYPE_USER") + auth.password = secret("SONATYPE_PASSWORD") + syncToMavenCentral = true } - directory { - directory = "../build/maven" + // use "deployNexusSnapshot" to deploy to sonatype snapshots repo + nexusSpec("snapshot") { + auth.user = secret("SONATYPE_USER") + auth.password = secret("SONATYPE_PASSWORD") + repositoryUrl = ossrhSnapshots1 + release.version = "latest-SNAPSHOT" } - directory("shared") { - directory = file(repositories.mavenLocal().url).absolutePath + // use "deployGithub" to deploy to github packages + githubSpec { + repository = "Grease" + owner = "deepmedia" + auth { + user = secret("GHUB_USER") + token = secret("GHUB_PERSONAL_ACCESS_TOKEN") + } } } \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/GreaseExtension.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreaseExtension.kt new file mode 100644 index 0000000..bb2ce2b --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreaseExtension.kt @@ -0,0 +1,33 @@ +package io.deepmedia.tools.grease + +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer +import org.gradle.api.Action +import org.gradle.api.model.ObjectFactory +import org.gradle.kotlin.dsl.listProperty +import org.gradle.kotlin.dsl.newInstance +import org.gradle.kotlin.dsl.property +import javax.inject.Inject + +abstract class GreaseExtension @Inject constructor(objects: ObjectFactory) { + + internal val prefix = objects.property().convention("") + internal val relocators = objects.listProperty() + internal val transformers = objects.listProperty() + + fun relocate(prefix: String = "grease") { + this.prefix.set(prefix) + } + + fun relocate(from: String, to: String, configure: Action = Action { }) { + val relocator = SimpleRelocator(from, to, emptyList(), emptyList()) + configure.execute(relocator) + relocators.add(relocator) + } + + fun transform(transformer: T, configure: Action = Action { }) { + configure.execute(transformer) + transformers.add(transformer) + } +} \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt index 0bda43a..b23770d 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreasePlugin.kt @@ -2,28 +2,44 @@ package io.deepmedia.tools.grease +import com.android.build.api.component.analytics.AnalyticsEnabledLibraryVariant +import com.android.build.api.component.analytics.AnalyticsEnabledVariant +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant +import com.android.build.api.variant.impl.getApiString import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.api.LibraryVariant -import com.android.build.gradle.api.LibraryVariantOutput import com.android.build.gradle.internal.LibraryTaskManager import com.android.build.gradle.internal.LoggerWrapper import com.android.build.gradle.internal.TaskManager +import com.android.build.gradle.internal.component.ComponentCreationConfig +import com.android.build.gradle.internal.manifest.parseManifest import com.android.build.gradle.internal.publishing.AndroidArtifacts import com.android.build.gradle.internal.res.GenerateLibraryRFileTask import com.android.build.gradle.internal.res.ParseLibraryResourcesTask import com.android.build.gradle.internal.scope.InternalArtifactType import com.android.build.gradle.internal.scope.MutableTaskContainer -import com.android.build.gradle.internal.tasks.* +import com.android.build.gradle.internal.tasks.LibraryJniLibsTask +import com.android.build.gradle.internal.tasks.MergeFileTask +import com.android.build.gradle.internal.tasks.Workers import com.android.build.gradle.internal.tasks.factory.TaskCreationAction -import com.android.build.gradle.internal.tasks.manifest.mergeManifestsForApplication -import com.android.build.gradle.tasks.* +import com.android.build.gradle.internal.tasks.manifest.mergeManifests +import com.android.build.gradle.tasks.BundleAar +import com.android.build.gradle.tasks.MergeResources +import com.android.build.gradle.tasks.ProcessLibraryManifest +import com.android.builder.errors.DefaultIssueReporter +import com.android.ide.common.resources.CopyToOutputDirectoryResourceCompilationService import com.android.manifmerger.ManifestMerger2 import com.android.manifmerger.ManifestProvider +import com.android.utils.StdLogger +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.Configuration -import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.support.unzipTo +import org.gradle.kotlin.dsl.support.zipTo +import java.io.File /** * Adds grease configurations for bundling dependencies in AAR files. @@ -36,7 +52,7 @@ import org.gradle.kotlin.dsl.get */ open class GreasePlugin : Plugin { - private val Project.greaseDir get() = buildDir.folder("grease") + private val defaultIssueReporter = DefaultIssueReporter(StdLogger(StdLogger.Level.WARNING)) @Suppress("NAME_SHADOWING") override fun apply(target: Project) { @@ -45,25 +61,36 @@ open class GreasePlugin : Plugin { } val log = Logger(target, "grease") val android = target.extensions["android"] as LibraryExtension - debugConfigurationHierarchy(target, log) + val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java) + val greaseExtension = target.extensions.create("grease", GreaseExtension::class.java) + + debugGreasyConfigurationHierarchy(target, log) // Create the configurations. - target.createRootConfiguration(log) - target.createProductFlavorConfigurations(android.productFlavors, log) - target.createBuildTypeConfigurations(android.buildTypes, log) - target.createVariantConfigurations(android.libraryVariants, log) + fun createConfigurations(isTransitive: Boolean) { + target.createRootConfiguration(isTransitive, log) + target.createProductFlavorConfigurations(androidComponents, isTransitive, log) + target.createBuildTypeConfigurations(android.buildTypes, isTransitive, log) + target.createVariantConfigurations(androidComponents, isTransitive, log) + } + createConfigurations(false) + createConfigurations(true) + fun configure(variant: Variant, vararg configurations: Configuration) { + configureVariantManifest(target, variant, configurations, log) + configureVariantJniLibs(target, variant, configurations, log) + configureVariantResources(target, variant, configurations, log) + configureVariantSources(target, variant, configurations, greaseExtension, log) + configureVariantAssets(target, variant, configurations, log) + configureVariantProguardFiles(target, variant, configurations, log) + } // Configure all variants. - android.libraryVariants.configureEach { + androidComponents.onVariants { variant -> val log = log.child("configureVariant") - log.i { "Configuring variant ${this.name}..." } - val configuration = target.greaseOf(this) - configureVariantManifest(target, this, configuration, log) - configureVariantJniLibs(target, this, configuration, log) - configureVariantResources(target, this, configuration, log) - configureVariantSources(target, this, configuration, log) - configureVariantAssets(target, this, configuration, log) - configureVariantProguardFiles(target, this, configuration, log) + log.d { "Configuring variant ${variant.name}..." } + target.afterEvaluate { + configure(variant, target.greaseOf(variant), target.greaseOf(variant, true)) + } } } @@ -89,79 +116,84 @@ open class GreasePlugin : Plugin { */ private fun configureVariantManifest( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, logger: Logger ) { val log = logger.child("configureVariantManifest") - variant.outputs.configureEach { - val variantOutput = this - variantOutput as LibraryVariantOutput - log.i { "Configuring variant output ${variantOutput.name}..." } + log.d { "Configuring variant output ${variant.name}..." } + + val componentConfig = variant.componentCreationConfigOrThrow() + + target.locateTask(componentConfig.computeTaskName("process", "Manifest"))?.configure { + val processManifestTask = this as ProcessLibraryManifest - // cast ManifestProcessorTask to ProcessLibraryManifest - @Suppress("UNCHECKED_CAST") - val processManifestTask = processManifestProvider as TaskProvider + val extraManifests = configurations.artifactsOf(AndroidArtifacts.ArtifactType.MANIFEST) + dependsOn(extraManifests) // After the file is copied we can go on with the actual manifest merging. // This task will overwrite the original AndroidManifest.xml. - val reprocessManifestTask = target.tasks.register(processManifestTask.name.greasify()) { - dependsOn(processManifestTask) + doLast { + + val reportFile = target.greaseBuildDir.get().file("manifest_report.txt") + target.delete(reportFile) // To retrieve the secondary files, we must query the configuration artifacts. - val primaryManifest = processManifestTask.get().manifestOutputFile.asFile // overwrite - val secondaryManifests = configuration.artifactsOf(AndroidArtifacts.ArtifactType.MANIFEST) - inputs.file(primaryManifest) - inputs.files(secondaryManifests) - outputs.file(primaryManifest) - - doLast { - log.i { "Merging manifests... primary=${primaryManifest.get()}, secondary=${secondaryManifests.files.joinToString()}" } - mergeManifestsForApplication( - mainManifest = primaryManifest.get(), - /* Overlays are other manifests from the current 'source set' of this lib. */ - manifestOverlays = if (false) secondaryManifests.files.toList() else listOf(), - /* Dependencies are other manifests from other libraries, which should be our + val primaryManifest = processManifestTask.manifestOutputFile.asFile // overwrite + + @Suppress("DEPRECATION") + val mergedFlavor = componentConfig.oldVariantApiLegacySupport?.mergedFlavor + + log.d { "Merging manifests... primary=${primaryManifest.get()}, secondary=${extraManifests.files.joinToString()}" } + + mergeManifests( + mainManifest = primaryManifest.get(), + /* Overlays are other manifests from the current 'source set' of this lib. */ + manifestOverlays = if (false) extraManifests.files.toList() else listOf(), + /* Dependencies are other manifests from other libraries, which should be our * case but it's not clear if we can use them with the LIBRARY merge type. */ - dependencies = if (true) secondaryManifests.files.map { object : ManifestProvider { + dependencies = if (true) extraManifests.files.map { + object : ManifestProvider { override fun getManifest() = it override fun getName() = null - } } else listOf(), - /* Not sure what this is but it can be empty. */ - navigationJsons = listOf(), - /* Probably something about feature modules? Ignore */ - featureName = null, - /* Need to apply the libraryVariant package name */ - packageOverride = variant.applicationId, - /* Version data */ - /* The merged flavor represents all flavors plus the default config. */ - versionCode = variant.mergedFlavor.versionCode ?: 1, // Should we inspect the buildType as well? - versionName = variant.mergedFlavor.versionName ?: "", // Should we inspect the buildType as well? - minSdkVersion = variant.mergedFlavor.minSdkVersion?.apiString, - targetSdkVersion = variant.mergedFlavor.targetSdkVersion?.apiString, - maxSdkVersion = variant.mergedFlavor.maxSdkVersion, - /* The output destination */ - outMergedManifestLocation = primaryManifest.get().absolutePath, - /* Extra outputs that can probably be null. */ - outAaptSafeManifestLocation = null, - /* Either LIBRARY or APPLICATION. When using LIBRARY we can't add lib dependencies */ - mergeType = if (true) ManifestMerger2.MergeType.APPLICATION else ManifestMerger2.MergeType.LIBRARY, - /* Manifest placeholders. Doing this the way the library manifest does. */ - placeHolders = variant.mergedFlavor.manifestPlaceholders.also { - it.putAll(variant.buildType.manifestPlaceholders) - }, - /* Optional features to be enabled. */ - optionalFeatures = setOf(ManifestMerger2.Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT), - /* Not sure, but it's empty in the lib processor */ - dependencyFeatureNames = setOf(), - /* Output file with diagnostic info. I think. */ - reportFile = target.greaseDir.file("manifest_report.txt"), - /* Logging */ - logger = LoggerWrapper(target.logger) - ) - } + } + } else listOf(), + namespace = variant.namespace.get(), + /* Not sure what this is but it can be empty. */ + navigationJsons = listOf(), + /* Probably something about feature modules? Ignore */ + featureName = null, + /* Need to apply the libraryVariant package name */ + packageOverride = componentConfig.applicationId.get(), + /* Version data */ + /* The merged flavor represents all flavors plus the default config. */ + versionCode = mergedFlavor?.versionCode, + versionName = mergedFlavor?.versionName, + minSdkVersion = componentConfig.minSdk.getApiString(), + targetSdkVersion = mergedFlavor?.targetSdkVersion?.apiString, + maxSdkVersion = mergedFlavor?.maxSdkVersion, + testOnly = false, + extractNativeLibs = null, + generatedLocaleConfigAttribute = null, + profileable = false, + /* The output destination */ + outMergedManifestLocation = primaryManifest.get().absolutePath, + /* Extra outputs that can probably be null. */ + outAaptSafeManifestLocation = null, + /* Either LIBRARY or APPLICATION. When using LIBRARY we can't add lib dependencies */ + mergeType = ManifestMerger2.MergeType.FUSED_LIBRARY, + /* Manifest placeholders. Doing this the way the library manifest does. */ + placeHolders = mergedFlavor?.manifestPlaceholders.orEmpty() + variant.manifestPlaceholders.get(), + /* Optional features to be enabled. */ + optionalFeatures = setOf(ManifestMerger2.Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT), + /* Not sure, but it's empty in the lib processor */ + dependencyFeatureNames = setOf(), + /* Output file with diagnostic info. I think. */ + reportFile = reportFile.asFile, + /* Logging */ + logger = LoggerWrapper(target.logger) + ) } - processManifestTask.configure { finalizedBy(reprocessManifestTask) } } } @@ -185,53 +217,28 @@ open class GreasePlugin : Plugin { */ private fun configureVariantJniLibs( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, logger: Logger ) { val log = logger.child("configureVariantJniLibs") - log.i { "Configuring variant ${variant.name}..." } - - val anchorTaskName = nameOf("copy", variant.name, "JniLibsProjectAndLocalJars") - val extractorTask = target.tasks.register(anchorTaskName.greasify()) { - val anchorTask = target.tasks[anchorTaskName] as LibraryJniLibsTask - inputs.files(configuration.artifactsOf(AndroidArtifacts.ArtifactType.JNI)) - // Option 1: use the directory property. This fails, Gradle can't create this task - // because it is already the output directory of someone else. - // outputs.dir(anchorTask.outputDirectory) - // Option 2: resolve the directory. I think that in this case we MUST add a dependsOn() - // or we won't be sure that the directory is available. - dependsOn(anchorTask) - outputs.dir(anchorTask.outputDirectory.get()) - log.i { "Configured output directory to be ${anchorTask.outputDirectory.get()}." } - // Option 3: declare the exact files we'll be copying. This was valuable in my opinion, - // But this configuration is causing deadlocks. Asked about it here - // https://discuss.gradle.org/t/task-outputs-deadlock-how-to-declare-lazy-outputs-based-on-lazy-inputs/37107 - /* outputs.files(inputs.files.elements.map { - log.i { "Configuring outputs - input artifacts were resolved, ${it.size}. Mapping them to output files." } - val outputRoot = anchorTask.outputDirectory.get() - val outputFiles = it.flatMap { inputRoot -> - val inputSharedLibraries = inputRoot.asFile.listSharedLibrariesRecursive() - log.i { "Configuring outputs - found ${inputSharedLibraries.size} libraries in ${inputRoot.asFile}, mapping to ${outputRoot.asFile}..." } - inputSharedLibraries.map { sharedLibrary -> - log.i { "Configuring outputs - mapping shared library ${sharedLibrary.toRelativeString(inputRoot.asFile)}..." } - sharedLibrary.moveHierarchy(inputRoot.asFile, outputRoot.asFile) - } - } - // Spread files and create a new collection, so that we return Provider - // to the outputs. This is also useful to lose the task dependency information that - // we don't want to carry over from inputs to outputs. - target.files(*outputFiles.toTypedArray()) - }) */ + log.d { "Configuring variant ${variant.name}..." } - doFirst { - log.i { "Executing for variant ${variant.name} and ${inputs.files.files.size} roots..." } - inputs.files.files.forEach { inputRoot -> - log.i { "Found shared libraries root: $inputRoot" } - val outputRoot = anchorTask.outputDirectory.get().asFile + val creationConfig = variant.componentCreationConfigOrThrow() + + target.locateTask(creationConfig.computeTaskName("copy", "JniLibsProjectAndLocalJars"))?.configure { + val copyJniTask = this as LibraryJniLibsTask + val extraJniLibs = configurations.artifactsOf(AndroidArtifacts.ArtifactType.JNI) + dependsOn(extraJniLibs) + + fun injectJniLibs() { + log.d { "Executing for variant ${variant.name} and ${extraJniLibs.files.size} roots..." } + extraJniLibs.files.forEach { inputRoot -> + log.d { "Found shared libraries root: $inputRoot" } + val outputRoot = copyJniTask.outputDirectory.get().asFile val sharedLibraries = inputRoot.listFilesRecursive("so") sharedLibraries.forEach { - log.i { "Copying ${it.toRelativeString(inputRoot)} from inputRoot=$inputRoot to outputRoot=$outputRoot..." } + log.d { "Copying ${it.toRelativeString(inputRoot)} from inputRoot=$inputRoot to outputRoot=$outputRoot..." } target.copy { from(it) into(it.relocate(inputRoot, outputRoot).parentFile) @@ -239,10 +246,12 @@ open class GreasePlugin : Plugin { } } } - } - target.tasks.configureEach { - if (name == anchorTaskName) { - finalizedBy(extractorTask) + + val files = projectNativeLibs.get().files().files + localJarsNativeLibs?.files.orEmpty() + if (files.isNotEmpty()) { + doLast { injectJniLibs() } + } else { + injectJniLibs() } } } @@ -305,41 +314,52 @@ open class GreasePlugin : Plugin { */ private fun configureVariantResources( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, logger: Logger ) { + val log = logger.child("configureVariantResources") - log.i { "Configuring variant ${variant.name}..." } - val anchorTaskName = nameOf("package", variant.name, "Resources") - val greaseTask = target.tasks.register(anchorTaskName.greasify()) { - val anchorTask = target.tasks[anchorTaskName] as MergeResources - dependsOn(anchorTask) - inputs.files(configuration.artifactsOf(AndroidArtifacts.ArtifactType.ANDROID_RES)) - outputs.dir(anchorTask.outputDir.get()) - doFirst { - log.i { "Executing for variant ${variant.name} and ${inputs.files.files.size} roots..." } - inputs.files.files.forEach { inputRoot -> - log.i { "Found resources root: $inputRoot" } - val outputRoot = anchorTask.outputDir.get().asFile - val outputPrefix = inputRoot.parentFile.name.map { char -> - if (char.isLetterOrDigit()) char else '_' - }.joinToString(separator = "") - val resources = inputRoot.listFilesRecursive("xml") - resources.forEach { - log.i { "Copying ${it.toRelativeString(inputRoot)} (prefix=$outputPrefix) from inputRoot=$inputRoot to outputRoot=$outputRoot..." } - target.copy { - from(it) - into(it.relocate(inputRoot, outputRoot).parentFile) - rename { "${outputPrefix}_$it" } - } - } - } + log.d { "Configuring variant ${variant.name}..." } + val creationConfig = variant.componentCreationConfigOrThrow() + + target.locateTask(creationConfig.computeTaskName("package", "Resources"))?.configure { + this as MergeResources + + val resourcesMergingWorkdir = target.greaseBuildDir.get().dir(variant.name).dir("resources") + val mergedResourcesDir = resourcesMergingWorkdir.dir("merged") + val blameDir = resourcesMergingWorkdir.dir("blame") + val extraAndroidRes = configurations.artifactsOf(AndroidArtifacts.ArtifactType.ANDROID_RES) + dependsOn(extraAndroidRes) + + outputs.upToDateWhen { false } // always execute + + fun injectResources() { + target.delete(resourcesMergingWorkdir) + + val executorFacade = Workers.withGradleWorkers( + creationConfig.services.projectInfo.path, + path, + workerExecutor, + analyticsService + ) + log.d { "Merge additional resources into $mergedResourcesDir" } + mergeResourcesWithCompilationService( + resCompilerService = CopyToOutputDirectoryResourceCompilationService, + incrementalMergedResources = mergedResourcesDir.asFile, + mergedResources = outputDir.asFile.get(), + resourceSets = extraAndroidRes.files.toList(), + minSdk = minSdk.get(), + aaptWorkerFacade = executorFacade, + blameLogOutputFolder = blameDir.asFile, + logger = this.logger + ) } - } - target.tasks.configureEach { - if (name == anchorTaskName) { - finalizedBy(greaseTask) + + if (inputs.hasInputs) { + doLast { injectResources() } + } else { + injectResources() } } } @@ -363,35 +383,173 @@ open class GreasePlugin : Plugin { */ private fun configureVariantSources( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, + greaseExtension: GreaseExtension, logger: Logger ) { val log = logger.child("configureVariantSources") - log.i { "Configuring variant ${variant.name}..." } - val compileTask = variant.javaCompileProvider // compile<>JavaWithJavac - val bundleTaskName = nameOf("sync", variant.name, "LibJars") - val greaseTask = target.tasks.register(compileTask.name.greasify()) { - dependsOn(compileTask) + log.d { "Configuring variant ${variant.name}..." } + + val creationConfig = variant.componentCreationConfigOrThrow() + + val workdir = target.greaseBuildDir.get().dir(variant.name) + val aarExtractWorkdir = workdir.dir("extract").dir("aar") + val jarExtractWorkdir = workdir.dir("extract").dir("jar") + val jarFileName = "classes.jar" + + val bundleLibraryTask = creationConfig.taskContainer.bundleLibraryTask + + val greaseExpandTask = target.tasks.locateOrRegisterTask( + creationConfig.computeTaskName("extract", "Aar").greasify(), + ) { + val bundleAar = bundleLibraryTask?.get() as BundleAar + + outputs.upToDateWhen { false } // always execute + + inputs.file(bundleAar.archiveFile) + outputs.file(aarExtractWorkdir.file(jarFileName).asFile) + + doFirst { + target.delete(aarExtractWorkdir) + unzipTo(aarExtractWorkdir.asFile, bundleAar.archiveFile.get().asFile) + } + } + + val greaseProcessTask = target.tasks.locateOrRegisterTask( + creationConfig.computeTaskName("process", "Jar").greasify(), + ) { + // There are many options here. PROCESSED_JAR, PROCESSED_AAR, CLASSES, CLASSES_JAR ... // CLASSES_JAR seems to be the best though it's not clear if it's jetified or not. - inputs.files(configuration.artifactsOf(AndroidArtifacts.ArtifactType.CLASSES_JAR)) - outputs.dir(compileTask.get().destinationDirectory.get()) + val extraJars = configurations.artifactsOf(AndroidArtifacts.ArtifactType.CLASSES_JAR) + dependsOn(extraJars) + dependsOn(greaseExpandTask) + + outputs.upToDateWhen { false } // always execute + inputs.files(greaseExpandTask.get().outputs.files) + outputs.dir(jarExtractWorkdir) + + fun injectClasses(inputJar: File) { + log.d { "Processing inputJar=$inputJar outputDir=${jarExtractWorkdir}..." } + val inputFiles = target.zipTree(inputJar).matching { include("**/*.class") } + target.copy { + from(inputFiles) + into(jarExtractWorkdir) + } + } + doFirst { - log.i { "Executing for variant ${variant.name} and ${inputs.files.files.size} roots..." } - inputs.files.files.forEach { inputJar -> - log.i { "Processing inputJar=$inputJar outputDir=${compileTask.get().destinationDirectory.get()}..." } - val inputFiles = target.zipTree(inputJar).matching { include("**/*.class") } - target.copy { - from(inputFiles) - into(outputs.files.singleFile) + target.delete(jarExtractWorkdir) + log.d { "Executing merging for variant ${variant.name} and ${extraJars.files.size} roots..." } + extraJars.files.forEach(::injectClasses) + aarExtractWorkdir.file(jarFileName).asFile.run(::injectClasses) + } + } + + val greaseShadowTask = target.tasks.locateOrRegisterTask( + creationConfig.computeTaskName("shadow", "Aar").greasify(), + ShadowJar::class.java + ) { + val compileTask = creationConfig.taskContainer.javacTask + val extraManifests = configurations.artifactsOf(AndroidArtifacts.ArtifactType.MANIFEST) + val greaseShadowDir = workdir.dir("shadow") + val bundleAar = bundleLibraryTask?.get() as BundleAar + + outputs.upToDateWhen { false } // always execute + + dependsOn(extraManifests) + dependsOn(greaseExpandTask) + dependsOn(greaseProcessTask) + + archiveFileName.set(jarFileName) + destinationDirectory.set(greaseShadowDir) + + from(greaseProcessTask.get().outputs) + val packagesToReplace = mutableMapOf() + + doFirst { + target.delete(greaseShadowDir) + greaseShadowDir.asFile.mkdirs() + + log.d { "Executing shadowing for variant ${variant.name} and ${extraManifests.files.size} roots with namespace ${variant.namespace.get()}..." } + extraManifests.forEach { inputFile -> + val manifestData = parseManifest(inputFile, true, { true }, defaultIssueReporter) + manifestData.packageName?.let { fromPackageName -> + log.d { "Processing R class from $fromPackageName manifestInput=${inputFile.path} outputDir=${compileTask.get().destinationDirectory.get()}..." } + relocate(RClassRelocator(fromPackageName, variant.namespace.get(), log)) } } + + val relocationPrefix = greaseExtension.prefix.get() + if (relocationPrefix.isNotEmpty()) { + greaseProcessTask.get().outputs.files + .asSequence() + .flatMap { inputFile -> inputFile.packageNames } + .distinct() + .forEach { packageName -> + val newPackageName = "${relocationPrefix}.$packageName" + log.d { "Relocate package from $packageName to $newPackageName" } + relocate(packageName, newPackageName) + packagesToReplace[packageName] = newPackageName + } + } + + greaseExtension.relocators.get().forEach { relocator -> + relocate(relocator) + if (relocator is SimpleRelocator) { + packagesToReplace[relocator.pattern] = relocator.shadedPattern + } + } + + greaseExtension.transformers.get().forEach(::transform) + } + + doLast { + val shadowJar = greaseShadowDir.file(jarFileName).asFile + val shadowManifest = greaseShadowDir.file("AndroidManifest.xml").asFile + log.d { "Copy shaded inputJar=${shadowJar} outputDir=$aarExtractWorkdir..." } + target.copy { + from(shadowJar) + into(aarExtractWorkdir) + } + + val manifestWriter = shadowManifest.bufferedWriter() + val manifestReader = aarExtractWorkdir.file("AndroidManifest.xml").asFile.bufferedReader() + + manifestReader.useLines { strings -> + strings + .map { string -> + packagesToReplace.entries.fold(string) { acc, (from, to) -> + acc.replace(from, to) + } + }.forEach { + manifestWriter.write(it) + manifestWriter.newLine() + } + } + manifestWriter.close() + target.copy { + from(shadowManifest) + into(aarExtractWorkdir) + } + + val oldArchive = bundleAar.archiveFile.get().asFile + val archiveParent = oldArchive.parentFile + val archiveName = oldArchive.name + target.delete(oldArchive) + zipTo(archiveParent.file(archiveName), aarExtractWorkdir.asFile) } } - compileTask.configure { finalizedBy(greaseTask) } - target.tasks.configureEach { - if (name == bundleTaskName) dependsOn(greaseTask) + + bundleLibraryTask?.configure { + finalizedBy(greaseExpandTask) + } + greaseExpandTask.configure { + finalizedBy(greaseProcessTask) + } + greaseProcessTask.configure { + finalizedBy(greaseShadowTask) } } @@ -406,31 +564,33 @@ open class GreasePlugin : Plugin { */ private fun configureVariantAssets( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, logger: Logger ) { val log = logger.child("configureVariantAssets") - log.i { "Configuring variant ${variant.name}..." } - val compileTask = variant.mergeAssetsProvider // package<>Assets - val greaseTask = target.tasks.register(compileTask.name.greasify()) { - dependsOn(compileTask) - inputs.files(configuration.artifactsOf(AndroidArtifacts.ArtifactType.ASSETS)) - outputs.dir(compileTask.get().outputDir.get()) - doFirst { - log.i { "Executing for variant ${variant.name} and ${inputs.files.files.size} roots..." } - inputs.files.files.forEach { inputRoot -> - log.i { "Found asset folder root: $inputRoot" } - // TODO crash/warn if any asset is overwritten + log.d { "Configuring variant ${variant.name}..." } + val creationConfig = variant.componentCreationConfigOrThrow() + creationConfig.taskContainer.mergeAssetsTask.configure { + val extraAssets = configurations.artifactsOf(AndroidArtifacts.ArtifactType.ASSETS) + dependsOn(extraAssets) + fun injectAssets() { + log.d { "Executing for variant ${variant.name} and ${extraAssets.files.size} roots..." } + extraAssets.files.forEach { inputRoot -> + log.d { "Found asset folder root: $inputRoot" } val inputFiles = target.fileTree(inputRoot) target.copy { from(inputFiles) - into(outputs.files.singleFile) + into(outputDir.get()) } } } + if (inputs.hasInputs) { + doLast { injectAssets() } + } else { + injectAssets() + } } - compileTask.configure { finalizedBy(greaseTask) } } /** @@ -458,28 +618,33 @@ open class GreasePlugin : Plugin { */ private fun configureVariantProguardFiles( target: Project, - variant: LibraryVariant, - configuration: Configuration, + variant: Variant, + configurations: Array, logger: Logger ) { val log = logger.child("configureVariantProguardFiles") - log.i { "Configuring variant ${variant.name}..." } - target.tasks.configureEach { - if (name == nameOf("merge", variant.name, "ConsumerProguardFiles")) { - val task = this as MergeFileTask - // UNFILTERED_PROGUARD_RULES, FILTERED_PROGUARD_RULES, AAPT_PROGUARD_RULES, ... - // UNFILTERED_PROGUARD_RULES is output of the AarTransform. FILTERED_PROGUARD_RULES - // is processed by another transform and is probably what we want in the end. - val extraInputs = configuration.artifactsOf(AndroidArtifacts.ArtifactType.FILTERED_PROGUARD_RULES) - val inputs = task.inputFiles.plus(extraInputs) - task.inputFiles = inputs - task.doFirst { - require (task.inputFiles === inputs) { - "Input proguard files have been changed after our configureEach callback!" - } - log.i { "Input proguard files: ${inputFiles.files.joinToString()}" } - } + log.d { "Configuring variant ${variant.name}..." } + val creationConfig = variant.componentCreationConfigOrThrow() + target.locateTask(creationConfig.computeTaskName("merge", "ConsumerProguardFiles"))?.configure { + val mergeFileTask = this as MergeFileTask + // UNFILTERED_PROGUARD_RULES, FILTERED_PROGUARD_RULES, AAPT_PROGUARD_RULES, ... + // UNFILTERED_PROGUARD_RULES is output of the AarTransform. FILTERED_PROGUARD_RULES + // is processed by another transform and is probably what we want in the end. + val extraInputs = configurations.artifactsOf(AndroidArtifacts.ArtifactType.FILTERED_PROGUARD_RULES) + dependsOn(extraInputs) + + mergeFileTask.inputs.files(extraInputs + mergeFileTask.inputFiles.files) + mergeFileTask.doFirst { + log.d { "Input proguard files: ${mergeFileTask.inputs.files.joinToString()}" } } } } } + +private fun Variant.componentCreationConfigOrThrow(): ComponentCreationConfig { + return when (this) { + is ComponentCreationConfig -> this + is AnalyticsEnabledVariant -> this.delegate.componentCreationConfigOrThrow() + else -> error("Could not find ComponentCreationConfig in $this.") + } +} diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/Logger.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/Logger.kt index b94c0b4..8dcb041 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/Logger.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/Logger.kt @@ -4,25 +4,17 @@ import org.gradle.api.Project import org.gradle.api.logging.LogLevel internal class Logger(private val project: Project, private val tag: String) { - companion object { - val INFO = LogLevel.INFO - val WARNING = LogLevel.WARN - val ERROR = LogLevel.ERROR - } fun log(level: LogLevel, message: () -> String?) { message.invoke()?.let { - if (level == INFO) { - println("$tag: $it") - } else { - project.logger.log(level, "$tag: $it") - } + project.logger.log(level, "$tag: $it") } } - fun i(message: () -> String?) = log(INFO, message) - fun w(message: () -> String?) = log(WARNING, message) - fun e(message: () -> String?) = log(ERROR, message) + fun i(message: () -> String?) = log(LogLevel.INFO, message) + fun d(message: () -> String?) = log(LogLevel.DEBUG, message) + fun w(message: () -> String?) = log(LogLevel.WARN, message) + fun e(message: () -> String?) = log(LogLevel.ERROR, message) fun child(tag: String) = Logger(project, "${this.tag} > $tag") } \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/RClassRelocator.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/RClassRelocator.kt new file mode 100644 index 0000000..4272338 --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/RClassRelocator.kt @@ -0,0 +1,33 @@ +package io.deepmedia.tools.grease + +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator + +internal class RClassRelocator( + fromPackage: String, + toPackage: String, + logger: Logger +) : SimpleRelocator(fromPackage, toPackage, emptyList(), emptyList()) { + + private val logger = logger.child("R-relocator") + private val fromRPath = fromPackage.replace(".", "/") + private val toRPath = toPackage.replace(".", "/") + "/R" + private val fromRPathRegex = "$fromRPath.*/R".toRegex() + + init { + include( "%regex[$fromRPathRegex\\$.*]") + include( "%regex[$fromRPathRegex]") + } + + override fun canRelocateClass(className: String?): Boolean = false + + override fun relocatePath(context: RelocatePathContext): String { + val foundedPath = fromRPathRegex.find(context.path)?.value + if (foundedPath == null) { + logger.d { "Can't move from ${context.path} to $toRPath" } + return context.path + } + return context.path.replaceFirst(foundedPath, toRPath) + } + +} \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/configurations.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/configurations.kt index 03833fa..0bf055e 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/configurations.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/configurations.kt @@ -1,38 +1,52 @@ package io.deepmedia.tools.grease import com.android.build.api.attributes.BuildTypeAttr -import com.android.build.gradle.api.LibraryVariant +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.api.variant.Variant import com.android.build.gradle.internal.dsl.BuildType -import com.android.build.gradle.internal.dsl.ProductFlavor import com.android.build.gradle.internal.publishing.AndroidArtifacts -import org.gradle.api.DomainObjectSet import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Usage +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.file.CompositeFileCollection +import org.gradle.api.internal.file.FileCollectionInternal import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.named +import java.util.function.Consumer -internal fun Configuration.artifactsOf(type: AndroidArtifacts.ArtifactType) - = incoming.artifactView { +internal fun Configuration.artifactsOf(type: AndroidArtifacts.ArtifactType): FileCollection = incoming.artifactView { attributes { attribute(AndroidArtifacts.ARTIFACT_TYPE, type.type) } }.files -internal val Project.grease get() = greaseOf("") +internal fun Array.artifactsOf(type: AndroidArtifacts.ArtifactType): FileCollection = map { + it.incoming.artifactView { + attributes { + attribute(AndroidArtifacts.ARTIFACT_TYPE, type.type) + } + }.files as FileCollectionInternal +}.let { + ArtifactsFileCollection(it) +} -internal fun Project.greaseOf(flavor: com.android.builder.model.ProductFlavor) - = greaseOf(flavor.name) +private class ArtifactsFileCollection(private val fileCollections: List) : CompositeFileCollection() { + override fun getDisplayName(): String = "grease file collection" -internal fun Project.greaseOf(buildType: com.android.builder.model.BuildType) - = greaseOf(buildType.name) + override fun visitChildren(visitor: Consumer) { + fileCollections.forEach(visitor::accept) + } +} -internal fun Project.greaseOf(variant: LibraryVariant) - = greaseOf(variant.name) +internal fun Project.grease(isTransitive: Boolean) = greaseOf("".configurationName(isTransitive)) -private fun Project.greaseOf(name: String) - = configurations[name.greasify()] +internal fun Project.greaseOf(variant: Variant, isTransitive: Boolean = false) = + greaseOf(variant.name.configurationName(isTransitive)) + +private fun Project.greaseOf(name: String, isTransitive: Boolean = false) = + configurations[name.configurationName(isTransitive).greasify()] /** @@ -50,8 +64,13 @@ private fun Project.greaseOf(name: String) * Note that the counterpart for "compileClasspath" also exists and it's called "runtimeClasspath". * They map to the org.gradle.usage attribute of java-api and java-runtime respectively. */ -private fun Project.createGrease(name: String): Configuration { - val configuration = configurations.create(name.greasify()) +private fun Project.createGrease(name: String, isTransitive: Boolean): Configuration { + val greasifiedName = name.configurationName(isTransitive).greasify() + val existed = configurations.findByName(greasifiedName) + if (existed != null) return existed + + val configuration = configurations.create(greasifiedName) + configuration.isTransitive = isTransitive configuration.attributes { // This should make sure that we don't pull in compileOnly dependencies that should not be in // the final bundle. The other usage, JAVA_API, would only include exposed dependencies, @@ -62,39 +81,62 @@ private fun Project.createGrease(name: String): Configuration { configurations.configureEach { val other = this if (other.name == nameOf(name, "compileClasspath")) { - extendsFrom(configuration) + other.extendsFrom(configuration) } } return configuration } +private fun String.configurationName(isTransitive: Boolean) = if (isTransitive) nameOf(this, "tree") else this + // Create the root configuration. Make compileOnly extend from it so that grease // artifacts are in the classpath and we don't have compile issues. -internal fun Project.createRootConfiguration(log: Logger) { - log.i { "Creating root configuration..." } - createGrease("") +internal fun Project.createRootConfiguration(isTransitive: Boolean, log: Logger) { + log.d { "Creating root configuration..." } + createGrease("", isTransitive) } // Create one configuration per product flavor. // Make it extend the root configuration so that artifacts are inherited. internal fun Project.createProductFlavorConfigurations( - flavors: NamedDomainObjectContainer, log: Logger) { - flavors.configureEach { - log.i { "Creating product flavor configuration ${this.name.greasify()}..." } - val config = createGrease(this.name) - config.extendsFrom(grease) + androidComponent: AndroidComponentsExtension<*, *, *>, + isTransitive: Boolean, + log: Logger, +) = androidComponent.onVariants { variant -> + variant.flavorName?.let { flavorName -> + log.d { "Creating product flavor configuration ${flavorName.greasify()}..." } + val flavorConfiguration = createGrease(flavorName, isTransitive) + flavorConfiguration.extendsFromSafely(grease(isTransitive), log) + + variant.productFlavors.forEach { (_, subFlavor) -> + log.d { "Creating sub product flavor configuration ${subFlavor.greasify()}..." } + val config = createGrease(subFlavor, isTransitive) + config.extendsFromSafely(grease(isTransitive), log) + flavorConfiguration.extendsFromSafely(config, log) + } + variant.productFlavors.forEach { (_, subFlavor) -> + val buildTypedSubFlavor = nameOf(subFlavor, variant.buildType.orEmpty()) + log.d { "Creating buildTyped sub product flavor configuration ${buildTypedSubFlavor.greasify()}..." } + val config = createGrease(buildTypedSubFlavor, isTransitive) + config.extendsFromSafely(grease(isTransitive), log) + config.extendsFromSafely(greaseOf(subFlavor, isTransitive), log) + config.extendsFromSafely(flavorConfiguration, log) + } } } // Create one configuration per build type. // Make it extend the root configuration so that artifacts are inherited. internal fun Project.createBuildTypeConfigurations( - buildTypes: NamedDomainObjectContainer, log: Logger) { + buildTypes: NamedDomainObjectContainer, + isTransitive: Boolean, + log: Logger +) { buildTypes.configureEach { val buildType = this - log.i { "Creating build type configuration ${buildType.name.greasify()}..." } - val config = createGrease(buildType.name) - config.extendsFrom(grease) + log.d { "Creating build type configuration ${buildType.name.greasify()}..." } + val config = createGrease(buildType.name, isTransitive) + config.extendsFromSafely(grease(isTransitive), log) config.attributes { attribute(BuildTypeAttr.ATTRIBUTE, objects.named(BuildTypeAttr::class, buildType.name)) } @@ -105,17 +147,27 @@ internal fun Project.createBuildTypeConfigurations( // In addition to the root configuration, we should inherit from the build type config // and the configurations for all flavors that make this variant. internal fun Project.createVariantConfigurations( - variants: DomainObjectSet, log: Logger) { - variants.configureEach { - val flavors = this.productFlavors - if (flavors.isEmpty()) { - log.i { "Variant has no flavors, reusing the build type configuration..." } - } else { - log.i { "Creating variant configuration ${this.name.greasify()}..." } - val config = createGrease(this.name) - config.extendsFrom(grease) - config.extendsFrom(greaseOf(buildType)) - config.extendsFrom(*flavors.map { greaseOf(it) }.toTypedArray()) + androidComponent: AndroidComponentsExtension<*, *, *>, + isTransitive: Boolean, + log: Logger +) = androidComponent.onVariants { variant -> + log.d { "Creating variant configuration ${variant.name.greasify()}..." } + val config = createGrease(variant.name, isTransitive) + config.extendsFromSafely(grease(isTransitive), log) + config.extendsFromSafely(greaseOf(variant.buildType.orEmpty(), isTransitive), log) + variant.flavorName?.let { flavor -> + config.extendsFromSafely(greaseOf(flavor, isTransitive), log) + variant.productFlavors.forEach { (_, subFlavor) -> + config.extendsFromSafely(greaseOf(subFlavor, isTransitive), log) + config.extendsFromSafely(greaseOf(nameOf(subFlavor, variant.buildType.orEmpty()), isTransitive), log) } } + } + +private fun Configuration.extendsFromSafely(configuration: Configuration, log: Logger? = null) { + if (configuration.name != name) { + log?.d { "Extend $name from ${configuration.name}..." } + extendsFrom(configuration) + } +} \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/debug.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/debug.kt index f2f0d61..28f701e 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/debug.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/debug.kt @@ -16,7 +16,7 @@ internal fun debugConfigurationHierarchy(target: Project, logger: Logger) { val log = logger.child(this.name) val attrKeys = attributes.keySet() val attrs = attrKeys.map { it to attributes.getAttribute(it) } - log.i { + log.d { "Configuration added - " + "canBeResolved=${isCanBeResolved} " + "canBeConsumed=${isCanBeConsumed} " + @@ -27,21 +27,34 @@ internal fun debugConfigurationHierarchy(target: Project, logger: Logger) { } } +internal fun debugGreasyConfigurationHierarchy(target: Project, logger: Logger) { + target.afterEvaluate { + configurations.configureEach { + if (name.startsWith("grease")) { + logger.d { name } + extendsFrom.forEach {sub -> + logger.d { "| ${sub.name}" } + } + } + } + } +} + internal fun debugSourcesTasks(target: Project, logger: Logger) { target.tasks.configureEach { val log = logger.child(this.name).child(this::class.java.simpleName) when (val task = this) { is LibraryAarJarsTask -> doFirst { - log.i { "mainScopeClassFiles (i): ${task.mainScopeClassFiles.files.joinToString()}" } - log.i { "mainClassLocation (o): ${task.mainClassLocation.orNull}" } - log.i { "localJarsLocation (o): ${task.localJarsLocation.orNull}" } + log.d { "mainScopeClassFiles (i): ${task.mainScopeClassFiles.files.joinToString()}" } + log.d { "mainClassLocation (o): ${task.mainClassLocation.orNull}" } + log.d { "localJarsLocation (o): ${task.localJarsLocation.orNull}" } } is JavaCompile -> doFirst { - log.i { "source (i): ${task.source.files.joinToString()}" } - log.i { "source (i): ${task.source.files.joinToString()}" } - log.i { "generatedSourceOutputDirectory (o): ${task.options.generatedSourceOutputDirectory.orNull}" } - log.i { "headerOutputDirectory (o): ${task.options.headerOutputDirectory.orNull}" } - log.i { "destinationDirectory (o): ${task.destinationDirectory.orNull}" } + log.d { "source (i): ${task.source.files.joinToString()}" } + log.d { "source (i): ${task.source.files.joinToString()}" } + log.d { "generatedSourceOutputDirectory (o): ${task.options.generatedSourceOutputDirectory.orNull}" } + log.d { "headerOutputDirectory (o): ${task.options.headerOutputDirectory.orNull}" } + log.d { "destinationDirectory (o): ${task.destinationDirectory.orNull}" } } } } @@ -52,42 +65,42 @@ internal fun debugResourcesTasks(target: Project, logger: Logger) { val log = logger.child(this.name).child(this::class.java.simpleName) when (val task = this) { is GenerateLibraryRFileTask -> doFirst { - log.i { "localResourcesFile (i): ${task.localResourcesFile.orNull}" } - log.i { "dependencies (i): ${task.dependencies.files.joinToString()}" } - log.i { "rClassOutputJar (o): ${task.rClassOutputJar.orNull}" } - log.i { "sourceOutputDir (o): ${task.sourceOutputDir}" } - log.i { "textSymbolOutputFileProperty (o): ${task.textSymbolOutputFileProperty.orNull}" } - log.i { "symbolsWithPackageNameOutputFile (o): ${task.symbolsWithPackageNameOutputFile.orNull}" } - log.i { "symbolsWithPackageNameOutputFile (o): ${task.symbolsWithPackageNameOutputFile.orNull}" } + log.d { "localResourcesFile (i): ${task.localResourcesFile.orNull}" } + log.d { "dependencies (i): ${task.dependencies.files.joinToString()}" } + log.d { "rClassOutputJar (o): ${task.rClassOutputJar.orNull}" } + log.d { "sourceOutputDir (o): ${task.sourceOutputDir}" } + log.d { "textSymbolOutputFileProperty (o): ${task.textSymbolOutputFileProperty.orNull}" } + log.d { "symbolsWithPackageNameOutputFile (o): ${task.symbolsWithPackageNameOutputFile.orNull}" } + log.d { "symbolsWithPackageNameOutputFile (o): ${task.symbolsWithPackageNameOutputFile.orNull}" } } is ParseLibraryResourcesTask -> doFirst { - log.i { "inputResourcesDir (i): ${task.inputResourcesDir.orNull}" } - log.i { "librarySymbolsFile (o): ${task.librarySymbolsFile.orNull}" } + log.d { "inputResourcesDir (i): ${task.inputResourcesDir.orNull}" } + log.d { "librarySymbolsFile (o): ${task.librarySymbolsFile.orNull}" } } is GenerateBuildConfig -> doFirst { - log.i { "mergedManifests (i): ${task.mergedManifests.orNull}" } // empty - log.i { "items (i): ${task.items.orNull}" } // empty - log.i { "sourceOutputDir (o): ${task.sourceOutputDir}" } // generated/source/. contains the BuildConfig file + log.d { "mergedManifests (i): ${task.mergedManifests.orNull}" } // empty + log.d { "items (i): ${task.items.orNull}" } // empty + log.d { "sourceOutputDir (o): ${task.sourceOutputDir}" } // generated/source/. contains the BuildConfig file } is GenerateResValues -> doFirst { - log.i { "items (i): ${task.items.orNull}" } // empty - log.i { "resOutputDir (o): ${task.resOutputDir}" } // generated/res/resValues/. nothing there for now + log.d { "items (i): ${task.items.orNull}" } // empty + log.d { "resOutputDir (o): ${task.resOutputDir}" } // generated/res/resValues/. nothing there for now } is MergeResources -> doFirst { // When packageResources: intermediates/packaged_res/. // There we find a copy of the resources. // When mergeResources (soon after): intermediates/res/merged/. // This is clearly the input of the verify task below. - log.i { "outputDir (o): ${task.outputDir.orNull}" } + log.d { "outputDir (o): ${task.outputDir.orNull}" } // When packageResources: intermediates/public_res//public.txt // When mergeResources: empty. - log.i { "publicFile (o): ${task.publicFile.orNull}" } + log.d { "publicFile (o): ${task.publicFile.orNull}" } } is VerifyLibraryResourcesTask -> doFirst { - log.i { "manifestFiles (i): ${task.manifestFiles.orNull}" } // intermediates/aapt_friendly_merged_manifests//aapt - log.i { "inputDirectory (i): ${task.inputDirectory.orNull}" } // intermediates/res/merged// . Contains a copy of the resources, "merged" probably in the sense that all values are in a single file called values.xml ? - log.i { "compiledDependenciesResources (i): ${task.compiledDependenciesResources.files.joinToString()}" } // empty - log.i { "compiledDirectory (o): ${task.compiledDirectory}" } // intermediates/res/compiled// . Contains the input resources compiled to a misterious *.flat format, probably used in APKs. + log.d { "manifestFiles (i): ${task.manifestFiles.orNull}" } // intermediates/aapt_friendly_merged_manifests//aapt + log.d { "inputDirectory (i): ${task.inputDirectory.orNull}" } // intermediates/res/merged// . Contains a copy of the resources, "merged" probably in the sense that all values are in a single file called values.xml ? + log.d { "compiledDependenciesResources (i): ${task.compiledDependenciesResources.files.joinToString()}" } // empty + log.d { "compiledDirectory (o): ${task.compiledDirectory}" } // intermediates/res/compiled// . Contains the input resources compiled to a misterious *.flat format, probably used in APKs. } } } diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/files.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/files.kt index bd79870..73b9241 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/files.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/files.kt @@ -1,6 +1,7 @@ package io.deepmedia.tools.grease import java.io.File +import java.util.jar.JarFile internal fun File.folder(name: String) = File(this, name).also { it.mkdirs() } @@ -28,4 +29,21 @@ internal fun File.listFilesRecursive(extension: String): List { internal fun File.relocate(from: File, to: File): File { assert(absolutePath.startsWith(from.absolutePath)) { "File not contained in $from!" } return File(absolutePath.replaceFirst(from.absolutePath, to.absolutePath)) -} \ No newline at end of file +} + +val JarFile.packageNames: Set + get() = entries().asSequence().mapNotNull { entry -> + if (entry.name.endsWith(".class") && entry.name != "module-info.class") { + entry.name.substring(0 until entry.name.lastIndexOf('/')).replace('/', '.') + } else null + }.toSet() + +val File.packageNames: Set + get() = listFilesRecursive("class").mapNotNull { file -> + if (file.name != "module-info.class") { + val cleanedPath = file.path.removePrefix(this.path).removePrefix("/") + cleanedPath + .substring(0 until cleanedPath.lastIndexOf('/')) + .replace('/', '.') + } else null + }.toSet() \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/merge.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/merge.kt new file mode 100644 index 0000000..1d3b9de --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/merge.kt @@ -0,0 +1,61 @@ +package io.deepmedia.tools.grease + +import com.android.build.gradle.internal.LoggerWrapper +import com.android.ide.common.blame.MergingLog +import com.android.ide.common.rendering.api.ResourceNamespace +import com.android.ide.common.resources.MergedResourceWriter +import com.android.ide.common.resources.MergedResourceWriterRequest +import com.android.ide.common.resources.ResourceCompilationService +import com.android.ide.common.resources.ResourceMerger +import com.android.ide.common.resources.ResourceSet +import com.android.ide.common.workers.WorkerExecutorFacade +import org.gradle.api.logging.Logger +import java.io.File + +internal fun mergeResourcesWithCompilationService( + resCompilerService: ResourceCompilationService, + incrementalMergedResources: File, + mergedResources: File, + resourceSets: List, + minSdk: Int, + aaptWorkerFacade: WorkerExecutorFacade, + blameLogOutputFolder: File, + logger: Logger) { + val mergedResourcesDir = mergedResources.also { + it.mkdirs() + } + val sourcesResourceSet = ResourceSet( + null, ResourceNamespace.RES_AUTO, null, false, null + ).apply { + addSources(resourceSets.reversed()) + } + val resourceMerger = ResourceMerger(minSdk).apply { + sourcesResourceSet.loadFromFiles(LoggerWrapper(logger)) + addDataSet(sourcesResourceSet) + } + aaptWorkerFacade.use { workerExecutorFacade -> + resCompilerService.use { resCompilationService -> + val mergeResourcesWriterRequest = MergedResourceWriterRequest( + workerExecutor = workerExecutorFacade, + rootFolder = mergedResourcesDir, + publicFile = null, + blameLog = getCleanBlameLog(blameLogOutputFolder), + preprocessor = null, + resourceCompilationService = resCompilationService, + temporaryDirectory = incrementalMergedResources, + dataBindingExpressionRemover = null, + notCompiledOutputDirectory = null, + pseudoLocalesEnabled = false, + crunchPng = false, + moduleSourceSets = emptyMap() + ) + val writer = MergedResourceWriter(mergeResourcesWriterRequest) + resourceMerger.mergeData(writer, true) + resourceMerger.writeBlobTo(incrementalMergedResources, writer, false) + } + } +} + +private fun getCleanBlameLog(blameLogOutputFolder: File): MergingLog { + return MergingLog(blameLogOutputFolder) +} diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/project.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/project.kt new file mode 100644 index 0000000..6a56952 --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/project.kt @@ -0,0 +1,8 @@ +package io.deepmedia.tools.grease + +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider + +internal val Project.greaseBuildDir: Provider + get() = layout.buildDirectory.dir("grease") diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/strings.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/strings.kt index 6041c4c..d6fb9e3 100644 --- a/grease/src/main/kotlin/io/deepmedia/tools/grease/strings.kt +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/strings.kt @@ -1,9 +1,13 @@ package io.deepmedia.tools.grease +import java.util.* + internal fun String.greasify() = nameOf("grease", this) internal fun nameOf(vararg values: String) = values .filter { it.isNotBlank() } - .mapIndexed { index, string -> - if (index == 0) string.decapitalize() else string.capitalize() - }.joinToString(separator = "") \ No newline at end of file + .mapIndexed { index, string -> if (index == 0) string.decapitalize() else string.capitalize() + }.joinToString(separator = "") + +private fun String.decapitalize() = replaceFirstChar { it.lowercase(Locale.getDefault()) } +private fun String.capitalize() = replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } \ No newline at end of file diff --git a/grease/src/main/kotlin/io/deepmedia/tools/grease/task.kt b/grease/src/main/kotlin/io/deepmedia/tools/grease/task.kt new file mode 100644 index 0000000..fe10791 --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/task.kt @@ -0,0 +1,70 @@ +package io.deepmedia.tools.grease + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider + +/** + * Locates a task by [name] and [type], without triggering its creation or configuration. + */ +internal fun Project.locateTask(name: String, type: Class): TaskProvider? = tasks.locateTask(name, type) + +internal fun Project.locateTask(name: String): TaskProvider? = tasks.locateTask(name) + +/** + * Locates a task by [name] and [type], without triggering its creation or configuration. + */ +internal fun TaskContainer.locateTask(name: String, type: Class): TaskProvider? = + if (names.contains(name)) named(name, type) else null + +internal fun TaskContainer.locateTask(name: String): TaskProvider? = + if (names.contains(name)) named(name) else null + +/** + * Locates a task by [name] and [type], without triggering its creation or configuration or registers new task + * with [name], type [T] and initialization script [body] + */ +internal fun Project.locateOrRegisterTask( + name: String, + type: Class, + body: T.() -> (Unit) +): TaskProvider { + return project.locateTask(name, type) ?: project.registerTask(name, type, body = body) +} + +internal fun TaskContainer.locateOrRegisterTask( + name: String, + type: Class, + body: T.() -> (Unit) +): TaskProvider { + return locateTask(name, type) ?: registerTask(name, type, body = body) +} + +@Suppress("UNCHECKED_CAST") +internal fun TaskContainer.locateOrRegisterTask(name: String, body: Task.() -> (Unit)): TaskProvider { + return (locateTask(name, DefaultTask::class.java) ?: registerTask(name, DefaultTask::class.java, body = body)) as TaskProvider +} + +internal fun Project.registerTask( + name: String, + type: Class, + constructorArgs: List = emptyList(), + body: (T.() -> (Unit))? = null +): TaskProvider { + return project.tasks.registerTask(name, type, constructorArgs, body) +} + +internal fun TaskContainer.registerTask( + name: String, + type: Class, + constructorArgs: List = emptyList(), + body: (T.() -> (Unit))? = null +): TaskProvider { + val resultProvider = register(name, type, *constructorArgs.toTypedArray()) + if (body != null) { + resultProvider.configure(body) + } + return resultProvider +} \ No newline at end of file diff --git a/grease/src/main/resources/META-INF/gradle-plugins/io.deepmedia.tools.grease.properties b/grease/src/main/resources/META-INF/gradle-plugins/io.deepmedia.tools.grease.properties deleted file mode 100644 index 7f2d77b..0000000 --- a/grease/src/main/resources/META-INF/gradle-plugins/io.deepmedia.tools.grease.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=io.deepmedia.tools.grease.GreasePlugin diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts deleted file mode 100644 index 46124b8..0000000 --- a/sample/build.gradle.kts +++ /dev/null @@ -1,60 +0,0 @@ -plugins { - id("com.android.library") - id("io.deepmedia.tools.grease") -} - -android { - setCompileSdkVersion(29) - ndkVersion = "20.1.5948944" - defaultConfig { - setMinSdkVersion(21) - setTargetSdkVersion(29) - versionCode = 1 - versionName = "1.0" - - // Configure manifest placeholders to test that they are correctly replaced - // in our manifest processing step. - manifestPlaceholders["placeholder"] = "replacement" - - // Configure native library libgrease to test that it's correctly packed - // in the output together with those of our dependencies. - ndk { - abiFilters.addAll(setOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a")) - } - - // Configure proguard files. - proguardFiles(getDefaultProguardFile(com.android.build.gradle.ProguardFiles.ProguardFile.OPTIMIZE.fileName), "proguard-rules.pro") - consumerProguardFile("consumer-rules.pro") - - // Configure some flavors for testing configurations. - flavorDimensions("color", "shape") - productFlavors.create("blue") { dimension = "color" } - productFlavors.create("green") { dimension = "color" } - productFlavors.create("circle") { dimension = "shape" } - productFlavors.create("triangle") { dimension = "shape" } - } - - buildTypes["debug"].isMinifyEnabled = false - buildTypes["release"].isMinifyEnabled = true - - externalNativeBuild { - cmake { - path = file("src/main/CMakeLists.txt") - } - } -} - -configurations.configureEach { - if (name == "greaseGreenCircleDebug") isTransitive = false -} - -dependencies { - // Includes resource and some manifest changes - greaseDebug("androidx.core:core:1.3.2") - // Includes native libraries - greaseRelease("org.tensorflow:tensorflow-lite:2.3.0") - // Manifest changes, layout resources - afterEvaluate { - add("greaseGreenCircleDebug","com.otaliastudios:cameraview:2.6.3") - } -} \ No newline at end of file diff --git a/sample/consumer-rules.pro b/sample/consumer-rules.pro deleted file mode 100644 index 57eb197..0000000 --- a/sample/consumer-rules.pro +++ /dev/null @@ -1,3 +0,0 @@ --keepclassmembers class sample.class.from.consumer.rules { - public *; -} \ No newline at end of file diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro deleted file mode 100644 index 7d9d656..0000000 --- a/sample/proguard-rules.pro +++ /dev/null @@ -1,5 +0,0 @@ - --keepattributes SourceFile,LineNumberTable --keepclassmembers class sample.class.from.default.rules { - public *; -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index daccef8..8c8f6c4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,23 @@ -include(":sample") -include(":grease") +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google() + gradlePluginPortal() + mavenCentral() + } + versionCatalogs { + create("libs") + } +} + +rootProject.name = "Grease" + +include(":grease") \ No newline at end of file diff --git a/tests/gradle.properties b/tests/gradle.properties new file mode 100644 index 0000000..83df5e3 --- /dev/null +++ b/tests/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official diff --git a/sample/.gitignore b/tests/sample-dependency-pure/.gitignore similarity index 100% rename from sample/.gitignore rename to tests/sample-dependency-pure/.gitignore diff --git a/tests/sample-dependency-pure/build.gradle.kts b/tests/sample-dependency-pure/build.gradle.kts new file mode 100644 index 0000000..c3b88ba --- /dev/null +++ b/tests/sample-dependency-pure/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "io.deepmedia.tools.grease.sample.dependency.pure" + compileSdk = 34 + defaultConfig { + minSdk = 21 + } +} + +dependencies { + // Empty +} \ No newline at end of file diff --git a/tests/sample-dependency-pure/src/main/AndroidManifest.xml b/tests/sample-dependency-pure/src/main/AndroidManifest.xml new file mode 100644 index 0000000..972c3a8 --- /dev/null +++ b/tests/sample-dependency-pure/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt b/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt new file mode 100644 index 0000000..dd8276f --- /dev/null +++ b/tests/sample-dependency-pure/src/main/java/io/deepmedia/tools/grease/sample/dependency/pure/PureDependencyClass.kt @@ -0,0 +1,5 @@ +package io.deepmedia.tools.grease.sample.dependency.pure + +object PureDependencyClass { + fun foo() = "bar" +} \ No newline at end of file diff --git a/tests/sample-library/.gitignore b/tests/sample-library/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/tests/sample-library/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tests/sample-library/build.gradle.kts b/tests/sample-library/build.gradle.kts new file mode 100644 index 0000000..55b2e61 --- /dev/null +++ b/tests/sample-library/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.android.library) + id("io.deepmedia.tools.grease") +} + +grease { + relocate() +} + +android { + namespace = "io.deepmedia.tools.grease.sample.library" + ndkVersion = "23.1.7779620" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + + // Configure manifest placeholders to test that they are correctly replaced + // in our manifest processing step. + manifestPlaceholders["placeholder"] = "replacement" + + // Configure native library libgrease to test that it's correctly packed + // in the output together with those of our dependencies. + ndk { + abiFilters.addAll(setOf("x86", "x86_64", "armeabi-v7a", "arm64-v8a")) + } + + // Configure proguard files. + proguardFiles( + getDefaultProguardFile(com.android.build.gradle.ProguardFiles.ProguardFile.OPTIMIZE.fileName), + "proguard-rules.pro" + ) + consumerProguardFile("consumer-rules.pro") + + // Configure some flavors for testing configurations. + flavorDimensions.addAll(listOf("color", "shape")) + productFlavors { + create("blue") { dimension = "color" } + create("green") { dimension = "color" } + create("circle") { dimension = "shape" } + create("triangle") { dimension = "shape" } + } + } + + buildTypes { + release { + isMinifyEnabled = true + } + } + + externalNativeBuild { + cmake { + path = file("src/main/CMakeLists.txt") + } + } +} + +dependencies { + grease("androidx.core:core:1.0.0") + + // include deps to pom when publishing + api("com.google.android.material:material:1.0.0") + // Includes resource and some manifest changes + implementation("androidx.lifecycle:lifecycle-runtime:2.8.4") + + // Includes native libraries + grease("org.tensorflow:tensorflow-lite:2.3.0") + // Manifest changes, layout resources + grease("com.otaliastudios:cameraview:2.7.2") + + // Doesn't work. TODO: we need to configure grease configurations so that in case of multiple matching + // variants, they prefer one where com.android.build.api.attributes.BuildTypeAttr is set to release + // grease(project(":sample-dependency-pure")) +} \ No newline at end of file diff --git a/sample/src/main/assets/sample_asset_2.png b/tests/sample-library/consumer-rules.pro similarity index 100% rename from sample/src/main/assets/sample_asset_2.png rename to tests/sample-library/consumer-rules.pro diff --git a/tests/sample-library/proguard-rules.pro b/tests/sample-library/proguard-rules.pro new file mode 100644 index 0000000..c14a015 --- /dev/null +++ b/tests/sample-library/proguard-rules.pro @@ -0,0 +1,2 @@ + +-keepattributes SourceFile,LineNumberTable \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/tests/sample-library/src/main/AndroidManifest.xml similarity index 90% rename from sample/src/main/AndroidManifest.xml rename to tests/sample-library/src/main/AndroidManifest.xml index b9035a1..706adb1 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/tests/sample-library/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/sample/src/main/CMakeLists.txt b/tests/sample-library/src/main/CMakeLists.txt similarity index 100% rename from sample/src/main/CMakeLists.txt rename to tests/sample-library/src/main/CMakeLists.txt diff --git a/sample/src/main/assets/sample_asset_1 b/tests/sample-library/src/main/assets/sample_asset_1 similarity index 100% rename from sample/src/main/assets/sample_asset_1 rename to tests/sample-library/src/main/assets/sample_asset_1 diff --git a/sample/src/main/assets/sample_asset_3.log b/tests/sample-library/src/main/assets/sample_asset_2.png similarity index 100% rename from sample/src/main/assets/sample_asset_3.log rename to tests/sample-library/src/main/assets/sample_asset_2.png diff --git a/tests/sample-library/src/main/assets/sample_asset_3.log b/tests/sample-library/src/main/assets/sample_asset_3.log new file mode 100644 index 0000000..e69de29 diff --git a/sample/src/main/cc/grease-api.cc b/tests/sample-library/src/main/cc/grease-api.cc similarity index 100% rename from sample/src/main/cc/grease-api.cc rename to tests/sample-library/src/main/cc/grease-api.cc diff --git a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleActivity.java similarity index 70% rename from sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java rename to tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleActivity.java index 3923a35..c7e6ec2 100644 --- a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java +++ b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleActivity.java @@ -1,7 +1,6 @@ -package io.deepmedia.tools.grease.sample; +package io.deepmedia.tools.grease.sample.library; import android.app.Activity; -import android.app.Application; import android.os.Bundle; import android.util.Log; @@ -10,6 +9,7 @@ class SampleActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.sample_layout); +// int abcActionBarTitleItem = androidx.appcompat.R.layout.abc_action_bar_title_item; Log.i("SampleActivity", "Something."); } } diff --git a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleApplication.java b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleApplication.java similarity index 82% rename from sample/src/main/java/io/deepmedia/tools/grease/sample/SampleApplication.java rename to tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleApplication.java index dd06af7..1525fa3 100644 --- a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleApplication.java +++ b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleApplication.java @@ -1,4 +1,4 @@ -package io.deepmedia.tools.grease.sample; +package io.deepmedia.tools.grease.sample.library; import android.app.Application; import android.util.Log; diff --git a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleView.java b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleView.java similarity index 82% rename from sample/src/main/java/io/deepmedia/tools/grease/sample/SampleView.java rename to tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleView.java index 459666c..6921d51 100644 --- a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleView.java +++ b/tests/sample-library/src/main/java/io/deepmedia/tools/grease/sample/library/SampleView.java @@ -1,4 +1,4 @@ -package io.deepmedia.tools.grease.sample; +package io.deepmedia.tools.grease.sample.library; import android.content.Context; import android.util.Log; diff --git a/sample/src/main/res/layout/sample_layout.xml b/tests/sample-library/src/main/res/layout/sample_layout.xml similarity index 82% rename from sample/src/main/res/layout/sample_layout.xml rename to tests/sample-library/src/main/res/layout/sample_layout.xml index 94c192d..08dd18d 100644 --- a/sample/src/main/res/layout/sample_layout.xml +++ b/tests/sample-library/src/main/res/layout/sample_layout.xml @@ -1,5 +1,5 @@ -