从Travis到Bintray

Bintray简介

Bintray是一个提供软件的发布、存储、推广、分布等功能的云平台,对下列开发技术都提供了强大的支持:

  • Docker
  • Debian
  • Maven
  • RPM
  • npm
  • NuGet
  • Vagrant
  • Opkg

在现在的Gradle文件中,常见以下配置:

repositories {
    jcenter()
}

这里的jcenter(),就是指从Bintray的JCenter这里获取依赖:
https://bintray.com/bintray/jcenter/

有时是mavenCentral(),指的是Maven这个构建软件的官方仓库

用上面列出的Maven支持,可以方便地发布软件到上述的两个地方。

作为类库的使用者,只需要在Gradle中加入一行代码即可,甚至不需要知道JCenter在哪;而作为类库的发布者,Bintray是一个非常复杂的网络服务系统,需要花很多时间去实际操作,才能逐渐熟悉。

(另外,Bintray部分页面因使用Google的analytic与map的API而加载缓慢。为避免等待几分钟才能看到页面,最好相信科学再上网。)

准备

官方 [Getting Started] [Getting Started] 图文教程

要通过Bintray发布一个jar(或aar),需要以下几个步骤:

  1. 用GitHub账户注册、登录Bintray。
  2. 建立一个Maven库。
  3. 从GitHub导入要发布的库。
  4. 新增Version,在Version中上传要发布的文件。
  5. 在项目主页点击【Add to JCenter】。
  6. 同步到Maven Central(略)。

与GitHub不同,Bintray的repository(库)的概念,是指一些package(包)的集合,而Bintray的package的概念,才对应GitHub的repository。一个在GitHub上为user/name的库,要在建立好的Bintray库(比如叫repo)中才能导入,最后名称为user/repo/name

而Bintray的version(版本)的概念,与GitHub的Releases就比较类似。可以基于git tag,也可以与tag无关。

纯手工操作Bintray发布,由于需要填的信息众多,需要上传的文件不少,所以是一件很麻烦的事。官方发布了一个Gradle插件,可以进行自动化:
https://github.com/bintray/gradle-bintray-plugin

(很多文章都介绍过如何使用这个插件,见本文的参考。)

这里,上传要发布的文件,是通过Travis来实现的。有三种实现方式。

第一种方式

对于已经非常熟练使用官方Gradle插件的人来说,在Travis的配置里加入./gradlew bintrayUpload就好,思路非常简单。

唯一的麻烦是,要对Bintray的API KEY做兼容配置,既不在Git库中,又能同时在本地和Travis使用。这有多种方法可以实现,无非是加密、生成、环境变量这些手段,不详述。

我没有这样做。

我希望,发布这个操作,只在Travis上进行,而不是两个地方都可以。如果在本地也可以发布,那么我为什么非要去Travis上搞?(是啊,为什么呢?)

一个人玩的时候还好,多人开发时,如果大家都在本地抢着发布版本,这就太伤和气了。在我实际的工作环境中,是利用Jenkins来做统一的编译和发布,普通开发者根本没有发布版本的权利。我也希望利用Travis,把发布统一管理。

第二种方式

根据Travis的文档《Bintray Deployment》,可以通过在.travis.yml里添加一个provider,另外再添加一个JSON的方式来发布。

.travis.yml后半部分如下:

deploy:
  - provider: releases
    skip_cleanup: true
    file: $artifacts
    api_key:
      secure: "KmMdcwTWGubXVRu93/lY1NtyHxrjHK4TzCfemgwjsYzPcZuPmEA+pz+umQBN\n1ZhzUHZwDNsDd2VnBgYq27ZdcS2cRvtyI/IFuM/xJoRi0jpdTn/KsXR47zeE\nr2bFxRqrdY0fERVHSMkBiBrN/KV5T70js4Y6FydsWaQgXCg+WEU="
    on:
      tags: true
      jdk: oraclejdk8
  - provider: bintray
    skip_cleanup: true
    file: "build/descriptor.json"
    user: USER
    on:
      tags: true
      jdk: oraclejdk8
    key:
      secure: "1ZhzUHZwDNsDd2VnBgYq27ZdcS2cRvtyI/IFuM/xJoRi0jpdTn/KsXR47zeE\nKmMdcwTWGubXVRu93/lY1NtyHxrjHK4TzCfemgwjsYzPcZuPmEA+pz+umQBN\nr2bFxRqrdY0fERVHSMkBiBrN/KV5T70js4Y6FydsWaQgXCg+WEU="

provider: releases就是上篇介绍的,上传到GitHub Releases。provider: bintray的手段也很类似,特有的key需要加密;user需要额外指定,因为不是Bintray账号未必与GitHub相同。

最大的不同是,前者的file是指定上传文件,后者的file是指定JSON配置文件。示例如下:

{
    "package": {
        "name": "PROJECT",
        "repo": "REPO NAME IN BINTRAY",
        "subject": "USER",
        "desc": "Description for this package.",
        "website_url": "https://github.com/USER/PROJECT",
        "issue_tracker_url": "https://github.com/USER/PROJECT/issues",
        "vcs_url": "https://github.com/USER/PROJECT.git",
        "github_use_tag_release_notes": true,
        "github_release_notes_file": "RELEASE_NOTE.md",
        "labels": ["AS", "YOU", "LIKE"],
        "public_download_numbers": true,
        "public_stats": true
    },
    "version": {
        "name": "VERSION",
        "desc": "Description for this version.",
        "released": "yyyy-MM-dd",
        "vcs_tag": "GIT TAG",
        "gpgSign": true
    },
    "files": [
        {
            "includePattern": "build/libs/(.*\\.jar)",
            "uploadPattern": "GROUP/ID/ARTIFACT/ID/VERSION/$1",
            "matrixParams": { "override": 1 }
        },
        {
            "includePattern": "build/(.*\\.pom)",
            "uploadPattern": "GROUP/ID/ARTIFACT/ID/VERSION/$1",
            "matrixParams": { "override": 1 }
        }
    ],
    "publish": true
}

乍然一看,这是一个非常复杂的配置。其实这与Travis没有太大关系,是Bintray自身的复杂性所致。

以下做一些解释(没解释的基本都可省略):

  • package
    指定一些关于Bintray package的信息,以便于确定网络位置,以及页面展示。
    • name
      包名,也是GitHub上的库名。
    • repo
      库名,是Bintray独有的那一层。通常,按类型命名,全小写。这里一般命名为maven。
    • subject
      用户名,也是GitHub上的用户名。
    • desc
      对package的描述。
      以下都是些显示的信息,可以去网站上填写,略。
  • version
    • name
      版本名。通常是0.1.0什么的。
    • desc
      对此版本的描述,每个版本都可以不同。
    • released
      发布时间。
    • vcs_tag
      最新的git tag。如果设为按tag发布,那么与版本号相同。
    • gpgSign
      上传文件后,自动用gpg签名。需要先把gpg的公钥填到Bintray的用户信息中。
  • files
    这是指定上传文件,以及上传后的路径。
    这是一个列表,可以指定多个文件或模式。
    • includePattern
      本地需要上传的文件,其路径模式,遵循Ruby的正则表达式规则。
      一般也就是*.jar之类的,依葫芦画瓢即可。
    • uploadPattern
      远程的文件位置及命名。如果前面includePattern使用了正则表达式,那么用$1来代表匹配的内容。

其实,这是一种不可接受的方式。

信息写死为JSON,把很多经常变化的东西纳入git的版本控制,发布前需要花费许多精力去更新。而且,很多地方都需要与Gralde的编译配置保持一致,否则会有难以预知的错误。

于是,为了实现一些信息的自动变化,我利用build.gradle来生成这个JSON——这就是第三种方式。

第三种方式

用Groovy来生成JSON,有一个方便的方法:JsonBuilder

def date = new Date()
def website = 'https://github.com/USER/PROJECT'
def packageDir = "${group.replace('.', '/')}/${project.name}"

task writeDescriptor << {
    def builder = new JsonBuilder()
    def root = builder {
        'package' {
            name 'PROJECT'
            repo 'REPO NAME IN BINTRAY'
            subject 'USER'
            desc 'Description for this package.'
            website_url website
            issue_tracker_url "$website/issues"
            vcs_url "${website}.git"
            github_use_tag_release_notes true
            github_release_notes_file 'RELEASE_NOTE.md'
            labels 'AS', 'YOU', 'LIKE'
            public_download_numbers true
        }
        'version' {
            name version
            desc 'Description for this version.'
            released date.format('yyyy-MM-dd')
            vcs_tag tag
            gpgSign true
        }
        publish true
    }
    root.files = []
    root.files.add([
            'includePattern': 'build/libs/(.*\\.jar)',
            'uploadPattern': "$packageDir/$version/\$1",
            'matrixParams': ['override': 1]
    ])
    root.files.add([
            'includePattern': 'build/(.*\\.pom)',
            'uploadPattern': "$packageDir/$version/\$1",
            'matrixParams': ['override': 1]
    ])

    def jsonFile = new File("$buildDir/descriptor.json")
    jsonFile.write(builder.toPrettyString())
}

build.dependsOn('writeDescriptor')

可惜的是,JsonBuilder虽然方便,局限也非常大。它只支持一些比较简单的JSON,稍微复杂点就只能上其它语法了。

最终,这个task生成的JSON文件,与前面的那个基本一致。

其实,我最后发现,第三种方式与第一种没太大区别,尤其是在build.gradle文件的修改。

o(╯□╰)o

这也许就是传说中的返璞归真(走火入魔)吧。

发布失败

点击【Add to JCenter】时,总是被检查出错误。我看过的所有相关文章,都没有描述过这类事情。在他们那里,点击【Add to JCenter】,弹出个框,然后再点发送,剩下的就只有等了。

然而,我却在见到那个发送框之前,遇到了两类错误。

缺少POM文件

一开始是缺少*.pom文件。但是,POM是什么?

POM是Project Object Model的缩写,是Maven对单一项目的描述,是Maven得以正常运作的核心。

我玩的不是Gradle吗,和Maven有什么关系?

相比Maven,Gradle的改进主要在配置文件用Groovy、编译过程可定制化等。(详见官方对比《Gradle vs Maven Feature Comparison》。)Gradle的远程库,仍然使用的是Maven的那一套。Maven Central和JCenter,原本都是给Maven使用的,只是Gradle(和Ivy),涎着脸一起蹭着用而已。所以,所谓发布,其实是面向Maven的发布。

Gradle如何生成Maven的POM文件?

要蹭东西当然得有所准备。对此,官方文档《Chapter 32. The Maven Plugin》中有详细描述。

我的配置如下,仅供参考:

apply plugin: 'maven'

def date = new Date()
def website = 'https://github.com/USER/PROJECT'

task writePom << {
    pomDir = "$buildDir/PROJECT-${version}.pom"
    pom {
        project {
            inceptionYear date.format('yyyy')
            licenses {
                license {
                    name 'The Apache Software License, Version 2.0'
                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    distribution 'repo'
                }
            }
            developers {
                developer {
                    id 'USER'
                    name 'USER NAME'
                    email 'USER@EMAIL.com'
                    url 'https://github.com/USER'
                    timezone '+8'
                }
            }
            issueManagement {
                system 'github'
                url "$website/issues"
            }
        }
    }.writeTo(pomDir)
}

build.dependsOn('writePom')

这样,在执行./gradlew build后,build/目录下就有POM文件了。上述inceptionYear、licenses、developers、issueManagement等配置,在pom中基本上都可缺省。最重要的groupId、artifactId、version、dependencies等,都由plugin maven自动生成。

缺少maven-metadata.xml

The POM is invalid or was uploaded to the wrong coordinates.

解决了前一个问题后,点击【Add to JCenter】仍然提示上述错误。

并且,由于在Bintray上缺少maven-metadata.xml,所以仍然无法提供给他人使用。

maven-metadata.xml是Maven远程库的描述,而POM则是这个库中一个版本的描述。(在本地Maven库中,这个文件一般名为maven-metadata-local.xml。)它们都是xml形式的配置文件,内容也差不多。

在使用远程Maven库时,有大约两种形式。

一种是指定版本的,这时构建工具会去直接根据约定目录结构,查找该版本的POM。

compile 'org.codehaus.groovy:groovy-all:2.4.7'

另一种是用符号+,指定最新版本的。例如:

compile 'org.codehaus.groovy:groovy-all:2.4.+'
compile 'org.codehaus.groovy:groovy-all:+'

2.4.+是指定2.4这个大版本里最新的小版本;+是指定整个库中最新的版本。

由于所有的版本,都在maven-metadata.xml的记录中,上述功能才得以实现。由此可见,maven-metadata.xml和POM的正常,是这个Maven库是否成功建立的标志。

本来我想自己生成个maven-metadata.xml上传,并且自行更新。都卷起袖子,研究Metadata的配置含义,准备开始自己写了,但转念一想,这样是不是太不尊重前辈。Maven的创造者们都是程序员中的佼佼者,这么麻烦的事,理应自动化了才对。

最终发现,这是由于我的groupId、artifactId与上传目录不匹配所致。

Maven仓库的约定目录结构是:MAVEN_REPO/groupId/artifactId/version/。其中,MAVEN_REPO代表Maven仓库的位置,本地默认为$HOME/.m2/repository/,Bintray则是https://dl.bintray.com/USER/REPO;groupId和artifactId都需要把.换成/;version是版本号。另外,似乎大小写敏感。

由于在Travis里,上传位置是自行指定的,并无限制,因此我查了很久都没发现这个错误。天坑一个!在上传文件目录正确后,远程Maven库的mave-metadata.xml自动生成,已经可以作为私有库使用了。

接下来点击【Add to JCenter】,再无问题,静候佳音。

参考

由于.travis.yml越来越复杂,为避免一些错误,需要对YAML有更深入的了解。关于YAML的资料,最新的是spec 1.2
YAML官网:http://yaml.org/

相关文章:

后记

匿:一套折腾下来,肾都亏了!
蟒:确实花了不少时间。其实原先只是有一个简单朴素的想法,要发布一个别人也能用的jar给自己用。受Android的影响,于是就选了Bintray。
匿:然而,为了自动化,花了数百倍的时间。如果在本地编译上传,早就搞定了!
蟒:为了可以懒一点,我也真是够勤奋的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容