Pythonでインストールされたパッケージの出所についての情報の取得方法

Pythonのエコシステムでインストールされたパッケージの出所に関する情報を入手する方法を見ていきましょう。このアイデアは、今日現在ドラフト状態のPEP-710の一部です。

チェコ共和国・ジドロホヴィツェ - アカシアの塔の展望台。筆者による写真。


このチュートリアルでは、github.com/fridex/pip-provenanceで入手できるファイルを使用します。

Chainguard's Pythonイメージを使って、シンプルなPythonアプリケーションを作成しましょう。このアプリケーションはシンプルなflaskのhello worldアプリケーションになります。app.pyスクリプトは以下の内容になります:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello, world!'

app.run(host='0.0.0.0', port=8080)

さらに、以下の内容のrequirements.inファイルを作成しましょう:

flask

再現性のために特定のバージョンに依存関係をロックし、インストールされたPythonディストリビューションのハッシュを保持するために、pip-toolsを使用します:

pip-compile --generate-hashes

上記のコマンドはrequirements.txtファイルを作成します。そのようなファイルの例はこちらで見つけることができます

次に、アプリケーションを含むコンテナ環境を作成しましょう。

アップストリームのpipを使用して

最初に、Chainguard'sイメージにも同梱されているアップストリームのpipを使用します。Chainguardによって記述されたDockerfileを、コンテナ化されたアプリケーションを確実にするために最小限の変更を加えて使用します:

FROM cgr.dev/chainguard/python:latest-dev as builder

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt --user

FROM cgr.dev/chainguard/python:latest

WORKDIR /app
# Pythonのバージョンをパス内で更新することを忘れないでください
COPY --from=builder /home/nonroot/.local/lib/python3.11/site-packages /home/nonroot/.local/lib/python3.11/site-packages
COPY app.py .

ENTRYPOINT ["python", "/app/app.py"]

コンテナ化されたアプリケーションのビルド:

podman build -f raw/Dockerfile -t pip-provenance:raw .

その後、ビルドされたアプリケーションは実行され、locahost:8080でアクセスできます:

podman run -p 8080:8080 pip-provenance:raw

では、誰かがこのイメージをレジストリに公開したと想像し、インストールされたパッケージについての情報を得たいとしましょう。pip-provenance:rawイメージを引っ張ってきてpip freezeを実行することができます。残念ながら、pip freezeはインストールされたPythonパッケージとそのバージョンのみを示します:

$ pip freeze                     
click==8.1.3
Flask==2.2.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
Werkzeug==2.2.3

これらのパッケージが実際にどこからインストールされたのかについての情報はありません。また、これらのパッケージのダイジェストに関する情報もありません。PEP-610に従って直接URLを使用してインストールされたパッケージは例外ですが、私たちの例ではそうではありません。

パッチされたpipを使用して

PEP-710には、名前で特定されたパッケージ、およびオプションでそのバージョン(私たちの例にある)のインストール時に出所情報を保存するという提案がありました。どのような情報が保存されているか、そして私たちはそれをどのようにアクセスできるかを見てみましょう。

まず、PEP-710に従うpipのパッチ済みバージョンを使用するようにDockerfileを調整しましょう:

FROM cgr.dev/chainguard/python:latest-dev as builder

WORKDIR /app
COPY requirements.txt .
# ----->%------
USER root
RUN pip install --force-reinstall pip install git+https://github.com/fridex/pip.git@provenance-url
USER nonroot
# -----%<------
RUN pip install -r requirements.txt --user

FROM cgr.dev/chainguard/python:latest

WORKDIR /app
# Pythonのバージョンをパス内で更新することを忘れないでください
COPY --from=builder /home/nonroot/.local/lib/python3.11/site-packages /home/nonroot/.local/lib/python3.11/site-packages
COPY app.py .

ENTRYPOINT ["python", "/app/app.py"]

このアプリケーションをビルドしましょう:

podman build -f patched/Dockerfile -t pip-provenance:patched .

変更によって影響はなく、アプリケーションを実行してlocalhost:8080でアクセスすることができます:

podman run -p 8080:8080 pip-provenance:patched

PEP-710に従い、pipはsite-packagesにある*.dist-infoディレクトリに出所情報を格納します。コンテナ化環境からsite-packagesディレクトリをコピーして、そこに何がインストールされているかを確認しましょう(前の例で実行されたコンテナ化環境のハッシュと[CONTAINER_HASH]を置換します):

podman cp [CONTAINER_HASH]:/home/nonroot/.local/lib/python3.11/site-packages site-packages

パッケージflaskのためのprovenance_url.jsonファイルを見てみましょう*:

$ cat ./site-packages/Flask-2.2.3.dist-info/provenance_url.json | jq
{
  "archive_info": {
    "hash": "sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d",
    "hashes": {
      "sha256": "c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"
    }
  },
  "url": "https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl"
}

このファイルはパッチ済みのpipによって作成され、PEP-710でより詳細に記述されています。

pip-preserveという小さなツールは、site-packagesディレクトリの内容を読み取り、インストールされた各Pythonパッケージのprovenance_url.jsonを理解することができます。さらに、パッケージが直接URLを使用してインストールされた場合、このツールはPEP-610に記述されているdirect_url.jsonも読み取って、環境を完全に再構築することができます。コンテナ化された環境からのsite-packagesディレクトリに対してこのツールを使用しましょう:

$ pip install pip-preserve
...
$ pip-preserve --ignore-errors --site-packages ./site-packages      
#
# This file is autogenerated by pip-preserve version 0.0.2.post1 with Python 3.10.6.
#
click==8.1.3 \
  --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
flask==2.2.3 \
  --hash=sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d
itsdangerous==2.1.2 \
  --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44
jinja2==3.1.2 \
  --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
markupsafe==2.1.2 \
  --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6
werkzeug==2.2.3 \
  --hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612

ご覧のように、このツールはインストールされたパッケージのリスト、そのバージョンおよびハッシュを記載したrequirements.txtファイルを再構築しました。

読者は、再構築されたファイルに各パッケージごとに1つのハッシュしかないことに気づくかもしれません。その理由は、pipは1つのパッケージしかインストールしないからです。当初のrequirements.txtファイルは複数のハッシュをリストしており、それはPythonディストリビューションがPyPIで公開された時点でのものに対応しています。インストール時に、pipはPythonディストリビューションがインストールされる環境に一致するものを取ります。例えば、pipはflask==2.2.3のためにPyPI上で公開されているビルドファイルを取りましたが、ソースディストリビューションは取りませんでした(アーティファクトのハッシュを確認することによって確認できます)。パッチ済みのpipを使用することで、インストールされた正確なアーティファクトを指し示すことができます。

--direct-urlオプションをpip-preserveツールに渡すと、Pythonパッケージがインストールされた正確なURLを得ることができます:

$ pip-preserve --ignore-errors --direct-url --site-packages ./site-packages
#
# This file is autogenerated by pip-preserve version 0.0.2.post1 with Python 3.10.6.
#
https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl \
  --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl \
  --hash=sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d
https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl \
  --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44
https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl \
  --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
https://files.pythonhosted.org/packages/5a/94/d056bf5dbadf7f4b193ee2a132b3d49ffa1602371e3847518b2982045425/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl \
  --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6
https://files.pythonhosted.org/packages/f6/f8/9da63c1617ae2a1dec2fbf6412f3a0cfe9d4ce029eccbda6e1e4258ca45f/Werkzeug-2.2.3-py3-none-any.whl \
  --hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612

これが便利な理由は?

さて、[PEP-710](https://peps.python.org/pep

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/fridex/how-to-get-information-about-the-provenance-of-python-packages-installed-4f65