來開源吧!發布開源組件到 MavenCentral 倉庫超詳細攻略

請點贊關注,你的支援對我意義重大。

🔥 Hi,我是小彭。本文已收錄到 GitHub · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。

小彭今天和群友討論了一下學習方法的問題,覺得還挺感同身受的。有時候我們遇到不懂的地方,潛意識會產生厭惡和恐懼,大腦會驅使我們去學習和查看這個不懂的地方,結果有可能是陷入到另一個不懂的循環里,忘記了最初的目的。關於系統化學習和碎片化學習,你的想法是怎樣的呢?評論區里告訴我吧。

前言

當一個開發者的水平提升到一定程度時,會有由內向外輸出價值的需求,包括發布開源項目。而要發布開源組件,則需要將組件發布到公開的遠程倉庫,如 Jitpack、JenCenter 和 MavenCentral。其中,MavenCentral 是最流行的中央倉庫,也是 Gradle 默認使用的倉庫之一。

在這篇文章里,我將手把手帶你發布組件到 MavenCentral 中央倉庫。本文的示常式序使用小彭的開源項目 ModularEventBus 有用請給 Star,謝謝。

這不僅僅是一份攻略,還帶著踩過一個個坑留下的淚和撓掉一根根落的寶貴髮絲~~~


開發者系列文章:


1. 概念梳理

1.1 什麼是 POM?

POM(Project Object Model)指項目對象模型,用於描述項目構件的基本資訊。一個有效的 POM 節點中主要包含以下參數:

參數 描述 舉例
groupId 組織 / 公司名 io.github.pengxurui
artifactId 組件名 modular-eventbus-annotation
version 組件版本 1.0.0
packaging 格式 jar

1.2 什麼是倉庫(repository)

在項目中,我們會需要依賴各種各樣的二方庫或三方庫,這些依賴一定會存放在某個位置(Place),這個 「位置」 就叫做倉庫。使用倉庫可以幫助我們管理項目構件,例如 jar、aar 等等。

主流的構建工具都有 2 個層次的倉庫概念:

  • 1、本地倉庫: 無論使用 Linux 還是 Window,電腦中會有一個目錄用來存放從中央倉庫或遠程倉庫下載的依賴文件;
  • 2、遠程倉庫: 包括中央倉庫和私有倉庫。中央倉庫是開源社區提供的倉庫,是絕大多數開源庫的存放位置。比如 Maven 社區的中央倉庫 Maven Central;私有倉庫是公司或組織的自定義倉庫,可以理解為二方庫的存放位置。

1.3 Sonatype、Nexus 和 Maven 的關係:

  • Sonatype: 完整名稱是 Sonatype OSSRH(OSS Repository Hosting),為開源項目提供免費的中央存儲倉庫服務。其中需要用到 Nexus 作為倉庫管理器;
  • Nexus: 完整名稱是 Sonatype Nexus Repository Manager,是 Sonatype 的另一款產品,用作提供倉庫管理器。Sonatype 基於 Nexus 提供中央倉庫,各個公司也可以使用 Nexus 搭建私有倉庫;
  • Maven: 完整名稱是 Apache Maven,是一種構建系統。除了 Maven 之外,Apache Ant 和 Gradle 都可以發布組件。

2. 新建 Sonatype 項目

從這一節開始,我將帶你一步步完成發布組件到中央倉庫的操作(帶你踩坑)。

2.1 準備 Sonatype JIRA 帳號

進入 Sonatype 儀錶盤介面,登錄或註冊新帳號://issues.sonatype.org:

2.2 新建工單

點擊儀錶盤面板右上角的 」新建「 按鈕,按照以下步驟向 Sonotype 提交新建項目的工單:

