Folha de dicas de Gradle

Parece-me que a maioria das pessoas começa a lidar com gradle apenas quando algo precisa ser adicionado ao projeto ou algo de repente quebra - e depois de resolver o problema "adquirido pelo excesso de trabalho", a experiência é esquecida com segurança. Além disso, muitos exemplos na Internet são semelhantes a feitiços altamente especializados que não acrescentam entendimento do que está acontecendo:


android { compileSdkVersion 28 defaultConfig { applicationId "com.habr.hello" minSdkVersion 20 targetSdkVersion 28 } buildTypes { release { minifyEnabled false } } } 

Não vou descrever em detalhes para que servem cada linha acima - esses são detalhes particulares da implementação do plug-in do Android. Há algo mais valioso - uma compreensão de como tudo está organizado. As informações estão espalhadas em vários sites / documentação oficial / fontes do berço e plug-ins - em geral, esse é um conhecimento um pouco mais universal que eu não quero esquecer.


O texto adicional pode ser considerado uma folha de dicas para aqueles que estão apenas dominando o gradle ou que já se esqueceram.


Links úteis


  • A documentação oficial é bastante volumosa, mas em alguns lugares pode haver falta de detalhes.
  • códigos-fonte no github , javadoc - por causa da digitação dinâmica no groovy, o ambiente de desenvolvimento está longe de ser sempre capaz de fornecer uma lista de campos / métodos disponíveis e, por nomes curtos de métodos e tipos de argumento (fechamento de fechamento), nem sempre é possível entender por que eles são necessários.
  • um artigo em um hub com vários exemplos é uma tradução do segundo capítulo do livro "Construindo e testando com gradle". O livro também pode ser lido, está disponível gratuitamente.
  • mais um artigo - sobre o buildSrc

Console


O Android studio / IDEA oculta meticulosamente os comandos gradle do desenvolvedor e, mesmo ao alterar os arquivos build.gradle, ele começa a ser estúpido ou reinicia o projeto.


Nesses casos, chamar o gradle a partir do console é muito mais fácil e rápido. O wrapper grapper geralmente vem com o projeto e funciona bem em linux / macos / windows, a menos que você precise chamar o arquivo bat em vez do wrapper.


Tarefas de desafio


 ./gradlew tasks 

escreve tarefas disponíveis.


 ./gradlew subprojectName:tasks --all 

Você pode exibir as tarefas de um subprojeto separado e, mesmo com a opção --all , todas as tarefas, incluindo as secundárias, serão exibidas.


Você pode chamar qualquer tarefa e todas as tarefas das quais depende serão chamadas.


 ./gradlew app:assembleDevelopDebug 

Se você estiver com preguiça de escrever o nome inteiro, jogue letras minúsculas:


 ./gradlew app:assembleDD 

Se o granizo não conseguir adivinhar claramente qual tarefa eles tinham em mente, ele exibirá uma lista de opções adequadas.


Registo


A quantidade de informações exibidas no console ao iniciar uma tarefa depende muito do nível de log.
Além do padrão, existem -q, -w, -i, -d , well ou --quiet, --warn, --info, --debug em quantidade crescente de informações. Em projetos complexos, a saída com -d pode ocupar mais de um megabyte e, portanto, é melhor salvá-lo em um arquivo imediatamente e procurar ali pesquisando palavras-chave:


 ./gradlew app:build -d > myLog.txt 

Se uma exceção for lançada em algum lugar, a opção -s para stacktrace.


Você pode escrever no log você mesmo:


 logger.warn('A warning log message.') 

o logger é uma implementação do SLF4J.


Groovy


O que acontece nos arquivos build.gradle é apenas um código groovy.


Por alguma razão, o Groovy, como linguagem de programação, não é muito popular, embora, como me pareça, seja digno de pelo menos um pouco de estudo. A linguagem nasceu em 2003 e se desenvolveu lentamente. Recursos interessantes:


  • Quase qualquer código java é um código groovy válido. Ajuda muito a escrever intuitivamente o código de trabalho.
  • Simultaneamente com a estática, a digitação dinâmica é suportada no sulco, em vez da String a = "a" você pode escrever com segurança def a = "a" ou mesmo def map = ['one':1, 'two':2, 'list' = [1,false]]
  • Existem fechamentos para os quais você pode determinar dinamicamente o contexto de execução. Esses mesmos blocos android {...} fecham e os executam para algum objeto.
  • Há interpolação das cadeias "$a, ${b}" , cadeias de linhas múltiplas """yep, ${c}""" , e as cadeias java comuns são enquadradas por aspas simples: 'text'
  • Há uma aparência de métodos de extensão. A coleção de idiomas padrão já possui métodos como any, every, each, findAll. Para mim, pessoalmente, os nomes dos métodos parecem incomuns, mas o principal é que são .
  • Delicioso açúcar sintático, o código é muito mais curto e mais simples. Você não precisa escrever colchetes em torno dos argumentos da função; para a declaração de listas e [a,b,c], [key1: value1, key2: value2] hash [a,b,c], [key1: value1, key2: value2] sintaxe agradável é: [a,b,c], [key1: value1, key2: value2]

Em geral, por que linguagens como Python / Javascript dispararam e o Groovy não - é um mistério para mim. Por enquanto, quando não havia lambdas no java, e alternativas como o kotlin / scala estavam aparecendo ou ainda não existiam, o Groovy teve que parecer uma linguagem realmente interessante.


Foi a flexibilidade da sintaxe groovy e da digitação dinâmica que nos permitiu criar DSLs concisas em gradle.


Agora, na documentação oficial da Gradle, exemplos são duplicados no Kotlin, e parece que está planejado mudar para ele, mas o código não parece mais tão simples e se parece mais com o código normal:


 task hello { doLast { println "hello" } } 

vs


 tasks.register("hello") { doLast { println("hello") } } 

No entanto, a renomeação no Kradle ainda não está planejada.


Etapas de montagem


Eles são divididos em inicialização, configuração e execução.


A idéia é que o gradle colete um gráfico de dependência acíclica e chame apenas o mínimo necessário deles. Se bem entendi, o estágio de inicialização ocorre no momento em que o código de build.gradle é executado.


Por exemplo, isto:


 copy { from source to dest } 

Ou assim:


 task epicFail { copy{ from source to dest } } 

Talvez isso não seja óbvio, mas o acima irá desacelerar a inicialização. Para não se envolver na cópia de arquivos em cada inicialização, você precisa usar o doLast{...} ou doFirst{...} na tarefa - o código será encerrado e será chamado quando a tarefa for concluída.


 task properCopy { doLast { copy { from dest to source } } } 

mais ou menos


 task properCopy(type: Copy) { from dest to source } 

Nos exemplos antigos, doLast pode ver o operador << em vez de doLast , mas posteriormente o abandonaram devido ao comportamento não óbvio.


 task properCopy << { println("files copied") } 

task.all


O que é engraçado, com doLast e doFirst você pode suspender algum tipo de ação em qualquer tarefa:


 tasks.all { doFirst { println("task $name started") } } 

O IDE sugere que as tasks têm um whenTaskAdded(Closure ...) , mas o método all(Closure ...) funciona muito mais interessante - o fechamento é chamado para todas as tarefas existentes, bem como para novas tarefas quando elas são adicionadas.


Crie uma tarefa que imprima as dependências de todas as tarefas:


 task printDependencies { doLast { tasks.all { println("$name dependsOn $dependsOn") } } } 

ou mais:


 task printDependencies { doLast { tasks.all { Task task -> println("${task.name} dependsOn ${task.dependsOn}") } } } 

Se tasks.all{} chamado em tempo de execução (no bloco doLast ), veremos todas as tarefas e dependências.
Se você fizer o mesmo sem doLast (ou seja, durante a inicialização), as tarefas impressas poderão não ter dependências, pois ainda não foram adicionadas.


Oh sim, vícios! Se outra tarefa depender dos resultados de nossa implementação, vale a pena adicionar uma dependência:


 anotherTask.dependsOn properCopy 

Ou mesmo assim:


 tasks.all{ task -> if (task.name.toLowerCase().contains("debug")) { task.dependsOn properCopy } } 

entradas, saídas e montagem incremental


Uma tarefa comum será chamada sempre. Se você especificar que uma tarefa baseada no arquivo A gera o arquivo B, o gradle ignorará a tarefa se esses arquivos não tiverem sido alterados. E o gradle não verifica a data de modificação do arquivo, mas seu conteúdo.


 task generateCode(type: Exec) { commandLine "generateCode.sh", "input.txt", "output.java" inputs.file "input.txt" output.file "output.java" } 

Da mesma forma, você pode especificar pastas, bem como alguns valores: inputs.property(name, value) .


descrição da tarefa


Ao chamar ./gradlew tasks --all as tarefas padrão têm uma descrição bonita e são de alguma forma agrupadas. Para suas tarefas, isso é adicionado de maneira muito simples:


 task hello { group "MyCustomGroup" description "Prints 'hello'" doLast{ print 'hello' } } 

task.enabled


você pode "desativar" a tarefa - então suas dependências ainda serão chamadas, mas ela mesma não será.


 taskName.enabled false 

vários projetos (módulos)


compilações de múltiplos projetos na documentação


No projeto principal, você pode colocar vários outros módulos. Por exemplo, isso é usado em projetos Android - não há quase nada no projeto raiz, o plug-in Android está incluído no subprojeto. Se você deseja adicionar um novo módulo, pode adicionar outro e, por exemplo, também é possível conectar o plug-in Android, mas use outras configurações para ele.


Outro exemplo: ao publicar um projeto usando o jitpack, o projeto raiz descreve com quais configurações publicar um módulo filho que talvez nem suspeite da publicação.


Os módulos filhos são especificados em settings.gradle:


 include 'name' 

Leia mais sobre dependências entre projetos aqui.


buildSrc


Se build.gradle muito código no build.gradle ou se for duplicado, ele poderá ser movido para um módulo separado. Precisamos de uma pasta com o nome mágico buildSrc , na qual você pode colocar o código em groovy ou java. (bem, ou melhor, no buildSrc/src/main/java/com/smth/ code, os testes podem ser adicionados ao buildSrc/src/test ). Se você quiser algo mais, por exemplo, escreva sua tarefa no scala ou use algumas dependências, diretamente no buildSrc você precisará criar o build.gradle e especificar as dependências necessárias nele / ativar plug-ins.


Infelizmente, com um projeto no buildSrc IDE pode ser estúpido com dicas, então você terá que escrever importações e classes / tarefas a partir daí, também precisará importá-lo para o build.gradle usual. import com.smth.Taskname não é difícil, você só precisa se lembrar disso e não buildSrc por que a tarefa do buildSrc não buildSrc encontrada).


Por esse motivo, é conveniente primeiro escrever algo que funcione diretamente no build.gradle e somente depois transferir o código para o buildSrc .


Tipo de tarefa própria


A tarefa é herdada do DefaultTask , na qual existem muitos, muitos campos, métodos e outras coisas. Código AbstractTask herdado de DefaultTask.


Pontos úteis:


  • em vez de adicionar manualmente inputs e outputs você pode usar campos e anotações para eles: @Input, @OutputFile etc.
  • método que será executado quando a tarefa for executada: @TaskAction .
  • métodos convenientes como copy{from ... , into... } ainda podem ser chamados, mas é necessário chamá-los explicitamente para o projeto: project.copy{...}

Quando alguém no build.gradle escreve para a nossa tarefa


 taskName { ... //some code } 

o método configure(Closure) é chamado na tarefa.


Não tenho certeza se essa é a abordagem correta, mas se a tarefa tiver vários campos cujo estado mútuo é difícil de controlar com getter-setters, parece bastante conveniente redefinir o método da seguinte maneira:


 override def configure(Closure closure){ def result = super().configure(closure) //    / - return result; } 

E mesmo se você escrever


 taskName.fieldName value 

então o método configure ainda será chamado.


Próprio plugin


Como uma tarefa, você pode escrever seu próprio plug-in, que irá configurar algo ou criar tarefas. Por exemplo, o que está acontecendo no android{...} é inteiramente um mérito magia negra O plug-in Android, que além disso cria várias tarefas como app: assembleDevelopDebug para todas as combinações possíveis de sabor / tipo de construção / dimensão. Não há nada complicado ao escrever seu plug-in, para uma melhor compreensão, você pode ver o código de outros plug-ins.


Há uma terceira etapa - você pode colocar o código não no buildSrc , mas torná-lo um projeto separado. Em seguida, usando https://jitpack.io ou outra coisa, publique o plug-in e conecte-o de maneira semelhante aos outros.


O fim


Os exemplos acima podem incluir erros de digitação e imprecisões. Escreva em uma nota pessoal ou marque com ctrl+enter - eu a corrigirei. Exemplos específicos são extraídos da documentação e veja este artigo como uma pequena lista de "como fazê-lo".

Source: https://habr.com/ru/post/pt458046/


All Articles