diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b2de5ac..bbacc6fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ jobs: strategy: fail-fast: false # We want to see all results matrix: - java: [ '11' ] - agp: ['7.0.0', '7.1.0-alpha05'] + java: ['11'] + agp: ['7.1.0', '7.2.0-beta01'] tracerefs: [true, false] job: ['instrumentation', 'plugin'] steps: diff --git a/build.gradle b/build.gradle index 0ca74bec..1c5e22a3 100644 --- a/build.gradle +++ b/build.gradle @@ -22,10 +22,10 @@ buildscript { mavenCentral() } - String defaultAgpVersion = "7.0.0" + String defaultAgpVersion = "7.1.0" String agpVersion = findProperty("keeperTest.agpVersion")?.toString() ?: defaultAgpVersion dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" classpath "com.android.tools.build:gradle:$agpVersion" classpath "com.slack.keeper:keeper" } diff --git a/docs/configuration.md b/docs/configuration.md index 315cb962..488b0450 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,33 @@ -Keeper's default behavior with no configuration will enable it for only buildTypes that set -`minifyEnabled` to true. This may not be what you want for your actual production builds that you -plan to distribute. +## Basic Configuration + +In order to enable Keeper on a particular variant, you must register the `KeeperVariantMarker` +extension in AGP's `VariantBuilder` API like so: + +```kotlin +androidComponents { + beforeVariants { builder -> + if (shouldRunKeeperOnVariant()) { + builder.optInToKeeper() // Helpful extension function + } + } +} +``` + +Or in Groovy + +```kotlin +androidComponents { + beforeVariants { builder -> + if (shouldRunKeeperOnVariant(builder)) { + builder.registerExtension(KeeperVariantMarker.class, KeeperVariantMarker.INSTANCE) + } + } +} +``` + +Keeper's default behavior with no configuration effectively be a no-op, which isn't what you want! + +## Advanced Configuration Normally, your app variant's minification task doesn't depend on compilation of its corresponding `androidTest` variant. This means you can call `assembleRelease` and `assembleAndroidTestRelease` @@ -9,9 +36,9 @@ Normally, your app variant's minification task doesn't depend on compilation of you likely _do_ want these "test-only" APIs removed if possible though. There are a few patterns to better control this behavior via Gradle property. -## Simplest solution +### Simplest solution -The simplest solution is to add a new build type that extends release but is solely used for these +The simplest solution is to add a new build type that extends `release` but is solely used for these tests. This way it's identical to release in everything except the name. ```groovy @@ -69,27 +96,6 @@ if (!hasProperty("productionBuild")) { } ``` -### Variant filter - -By default, Keeper will run on any app variant that sets `minifyEnabled` to true. - -Alternatively, you can specify a `variantFilter` on the `keeper` extension and dynamically configure -which variants Keeper operates on. This is nearly identical to AGP's native `variantFilter` API except -that there is no `defaultConfig` property. - -**Note:** Variants with different `enabled` values will have to be compiled separately. This is common -in most multi-variant projects anyway, but something to be aware of. - -```groovy -keeper { - variantFilter { - if (name == "variantThatShouldTotallyBeIgnored") { - setIgnore(true) - } - } -} -``` - ### Everyone's project is different, so you should do whatever works for you! We're open to suggestions diff --git a/docs/index.md b/docs/index.md index 5d11e6fd..eaec4f9c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,24 +10,25 @@ result in runtime errors if APIs used by tests are removed. This is (really) useful _only_ if you run your instrumentation tests against your minified release builds! If you don't run these tests against minified builds, then you don't need this plugin. The -build type that you test against is controlled by the `testBuildType` flag, which is set to +build type that you test against is controlled by the `testBuildType` flag, which is set to `debug` by default. This is a workaround until AGP supports this: https://issuetracker.google.com/issues/126429384. **Note:** Keeper uses private APIs from AGP and could break between releases. It is currently -tested against AGP version `7.0.0` and `7.1.0-alpha05` (or whatever `ci_agp_version` env +tested against AGP version `7.1.0` and `7.2.0-beta01` (or whatever `ci_agp_version` env vars are described [here](https://github.com/slackhq/keeper/blob/main/.github/workflows/ci.yml). ## Installation Keeper is distributed via Maven Central. Apply the keeper Gradle plugin in your application's -build.gradle. Keeper requires Gradle 7.0 or higher. +build.gradle. Keeper requires Gradle 7.0 or higher and AGP 7.1.0 or higher. Keeper can be consumed via regular gradle `plugins {}` block. ```kotlin plugins { + id("com.android.application") // <- Keeper only works with com.android.application! id("com.slack.keeper") version "x.y.z" } ``` @@ -58,11 +59,7 @@ apply plugin: "com.android.application" // <- Keeper only works with com.android apply plugin: "com.slack.keeper" ``` -Note that Keeper _must_ be applied after the Android gradle plugin. - -Optional configuration options can be found on the [Configuration page](configuration.md). - -As of 0.11.0, Keeper requires at least AGP 7.0.0. +Full configuration defaults can be found on the [Configuration page](configuration.md). Snapshots of the development version are available in [Sonatype's `snapshots` repository][snapshots]. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05679dc3..2e6e5897 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882e..1b6c7873 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# 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. @@ -17,67 +17,101 @@ # ############################################################################## -## -## 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/master/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_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${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"' # 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 - ;; - MSYS* | 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 @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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 @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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" = "true" -o "$msys" = "true" ] ; 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 +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # 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=`expr $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; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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. +# -# 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" +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/keeper-gradle-plugin/api/keeper-gradle-plugin.api b/keeper-gradle-plugin/api/keeper-gradle-plugin.api index b2ed64f8..5141cb39 100644 --- a/keeper-gradle-plugin/api/keeper-gradle-plugin.api +++ b/keeper-gradle-plugin/api/keeper-gradle-plugin.api @@ -35,7 +35,7 @@ public final class com/slack/keeper/InferAndroidTestKeepRules$Companion { public final fun invoke (Ljava/lang/String;Lorg/gradle/api/tasks/TaskProvider;Lorg/gradle/api/tasks/TaskProvider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Provider;Lorg/gradle/api/provider/Property;Lorg/gradle/api/provider/Property;Lorg/gradle/api/provider/ListProperty;Lorg/gradle/api/provider/Property;Lorg/gradle/api/provider/ListProperty;Lorg/gradle/api/artifacts/Configuration;)Lkotlin/jvm/functions/Function1; } -public class com/slack/keeper/KeeperExtension { +public abstract class com/slack/keeper/KeeperExtension { public fun (Lorg/gradle/api/model/ObjectFactory;)V public final fun getAutomaticR8RepoManagement ()Lorg/gradle/api/provider/Property; public final fun getEmitDebugInformation ()Lorg/gradle/api/provider/Property; @@ -43,7 +43,6 @@ public class com/slack/keeper/KeeperExtension { public final fun getEnableL8RuleSharing ()Lorg/gradle/api/provider/Property; public final fun getR8JvmArgs ()Lorg/gradle/api/provider/ListProperty; public final fun traceReferences (Lorg/gradle/api/Action;)V - public final fun variantFilter (Lorg/gradle/api/Action;)V } public final class com/slack/keeper/KeeperPlugin : org/gradle/api/Plugin { @@ -56,14 +55,12 @@ public final class com/slack/keeper/KeeperPlugin : org/gradle/api/Plugin { public fun apply (Lorg/gradle/api/Project;)V } -public final class com/slack/keeper/KeeperPlugin$configureKeepRulesGeneration$2$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { - public fun (Lkotlin/jvm/functions/Function1;)V - public final synthetic fun execute (Ljava/lang/Object;)V +public final class com/slack/keeper/KeeperVariantMarker { + public static final field INSTANCE Lcom/slack/keeper/KeeperVariantMarker; } -public final class com/slack/keeper/KeeperPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { - public fun (Lkotlin/jvm/functions/Function1;)V - public final synthetic fun execute (Ljava/lang/Object;)V +public final class com/slack/keeper/KeeperVariantMarkerKt { + public static final fun optInToKeeper (Lcom/android/build/api/variant/VariantBuilder;)V } public abstract class com/slack/keeper/TraceReferences { @@ -80,10 +77,3 @@ public abstract class com/slack/keeper/VariantClasspathJar : com/slack/keeper/Ba public abstract fun getClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; } -public abstract interface class com/slack/keeper/VariantFilter { - public abstract fun getBuildType ()Lcom/android/builder/model/BuildType; - public abstract fun getFlavors ()Ljava/util/List; - public abstract fun getName ()Ljava/lang/String; - public abstract fun setIgnore (Z)V -} - diff --git a/keeper-gradle-plugin/build.gradle.kts b/keeper-gradle-plugin/build.gradle.kts index b16284a8..c8d07fe8 100644 --- a/keeper-gradle-plugin/build.gradle.kts +++ b/keeper-gradle-plugin/build.gradle.kts @@ -20,11 +20,10 @@ import java.net.URL plugins { `kotlin-dsl` `java-gradle-plugin` - kotlin("jvm") version "1.5.21" - kotlin("kapt") version "1.5.21" - id("org.jetbrains.dokka") version "1.5.0" - id("com.vanniktech.maven.publish") version "0.17.0" - id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.6.0" + kotlin("jvm") version "1.6.10" + id("org.jetbrains.dokka") version "1.6.10" + id("com.vanniktech.maven.publish") version "0.18.0" + id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.8.0" } buildscript { @@ -43,11 +42,16 @@ repositories { tasks.withType().configureEach { kotlinOptions { jvmTarget = "1.8" - // Because Gradle's Kotlin handling is stupid - apiVersion = "1.4" - languageVersion = "1.4" + // Because Gradle's Kotlin handling is stupid, this falls out of date quickly + apiVersion = "1.5" + languageVersion = "1.5" // @Suppress("SuspiciousCollectionReassignment") // freeCompilerArgs += listOf("-progressive") + // We use class SAM conversions because lambdas compiled into invokedynamic are not + // Serializable, which causes accidental headaches with Gradle configuration caching. It's + // easier for us to just use the previous anonymous classes behavior + @Suppress("SuspiciousCollectionReassignment") + freeCompilerArgs += "-Xsam-conversions=class" } } @@ -62,8 +66,13 @@ sourceSets { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +tasks.withType().configureEach { + options.release.set(8) } gradlePlugin { @@ -94,15 +103,15 @@ tasks.withType().configureEach { } } -val defaultAgpVersion = "7.0.0" +val defaultAgpVersion = "7.1.0" val agpVersion = findProperty("keeperTest.agpVersion")?.toString() ?: defaultAgpVersion // See https://github.com/slackhq/keeper/pull/11#issuecomment-579544375 for context val releaseMode = hasProperty("keeper.releaseMode") dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.21") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21") - implementation("com.android:zipflinger:7.0.0") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.10") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") + compileOnly("com.android:zipflinger:7.1.0") if (releaseMode) { compileOnly("com.android.tools.build:gradle:$defaultAgpVersion") @@ -110,11 +119,8 @@ dependencies { implementation("com.android.tools.build:gradle:$agpVersion") } - compileOnly("com.google.auto.service:auto-service-annotations:1.0") - kapt("com.google.auto.service:auto-service:1.0") - testImplementation("com.squareup:javapoet:1.13.0") testImplementation("com.squareup:kotlinpoet:1.9.0") - testImplementation("com.google.truth:truth:1.1.2") + testImplementation("com.google.truth:truth:1.1.3") testImplementation("junit:junit:4.13.2") } diff --git a/keeper-gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/keeper-gradle-plugin/gradle/wrapper/gradle-wrapper.properties index 05679dc3..2e6e5897 100644 --- a/keeper-gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/keeper-gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/keeper-gradle-plugin/gradlew b/keeper-gradle-plugin/gradlew index 744e882e..1b6c7873 100755 --- a/keeper-gradle-plugin/gradlew +++ b/keeper-gradle-plugin/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# 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. @@ -17,67 +17,101 @@ # ############################################################################## -## -## 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/master/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_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${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"' # 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 - ;; - MSYS* | 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 @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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 @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." 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*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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" = "true" -o "$msys" = "true" ] ; 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 +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # 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=`expr $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; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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. +# -# 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" +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/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperExtension.kt b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperExtension.kt index 790ce8ec..65648f2f 100644 --- a/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperExtension.kt +++ b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperExtension.kt @@ -16,8 +16,6 @@ package com.slack.keeper -import com.android.builder.model.BuildType -import com.android.builder.model.ProductFlavor import org.gradle.api.Action import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty @@ -29,20 +27,7 @@ import javax.inject.Inject import kotlin.DeprecationLevel.ERROR /** Configuration for the [InferAndroidTestKeepRules]. */ -public open class KeeperExtension @Inject constructor(objects: ObjectFactory) { - @Suppress("PropertyName") - internal var _variantFilter: Action? = null - - /** - * Applies a variant filter for Android. Note that the variant tested is the _app_ variant, not - * the test variant. - * - * @param action the configure action for the [VariantFilter] - */ - public fun variantFilter(action: Action) { - this._variantFilter = action - } - +public abstract class KeeperExtension @Inject constructor(objects: ObjectFactory) { /** * Controls whether or not to automatically add the R8 repository for dependencies. Default is * true. Disable if you want to define your own repo for fetching the R8 dependency. @@ -86,28 +71,6 @@ public open class KeeperExtension @Inject constructor(objects: ObjectFactory) { } } -public interface VariantFilter { - /** - * Indicate whether or not to ignore this particular variant. Default is false. - */ - public fun setIgnore(ignore: Boolean) - - /** - * Returns the Build Type. - */ - public val buildType: BuildType - - /** - * Returns the list of flavors, or an empty list. - */ - public val flavors: List - - /** - * Returns the unique variant name. - */ - public val name: String -} - public abstract class TraceReferences @Inject constructor(objects: ObjectFactory) { /** * Controls whether or not to use the new experimental TraceReferences entry-point. diff --git a/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperPlugin.kt b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperPlugin.kt index b2a2c954..100ac1d4 100644 --- a/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperPlugin.kt +++ b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperPlugin.kt @@ -18,16 +18,16 @@ package com.slack.keeper +import com.android.build.api.artifact.MultipleArtifact +import com.android.build.api.variant.AndroidTest +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.ApplicationVariant import com.android.build.gradle.AppExtension -import com.android.build.gradle.api.BaseVariant -import com.android.build.gradle.api.TestVariant import com.android.build.gradle.internal.publishing.AndroidArtifacts import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType import com.android.build.gradle.internal.tasks.L8DexDesugarLibTask -import com.android.build.gradle.internal.tasks.ProguardConfigurableTask import com.android.build.gradle.internal.tasks.R8Task -import com.android.builder.model.BuildType -import com.android.builder.model.ProductFlavor +import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task @@ -94,7 +94,7 @@ public class KeeperPlugin : Plugin { const val PRINTUSES_DEFAULT_VERSION = "1.6.53" const val TRACE_REFERENCES_DEFAULT_VERSION = "3.0.9-dev" const val CONFIGURATION_NAME = "keeperR8" - private val MIN_GRADLE_VERSION = GradleVersion.version("6.0") + private val MIN_GRADLE_VERSION = GradleVersion.version("7.2") fun interpolateR8TaskName(variantName: String): String { return "minify${variantName.capitalize(Locale.US)}WithR8" @@ -108,20 +108,18 @@ public class KeeperPlugin : Plugin { override fun apply(project: Project) { val gradleVersion = GradleVersion.version(project.gradle.gradleVersion) check(gradleVersion >= MIN_GRADLE_VERSION) { - "Keeper requires Gradle 6.0 or later." + "Keeper requires Gradle ${MIN_GRADLE_VERSION.version} or later." } project.pluginManager.withPlugin("com.android.application") { val appExtension = project.extensions.getByType() + val appComponentsExtension = + project.extensions.getByType() val extension = project.extensions.create("keeper") - project.configureKeepRulesGeneration(appExtension, extension) - project.configureL8(appExtension, extension) + project.configureKeepRulesGeneration(appExtension, appComponentsExtension, extension) + project.configureL8(appExtension, appComponentsExtension, extension) } } - private fun Project.r8TaskFor(variantName: String): TaskProvider { - return tasks.named(interpolateR8TaskName(variantName)) - } - /** * Configures L8 support via rule sharing and clearing androidTest dex file generation by patching * the respective app and test [L8DexDesugarLibTask] tasks. @@ -148,28 +146,37 @@ public class KeeperPlugin : Plugin { * androidTest [L8DexDesugarLibTask] task. */ private fun Project.configureL8( - appExtension: AppExtension, - extension: KeeperExtension + appExtension: AppExtension, + appComponentsExtension: ApplicationAndroidComponentsExtension, + extension: KeeperExtension ) { - afterEvaluate { + appComponentsExtension.onApplicableVariants(project, + appExtension, + verifyMinification = false + ) { testVariant, appVariant -> + // TODO ideally move to components entirely https://issuetracker.google.com/issues/199411020 if (appExtension.compileOptions.isCoreLibraryDesugaringEnabled) { - appExtension.onApplicableVariants(project, extension) { testVariant, appVariant -> - - // First merge the L8 rules into the app's L8 task - val inputFiles = r8TaskFor(testVariant.name) - .flatMap { it.projectOutputKeepRules } + // namedLazy nesting here is unfortunate but necessary because these R8/L8 tasks don't + // exist yet during this callback. https://issuetracker.google.com/issues/199509581 + project + .namedLazy(interpolateL8TaskName(appVariant.name)) { l8Task -> + // First merge the L8 rules into the app's L8 task + project.namedLazy(interpolateR8TaskName(testVariant.name)) { provider -> + l8Task.configure { + keepRulesFiles.from(provider.flatMap { it.projectOutputKeepRules }) + } + } - tasks - .named(interpolateL8TaskName(appVariant.name)) + l8Task .configure { val taskName = name - keepRulesFiles.from(inputFiles) keepRulesConfigurations.set(listOf("-dontobfuscate")) val diagnosticOutputDir = layout.buildDirectory.dir( - "$INTERMEDIATES_DIR/l8-diagnostics/$taskName") - .forUseAtConfigurationTime() - .get() - .asFile + "$INTERMEDIATES_DIR/l8-diagnostics/$taskName" + ) + .forUseAtConfigurationTime() + .get() + .asFile // We can't actually declare this because AGP's NonIncrementalTask will clear it // during the task action @@ -179,48 +186,50 @@ public class KeeperPlugin : Plugin { if (extension.emitDebugInformation.getOrElse(false)) { doFirst { val mergedFilesContent = keepRulesFiles.files.asSequence() - .flatMap { it.walkTopDown() } - .filterNot { it.isDirectory } - .joinToString("\n") { - "# Source: ${it.absolutePath}\n${it.readText()}" - } + .flatMap { it.walkTopDown() } + .filterNot { it.isDirectory } + .joinToString("\n") { + "# Source: ${it.absolutePath}\n${it.readText()}" + } val configurations = keepRulesConfigurations.orNull.orEmpty() - .joinToString( - "\n", - prefix = "# Source: extra configurations\n" - ) + .joinToString( + "\n", + prefix = "# Source: extra configurations\n" + ) File(diagnosticOutputDir, "patchedL8Rules.pro") - .apply { - if (exists()) { - delete() - } - parentFile.mkdirs() - createNewFile() + .apply { + if (exists()) { + delete() } - .writeText("$mergedFilesContent\n$configurations") + parentFile.mkdirs() + createNewFile() + } + .writeText("$mergedFilesContent\n$configurations") } } } + } - // Now clear the outputs from androidTest's L8 task to end with - tasks - .named(interpolateL8TaskName(testVariant.name)) - .configure { + // Now clear the outputs from androidTest's L8 task to end with + project + .namedLazy(interpolateL8TaskName(testVariant.name)) { + it.configure { doLast { clearDir(desugarLibDex.asFile.get()) } } - } + } } } } private fun Project.configureKeepRulesGeneration( - appExtension: AppExtension, - extension: KeeperExtension + appExtension: AppExtension, + appComponentsExtension: ApplicationAndroidComponentsExtension, + extension: KeeperExtension ) { // Set up r8 configuration val r8Configuration = configurations.create(CONFIGURATION_NAME) { @@ -239,146 +248,124 @@ public class KeeperPlugin : Plugin { } val androidJarRegularFileProvider = layout.file(provider { - resolveAndroidEmbeddedJar(appExtension, "android.jar", checkIfExisting = true) + resolveAndroidEmbeddedJar(appExtension, appComponentsExtension, "android.jar", + checkIfExisting = true) }) val androidTestJarRegularFileProvider = layout.file(provider { - resolveAndroidEmbeddedJar(appExtension, "optional/android.test.base.jar", - checkIfExisting = false) + resolveAndroidEmbeddedJar( + appExtension, + appComponentsExtension, + "optional/android.test.base.jar", + checkIfExisting = false + ) }) - appExtension.testVariants.configureEach { - val appVariant = testedVariant - val extensionFilter = extension._variantFilter - val ignoredVariant = extensionFilter?.let { - logger.debug( - "$TAG Resolving ignored status for android variant ${appVariant.name}") - val filter = VariantFilterImpl(appVariant) - it.execute(filter) - logger.debug("$TAG Variant '${appVariant.name}' ignored? ${filter._ignored}") - filter._ignored - } ?: !appVariant.buildType.isMinifyEnabled - if (ignoredVariant) { - return@configureEach - } - if (!appVariant.buildType.isMinifyEnabled) { - logger.error(""" - Keeper is configured to generate keep rules for the "${appVariant.name}" build variant, but the variant doesn't - have minification enabled, so the keep rules will have no effect. To fix this warning, either avoid applying - the Keeper plugin when android.testBuildType = ${appVariant.buildType.name}, or use the variant filter feature - of the DSL to exclude "${appVariant.name}" from keeper: - keeper { - variantFilter { - setIgnore(name != ) - } - } - """.trimIndent()) - return@configureEach - } - } - appExtension.onApplicableVariants(project, extension) { testVariant, appVariant -> + appComponentsExtension.onApplicableVariants( + project, + appExtension, + verifyMinification = true + ) { testVariant, appVariant -> val intermediateAppJar = createIntermediateAppJar( - appVariant = appVariant, - emitDebugInfo = extension.emitDebugInformation + appVariant = appVariant, + emitDebugInfo = extension.emitDebugInformation ) val intermediateAndroidTestJar = createIntermediateAndroidTestJar( - emitDebugInfo = extension.emitDebugInformation, - testVariant = testVariant, - appJarsProvider = intermediateAppJar.flatMap { it.appJarsFile } + emitDebugInfo = extension.emitDebugInformation, + testVariant = testVariant, + appJarsProvider = intermediateAppJar.flatMap { it.appJarsFile } ) val inferAndroidTestUsageProvider = tasks.register( - "infer${testVariant.name.capitalize(Locale.US)}KeepRulesForKeeper", - InferAndroidTestKeepRules( - variantName = testVariant.name, - androidTestJarProvider = intermediateAndroidTestJar, - releaseClassesJarProvider = intermediateAppJar, - androidJar = androidJarRegularFileProvider, - androidTestJar = androidTestJarRegularFileProvider, - automaticallyAddR8Repo = extension.automaticR8RepoManagement, - enableAssertions = extension.enableAssertions, - extensionJvmArgs = extension.r8JvmArgs, - traceReferencesEnabled = extension.traceReferences.enabled, - traceReferencesArgs = extension.traceReferences.arguments, - r8Configuration = r8Configuration - ) + "infer${testVariant.name.capitalize(Locale.US)}KeepRulesForKeeper", + InferAndroidTestKeepRules( + variantName = testVariant.name, + androidTestJarProvider = intermediateAndroidTestJar, + releaseClassesJarProvider = intermediateAppJar, + androidJar = androidJarRegularFileProvider, + androidTestJar = androidTestJarRegularFileProvider, + automaticallyAddR8Repo = extension.automaticR8RepoManagement, + enableAssertions = extension.enableAssertions, + extensionJvmArgs = extension.r8JvmArgs, + traceReferencesEnabled = extension.traceReferences.enabled, + traceReferencesArgs = extension.traceReferences.arguments, + r8Configuration = r8Configuration + ) ) val prop = layout.dir( - inferAndroidTestUsageProvider.flatMap { it.outputProguardRules.asFile }) - val testProguardFiles = testVariant.runtimeConfiguration - .proguardFiles() + inferAndroidTestUsageProvider.flatMap { it.outputProguardRules.asFile }) + val testProguardFiles = runtimeConfigurationFor(testVariant.name) + .proguardFiles() applyGeneratedRules(appVariant.name, prop, testProguardFiles) } } private fun resolveAndroidEmbeddedJar( - appExtension: AppExtension, - path: String, - checkIfExisting: Boolean + appExtension: AppExtension, + appComponentsExtension: ApplicationAndroidComponentsExtension, + path: String, + checkIfExisting: Boolean ): File { val compileSdkVersion = appExtension.compileSdkVersion - ?: error("No compileSdkVersion found") - val file = File("${appExtension.sdkDirectory}/platforms/${compileSdkVersion}/${path}") + ?: error("No compileSdkVersion found") + val file = File( + "${appComponentsExtension.sdkComponents.sdkDirectory.get().asFile}/platforms/${compileSdkVersion}/${path}") check(!checkIfExisting || file.exists()) { "No $path found! Expected to find it at: ${file.absolutePath}" } return file } - private fun AppExtension.onApplicableVariants( - project: Project, - extension: KeeperExtension, - body: (TestVariant, BaseVariant) -> Unit + private fun ApplicationAndroidComponentsExtension.onApplicableVariants( + project: Project, + appExtension: AppExtension, + verifyMinification: Boolean, + body: (AndroidTest, ApplicationVariant) -> Unit ) { - testVariants.configureEach { - val testVariant = this - val appVariant = testedVariant - val extensionFilter = extension._variantFilter - val ignoredVariant = extensionFilter?.let { - project.logger.debug( - "$TAG Resolving ignored status for android variant ${appVariant.name}") - val filter = VariantFilterImpl(appVariant) - it.execute(filter) - project.logger.debug("$TAG Variant '${appVariant.name}' ignored? ${filter._ignored}") - filter._ignored - } ?: false - if (ignoredVariant) { - return@configureEach - } - if (!appVariant.buildType.isMinifyEnabled) { - project.logger.error(""" - Keeper is configured to generate keep rules for the "${appVariant.name}" build variant, but the variant doesn't - have minification enabled, so the keep rules will have no effect. To fix this warning, either avoid applying - the Keeper plugin when android.testBuildType = ${appVariant.buildType.name}, or use the variant filter feature - of the DSL to exclude "${appVariant.name}" from keeper: - keeper { - variantFilter { - setIgnore(name != ) - } - } - """.trimIndent()) - return@configureEach - } + onVariants { appVariant -> + val buildType = appVariant.buildType ?: return@onVariants + // Look for our marker extension + appVariant.getExtension(KeeperVariantMarker::class.java) ?: return@onVariants + appVariant.androidTest?.let { testVariant -> + // TODO use only components after https://issuetracker.google.com/issues/199411018 + if (verifyMinification && !appExtension.buildTypes.getByName(buildType).isMinifyEnabled) { + project.logger.error( + """ + Keeper is configured to generate keep rules for the "${appVariant.name}" build variant, but the variant doesn't + have minification enabled, so the keep rules will have no effect. To fix this warning, either avoid applying + the Keeper plugin when android.testBuildType = $buildType or enable minification on this variant. + """.trimIndent() + ) + return@let + } - body(testVariant, appVariant) + body(testVariant, appVariant) + } } } private fun Project.applyGeneratedRules( - appVariant: String, - prop: Provider, - testProguardFiles: ArtifactCollection + appVariant: String, + prop: Provider, + testProguardFiles: ArtifactCollection ) { val targetName = interpolateR8TaskName(appVariant) - tasks.withType() - .matching { it.name == targetName } - .configureEach { - logger.debug( - "$TAG: Patching task '$name' with inferred androidTest proguard rules") - configurationFiles.from(prop) - configurationFiles.from(testProguardFiles.artifactFiles) - } + tasks.withType() + .matching { it.name == targetName } + .configureEach { + logger.debug( + "$TAG: Patching task '$name' with inferred androidTest proguard rules" + ) + configurationFiles.from(prop) + configurationFiles.from(testProguardFiles.artifactFiles) + } + } + + // TODO can this return a provider? + // TODO hopefully can be removed with https://issuetracker.google.com/issues/199436586 + private fun Project.runtimeConfigurationFor(variantName: String): Configuration { + return configurations.getByName("${variantName}RuntimeClasspath") } /** @@ -386,29 +373,26 @@ public class KeeperPlugin : Plugin { * This output is used in the inferAndroidTestUsage task. */ private fun Project.createIntermediateAndroidTestJar( - emitDebugInfo: Provider, - testVariant: TestVariant, - appJarsProvider: Provider + emitDebugInfo: Provider, + testVariant: AndroidTest, + appJarsProvider: Provider ): TaskProvider { return tasks.register( - "jar${testVariant.name.capitalize(Locale.US)}ClassesForKeeper") { + "jar${testVariant.name.capitalize(Locale.US)}ClassesForKeeper" + ) { group = KEEPER_TASK_GROUP this.emitDebugInfo.value(emitDebugInfo) this.appJarsFile.set(appJarsProvider) with(testVariant) { - from(layout.dir(javaCompileProvider.map { it.destinationDir })) - setArtifacts(runtimeConfiguration.classesJars()) - tasks.providerWithNameOrNull( - "compile${name.capitalize(Locale.US)}Kotlin") - ?.let { kotlinCompileTask -> - from(layout.dir(kotlinCompileTask.map { it.destinationDir })) - } + from(artifacts.getAll(MultipleArtifact.ALL_CLASSES_DIRS)) + setArtifacts(runtimeConfigurationFor(name).classesJars()) } val outputDir = layout.buildDirectory.dir("$INTERMEDIATES_DIR/${testVariant.name}") val diagnosticsDir = layout.buildDirectory.dir( - "$INTERMEDIATES_DIR/${testVariant.name}/diagnostics") + "$INTERMEDIATES_DIR/${testVariant.name}/diagnostics" + ) this.diagnosticsOutputDir.set(diagnosticsDir) archiveFile.set(outputDir.map { it.file("classes.jar") @@ -421,27 +405,23 @@ public class KeeperPlugin : Plugin { * output is used in the inferAndroidTestUsage task. */ private fun Project.createIntermediateAppJar( - appVariant: BaseVariant, - emitDebugInfo: Provider + appVariant: ApplicationVariant, + emitDebugInfo: Provider ): TaskProvider { return tasks.register( - "jar${appVariant.name.capitalize(Locale.US)}ClassesForKeeper") { + "jar${appVariant.name.capitalize(Locale.US)}ClassesForKeeper" + ) { group = KEEPER_TASK_GROUP this.emitDebugInfo.set(emitDebugInfo) with(appVariant) { - from(layout.dir(javaCompileProvider.map { it.destinationDir })) - setArtifacts(runtimeConfiguration.classesJars()) - - tasks.providerWithNameOrNull( - "compile${name.capitalize(Locale.US)}Kotlin") - ?.let { kotlinCompileTask -> - from(layout.dir(kotlinCompileTask.map { it.destinationDir })) - } + from(artifacts.getAll(MultipleArtifact.ALL_CLASSES_DIRS)) + setArtifacts(runtimeConfigurationFor(name).classesJars()) } val outputDir = layout.buildDirectory.dir("$INTERMEDIATES_DIR/${appVariant.name}") val diagnosticsDir = layout.buildDirectory.dir( - "$INTERMEDIATES_DIR/${appVariant.name}/diagnostics") + "$INTERMEDIATES_DIR/${appVariant.name}/diagnostics" + ) diagnosticsOutputDir.set(diagnosticsDir) archiveFile.set(outputDir.map { it.file("classes.jar") }) appJarsFile.set(outputDir.map { it.file("jars.txt") }) @@ -459,25 +439,15 @@ private fun Configuration.proguardFiles(): ArtifactCollection { private fun Configuration.artifactView(artifactType: ArtifactType): ArtifactCollection { return incoming - .artifactView { - attributes { - attribute( - AndroidArtifacts.ARTIFACT_TYPE, - artifactType.type - ) - } + .artifactView { + attributes { + attribute( + AndroidArtifacts.ARTIFACT_TYPE, + artifactType.type + ) } - .artifacts -} - -private inline fun TaskContainer.providerWithNameOrNull( - name: String -): TaskProvider? { - return try { - named(name) - } catch (e: UnknownTaskException) { - null - } + } + .artifacts } /** Copy of the stdlib version until it's stable. */ @@ -499,6 +469,37 @@ internal fun String.capitalize(locale: Locale): String { return this } +/** + * Similar to [TaskContainer.named], but waits until the task is registered if it doesn't exist, + * yet. If the task is never registered, then this method will throw an error after the + * configuration phase. + */ +private inline fun Project.namedLazy( + targetName: String, + crossinline action: (TaskProvider) -> Unit +) { + try { + action(tasks.named(targetName)) + return + } catch (ignored: UnknownTaskException) { + } + + var didRun = false + + tasks.withType { + if (name == targetName) { + action(tasks.named(name)) + didRun = true + } + } + + afterEvaluate { + if (!didRun) { + throw GradleException("Didn't find task $name with type ${T::class}.") + } + } +} + private fun clearDir(path: File) { if (!path.isDirectory) { if (path.exists()) { @@ -512,16 +513,3 @@ private fun clearDir(path: File) { path.listFiles()?.forEach(File::deleteRecursively) } - -private class VariantFilterImpl(variant: BaseVariant) : VariantFilter { - @Suppress("PropertyName") - var _ignored: Boolean = false - - override fun setIgnore(ignore: Boolean) { - _ignored = ignore - } - - override val buildType: BuildType = variant.buildType - override val flavors: List = variant.productFlavors - override val name: String = variant.name -} diff --git a/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperVariantMarker.kt b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperVariantMarker.kt new file mode 100644 index 00000000..136a5118 --- /dev/null +++ b/keeper-gradle-plugin/src/main/java/com/slack/keeper/KeeperVariantMarker.kt @@ -0,0 +1,24 @@ +package com.slack.keeper + +import com.android.build.api.variant.VariantBuilder + +/** + * Register this with [VariantBuilder.registerExtension] to opt this variant into + * Keeper. + * + * ``` + * androidComponents { + * beforeVariants { variantBuilder -> + * if (shouldRunKeeperOnVariant()) { + * variantBuilder.registerExtension(KeeperVariantMarker.class, KeeperVariantMarker) + * } + * } + * } + * ``` + */ +public object KeeperVariantMarker + +/** Shorthand to register Keeper on this variant. */ +public fun VariantBuilder.optInToKeeper() { + registerExtension(KeeperVariantMarker::class.java, KeeperVariantMarker) +} \ No newline at end of file diff --git a/keeper-gradle-plugin/src/test/kotlin/com/slack/keeper/KeeperFunctionalTest.kt b/keeper-gradle-plugin/src/test/kotlin/com/slack/keeper/KeeperFunctionalTest.kt index 938a4583..1b634edc 100644 --- a/keeper-gradle-plugin/src/test/kotlin/com/slack/keeper/KeeperFunctionalTest.kt +++ b/keeper-gradle-plugin/src/test/kotlin/com/slack/keeper/KeeperFunctionalTest.kt @@ -74,7 +74,7 @@ import com.squareup.kotlinpoet.ClassName as KpClassName * ``` */ @RunWith(Parameterized::class) -class KeeperFunctionalTest(private val minifierType: MinifierType) { +internal class KeeperFunctionalTest(private val minifierType: MinifierType) { companion object { @JvmStatic @@ -89,7 +89,7 @@ class KeeperFunctionalTest(private val minifierType: MinifierType) { * * @property taskName The representation in a gradle task name. * @property expectedRules The expected generated rules outputted by `-printconfiguration`. - * @property gradleArgs The requisite gradle invocation parameters to enable this minifier. + * @property keeperExtraConfig Extra [KeeperExtension] configuration. */ enum class MinifierType( val taskName: String, @@ -153,13 +153,13 @@ class KeeperFunctionalTest(private val minifierType: MinifierType) { } } - // Asserts that our variant filter properly filters things out. In our fixture project, the + // Asserts that our extension marker properly opts variants in. In our fixture project, the // "externalRelease" build variant will be ignored, while tasks will be generated for the // "internalRelease" variant. @Test - fun variantFilter() { + fun extensionMarker() { val (projectDir, _) = prepareProject(temporaryFolder, buildGradleFile("release", - keeperExtraConfig = KeeperExtraConfig.ONLY_INTERNAL_RELEASE)) + androidExtraConfig = AndroidExtraConfig.ONLY_INTERNAL_RELEASE)) val result = runGradle(projectDir, "assembleExternalRelease", "assembleInternalRelease", "-x", "lintVitalExternalRelease", "-x", "lintVitalInternalRelease") @@ -175,10 +175,10 @@ class KeeperFunctionalTest(private val minifierType: MinifierType) { // Asserts that if Keeper was configured to create keep rules for a variant that isn't minified, // an error will be emitted, and the tasks won't be created. @Test - fun variantFilterWarning() { - // internalDebug variant isn't minified, but the variantFilter includes it. + fun extensionMarkerWarning() { + // internalDebug variant isn't minified, but the variant is opted into Keeper. val (projectDir, _) = prepareProject(temporaryFolder, buildGradleFile("debug", - keeperExtraConfig = KeeperExtraConfig.ONLY_INTERNAL_DEBUG)) + androidExtraConfig = AndroidExtraConfig.ONLY_INTERNAL_DEBUG)) val result = runGradle(projectDir, "assembleInternalDebug") @@ -296,25 +296,56 @@ private val TEST_PROGUARD_RULES = """ -dontnote ** """.trimIndent() -enum class KeeperExtraConfig(val groovy: String) { +internal enum class KeeperExtraConfig(val groovy: String) { NONE(""), - ONLY_INTERNAL_RELEASE( + TRACE_REFERENCES_ENABLED( + """ + traceReferences {} + """.trimIndent() + ); +} + +internal enum class AndroidExtraConfig(val groovy: String) { + ONLY_EXTERNAL_STAGING( """ - variantFilter { - setIgnore(name != "internalRelease") + androidComponents { + beforeVariants(selector().all()) { variantBuilder -> + if (variantBuilder.name == "externalStaging") { + variantBuilder.registerExtension( + com.slack.keeper.KeeperVariantMarker.class, + com.slack.keeper.KeeperVariantMarker.INSTANCE + ) + } + } } """.trimIndent() ), - ONLY_INTERNAL_DEBUG( + ONLY_INTERNAL_RELEASE( """ - variantFilter { - setIgnore(name != "internalDebug") + androidComponents { + beforeVariants(selector().all()) { variantBuilder -> + if (variantBuilder.name == "internalRelease") { + variantBuilder.registerExtension( + com.slack.keeper.KeeperVariantMarker.class, + com.slack.keeper.KeeperVariantMarker.INSTANCE + ) + } + } } """.trimIndent() ), - TRACE_REFERENCES_ENABLED( + ONLY_INTERNAL_DEBUG( """ - traceReferences {} + androidComponents { + beforeVariants(selector().all()) { variantBuilder -> + if (variantBuilder.name == "internalDebug") { + variantBuilder.registerExtension( + com.slack.keeper.KeeperVariantMarker.class, + com.slack.keeper.KeeperVariantMarker.INSTANCE + ) + } + } + } """.trimIndent() ); } @@ -324,6 +355,7 @@ private fun buildGradleFile( testBuildType: String, automaticR8RepoManagement: Boolean = true, keeperExtraConfig: KeeperExtraConfig = KeeperExtraConfig.NONE, + androidExtraConfig: AndroidExtraConfig = AndroidExtraConfig.ONLY_EXTERNAL_STAGING, emitDebugInformation: Boolean = false, extraDependencies: Map = emptyMap() ): String { @@ -338,9 +370,9 @@ private fun buildGradleFile( dependencies { // Note: this version doesn't really matter, the plugin's version will override it in the test - classpath "com.android.tools.build:gradle:4.2.1" + classpath "com.android.tools.build:gradle:7.1.0" //noinspection DifferentKotlinGradleVersion - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" } } @@ -405,6 +437,8 @@ private fun buildGradleFile( } } + ${androidExtraConfig.groovy} + keeper { emitDebugInformation.set($emitDebugInformation) automaticR8RepoManagement.set($automaticR8RepoManagement) diff --git a/sample-libraries/b/build.gradle b/sample-libraries/b/build.gradle index e5275942..c6af3c76 100644 --- a/sample-libraries/b/build.gradle +++ b/sample-libraries/b/build.gradle @@ -34,5 +34,5 @@ android { dependencies { implementation project(":sample-libraries:c") - implementation "com.squareup.okio:okio:2.10.0" + implementation "com.squareup.okio:okio:3.0.0" } diff --git a/sample-libraries/c/build.gradle b/sample-libraries/c/build.gradle index e8318a21..57a4e082 100644 --- a/sample-libraries/c/build.gradle +++ b/sample-libraries/c/build.gradle @@ -24,5 +24,5 @@ sourceSets { } dependencies { - implementation "com.squareup.okio:okio:2.10.0" + implementation "com.squareup.okio:okio:3.0.0" } diff --git a/sample/build.gradle b/sample/build.gradle index 20a49a31..eb9526be 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -14,9 +14,11 @@ * limitations under the License. */ +import com.android.build.api.variant.VariantBuilder import com.android.build.gradle.internal.tasks.L8DexDesugarLibTask import com.android.build.gradle.internal.tasks.R8Task import com.slack.keeper.InferAndroidTestKeepRules +import com.slack.keeper.KeeperVariantMarker import kotlin.PreconditionsKt apply plugin: 'com.android.application' @@ -86,20 +88,21 @@ boolean useTraceReferences = providers.gradleProperty("keeperTest.enableTraceRef .map { Boolean.parseBoolean(it) } .orElse(false) -keeper { - // Example: Only enable on "externalStaging" - variantFilter { - setIgnore(name != "externalStaging") +// Example: Only enable on "externalStaging" +androidComponents { + beforeVariants(selector().all()) { VariantBuilder variantBuilder -> + if (variantBuilder.name == "externalStaging") { + variantBuilder.registerExtension(KeeperVariantMarker.class, KeeperVariantMarker.INSTANCE) + } } +} +keeper { // Example: emit extra debug information during Keeper's execution. - emitDebugInformation = true + emitDebugInformation.set(true) // Example: automatic R8 repo management (more below) - automaticR8RepoManagement = false - - // Example: share proguard rules for L8 (library desugaring). - enableL8RuleSharing = true + automaticR8RepoManagement.set(false) // Uncomment this line to debug the R8 from a remote instance. //r8JvmArgs.addAll(Arrays.asList("-Xdebug", "-Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y")) @@ -108,7 +111,7 @@ keeper { // Enable the new experimental TraceReferences tool. traceReferences { // Don't fail the build if missing definitions are found. - arguments = ["--map-diagnostics:MissingDefinitionsDiagnostic", "error", "info"] + arguments.set(["--map-diagnostics:MissingDefinitionsDiagnostic", "error", "info"]) } } } @@ -195,11 +198,11 @@ dependencies { coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" androidTestImplementation project(":sample-libraries:c") - androidTestImplementation "com.squareup.okio:okio:2.10.0" - androidTestImplementation "androidx.annotation:annotation:1.2.0" + androidTestImplementation "com.squareup.okio:okio:3.0.0" + androidTestImplementation "androidx.annotation:annotation:1.3.0" androidTestImplementation "androidx.test:rules:1.4.0" androidTestImplementation "androidx.test:runner:1.4.0" - androidTestUtil "androidx.test:orchestrator:1.4.0" + androidTestUtil "androidx.test:orchestrator:1.4.1" androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "junit:junit:4.13.2" androidTestImplementation "com.google.truth:truth:1.1.3"