填寫方法總結如下:

  • 項目: 使用默認選項 Community Support - Open Source Project Repository Hosting (OSSRH);
  • 問題類型: 使用默認選項 New Project;
  • 概要: 填寫 Github 倉庫相同的名稱,以方便查找;
  • GroupId 組織名: 填寫發布組件時使用的 groupId,後續步驟中會檢查你是否真實擁有該 groupId,所以不可以隨便填寫,有 2 種填寫方式:
    • 使用 Github 帳號:按照 io.github.[Github 用戶名] 的格式填寫,後續步驟中 Sonatype 通過要求我們在個人 Github 倉庫中新建指定名稱的臨時程式碼庫的方式來做身份驗證;
    • 使用個人域名: 按照逆序域名的格式填寫,例如個人域名為 oss.sonotype.org ,則填寫 org.sonotype.oss 。
  • Project URL 項目地址: 填寫 Github 項目地址,例如: //github.com/pengxurui/ModularEventBus ;
  • SCM url 版本控制地址: 在 Github 項目地址後加 .git ,例如 //github.com/pengxurui/ModularEventBus.git 。

2.3 驗證 GroupId 所有權

點擊彈出的消息進入工單詳情頁,剛新建的工單要等待 Sonotype 機器人回復,等待大概十幾分鐘後,在工單底部的評論區會告訴我們怎麼操作。

至此,Sonotype 項目準備完畢。


3. 新建 GPG 密鑰對

GPG(GNU Privacy Guard) 是基於 OpenPGP 標準實現的加密軟體,它提供了對文件的非對稱加密和簽名驗證功能。所有發布到 Maven 倉庫的文件都需要進行 GPG 簽名,以驗證文件的合法性。

3.1 安裝 GPG 軟體

安裝 GPG 軟體有兩種方式:

  • 方式 1 - 下載安裝包: 通過 GPG 官方 下載安裝包,這個我沒試過;
  • 方式 2 - 通過 Homebrew 安裝: 使用 Homebrew 執行以下命令:

命令行

# 通過 Homebrew 安裝 gpg
brew install gpg

