diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1efc5d9..814cd8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,6 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 17 - name: Perform base checks run: ./gradlew build publishToDirectory \ No newline at end of file diff --git a/.gitignore b/.gitignore index 603b140..7ef5a0e 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 +/.idea/ .DS_Store /build /captures diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 976bc20..b5d9bbf 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,22 +1,8 @@ - - + diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 61a9130..b589d56 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 02cb845..cb258ed 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,9 +4,20 @@ diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 1d6cae2..199e049 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -49,7 +49,12 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3e91cc6..bb67fd9 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + 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/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..7f93135 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..1aa94a4 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,127 @@ -#!/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. +# ############################################################################## -## -## 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/subprojects/plugins/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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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 +130,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..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@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 + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ 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. @@ -35,7 +55,7 @@ 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% @@ -45,38 +65,26 @@ echo location of your Java installation. 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..7b8948e 100644 --- a/grease/build.gradle.kts +++ b/grease/build.gradle.kts @@ -1,39 +1,38 @@ -import io.deepmedia.tools.publisher.common.* - plugins { `kotlin-dsl` - id("io.deepmedia.tools.publisher") -} - -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 + alias(libs.plugins.publisher) } -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 +group = "io.deepmedia.tools" +version = "0.3.0" - bintray { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" +gradlePlugin { + plugins { + create("grease") { + id = "io.deepmedia.tools.grease" + implementationClass = "io.deepmedia.tools.grease.GreasePlugin" + } } +} + +dependencies { + implementation(libs.asm.commons) + implementation(libs.gradle.shadow) + implementation(libs.bundles.gradle.android) +} - directory { - directory = "../build/maven" +deployer { + content { + gradlePluginComponents { + kotlinSources() + emptyDocs() + } } - directory("shared") { - directory = file(repositories.mavenLocal().url).absolutePath + projectInfo { + description = "Fat AARs for Android." + url = "https://github.com/deepmedia/Grease" + scm.fromGithub("deepmedia", "Grease") + developer("natario1", "mattia@deepmedia.io", "DeepMedia", "https://deepmedia.io") } } \ No newline at end of file diff --git a/grease/settings.gradle.kts b/grease/settings.gradle.kts new file mode 100644 index 0000000..b871715 --- /dev/null +++ b/grease/settings.gradle.kts @@ -0,0 +1,23 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google() + gradlePluginPortal() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "grease" \ 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..15c79da --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/GreaseExtension.kt @@ -0,0 +1,37 @@ +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 + +abstract class GreaseExtension { + + internal val relocators = mutableListOf() + internal val transformers = mutableListOf() + + internal var isRelocationEnabled = false + var relocationPrefix: String = "shadow" + set(value) { + field = value + isRelocationEnabled = true + } + + fun relocate( + from: String, + to: String, + configure: (SimpleRelocator.() -> Unit)? = null + ) { + val relocator = SimpleRelocator(from, to, emptyList(), emptyList()) + configure?.invoke(relocator) + relocators.add(relocator) + } + + fun transform( + transformer: T, + configure: (T.() -> Unit)? = null + ) { + configure?.invoke(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..45c4ed5 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,42 @@ package io.deepmedia.tools.grease +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 +50,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 +59,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 +114,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 as ComponentCreationConfig + + 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 +215,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 as ComponentCreationConfig + + 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 +244,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 +312,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 as ComponentCreationConfig + + 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 +381,172 @@ 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 as ComponentCreationConfig + + 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)) } } + + if (greaseExtension.isRelocationEnabled) { + greaseProcessTask.get().outputs.files + .asSequence() + .flatMap { inputFile -> inputFile.packageNames } + .distinct() + .forEach { packageName -> + val newPackageName = "${greaseExtension.relocationPrefix}.$packageName" + log.d { "Relocate package from $packageName to $newPackageName" } + relocate(packageName, newPackageName) + packagesToReplace[packageName] = newPackageName + } + + greaseExtension.relocators.forEach { relocator -> + relocate(relocator) + if (relocator is SimpleRelocator) { + packagesToReplace[relocator.pattern] = relocator.shadedPattern + } + } + greaseExtension.transformers.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) + } + } + + bundleLibraryTask?.configure { + finalizedBy(greaseExpandTask) + } + greaseExpandTask.configure { + finalizedBy(greaseProcessTask) } - compileTask.configure { finalizedBy(greaseTask) } - target.tasks.configureEach { - if (name == bundleTaskName) dependsOn(greaseTask) + greaseProcessTask.configure { + finalizedBy(greaseShadowTask) } } @@ -406,31 +561,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 as ComponentCreationConfig + 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,27 +615,24 @@ 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 as ComponentCreationConfig + 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()}" } } } } 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..52f22d6 --- /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..76bfdf1 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,56 @@ 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) +} + +private class ArtifactsFileCollection(private val fileCollections: List) : CompositeFileCollection() { + override fun getDisplayName(): String = "grease file collection" + + override fun visitChildren(visitor: Consumer) { + fileCollections.forEach(visitor::accept) + } +} -internal fun Project.greaseOf(flavor: com.android.builder.model.ProductFlavor) - = greaseOf(flavor.name) -internal fun Project.greaseOf(buildType: com.android.builder.model.BuildType) - = greaseOf(buildType.name) +internal fun Project.grease(isTransitive: Boolean) = greaseOf(if (isTransitive) "api" else "") -internal fun Project.greaseOf(variant: LibraryVariant) - = greaseOf(variant.name) +internal fun Project.greaseOf(buildType: com.android.builder.model.BuildType, isTransitive: Boolean = false) = + greaseOf(buildType.name.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 +68,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, @@ -68,33 +91,56 @@ private fun Project.createGrease(name: String): Configuration { return configuration } +private fun String.configurationName(isTransitive: Boolean) = if (isTransitive) nameOf(this, "api") 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 +151,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..eaed755 --- /dev/null +++ b/grease/src/main/kotlin/io/deepmedia/tools/grease/task.kt @@ -0,0 +1,69 @@ +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) +} + +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 index 1a80865..92ad8f2 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -1,16 +1,19 @@ plugins { - id("com.android.library") + alias(libs.plugins.android.library) id("io.deepmedia.tools.grease") } +grease { + relocationPrefix = "temp" +} + android { - setCompileSdkVersion(29) + namespace = "io.deepmedia.tools.grease.sample" ndkVersion = "20.1.5948944" + compileSdk = 29 + defaultConfig { - setMinSdkVersion(21) - setTargetSdkVersion(29) - versionCode = 1 - versionName = "1.0" + minSdk = 21 // Configure manifest placeholders to test that they are correctly replaced // in our manifest processing step. @@ -23,19 +26,27 @@ android { } // Configure proguard files. - proguardFiles(getDefaultProguardFile(com.android.build.gradle.ProguardFiles.ProguardFile.OPTIMIZE.fileName), "proguard-rules.pro") + 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" } + flavorDimensions.addAll(listOf("color", "shape")) + productFlavors { + create("blue") { dimension = "color" } + create("green") { dimension = "color" } + create("circle") { dimension = "shape" } + create("triangle") { dimension = "shape" } + } } - buildTypes["debug"].isMinifyEnabled = false - buildTypes["release"].isMinifyEnabled = true + buildTypes { + release { + isMinifyEnabled = true + } + } externalNativeBuild { cmake { @@ -44,17 +55,18 @@ android { } } -configurations.configureEach { - if (name == "greaseGreenCircleDebug") isTransitive = false -} - dependencies { + greaseApi("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 - grease("androidx.core:core:1.3.2") - // Includes native libraries - greaseRelease("org.tensorflow:tensorflow-lite:2.3.0") - // Manifest changes, layout resources + implementation("androidx.lifecycle:lifecycle-runtime:2.8.4") + afterEvaluate { - add("greaseGreenCircleDebug","com.otaliastudios:cameraview:2.6.3") + // Includes native libraries + "greaseApi"("org.tensorflow:tensorflow-lite:2.3.0") + // Manifest changes, layout resources + "grease"("com.otaliastudios:cameraview:2.7.2") } } \ No newline at end of file diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index b9035a1..706adb1 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java b/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java index 3923a35..ccd27b2 100644 --- a/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java +++ b/sample/src/main/java/io/deepmedia/tools/grease/sample/SampleActivity.java @@ -1,7 +1,6 @@ package io.deepmedia.tools.grease.sample; 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/settings.gradle.kts b/settings.gradle.kts index daccef8..ea26650 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,21 @@ -include(":sample") -include(":grease") +pluginManagement { + includeBuild("./grease") + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + @Suppress("UnstableApiUsage") + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "grease" + +include(":sample") \ No newline at end of file