Написание плагинов Gradle

Плагин Android Gradle (AGP) — официальная система сборки приложений для Android. Он поддерживает компиляцию различных типов исходного кода и их объединение в приложение, которое можно запустить на физическом устройстве Android или в эмуляторе.

AGP содержит точки расширения для плагинов, которые позволяют управлять входными данными сборки и расширять её функциональность с помощью новых шагов, которые можно интегрировать со стандартными задачами сборки. В предыдущих версиях AGP не было официальных API, чётко разделённых с внутренними реализациями. Начиная с версии 7.0, AGP предлагает набор официальных, стабильных API , на которые вы можете положиться.

Жизненный цикл API AGP

AGP следует жизненному циклу функций Gradle для обозначения состояния своих API:

  • Внутренний : не предназначен для публичного использования.
  • Инкубация : доступно для публичного использования, но не является финальным вариантом, что означает, что они могут быть несовместимы с финальной версией.
  • Публично : доступно для публичного использования и стабильно
  • Устаревший : больше не поддерживается и заменен новыми API.

Политика устаревания

AGP развивается по мере устаревания старых API и их замены новыми, стабильными API и новым предметно-ориентированным языком (DSL). Эта эволюция охватит несколько выпусков AGP, и вы можете узнать больше о ней на временной шкале миграции AGP API/DSL .

Если API AGP будут объявлены устаревшими, будь то для данной миграции или по какой-либо другой причине, они останутся доступны в текущем основном выпуске, но будут генерировать предупреждения. Устаревшие API будут полностью удалены из AGP в следующем основном выпуске. Например, если API объявлен устаревшим в AGP 7.0, он будет доступен в этой версии и будет генерировать предупреждения. Этот API больше не будет доступен в AGP 8.0.

Чтобы увидеть примеры новых API, используемых для распространённых настроек сборки, ознакомьтесь с рецептами плагина Android Gradle . Они содержат примеры распространённых настроек сборки. Более подробную информацию о новых API можно найти в нашей справочной документации .

Основы сборки Gradle

Это руководство не охватывает всю систему сборки Gradle. Однако оно охватывает минимально необходимый набор концепций для интеграции с нашими API и содержит ссылки на основную документацию Gradle для дальнейшего изучения.

Мы предполагаем наличие базовых знаний о работе Gradle, включая настройку проектов, редактирование файлов сборки, применение плагинов и запуск задач. Чтобы узнать об основах Gradle в отношении AGP, рекомендуем ознакомиться с разделом «Настройка сборки» . Чтобы узнать об общей структуре настройки плагинов Gradle, см. раздел «Разработка пользовательских плагинов Gradle» .

Глоссарий ленивых типов Gradle

Gradle предлагает ряд типов, которые ведут себя «лениво» или помогают отложить сложные вычисления или создание Task на более поздние этапы сборки. Эти типы лежат в основе многих API Gradle и AGP. Ниже представлен список основных типов Gradle, участвующих в ленивом выполнении, и их ключевых методов.

Provider<T>
Предоставляет значение типа T (где «T» означает любой тип), которое можно прочитать на этапе выполнения с помощью метода get() или преобразовать в новый Provider<S> (где «S» означает другой тип) с помощью методов map() , flatMap() и zip() . Обратите внимание, что метод get() ни в коем случае не следует вызывать на этапе конфигурации.
  • map() : принимает лямбда-выражение и создаёт Provider типа S , Provider<S> . Аргумент лямбда-выражения для map() принимает значение T и создаёт значение S . Лямбда-выражение не выполняется немедленно; вместо этого его выполнение откладывается до момента вызова get() для полученного Provider<S> , что делает всю цепочку ленивой.
  • flatMap() : также принимает лямбда-функцию и возвращает Provider<S> , но лямбда-функция принимает значение T и возвращает Provider<S> (вместо того, чтобы напрямую возвращать значение S ). Используйте flatMap(), когда S невозможно определить во время конфигурации и можно получить только Provider<S> . С практической точки зрения, если вы использовали map() и получили результат типа Provider<Provider<S>> , это, вероятно, означает, что вместо этого следовало использовать flatMap() .
  • zip() : позволяет объединить два экземпляра Provider для создания нового Provider со значением, вычисленным с помощью функции, которая объединяет значения из двух входных экземпляров Providers .