如果本地沒有 Homebrew 環境則需要先安裝,這裡也容易踩坑。小彭本地原本就有 Homebrew 環境,但是安裝 gpg 的過程中各種報錯,最後還是用了最暴力的解法才解決 —— 卸載重裝 Homebrew:(

參考資料: MacOS下開發環境配置--homebrew的安裝

3.2 生成 GPG 密鑰對

使用 --generate-key 參數,按照指引填寫相關資訊和 passphrase 私鑰口令。另外,使用 --list-keys 參數可以查看當前系統中生成過的密鑰。

命令行

# 密鑰生成命令
gpg --generate-key

# 密鑰查看命令
gpg --list-keys

命令行演示

GPG 在生成密鑰對時,會要求開發者做一些隨機的舉動,以給隨機數加入足夠多的擾動,稍等片刻就會生成完成了。完成後可以隨時使用 —list-keys 參數查看密鑰對資訊:

命令行演示

解釋一下其中的資訊:

/Users/pengxurui/.gnupg/pubring.kbx
-----------------------------------
pub   ed25519 2022-08-23 [SC] [expires: 2024-08-22]
      D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA
uid           [ultimate] test <[email protected]>
sub   cv25519 2022-08-23 [E] [expires: 2024-08-22]

# pubring.kbx:本地存儲公鑰的文件
# 2022-08-23 [SC] [expires: 2024-08-22]:表示密鑰對的創建時間和失效時間
# test <[email protected]>:用戶名和郵箱
# ed25519:表示生成公鑰的演算法
# cv25519:表示生成私鑰的演算法
# D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA:密鑰指紋 / KeyId

至此,你已經在本地生成一串新的密鑰對,現在你手上有:

  • 密鑰指紋 / KeyId: 密鑰指紋是密鑰對的唯一標識,即上面 D8BCD08568BE5D2D634DD99EFD4ECE3B54DE73AA 這一串。有時也可以使用較短的格式,取其最後 8 個字元,即 B54DE73AA 這一串;
  • 公鑰: 該密鑰指紋對應的公鑰;
  • 私鑰: 該密鑰指紋對應的私鑰;
  • passphrase 密鑰口令: 生成密鑰對時輸入的口令,私鑰與密鑰口令共同組成密鑰對的私有資訊。

3.3 刪除密鑰對

有時候需要刪除密鑰對,可以使用以下命令:

# 先刪除私鑰後,才能刪除公鑰

# 刪除私鑰
gpg --delete-secret-keys  [密鑰指紋]
# 刪除公鑰
gpg --delete-keys  [密鑰指紋]

3.4 上傳公鑰

密鑰對中的公鑰資訊需要公開,其他人才能拿到公鑰來驗證你簽名的數據,公開的方法就是上傳到公鑰伺服器。公鑰伺服器是專門儲存用戶公鑰的伺服器,並且會用交換機制將數據同步給其它公鑰伺服器,因此你只要上傳到其中一個伺服器即可。我最後是上傳到 hkp://keyserver.ubuntu.com 伺服器的。以下伺服器都可以嘗試:

  • pool.sks-keyservers.net
  • keys.openpgp.org
  • keyserver.ubuntu.com
  • pgp.mit.edu

命令行

// 上傳公鑰
gpg --keyserver 【伺服器地址】:11371 --send-keys 【密鑰指紋】
// 驗證公鑰
gpg --keyserver 【伺服器地址】:11371 --recv-keys 【密鑰指紋】

3.5 導出密鑰文件

後文發布組件的時候需要用到密鑰口令和私鑰文件,可以使用以下參數導出

命令行

# 默認導出到本地目錄 /User/[用戶名]/

# 導出公鑰
gpg --export 【密鑰指紋】 > xiaopeng_pub.gpg
# 導出私鑰
gpg --export-secret-keys 【密鑰指紋】 > xiaopeng_pri.gpg

3.6 踩坑:PGPException: unknown public key algorithm encountered

我在發布組件時遇到 PGPException: unknown public key algorithm encountered 報錯,最後排查下來是使用了 Gradle signing 插件不支援 EDDSA 演算法,需要使用 RSA 演算法。

可以看到上文 3.1 節生成的公鑰,可以看到是 ed 開頭的,表示使用的是 EDDSA 演算法,應該是不同版本中的 --generate-key 參數使用的默認演算法不一樣。

3.1 節生成的公鑰資訊

pub   ed25519 2022-08-23 [SC] [expires: 2024-08-22]

解決方法是使用 --full-generate-key 參數選擇使用 RSA 演算法生成密鑰對:

命令行演示

至此,密鑰對準備完畢。


4. 配置發布腳本

完成 Sonatype 項目和密鑰對的準備工作後,現在著手配置項目的 Gradle 腳本了。Gradle 提供了兩個 Maven 插件:

  • maven 插件: 舊版發布插件,從 Gradle 7.0 開始無法使用;
  • maven-publish 插件: 新版發布插件。

我最初的想法是分別整理出這兩個插件的通用腳本,一開始是參考 ARouter 項目里的 publish.gradle 腳本,過程中也遇到各種問題,例如 Javadoc generation failed ,可能是因為 ARouter 是純 Java 實現的,所以暴露的問題較少。耽擱了一周後,剛好這兩天在看 LeakCanary 源碼,果然在 LeakCanary 里發現寶藏 —— vanniktech 的發布插件!

報錯

Execution failed for task ':eventbus_api:androidJavadocs'.
> Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting): '/Users/pengxurui/workspace/public/ModularEventBus/eventbus_api/build/tmp/androidJavadocs/javadoc.options'

4.1 使用 maven 插件發布

這塊腳本是參考 ARouter 項目中 publish.gradle 腳本的,我在此基礎上增加了注釋和少量改動,如果遇到生成 Javadoc 出現問題,可以把 archives androidJavadocsJar 這一行注釋掉。

maven_sonatype.gradle

// 在 ARouter 項目的 publish.gradle 上修改

apply plugin: 'maven'
apply plugin: 'signing'

version = VERSION_NAME
group = GROUP

