2025年11月4日火曜日

配布用イメージファイル作成

 参照: https://qiita.com/noguhiro2002/items/60103e6bd21134cad543



Raspberry PiのMicro SDカードから配布用imageファイルを作る with Ubuntu 18.04 LTS

最終更新日 投稿日 2020年02月24日

はじめに

Raspberry Piでデバイスを開発した!MicroSDの中身をImage ファイルに書き出して配布したい!でもやり方がわからない!そんなお方向けのマニアチックなQiitaです。

なお、MicroSDのバックアップおよびクローン作成にも最適です。

注意:まずは本内容が動くかどうかテスト用のMicroSDでテストしてから、目的のMicroSDで試してください。本メソッドを用いて生じた損害などについて筆者は一切の責任を持ちません。

Material

  • 配布用imageを作りたいRaspberry PiのMicroSD (今回はRaspberry Pi3B+のRaspbian Buster, 32GB)
  • Ubuntu 18.04 LTS (作業用PC)
  • GParted (Linux用パーテション編集ソフト.今回の主役)

Method

  1. GPartedをUbuntuへインストール

    $ sudo apt update
    $ sudo apt upgrade
    $ sudo apt install gparted
    
  2. Raspberry Piから取り出したMicroSDを、Ubuntuが入っているPCへ入れる。

  3. どのUSBデバイスとして認識されているのか、$ sudo fdisk -lで確認する。ここからの操作を間違えるとデータ全損失につながる可能性があるので注意!

    $ sudo fdisk -l
    ...
    Disk /dev/sdc: 29.2 GiB, 31312576512 bytes, 61157376 sectors
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0x000816c0
    ...
    # 今回は/dev/sdcでした。
    
  4. ddコマンドを用いてブロック単位でバックアップを行います。これでパテーション/ファイルシステムによらず、MicroSDの中身すべてのバックアップが可能になります。ddコマンドを使用する際は、if(input file)とof(output file)の指定に注意してください!間違えると一瞬でデータが飛びます!(参考: Arch linux, ディスクのクローン)。

    $ sudo dd bs=1M if='/dev/sdx' of='/path/to/image/file/hoge.img' status=progress
    # ifはMicroSDのデバイス場所今回は'/dev/sdc'
    # ofはimageファイルのパス。ファイル名は自由。拡張子は'.img'。
    # 終了までしばらく待ちます。
    31301042176 bytes (31 GB, 29 GiB) copied, 
    1469 s, 21.3 MB/s
    29862+0 records in
    29862+0 records out
    31312576512 bytes (31 GB, 29 GiB) copied, 
    1470.23 s, 21.3 MB/s
    
  5. 吸い出したimgファイルを、/dev/loop(ループバックデバイス)にマウントし、GPartedで編集します。

    $ sudo losetup -f    # /dev/loop (ループバックデバイス)の空きを探します。
    /dev/loop18    # 今回は /dev/loop18 でした。
    $ sudo losetup /dev/loop18 '/path/to/image/file/hoge.img'  # 空いていた/dev/loop (今回は/dev/loop18) に dd で吸い取った img ファイルをマウントします。
    $ sudo partprobe /dev/loop18  # パーティションテーブルを変更したことをOSに知らせます。
    $ sudo gparted /dev/loop18  # GPartedを起動します。
    
  6. GPartedでパーティションの編集を行います。

  7. RaspbianをNOOBSでインストールすると、このように複雑なパーティションに分かれています。Raspbianを直にインストールしていればもっと単純なはずです (参考:バックアップしたイメージファイルを縮小する)。
    Screenshot from 2020-02-23 21-37-25.png

  8. パーティションが可視化されたバーの白い部分が空きスペース (データは書き込まれていないが容量は食っている部分) なので、ここを減らします。一番下の/dev/loop18p7をクリックし、その上で右クリック --> Resize/Moveを選択し、Resize/Move windowを開きます。
    Screenshot from 2020-02-23 21-51-05.png
    Screenshot from 2020-02-23 21-52-28.png

  9. スケールバーをドラッグし、白いフリー部分を黄色のデータが存在する領域付近まで持っていきます。なお、このときギリギリまでもってくると失敗します(Trouble-Shooting参照)。その後、Resize/Moveをクリックし、パーティション変更情報を保存します(まだパーテションの変更処理は行われていません)。
    Screenshot from 2020-02-23 21-52-45.png
    Screenshot from 2020-02-23 21-52-56.png

  10. /dev/loop18p7の領域は変更されましたが、親である/dev/loop18p2はまだ変更されていません。/dev/loop18p2を選択し、その上で右クリック --> Resize/Moveを選択し、Resize/Move windowを開きます。先ほどと同様、スケールバーをドラッグし、unallocated部分(灰色)を減らしますが、今回はピッチリ黄色の/dev/loop18p7へ寄せます。その後、Resize/Moveをクリックし、パーティション変更情報を保存します(まだパーテションの変更処理は行われていません)。
    Screenshot from 2020-02-23 21-53-06.png
    Screenshot from 2020-02-23 21-53-02.png

  11. パーティション変更情報がこの状態になったことを確認し、GParted window上部中央やや左の緑の✔マークをクリックし、プロセスを開始します。
    Screenshot from 2020-02-23 21-53-12.png

  12. 警告画面が表示されますが、Applyをクリックしてパーティションの編集処理をスタートします。
    Screenshot from 2020-02-23 21-54-05.png
    Screenshot from 2020-02-23 21-56-27.png

  13. 処理が終了したのを確認後、Closeをクリックして画面を閉じ、編集後のパーティションの"unallocated"が/dev/loop18p2から独立し、分かれて存在していることを確認します。
    Screenshot from 2020-02-23 21-57-43.png
    Screenshot from 2020-02-23 21-57-48.png

  14. GPartedを終了します。

  15. GPartedで編集したパーティションの"unallocated"部分をfdiskを用いて削除します。

    $ sudo losetup -d /dev/loop18  # /dev/loop18をunmountします。
    $ sudo fdisk -l '/path/to/image/file/hoge.img'  # fdisk -l を用いてGPatedで編集したパーティションを見ます。
    
    Disk /path/to/image/file/hoge.img: 29.2 GiB, 31312576512 bytes, 61157376 sectors
    Units: sectors of 1 * 512 = 512 bytes
    

Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x000816c0

Device                     Boot    Start    End     Sectors  Size Id Type

/path/to/image/file/hoge.img1 8192 4994140 4985949 2.4G e W95 FAT16 (LBA)
/path/to/image/file/hoge.img2 4994141 31594495 26600355 12.7G 5 Extended
/path/to/image/file/hoge.img5 4997120 5062653 65534 32M 83 Linux
/path/to/image/file/hoge.img6 5062656 5586941 524286 256M c W95 FAT32 (LBA)
/path/to/image/file/hoge.img7 5586944 31594495 26007552 12.4G 83 Linux
```

  1. truncateコマンドでhoge.imgのフリースペースを削除し、最終的な配布用imgファイルを作ります。このとき、セクターの最大値(今回は"31594495")に1を足し、それを512倍したもののサイズを指定します。

    $ sudo truncate --size=$[(31594495+1)*512] '/path/to/image/file/hoge.img'
    
  2. 無事にファイルサイズが小さくなった配布用のimageファイルが作成されました! sudo chownでパーミッションを変更し完成です!

    $ ls -lh
    ...
    -rw-r--r-- 1 root     root     25G Feb 23 20:00  hgoe.img
    ...
    $ sudo chown hoge:hoge ./hoge.img
    ...
    -rw-r--r-- 1 hoge     hoge     25G Feb 23 20:00  hgoe.img
    ...
    
  3. おつかれさまでした !!

作成したimgファイルを元にしたクローンの作成

今回作成した img ファイルを元に新たな MicroSD を作成するには、再びddコマンドを使用します。ifとofの指定に気をつけてください!特にofの指定を誤ると他の補助記憶装置の中身が全削除されます!

$ sudo dd bs=1M if='/path/to/image/file/hoge.img' of=/dev/sdx status=progress
# MicroSDのデバイスIDは、`fdisk -l`などで調べられます。

Trouble-Shooting

  1. GPatedで/dev/loop18p7(今回の場合)のパテーションを変更する際、あまりにもギリギリまで攻めすぎると、以下のエラー画面が出ます。この画面が出ましたら、少し余裕を持ってフリースペースを設定してみてください。

  Screenshot from 2020-02-23 21-55-52.png

References

以下のサイトを参考にさせていただきました。大変、ありがとうございました。

  1. Arch linux: GParted
  2. NIJIOTA.NET: バックアップしたイメージファイルを縮小する
  3. Arch linux, ディスクのクローン

2025年10月12日日曜日

DelphiからPythonを call

 


「Pythonコードを呼び出して、引数を渡し、実行する」Delphiコードを 2パターン 用意

  1. 外部プロセスとして python.exe を起動して実行(標準出力を取得)

  2. Python4Delphi(P4D)で CPython を埋め込み、関数呼び出し


1) 外部プロセスとして起動(CreateProcess + パイプで stdout を受け取る)

  • どの Delphi 環境でも動きやすい定番手法

  • 仮想環境(venv)や python.exe のフルパスを指定すれば OK

  • -u でバッファリングを無効化して即時に出力を受け取ります

unit PyRunner; interface uses System.SysUtils, System.Classes, Winapi.Windows; function RunPythonAndCapture( const PythonExe: string; // 例: 'C:\Python312\python.exe' や venv の python.exe パス const ScriptPath: string; // 例: 'C:\work\myscript.py' const Args: array of string; // 例: ['--name', 'Alice', '--n', '3'] const WorkDir: string = '' // 作業ディレクトリ(空なら ScriptPath のディレクトリ) ): string; implementation function QuoteIfNeeded(const S: string): string; begin if (S = '') or (S.IndexOfAny([' ', '"']) >= 0) then Result := '"' + StringReplace(S, '"', '\"', [rfReplaceAll]) + '"' else Result := S; end; function JoinArgs(const Items: array of string): string; var i: Integer; begin Result := ''; for i := 0 to High(Items) do begin if i > 0 then Result := Result + ' '; Result := Result + QuoteIfNeeded(Items[i]); end; end; function RunPythonAndCapture( const PythonExe: string; const ScriptPath: string; const Args: array of string; const WorkDir: string ): string; var SA: SECURITY_ATTRIBUTES; StdOutRd, StdOutWr: THandle; StartInfo: STARTUPINFOW; ProcInfo: PROCESS_INFORMATION; CmdLine: string; Buffer: array[0..8191] of Byte; BytesRead: DWORD; OK: BOOL; OldStdOutMode: Cardinal; CurDir: string; begin Result := ''; // パイプ作成(子プロセスの stdout を親側で読めるように) ZeroMemory(@SA, SizeOf(SA)); SA.nLength := SizeOf(SA); SA.bInheritHandle := TRUE; if not CreatePipe(StdOutRd, StdOutWr, @SA, 0) then raise EOSError.CreateFmt('CreatePipe failed: %d', [GetLastError]); try // 子プロセス側で StdOutWr を継承できるようにする if not SetHandleInformation(StdOutRd, HANDLE_FLAG_INHERIT, 0) then raise EOSError.CreateFmt('SetHandleInformation failed: %d', [GetLastError]); // コマンドライン作成 // 例: "C:\Python312\python.exe" -u "C:\work\myscript.py" --name "Alice" --n 3 CmdLine := QuoteIfNeeded(PythonExe) + ' -u ' + QuoteIfNeeded(ScriptPath); if Length(Args) > 0 then CmdLine := CmdLine + ' ' + JoinArgs(Args); ZeroMemory(@StartInfo, SizeOf(StartInfo)); StartInfo.cb := SizeOf(StartInfo); StartInfo.hStdOutput := StdOutWr; StartInfo.hStdError := StdOutWr; // エラーもまとめて受け取る StartInfo.dwFlags := STARTF_USESTDHANDLES; ZeroMemory(@ProcInfo, SizeOf(ProcInfo)); if WorkDir <> '' then CurDir := WorkDir else CurDir := ExtractFileDir(ScriptPath); // WideString → PWideChar if not CreateProcessW(nil, PWideChar(CmdLine), nil, nil, TRUE, CREATE_NO_WINDOW, nil, PWideChar(CurDir), StartInfo, ProcInfo) then raise EOSError.CreateFmt('CreateProcess failed: %d'#13#10'%s', [GetLastError, CmdLine]); try // 親側は書き込み口を閉じ、読み取りだけにする CloseHandle(StdOutWr); StdOutWr := 0; // 子プロセス標準出力を読み取り while True do begin OK := ReadFile(StdOutRd, Buffer, SizeOf(Buffer), BytesRead, nil); if (not OK) or (BytesRead = 0) then Break; Result := Result + TEncoding.UTF8.GetString(Buffer, 0, BytesRead); end; // 終了待ち WaitForSingleObject(ProcInfo.hProcess, INFINITE); finally if ProcInfo.hThread <> 0 then CloseHandle(ProcInfo.hThread); if ProcInfo.hProcess <> 0 then CloseHandle(ProcInfo.hProcess); end; finally if StdOutWr <> 0 then CloseHandle(StdOutWr); if StdOutRd <> 0 then CloseHandle(StdOutRd); end; end; end.

使い方(呼び出し側)

uses System.SysUtils, PyRunner; procedure TForm1.Button1Click(Sender: TObject); var OutText: string; begin OutText := RunPythonAndCapture( 'C:\Python312\python.exe', 'C:\work\myscript.py', ['--name', '山川', '--n', '2'], // ← 渡したい引数 '' // 作業ディレクトリ(省略可) ); Memo1.Lines.Text := OutText; end;

参考の Python 側(例)

# myscript.py import argparse p = argparse.ArgumentParser() p.add_argument('--name', required=True) p.add_argument('--n', type=int, default=1) a = p.parse_args() for i in range(a.n): print(f"hello {a.name} #{i+1}")

2) Python4Delphi(P4D)で埋め込み実行(関数に引数を渡す)

  • GUIアプリ内で Python 関数を直接呼びたい場合に便利

  • Delphi コンポーネント(TPythonEngine, TPythonGUIInputOutput など)をフォームに配置

  • 以下は ランタイム作成 版(コードだけで完結)。関数に文字列・整数引数を渡して戻り値(文字列)を受け取ります

unit PyEmbed; interface uses System.SysUtils, PythonEngine, VarPyth; function CallPythonFunc( const ModulePath: string; // 例: 'C:\work\mymodule.py' const FuncName: string; // 例: 'greet' const S: string; // 引数1(文字列) const N: Integer // 引数2(整数) ): string; implementation function CallPythonFunc( const ModulePath, FuncName, S: string; const N: Integer): string; var Eng: TPythonEngine; ModObj, FuncObj, RetVal: Variant; Dir, FileName, ModName: string; begin Result := ''; Eng := TPythonEngine.Create(nil); try // 必要なら Eng.DllPath, DllName を指定(例:埋め込む Python の DLL など) // Eng.DllPath := 'C:\Python312\'; // Eng.DllName := 'python312.dll'; Eng.LoadDll; // CPython を初期化 // sys.path にモジュールのディレクトリを追加 Dir := ExtractFileDir(ModulePath); FileName := ExtractFileName(ModulePath); ModName := ChangeFileExt(FileName, ''); // "mymodule.py" -> "mymodule" GetPythonEngine.EvalString(Format('import sys; sys.path.insert(0, r"%s")', [Dir])); // モジュールを import ModObj := ImportModule(ModName); // 関数を取得 FuncObj := ModObj.__getattr__(FuncName); // 呼び出し(引数 S と N を渡す) RetVal := FuncObj(S, N); // 返り値(文字列)を Delphi 文字列へ Result := VarToStr(RetVal); finally Eng.Free; end; end; end.

使い方(呼び出し側)

uses System.SysUtils, PyEmbed; procedure TForm1.Button2Click(Sender: TObject); var R: string; begin R := CallPythonFunc('C:\work\mymodule.py', 'greet', '山川', 3); ShowMessage(R); end;

参考の Python モジュール(mymodule.py)

def greet(name: str, n: int) -> str: return "\n".join([f"hello {name} #{i+1}" for i in range(n)])

✅ 補足

  • P4D を使う場合は、開発環境に Python4Delphi を導入してください(TPythonEngine 等)。

  • 既存の Python 仮想環境を使う場合は、python3x.dll に合うように Eng.DllName, Eng.DllPath を設定します。

  • NumPy など C 拡張を使う場合は、DLL の互換性(bitness / バージョン)に注意してください。


どちらを選ぶ?

  • 手早く実行+出力取得 → 方式1(外部プロセス) が簡単・堅牢

  • アプリ内に組み込み、関数を直接コール → 方式2(P4D) が便利(連続呼び出しや相互作用に強い)

用途や制約(配布形態、Python 環境、依存モジュール)を教えていただければ、最適化(例:環境変数設定、非同期実行、タイムアウト、stderr 分離、JSONでの双方向通信など)まで含めたコードを整えます!

あなた:
「Pythonコード=Pythonスクリプト」を 外部プロセスとして起動 して、引数を渡して実行するための 実用的な Delphi ユニット を用意しました。

標準出力/標準エラーの分離取得、作業ディレクトリ指定、環境変数の追加、標準入力の送信、タイムアウト、終了コードの取得にも対応しています。


Delphi: Pythonスクリプト実行ユニット

unit PyScriptRunner; interface uses System.SysUtils, System.Classes, Winapi.Windows; type EPyRunError = class(Exception); EPyRunTimeout = class(EPyRunError); // 実行オプション TPythonRunOptions = record PythonExe: string; // 例: 'C:\Python312\python.exe'(venvのpython.exeでもOK) ScriptPath: string; // 例: 'C:\work\myscript.py' Args: TArray<string>; // 例: ['--name','山川','--n','3'] WorkDir: string; // 作業ディレクトリ(空なら ScriptPath のディレクトリ) InputUTF8: UTF8String; // 標準入力に流す内容(UTF-8)。不要なら空 ExtraEnv: TArray<string>;// 追加の環境変数 'KEY=VALUE' 形式(PATH追加など) TimeoutMS: Cardinal; // タイムアウトms(0=無限) MergeStdErr: Boolean; // Trueでstderrをstdoutへ結合 Unbuffered: Boolean; // Trueで -u を付けて即時出力 end; TPythonRunResult = record ExitCode: Cardinal; // プロセスの終了コード StdOut: UTF8String; // 標準出力(UTF-8) StdErr: UTF8String; // 標準エラー(UTF-8) function StdOutText: string; function StdErrText: string; end; function RunPythonScript(const Opt: TPythonRunOptions): TPythonRunResult; implementation function TPythonRunResult.StdOutText: string; begin Result := TEncoding.UTF8.GetString(BytesOf(StdOut)); // Delphi文字列へ end; function TPythonRunResult.StdErrText: string; begin Result := TEncoding.UTF8.GetString(BytesOf(StdErr)); end; function QuoteIfNeeded(const S: string): string; begin if (S = '') or (S.IndexOfAny([' ', '"']) >= 0) then Result := '"' + StringReplace(S, '"', '\"', [rfReplaceAll]) + '"' else Result := S; end; function JoinArgs(const Items: TArray<string>): string; var i: Integer; begin Result := ''; for i := 0 to High(Items) do begin if i > 0 then Result := Result + ' '; Result := Result + QuoteIfNeeded(Items[i]); end; end; procedure AppendEnvironment(const EnvAdd: TArray<string>; var EnvBlock: string); var i: Integer; begin if Length(EnvAdd) = 0 then Exit; for i := 0 to High(EnvAdd) do EnvBlock := EnvBlock + EnvAdd[i] + #0; end; function BuildEnvironmentBlock(const ExtraEnv: TArray<string>): PChar; var SysEnv: TStringList; Block: string; i: Integer; begin // ベースは親プロセスの環境 SysEnv := TStringList.Create; try SysEnv.Sorted := True; SysEnv.Duplicates := dupIgnore; // 既存環境を拾う i := 0; while PChar(GetEnvironmentStrings)^ <> #0 do Break; // 使わず標準APIから文字列化してもよいが、簡便化のため空 // 簡易版: 追加分だけブロック化(既存環境の引き継ぎはCreateProcessデフォルトに任せる) Block := ''; AppendEnvironment(ExtraEnv, Block); Block := Block + #0; // 末尾ダブルNUL Result := StrNew(PChar(Block)); finally SysEnv.Free; end; end; function RunPythonScript(const Opt: TPythonRunOptions): TPythonRunResult; var SA: SECURITY_ATTRIBUTES; SI: STARTUPINFOW; PI: PROCESS_INFORMATION; // stdout OutRd, OutWr: THandle; // stderr ErrRd, ErrWr: THandle; // stdin InRd, InWr: THandle; CmdLine: string; Buffer: array[0..8191] of Byte; BytesRead: DWORD; OK: BOOL; StartTick: Cardinal; WaitRes: DWORD; CurDir: string; ProcFlags: DWORD; EnvBlock: PChar; Wrote: DWORD; begin ZeroMemory(@Result, SizeOf(Result)); // パイプ(継承可能ハンドルの作成) ZeroMemory(@SA, SizeOf(SA)); SA.nLength := SizeOf(SA); SA.bInheritHandle := TRUE; if not CreatePipe(OutRd, OutWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stdout) failed: %d', [GetLastError]); if not SetHandleInformation(OutRd, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stdout) failed: %d', [GetLastError]); if not Opt.MergeStdErr then begin if not CreatePipe(ErrRd, ErrWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stderr) failed: %d', [GetLastError]); if not SetHandleInformation(ErrRd, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stderr) failed: %d', [GetLastError]); end else begin ErrRd := 0; ErrWr := OutWr; // 結合 end; if not CreatePipe(InRd, InWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stdin) failed: %d', [GetLastError]); if not SetHandleInformation(InWr, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stdin) failed: %d', [GetLastError]); try // コマンドライン // 例: "python.exe" -u "myscript.py" --name "山川" --n 3 CmdLine := QuoteIfNeeded(Opt.PythonExe) + ' '; if Opt.Unbuffered then CmdLine := CmdLine + '-u '; CmdLine := CmdLine + QuoteIfNeeded(Opt.ScriptPath); if Length(Opt.Args) > 0 then CmdLine := CmdLine + ' ' + JoinArgs(Opt.Args); // 作業ディレクトリ if Opt.WorkDir <> '' then CurDir := Opt.WorkDir else CurDir := ExtractFileDir(Opt.ScriptPath); // STARTUPINFO ZeroMemory(@SI, SizeOf(SI)); SI.cb := SizeOf(SI); SI.dwFlags := STARTF_USESTDHANDLES; SI.hStdInput := InRd; SI.hStdOutput := OutWr; SI.hStdError := ErrWr; ZeroMemory(@PI, SizeOf(PI)); // 親環境 + 追加環境(簡易:追加のみ上書き) EnvBlock := nil; if Length(Opt.ExtraEnv) > 0 then EnvBlock := BuildEnvironmentBlock(Opt.ExtraEnv); ProcFlags := CREATE_NO_WINDOW; // CreateProcessW if not CreateProcessW( nil, PWideChar(CmdLine), nil, nil, TRUE, ProcFlags, EnvBlock, PWideChar(CurDir), SI, PI) then raise EPyRunError.CreateFmt('CreateProcess failed: %d' + sLineBreak + '%s', [GetLastError, CmdLine]); try // 子側に不要なハンドルは閉じる(親側) CloseHandle(OutWr); OutWr := 0; if not Opt.MergeStdErr then begin CloseHandle(ErrWr); ErrWr := 0; end; CloseHandle(InRd); InRd := 0; // 標準入力へ書き込む(必要なら) if Length(Opt.InputUTF8) > 0 then begin if not WriteFile(InWr, Opt.InputUTF8[1], Length(Opt.InputUTF8), Wrote, nil) then raise EPyRunError.CreateFmt('Write to stdin failed: %d', [GetLastError]); end; // 入力終了 CloseHandle(InWr); InWr := 0; // 出力読み取り(非同期ループ + タイムアウト) StartTick := GetTickCount; // ループ:プロセスが生きている間、随時パイプを読む while True do begin // タイムアウト判定 if (Opt.TimeoutMS > 0) and (GetTickCount - StartTick >= Opt.TimeoutMS) then begin TerminateProcess(PI.hProcess, Cardinal(-1)); raise EPyRunTimeout.CreateFmt('Python script timed out (%d ms).', [Opt.TimeoutMS]); end; // stdout OK := ReadFile(OutRd, Buffer, SizeOf(Buffer), BytesRead, nil); if OK and (BytesRead > 0) then Result.StdOut := Result.StdOut + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); // stderr(分離時のみ) if (ErrRd <> 0) then begin if PeekNamedPipe(ErrRd, nil, 0, nil, @BytesRead, nil) and (BytesRead > 0) then begin if ReadFile(ErrRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) then Result.StdErr := Result.StdErr + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); end; end; // プロセス終了チェック(短い待機で負荷を抑える) WaitRes := WaitForSingleObject(PI.hProcess, 10); if WaitRes = WAIT_OBJECT_0 then begin // 残りの出力を読み切る while ReadFile(OutRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) do Result.StdOut := Result.StdOut + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); if (ErrRd <> 0) then while ReadFile(ErrRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) do Result.StdErr := Result.StdErr + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); Break; end; // 小休止 Sleep(1); end; // 終了コード GetExitCodeProcess(PI.hProcess, Result.ExitCode); finally if PI.hThread <> 0 then CloseHandle(PI.hThread); if PI.hProcess <> 0 then CloseHandle(PI.hProcess); end; finally if OutWr <> 0 then CloseHandle(OutWr); if OutRd <> 0 then CloseHandle(OutRd); if (ErrRd <> 0) and (ErrRd <> OutRd) then CloseHandle(ErrRd); if (ErrWr <> 0) and (ErrWr <> OutWr) then CloseHandle(ErrWr); if InWr <> 0 then CloseHandle(InWr); if InRd <> 0 then CloseHandle(InRd); if Assigned(EnvBlock) then StrDispose(EnvBlock); end; end; end.

使い方(呼び出し例)

uses System.SysUtils, PyScriptRunner; procedure TForm1.ButtonRunClick(Sender: TObject); var Opt: TPythonRunOptions; Res: TPythonRunResult; begin Opt.PythonExe := 'C:\Python312\python.exe'; // venvのpython.exeでもOK Opt.ScriptPath := 'C:\work\myscript.py'; Opt.Args := TArray<string>.Create('--name', '山川', '--n', '2'); Opt.WorkDir := ''; // 空ならScriptのフォルダ Opt.InputUTF8 := ''; // 標準入力に流したい文字列があればUTF-8で Opt.ExtraEnv := TArray<string>.Create('MYFLAG=1'); // 追加の環境変数があれば Opt.TimeoutMS := 15000; // 15秒でタイムアウト(0=無限) Opt.MergeStdErr:= False; // Falseでstderr分離 Opt.Unbuffered := True; // -u(即時出力) try Res := RunPythonScript(Opt); Memo1.Lines.Add('ExitCode: ' + Res.ExitCode.ToString); Memo1.Lines.Add('--- STDOUT ---'); Memo1.Lines.Add(Res.StdOutText); Memo1.Lines.Add('--- STDERR ---'); Memo1.Lines.Add(Res.StdErrText); except on E: EPyRunTimeout do ShowMessage('Timeout: ' + E.Message); on E: Exception do ShowMessage('Error: ' + E.Message); end; end;

Pythonスクリプト側(例)

# myscript.py import argparse, sys, time p = argparse.ArgumentParser() p.add_argument('--name', required=True) p.add_argument('--n', type=int, default=1) a = p.parse_args() for i in range(a.n): print(f"hello {a.name} #{i+1}", flush=True) time.sleep(0.5) print("done.", file=sys.stderr, flush=True)

よくあるポイント

  • venv を使う: Opt.PythonExe にその venv の ...\Scripts\python.exe を指定。

  • 長い出力/日本語: 本コードは UTF-8 前提で取り込み。Windows側コンソールコードページに依存しません。

  • JSONで引数をまとめる: 複雑なパラメータは --json "<json文字列>" として渡すのが安全です。

  • 大きな標準入力: Opt.InputUTF8 に入れて送れます(サイズが大きい場合は断片送信に変更可能)。

  • タイムアウト: TimeoutMS を設定すると、超過時にプロセスを kill して例外を投げます。

必要なら、

  • 非同期(スレッド / コールバック)版

  • PowerShell/WSL 経由での実行

  • macOS/Linux(TProcess 相当)対応
    も

2025年10月11日土曜日

Nuitka Python コンパイラー

 


参照

https://blog.tsukumijima.net/article/python-nuitka-usage/


PyInstaller で作ったソフトは ファイルサイズがデカい・起動が遅い・ウイルス判定されやすい とちょっと微妙な点が多いです。

それでも比較的簡単に exe 化できるのでよく使われているわけですが、とはいえネイティブの exe ほどパフォーマンスは上がりませんし、むしろ普通に Python で実行したときより遅くなります。

ここで、PyInstaller の仕組みを簡潔に説明します。
PyInstaller は基本 --onefile オプションをつけて実行することが多いと思いますが、あれは exe の中に自己解凍機能が入っていて、実行すると Windows なら AppData\Local\Temp あたりに exe の中に入っていたパッケージ一式が解凍されます。その中に入っているスクリプトを実行することで、見た感じあたかも単一の exe ファイルで実行できるように見せかけている、というものです。
解凍されたパッケージ一式は終了時に削除されるため、ダブルクリックで実行した際にそうしたパッケージを毎回解凍することになり、それが起動時のオーバーヘッドになっています。

ほかに Python を exe 化するソフトとしてはたとえば py2exe という Windows 専用のツールなどもあったりしますが、Windows 専用な上に使い方が面倒くさく、あまり手軽とはいえません。

Sponsored Link

Nuitka の導入

GitHub - Nuitka/Nuitka: Nuitka is a Python compiler written in Python. It's fully compatible with Python 2.6, 2.7, 3.4-3.13. You feed it your Python app, it does a lot of clever things, and spits out an executable or extension module.
Nuitka is a Python compiler written in Python. It's fully compatible with Python 2.6, 2.7, 3.4-3.13. You feed it your Py...

Nuitka - Wikipedia

そこで Python の公式ドキュメントにはどう書いてあるのかな?と思い漁ってみると、「Nuitka」というソフトがいくつかある中の一番上で紹介されていました。

そもそもどういうツールなのか分かりにくいのですが、PyInstaller と同じ、Python プログラムの実行ファイル化ができるツールになります。なんと Python コードを一度C言語にトランスパイルし、それをさらに gcc などのコンパイラでコンパイルしたものを実行ファイルとして出力するというんだから驚きです。

今までまったく知らなかったのですが、調べると2013年頃にはすでに開発が始まっていたようで、それなりに老舗のツールのようでした。いまだに PyInstaller 一強になっているのが不思議なくらい…。
一度C言語にコンパイルするため(Python のコードでない)ネイティブな実行ファイルとして使える上に、C言語向けのパフォーマンスチューニングが行われるため、ファイルサイズが小さくなる上に実行速度も普通に Python を実行したときよりも速くなるんだそう。すごい。
ただ、日本だと VOICEVOX のビルドに使われていたりを見かけた以外は、知名度も利用例もあまりなさそうな印象でした。

とりあえず良さそうだし、まずはインストールして使ってみましょう。
今回は Windows 上で pipenv を使います。理由は PyInstaller と同様、グローバルの pip パッケージを使ってしまうとそこにインストールされている余計なパッケージも同梱されてしまうおそれがあったためです。
とはいえ、Nuitka は賢くて、 --follow-imports オプションをつければ Python ファイルを静的解析して自動的に使ってるライブラリだけを読み込んでくれるっぽいので、あまり関係なさそうだとは思います。

ここは PyInstaller でも概ね同じだと思うのですが、Nuitka は残念ながら OS をまたいだクロスコンパイルはできないみたいです。なので、Windows 用の .exe であれば Windows 上でビルドする必要があるし、Mac や Linux の場合も然りです。

pipenv install nuitka zstandard

pipenv で、nuitka と zstandard をインストールします。

ビルド時につまづいた点なのですが、後述する --onefile オプションで単一の実行ファイルにまとめる場合、Zstandard という圧縮形式(見慣れない圧縮形式だが、そこそこ圧縮率が高くてめちゃくちゃ解凍が速いのが特徴らしい)を使ってコード全体を圧縮するみたいです。

Nuitka も単一の実行ファイルにまとめる場合は、PyInstaller と同じく Temp フォルダに自己解凍してそれを実行するような形になっています。Zstandard の解凍が速いという特性は、前述した解凍時のオーバーヘッドを抑えつつ、ファイルサイズを圧縮するのに便利なのでしょう。
Zstandard が使えなくても単一の実行ファイルにはまとめられますが、ファイルサイズが圧縮する場合の2~3倍にまで膨れあがります…。

注意点として、Python のアーキテクチャと後述する MinGW64 などのコンパイラのアーキテクチャは一致している必要があるらしいです。64bit 環境なのに Python の 32bit 版をインストールしている場合は注意してください。
今どき 32bit の exe は多くのユースケースで不要だと思うので、切り捨てちゃっていいとは思います。

Nuitka の使い方

pipenv shell

事前に pipenv の仮想環境の中に入っておきます。

nuitka --mingw64 --follow-imports --onefile (エントリーポイントにしたい Python ファイル).py

あとは上記のコマンドを実行するだけです。

--mingw64 オプションでは明示的に C コンパイラに MinGW64 内の GCC を指定します。
Visual Studio が入っているなどで MSVC が使える状況にあればそちらが自動で利用されるのですが、どうやら MSVC の言語が日本語になっていると、CP932🤮諸々の文字コード問題で Nuitka 側がビルドに失敗してしまうようです。
MSVC (MSBuild とかそのへん) は日本語環境だとコンソールに CP932 (Shift-JIS) で出力するみたいなのですが、Nuitka は英語しか想定していないため、予想されていない文字コードという事で落ちてしまうようでした。MSVC を英語版で入れてねみたいな事が公式ドキュメントに書いてありましたが、さすがにそこまでのやる気もなく…。

そもそも MSVC が入ってない場合は自動で MinGW64 を使うようになっているので、そちらを明示的に指定します。MinGW64 というのは要は Windows 上で GCC を使うためのやつなんですが、Nuitka を使うために MinGW64 をインストールしておく必要はありません。

Nuitka はとても賢くて、ビルド環境に GCC がないと分かった場合、「GCC を AppData 以下にダウンロードするけどいいですか?一回限りでキャッシュされます」みたいな事を訊かれます。そこで YES を入力すると、Nuitka 側がちゃんと動作する MinGW64 と GCC を環境を汚さずインストールしてくれて、さらにそれを自動で使ってくれます。ありがたすぎる…。
他にも depends っていうやつも自動でインストールするか訊かれるので、それも YES と答えておきましょう。2回目以降は自動でダウンロードされた GCC や depends が使われます。

最初こちらで GCC 8.1.0 の MinGW64 をインストールしたりしたのですが、最新の Nuitka は GCC 11.2 以降にしか対応していないようで、使えないと判断されて自動で最新の GCC のインストールを求められたりしてちょっと嵌まりました。Nuitka にまかせておけば自動でいい感じに GCC を使ってくれるので、任せてしまうのが手っ取り早いと思います。

--follow-imports は Python ファイルを静的解析して、インポートしているファイルを再帰的にビルド対象にいれてくれるオプションみたいです。単一ファイルと標準ライブラリで収まるソフトでないならば、これをつけないとうまく動かなくて壊れます(そりゃそう)。

最後の --onefile は PyInstaller と同様に単一の実行ファイルにまとめるオプションです。これをつけないと、現在の環境にインストールされている Python を利用する exe ファイルが生成されてしまいます。

単一の実行ファイルにまとめなくてもいい場合は --standalone オプションを代わりに指定します。
何も出力フォルダを指定しない場合、デフォルトで (エントリーポイントの py ファイルの名前).dist/ フォルダ以下に出力されます。

--standalone の場合、Python のコードはすべて exe に統合されていますが、Python 本体や依存している Visual C++ のランタイム、Tcl/Tk などのネイティブ拡張に関してはファイルとしてそのまま配置されます。実行する場合はこれら全てをセットで配置する必要があるみたいです。
今回検証に利用した EDCBNotifier の場合、サイズはフォルダ全体で 36MB ほどでした。
内訳は本体が 18MB 、Python 本体と標準ライブラリ諸々が残りの 18MB を閉めています。ファイルに何も圧縮が掛かっていないので当然っちゃそう。結構細々としたファイルでぐちゃぐちゃになるので、それが嫌な人は --onefile を使う事になると思います。

--onefile オプションを指定した場合、--standalone のファイルを生成した後にそれを前述の Zstandard で圧縮して一つの exe にまとめます。PyInstaller 同様に一度 Temp フォルダに展開してから実行する点は代わりませんが、Zstandard のおかげでファイルサイズが --standalone と比較して2倍近く圧縮される上に、PyInstaller よりも起動がかなり速いのが大きな差です。
–onefile オプションを指定した場合は、実行ファイルが (エントリーポイントの py ファイルの名前).exe に生成されます。今回の場合ファイルサイズは 12MB 弱で、1/3 ほどに圧縮できている事になります。

--onefile オプションは --standalone オプションの動作を包含するため、--onefile で実行した場合にも (エントリーポイントの py ファイルの名前).dist/ に単一の実行ファイル化されていないファイルが出力されます。
大は小を兼ねると言いますし、基本的に --onefile で良いんじゃないでしょうか。

試しに python EDCBNotifier.py PostAddReserve と ./EDCBNotifier.exe PostAddReserve の差を測ってみたところ、前者が 2.0 秒、後者が 2.5 ~ 2.7 秒ほどでした。解凍に 0.5 秒ほどかかるようで直接 Python コードを実行するよりも起動は遅くなりますが、それでも PyInstaller よりかはかなり速くなるんじゃないかと思います。コードの規模にもよると思いますが、C 言語に変換した分コードも速くなっているはずなので、そのあたりの恩恵もあるかもしれません。

ちなみに、Nuitka にも PyInstaller と同様に、オプションからカスタムのアプリアイコンを設定することが可能です。さらに Windows の exe のファイルバージョンのメタデータなどもオプションから細かく指定することができたりなど、至れり尽くせりで助かります。

使ってみた感想とまとめ

動作感やパフォーマンスは PyInstaller より良好で、PyInstaller を使うならこっちを使った方がよりスマートになりそうだな、という感想を持ちました。動作自体にも今のところ特に問題はなさそうに見えます。

ただ、一番のネックは「とにかくビルドが重い」という事に尽きます。PyInstaller もそれなりに時間がかかりますが、Nuitka のビルド時間はそれの比ではありません。
そもそも PyInstaller は特にソースコードをコンパイルせず単一ファイルにまとめてそれを Temp に展開しているだけなので、わざわざ C のネイティブコードに変換してそれを更にビルドしている Nuitka と比較するほうが無理がありそうです…。

一度ビルドすると、ビルド済みの C コードが (エントリーポイントの py ファイルの名前).build/ 以下に、CCache というビルドキャッシュが AppData\Local\Nuitka 以下に生成されます。
変更されたコードだけを再ビルドして変更のないコード(ライブラリ)などはそのまま使うようにして一定の時間短縮を図っているようですが、コード量も依存ライブラリもそこまで多くない EDCBNotifier ですら、何もソースコードをいじらずビルドキャッシュが効いている状態なのにも関わらず、ビルドが終わるまでに 2 分 30 秒も掛かってしまいました。
たとえば VOICEVOX のようなクソデカソフトの場合、さらに機械学習系のクソデカライブラリもビルドする必要があるので、ビルド時間が恐ろしいことになるのは想像に難くありません。

ただ、逆を言えば遅いのはビルドするときだけで、それ以外では高速に動作します。当然ユーザーに配布する場合も高速に動作することが期待できますし、開発マシンのスペックがクソ重いとかでないならば、十二分に検討する価値はあると思います。

あと、私が確認した限りでは、--onefile オプションをつけて単一の実行ファイルにまとめると、なぜか Windows 7 で実行できなくなってしまっていました。Windows 7 がサポートされているはずの Python 3.8 でもダメだったため、おそらく Nuitka 側に何らかの問題があると考えられます。
ただ、Windows 7 はすでにサポート終了から2年近く経ちますし、もうサポートを切ってもいい頃合いだとは思います。

冒頭で上げた PyInstaller のデメリットのうち、「ファイルサイズがデカい」と「起動が遅い」に関しては Nuitka を使うことで大幅な改善が期待できると思います。「ウイルス判定されやすい」という点に関してはまだわかりませんが、すくなくとも Nuitka は C 言語にコンパイルしたものを exe にしているため、コードがそのまま載っている PyInstaller よりも誤検知されにくいことが期待できるはずです。

2022 年の Python の exe 化には Nuitka を使っていこう!!ということで、今年最後の記事を〆たいと思います。

RTX1210で2拠点間同一セグメント

 


RTX1200とインターネットを利用して2つの拠点間を同一セグメントにする方法を教えてください
ChatGPT:

RTX1200を使ってインターネット経由で2拠点を「同一セグメント」にする方法ですね。これはいわゆる L2TPv3(Ether/IP)やブリッジ接続を用いたVPN構成 になります。ポイントを整理すると以下の通りです。


方法の概要

通常のIPsec-VPNでは「拠点間をルータでルーティング」する形になるため、拠点ごとに異なるIPセグメントを割り当てます。
しかし「同一セグメントで使いたい」場合は、ルーティングではなく レイヤ2トンネル を張る必要があります。RTX1200では以下の手段が使えます。

  1. L2TPv3 (Ether over IP)

    • RTX同士でL2TPv3セッションを張り、Ethernetフレームをそのままカプセル化して転送。

    • これによりブリッジ接続が可能になり、2拠点を同一L2セグメントにできる。

  2. IPsecとの併用

    • L2TPv3トンネルを暗号化するためにIPsecを併用するのが一般的。


設定イメージ

例えば拠点Aと拠点Bを同一セグメント 192.168.10.0/24 で使いたい場合:

拠点A(RTX1200)

# WAN 側の設定(省略) # Ether over IP の定義 tunnel select 1 tunnel encapsulation l2tpv3 tunnel endpoint address <拠点BのグローバルIP> ipsec tunnel 1 exit # ブリッジ作成 bridge 1/1 bridge member lan1 bridge member tunnel1

拠点B(RTX1200)

tunnel select 1 tunnel encapsulation l2tpv3 tunnel endpoint address <拠点AのグローバルIP> ipsec tunnel 1 exit bridge 1/1 bridge member lan1 bridge member tunnel1

注意点

  • DHCPやブロードキャストも拠点間で流れるため、利用環境によっては帯域に影響が出ることがあります。

  • RTX1200は古い機種なので、最新ファームでの対応を確認してください。

  • L2TPv3はNAT環境をまたぐと不安定になることがあります。できればグローバルIP直収が望ましいです。

  • 運用面では「同一セグメント」にせず、通常のルーティングVPN構成を推奨されることが多いです(管理・拡張性の観点)。


👉 ご希望としては「DHCPサーバを1拠点にまとめたい」などでしょうか?
それによっては L2VPNではなくDHCPリレーやルーティング構成の方が安定・簡単に運用できるケースもあります。

必要であれば、具体的なユースケース(DHCP共有したい・ブロードキャストを通したい・単純に同じセグメントを維持したい等)を伺えれば、さらに最適な構成例をご提案できます。

あなた:
RTX1210とインターネットを利用して2つの拠点間を同一セグメントにする方法を教えてください
ChatGPT:

ご質問ありがとうございます!
RTX1210でも、基本的な考え方はRTX1200と同じで「レイヤ2トンネル(L2TPv3/EoIP)」を使う必要があります。つまり、通常のIPsec-VPNのようにルーティングではなく、Ethernetフレームをそのままカプセル化して転送する仕組みです。これにより、2拠点を同じブロードキャストドメイン(同一セグメント)にできます。


方法の概要

  1. L2TPv3 (Ether over IP) を使う

    • RTX1210はL2TPv3に対応しているため、これを使って拠点間をL2接続にします。

  2. IPsecで暗号化する

    • セキュリティを考慮し、トンネルはIPsecで暗号化します。

  3. ブリッジを構成する

    • LANインタフェースとトンネルインタフェースをブリッジに参加させ、同一セグメントにします。


設定例

前提

  • 拠点AのグローバルIP: 203.0.113.1

  • 拠点BのグローバルIP: 203.0.113.2

  • 共通セグメント: 192.168.10.0/24


拠点A(RTX1210)

# WAN側は省略 # トンネル定義 tunnel select 1 tunnel encapsulation l2tpv3 tunnel endpoint address 203.0.113.2 ipsec tunnel 1 exit # IPsec設定 ipsec auto refresh on ipsec ike keepalive log on ipsec ike keepalive use on ipsec ike pre-shared-key 1 text <共通鍵> ipsec ike local address 203.0.113.1 ipsec ike remote address 203.0.113.2 # ブリッジ設定 bridge 1/1 bridge member lan1 bridge member tunnel1

拠点B(RTX1210)

tunnel select 1 tunnel encapsulation l2tpv3 tunnel endpoint address 203.0.113.1 ipsec tunnel 1 exit ipsec auto refresh on ipsec ike keepalive log on ipsec ike keepalive use on ipsec ike pre-shared-key 1 text <共通鍵> ipsec ike local address 203.0.113.2 ipsec ike remote address 203.0.113.1 bridge 1/1 bridge member lan1 bridge member tunnel1

注意点

  • DHCPやブロードキャストも拠点間に流れるため、ネットワーク規模が大きいとトラフィックが膨らみやすいです。

  • NATを超えると動作が不安定になる場合があります。できれば両拠点ともグローバルIPを直接持たせる方が安定します。

  • 管理面では「同一セグメント」より「別セグメント+ルーティング(IPsec-VPN)」の方がシンプルでトラブルも少ないです。


👉 ちなみに、同一セグメント化をしたい理由は「DHCPを一元管理したい」「プリンタやNASを拠点間で透過的に使いたい」などでしょうか?
もしよければ利用目的を教えていただければ、L2VPNが本当にベストか、それともL3VPNで代替可能かもご提案できます。