Creación y publicación de librerías Android utilizando MavenCentral (OSSRH)
- Obtener enlace
- Correo electrónico
- Otras aplicaciones
Este artículo pretende explicar de la forma más sencilla posible la creación de un proyecto de librería para android enfocado a su despliegue en MavenCentral, a fin de que esté disponible en el buscador de paquetes de Android Studio.
JetBrains, como no, proporciona también un tutorial bastante cómodo de seguir (practicamente lo mismo que esto, pero con menos imágenes) aunque es para Jetbrains Spaces.
Para ganar tiempo, y dejar el entorno preparado, lo primero que debemos hacer es registrarnos en Sonatype, siguiendo los siguientes pasos:
- Accederemos a la web de incidencias de Sonatype (es un Jira), y nos registraremos como usuario (si no estamos registrados ya).
- Pulsaremos sobre Signup, y comenzaremos el proceso de registro.
- Una vez acabado el proceso de registro, se nos notifica.
- En el primer inicio de sesión, aparece el asistente típico de Jira, en el que configuraremos varios aspectos del sistema.
- Una vez tenemos todo configurado (Idioma y avatar), nos aparece la página principal para Jira, en la que tenemos pocas opciones para elegir.
- Pulsaremos sobre Crear Incidencia, y seleccionaremos la opción Community Support - Open Source Project Repository Hosting (OSSRH) y como tipo de incidencia, New Project.
- Completaremos todos los campos de la incidencia*
Como podemos ver, tenemos un asunto, que será el "título" del proyecto, una breve descripción de lo que vamos a subir y a continuación los campos realmente importantes: - Group Id: Es la base para los namespaces del proyecto. MUY IMPORTANTE si alojamos nuestro proyecto en GitHub, el package de nuestra librería debe comenzar sí o sí por com.github, en mi caso, establezco com.github.afalabarce
- Project URL: Es la url del proyecto, en mi caso en GitHub.
- SCM url: Es la url para la descarga y clonado del proyecto (normalmente la misma que la de Project URL, terminada en .git).
- Username(s): Será la lista de usuarios (separados por ,) que podrán publicar artifacts en MavenCentral, normalmente, salvo que sea un proyecto colaborativo, seremos nosotros mismos.
- Already Synced to Central: Puesto que es la primera vez que solicitamos creación de un nuevo proyecto, indicaremos que NO.
- Por último, pulsaremos sobre Crear, a fin de que se genere la incidencia y nos creen nuestro nuevo proyecto de alojamiento de artifacts. Nos aparece la información necesaria de la incidencia. Cuando esté solucionada, esto es, nuestro proyecto creado, se nos notificará por email.
Nota Importante: Ha cambiado el criterio para asignaciones de GroupIds para GitHub, se nos notifica, por lo que debemos modificar el GroupId de nuestra incidencia, y crear un repo en nuestra cuenta de GitHub con el código que nos propone el bot. En cuanto lo realicemos, la incidencia pasa a abierta.
Nota Importante II: Gracias a la colaboración de @xavijimenezmulet, tenemos también el criterio para creación de proyectos desde BitBucket. Para la creación de proyectos BitBucket deberemos nombrarlos con el prefijo org.bitbucket. - Una vez el Bot (sí, un bot, antes lo hacía un humano y tardaba un par de días) valida todo, se marca la incidencia como resuelta y ya podremos acceder a nexus.
Como vemos, el bot nos indica dónde debemos acceder para gestionar nuestros artifacts.
Además se nos proporciona la siguiente url, con información de interés:
Hasta este punto, tenemos la primera fase de la creación de un proyecto de librería disponible para su uso a través de MavenCentral.
La segunda fase consiste en preparar nuestro recien creado proyecto para recibir los aar, POM, etc... Recordemos que MavenCentral está diseñado para Maven (que raro, ¿no?), por lo que tendremos nuestros artifacts, con todo lo que ello implica (POM, Paquetes binarios, Código, Documentación, etc, si procede).
Para comenzar con esta segunda fase, accederemos a la url que nos propone en bot, en mi caso: https://s01.oss.sonatype.org/#welcome
Los siguientes pasos están enfocados en preparar tanto el entorno de nexus, como nuestro propio entorno de desarrollo, ya que hay que realizar ciertas acciones en nuestro equipo:
- Hacemos Login, si nos fijamos en la imagen anterior, arriba a la derecha tenemos el enlace Login.
- Utilizaremos las mismas credenciales que en la sección anterior.
Importante: Si intentamos iniciar sesión antes de que se nos haya creado nuestro proyecto (la incidencia no se haya dado por finalizada), se mostrará el siguiente error: Puesto que ya tenemos acceso a Nexus, por ahora lo dejaremos aparcado... y, citando a @mouredev, comenzaremos a picar...
En nuestro Equipo de trabajo deberemos seguir los siguientes pasos:
a. Debemos crear una firma gpg, ya que será imprescindible para la firma de nuestros artifacts a la hora de subirlos a MavenCentral, para ello vamos a necesitar las utilidades gpg-tools:
b. Una vez creadas las claves gpg, las exportaremos a la carpeta de nuestro proyecto, a fin de tenerlas a la mano...% mkdir gpgKeys % cd gpgKeys % gpg --output public.pgp --armor --export afalabarce@gmail.com % gpg --export-secret-keys -o private.kbx Los tres siguientes comandos permiten subir nuestra clave a un keyserver (de ubuntu) utilizado por sonatype para validaciones, sin esto, el proceso de publicación fallará. 1234ABCD es el código de clave pública (SHORT, obtenido con el primero de los tres comandos) % gpg --list-secret-keys --keyid-format SHORT % gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 1234ABCD <- --keyserver="" --send-keys="" 1234abcd="" a="" clave.="" clave="" comprobar="" env="" existe.="" gpg="" hkp:="" keyserver.ubuntu.com="" la="" para="" pre="" si="" sirve="">
Es muy importante (en las capturas dejo la creación "estandar"), crear el fichero de claves privadas con el comando->gpg --export-secret-keys -o private.kbx
ya que de lo contrario, Android Studio nos dará constantemente un error de que no encuentra la cabecera pgp.
- Crearemos un nuevo proyecto para Android Studio, puesto que no vamos a necesitar Activities (o sí, dependerá de lo que vayamos a desarrollar) crearemos el proyecto sin actividades. En mi caso, para este tutorial, voy a crear un proyecto para generar un par de Composables.
- Una vez creado el proyecto, podemos apreciar que Android Studio nos ha creado el proyecto con un módulo de tipo app, esto no nos interesa, por lo que realizaremos las siguientes acciones:
- Agregar un módulo de tipo Librería.
- Eliminar el módulo de tipo App. Al final, el proyecto se nos debe quedar así...
Como vemos, solo nos queda el proyecto de librería, al que deberemos preparar adecuadamente su build.gradle.kts para las particularidades del proyecto. En mi caso, deberé preparar el build.gradle.kts para que soporte JetpackCompose. En otros casos, se agregarán los implementations necesarios para poder desarrollar la funcionalidad necesaria. - El siguiente paso en la creación de nuestra librería, publicable en MavenCentral, pasa por la adaptación de los ficheros de configuración de nuestro proyecto, para ello:
- Modificaremos nuestro fichero local.properties, en el que agregaremos las credenciales necesarias de sonatype para acceso por OSSRH:
Si nos fijamos## This file is automatically generated by Android Studio. # Do not modify this file -- YOUR CHANGES WILL BE ERASED! # # This file should *NOT* be checked into Version Control Systems, # as it contains information specific to your local configuration. # # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. sdk.dir=/Users/afalabarce/Library/Android/sdk signing.keyId = 1234ABCD # Clave en hexadecimal de nuestra clave privada GPG, se obtiene con gpg --list-secret-keys --keyid-format SHORT signing.password = miPasswordGPG signing.secretKeyRingFile = /Users/afalabarce/Desarrollo/AndroidStudioProjects/JetpackComposeComponents/gpgKeys/private.kbx ossrhUsername = miUsuarioSonatype ossrhPassword = miPasswordSonatype
- Modificaremos nuestro build.gradle.kts (el del módulo de librería), que, gracias al nuevo sistema de plugins se simplifica un poco (para ver cómo crear un módulo de este tipo con los build.gradle antiguos, puedes verlo en mi repo original). Este build.gradle.kts quedaría como sigue:
import java.io.FileInputStream import java.util.Properties plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id ("maven-publish") // Support for maven publishing artifacts id ("signing") // Support for signing artifacts } android { namespace = "io.github.afalabarce.jetpackcompose.ads.admob" compileSdk = 33 defaultConfig { minSdk = 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = Constants.composeCompilerVersion } } dependencies { implementation("androidx.core:core-ktx:${Constants.androidxCoreKtxVersion}") implementation("androidx.appcompat:appcompat:${Constants.androidxAppCompatVersion}") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:${Constants.lifeCycleRuntimeVersion}") implementation ("androidx.lifecycle:lifecycle-runtime-compose:${Constants.lifeCycleRuntimeVersion}") implementation ("androidx.compose.ui:ui:${Constants.composeVersion}") implementation ("androidx.compose.ui:ui-tooling:${Constants.composeVersion}") implementation ("androidx.compose.ui:ui-tooling-preview:${Constants.composeVersion}") implementation ("androidx.activity:activity-compose:${Constants.activityComposeVersion}") implementation ("androidx.constraintlayout:constraintlayout-compose:${Constants.constraintLayoutComposeVersion}") implementation ("androidx.compose.material3:material3:${Constants.composeMaterial3Version}") // ads dependencies implementation (platform("com.google.firebase:firebase-bom:${Constants.firebaseBomVersion}")) implementation ("com.google.firebase:firebase-appcheck-playintegrity") implementation ("com.google.firebase:firebase-appcheck-debug:${Constants.firebaseAppCheckDebugVersion}") implementation ("com.google.firebase:firebase-messaging:${Constants.firebaseMessagingVersion}") implementation ("com.google.android.gms:play-services-ads:${Constants.gmsPlayServicesAdsVersion}") implementation ("com.google.android.gms:play-services-ads-identifier:${Constants.gmsPlayServicesAdsVersionIdentifierVersion}") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } } val androidSourcesJarTask by tasks.register("androidSourcesJar", Jar::class.java) { archiveClassifier.set("sources") android.sourceSets.toTypedArray().forEach { println("${it.name}(${it.java.name}) -> ${it.java}") } val mainSourceSet = android.sourceSets.first{sourceSets -> sourceSets.name == "main"} from(mainSourceSet.java.srcDirs).source } artifacts { archives(androidSourcesJarTask) } group = SonatypePublishing.publishedGroupId version = SonatypePublishing.libraryVersionId val signingData = mutableMapOf( "signing.keyId" to "", "signing.password" to "", "signing.secretKeyRingFile" to "", "ossrhUsername" to "", "ossrhPassword" to "" ) val secretPropsFile = project.rootProject.file("local.properties") if (secretPropsFile.exists()) { println("Found secret props file, loading props") val props = Properties() props.load(FileInputStream(secretPropsFile)) for ((name, value) in props) { println("Prop: $name -> $value") signingData[name.toString()] = value.toString() } } publishing { publications { create<MavenPublication>("Maven"){ groupId = SonatypePublishing.publishedGroupId artifactId = SonatypePublishing.artifact version = SonatypePublishing.libraryVersionCode println ("groupId: ${SonatypePublishing.publishedGroupId}") println ("Artifact: ${SonatypePublishing.artifact}") println ("Version: ${SonatypePublishing.libraryVersionCode}") } withType<MavenPublication>{ println(project.name) // Two artifacts, the `aar` and the sources artifact("$buildDir/outputs/aar/${project.name}-release.aar") artifact(androidSourcesJarTask) pom { packaging = "aar" name.set(SonatypePublishing.artifact) description.set(SonatypePublishing.libraryDescription) url.set(SonatypePublishing.gitUrl) licenses { license { name.set(SonatypePublishing.licenseName) url.set(SonatypePublishing.licenseUrl) } } issueManagement { system.set("Github") url.set("https://github.com/afalabarce/jetpackcompose-admob/issues") } scm { connection.set("scm:git:github.com/afalabarce/jetpackcompose-admob.git") developerConnection.set("scm:git:ssh://github.com/afalabarce/jetpackcompose-admob.git") url.set("https://github.com/afalabarce/jetpackcompose-admob/tree/master") } developers { developer { id.set(SonatypePublishing.developerId) name.set(SonatypePublishing.developerName) email.set(SonatypePublishing.developerEmail) } } // A slightly hacky fix so that your POM will include any transitive dependencies // that your library builds upon withXml { val dependenciesNode = asNode().appendNode("dependencies") project.configurations.implementation.configure { dependencies.forEach { dependency -> val dependencyNode = dependenciesNode.appendNode("dependency") dependencyNode.appendNode("groupId", dependency.group) dependencyNode.appendNode("artifactId", dependency.name) dependencyNode.appendNode("version", dependency.version) } } } } } } repositories { // The repository to publish to, Sonatype/MavenCentral maven { // This is an arbitrary name, you may also use "mavencentral" or // any other name that's descriptive for you name = "sonatype" // these urls depend on the configuration provided to the user by sonatype val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" // You only need this if you want to publish snapshots, otherwise just set the URL // to the release repo directly setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) // The username and password we've fetched earlier credentials { username = signingData["ossrhUsername"] password = signingData["ossrhPassword"] } } } } signing { val keyRingPrivateKey = File(signingData["signing.secretKeyRingFile"].toString()).readText(Charsets.UTF_8) println("Signing KeyId: ${signingData["signing.keyId"]}") println("Signing Password: ${signingData["signing.password"]}") println("Signing Key:\n$keyRingPrivateKey\n---------") useInMemoryPgpKeys( signingData["signing.keyId"], keyRingPrivateKey, signingData["signing.password"], ) publishing.publications.forEach { publication -> println("Signing [${publication.name}] Publication...") sign(publication) } }
Si nos fijamos un poco en este build.gradle.kts, tenemos ciertas secciones que son poco comunes (o nada comunes), estas son signing, repositories y publishing, las cuales se alimentan de las configuraciones que hayamos establecido en nuestro local.properties, a fin de configurar el despliegue, creando los pom necesarios, que serán subidos al repo de mavenCentral. - En este punto, ya tenemos todo configurado, quedando únicamente, y ya desde el IDE (después de sincronizar el proyecto con gradle), publicar en mavenCentral.
- Por último, y una vez gradle nos da el ok a la subida, podremos comprobar en nexus que todo ha ido bien:
- En este punto, ya tenemos nuestro aar subido a MavenCentral, pero aún no ha sido publicado.
- Guardaremos el artifact, seleccionándolo y pulsando sobre Close. Nos pedirá una descripción que, si bien es opcional, es muy recomendable, para proporcionar, al menos, algo de información sobre la release que acabamos de publicar.
Una vez confirmado el cierre, durante unos segundos pasa a "Operation in progress" y a continuación a "Closed". - Una vez cerrado, ya solo nos queda publicar, pulsando sobre Release.
El resultado final, nuestro artifact publicado en el repositorio release de mavenCentral, cuestión de tiempo que aparezca en las búsquedas de librerías de Android Studio (unas tres horas). Si tenemos prisa, podemos configurar directamente en build.gradle.kts nuestro recien subido artifact, ya que tras unos quince minutos está disponible.
Aunque, como digo, lo más satisfactorio es que, tras esas 3 horas, si en android studio buscamos nuestra librería...
Comentarios
Publicar un comentario