// 是否 Release 發布(根據是否包含 SNAPSHOT 判斷)
def isReleaseBuild() {
    return VERSION_NAME.contains("SNAPSHOT") == false
}

// Central Repository: //central.sonatype.org/publish/publish-guide/
// Release 倉庫地址(默認先發布到 staging 暫存庫,需要手動發布到中央倉庫)
def getReleaseRepositoryUrl() {
    return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : "//s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
}

// Snapshot 倉庫地址
def getSnapshotRepositoryUrl() {
    return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : "//s01.oss.sonatype.org/content/repositories/snapshots/"
}

// 倉庫帳號
def getRepositoryUsername() {
    return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : ""
}

// 倉庫密碼
def getRepositoryPassword() {
    return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : ""
}

// 組件配置
def configurePom(pom) {
    // 組織名
    pom.groupId = GROUP
    // 組件名
    pom.artifactId = POM_ARTIFACT_ID
    // 組件版本
    pom.version = VERSION_NAME

    pom.project {
        // 名稱
        name POM_NAME
        // 發布格式
        packaging POM_PACKAGING
        // 描述資訊
        description POM_DESCRIPTION
        // 主頁
        url POM_URL

        scm {
            url POM_SCM_URL
            connection POM_SCM_CONNECTION
            developerConnection POM_SCM_DEV_CONNECTION
        }

        // Licenses 資訊
        licenses {
            license {
                name POM_LICENCE_NAME
                url POM_LICENCE_URL
                distribution POM_LICENCE_DIST
            }
        }

        // 開發者資訊
        developers {
            developer {
                id POM_DEVELOPER_ID
                name POM_DEVELOPER_NAME
            }
        }
    }
}

afterEvaluate { project ->
    // 配置 Maven 插件的 uploadArchives 任務
    uploadArchives {
        repositories {
            mavenDeployer {
                // 配置發布前需要簽名
                beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
                // 配置 Release 倉庫地址與帳號密碼
                repository(url: getReleaseRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
                // 配置 Snapshot 倉庫地址與帳號密碼
                snapshotRepository(url: getSnapshotRepositoryUrl()) {
                    authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
                }
                // 配置 POM 資訊
                configurePom(pom)
            }
        }
    }
    // 配置 Maven 本地發布任務
    tasks.create("installLocally", Upload) {
        configuration = configurations.archives

        repositories {
            mavenDeployer {
                // 本地倉庫地址
                repository(url: "file://${rootProject.buildDir}/localMaven")
                // 配置 POM 資訊
                configurePom(pom)
            }
        }
    }

    // 配置簽名參數,部分需要在 local.properties 中配置
    signing {
        required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
        sign configurations.archives
    }

    if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) {
        // Android 類型組件
        task install(type: Upload, dependsOn: assemble) { // 依賴於 AGP assemble 任務
            repositories.mavenInstaller {
                configuration = configurations.archives

                configurePom(pom)
            }
        }

        task androidJavadocs(type: Javadoc) {
            source = android.sourceSets.main.java.source
            classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
        }

        task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
            classifier = 'javadoc'
            from androidJavadocs.destinationDir
        }

        // 生成源碼產物
        task androidSourcesJar(type: Jar) {
            classifier = 'sources'
            from android.sourceSets.main.java.source
        }
    } else {
        // 純 Java / Kotlin 類型組件(如 Gradle 插件、APT 組件)
        install {
            repositories.mavenInstaller {
                configurePom(pom)
            }
        }

        // 生成源碼產物
        task sourcesJar(type: Jar, dependsOn: classes) {
            classifier = 'sources'
            from sourceSets.main.allSource
        }

        // 生成 javadoc 產物
        task javadocJar(type: Jar, dependsOn: javadoc) {
            classifier = 'javadoc'
            from javadoc.destinationDir
        }
    }

    // Java8 適配
    if (JavaVersion.current().isJava8Compatible()) {
        allprojects {
            tasks.withType(Javadoc) {
                options.addStringOption('Xdoclint:none', '-quiet')
            }
        }
    }

    // 配置源碼和 Javadoc 發布產物
    if (!isReleaseBuild()) {
        // 快照版本跳過,提高效率
        artifacts {
            if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) {
                // Android 類型組件
                archives androidSourcesJar // 源碼
                archives androidJavadocsJar // Javadoc,如果報錯需要把這一行注釋掉
            } else {
                // 純 Java / Kotlin 類型組件(如 Gradle 插件、APT 組件)
                archives sourcesJar // 源碼
                archives javadocJar // Javadoc
            }
        }
    }
}

在需要發布的組件里應用這個腳本後,在 gradle.properties 里配置相關參數後就可以發布了。具體可以參考示常式序 ModularEventBus 中被注釋掉的參數,也可以參考 ARouter 項目,這裡就不展開了,建議用 4.2 節 vanniktech 的發布插件。

項目級 gradle.properties

######################################################################
# for maven_sonatype.gradle
######################################################################
# GROUP=io.github.pengxurui
#
# POM_URL=//github.com/pengxurui/ModularEventBus/
# POM_SCM_URL=//github.com/pengxurui/ModularEventBus/
# POM_SCM_CONNECTION=scm:git:git:github.com/pengxurui/ModularEventBus.git
# POM_SCM_DEV_CONNECTION=scm:git:ssh://[email protected]/pengxurui/ModularEventBus.git
#
# POM_LICENCE_NAME=The Apache Software License, Version 2.0
# POM_LICENCE_URL=//www.apache.org/licenses/LICENSE-2.0.txt
# POM_LICENCE_DIST=repo
#
# POM_DEVELOPER_ID=pengxurui
# POM_DEVELOPER_NAME=Peng Xurui
#
# SONATYPE_NEXUS_USERNAME=[provide your Sonatype user name]
# SONATYPE_NEXUS_PASSWORD=[provide your Sonatype password]
#
# signing.keyId=[provide you gpg key]
# signing.password=[provide you gpg passphrase]
# signing.secretKeyRingFile=[provide you gpg secret file]

模組級 gradle.properties

######################################################################
# for maven_sonatype.gradle
######################################################################
# POM_NAME=ModularEventBus Annotations
# POM_ARTIFACT_ID=modular-eventbus-annotation
# POM_PACKAGING=jar
# POM_DESCRIPTION=The annotation used in ModularEventBus api
# VERSION_NAME=1.0.0

4.2 使用 vanniktech 的發布插件(推薦)

gradle-maven-publish-plugin ********是一個外國大佬 vanniktech 開源的 Gradle 插件,需要使用 Gradle 7.2.0 以上的 Gradle 環境。它會創建一個 publish Task,支援將 Java、Kotlin 或 Android 組件發布到任何 Maven 倉庫,同時也支援發布攜帶 Java / Kotlin 程式碼的 Javadoc 產物和 Sources 產物。雖然目前(2022/08/24)這個項目的最新版本只是 0.21.0,不過既然已經在 LeakCanary 上驗證過,大膽用起來吧。

以下為配置步驟:在項目級 build.gradle 中添加插件地址,在模組級 build.gradle 中應用插件:

項目級 build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        // vanniktech 發布插件
        classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0'
        // Kotlin Javadoc,非必須
        classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.20"
        // 最新版 1.7.10 和 0.21.0 組合有問題,應該是沒兼容好。上面兩個版本組合我驗證過是可以的。
    }
}

模組級 build.gradle

apply plugin: "com.vanniktech.maven.publish"
// Kotlin Javadoc,非必須。如果有這個插件,發布時會生成 Javadoc,會延長發布時間。建議在 snapshot 階段關閉
apply plugin: "org.jetbrains.dokka"

Sync 項目後,插件會為模組增加兩個 Task 任務:

  • publish: 發布到遠程 Maven 倉庫,默認是 Sonatype 中央倉庫;
  • publishToMavenLocal: 發布到當前機器的本地 Maven 倉庫,即 ~/.m2/repository。

Gradle 面板

4.3 配置 vanniktech 插件的發布參數

分別在項目級 gradle.properties 和模組級 gradle.properties 中配置以下參數:

項目級 gradle.properties

######################################################################
# for vanniktech
######################################################################
# 伺服器地址
SONATYPE_HOST=S01

# 發布 release 組件時是否簽名
RELEASE_SIGNING_ENABLED=true

# 組織名
GROUP=io.github.pengxurui

# 主頁
POM_URL=//github.com/pengxurui/ModularEventBus/

# 版本控制資訊
POM_SCM_URL=//github.com/pengxurui/ModularEventBus/
POM_SCM_CONNECTION=scm:git:git:github.com/pengxurui/ModularEventBus.git
POM_SCM_DEV_CONNECTION=scm:git:ssh://[email protected]/pengxurui/ModularEventBus.git

# Licenses 資訊
POM_LICENSE_NAME=The Apache Software License, Version 2.0
POM_LICENSE_URL=//www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENSE_DIST=repo

# 開發者資訊
POM_DEVELOPER_ID=pengxurui
POM_DEVELOPER_NAME=Peng Xurui
POM_DEVELOPER_URL=//github.com/pengxurui/

mavenCentralUsername=[填 Sonatype 帳號名]
mavenCentralPassword=[填 Sonatype 密碼]

signing.keyId=[密鑰指紋,取後 8 位即可]
signing.password=[passphrase 密鑰口令]
signing.secretKeyRingFile=[導出的私鑰文件路徑,如 /Users/pengxurui/xxx.gpg]

模組級 gradle.properties

POM_NAME=ModularEventBus Annotations
POM_ARTIFACT_ID=modular-eventbus-annotation
POM_PACKAGING=jar
POM_DESCRIPTION=The annotation used in ModularEventBus api
VERSION_NAME=1.0.0

特別注意:私有資訊不要提交到 git 版本管理中,可以寫在 local.properties 中,等到要發布組件時再複製到 gradle.properties 中。而私鑰文件也不要保存在當前工程的目錄里,可以統一放到工程外的一個目錄。

至此,所有準備工作完成。

4.4 淺嘗一下 vanniktech 插件的源碼

畢竟發布邏輯都被人家封裝在插件里了,有必要知道它背後的工作,淺嘗一下。

  • 支援的 Snoatype 伺服器:

SonatypeHost.kt

enum class SonatypeHost(
    internal val rootUrl: String
) {
    DEFAULT("//oss.sonatype.org"),
    S01("//s01.oss.sonatype.org"),
}
  • 支援 Dokka 插件,需要手動依賴:

MavenPublishPlugin.kt

private fun Project.defaultJavaDocOption(): JavadocJar? {
    return if (plugins.hasPlugin("org.jetbrains.dokka") || plugins.hasPlugin("org.jetbrains.dokka-android")) {
        JavadocJar.Dokka(findDokkaTask())
    } else {
        null
    }
}
  • 支援多種模組類型:

MavenPublishPlugin.kt

afterEvaluate {
    when {
        plugins.hasPlugin("org.jetbrains.kotlin.multiplatform") -> {} // Handled above.
        plugins.hasPlugin("com.android.library") -> {} // Handled above.
        plugins.hasPlugin("java-gradle-plugin") ->
            baseExtension.configure(GradlePlugin(defaultJavaDocOption() ?: javadoc()))
        plugins.hasPlugin("org.jetbrains.kotlin.jvm") ->
            baseExtension.configure(KotlinJvm(defaultJavaDocOption() ?: javadoc()))
        plugins.hasPlugin("org.jetbrains.kotlin.js") ->
            baseExtension.configure(KotlinJs(defaultJavaDocOption() ?: JavadocJar.Empty()))
        plugins.hasPlugin("java-library") ->
            baseExtension.configure(JavaLibrary(defaultJavaDocOption() ?: javadoc()))
        plugins.hasPlugin("java") ->
            baseExtension.configure(JavaLibrary(defaultJavaDocOption() ?: javadoc()))
        else -> logger.warn("No compatible plugin found in project $name for publishing")
    }
}

5. 發布組件到 MavenCentral 倉庫

終於終於,所有準備和配置工作都完成了!在發布之前,有必要先解釋下 Sonatype 中用到的倉庫地址:

5.1 倉庫地址

如果你沒有自定義發布的 Maven 倉庫,vanniktech 插件默認會發布到 Sonatype 管理的中央倉庫中。由於歷史原因,Sonatype 中央倉庫有 2 個域名:

按照 官方的說法 ,oss.sonatype.org 是過時的,從 2021 年 2 月開始啟用 s01.oss.sonatype.org/

截圖

官方也會提示目前最新的倉庫地址:

截圖

5.2 Staging 暫存庫

細心的朋友會發現官方提供的 snapshot 倉庫和 release 倉庫的格式不一樣,為什麼呢?—— 這是因為發布 release 組件是敏感操作,一旦組件發布 release 版本到中央倉庫,就永遠無法修改或刪除這個版本的組件內容(這個規則是出於穩定性和可靠性考慮,如果可以修改,那些本地已經下載過組件的用戶就得不到最新內容了)。所以 Sonatype 對發布 snapshot 組件和 release 組件採取了不同策略:

  • snapshot 組件: 直接發布到 snapshot 中央倉庫;
  • release 組件: 使用 Staging 暫存策略,release 組件需要先發布到暫存庫,經過測試驗證通過後,再由開發者手動提升到 release 中央倉庫。
中央 release 倉庫:"//s01.oss.sonatype.org/content/repositories/releases"
中央 snapshot 倉庫:"//s01.oss.sonatype.org/content/repositories/snapshots"
暫存庫:"//s01.oss.sonatype.org/service/local/staging/deploy/maven2"

vanniktech 插件默認也是按照 Sonatype 的策略走的,淺看一下源碼:

MavenPublishBaseExtension.kt

// 暫存庫:
if (stagingRepositoryId != null) {
    repo.setUrl("${host.rootUrl}/service/local/staging/deployByRepositoryId/$stagingRepositoryId/")
} else {
    repo.setUrl("${host.rootUrl}/service/local/staging/deploy/maven2/")
}

// snapshot 庫:
if (it.version.toString().endsWith("SNAPSHOT")) {
    if (stagingRepositoryId != null) {
        throw IllegalArgumentException("Staging repositories are not supported for SNAPSHOT versions.")
    }
    repo.setUrl("${host.rootUrl}/content/repositories/snapshots/")
}

5.3 發布 snapshot 組件

版本號中帶 SNAPSHOT 將被視為 snapshot 組件,會直接發布到 snapshot 中央倉庫。經過小彭驗證,確實在前端發布後,立馬可以在 snapshot 中央倉庫搜索到,例如 小彭的組件。

驗證截圖

5.4 發布 release 組件到 Staging 暫存庫

版本號中未帶 SNAPSHOT 將視為 release 組件,發布 release 組件後,進入 Nexus 面板查看暫存庫(右上角 Log in 登錄):

操作截圖

5.5 發布 release 組件到中央倉庫

確認要發布組件後,先點擊 Close,再點擊 Release 即可發布:

操作截圖

Close 的過程會對組件進行驗證,驗證失敗的話就會報錯了。你可以直接從 Activity 面板中查看報錯提示,我遇到的幾次問題都是參數缺失的小問題。

報錯提示

點擊 Drop 按鈕刪除有問題的組件:

操作截圖

如果驗證通過,Release 按鈕就會高亮,點擊按鈕就終於終於發布了。

操作截圖

5.6 查看已發布的 release 組件

發布成功後,有 3 種方式查看自己的組件:

  • 方法 1 - 在 Sonatype Nexus 面板上查看:

操作截圖

操作截圖

  • 方式 3 - 在 MavenCentral 搜索欄 查找: 這是最正式的方式,缺點是不實時更新,大概有 的延遲,而前兩種方式在發布後立即更新:

操作截圖

按照 官方的說法 ,發布後的組件會在 30 分鐘內同步到中央倉庫,但搜索功能需要達到 4 個小時:

Upon release, your component will be published to Central: 
this typically occurs within 30 minutes,
though updates to search can take up to four hours.

5.6 依賴已發布的組件

怎麼依賴大家都懂。講一下倉庫吧,如果是已經發布到 release 中央倉庫,你的工程只要包含 mavenCentral() 這個倉庫地址就可以了。

示常式序

repositories {
    // 中央倉庫(不包含 snapshot 中央倉庫)
    mavenCentral()
    // release 中央倉庫
    maven { url '//s01.oss.sonatype.org/content/repositories/releases'}
    // snapshot 中央倉庫
    maven { url '//s01.oss.sonatype.org/content/repositories/snapshots/'}
    // 暫存庫,用於驗證
    maven { url "//s01.oss.sonatype.org/service/local/staging/deploy/maven2"}		
}

6. 報錯記錄

  • Sonatype 帳號密碼錯誤:
Failed to publish publication 'maven' to repository 'mavenCentral'
   > Could not PUT '//s01.oss.sonatype.org/service/local/staging/deploy/maven2/io/github/pengxurui/modular-eventbus-annotation/1.0.2/modular-eventbus-annotation-1.0.2.jar'. Received status code 401 from server: Unauthorized
  • GPG 密鑰錯誤:
Execution failed for task ':eventbus_annotation:signMavenPublication'.
> Error while evaluating property 'signatory' of task ':eventbus_annotation:signMavenPublication'
   > org.bouncycastle.openpgp.PGPException: checksum mismatch at 0 of 20
  • GPG 密鑰演算法錯誤:
Execution failed for task ':eventbus_annotation:signMavenPublication'.
> Error while evaluating property 'signatory' of task ':eventbus_annotation:signMavenPublication'
   > org.bouncycastle.openpgp.PGPException: unknown public key algorithm encountered 
  • Javadoc 生成報錯:
Execution failed for task ':eventbus_api:androidJavadocs'.
> Javadoc generation failed. Generated Javadoc options file (useful for troubleshooting): '/Users/pengxurui/workspace/public/ModularEventBus/eventbus_api/build/tmp/androidJavadocs/javadoc.options'
  • vanniktech 插件與 Dokka 插件兼容問題:
Execution failed for task ':eventbus_api:javaDocReleaseGeneration'.
> 'void org.jetbrains.dokka.DokkaSourceSetImpl.<init>(java.lang.String, org.jetbrains.dokka.DokkaSourceSetID,
...
  • POM 驗證錯誤:


7. 尋求 Sonatype 官方幫助

如果你在使用 Sonatype 的過程中遇到任何問題,可以嘗試向官方提問。我試過一次,10 分鐘後就收到回復了,還是很 Nice 的。

操作截圖

操作截圖


8. 總結

恭喜,到這裡,我們已經能夠實現發布開源項目到 MavenCentral 中央倉庫。還沒完,引出兩個問題:

  • Github Action: 每次發布都需要我們手動執行 upload 任務,Github 倉庫中的 Releases 面板也不會同步顯示手動發布的版本記錄。 我們期望的效果是在 Github 倉庫上發布一個 Release 版本時,自動觸發將該版本發布到 MavenCentral 中央倉庫。 這需要用到 Github 提供的 CI/CD 服務 —— Github Action;
  • ModularEventBus: 本文的示常式序,它是做什麼的呢?

關注我,帶你了解更多。


參考資料

你的點贊對我意義重大!微信搜索公眾號 [彭旭銳],希望大家可以一起討論技術,找到志同道合的朋友,我們下次見!

Tags: