PyInstallerで –onefile オプションを使ってツールを配布する際、プログラムの中で書き出したバッチファイルがうまく動かなくてハマったことはありませんか?
スクリプトとして実行している時と、exe化した後では、ファイルの展開先や「今どこにいるか(カレントディレクトリ)」の扱いが全然違います。この違いを理解してパスを正しく指定しないと、ファイルが見つからずにエラーになってしまいます。
–onefileオプション
PyInstallerの--onefileオプションは、Pythonスクリプト、依存ライブラリ、Pythonインタープリタを1つの独立したEXEファイルにまとめる機能です。配布が容易になる一方、ファイルサイズが大きくなり、初回起動が少し遅くなるデメリットもあります。
pyinstaller --onefile main.pyなぜexe化するとパスが狂うのか
–onefile オプションを使うと、実行時に中身のデータが一時フォルダ(_MEIPASS)に解凍されます。しかし、バッチファイルなど「実行ファイル(.exe)と同じ場所に置きたいファイル」を扱うときは注意が必要です。
- os.getcwd():ユーザーがどのフォルダからコマンドを叩いたかに依存するので不安定。
- __file__:exe化すると一時フォルダの中身を指してしまう。
- sys.executable:実際に動いている .exe ファイルそのもののパスを返してくれる。
私の経験上、バッチファイル作成で失敗する原因のほとんどは、この sys.executable を使わずに相対パスでファイルを操作しようとしているケースです。
実際の運用現場では、ユーザーがデスクトップから実行したり、別のツールから呼び出したりするため、カレントディレクトリに頼るのは非常に危険です。
スクリプト・exe両対応のパス解決コード
どんな環境でも確実に .exe と同じ階層を特定するための、鉄板の関数を紹介します。
import os
import sys
import subprocess
def get_base_dir():
# PyInstallerで固められている(frozen)かどうかを判定
if getattr(sys, 'frozen', False):
# exeファイルの場所を取得
return os.path.dirname(sys.executable)
# 通常のスクリプト実行時はファイルの場所を返す
return os.path.dirname(os.path.abspath(__file__))
def run_batch_safely():
base_dir = get_base_dir()
batch_path = os.path.join(base_dir, "update_task.bat")
# バッチファイルを日本語環境向け(cp932)で書き出し
with open(batch_path, "w", encoding="cp932") as f:
f.write("@echo off\n")
f.write(f"echo 実行ファイルと同じ場所で動いています: {base_dir}\n")
f.write("pause")
# Pythonが終わった後も動き続けるように独立したプロセスで起動
subprocess.Popen(
["cmd.exe", "/c", batch_path],
cwd=base_dir,
shell=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
)現場でハマらないための注意点
- 書き込み権限:
C:\Program Filesの中などは管理者権限がないとファイルが作れません。エラーが出る場合は、ユーザーの AppData フォルダなどに保存先を逃がす検討が必要です。 - 作業ディレクトリ(cwd)の指定:subprocessでバッチを呼ぶときは、必ず cwd=base_dir を指定しましょう。これを忘れると、バッチ内の相対パスが死にます。
- ゾンビプロセス対策:
DETACHED_PROCESSを使うことで、Pythonを閉じてもバッチだけが生き残って処理を完遂できるようになります。
結論として、PyInstallerで外部ファイルを扱うなら sys.frozen による分岐は必須です。このパターンをテンプレート化しておくだけで、配布後の「自分の環境では動いたのに」というトラブルを劇的に減らすことができますよ。

