22"""Build script for creating PyInstaller binary."""
33import os
44import platform
5+ import shutil
6+ import subprocess
57import tomllib
68from datetime import date
79from pathlib import Path
1113sep = os .pathsep
1214
1315
16+ def clean_build ():
17+ build_dir = Path ('build' )
18+ if build_dir .exists ():
19+ shutil .rmtree (build_dir )
20+
21+
1422def build_version_file ():
1523 pyproject = Path (__file__ ).parent / "pyproject.toml"
1624 with open (pyproject , "rb" ) as f :
@@ -57,38 +65,150 @@ def build_version_file():
5765 f .write (TEMPLATE .strip ())
5866
5967
60- def build ():
61- """Build the binary using PyInstaller."""
62- system = platform .system ().lower ()
63- suffix = '.exe' if system == 'windows' else ''
64- arch = platform .machine ().lower ()
65-
66- # Map architecture names to Docker-compatible format
68+ def normalize_architecture (arch : str ) -> str :
6769 if arch in ['x86_64' , 'amd64' ]:
68- arch = 'amd64'
70+ return 'amd64'
6971 elif arch in ['aarch64' , 'arm64' ]:
70- arch = 'arm64'
72+ return 'arm64'
7173 elif arch .startswith ('arm' ):
72- arch = 'arm64' # Assume ARM64 for Docker compatibility
74+ return 'arm64'
75+ return arch
7376
74- system = "macos" if system == 'darwin' else system
75- name = f'perfecto-mcp-{ system } -{ arch } { suffix } '
7677
77- icon = 'app.ics' if system == 'macos' else 'app.ico'
78+ def normalize_system_name (system : str ) -> str :
79+ return "macos" if system == 'darwin' else system
80+
81+
82+ def get_binary_name (system : str , arch : str ) -> str :
83+ suffix = '.exe' if system == 'windows' else ''
84+ return f'perfecto-mcp-{ system } -{ arch } { suffix } '
85+
7886
87+ def get_icon_file (system : str ) -> str :
88+ return 'app.icns' if system == 'macos' else 'app.ico'
89+
90+
91+ def run_pyinstaller (name : str , icon : str ):
7992 PyInstaller .__main__ .run ([
8093 'main.py' ,
8194 '--onefile' ,
8295 '--version-file=version_info.txt' ,
8396 f'--add-data=pyproject.toml{ sep } .' ,
84- f'--add-data=resources/app.png { sep } resources' ,
97+ f'--add-data=resources{ sep } resources' ,
8598 f'--name={ name } ' ,
8699 f'--icon={ icon } ' ,
87100 '--clean' ,
88101 '--noconfirm' ,
89102 ])
90103
91104
105+ def build ():
106+ clean_build ()
107+
108+ system = normalize_system_name (platform .system ().lower ())
109+ arch = normalize_architecture (platform .machine ().lower ())
110+ name = get_binary_name (system , arch )
111+ icon = get_icon_file (system )
112+
113+ run_pyinstaller (name , icon )
114+ clean_build ()
115+
116+ if system == "macos" :
117+ create_app_bundle (name , arch , dist_dir = Path ("dist" ))
118+ elif system == "linux" :
119+ create_sha256_checksum (name , dist_dir = Path ("dist" ))
120+
121+
122+ def create_app_directory_structure (app_path : Path ) -> Path :
123+ macos_path = app_path / "Contents" / "MacOS"
124+ macos_path .mkdir (parents = True , exist_ok = True )
125+ return macos_path
126+
127+
128+ def copy_binary_to_app (binary_path : Path , target_path : Path ):
129+ if not binary_path .exists ():
130+ raise FileNotFoundError (f"Binary not found: { binary_path } " )
131+ shutil .copy2 (binary_path , target_path )
132+ os .chmod (target_path , 0o755 )
133+
134+
135+ def create_launcher_script (launcher_path : Path ):
136+ launcher_content = """#!/bin/bash
137+ set -e
138+
139+ BIN_DIR="$(cd "$(dirname "$0")" && pwd)"
140+ BIN="$BIN_DIR/perfecto-mcp"
141+
142+ if [ -t 1 ]; then
143+ exec "$BIN" "$@"
144+ else
145+ open -a Terminal "$BIN"
146+ fi
147+ """
148+ with open (launcher_path , "w" , encoding = "utf-8" ) as f :
149+ f .write (launcher_content )
150+ os .chmod (launcher_path , 0o755 )
151+
152+
153+ def create_info_plist (plist_path : Path ):
154+ info_plist_content = """<?xml version="1.0" encoding="UTF-8"?>
155+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
156+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157+ <plist version="1.0">
158+ <dict>
159+ <key>CFBundleExecutable</key>
160+ <string>launcher.sh</string>
161+
162+ <key>CFBundleIdentifier</key>
163+ <string>com.perfecto.mcp</string>
164+
165+ <key>CFBundleName</key>
166+ <string>Perfecto MCP</string>
167+
168+ <key>CFBundlePackageType</key>
169+ <string>APPL</string>
170+ </dict>
171+ </plist>
172+ """
173+ with open (plist_path , "w" , encoding = "utf-8" ) as f :
174+ f .write (info_plist_content )
175+
176+
177+ def create_app_bundle (binary_name : str , arch : str , dist_dir : Path ):
178+ app_name = f"perfecto-mcp-{ arch } .app"
179+ app_path = dist_dir / app_name
180+ contents_path = app_path / "Contents"
181+
182+ macos_path = create_app_directory_structure (app_path )
183+
184+ binary_path = dist_dir / binary_name
185+ copy_binary_to_app (binary_path , macos_path / "perfecto-mcp" )
186+
187+ create_launcher_script (macos_path / "launcher.sh" )
188+ create_info_plist (contents_path / "Info.plist" )
189+
190+ binary_path .unlink ()
191+ print (f"Created { app_name } in { dist_dir } " )
192+
193+
194+ def create_sha256_checksum (binary_name : str , dist_dir : Path ):
195+ binary_path = dist_dir / binary_name
196+ checksum_path = dist_dir / f"{ binary_name } .sha256"
197+
198+ if not binary_path .exists ():
199+ raise FileNotFoundError (f"Binary not found: { binary_path } " )
200+
201+ with open (checksum_path , "w" ) as f :
202+ subprocess .run (
203+ ["sha256sum" , binary_name ],
204+ cwd = dist_dir ,
205+ stdout = f ,
206+ check = True ,
207+ )
208+
209+ print (f"Created { checksum_path .name } in { dist_dir } " )
210+
211+
92212if __name__ == "__main__" :
93213 build_version_file ()
94- build ()
214+ build ()
0 commit comments