【Python】requirements.txtをハッシュピン+wheelsでpip installをオフラインで実行する方法

概要

今回はPythonツールを業務端末やエアギャップ環境で安全に配布するための、requirements.txtのハッシュピンとwheelsを使ったオフラインインストールについて解説していきたいと思います。

社内ツールを配布していると「この端末、インターネットにつながりません」といわれることありませんか(- -;

情シス管理下の端末だとプロキシでpip接続がブロックされてたり、そもそもインターネット非接続のエアギャップ環境だったりします。

さらに最近はサプライチェーン攻撃(PyPIにマルウェア混入パッケージが上がる事案)も増えていて、「社内ツールだから適当にpip installでOK」という時代ではなくなってきました。

そこで本記事では、SHA256ハッシュで依存パッケージを検証しつつ、ローカルのwheelファイルだけで完結するオフラインインストールの手順を、実プロジェクトで使っている構成そのままで紹介します。

これで少しは安心してPythonを使用できると思います!

それではやっていきましょう!

目次

この構成でできるようになること

<オフライン配布の実現事項>
  • PyPIに接続できない端末でもpip installが完結する
  • 配布時にサプライチェーン改ざんを検知できる(SHA256不一致で失敗する)
  • 配布物から何が入るかを「requirements.txt」と「wheels/」だけで完全に把握できる

逆にこの方式では依存関係の自動解決は捨てることになります。
新しいパッケージを足すには、手動でwhlを拾いにいってハッシュを取り直す手間が発生するので、頻繁に依存を足し引きしたいプロジェクトには向きません。

社内で「バージョン固定で、ずっと同じ組み合わせで動かしたい」業務ツールと相性が良い方式です。

ディレクトリ構成

配布時のプロジェクト構成はこうなります。

<配布アーカイブの構成>
  • src/
    • アプリ本体
  • requirements.txt
    • 依存パッケージとSHA256ハッシュ
  • wheels/
    • .whlファイルを全て同梱
  • README.md
    • インストール手順

PyPIから落とした.whlをそのままwheels/に入れて、一緒にZIPで渡すイメージです。

手順1:パッケージバージョンを固定する

まず依存パッケージのバージョンを== で厳密に固定します。

>=~= は使いません。マイナーバージョンが変わるだけで挙動が変わる可能性があり、オフライン検証の意味が薄れるためです。

requirements.txt(バージョン固定のみ)nonum

1
pymupdf==1.27.2.2

このあと、このバージョンにハッシュを付けていきます。

具体例で使うライブラリ

今回はPDFを画像化するツールで使っているPyMuPDF 1.27.2.2を例にします。

MuPDF本体はwheelに同梱されるので追加のDLLは不要で、Windows向けに配るときも楽な構成です。

PyMuPDFはAGPL / Artifex商用のデュアルライセンスです。
社外配布やSaaS化する場合はライセンス再評価が必要なので、別記事もあわせて確認してください。

手順2:wheelファイルをダウンロードする

インターネット接続のある端末で、PyPIから.whlを直接ダウンロードします。

wheelダウンロード

1
python -m pip download pymupdf==1.27.2.2 -d wheels --only-binary=:all:
<オプションの意味>
  • -d wheels : ダウンロード先ディレクトリ
  • --only-binary=:all: : ソース配布(.tar.gz)を禁止、wheelのみ取得

これでwheels/pymupdf-1.27.2.2-cp39-abi3-win_amd64.whl(Windows 64bit版の場合)が取得できます。

またはgithubのreleasesからダウンロードすることも可能です。

手順3:SHA256ハッシュを取得する

次に、ダウンロードした.whlSHA256ハッシュを取得します。

PowerShellならGet-FileHashコマンドで取得可能です(^^/

SHA256取得

1
Get-FileHash wheels\pymupdf-1.27.2.2-cp39-abi3-win_amd64.whl -Algorithm SHA256

出力のHash欄に64文字の16進数が出てきます。これを後でrequirements.txtに貼り付けます。

その後取得したハッシュがPyPI側に記載された正規の値と一致するかを手動で照合してください。

照合用の公式ハッシュは、以下のいずれかの方法で取得できます。

方法A:PyPIのDownload filesページ(GUI)

PyPIのパッケージページから確認する方法です。

    1. https://pypi.org/project/PyMuPDF/ を開く
    1. 左サイドバー「Download files」タブをクリック
    1. 各 .whl ファイル名の下にある「view hashes」リンクをクリック
    1. ダイアログに SHA256 / MD5 / BLAKE2 が表示される
UIリニューアルでリンク名が変わる可能性があります。
「view hashes」が見当たらない場合は「view details」や「ℹ️」アイコンを探してみてください。

方法B:PyPI JSON API(確実)

PyPIはパッケージメタデータをJSONで公開しているので、ブラウザでURLを叩けば全wheelのハッシュが一覧で取れます
GUIが見つからない・バージョンが古い場合はこちらの方が確実です。

アクセスURL

1
https://pypi.org/pypi/PyMuPDF/1.27.2.2/json

ブラウザで開いたら Ctrl + Fsha256 を検索すると、各wheelのハッシュが見つかります。
JSON構造はこうなっています。

JSONレスポンス抜粋

1
2
3
4
5
6
7
8
9
10
11
12
{
"urls": [
{
"filename": "pymupdf-1.27.2.2-cp39-abi3-win_amd64.whl",
"digests": {
"sha256": "ここに公式ハッシュが入っている",
"md5": "...",
"blake2b_256": "..."
}
}
]
}

filename が自分のダウンロードした .whl と一致する要素の digests.sha256 が照合対象です。

方法C:コマンドで一発取得

PowerShellから直接JSONを取得して、自分のwhlのハッシュだけ抜き出すこともできます。

いきなり特定ファイル名で絞り込むと、ファイル名が1文字違っただけで何も出力されません
まずは全wheelのファイル名とハッシュを一覧表示して、自分のwhlと同じ行を探す手順が安全です。

全wheelの一覧表示

1
2
$json = Invoke-RestMethod "https://pypi.org/pypi/PyMuPDF/1.27.2.2/json"
$json.urls | Select-Object filename, @{n="sha256"; e={$_.digests.sha256}} | Format-Table -Wrap

こうするとこんな一覧が出ます。

出力イメージ

1
2
3
4
5
6
7
filename                                              sha256
-------- ------
pymupdf-1.27.2.2-cp39-abi3-macosx_10_9_x86_64.whl abcd1234...
pymupdf-1.27.2.2-cp39-abi3-macosx_11_0_arm64.whl efgh5678...
pymupdf-1.27.2.2-cp39-abi3-manylinux_2_28_x86_64.whl ijkl9012...
pymupdf-1.27.2.2-cp39-abi3-win_amd64.whl mnop3456...
pymupdf-1.27.2.2.tar.gz qrst7890...

自分がダウンロードした .whl と同じファイル名の行のsha256を、手順3で取得した Get-FileHash の結果と比較してください。

ここでハッシュが食い違った場合、
ダウンロード経路でファイルが差し替えられた可能性があります。
そのwheelは破棄して、別経路で取り直してください。

手順4:requirements.txtにハッシュを書き込む

取得したハッシュを--hash=sha256:<値>の形式でrequirements.txtに追記します。

requirements.txt(ハッシュピン版)

1
2
pymupdf==1.27.2.2 \
--hash=sha256:取得したハッシュ値をここに64文字で貼り付ける
<ポイント>
  • 行末の \ で改行継続
  • --hash=sha256: の直後にスペースなしでハッシュ値
  • 複数プラットフォーム対応なら、同じパッケージ行に --hash= を複数重ねる

手順5:オフラインインストールを実行する

配布先端末では、--no-index--find-links--require-hashes を組み合わせてpip installします。

オフラインインストール

1
2
3
4
python -m pip install -r requirements.txt ^
--no-index ^
--find-links wheels ^
--require-hashes
<それぞれの役割>
  • --no-index : PyPIを見に行かず、ローカルだけで完結
  • --find-links wheels : 指定フォルダをパッケージリポジトリ扱い
  • --require-hashes : requirements.txt内のハッシュ必須モード

--require-hashesが今回の要です。
これを付けるとrequirements.txt内の全パッケージにハッシュ指定が必須になり、1行でもハッシュ未指定があればエラーで止まります

ハッシュが一致しなければ当然インストールも失敗するので、結果的に「ローカルのwheelフォルダが正規の物か」が毎回自動で検証される形になります。

失敗する例

ハッシュを変更して再実行すると、こんなエラーが出ます。

ハッシュ不一致エラー

1
2
3
4
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.
pymupdf==1.27.2.2 from file:///.../wheels/pymupdf-1.27.2.2-cp39-abi3-win_amd64.whl:
Expected sha256 正しいハッシュ
Got 実際のハッシュ

これが出た時点でファイルが改ざんされているか、記載ミスのどちらかなので、インストール作業をストップして原因を調べる運用にすると安全です。

運用フロー(依存パッケージを更新するとき)

バージョンアップやセキュリティパッチ適用の際は、次の順で進めます。

更新フロー

1
2
3
4
5
6
7
1. 外部接続可能な端末で新バージョンを pip download
2. 新しい .whl の SHA256 を Get-FileHash で取得
3. PyPI 側の公式ハッシュと目視照合
4. requirements.txt のバージョンとハッシュを書き換え
5. 旧 .whl を wheels/ から削除、新 .whl を配置
6. 社内検証環境で pip install --require-hashes が成功することを確認
7. 本番端末に配布

この流れを手順として README に明文化しておくと、担当者が変わっても供給チェーン管理のレベルを維持できます。

参考:なぜ –require-hashes を付けるのか

ハッシュ指定はpip installのデフォルトでは任意です。

requirements.txtにハッシュが書いてあっても、--require-hashesなしならpipは「ハッシュ指定が無いパッケージは普通にネット経由で取りに行く」挙動をします。

つまりpipはrequirements.txtを「ハッシュ付きの行だけ厳密に、そうでない行は普通に」と解釈してしまうので、1行でも書き漏らすとサプライチェーン検知の網に穴が空きます。

--require-hashesを付けておけば「全行ハッシュ必須、無ければエラーで停止」になり、抜け漏れを自動で検出できるようになります。

締め

今回はハッシュピン+wheelsフォルダでPythonツールをオフライン配布する方法を紹介しました。

ポイントをおさらいすると以下のとおりです。

<まとめ>
  • バージョンは == で厳密固定
  • wheelはPyPIから事前ダウンロードして同梱
  • SHA256ハッシュは公式値と手動照合
  • pip install --no-index --find-links wheels --require-hashes でオフライン化
  • 更新フローをREADMEに明記して属人化を避ける

情シスや法務が厳しめの組織なら、「このツールが何をダウンロードするか説明できない」と配布許可が下りないケースも多いです。

この構成にしておけばrequirements.txtとwheelsフォルダだけ監査してもらえばOKな状態になるので、社内配布のハードルが一気に下がります(^^b

業務用のPythonツールを作る人は、最初からこの形式で書いておくのがオススメです。

以上となります。
厳しい制限の中でもエンジニアとして最大効率を目指し頑張りましょう!
それではお疲れさまでした!