Repository navigation
Expand file tree
/
Copy pathbuild.py
More file actions
executable file
·292 lines (233 loc) · 8.44 KB
/
Copy pathbuild.py
File metadata and controls
executable file
·292 lines (233 loc) · 8.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#!/usr/bin/env python3
"""
Build script for OpenHands CLI using PyInstaller.
This script packages the OpenHands CLI into a standalone executable binary
using PyInstaller with the custom spec file.
"""
import argparse
import os
import select
import shutil
import subprocess
import sys
import time
from pathlib import Path
from openhands_cli.utils import get_default_cli_agent, get_llm_metadata
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
from openhands.sdk import LLM
# =================================================
# SECTION: Build Binary
# =================================================
def clean_build_directories() -> None:
"""Clean up previous build artifacts."""
print('🧹 Cleaning up previous build artifacts...')
build_dirs = ['build', 'dist', '__pycache__']
for dir_name in build_dirs:
if os.path.exists(dir_name):
print(f' Removing {dir_name}/')
shutil.rmtree(dir_name)
# Clean up .pyc files
for root, _dirs, files in os.walk('.'):
for file in files:
if file.endswith('.pyc'):
os.remove(os.path.join(root, file))
print('✅ Cleanup complete!')
def check_pyinstaller() -> bool:
"""Check if PyInstaller is available."""
try:
subprocess.run(
['uv', 'run', 'pyinstaller', '--version'], check=True, capture_output=True
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
print(
'❌ PyInstaller is not available. Use --install-pyinstaller flag or install manually with:'
)
print(' uv add --dev pyinstaller')
return False
def build_executable(
spec_file: str = 'openhands.spec',
clean: bool = True,
) -> bool:
"""Build the executable using PyInstaller."""
if clean:
clean_build_directories()
# Check if PyInstaller is available (installation is handled by build.sh)
if not check_pyinstaller():
return False
print(f'🔨 Building executable using {spec_file}...')
try:
# Run PyInstaller with uv
cmd = ['uv', 'run', 'pyinstaller', spec_file, '--clean']
print(f'Running: {" ".join(cmd)}')
subprocess.run(cmd, check=True, capture_output=True, text=True)
print('✅ Build completed successfully!')
# Check if the executable was created
dist_dir = Path('dist')
if dist_dir.exists():
executables = list(dist_dir.glob('*'))
if executables:
print('📁 Executable(s) created in dist/:')
for exe in executables:
size = exe.stat().st_size / (1024 * 1024) # Size in MB
print(f' - {exe.name} ({size:.1f} MB)')
else:
print('⚠️ No executables found in dist/ directory')
return True
except subprocess.CalledProcessError as e:
print(f'❌ Build failed: {e}')
if e.stdout:
print('STDOUT:', e.stdout)
if e.stderr:
print('STDERR:', e.stderr)
return False
# =================================================
# SECTION: Test and profile binary
# =================================================
WELCOME_MARKERS = ['welcome', 'openhands cli', 'type /help', 'available commands', '>']
def _is_welcome(line: str) -> bool:
s = line.strip().lower()
return any(marker in s for marker in WELCOME_MARKERS)
def test_executable(dummy_agent) -> bool:
"""Test the built executable, measuring boot time and total test time."""
print('🧪 Testing the built executable...')
spec_path = os.path.join(PERSISTENCE_DIR, AGENT_SETTINGS_PATH)
specs_path = Path(os.path.expanduser(spec_path))
if specs_path.exists():
print(f'⚠️ Using existing settings at {specs_path}')
else:
print(f'💾 Creating dummy settings at {specs_path}')
specs_path.parent.mkdir(parents=True, exist_ok=True)
specs_path.write_text(dummy_agent.model_dump_json())
exe_path = Path('dist/openhands')
if not exe_path.exists():
exe_path = Path('dist/openhands.exe')
if not exe_path.exists():
print('❌ Executable not found!')
return False
try:
if os.name != 'nt':
os.chmod(exe_path, 0o755)
boot_start = time.time()
proc = subprocess.Popen(
[str(exe_path)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
env={**os.environ},
)
# --- Wait for welcome ---
deadline = boot_start + 60
saw_welcome = False
captured = []
while time.time() < deadline:
if proc.poll() is not None:
break
rlist, _, _ = select.select([proc.stdout], [], [], 0.2)
if not rlist:
continue
line = proc.stdout.readline()
if not line:
continue
captured.append(line)
if _is_welcome(line):
saw_welcome = True
break
if not saw_welcome:
print('❌ Did not detect welcome prompt')
try:
proc.kill()
except Exception:
pass
return False
boot_end = time.time()
print(f'⏱️ Boot to welcome: {boot_end - boot_start:.2f} seconds')
# --- Run /help then /exit ---
if proc.stdin is None:
print('❌ stdin unavailable')
proc.kill()
return False
proc.stdin.write('/help\n/exit\n')
proc.stdin.flush()
out, _ = proc.communicate(timeout=60)
total_end = time.time()
full_output = ''.join(captured) + (out or '')
print(f'⏱️ End-to-end test time: {total_end - boot_start:.2f} seconds')
if 'available commands' in full_output.lower():
print('✅ Executable starts, welcome detected, and /help works')
return True
else:
print('❌ /help output not found')
print('Output preview:', full_output[-500:])
return False
except subprocess.TimeoutExpired:
print('❌ Executable test timed out')
try:
proc.kill()
except Exception:
pass
return False
except Exception as e:
print(f'❌ Error testing executable: {e}')
try:
proc.kill()
except Exception:
pass
return False
# =================================================
# SECTION: Main
# =================================================
def main() -> int:
"""Main function."""
parser = argparse.ArgumentParser(description='Build OpenHands CLI executable')
parser.add_argument(
'--spec', default='openhands.spec', help='PyInstaller spec file to use'
)
parser.add_argument(
'--no-clean', action='store_true', help='Skip cleaning build directories'
)
parser.add_argument(
'--no-test', action='store_true', help='Skip testing the built executable'
)
parser.add_argument(
'--install-pyinstaller',
action='store_true',
help='Install PyInstaller using uv before building',
)
parser.add_argument(
'--no-build', action='store_true', help='Skip testing the built executable'
)
args = parser.parse_args()
print('🚀 OpenHands CLI Build Script')
print('=' * 40)
# Check if spec file exists
if not os.path.exists(args.spec):
print(f"❌ Spec file '{args.spec}' not found!")
return 1
# Build the executable
if not args.no_build and not build_executable(args.spec, clean=not args.no_clean):
return 1
# Test the executable
if not args.no_test:
dummy_agent = get_default_cli_agent(
llm=LLM(
model='dummy-model',
api_key='dummy-key',
litellm_extra_body={"metadata": get_llm_metadata(model_name='dummy-model', llm_type='openhands')},
)
)
if not test_executable(dummy_agent):
print('❌ Executable test failed, build process failed')
return 1
print('\n🎉 Build process completed!')
print("📁 Check the 'dist/' directory for your executable")
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as e:
print(e)
print('❌ Executable test failed')
sys.exit(1)