Me parece que la mayoría de las personas comienzan a lidiar con el problema solo cuando se necesita agregar algo al proyecto o algo se rompe repentinamente, y después de resolver el problema "adquirido por el exceso de trabajo", la experiencia se olvida con seguridad. Además, muchos ejemplos en Internet son similares a hechizos altamente especializados que no agregan comprensión de lo que está sucediendo:
android { compileSdkVersion 28 defaultConfig { applicationId "com.habr.hello" minSdkVersion 20 targetSdkVersion 28 } buildTypes { release { minifyEnabled false } } }
No voy a describir en detalle para qué sirve cada línea anterior: estos son detalles privados de la implementación del complemento de Android. Hay algo más valioso: una comprensión de cómo está organizado todo. La información se encuentra dispersa en varios sitios / documentación oficial / fuentes de la base y complementos para ella; en general, este es un conocimiento un poco más universal que no quiero olvidar.
El texto adicional se puede considerar como una hoja de trucos para aquellos que solo están dominando el gradle o ya lo han olvidado.
Enlaces utiles
Consola
Android studio / IDEA oculta minuciosamente los comandos gradle del desarrollador, e incluso al cambiar los archivos build.gradle comienza a ser estúpido o reinicia el proyecto.
En tales casos, llamar a gradle desde la consola es mucho más fácil y rápido. El contenedor grapper generalmente viene con el proyecto y funciona bien en linux / macos / windows, a menos que en este último necesite llamar al archivo bat en lugar del contenedor.
Tareas de desafío
./gradlew tasks
escribe tareas disponibles.
./gradlew subprojectName:tasks --all
Puede mostrar las tareas de un subproyecto separado e incluso con la opción --all
, se mostrarán todas las tareas, incluidas las secundarias.
Puede llamar a cualquier tarea, y se llamarán todas las tareas de las que depende.
./gradlew app:assembleDevelopDebug
Si eres demasiado vago para escribir el nombre completo, puedes tirar letras pequeñas:
./gradlew app:assembleDD
Si el granizo no puede adivinar claramente qué tarea tenían en mente, mostrará una lista de opciones adecuadas.
Registro
La cantidad de información que se muestra en la consola al iniciar una tarea depende en gran medida del nivel de registro.
Además del valor predeterminado, hay -q, -w, -i, -d
, bien o --quiet, --warn, --info, --debug
en una cantidad cada vez mayor de información. En proyectos complejos, el resultado con -d puede ocupar más de un megabyte y, por lo tanto, es mejor guardarlo en un archivo de inmediato y buscar palabras clave:
./gradlew app:build -d > myLog.txt
Si se lanza una excepción en alguna parte, la opción -s
para stacktrace.
Puede escribir en el registro usted mismo:
logger.warn('A warning log message.')
el registrador es una implementación de SLF4J.
Maravilloso
Lo que sucede en los archivos build.gradle
es simplemente un código maravilloso.
Por alguna razón, Groovy, como lenguaje de programación, no es muy popular, aunque, según me parece, es digno de al menos un poco de estudio. El lenguaje nació en 2003 y se desarrolló lentamente. Características interesantes:
- Casi cualquier código Java es un código maravilloso válido. Ayuda mucho escribir intuitivamente código de trabajo.
- Simultáneamente con la escritura estática, se admite la escritura dinámica en la ranura, en lugar de
String a = "a"
puede escribir de manera segura def a = "a"
o incluso def map = ['one':1, 'two':2, 'list' = [1,false]]
- Hay cierres para los que puede determinar dinámicamente el contexto de ejecución. Esos mismos bloques de
android {...}
toman cierres y luego los ejecutan para algún objeto. - Hay una interpolación de las cadenas
"$a, ${b}"
, cadenas multilínea """yep, ${c}"""
, y las cadenas normales de Java se enmarcan entre comillas simples: 'text'
- Hay una apariencia de métodos de extensión. La colección de idiomas estándar ya tiene métodos como any, every, each, findAll. Personalmente, los nombres de los métodos me parecen inusuales, pero lo principal es que lo son .
- Delicioso azúcar sintáctico, el código es mucho más corto y simple. No tiene que escribir corchetes alrededor de los argumentos de la función, para la declaración de listas y
[a,b,c], [key1: value1, key2: value2]
hash [a,b,c], [key1: value1, key2: value2]
buena sintaxis es: [a,b,c], [key1: value1, key2: value2]
En general, por qué los lenguajes como Python / Javascript se han disparado y Groovy no, es un misterio para mí. Para su época, cuando ni siquiera había lambdas en Java, y aparecían alternativas como kotlin / scala o todavía no existían, Groovy tenía que parecer un lenguaje realmente interesante.
Fue la flexibilidad de la sintaxis maravillosa y la escritura dinámica lo que nos permitió crear DSL concisos en gradle.
Ahora, en la documentación oficial de Gradle, los ejemplos están duplicados en Kotlin, y parece que está previsto cambiar a él, pero el código ya no parece tan simple y se parece más al código normal:
task hello { doLast { println "hello" } }
vs
tasks.register("hello") { doLast { println("hello") } }
Sin embargo, el cambio de nombre en Kradle aún no está planeado.
Etapas de montaje
Se dividen en inicialización, configuración y ejecución.
La idea es que gradle recopila un gráfico de dependencia acíclico y llama solo el mínimo requerido de ellos. Si entiendo correctamente, la etapa de inicialización ocurre en el momento en que se ejecuta el código de build.gradle.
Por ejemplo, esto:
copy { from source to dest }
O así:
task epicFail { copy{ from source to dest } }
Quizás esto no sea obvio, pero lo anterior ralentizará la inicialización. Para no participar en la copia de archivos en cada inicialización, debe usar el doLast{...}
o doFirst{...}
en la tarea; luego, el código se envolverá en un cierre y se llamará cuando se complete la tarea.
task properCopy { doLast { copy { from dest to source } } }
mas o menos
task properCopy(type: Copy) { from dest to source }
En los ejemplos anteriores, puede ver el operador <<
lugar de doLast
, pero luego lo abandonaron debido al comportamiento no obvio.
task properCopy << { println("files copied") }
tareas.todos
Lo que es divertido, con doLast
y doFirst
puede colgar algún tipo de acción en cualquier tarea:
tasks.all { doFirst { println("task $name started") } }
El IDE sugiere que las tasks
tienen un whenTaskAdded(Closure ...)
, pero el método all(Closure ...)
funciona mucho más interesante: se llama al cierre para todas las tareas existentes, así como para las nuevas tareas cuando se agregan.
Cree una tarea que imprima las dependencias de todas las tareas:
task printDependencies { doLast { tasks.all { println("$name dependsOn $dependsOn") } } }
más o menos:
task printDependencies { doLast { tasks.all { Task task -> println("${task.name} dependsOn ${task.dependsOn}") } } }
Si tasks.all{}
llama a tasks.all{}
en tiempo de ejecución (en el bloque doLast
), veremos todas las tareas y dependencias.
Si hace lo mismo sin doLast
(es decir, durante la inicialización), las tareas impresas pueden carecer de dependencias, ya que aún no se han agregado.
Oh si, adicciones! Si otra tarea depende de los resultados de nuestra implementación, entonces vale la pena agregar una dependencia:
anotherTask.dependsOn properCopy
O incluso así:
tasks.all{ task -> if (task.name.toLowerCase().contains("debug")) { task.dependsOn properCopy } }
Se llamará una tarea común cada vez. Si especifica que una tarea basada en el archivo A genera el archivo B, entonces Gradle omitirá la tarea si estos archivos no han cambiado. Y gradle no verifica la fecha de modificación del archivo, sino su contenido.
task generateCode(type: Exec) { commandLine "generateCode.sh", "input.txt", "output.java" inputs.file "input.txt" output.file "output.java" }
Del mismo modo, puede especificar carpetas, así como algunos valores: inputs.property(name, value)
.
descripción de la tarea
Al llamar a ./gradlew tasks --all
las tareas estándar tienen una descripción hermosa y de alguna manera están agrupadas. Para sus tareas, esto se agrega de manera muy simple:
task hello { group "MyCustomGroup" description "Prints 'hello'" doLast{ print 'hello' } }
task.enabled
puede "desactivar" la tarea, entonces sus dependencias aún se invocarán, pero en sí no.
taskName.enabled false
varios proyectos (módulos)
construcciones multiproyecto en la documentación
En el proyecto principal, puede colocar varios módulos más. Por ejemplo, esto se usa en proyectos de Android: no hay casi nada en el proyecto raíz, el complemento de Android está incluido en el subproyecto. Si desea agregar un nuevo módulo, puede agregar otro, y allí, por ejemplo, también puede conectar el complemento de Android, pero usar otras configuraciones para ello.
Otro ejemplo: al publicar un proyecto usando jitpack, el proyecto raíz describe con qué configuraciones publicar un módulo secundario que ni siquiera sospeche de la publicación.
Los módulos secundarios se especifican en settings.gradle:
include 'name'
Lea más sobre dependencias entre proyectos aquí.
buildSrc
Si build.gradle
mucho código en build.gradle
o está duplicado, se puede mover a un módulo separado. Necesitamos una carpeta con el nombre mágico buildSrc
, en el que puede colocar el código en groovy o java. (bueno, o más bien, en buildSrc/src/main/java/com/smth/
code, se pueden agregar pruebas a buildSrc/src/test
). Si desea algo más, por ejemplo, escriba su tarea en scala o use algunas dependencias, entonces directamente en buildSrc
necesita crear build.gradle
y especificar las dependencias necesarias en él / habilitar complementos.
Desafortunadamente, con un proyecto en buildSrc
IDE puede ser estúpido con sugerencias, allí tendrá que escribir importaciones y clases / tareas desde allí, también tendrá que importarlo en el build.gradle
habitual. import com.smth.Taskname
no es difícil, solo necesita recordar esto y no buildSrc
por buildSrc
no se encontró la tarea de buildSrc
).
Por esta razón, es conveniente escribir primero algo que funcione directamente en build.gradle
, y solo luego transferir el código a buildSrc
.
Propio tipo de tarea
La tarea hereda de DefaultTask
, en la que hay muchos, muchos campos, métodos y otras cosas. Código AbstractTask heredado de DefaultTask.
Puntos útiles:
- en lugar de agregar
inputs
y outputs
manualmente outputs
puede usar campos y anotaciones para ellos: @Input, @OutputFile
, etc. - Método que se ejecutará cuando se ejecute la tarea:
@TaskAction
. - Todavía se pueden llamar métodos convenientes como
copy{from ... , into... }
, pero debe llamarlos explícitamente para el proyecto: project.copy{...}
Cuando alguien en build.gradle
escribe para nuestra tarea
taskName { ...
el método de configure(Closure)
se llama en la tarea.
No estoy seguro de si este es el enfoque correcto, pero si la tarea tiene varios campos cuyo estado mutuo es difícil de controlar con getter-setters, entonces parece bastante conveniente redefinir el método de la siguiente manera:
override def configure(Closure closure){ def result = super().configure(closure)
E incluso si escribes
taskName.fieldName value
entonces se seguirá llamando al método de configure
.
Complemento propio
Como una tarea, puede escribir su propio complemento, que configurará algo o creará tareas. Por ejemplo, lo que está sucediendo en android{...}
es completamente un mérito magia oscura Complemento de Android, que además crea un montón de tareas como la aplicación: assembleDevelopDebug para todas las combinaciones posibles de sabor / tipo de construcción / dimensión. No hay nada complicado en escribir su complemento, para una mejor comprensión puede ver el código de otros complementos.
Hay un tercer paso: puede colocar el código no en buildSrc
, pero hacerlo un proyecto separado. Luego, usando https://jitpack.io u otra cosa, publique el complemento y conéctelo de manera similar a los demás.
El final
Los ejemplos anteriores pueden incluir errores tipográficos e inexactitudes. Escriba una nota personal o marque con ctrl+enter
: lo corregiré. Los ejemplos específicos se toman mejor de la documentación, y vea este artículo como una pequeña lista de "cómo hacerlo".