rcmdnk's blog

Dreams Come True [Analog]

rcmdnk/homebrew-file のスクリプトがだいぶ長くて辛くなってきたので 複数のファイルの分けた上で後で1つのスクリプトとしてまとめ直すようにしたことについて。

Homebrew-file

Homebrewでインストールしたパッケージ等をBrewfileを使って管理するためのツール。

Homebrew公式でも bundle というBrewfileを書き出すコマンドがありますが、 homebrew-file (brew-file)はBrewfileをGitHubなどを使って管理して 他の環境と共有したり出来ます。

このツールはbrew-file というスクリプトをHomebrewの実行ファイルが入っているbinディレクトリにインストール(リンク)することで、 brewコマンドにfileというサブコマンドを追加します。 (brewはPATH下にあるbrew-XXXというコマンドを認識してサブコマンドと取り入れてくれる。)

Homebrewでインストールするのに一番簡単なのは1つの実行ファイルをこのようにbinに入れるようにすることです。

なのでこれまで1つのファイルに全部書いてそれだけあれば実行できるスクリプトとして管理していました。

ただかなり長いスクリプトになってしまって、複数のClassとかもあるとどの関数がどのClassのものか、もパッと見分かりづらいし 色々辛くなってきたので分けたいな、と。

Pythonで作っているので、PyPIに公開してpipでインストールして コマンドをインストールするようにすればもともと複数のファイルに分けていても問題ないのですが、 HomebrewのツールなのでHomebrewで管理できるようにしたいところ。

Python以外にして実行ファイルを作る方法もありますが、とりあえず現在Pythonで作ってるものをどうするか。

適当に分けて組み合わせる

とりあえず現状はClass単位とかでファイルに分けて、それをシェルスクリプトで組み合わせるようにしました。

  • brew-fileという1つのスクリプトを以下のファイルに分割
    • info.py: __で囲われたスクリプトの属性情報のリスト
    • utils.py: Classに含まれない関数など
    • brew_helper.py: BrewHelper
    • brew_info.py: BrewInfo
    • brew_file.py: BrewFile
    • main.py: main関数及びその実行スクリプト

これをPoetryのレポジトリ構成で管理するようにしています。

pyproject.tomlの中で

1
2
[tool.poetry.scripts]
brew-file = "brew_file.main:main"

と定義しておくことでpoetry install後、poetry環境でbrew-fileが最終的なスクリプトと同じ動作になるようにしています。 (現在は主にテスト用。)

各ファイル間で、他のファイルの関数やクラスを使いたい場合には同パッケージ内のimportになるので

1
from .brew_file import BrewFile

みたいな感じになります。

ここで、必ずfrom .<file> import Xの形でimportするようにしておきます。これは後でまとめるときにやりやすくするため。

これで、以下のようなスクリプトでまとめて1つのスクリプトにしています。

やってることは

  • src以下のファイルたちにblack, autoflake, autopep8, isortをあててformatしておく(これは別になくてもいい)。
  • 上のファイルの順で、import部分と残りの部分と分けて取得。
    • この際、from .で始まるものは除く。
  • スクリプトの先頭にshebangを書き込む。
  • import部分をつなげてスクリプトに書き込む。
  • 残りの部分をつなげてスクリプトに書き込む。
  • 出来たスクリプトをblack, autoflake, autopep8, isortでformatする。

他のファイルからのimportを from .<file> import Xの形にすることで、そのファイルの中で使う形は最終的なスクリプトの中で使う形と同じにできます。 ディレクトリ構造を作っている場合でもfrom .<dir>.<file> import Xのように中のClassや関数を直接指定して(かつ、その中のモジュールを直接取り出すようなことをしなければ)同じようにできます。

その上でfrom .部分を落とすことでスクリプト内で完結する部分を成立させます。

importに関するものはスクリプトトップに書き込んで、書き込む際には特に重複など気にしないでOK。 後でisortが全部いい感じに重複やら順番やらを調整してくれます。

最初はgrep -E "(^import|^from)"的に安易に抜き出しを行ってたんですが、 importするものの数多くなると行幅制限以下にするために

1
2
3
4
5
6
from .utils import (
    aaa,
    bbb,
    ccc,
    ...
)

みたいに括弧を使った形でfromがない部分もimport文になるため、 スクリプトの中ではこの辺をチェックしながらimportに関する部分を抜き出すようにしています 1

後は適当に取得したimport部分とその他の部分をつなげて書き込んでるだけですが、 formatter達が行間とかもいい感じに直してくれるのでこれで今のところ問題なくうまく合成できています。

その他の方法

PyPIで公開してpipで入れるようにする

上のレポジトリがすでにpoetry仕様になっているのでPyPIに公開しようと思えば直ぐにできます。 pip install brew-fileとかでbrew-fileコマンドがインストールされ、 Homebrewで入れたのと同じように使えます。

問題としてはPythonの別の仮想環境とかに入ってしまうと使えなくなるとか。

そもそも該当するPythonの環境を(Homebrewでもいいですが)整えてからpipで別途入れないといけないとか。

一方でこれにすると他のライブラリとかに依存しても良いので開発としては楽は出来るかもしれません。 現状では基本的に別途pip installするようなパッケージは必要ないように作ってます。

これを本命にするのはないとしてもPyPIに公開するくらいはしても良いかもしれない、とは考えてたりします。

単独バイナリファイル化

PyInstallerは Pythonスクリプトを実行バイナリファイルにして配布できるようしてくれるツール。 pipで入れられます。

1
$ pip install pyinstaller

これで、pyinstallerコマンドが使えるようになります。

Poetryで作ったレポジトリだとcliとして実行できるスクリプトがそのままあるわけではないので、 一旦Poetryの仮想環境にインストールした実行ファイルをpyinstallerにかけます。

1
2
3
4
$ poetry install
$ poetry run which brew-file
.../.cache/pypoetry/virtualenvs/brew-file-XXXXXX-py3.10/bin/brew-file
$ pyinstaller --onefile .../.cache/pypoetry/virtualenvs/brew-file-XXXXXX-py3.10/bin/brew-file

--onefile(もしくは-F)を与えるとライブラリとかも含めて1つの実行ファイルとしてまとめてくれます。 (つけないと同じディレクトリに実行ファイル以外にライブラリふぁいるも出来て実行時にそれらも必要となる。)

dist/brew-fileというファイルが出来ているので、これを実行すると上で作ったPythonのbrew-fileスクリプトと同じ結果になります。

また、この際にはPythonがインストールされて無くても実行できます。

仮に

1
$ pyinsataller --onefile src/brew-file/main.py

みたいにmainファイルを直接指定するとdist/mainというバイナリファイルが出来ますが、実行すると

1
2
3
4
5
$ ./dist/main
Traceback (most recent call last):
  File “brew_file/main.py”, line 5, in <module>
  ImportError: attempted relative import with no known parent package
  [91605] Failed to execute script ‘main’ due to unhandled exception!

みたいなエラーが出るかと思います。

dist/brew-fileとして出来たちゃんと実行できるバイナリファイルに関して、 これがどこでも確実に使えれば良いんですが、少なくともmacOSでcompileしたものはUbuntuで動かないし Linuxでも環境によって使えるものが変わります。

なのでそれぞれごとに作らないと行けない上にFormulaもそれように対応しなくてはいけないのでかなり大変です。 (GitHub Actionsだけではまかないきれないので。)

というわけでこれはちょっと難しそう。macOSだけであれば対応masOS version絞ってやればできそうな感じはしますし Pythonへの依存性も消えるのでより便利かもしれませんが。

他にも py2exePyOxidizer といったものがありますが、基本的には同じような感じです。

他のPythonスクリプト化

Pythonのスクリプトとしてsingle scriptにまとめるツールというのもいくつか存在します。

stickytapeの方はPoetryの構造だとそのまま使えませんが、 src/brew_fileのファイルを別のディレクトリにコピーして、各ファイルのimportのfrom .utils ...などと .で始まってる部分からピリオドを落とすように書き換えた後、

1
$ stickytape modified_dir/main.py --output-file brew-file

みたいにするとbrew-fileスクリプトが出来ます。 このスクリプトの中を見ると

1
2
3
4
5
6
7
8
#!/usr/bin/env python
import contextlib as __stickytape_contextlib
@__stickytape_contextlib.contextmanager
def __stickytape_temporary_dir():
    import tempfile
    import shutil
    dir_path = tempfile.mkdtemp()
...

みたいな感じで始まってmain.pyにあたる部分は下の方に大体そのままコピーされているような感じですが、 他のimportされたファイルの中身は__stickytape_write_moduleという関数の中に文字列として中身が渡されているような感じになっています。

それらから一時ファイルとして各モジュールファイルを作成してimportして、みたいなことをしているみたいです。

一応これで出来たファイルは正しく動いているように見えました。

ただこれ使うくらいなら上のシンプルなcombineスクリプトで良いかな、と思ってます。

上の例ではimportの形を制限したりしているので、そうでない場合に自分でスクリプト書くのが 面倒な場合はこれを試してみるのもありかもしれません。 ただ、PoetryのようなPythonパッケージ構造の場合にはそのままでは使えないので注意です。

pinlinerの方はそもそもスクリプトの作成ではなくパッケージをシングルファイルに、 というものなので今回はうまく使えなそうな感じでした。

まとめ

とりあえず少しファイルを分けられたことで開発する際に大分やりやすくなりました。

Homebrewで簡単にインストールするためにsingle scriptにする必要がある状態ですが、 もともと1つのスクリプトになってたものだったので、適当にファイルに分けていても 比較的簡単に1つにまとめることが出来るようになっています。

完全に汎用的に使える手段ではないですが、 ある程度簡単なルール決めさえしておけば使える手段です。

簡単につなげるだけでも、isortなどのformatterが強力なのでいい感じの状態に 直してくれる、という部分も大きいです。

homebrew-fileに関してはしばらくはこんな感じの運用で行く予定です。

Sponsored Links
Sponsored Links

« HomebrewのTapを管理しているレポジトリのデフォルトブランチを変更した際に起こったこと Homebrew-file 9.0.Xへアップデート: Homebrew 4.0.Xへの対応など »

}