File size: 7,402 Bytes
e55536e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
"""
Gradio app: 入力されたpip依存関係(requirements.txtスタイル or パッケージリスト)を受け取り、
指定したディレクトリに`pip install --target`でパッケージ(依存関係含む)をインストールし、
その内容をZIPにまとめてダウンロードできるようにします。

使い方:
  - requirements テキストボックスにパッケージを1行ごとに書くか、requirements.txtの中身を貼る
  - "依存関係を含める" のチェックを入れると pip が依存も自動でインストールします(デフォルト: True)
  - "実行" を押すと処理が行われ、ZIPファイルとログが出力されます

注意事項:
  - 任意のパッケージのインストールは任意のコード実行を伴うため、悪意あるパッケージをインストールすると
    サーバーに危害が及ぶ可能性があります。公開サーバーでの運用は十分注意してください。
  - ネイティブ拡張(C拡張)を含むパッケージはビルドが必要で、ビルド環境が整っていないと失敗します。
  - 大きなパッケージや多数の依存関係は時間がかかる場合があります。

このファイルは1つのPythonスクリプトとして実行できます (Python 3.8+ 推奨)。
前提: gradio がインストールされていること。

例:
  python gradio_pip_installer_zipper.py

"""

import gradio as gr
import tempfile
import shutil
import subprocess
import sys
import os
import zipfile
from pathlib import Path
import time


def install_and_zip(requirements_text: str, include_deps: bool = True, zip_name_prefix: str = "packages"):
    """requirements_text: requirements.txt の中身またはパッケージを改行区切りで受け取る。
    include_deps: True のとき pip は依存も解決してインストールする。
    戻り値: (zip_path, log_text)
    """
    start_ts = time.time()
    log_lines = []

    if not requirements_text or not requirements_text.strip():
        return None, "エラー: requirements の入力が空です。"

    # 一時ディレクトリを作成
    work_dir = Path(tempfile.mkdtemp(prefix="pip_zipper_"))
    target_dir = work_dir / "site-packages"
    target_dir.mkdir(parents=True, exist_ok=True)
    req_file = work_dir / "requirements.txt"

    # クリーンな requirements.txt を作る(空行とコメントを除去)
    pkgs = []
    for line in requirements_text.splitlines():
        s = line.split("#", 1)[0].strip()
        if s:
            pkgs.append(s)
    if not pkgs:
        shutil.rmtree(work_dir, ignore_errors=True)
        return None, "エラー: 有効なパッケージ指定が見つかりませんでした。"

    req_file.write_text("\n".join(pkgs), encoding="utf-8")
    log_lines.append(f"作業ディレクトリ: {work_dir}")
    log_lines.append(f"インストール先ディレクトリ: {target_dir}")
    log_lines.append(f"requirements:\n" + "\n".join(pkgs))

    # pip コマンドの構築
    pip_cmd = [sys.executable, "-m", "pip", "install", "-r", str(req_file), "--target", str(target_dir)]
    if not include_deps:
        pip_cmd.append("--no-deps")

    # 環境によっては pip が古くて失敗することがあるため最小限のアップグレードを試みる(任意)
    try:
        log_lines.append("pip のバージョン確認...")
        out = subprocess.check_output([sys.executable, "-m", "pip", "--version"], stderr=subprocess.STDOUT, text=True)
        log_lines.append(out.strip())
    except subprocess.CalledProcessError as e:
        log_lines.append("pip バージョン確認に失敗しました: " + str(e))

    # 実行
    log_lines.append("\n=== pip install を実行します ===")
    log_lines.append("実行コマンド: " + " ".join(pip_cmd))
    try:
        proc = subprocess.run(pip_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=str(work_dir), timeout=60*30)
        log_lines.append(proc.stdout)
        if proc.returncode != 0:
            log_lines.append(f"pip install は終了コード {proc.returncode} で失敗しました。")
            # ZIP は作らず一時ディレクトリを残してログを返す
            zip_path = None
            total_time = time.time() - start_ts
            log_lines.append(f"経過時間: {total_time:.1f}s")
            return zip_path, "\n".join(log_lines)
    except subprocess.TimeoutExpired:
        log_lines.append("pip install がタイムアウトしました。")
        shutil.rmtree(work_dir, ignore_errors=True)
        return None, "\n".join(log_lines)
    except Exception as e:
        log_lines.append("予期せぬエラー: " + str(e))
        shutil.rmtree(work_dir, ignore_errors=True)
        return None, "\n".join(log_lines)

    # 成功したら ZIP を作る
    zip_filename = f"{zip_name_prefix}_{int(time.time())}.zip"
    zip_path = work_dir / zip_filename
    log_lines.append("\n=== ZIP 生成 ===")
    try:
        with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
            # target_dir 以下をすべて追加
            for root, dirs, files in os.walk(target_dir):
                for file in files:
                    fullpath = Path(root) / file
                    # ZIP 内のパスは target_dir の相対パスにする
                    relpath = fullpath.relative_to(work_dir)
                    zf.write(fullpath, arcname=str(relpath))
        log_lines.append(f"ZIP 作成完了: {zip_path} (サイズ: {zip_path.stat().st_size} bytes)")
    except Exception as e:
        log_lines.append("ZIP 作成中にエラー: " + str(e))
        shutil.rmtree(work_dir, ignore_errors=True)
        return None, "\n".join(log_lines)

    total_time = time.time() - start_ts
    log_lines.append(f"経過時間: {total_time:.1f}s")

    # 注意: 一時ディレクトリは残しておく(ユーザーがZIPをダウンロードできるように)
    # 運用サーバーであれば定期クリーンアップを別途実装してください。

    return str(zip_path), "\n".join(log_lines)


# Gradio UI
with gr.Blocks() as demo:
    gr.Markdown("""# Pip 依存関係インストール → ZIP 生成

    下に requirements.txt の中身、または1行ごとのパッケージ名を貼り付けてください。
    """)

    with gr.Row():
        req_input = gr.Textbox(label="requirements またはパッケージ一覧 (1行に1件)", lines=10, placeholder="例:\nrequests==2.31.0\nnumpy>=1.25\n# コメントは無視されます")

    with gr.Row():
        include_deps = gr.Checkbox(value=True, label="依存関係を含める (通常はオン) ")
        prefix = gr.Textbox(value="packages", label="生成ZIPのプレフィックス (任意)")

    run_btn = gr.Button("実行")

    output_file = gr.File(label="ダウンロード ZIP")
    log_box = gr.Textbox(label="実行ログ", lines=15)

    def _run(req_text, incl_deps, pref):
        zip_path, log = install_and_zip(req_text, include_deps=incl_deps, zip_name_prefix=(pref or "packages"))
        if zip_path:
            return zip_path, log
        else:
            return None, log

    run_btn.click(_run, inputs=[req_input, include_deps, prefix], outputs=[output_file, log_box])


if __name__ == "__main__":
    demo.launch()