Skip to content

Commit b8931dc

Browse files
committed
refactor: improve error handling and code structure in build process
1 parent 2e3c665 commit b8931dc

File tree

2 files changed

+135
-159
lines changed

2 files changed

+135
-159
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,14 @@ jobs:
1919
strategy:
2020
matrix:
2121
os: [windows-latest, ubuntu-latest, macos-latest]
22-
python-version: ['3.9', '3.10', '3.11']
23-
exclude:
24-
- os: macos-latest
25-
python-version: '3.9'
26-
- os: macos-latest
27-
python-version: '3.10'
2822

2923
steps:
3024
- uses: actions/checkout@v4
3125

32-
- name: Set up Python ${{ matrix.python-version }}
26+
- name: Set up Python
3327
uses: actions/setup-python@v5
3428
with:
35-
python-version: ${{ matrix.python-version }}
29+
python-version: '3.11'
3630
cache: 'pip'
3731
cache-dependency-path: |
3832
requirements.txt
@@ -70,7 +64,7 @@ jobs:
7064
if: always()
7165
uses: actions/upload-artifact@v4
7266
with:
73-
name: test-results-${{ matrix.os }}-py${{ matrix.python-version }}
67+
name: test-results-${{ matrix.os }}
7468
path: |
7569
pytest.xml
7670
coverage.xml

build.py

Lines changed: 132 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -7,95 +7,109 @@
77
import shutil
88
import platform
99
from pathlib import Path
10-
import PyInstaller.__main__
10+
from typing import Dict, List, Tuple
1111

1212
def clean_build():
1313
"""Clean previous build artifacts"""
1414
dirs_to_clean = ['build', 'dist']
1515
for dir_name in dirs_to_clean:
16-
if os.path.exists(dir_name):
17-
shutil.rmtree(dir_name)
18-
print(f"Cleaned {dir_name} directory")
16+
try:
17+
if os.path.exists(dir_name):
18+
shutil.rmtree(dir_name)
19+
except Exception as e:
20+
print(f"Error cleaning {dir_name}: {e}")
1921

20-
def check_environment():
22+
def check_environment() -> bool:
2123
"""Check if build environment is properly configured"""
2224
if sys.version_info >= (3, 12):
23-
print("\nWARNING: You are using Python 3.12 or higher.")
24-
print("This version may have compatibility issues with PyInstaller.")
25-
print("Recommended: Use Python 3.9-3.11 for more reliable builds.")
26-
if input("\nContinue with build anyway? (y/n): ").lower() != 'y':
27-
return False
28-
return True
29-
30-
def create_spec_file():
31-
"""Create PyInstaller spec file based on platform"""
32-
print("Generating platform-specific spec file...")
33-
34-
is_windows = platform.system() == "Windows"
35-
is_macos = platform.system() == "Darwin"
25+
print("Warning: Python 3.12+ might have compatibility issues. Using 3.9-3.11 is recommended.")
3626

37-
# Get absolute paths and ensure they exist
38-
root_dir = os.path.dirname(os.path.abspath(__file__))
39-
assets_dir = os.path.join(root_dir, 'assets')
40-
cli_path = os.path.join(root_dir, 'networkmonitor', 'scripts', 'networkmonitor_cli.py') # Fixed path
27+
required_modules = ['PyInstaller', 'Flask', 'click', 'scapy']
28+
missing_modules = []
4129

42-
# Verify paths exist
43-
if not os.path.exists(cli_path):
44-
print(f"Error: networkmonitor_cli.py not found at {cli_path}")
45-
print("Current directory:", os.getcwd())
46-
print("Directory contents:", os.listdir(os.path.join(root_dir, 'networkmonitor', 'scripts')))
30+
for module in required_modules:
31+
try:
32+
__import__(module)
33+
except ImportError:
34+
missing_modules.append(module)
35+
36+
if missing_modules:
37+
print("Missing required modules:")
38+
for module in missing_modules:
39+
print(f" - {module}")
4740
return False
4841

49-
if not os.path.exists(assets_dir):
50-
os.makedirs(assets_dir, exist_ok=True)
51-
print(f"Created assets directory at {assets_dir}")
52-
53-
# Common options for all platforms
54-
datas = [
55-
(os.path.join(root_dir, 'assets'), 'assets')
56-
]
42+
return True
43+
44+
def get_platform_settings() -> Tuple[str, Dict]:
45+
"""Get platform-specific build settings"""
46+
system = platform.system().lower()
5747

58-
# Use absolute paths for icons
59-
icon_path = None
60-
if is_windows:
61-
icon_file = os.path.join(assets_dir, 'icon.ico')
62-
if os.path.exists(icon_file):
63-
icon_path = icon_file
64-
elif is_macos:
65-
icon_file = os.path.join(assets_dir, 'icon.icns')
66-
if os.path.exists(icon_file):
67-
icon_path = icon_file
48+
# Base settings common to all platforms
49+
base_settings = {
50+
'name': 'NetworkMonitor',
51+
'console': True,
52+
'debug': False,
53+
'noconfirm': True,
54+
'strip': True,
55+
'clean': True
56+
}
6857

69-
if icon_path:
70-
print(f"Using icon from: {icon_path}")
71-
else:
72-
print("Warning: No platform-specific icon found")
58+
# Platform-specific settings
59+
if system == 'windows':
60+
icon_file = 'assets/icon.ico'
61+
base_settings.update({
62+
'uac_admin': True,
63+
'win_private_assemblies': True,
64+
'win_no_prefer_redirects': True,
65+
'hiddenimports': ['win32com', 'win32com.shell', 'win32api', 'wmi']
66+
})
67+
elif system == 'darwin':
68+
icon_file = 'assets/icon.icns'
69+
base_settings['hiddenimports'] = ['pkg_resources.py2_warn']
70+
else: # Linux
71+
icon_file = 'assets/icon.ico' # Use .ico for Linux as well
72+
base_settings['hiddenimports'] = ['pkg_resources.py2_warn']
7373

74-
spec_content = f"""# -*- mode: python ; coding: utf-8 -*-
75-
import os
74+
return icon_file, base_settings
7675

77-
block_cipher = None
76+
def create_spec_file() -> bool:
77+
"""Create PyInstaller spec file based on platform"""
78+
try:
79+
# Get platform-specific settings
80+
icon_file, settings = get_platform_settings()
81+
82+
if not os.path.exists(icon_file):
83+
print(f"Warning: Icon file not found at {icon_file}")
84+
icon_file = None
85+
86+
# Convert settings to spec file content
87+
spec_content = """# -*- mode: python ; coding: utf-8 -*-
7888
79-
# Get absolute paths
80-
root_dir = os.path.dirname(os.path.abspath(SPECPATH))
89+
from PyInstaller.building.api import PYZ, EXE, COLLECT
90+
from PyInstaller.building.build_main import Analysis
91+
92+
datas = [
93+
('assets/*', 'assets'),
94+
('networkmonitor/web/build/*', 'web/build')
95+
]
8196
8297
a = Analysis(
83-
[r'{cli_path}'], # Use raw string for Windows compatibility
84-
pathex=[root_dir],
98+
['networkmonitor/scripts/networkmonitor_cli.py'],
99+
pathex=[],
85100
binaries=[],
86-
datas={datas},
87-
hiddenimports=['scapy.layers.all', 'engineio.async_drivers.threading'],
101+
datas=datas,
102+
hiddenimports={hiddenimports},
88103
hookspath=[],
89104
hooksconfig={{}},
90105
runtime_hooks=[],
91-
excludes=['networkmonitor.web'], # Exclude web frontend
92-
win_no_prefer_redirects=False,
93-
win_private_assemblies=False,
94-
cipher=block_cipher,
106+
excludes=[],
107+
win_no_prefer_redirects={win_no_prefer_redirects},
108+
win_private_assemblies={win_private_assemblies},
95109
noarchive=False,
96110
)
97111
98-
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
112+
pyz = PYZ(a.pure, a.zipped_data)
99113
100114
exe = EXE(
101115
pyz,
@@ -104,111 +118,68 @@ def create_spec_file():
104118
a.zipfiles,
105119
a.datas,
106120
[],
107-
name='NetworkMonitor',
108-
debug=False,
109-
bootloader_ignore_signals=False,
110-
strip=False,
111-
upx=True,"""
112-
113-
if icon_path:
114-
spec_content += f"""
115-
icon=[r'{icon_path}'],"""
116-
117-
if is_macos:
118-
spec_content += """
119-
console=False,
120-
disable_windowed_traceback=False,
121-
argv_emulation=True,
122-
target_arch=None,
123-
codesign_identity=None,
124-
entitlements_file=None,
125-
info_plist={{
126-
'CFBundleShortVersionString': '1.0.0',
127-
'CFBundleVersion': '1.0.0',
128-
'CFBundleIdentifier': 'com.networkmonitor.app',
129-
'CFBundleName': 'NetworkMonitor',
130-
'CFBundleDisplayName': 'NetworkMonitor',
131-
'CFBundlePackageType': 'APPL',
132-
'CFBundleSignature': '????',
133-
'LSMinimumSystemVersion': '10.13',
134-
'NSHighResolutionCapable': True,
135-
}}
136-
)"""
137-
elif is_windows:
138-
spec_content += """
139-
console=True,
140-
disable_windowed_traceback=False,
141-
argv_emulation=False,
142-
target_arch=None,
143-
codesign_identity=None,
144-
entitlements_file=None,
145-
uac_admin=True,
146-
version='file_version_info.txt'
147-
)"""
148-
else: # Linux
149-
spec_content += """
150-
console=True,
121+
name='{name}',
122+
debug={debug},
123+
strip={strip},
124+
upx=True,
125+
runtime_tmpdir=None,
126+
console={console},
151127
disable_windowed_traceback=False,
152128
argv_emulation=False,
153129
target_arch=None,
154130
codesign_identity=None,
155-
)"""
156-
157-
if is_macos:
158-
spec_content += """
159-
app = BUNDLE(
160-
exe,
161-
name='NetworkMonitor.app',
162-
icon=os.path.join(assets_dir, 'icon.icns'),
163-
bundle_identifier='com.networkmonitor.app',
164-
info_plist={{
165-
'CFBundleShortVersionString': '1.0.0',
166-
'CFBundleVersion': '1.0.0',
167-
'CFBundleIdentifier': 'com.networkmonitor.app',
168-
'CFBundleName': 'NetworkMonitor',
169-
'CFBundleDisplayName': 'NetworkMonitor',
170-
'CFBundlePackageType': 'APPL',
171-
'CFBundleSignature': '????',
172-
'LSMinimumSystemVersion': '10.13',
173-
'NSHighResolutionCapable': True,
174-
}}
175-
)"""
176-
177-
try:
178-
with open('NetworkMonitor.spec', 'w') as f:
131+
entitlements_file=None,""".format(
132+
name=settings['name'],
133+
hiddenimports=settings.get('hiddenimports', []),
134+
win_no_prefer_redirects=settings.get('win_no_prefer_redirects', False),
135+
win_private_assemblies=settings.get('win_private_assemblies', False),
136+
debug=settings['debug'],
137+
strip=settings['strip'],
138+
console=settings['console']
139+
)
140+
141+
# Add icon if available
142+
if icon_file:
143+
spec_content += f"\n icon=['{icon_file}'],"
144+
145+
spec_content += "\n)"
146+
147+
# Write spec file
148+
with open('NetworkMonitor.spec', 'w', encoding='utf-8') as f:
179149
f.write(spec_content)
180-
print("Generated NetworkMonitor.spec file")
150+
151+
print("Generated NetworkMonitor.spec file\n")
181152
return True
153+
182154
except Exception as e:
183155
print(f"Error creating spec file: {e}")
184156
return False
185157

186-
def build_executable():
187-
"""Build the executable using PyInstaller with size optimizations"""
188-
if not check_environment():
189-
return False
190-
191-
# Clean previous builds
192-
clean_build()
193-
158+
def build_executable() -> bool:
159+
"""Build the executable using PyInstaller"""
194160
try:
195-
# Create spec file if it doesn't exist
196-
if not os.path.exists('NetworkMonitor.spec') and not create_spec_file():
197-
print("Failed to create spec file")
198-
return False
161+
import PyInstaller.__main__
199162

200-
print("\nBuilding executable with optimized settings...")
163+
# Clean previous build
164+
clean_build()
165+
166+
# Create spec file
167+
if not create_spec_file():
168+
return False
169+
170+
# Build with optimized settings
171+
print("Building executable with optimized settings...")
201172
PyInstaller.__main__.run([
202173
'NetworkMonitor.spec',
174+
'--noconfirm',
203175
'--clean',
204-
'--noconfirm'
176+
'--strip'
205177
])
206178

207-
print("\nBuild completed successfully!")
208179
return True
209180

210181
except Exception as e:
211-
print(f"\nError during build: {str(e)}")
182+
print(f"Error during build: {e}")
212183
print("\nTroubleshooting tips:")
213184
print("1. Use Python 3.9-3.11 instead of 3.12+")
214185
print("2. Run 'pip install -r requirements.txt' to update dependencies")
@@ -217,5 +188,16 @@ def build_executable():
217188
return False
218189

219190
if __name__ == '__main__':
220-
success = build_executable()
221-
sys.exit(0 if success else 1)
191+
try:
192+
if not check_environment():
193+
sys.exit(1)
194+
195+
if not build_executable():
196+
sys.exit(1)
197+
198+
print("\nBuild completed successfully!")
199+
sys.exit(0)
200+
201+
except Exception as e:
202+
print(f"\nUnexpected error: {e}")
203+
sys.exit(1)

0 commit comments

Comments
 (0)