Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/de3be70daf4db4d8015fcd2f3b2d0e87 to your computer and use it in GitHub Desktop.

Select an option

Save aont/de3be70daf4db4d8015fcd2f3b2d0e87 to your computer and use it in GitHub Desktop.

Creating the same Windows .exe CLI launchers that pip makes — using distlib

English

When you pip install a package that exposes console entry points on Windows, pip (via its bundled distlib) produces small native .exe launcher files in the Scripts (or Scripts\) directory. Those .exe files are merely launchers that invoke the target Python interpreter and run your package’s entry point. You can reproduce that same functionality directly from Python by calling distlib.scripts.ScriptMaker — in other words, the .exe-creation logic is available as a callable library.

Below is a compact guide and example code you can drop into a script to generate the same Windows EXE wrappers pip would create.


Quick summary

  • The launcher creation logic lives in distlib (and pip vendors a copy as pip._vendor.distlib).

  • You can call distlib.scripts.ScriptMaker directly to create both POSIX scripts (with shebangs) and Windows .exe launchers.

  • Typical workflow:

    1. pip install distlib (or use pip._vendor.distlib — not recommended for production).
    2. Use ScriptMaker with target_dir set to your virtualenv’s Scripts folder (or any folder you prefer).
    3. Call make() (or make_multiple()) with either a path to a script file or an entry-spec string like cmdname=module:callable.
  • Caveat: the generated .exe usually embeds an absolute path to the Python executable used when creating it — so it’s not automatically portable across different environments.


Install distlib

pip install distlib

(Or if you intentionally want the exact implementation pip uses, you can import pip._vendor.distlib, but that is using an internal vendored copy and may change without notice.)


Example: create Windows .exe launchers

# create_launchers.py
from distlib.scripts import ScriptMaker
import os

# Example settings — adjust these paths to your environment
venv_scripts = r"C:\path\to\out"     # where to put the generated launchers
source_dir = r"C:\path\to\project"                 # where source files live (optional)

maker = ScriptMaker(source_dir=source_dir, target_dir=venv_scripts)

# Optional: explicitly set the python executable the launcher should use.
# If omitted, distlib will detect the interpreter running this script.
maker.executable = r"C:\path\to\your\venv\Scripts\python.exe"

# 1) Make a launcher that points to an entry-point style spec:
#    format: "cmdname=package.module:callable"
maker.make('mytool=yourpkg.cli:main')   # produces mytool.exe and mytool-script.py (on Windows)

# 2) Make a launcher from an actual script file (copy + wrap)
#    If you have a script file at source_dir\scripts\run_tool.py:
maker.make(os.path.join('scripts', 'run_tool.py'))

# 3) Make multiple at once:
maker.make_multiple([
    'tool1=yourpkg.tool1:main',
    'tool2=yourpkg.tool2:run'
])

Run this script (with the interpreter you want baked into the launchers). After running, check the venv_scripts folder: you should see mytool.exe (Windows native launcher) and a small Python wrapper script (used by platforms that prefer scripts).


What ScriptMaker accepts

  • A path to an actual Python script file (it will copy that and create the appropriate wrappers).
  • An entry specification string of the form name=module:callable (same format as console_scripts entry points). ScriptMaker will generate a small loader that imports that callable and executes it when invoked.

Important caveats & behaviors

  1. Interpreter path is embedded Generated .exe launchers commonly contain the absolute path to the Python interpreter they were created with. If you move the .exe to another machine or virtualenv, it may fail because it looks for the original python path.

  2. pip._vendor.distlib vs distlib

    • pip vendors distlib internally and uses it to create launchers during installation. You can import pip._vendor.distlib.scripts.ScriptMaker, but that relies on pip’s private API. Prefer installing the public distlib package where possible.
  3. Permissions / UAC / Antivirus Writing .exe files into system or protected locations may trigger UAC or antivirus heuristics. Write to a virtualenv Scripts dir or a user-writable directory.

  4. Windows vs POSIX differences On POSIX systems distlib writes scripts with shebang lines; on Windows it writes a native .exe launcher (plus a .py fallback wrapper). The same ScriptMaker API handles both.

  5. Not a bundler These .exe files are simple launchers. They do not bundle Python or your package’s dependencies. If you need a single-file executable containing the interpreter and all code, consider PyInstaller / cx_Freeze / Nuitka instead.


Alternatives / related tools

  • pip / pipx: pip uses distlib under the hood to produce launchers. pipx is useful if you specifically want to install isolated CLI tools into per-tool environments and get launchers placed on PATH.
  • setuptools / entry points: When packaging, declare console_scripts in setup.cfg / setup.py / pyproject.toml. pip will create the launchers for you on install.
  • PyInstaller / cx_Freeze / Nuitka: use these if you want a self-contained executable (these bundle the interpreter & dependencies; behavior and use-case differ from distlib-style launchers).

Recommendation

  • If your goal is to reproduce exactly what pip does at install-time (i.e., create small native launchers that call the interpreter and your entry point), use distlib.scripts.ScriptMaker from the official distlib package.
  • If you want to automate creation of launchers as part of a packaging or deployment script, call ScriptMaker from within the same interpreter you want the launcher to target and write into that environment’s Scripts directory.
  • If you need portability across different systems or environments, consider alternatives that embed the interpreter (PyInstaller) or create a shift in how you distribute (containerize, shipping a wheel and using pip on the target).

Closing example — one-liner usage

python -c "from distlib.scripts import ScriptMaker; ScriptMaker(target_dir='C:\\venv\\Scripts').make('hello=hello_pkg.cli:main')"

This single command will create hello.exe in C:\venv\Scripts that launches hello_pkg.cli:main using the Python interpreter running the -c command.

日本語

pip が作成する Windows 用 .exe CLI ランチャーを distlib で再現する方法

Windows でコンソールエントリポイントを公開するパッケージを pip install すると、pip(内部で同梱している distlib を使用)が Scripts(または Scripts\)ディレクトリに小さなネイティブ .exe ランチャーファイルを生成します。 これらの .exe は単なるランチャーであり、対象の Python インタプリタを起動して、パッケージのエントリポイントを実行するだけのものです。

この仕組みは、Python から distlib.scripts.ScriptMaker を呼び出すことで同じ機能を直接再現できます。つまり、.exe を作成するロジックはライブラリとして利用可能です。

以下は、pip が作成するのと同等の Windows 用 EXE ラッパーを生成するための簡潔なガイドとサンプルコードです。


クイックサマリー

  • ランチャー作成ロジックは distlib にあります(pippip._vendor.distlib として同梱)。

  • distlib.scripts.ScriptMaker を直接呼び出すことで、POSIX 用スクリプト(shebang 付き)と Windows 用 .exe ランチャーの両方を生成できます。

  • 一般的なワークフロー:

    1. pip install distlib を実行(または非推奨だが pip._vendor.distlib を使用)。
    2. target_dir に仮想環境の Scripts フォルダ(または任意の出力先)を指定して ScriptMaker を作成。
    3. スクリプトのパス、または cmdname=module:callable 形式のエントリ指定を使って make()(または make_multiple())を呼び出す。
  • 注意点:生成される .exe には、作成時に使用した Python 実行ファイルの絶対パスが埋め込まれるのが一般的です。そのため、別環境にそのまま移動しても動作するとは限りません。


distlib のインストール

pip install distlib

pip が内部で使っている実装と完全に同じものを使いたい場合は pip._vendor.distlib を import することもできますが、これは内部実装に依存するため、本番用途では推奨されません。)


例:Windows 用 .exe ランチャーを作成する

# create_launchers.py
from distlib.scripts import ScriptMaker
import os

# 例の設定 — 自分の環境に合わせてパスを調整してください
venv_scripts = r"C:\path\to\out"     # 生成したランチャーの出力先
source_dir = r"C:\path\to\project"  # ソースファイルの場所(任意)

maker = ScriptMaker(source_dir=source_dir, target_dir=venv_scripts)

# オプション:ランチャーが使用する Python 実行ファイルを明示的に指定
# 省略した場合、distlib はこのスクリプトを実行している Python を検出します
maker.executable = r"C:\path\to\your\venv\Scripts\python.exe"

# 1) エントリポイント形式の指定からランチャーを作成
#    形式: "cmdname=package.module:callable"
maker.make('mytool=yourpkg.cli:main')   # Windows では mytool.exe と mytool-script.py が生成される

# 2) 実際のスクリプトファイルからランチャーを作成(コピー+ラップ)
#    source_dir\scripts\run_tool.py が存在する場合:
maker.make(os.path.join('scripts', 'run_tool.py'))

# 3) 複数を一括で作成
maker.make_multiple([
    'tool1=yourpkg.tool1:main',
    'tool2=yourpkg.tool2:run'
])

このスクリプトを、ランチャーに埋め込みたい Python インタプリタで実行してください。 実行後、venv_scripts フォルダを確認すると、mytool.exe(Windows ネイティブランチャー)と小さな Python ラッパースクリプトが生成されているはずです。


ScriptMaker が受け付けるもの

  • 実在する Python スクリプトファイルへのパス(コピーして適切なラッパーを作成)。
  • name=module:callable 形式のエントリ指定文字列console_scripts エントリポイントと同じ形式)。 この場合、呼び出されたときにその callable を import して実行する小さなローダーが生成されます。

重要な注意点と挙動

  1. インタプリタのパスが埋め込まれる 生成された .exe には、作成時に使用した Python インタプリタの絶対パスが含まれることが一般的です。別のマシンや仮想環境に移動すると、そのパスが見つからず失敗する可能性があります。

  2. pip._vendor.distlibdistlib の違い

    • pip は内部で distlib を同梱し、インストール時のランチャー生成に使用しています。
    • pip._vendor.distlib.scripts.ScriptMaker を import することも可能ですが、これは pip の非公開 API に依存します。可能であれば、公開パッケージである distlib を使用してください。
  3. 権限 / UAC / アンチウイルス システムや保護された場所に .exe を書き込むと、UAC やアンチウイルスの検知が発生することがあります。仮想環境の Scripts ディレクトリや、ユーザー書き込み可能な場所を使用してください。

  4. Windows と POSIX の違い POSIX 系では shebang 付きスクリプトが生成され、Windows ではネイティブ .exe ランチャー(+ .py のフォールバックラッパー)が生成されます。どちらも同じ ScriptMaker API で扱えます。

  5. バンドラーではない これらの .exe は単なるランチャーであり、Python 本体や依存関係を同梱しません。 インタプリタや依存関係を含めた単一実行ファイルが必要な場合は、PyInstaller / cx_Freeze / Nuitka などを検討してください。


代替手段・関連ツール

  • pip / pipx: pip は内部で distlib を使ってランチャーを生成します。pipx は CLI ツールを個別の仮想環境に隔離してインストールし、PATH 上にランチャーを配置したい場合に便利です。
  • setuptools / entry points: パッケージング時に setup.cfg / setup.py / pyproject.tomlconsole_scripts を定義すれば、pip install 時に自動的にランチャーが作成されます。
  • PyInstaller / cx_Freeze / Nuitka: Python 本体と依存関係を含む自己完結型の実行ファイルを作りたい場合に使用します(distlib 方式とは用途が異なります)。

推奨事項

  • pip がインストール時に行うのと同じこと(インタプリタとエントリポイントを呼び出す小さなネイティブランチャーの作成)を再現したい場合は、公式の distlib パッケージに含まれる distlib.scripts.ScriptMaker を使用してください。
  • パッケージングやデプロイ用スクリプトの一部としてランチャー生成を自動化したい場合は、対象とするインタプリタで ScriptMaker を呼び出し、その環境の Scripts ディレクトリに出力してください。
  • 環境間での可搬性が必要な場合は、インタプリタを同梱する手法(PyInstaller など)や、配布方法の見直し(wheel 配布+ pip 利用、コンテナ化など)を検討してください。

仕上げの例 — ワンライナー

python -c "from distlib.scripts import ScriptMaker; ScriptMaker(target_dir='C:\\venv\\Scripts').make('hello=hello_pkg.cli:main')"

この 1 行コマンドで、C:\venv\Scriptshello.exe が作成され、-c を実行した Python インタプリタを使って hello_pkg.cli:main が起動されるようになります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment