Desmistificando o Gradle

Bruna Pereira
7 min readJul 26, 2020

English version here.

Ícone do Kotlin e Ícone do gradle com um coração no meio

É muito fácil usar uma ferramenta de build sem entender 10% do que aquele monte de código está fazendo. Isso porque geralmente as configurações de build de uma aplicação não mudam muito, então se você começa a trabalhar num projeto já existente, grandes são as chances de você não precisar mudar muito aquele bloco de código.

Por isso resolvi escrever este artigo que desmistifica alguns usos que fazemos no Gradle e nem entendemos muito bem da onde vem.

Para fins deste artigo, irei utilizar um script Gradle de uma API em Kotlin. O script é escrito usando o Gradle Kotlin DSL:

1. Linguagem

Como mencionado acima, o script foi escrito em Kotlin. Reparem que a extensão do arquivo é .kts e se você já trabalhou com gradle utilizando groovy, provavelmente não tinha essa extensão e a sintaxe era ligeiramente diferente.

Lá atrás, no lançamento da versão 3 do Gradle, foi anunciado que, para a nova versão, seria possível configurar seu build não só apenas utilizando Groovy — como tínhamos até então — mas também com uma DSL escrita em Kotlin!

Dentre as diversas vantagens descritas por Chris Beams nesse artigo, a que eu mais gostei foi a possibilidade de visitar o código-fonte das funcionalidades através da IDE, assim como fazemos com as bibliotecas que utilizamos no dia-a-dia em Java ou Kotlin.

2. Plugins

Das linhas 1 a 6 do arquivo acima, temos o bloco de definição de plugins.
Várias vezes já presenciei pessoas com dúvidas do que deveria ser um plugin e o que deveria ser uma dependência da aplicação.

Os plugins têm a finalidade de estender o funcionamento do Gradle em si, enquanto as dependências são extensões/bibliotecas para a sua aplicação.

Perceba que no arquivo há três diferentes formas de declaração de plugin, e eu vou explicar com mais detalhes o caso mais comum:

id("org.unbroken-dome.test-sets") version "3.0.1"

Assim como as dependências, os plugins tem o nome, composto por (group id e artifact id) e uma versão. Esta é a sintaxe mais comum de definição de plugin. Repare que também poderíamos defini-lo como a seguir:

id("org.unbroken-dome.test-sets") version("3.0.1")

Mas os parênteses do id são obrigatórios. Isso porque se você olhar no código-fonte, o version é uma infix function do kotlin, permitindo que uma função de apenas um parâmetro tenha os parênteses omitidos na hora da invocação.

3. Repositories

Essa seção vai definir da onde vamos baixar as dependências do projeto. Agora, como saber da onde vem a biblioteca, ou qual repositório você deveria usar?

A verdade é que do ponto de vista de quem está consumindo dependências, tanto faz da onde ela vem. No caso de você estar publicando uma biblioteca própria, aí você terá de escolher onde irá publicar.

Os mais populares repositórios são os descritos nas linhas 12 e 13.
O Maven Central é da Sonatype, a empresa dona do Nexus.
Já o JCenter é da JFrog, empresa dona do Artifactory.
Ambas as soluções são gratuitas para soluções open source.

O JCenter contém todas as dependências que o Maven Central mais algumas que estão exclusivamente no JCenter. Então, para a utilização dos repositórios públicos, não há necessidade de usar as duas em conjunto, o JCenter já seria suficiente.

4. Dependencies

Essa provavelmente é a seção mais modificada dos scripts gradle da vida. Aqui iremos definir todas as bibliotecas externas que iremos usar nos nossos projetos. Como você pode perceber, existem diversas formas de declarar uma dependência no exemplo acima, e elas são: implementation, compile, runtimeOnly, testCompile, testImplementation e testCompileOnly.

4.1 Biblioteca Kotlin

Vamos começar pelo mais diferente. Na linha 17 temos:

implementation(kotlin("stdlib-jdk8"))

Assim como mostramos nos plugins, aqui nas dependências também temos uma maneira mais curta de definir a dependência do kotlin, quando estamos utilizando a DSL em Kotlin. Esta linha é nada mais nada menos que:

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

Aqui definimos a dependência do kotlin, e a versão do kotlin instalada pode ser omitida, porque a partir do kotlin 1.1.2, a dependência assume a mesma versão do plugin definida anteriormente.

4.2 Diferenças entre compile e implementation

Vamos primeiro diferenciar as duas primeiras keywords, compile e implementation.

Antes do gradle 3, a única forma de definir uma dependência necessária em tempo de compilação era utilizando o compile. Lá em 2017, no lançamento do gradle 3, foi anunciado que o compile seria deprecado.
Isso aconteceu porque o compile por padrão tem o comportamento de adicionar no classpath dos consumidores, todas as dependências existentes na biblioteca. Observe a imagem a seguir:

Suponhamos que eu estou desenvolvendo um módulo chamado módulo C e este depende do módulo B, que por sua vez, depende do módulo A.

No cenário 1, usamos compile. O módulo A está presente no classpath do módulo C, mesmo eu nunca tendo precisado utilizar essa dependência. Além de impactar diretamente no tamanho do módulo C que eu estou construindo, haverá a necessidade de recompilação caso o módulo A seja atualizado.

No cenário 2, utilizamos o implementation. O módulo A é invisível para o módulo C, e se eu precisar dele por alguma razão, terei que eu mesma adicioná-lo nas dependências do meu projeto. Qualquer alteração na versão do módulo A é transparente para o módulo C.

O gradle trouxe uma nova configuração junto com essa mudança. Há também a configuração api. Usando essa configuração, você deliberadamente mantém o comportamento do cenário 1. Com o lançamento das duas novas configurações, o gradle recomenda que você não deve utilizar mais o compile. Como está no site deles:

A configuração compile ainda existe mas não deve ser utilizada, pois ela não oferece as garantias que as configurações api e implementation provêem.

4.3 Outras configurações

Existe uma série de configurações, como vimos no arquivo de exemplo. Na tabela a seguir há todas as configurações de definição de dependência e suas respectivas descrições:

5. Tasks

Há basicamente 3 formas de criação de uma task no gradle:
1. É uma task nativa;
2. Um plugin que contém uma task foi instalado;
3. Uma task foi internamente criada no próprio Gradle.

Nos dois primeiros cenários, as tasks podem ser estendidas, seja para definir alguma configuração do seu projeto, ou para alterar o comportamento. E é exatamente isso que está acontecendo nas linhas 41 a 57 do arquivo.

Primeiro, estamos adicionando algumas configurações à task de teste do gradle:

tasks.withType<Test> { 
useJUnitPlatform {
includeEngines(“spek2”)
}
testLogging {
events(“passed”, “skipped”, “failed”)
}
}

O que pode ser modificado em uma task vai ser muito específico, bastando apenas ler a documentação do plugin que definiu aquela task ou até mesmo a documentação do gradle para as tasks nativas. Aqui você encontra todas as configurações possíveis para a configuração da task de teste utilizada acima.

Geralmente o tipo da task tem o mesmo nome da própria task em si. No exemplo acima estamos modificando a task que é executada através do comando gradle test.

Nas linhas 50 a 57 temos uma sintaxe ligeiramente diferente, mas a verdade é que ela faz exatamente a mesma coisa.

No arquivo de exemplo, temos o seguinte bloco de código:

tasks {    
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
}

E esse código pode ser escrito usando exatamente a mesma sintaxe que utilizamos para a task de teste, ficando assim:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile#...
# other configurations and tasks
#...
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}

E na verdade não precisamos nem definir a versão target para ambas as tasks, a mesma versão que é atribuída para a task de compile deveria ser utilizada para compilar tanto o código-fonte quanto os testes.

Note que tivemos que importar a task KotlinCompile para que fosse possível configurá-la. Isso acontece porque ao contrário do exemplo anterior, esta task é definida por um plugin e não é interna do Gradle. Logo, temos que explicitamente importá-la no nosso script.

Ah, e se você não sabe o que exatamente porque essa configuração está aí, aqui vai a explicação: o Kotlin a partir da versão 1.1 pode compilar o código para bytecode compatível com as versões 1.6 ou mais novas da JVM.
Por padrão o Kotlin sempre vai converter para a versão 1.6, mas se você vai executar seu código em um servidor com a JVM em uma versão mais recente, você pode passar um parâmetro para o compilador, indicando qual é a versão que gostaria de ser usada.

6. Configurações

Nas seções de plugins, mencionamos que os plugins são capazes de atribuir habilidades extras para o seu script de build. Muitas vezes vamos precisar configurar esses plugins na nossa aplicação, seja para passar algum parâmetro ou para definir comportamentos desejados.

Por isso, os plugins oferecem uma api para que possamos definir as configurações.
Nas linhas 59 e 63, o que estamos fazendo é exatamente isso. Definindo parâmetro para os plugins das linhas 4 e 5.
Assim como nas Tasks, para você saber o que pode ser sobrescrito nesta seção, recorra à documentação.

Conclusão

A escrita de um script de build requer bastante conhecimento da API da ferramenta que está utilizando, mas não é indispensável.
É totalmente possível criar um script de build baseado na tentativa e erro, copiando e colando código da documentação e dos fóruns de dúvida da vida, mas para atingir eficiência no seu script de build e também ser capaz de organizar o código, você precisa entender as funcionalidades que está utilizando.

E você, tem algum bloco de código no script de build de algum projeto que está trabalhando agora que não faz a menor ideia do que ele faz? Corre lá pra conferir!

Referências:

https://docs.gradle.org/current/userguide/userguide.html

--

--