とある Java/Python 混在プロダクトで、パッケージ作成時のバージョン設定をまとめた話。
プロダクトは Java 主体なのでビルド設定は Gradle を使用し、成果物として以下を作っています。
- Java → .jar ファイル
- Python → .whl パッケージ
Gradle の設定は通常通り、build.gradle
があって、バージョン値は gradle.properties
に置いているとします。
version=1.0.0
これに Python のパッケージバージョンを連動させるようにします。
- バージョン値の設定は一箇所に集約する
- 全ビルド時はプロダクト全体でバージョンを統一する
- 部分ビルドで Python パッケージだけのビルドもしたい
Python パッケージ
Python パッケージは setuptools を使い、setup.py
というスクリプトを用意して、以下のコマンドを実行すると .whl ファイルが作られます。
スクリプトは setuptools.setup() メソッドにパッケージ情報を渡すだけです。
$ python setup.py bdist_wheel
# -*- 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通りの記述方法がありました。
[metadata]
version = 1.0.0
[metadata]
version = file: VERSION
ビルド時にバージョン値を自動で渡したいので、後者の VERSION
ファイルにバージョン値を書く方法を使いたいと思います。
なお、setup.cfg
には他のパッケージ情報をまとめて記述することができるので、setuptools.setup() に指定していた情報をすべて外部化できます。
(ここでは簡単のためバージョン値のみを扱います)
ただし、file:XXX
を使って設定値を外部ファイルから取得する記述は setuptools ライブラリが古いとサポートされないようなので、新しいものにしておきましょう。
私が試したのは 52.0.0 です。
部分ビルド
setup.cfg
へ外部ファイルでバージョン値を指定する形で、以下のように変更しました。
sample/
setup.py
setup.cfg
VERSION
...
[metadata]
version = file: VERSION
VERSION ファイル
1.0.0
# -*- 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
ファイルは含めないようにします。
*.egg-info/
VERSION
全ビルド
gradle の全ビルドに Python のパッケージ作成を組み込みます。
setup.py
を実行するスクリプトをタスクとして追加します。
root/
build.gradle
gradle.properties # バージョン値を設定
make_wheel.py # サブプロジェクトの setup.py を実行する
:
sample/ # Python のサブプロジェクト
setup.py
setup.cfg
VERSION
:
version=1.0.0
:
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
はこんな感じ。
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
の末尾に以下を追加
:
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
としました。
ここはプロジェクトの要件次第かと思います。
# バージョン値を 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