CLOVER🍀

That was when it all began.

Nexus 3向けにMavenのローカルリポジトリのファイルをリモートリポジトリにデプロイするスクリプトを書く

最近、MavenのRepositoryとしてSonatype Nexus 3を使っているのですが、2の頃と違ってリポジトリの構成がファイルシステム
そのものではなくて、バイナリな感じになりましたね?

これにより、Maven Centralに置いていないようなライブラリは、Nexusに個別にdeploy-fileする必要がありました。

Sonatype Nexus 3で、Third PartyのMavenアーティファクトをアップロードする(+リポジトリについて少し) - CLOVER

これはこれで仕方がなさそうですが、ライブラリが大量にあるとちょっと困ります。これをなんとかしようかなぁと。

目標とゴール感

要するに、Nexusに突っ込みたいライブラリを、依存ライブラリ含めて一式突っ込みたいわけです。

ですが、deploy-fileだと個別にファイルを指定する必要があります。とても面倒なうえに、そもそも依存関係が
わかりません。

で、どうしようかなぁと。

dependency:treeの結果を使おうとも思ったのですが、これだとpackagingがpomのものが入りません。それで困るの?
と聞かれれば、困ります。BOMや親pomが解決できませんからね。
※実際、そのパターンも作ってpomがどうにもならないことに気づいてやめました

とすると、ローカルリポジトリをごそっとアップロードできる方法を考えた方が良さそうです。

で、あらかじめローカルリポジトリに収集したファイルを順次デプロイするスクリプトを書いてみました。

やったこと

$HOME/.m2/repositoryにあるファイルは、deploy:deploy-fileでは直接デプロイできないので、まずは依存ライブラリを別のローカルリポジトリに
集める方法を考えます。

「-Dmaven.repo.local」を使ってごまかしましょう。

ソースコードの収集。

$ mvn dependency:sources -Dmaven.repo.local=localrepos

Javadocの収集。

$ mvn dependency:resolve -Dclassifier=javadoc -Dmaven.repo.local=localrepos

go-offline。

$ mvn dependency:go-offline -Dclassifier=javadoc -Dmaven.repo.local=localrepos

あとは、このローカルリポジトリからひたすらデプロイしていくスクリプトを書きます。Groovyですが。
deploy-local-repository.groovy

@Grab('org.apache.maven:maven-model:3.5.0')
import org.apache.maven.model.io.xpp3.MavenXpp3Reader

def localRepositoryDir = args[0]

def repositoryId = 'my-maven-hosted-repo'
def repositoryUrl = 'http://localhost:8081/repository/my-maven-hosted-repo/'

def deployFailedArtifacts = []

def select = { artifactId, groupId, version, packaging ->
  true
}

new File(localRepositoryDir).eachFileRecurse { file ->
  if (file.directory) {
    return
  }

  if (!file.name.endsWith('pom')) {
    return
  }

  def reader = new MavenXpp3Reader()
  file.withReader('UTF-8') { r ->
    def model = reader.read(r)
    def parent = model.parent

    def artifactId = model.artifactId
    def groupId = model.groupId ?: parent.groupId
    def version = model.version ?: parent.version
    def packaging = model.packaging

    if (!select(artifactId, groupId, version, packaging)) {
      return
    }

    def artifactBasePath = file.parent

    def artifactFilePath = null
    def javadocFilePath = null
    def sourcesFilePath = null

    new File(artifactBasePath).eachFile { f ->
      if (f.name.endsWith('-javadoc.jar')) {
        javadocFilePath = f.path
      } else if (f.name.endsWith('-sources.jar')) {
        sourcesFilePath = f.path
      } else if (f.name.endsWith(packaging)) {
        artifactFilePath = f.path
      }
    }

    if (artifactFilePath == null) {
      artifactFilePath = file.path
    }

    def command =
      [
        'mvn deploy:deploy-file',
        "-Dfile=${artifactFilePath}",
        "-DrepositoryId=${repositoryId}",
        "-Durl=${repositoryUrl}",
        "-DpomFile=${artifactBasePath}/${artifactId}-${version}.pom"
      ]

    if (javadocFilePath && new File(javadocFilePath).exists()) {
      command << "-Djavadoc=${javadocFilePath}"
    }

    if (sourcesFilePath && new File(sourcesFilePath).exists()) {
      command << "-Dsources=${sourcesFilePath}"
    }

    def process = command.join(' ').execute()
    println('=================================================================')
    println("command: ${command.join(' ')}")
    println("Result:")
    println(process.text)

    try {
      def result = process.waitFor()
      if (result != 0) {
        deployFailedArtifacts << "${groupId}:${artifactId}:${version}"
      }
    } finally {
      process.destroy()
    }
  }
}

if (deployFailedArtifacts) {
  println("Deploy Failed Artifacts: ${deployFailedArtifacts.size()}")
  println("===== detail =====")
  deployFailedArtifacts.each { println(it) }
}

指定されたローカルリポジトリにあるpomから、アーティファクトの情報を取り出し、実際にローカルリポジトリに置かれているファイルと
合わせてdeploy:deploy-file向けのコマンドを作り出します。

こんな感じで使います。

$ groovy deploy-local-repository.groovy localrepos

デプロイ先のリポジトリは、先頭に定義してあるので適当に変えてください。

def repositoryId = 'my-maven-hosted-repo'
def repositoryUrl = 'http://localhost:8081/repository/my-maven-hosted-repo/'

指定されたローカルリポジトリを再帰的に探索して

new File(localRepositoryDir).eachFileRecurse { file ->

pomからアーティファクトの情報を作り出します。

  def reader = new MavenXpp3Reader()
  file.withReader('UTF-8') { r ->
    def model = reader.read(r)
    def parent = model.parent

    def artifactId = model.artifactId
    def groupId = model.groupId ?: parent.groupId
    def version = model.version ?: parent.version
    def packaging = model.packaging

親のgroupIdやversionを引き継いでいる場合は、pomの解析結果がnullとなってしまうので、その場合は親の情報で補完するようにしています。

あとは、同じディレクトリ内にあるファイル群から、Javadocやソースコード、アーティファクトそのものを探します。

    def artifactBasePath = file.parent

    def artifactFilePath = null
    def javadocFilePath = null
    def sourcesFilePath = null

    new File(artifactBasePath).eachFile { f ->
      if (f.name.endsWith('-javadoc.jar')) {
        javadocFilePath = f.path
      } else if (f.name.endsWith('-sources.jar')) {
        sourcesFilePath = f.path
      } else if (f.name.endsWith(packaging)) {
        artifactFilePath = f.path
      }
    }

    if (artifactFilePath == null) {
      artifactFilePath = file.path
    }

最後に、これらをつなげて「mvn deploy:deploy-file」にして実行。

    def command =
      [
        'mvn deploy:deploy-file',
        "-Dfile=${artifactFilePath}",
        "-DrepositoryId=${repositoryId}",
        "-Durl=${repositoryUrl}",
        "-DpomFile=${artifactBasePath}/${artifactId}-${version}.pom"
      ]

    if (javadocFilePath && new File(javadocFilePath).exists()) {
      command << "-Djavadoc=${javadocFilePath}"
    }

    if (sourcesFilePath && new File(sourcesFilePath).exists()) {
      command << "-Dsources=${sourcesFilePath}"
    }

    def process = command.join(' ').execute()

デプロイに失敗したアーティファクトは、最後にまとめて出力するようにしています。

この実装だと、ローカルリポジトリにあるすべてのpomを対象にしてしまうので、それなりに時間がかかります。

対象を絞りたい場合は、フィルタリングして対象を絞るとよいでしょう。今回は、テンプレート的にこのように作りました。

def select = { artifactId, groupId, version, packaging ->
  true
}

この関数がtrueを返すと、デプロイ対象となります。

    if (!select(artifactId, groupId, version, packaging)) {
      return
    }

ローカルリポジトリにJARがダウンロードできていなければ失敗しますし、classfierなどすべての組み合わせで動く保証などはありませんが、
とりあえずこんな感じかなぁと。

なにかおかしいところがあったら、ちょこちょこと修正していくと思います。