3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Python][Gradle] Java と Python の混在プロジェクトでパッケージバージョンを統一する

Posted at

とある Java/Python 混在プロダクトで、パッケージ作成時のバージョン設定をまとめた話。

プロダクトは Java 主体なのでビルド設定は Gradle を使用し、成果物として以下を作っています。

  • Java → .jar ファイル
  • Python → .whl パッケージ

Gradle の設定は通常通り、build.gradle があって、バージョン値は gradle.properties に置いているとします。

gradle.properties
version=1.0.0

これに Python のパッケージバージョンを連動させるようにします。

  • バージョン値の設定は一箇所に集約する
  • 全ビルド時はプロダクト全体でバージョンを統一する
  • 部分ビルドで Python パッケージだけのビルドもしたい

Python パッケージ

Python パッケージは setuptools を使い、setup.py というスクリプトを用意して、以下のコマンドを実行すると .whl ファイルが作られます。
スクリプトは setuptools.setup() メソッドにパッケージ情報を渡すだけです。

$ python setup.py bdist_wheel
setup.py
# -*- coding: utf-8 -*-
import setuptools

d = setuptools.setup(
    name='sample-prj',
    version='1.0.0',
    author='Foo',
    author_email='foo@example.com',
    url='https://www.example.com',
    description='Sample project',
    packages=setuptools.find_packages()
)

デフォルトでは setup.py のパッケージ情報としてバージョン値を直接記述することになりますが、これを何らかの方法で外から指定できるようにします。
setup.py と同じディレクトリに setup.cfg というファイルを置いて設定を外部化する方法があり、以下の 2通りの記述方法がありました。

setup.cfg
[metadata]
version = 1.0.0
setup.cfg
[metadata]
version = file: VERSION

ビルド時にバージョン値を自動で渡したいので、後者の VERSION ファイルにバージョン値を書く方法を使いたいと思います。

なお、setup.cfg には他のパッケージ情報をまとめて記述することができるので、setuptools.setup() に指定していた情報をすべて外部化できます。
(ここでは簡単のためバージョン値のみを扱います)

ただし、file:XXX を使って設定値を外部ファイルから取得する記述は setuptools ライブラリが古いとサポートされないようなので、新しいものにしておきましょう。
私が試したのは 52.0.0 です。

部分ビルド

setup.cfg へ外部ファイルでバージョン値を指定する形で、以下のように変更しました。

sample/
  setup.py
  setup.cfg
  VERSION
  ...
setup.cfg
[metadata]
version = file: VERSION

VERSION ファイル

1.0.0
setup.py
# -*- coding: utf-8 -*-
import os
import setuptools

if not os.path.isfile("VERSION"):
    with open("VERSION", "w") as f:
        f.write("0.0.1")

d = setuptools.setup(
    name='sample-prj',
    author='Foo',
    author_email='foo@example.com',
    url='https://www.example.com',
    description='Sample project',
    packages=setuptools.find_packages()
)

setup.py の冒頭で VERSION ファイルの有無を確認し、なければデフォルトで作るようにしています。
部分ビルドでとりあえずパッケージを作りたいときには、デフォルト値で作られます。

全ビルドのときは、外部から VERSION ファイルを作ってから setup.py を実行するようにしてバージョン値を連携することができます。

git でバージョン管理する場合は VERSION ファイルは含めないようにします。

.gitignore
*.egg-info/
VERSION

全ビルド

gradle の全ビルドに Python のパッケージ作成を組み込みます。

setup.py を実行するスクリプトをタスクとして追加します。

root/
  build.gradle
  gradle.properties       # バージョン値を設定
  make_wheel.py           # サブプロジェクトの setup.py を実行する
    :
sample/                   # Python のサブプロジェクト
  setup.py
  setup.cfg
  VERSION
    :
gradle.properties
version=1.0.0
build.gradle
  :
task make_wheel(type:Exec) {
    workingDir '.'
    commandLine 'python3', 'make_wheel.py', getProperty('version')
}

tasks.installDist.dependsOn make_wheel

distributions {
    main {
        baseName = 'sample-package'
        contents {
              :
            into('wheel') {
                from 'build/wheel'
            }
              :
        }
    }
}

パッケージ作成スクリプトにバージョン値を引数で渡し、作成した wheel パッケージを build/wheel/*.whl として収集する形にします。
make_wheel.py はこんな感じ。

make_wheel.py
import os
import sys
import subprocess
import shutil

# 引数からバージョン値を取得
pkg_version = sys.argv[1]

# バージョン値を Java形式→Python形式へ変換
_vers = pkg_version.split('-')
if len(_vers) > 1:
    if _vers[1] == 'SNAPSHOT':
        pkg_version = _vers[0] + 'rc1'
    else:
        pkg_version = _vers[0] + 'a1'

# サブプロジェクトで setup.py を実行する
def run_setup(dir, task):
    subprocess.call('python3 setup.py ' + task, shell=True, cwd=dir)

INSTALL_DIR = os.path.join('build', 'wheel')
TARGET_DIRS = ['../sample']

# ビルド結果を配置するディレクトリ作成
if os.path.exists(INSTALL_DIR):
    shutil.rmtree(INSTALL_DIR)
os.makedirs(INSTALL_DIR)

# Python のサブプロジェクトに対して setup.py を実行する
for d in TARGET_DIRS:
    # VERSION ファイルを作成
    with open(os.path.join(d, 'VERSION'), 'w') as f:
            f.write(pkg_version)
    setup(d, 'bdist_wheel')

サブプロジェクトで作成した wheel パッケージを全体で集めたいので、setup.py の bdist_wheel 実行時に .whl ファイルを INSTALL_DIR へコピーするようにしておきます。

setup.py の末尾に以下を追加

setup.py
  :
d = setuptools.setup(...)

INSTALL_DIR = '../root/build/wheel'  # プロジェクト構成、build.gradle、make_wheel.py と整合させておく

if os.path.exists(SDK_DIR):
    for t, v, p in d.dist_files:
        print(t, v, p)
        if t == 'bdist_wheel':
            shutil.copy2(p, os.path.join(INSTALL_DIR, os.path.basename(p)))

d.dist_files は以下の値を返すようです。

t : bdist_wheel
v : 3.6
p : dist/sample-1.0.0-py3-none-any.whl

これで、全体の gradle ビルド時に Python のパッケージが作成され、成果物が収集されるようになります。

ちなみに、Java のパッケージには 1.0.0-SNAPSHOT のような値を指定したいことがありますが、これがそのまま指定されると Python 側でエラーになりました。

Python 側のバージョンは PEP 400 というルールがあるようです。

今回の例では、make_wheel.py の先頭で受け取った Java 形式のバージョンを Python 形式へ変換するようにしました。

1.0.0-SNAPSHOT の場合は 1.0.0rc1 とし、その他の 1.0.0-xxx だった場合は α版の 1.0.0a1 としました。
ここはプロジェクトの要件次第かと思います。

make_wheel.py
# バージョン値を Java形式→Python形式へ変換
_vers = pkg_version.split('-')
if len(_vers) > 1:
    if _vers[1] == 'SNAPSHOT':
        pkg_version = _vers[0] + 'rc1'
    else:
        pkg_version = _vers[0] + 'a1'

// EOF

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?