Il me semble que la plupart des gens ne commencent à travailler avec Gradle que lorsque quelque chose doit être ajouté au projet ou que quelque chose se brise soudainement - et après avoir résolu le problème "acquis par le surmenage", l'expérience est oubliée en toute sécurité. De plus, de nombreux exemples sur Internet sont similaires à des sorts hautement spécialisés qui n'ajoutent pas à la compréhension de ce qui se passe:
android { compileSdkVersion 28 defaultConfig { applicationId "com.habr.hello" minSdkVersion 20 targetSdkVersion 28 } buildTypes { release { minifyEnabled false } } }
Je ne vais pas décrire en détail à quoi sert chaque ligne ci-dessus - ce sont des détails privés de la mise en œuvre du plugin Android. Il y a quelque chose de plus précieux - une compréhension de la façon dont tout est organisé. Les informations sont dispersées sur divers sites / documentation officielle / sources du berceau et des plugins pour celui-ci - en général, c'est un peu plus de connaissance universelle que je ne veux pas oublier.
Le texte suivant peut être considéré comme une feuille de triche pour ceux qui maîtrisent le gradle ou qui ont déjà oublié.
Liens utiles
- La documentation officielle est assez volumineuse, mais à certains endroits, il peut y avoir un manque de détails.
- codes sources sur github , javadoc - en raison de la frappe dynamique dans groovy, l'environnement de développement est loin d'être toujours en mesure de donner une liste des champs / méthodes disponibles, et par des noms courts de méthodes et types d'arguments (fermeture de fermeture), il n'est pas toujours possible de comprendre pourquoi ils sont nécessaires.
- un article sur un hub avec un tas d'exemples est une traduction du deuxième chapitre du livre "Construire et tester avec gradle". Le livre peut également être lu, il est disponible gratuitement.
- un autre article - sur buildSrc
La console
Android studio / IDEA cache minutieusement les commandes gradle du développeur, et même lors de la modification des fichiers build.gradle, il commence à être stupide ou à redémarrer le projet.
Dans de tels cas, appeler gradle depuis la console est beaucoup plus facile et plus rapide. Le wrapper de grappin est généralement fourni avec le projet et fonctionne correctement dans linux / macos / windows, à moins que dans ce dernier, vous ayez besoin d'appeler le fichier bat au lieu du wrapper.
Tâches de défi
./gradlew tasks
écrit les tâches disponibles.
./gradlew subprojectName:tasks --all
Vous pouvez afficher les tâches d'un sous-projet distinct, et même avec l'option --all
, toutes les tâches, y compris les tâches secondaires, seront affichées.
Vous pouvez appeler n'importe quelle tâche et toutes les tâches dont elle dépend seront appelées.
./gradlew app:assembleDevelopDebug
Si vous êtes trop paresseux pour écrire le nom complet, vous pouvez jeter des petites lettres:
./gradlew app:assembleDD
Si la grêle ne peut pas deviner clairement quelle tâche elle avait en tête, elle affichera une liste d'options appropriées.
Journalisation
La quantité d'informations affichées dans la console lors du démarrage d'une tâche dépend fortement du niveau de journalisation.
En plus de celui par défaut, il existe -q, -w, -i, -d
, bien, ou --quiet, --warn, --info, --debug
dans une quantité croissante d'informations. Sur les projets complexes, la sortie avec -d peut prendre plus d'un mégaoctet, et il est donc préférable de l'enregistrer immédiatement dans un fichier et d'y regarder en recherchant des mots clés:
./gradlew app:build -d > myLog.txt
Si une exception est levée quelque part, l'option -s
pour stacktrace.
Vous pouvez écrire vous-même dans le journal:
logger.warn('A warning log message.')
l'enregistreur est une implémentation de SLF4J.
Groovy
Ce qui se passe dans les fichiers build.gradle
est juste du code groovy.
Pour une raison quelconque, Groovy, en tant que langage de programmation, n'est pas très populaire, bien qu'il me semble qu'il mérite en soi au moins une petite étude. La langue est née en 2003 et s'est lentement développée. Caractéristiques intéressantes:
- Presque n'importe quel code java est un code groovy valide. Cela aide beaucoup à écrire intuitivement du code de travail.
- Simultanément, le typage statique et dynamique est pris en charge dans le groove, au lieu de
String a = "a"
vous pouvez écrire en toute sécurité def a = "a"
ou mĂŞme def map = ['one':1, 'two':2, 'list' = [1,false]]
- Il existe des fermetures pour lesquelles vous pouvez déterminer dynamiquement le contexte d'exécution. Ces mêmes blocs
android {...}
prennent des fermetures puis les exécutent pour un objet. - Il y a interpolation des chaînes
"$a, ${b}"
, des chaînes multilignes """yep, ${c}"""
, et les chaînes java ordinaires sont encadrées par des guillemets simples: 'text'
- Il y a un semblant de méthodes d'extension. La collection de langues standard a déjà des méthodes comme any, every, each, findAll. Personnellement, les noms des méthodes me semblent inhabituels, mais l'essentiel est qu'ils le sont .
- Délicieux sucre syntaxique, le code est beaucoup plus court et plus simple. Vous n'avez pas besoin d'écrire des crochets autour des arguments de la fonction, pour la déclaration des listes et des
[a,b,c], [key1: value1, key2: value2]
hachage [a,b,c], [key1: value1, key2: value2]
syntaxe agréable est: [a,b,c], [key1: value1, key2: value2]
En général, pourquoi des langues comme Python / Javascript ont monté en flèche et Groovy pas - c'est un mystère pour moi. Pour l'époque, alors qu'il n'y avait même pas de lambdas en java, et que des alternatives comme kotlin / scala apparaissaient ou n'existaient pas encore, Groovy devait ressembler à une langue vraiment intéressante.
C'est la flexibilité de la syntaxe groovy et du typage dynamique qui nous a permis de créer des DSL concis en gradle.
Maintenant, dans la documentation officielle de Gradle, des exemples sont dupliqués sur Kotlin, et il semble qu'il soit prévu de passer à celui-ci, mais le code ne semble plus aussi simple et ressemble davantage à du code normal:
task hello { doLast { println "hello" } }
vs
tasks.register("hello") { doLast { println("hello") } }
Cependant, le changement de nom à Kradle n'est pas encore prévu.
Étapes d'assemblage
Ils sont divisés en initialisation, configuration et exécution.
L'idée est que gradle collecte un graphe de dépendance acyclique et n'en appelle que le minimum requis. Si je comprends bien, l'étape d'initialisation se produit au moment où le code de build.gradle est exécuté.
Par exemple, ceci:
copy { from source to dest }
Ou comme ça:
task epicFail { copy{ from source to dest } }
Ce n'est peut-être pas évident, mais ce qui précède ralentira l'initialisation. Afin de ne pas gérer la copie des fichiers à chaque initialisation, vous devez utiliser le doLast{...}
ou doFirst{...}
dans la tâche - le code sera alors enveloppé dans une fermeture et sera appelé lorsque la tâche sera terminée.
task properCopy { doLast { copy { from dest to source } } }
ou alors
task properCopy(type: Copy) { from dest to source }
Dans les anciens exemples, doLast
pouvez voir l'opérateur <<
au lieu de doLast
, mais ils l'ont abandonné plus tard en raison du comportement non évident.
task properCopy << { println("files copied") }
tasks.all
Ce qui est drĂ´le, avec doLast
et doFirst
vous pouvez suspendre une sorte d'action sur n'importe quelle tâche:
tasks.all { doFirst { println("task $name started") } }
L'IDE suggère que les tasks
ont une whenTaskAdded(Closure ...)
, mais la méthode all(Closure ...)
fonctionne beaucoup plus intéressant - la fermeture est appelée pour toutes les tâches existantes, ainsi que pour les nouvelles tâches lorsqu'elles sont ajoutées.
Créez une tâche qui imprime les dépendances de toutes les tâches:
task printDependencies { doLast { tasks.all { println("$name dependsOn $dependsOn") } } }
ou alors:
task printDependencies { doLast { tasks.all { Task task -> println("${task.name} dependsOn ${task.dependsOn}") } } }
Si tasks.all{}
appelé au moment de l'exécution (dans le bloc doLast
), nous verrons toutes les tâches et dépendances.
Si vous faites de mĂŞme sans doLast
(c'est-à -dire lors de l'initialisation), les tâches imprimées peuvent ne pas avoir de dépendances, car elles n'ont pas encore été ajoutées.
Oh oui, les dépendances! Si une autre tâche doit dépendre des résultats de notre implémentation, alors il vaut la peine d'ajouter une dépendance:
anotherTask.dependsOn properCopy
Ou même comme ça:
tasks.all{ task -> if (task.name.toLowerCase().contains("debug")) { task.dependsOn properCopy } }
Une tâche commune sera appelée à chaque fois. Si vous spécifiez qu'une tâche basée sur le fichier A génère le fichier B, gradle sautera la tâche si ces fichiers n'ont pas changé. Et gradle ne vérifie pas la date de modification du fichier, mais son contenu.
task generateCode(type: Exec) { commandLine "generateCode.sh", "input.txt", "output.java" inputs.file "input.txt" output.file "output.java" }
De même, vous pouvez spécifier des dossiers, ainsi que certaines valeurs: inputs.property(name, value)
.
description de la tâche
Lorsque vous appelez des ./gradlew tasks --all
les tâches standard ont une belle description et sont en quelque sorte regroupées. Pour vos tâches, cela s'ajoute très simplement:
task hello { group "MyCustomGroup" description "Prints 'hello'" doLast{ print 'hello' } }
task.enabled
vous pouvez "désactiver" la tâche - alors ses dépendances seront toujours appelées, mais elle ne le sera pas.
taskName.enabled false
plusieurs projets (modules)
builds multi-projets dans la documentation
Dans le projet principal, vous pouvez placer plusieurs modules supplémentaires. Par exemple, cela est utilisé dans les projets Android - il n'y a presque rien dans le projet racine, le plugin Android est inclus dans le sous-projet. Si vous souhaitez ajouter un nouveau module, vous pouvez en ajouter un autre, et là , par exemple, vous pouvez également connecter le plugin Android, mais utiliser d'autres paramètres pour cela.
Autre exemple: lors de la publication d'un projet à l'aide de jitpack, le projet racine décrit avec quels paramètres publier un module enfant qui pourrait même ne pas suspecter la publication.
Les modules enfants sont spécifiés dans settings.gradle:
include 'name'
En savoir plus sur les dépendances entre les projets ici.
buildSrc
S'il build.gradle
beaucoup de code dans build.gradle
ou s'il est dupliqué, il peut être déplacé vers un module séparé. Nous avons besoin d'un dossier avec le nom magique buildSrc
, dans lequel vous pouvez placer le code en groovy ou java. (enfin, ou plutĂ´t, dans buildSrc/src/main/java/com/smth/
code, des tests peuvent être ajoutés à buildSrc/src/test
). Si vous voulez autre chose, par exemple, écrivez votre tâche sur scala ou utilisez des dépendances, alors directement dans buildSrc
vous devez créer build.gradle
et spécifier les dépendances nécessaires dans it / enable plugins.
Malheureusement, avec un projet dans buildSrc
IDE peut être stupide avec des indices, là vous devrez écrire des importations et des classes / tâches à partir de là , build.gradle
devrez également l'importer dans le build.gradle
habituel. import com.smth.Taskname
n'est pas difficile, il suffit de s'en souvenir et de ne pas se buildSrc
pourquoi la tâche de buildSrc
pas buildSrc
trouvée).
Pour cette raison, il est pratique d'écrire d'abord quelque chose qui fonctionne directement dans build.gradle
, puis de transférer ensuite le code vers buildSrc
.
Type de tâche propre
La tâche hérite de DefaultTask
, dans laquelle il y a beaucoup, beaucoup de champs, de méthodes et d'autres choses. Code AbstractTask hérité de DefaultTask.
Points utiles:
- au lieu d'ajouter manuellement des
inputs
et des outputs
vous pouvez leur utiliser des champs et des annotations: @Input, @OutputFile
, etc. - méthode qui sera exécutée lorsque la tâche sera exécutée:
@TaskAction
. - des méthodes pratiques comme
copy{from ... , into... }
peuvent toujours être appelées, mais vous devez les appeler explicitement pour le projet: project.copy{...}
Quand quelqu'un dans build.gradle
écrit pour notre tâche
taskName { ...
la méthode configure(Closure)
est appelée sur la tâche.
Je ne sais pas si c'est la bonne approche, mais si une tâche a plusieurs champs dont l'état mutuel est difficile à contrôler avec les getter-setters, alors il semble assez pratique de redéfinir la méthode comme suit:
override def configure(Closure closure){ def result = super().configure(closure)
Et même si vous écrivez
taskName.fieldName value
alors la méthode configure
sera toujours appelée.
Propre plugin
Comme une tâche, vous pouvez écrire votre propre plugin, qui configurera quelque chose ou créera des tâches. Par exemple, ce qui se passe dans android{...}
est entièrement un mérite magie noire Plug-in Android, qui crée en outre un tas de tâches comme l'application: assembleDevelopDebug pour toutes les combinaisons possibles de saveur / type de construction / dimenstion. Il n'y a rien de compliqué à écrire votre plugin, pour une meilleure compréhension vous pouvez voir le code des autres plugins.
Il y a une troisième étape - vous pouvez placer le code non dans buildSrc
, mais en faire un projet distinct. Ensuite, en utilisant https://jitpack.io ou autre chose, publiez le plugin et connectez-le de la même manière aux autres.
La fin
Les exemples ci-dessus peuvent inclure des fautes de frappe et des inexactitudes. Écrivez une note personnelle ou marquez avec ctrl+enter
- je vais la corriger. Des exemples spécifiques sont mieux tirés de la documentation, et regardez cet article comme une petite liste de "comment faire".