Property<T>
Реализует Provider<T> , поэтому также предоставляет значение типа T . В отличие от Provider<T> , который доступен только для чтения, вы также можете задать значение для Property<T> . Это можно сделать двумя способами:
  • Установите значение типа T напрямую, когда оно доступно, без необходимости отложенных вычислений.
  • Установите другой Provider<T> в качестве источника значения Property<T> . В этом случае значение T материализуется только при вызове Property.get() .
TaskProvider
Реализует Provider<Task> . Для создания TaskProvider используйте tasks.register() , а не tasks.create() , чтобы гарантировать ленивое создание экземпляров задач только при необходимости. Для доступа к выходным данным Task до Task создания можно использовать flatMap() , что может быть полезно, если вы хотите использовать выходные данные в качестве входных данных для других экземпляров Task .

Поставщики и их методы преобразования необходимы для настройки входов и выходов задач ленивым способом, то есть без необходимости заранее создавать все задачи и разрешать значения.

Поставщики также содержат информацию о зависимости задач. При создании Provider путём преобразования выходных данных Task эта Task становится неявной зависимостью Provider и будет создаваться и запускаться всякий раз, когда значение Provider будет определено, например, когда это потребуется другой Task .

Вот пример регистрации двух задач, GitVersionTask и ManifestProducerTask , с отсрочкой создания экземпляров Task до тех пор, пока они действительно не понадобятся. Входное значение ManifestProducerTask устанавливается равным Provider полученному из выходных данных GitVersionTask , поэтому ManifestProducerTask неявно зависит от GitVersionTask .

// Register a task lazily to get its TaskProvider.
val gitVersionProvider: TaskProvider =
    project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
        it.gitVersionOutputFile.set(
            File(project.buildDir, "intermediates/gitVersionProvider/output")
        )
    }

...

/**
 * Register another task in the configuration block (also executed lazily,
 * only if the task is required).
 */
val manifestProducer =
    project.tasks.register(variant.name + "ManifestProducer", ManifestProducerTask::class.java) {
        /**
         * Connect this task's input (gitInfoFile) to the output of
         * gitVersionProvider.
         */
        it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
    }

Эти две задачи будут выполнены только в том случае, если они явно запрошены. Это может произойти в рамках вызова Gradle, например, при запуске ./gradlew debugManifestProducer или если вывод ManifestProducerTask связан с какой-либо другой задачей, и его значение становится обязательным.

Хотя вы будете писать собственные задачи, которые используют входные данные и/или генерируют выходные данные, AGP не предоставляет прямой публичный доступ к своим задачам. Это детали реализации, которые могут меняться от версии к версии. Вместо этого AGP предлагает API Variant и доступ к выходным данным своих задач, или артефактам сборки , которые можно читать и преобразовывать. Подробнее см. в разделе API Variant, Артефакты и Задачи в этом документе.

Фазы сборки Gradle

Создание проекта по своей сути является сложным и ресурсоемким процессом, и существуют различные функции, такие как избежание конфигурации задач, актуальные проверки и функция кэширования конфигурации, которые помогают минимизировать время, затрачиваемое на воспроизводимые или ненужные вычисления.

Чтобы применить некоторые из этих оптимизаций, скрипты и плагины Gradle должны подчиняться строгим правилам на каждом этапе сборки Gradle: инициализации, настройке и выполнении. В этом руководстве мы сосредоточимся на этапах настройки и выполнения. Подробнее обо всех этапах можно узнать в руководстве по жизненному циклу сборки Gradle .

Фаза конфигурации

На этапе конфигурации оцениваются скрипты сборки для всех проектов, входящих в сборку, применяются плагины и определяются зависимости сборки. Этот этап следует использовать для настройки сборки с использованием DSL-объектов, а также для ленивой регистрации задач и их входных данных.

Поскольку фаза конфигурации выполняется всегда, независимо от того, какая задача запрашивается для запуска, особенно важно поддерживать её компактность и ограничивать зависимость любых вычислений от входных данных, отличных от самих скриптов сборки. То есть, не следует запускать внешние программы, читать данные из сети или выполнять длительные вычисления, которые можно отложить до фазы выполнения в виде полноценных экземпляров Task .

Фаза исполнения

На этапе выполнения выполняются запрошенные задачи и их зависимые задачи. В частности, выполняются методы класса Task , отмеченные @TaskAction . Во время выполнения задачи разрешено читать входные данные (например, файлы) и разрешать ленивые поставщики, вызывая Provider<T>.get() . Разрешение ленивых поставщиков таким образом запускает последовательность вызовов map() или flatMap() , которые следуют за информацией о зависимостях задач, содержащейся в поставщике. Задачи выполняются лениво для материализации требуемых значений.

Вариант API, артефакты и задачи

Variant API — это механизм расширения в плагине Android Gradle, позволяющий управлять различными параметрами, обычно задаваемыми с помощью DSL в файлах конфигурации сборки, которые влияют на сборку Android. Variant API также предоставляет доступ к промежуточным и конечным артефактам, создаваемым в процессе сборки, таким как файлы классов, объединённый манифест или файлы APK/AAB.

Процесс сборки Android и точки расширения

При взаимодействии с AGP используйте специально созданные точки расширения вместо регистрации типичных обратных вызовов жизненного цикла Gradle (например, afterEvaluate() ) или настройки явных зависимостей Task . Задачи, созданные AGP, считаются деталями реализации и не предоставляются как публичный API. Избегайте попыток получить экземпляры объектов Task или угадывать имена Task и добавлять обратные вызовы или зависимости непосредственно к этим объектам Task .

AGP выполняет следующие шаги для создания и выполнения своих экземпляров Task , которые, в свою очередь, создают артефакты сборки. За основными этапами создания объекта Variant следуют обратные вызовы, позволяющие вносить изменения в определённые объекты, созданные в ходе сборки. Важно отметить, что все обратные вызовы происходят на этапе конфигурации (описанном на этой странице) и должны выполняться быстро, перекладывая всю сложную работу на соответствующие экземпляры Task на этапе выполнения.

  1. Анализ DSL : на этом этапе оцениваются скрипты сборки, а также создаются и задаются различные свойства объектов DSL Android из блока android . На этом этапе также регистрируются обратные вызовы API Variant, описанные в следующих разделах.
  2. finalizeDsl() : обратный вызов, позволяющий изменять объекты DSL до того, как они будут заблокированы для создания компонентов (вариантов). Объекты VariantBuilder создаются на основе данных, содержащихся в объектах DSL.

  3. Блокировка DSL : DSL теперь заблокирована, и внесение изменений больше невозможно.

  4. beforeVariants() : этот обратный вызов может влиять на то, какие компоненты создаются, и на некоторые их свойства через VariantBuilder . Он по-прежнему позволяет вносить изменения в процесс сборки и создаваемые артефакты.

  5. Создание варианта : список компонентов и артефактов, которые будут созданы, теперь определен и не может быть изменен.

  6. onVariants() : в этом обратном вызове вы получаете доступ к созданным объектам Variant и можете задать значения или поставщиков для содержащихся в них значений Property , которые будут вычисляться лениво.

  7. Блокировка вариантов : объекты вариантов теперь заблокированы, и внесение изменений больше невозможно.

  8. Созданные задачи : объекты Variant и значения их Property используются для создания экземпляров Task , необходимых для выполнения сборки.

AGP представляет расширение AndroidComponentsExtension , позволяющее регистрировать обратные вызовы для finalizeDsl() , beforeVariants() и onVariants() . Расширение доступно в скриптах сборки через блок androidComponents :

// This is used only for configuring the Android build through DSL.
android { ... }

// The androidComponents block is separate from the DSL.
androidComponents {
   finalizeDsl { extension ->
      ...
   }
}

Однако мы рекомендуем использовать скрипты сборки только для декларативной конфигурации с использованием DSL блока Android и переносить всю пользовательскую императивную логику в buildSrc или внешние плагины. Вы также можете ознакомиться с примерами buildSrc в нашем репозитории Gradle recipes на GitHub, чтобы узнать, как создать плагин в своём проекте. Вот пример регистрации обратных вызовов из кода плагина:

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            ...
        }
    }
}

Давайте подробнее рассмотрим доступные обратные вызовы и типы вариантов использования, которые ваш плагин может поддерживать в каждом из них:

finalizeDsl(callback: (DslExtensionT) -> Unit)

В этом обратном вызове вы можете получить доступ к объектам DSL, созданным путём анализа информации из блока android в файлах сборки, и изменить их. Эти объекты DSL будут использоваться для инициализации и настройки вариантов на последующих этапах сборки. Например, вы можете программно создавать новые конфигурации или переопределять свойства, но имейте в виду, что все значения должны быть определены во время настройки, поэтому они не должны зависеть от каких-либо внешних входных данных. После завершения выполнения этого обратного вызова объекты DSL становятся бесполезными, и вам больше не следует хранить ссылки на них или изменять их значения.

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            extension.buildTypes.create("extra").let {
                it.isJniDebuggable = true
            }
        }
    }
}

beforeVariants()

At this stage of the build, you get access to VariantBuilder objects, which determine the variants that will be created and their properties. For example, you can programmatically disable certain variants, their tests, or change a property's value (for example, minSdk ) only for a chosen variant. Similar to finalizeDsl() , all of the values you provide must be resolved at configuration time and not depend on external inputs. The VariantBuilder objects must not be modified once execution of the beforeVariants() callback finishes.

androidComponents {
    beforeVariants { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

Обратный вызов beforeVariants() может принимать VariantSelector , который можно получить через метод selector() в androidComponentsExtension . Его можно использовать для фильтрации компонентов, участвующих в вызове обратного вызова, по их имени, типу сборки или версии продукта.

androidComponents {
    beforeVariants(selector().withName("adfree")) { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

onVariants()

К моменту вызова onVariants() все артефакты, которые будут созданы AGP, уже определены, поэтому отключить их больше нельзя. Однако вы можете изменить некоторые значения, используемые для задач, установив их для атрибутов Property в объектах Variant . Поскольку значения Property будут определены только при выполнении задач AGP, вы можете безопасно подключить их к поставщикам из ваших собственных задач, которые будут выполнять все необходимые вычисления, включая чтение из внешних входных данных, таких как файлы или сеть.

// onVariants also supports VariantSelectors:
onVariants(selector().withBuildType("release")) { variant ->
    // Gather the output when we are in single mode (no multi-apk).
    val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE }

    // Create version code generating task
    val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
        it.outputFile.set(project.layout.buildDirectory.file("${variant.name}/versionCode.txt"))
    }
    /**
     * Wire version code from the task output.
     * map() will create a lazy provider that:
     * 1. Runs just before the consumer(s), ensuring that the producer
     * (VersionCodeTask) has run and therefore the file is created.
     * 2. Contains task dependency information so that the consumer(s) run after
     * the producer.
     */
    mainOutput.versionCode.set(versionCodeTask.map { it.outputFile.get().asFile.readText().toInt() })
}

Внесите сгенерированные исходники в сборку

Ваш плагин может предоставлять несколько типов сгенерированных источников, таких как:

Полный список источников, которые вы можете добавить, смотрите в API источников .

Этот фрагмент кода показывает, как добавить пользовательскую папку с исходным кодом ${variant.name} в исходный набор Java с помощью функции addStaticSourceDirectory() . Затем эта папка обрабатывается цепочкой инструментов Android.

onVariants { variant ->
    variant.sources.java?.let { java ->
        java.addStaticSourceDirectory("custom/src/kotlin/${variant.name}")
    }
}

Более подробную информацию смотрите в рецепте addJavaSource .

Этот фрагмент кода показывает, как добавить каталог с ресурсами Android, созданными в результате пользовательской задачи, в набор ресурсов res . Для других типов источников процесс аналогичен.

onVariants(selector().withBuildType("release")) { variant ->
    // Step 1. Register the task.
    val resCreationTask =
       project.tasks.register<ResCreatorTask>("create${variant.name}Res")

    // Step 2. Register the task output to the variant-generated source directory.
    variant.sources.res?.addGeneratedSourceDirectory(
       resCreationTask,
       ResCreatorTask::outputDirectory)
    }

...

// Step 3. Define the task.
abstract class ResCreatorTask: DefaultTask() {
   @get:OutputFiles
   abstract val outputDirectory: DirectoryProperty

   @TaskAction
   fun taskAction() {
      // Step 4. Generate your resources.
      ...
   }
}

Более подробную информацию смотрите в рецепте addCustomAsset .

Доступ к артефактам и их изменение

Помимо возможности изменять простые свойства объектов Variant , AGP также содержит механизм расширения, позволяющий читать или преобразовывать промежуточные и конечные артефакты, созданные в процессе сборки. Например, вы можете прочитать итоговое, объединённое содержимое файла AndroidManifest.xml в пользовательской Task для его анализа или полностью заменить его содержимое содержимым файла манифеста, сгенерированного вашей пользовательской Task .

Список поддерживаемых в настоящее время артефактов можно найти в справочной документации по классу Artifact . Каждый тип артефакта имеет определённые свойства, которые полезно знать:

Мощность

Мощность Artifact представляет собой количество экземпляров FileSystemLocation , то есть количество файлов или каталогов данного типа артефакта. Информацию о мощности артефакта можно получить, проверив его родительский класс: артефакты с одним экземпляром FileSystemLocation будут подклассом Artifact.Single ; артефакты с несколькими экземплярами FileSystemLocation будут подклассом Artifact.Multiple .

Тип FileSystemLocation

Проверить, представляет ли Artifact файлы или каталоги, можно, посмотрев на его параметризованный тип FileSystemLocation , который может быть либо RegularFile , либо Directory .

Поддерживаемые операции

Каждый класс Artifact может реализовать любой из следующих интерфейсов для указания того, какие операции он поддерживает:

  • Transformable : позволяет использовать Artifact в качестве входных данных для Task , которая выполняет над ним произвольные преобразования и выводит новую версию Artifact .
  • Appendable : применяется только к артефактам, являющимся подклассами Artifact.Multiple . Это означает, что к Artifact можно добавлять объекты, то есть пользовательская Task может создавать новые экземпляры этого типа Artifact , которые будут добавлены в существующий список.
  • Replaceable : Применяется только к артефактам, являющимся подклассами Artifact.Single . Заменяемый Artifact может быть заменён совершенно новым экземпляром, созданным в результате выполнения Task .

Помимо трех операций по изменению артефакта, каждый артефакт поддерживает операцию get() (или getAll() ), которая возвращает Provider с окончательной версией артефакта (после завершения всех операций над ним).

Несколько плагинов могут добавлять любое количество операций над артефактами в конвейер из обратного вызова onVariants() , а AGP обеспечит их корректную цепочку, чтобы все задачи выполнялись в нужное время, а артефакты корректно создавались и обновлялись. Это означает, что когда операция изменяет какие-либо выходные данные, добавляя, заменяя или преобразуя их, следующая операция будет видеть обновлённые версии этих артефактов в качестве входных данных и т. д.

Точкой входа в регистрацию операций является класс Artifacts . Следующий фрагмент кода показывает, как получить доступ к экземпляру Artifacts из свойства объекта Variant в обратном вызове onVariants() .

Затем вы можете передать свой пользовательский TaskProvider , чтобы получить объект TaskBasedOperation (1), и использовать его для соединения его входов и выходов с помощью одного из методов wiredWith* (2).

Точный метод, который вам нужно выбрать, зависит от мощности и типа FileSystemLocation , реализованного Artifact , который вы хотите преобразовать.

И наконец, вы передаете тип Artifact методу, представляющему выбранную операцию в объекте *OperationRequest , который вы получаете в ответ, например, toAppendTo() , toTransform() или toCreate() (3).

androidComponents.onVariants { variant ->
    val manifestUpdater = // Custom task that will be used for the transform.
            project.tasks.register(variant.name + "ManifestUpdater", ManifestTransformerTask::class.java) {
                it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
            }
    // (1) Register the TaskProvider w.
    val variant.artifacts.use(manifestUpdater)
         // (2) Connect the input and output files.
        .wiredWithFiles(
            ManifestTransformerTask::mergedManifest,
            ManifestTransformerTask::updatedManifest)
        // (3) Indicate the artifact and operation type.
        .toTransform(SingleArtifact.MERGED_MANIFEST)
}

В этом примере MERGED_MANIFEST — это SingleArtifact и RegularFile . Поэтому нам нужно использовать метод wiredWithFiles , который принимает одну ссылку RegularFileProperty на входе и одну RegularFileProperty на выходе. В классе TaskBasedOperation есть и другие методы wiredWith* , которые будут работать с другими комбинациями количества Artifact и типов FileSystemLocation .

Чтобы узнать больше о расширении AGP, рекомендуем прочитать следующие разделы руководства по системе сборки Gradle: