Flutterは単一のコードベースからiOS, Android, Web, Windowsなど多彩なプラットフォーム向けにアプリを作れるのが魅力です。

しかし、「Windowsアプリ(.exe)をビルドするにはWindows機が必要」 という制約があります。

普段MacやLinuxで開発していると、ビルドのためだけにPCを移動したり、Git経由で同期してWindows側でコマンドを叩くのは非常に手間ですよね。GitHub ActionsなどのCIサービスも便利ですが、ちょっとした動作確認のためにpushして待つのは時間がかかります。

そこで今回は、余っているWindows PCを「ビルド専用サーバー」にし、Macからワンクリックでビルドを依頼し、完成した .exe を受け取るシステム をPythonで自作しました。

作るもの:システム構成

仕組みはシンプルです。HTTP通信(REST API)を使ってソースコードと成果物をやり取りします。

  1. Client (Mac/Linux): 現在のコードをZip圧縮し、サーバーへ送信。
  2. Server (Windows): コードを受け取り、flutter build windows を実行。
  3. Server: 生成された .exe 等をZipにまとめて返却。
  4. Client: 成果物を受け取り、日時付きファイル名で保存してデスクトップ通知。

これを Visual Studio CodeAndroid Studio のタスクとして登録し、ショートカット一発で実行できるようにします。


1. サーバー側の構築 (Windows機)

まずはビルドを行うWindowsマシンの設定です。

項目要件・推奨バージョン備考
OSWindows 11 Home/Proどちらでも可
Flutter SDK3.x 以上推奨C:\src\flutter に配置
Visual StudioVisual Studio 2026「C++によるデスクトップ開発」をインストール済であること
Python3.10 以上APIサーバーの実行に使用
PowerShell5.0 以上ファイアウォール設定等で使用(標準でOK)

管理しやすくするため、今回はすべての関連ファイル(SDK含む)を C:¥src 配下に集約する構成にします。

1-1. 前提環境の準備

Windows 11のマシンを用意し、以下のフォルダ構成を作ります。
(※フォルダ構成は例です)

C:¥
 └─ src¥
     ├─ flutter¥           (Flutter SDK 本体)
     ├─ build_server.py    (今回作成するスクリプト)
     └─ server_workspace¥  (※スクリプトが自動生成します)
  1. Flutter SDK: 公式サイトからダウンロードし、C:¥src¥flutter に配置してください。環境変数のPathに「C:¥src¥flutter¥bin」を通してください。flutter doctor でVisual Studio (C++) の要件が満たされていることを確認します。
  2. Python 3.x: 公式サイトからインストールしてください。

※今回はWindowsアプリのビルドのみを目的としているため、Android Studioのインストールは不要です。

1-2. 必要なPythonライブラリのインストール

高速なAPIフレームワークである、FastAPI を使用します。コマンドプロンプトで以下を実行してください。

pip install fastapi uvicorn python-multipart

1-3. ファイアウォールの設定(重要)

LAN内のMac/Linuxから接続できるように、ポート 8000 を開放します。

「管理者権限」でPowerShell を開き、以下のコマンドを実行します。

New-NetFirewallRule -DisplayName "FlutterBuildServer" -Direction Inbound -LocalPort 8000 -Protocol TCP -Action Allow

※ セキュリティソフトのファイアウォール設定も合わせて調整してください。

1-4. サーバー用スクリプトの作成

C:¥src¥build_server.py というファイルを作成し、以下のコードを保存します。

# C:¥src¥build_server.py
import os
import shutil
import subprocess
import zipfile
import uuid
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
import uvicorn

app = FastAPI()

# 設定: C:¥src 配下を作業ディレクトリとする
BASE_DIR = r"C:¥src"
WORK_DIR = os.path.join(BASE_DIR, "server_workspace")
os.makedirs(WORK_DIR, exist_ok=True)

@app.post("/build")
async def build_flutter_app(file: UploadFile = File(...)):
    build_id = str(uuid.uuid4())
    project_dir = os.path.join(WORK_DIR, build_id)
    os.makedirs(project_dir, exist_ok=True)
    zip_path = os.path.join(project_dir, "source.zip")
    
    try:
        # 1. ソースコード受信・解凍
        with open(zip_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(project_dir)
            
        # 2. ビルド実行
        print(f"[{build_id}] Build started...")
        subprocess.run(["flutter", "pub", "get"], cwd=project_dir, check=True, shell=True)
        subprocess.run(["flutter", "build", "windows"], cwd=project_dir, check=True, shell=True)
        
        # 3. 成果物のパス特定 (Flutterバージョンによるパスの揺れを吸収)
        base_build = os.path.join(project_dir, "build", "windows")
        target_dir = os.path.join(base_build, "x64", "runner", "Release")
        if not os.path.exists(target_dir):
            target_dir = os.path.join(base_build, "runner", "Release")

        # 4. 圧縮して返却
        artifact_zip = os.path.join(WORK_DIR, f"{build_id}_artifact")
        shutil.make_archive(artifact_zip, 'zip', target_dir)
        
        print(f"[{build_id}] Build success.")
        return FileResponse(path=f"{artifact_zip}.zip", media_type='application/zip')

    except Exception as e:
        print(f"[{build_id}] Error: {e}")
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    print(f"Server started at {BASE_DIR}")
    uvicorn.run(app, host="0.0.0.0", port=8000)

サーバーの起動:

コマンドプロンプトで以下を実行し、サーバーを待機状態にします。

cd C:¥src
python build_server.py

※ ここでWindows機のIPアドレス(例: 192.168.1.10)を確認しておいてください。


2. クライアント側の構築 (Mac / Linux / Windows)

次に、普段開発を行っているマシン側の設定です。OSは問いません。

2-1. 必要なライブラリ

通信用とデスクトップ通知用のライブラリを入れます。

# Mac / Linux
pip3 install requests plyer

# Windows
pip install requests plyer

2-2. クライアントスクリプトの配置

Flutterプロジェクトのルートディレクトリ(pubspec.yamlがある場所)に client_builder.py を作成します。

重要: SERVER_URL の部分は、先ほどのWindows機のIPアドレスに書き換えてください。

# client_builder.py
import requests
import os
import zipfile
from datetime import datetime  # 追加: 日時取得用

# ==========================================
# 【設定】 WindowsサーバーのIPアドレスに書き換えてください
SERVER_URL = "http://192.168.0.130:8000/build"
# ==========================================

OUTPUT_DIR = "dist_server"

def zip_project(source_dir, output_zip):
    with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf:
        for root, dirs, files in os.walk(source_dir):
            dirs[:] = [d for d in dirs if d not in ['.git', 'build', '.dart_tool', 'dist_server', '.idea', '.vscode']]
            for file in files:
                if file == '.DS_Store': continue
                file_path = os.path.join(root, file)
                arcname = os.path.relpath(file_path, source_dir)
                zipf.write(file_path, arcname)

def main():
    print(f"--- Remote Build Start ---")
    zip_filename = "temp_source.zip"

    try:
        print("Compressing project...")
        zip_project(os.getcwd(), zip_filename)

        print(f"Uploading to {SERVER_URL} ...")
        with open(zip_filename, 'rb') as f:
            # タイムアウトを10分(600秒)に設定
            response = requests.post(SERVER_URL, files={'file': f}, stream=True, timeout=600)

        if response.status_code == 200:
            os.makedirs(OUTPUT_DIR, exist_ok=True)

            # --- 変更箇所: 日時付きファイル名の生成 ---
            # 例: 20250123141503 (YYYYMMDDhhmmss形式)
            timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
            filename = f"app_release_{timestamp}.zip"
            artifact_path = os.path.join(OUTPUT_DIR, filename)
            # -------------------------------------

            with open(artifact_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)

            print(f"Build Success! Saved to: {artifact_path}")
        else:
            print(f"Build Failed: {response.status_code}\n{response.text}")

    except Exception as e:
        print(f"Error: {e}")
    finally:
        if os.path.exists(zip_filename):
            os.remove(zip_filename)

if __name__ == "__main__":
    main()

3. IDEへの統合

スクリプトを毎回ターミナルから叩くのは面倒です。IDEからワンタッチで実行できるようにします。

3-1. Visual Studio Code の場合

プロジェクト内の .vscode/tasks.json に以下を記述します。

Mac/Linuxの python3 と Windowsの python コマンドの違いを吸収する設定にしています。

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Remote Build (Windows)",
            "type": "shell",
            "command": "${config:python.pythonPath}", 
            "args": [
                "${workspaceFolder}/client_builder.py"
            ],
            "options": {
                "env": {
                    "PYTHONIOENCODING": "utf-8"
                }
            },
            "windows": {
                "command": "python"
            },
            "osx": {
                "command": "python"
            },
            "linux": {
                "command": "python"
            },
            "group": "build",
            "presentation": {
                "reveal": "always",
                "panel": "new"
            }
        }
    ]
}
  • 使い方: Ctrl(Cmd) + Shift + B を押し、「Remote Build (Windows)」を選択します。

3-2. Android Studio の場合

「External Tools」機能を使用します。

  1. メニューから Preferences (WindowsはSettings) > Tools > External Tools を開きます。
  2. 「+」ボタンで新規追加し、以下のように設定します。
項目設定値
NameRemote Build Windows
Program(Mac/Linux) /usr/bin/python3 (または which python3 の結果)
(Windows) python
Argumentsclient_builder.py
Working directory$ProjectFileDir$
  • 使い方: メニューの Tools > External Tools > Remote Build Windows から実行。
  • 推奨: Keymap設定でショートカットキー(例: Cmd + Shift + W)を割り当てると非常に便利です。

実際に使ってみる

  1. Windowsサーバーで build_server.py を起動しておきます。
  2. MacのVS CodeやAndroid Studioで開発します。
  3. ビルドが必要になったら、IDEからタスクを実行します。
  4. ソースコードが転送され、サーバー側でビルドが走ります(初回は依存関係の解決などで少し時間がかかります)。
  5. 完了するとMac側に通知が届き、プロジェクト内の dist_server フォルダに app_release_YYYYMMDDhhmmss.zip が保存されます。

まとめ

これで、MacBook一台でカフェにいながら(VPN等を介して)、自宅のパワフルなWindowsマシンにビルドを投げ、Windowsアプリを生成する環境が整いました。

簡易的なCI/CDですが、個人開発や少人数のチーム開発であれば、これだけで十分な効率化になります。ぜひ試してみてください!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる