7.6 KiB
Embedded Resources in Knoe Installer
Overview
The Knoe Installer package includes several embedded resources that must be accessible both when running from source and when packaged as a standalone binary.
Embedded Resources
1. Images (img/)
- knoeIcon.png (1.5 MB) - Application icon
- knoeLogo.png (2.2 MB) - Main logo
- knoeLogoSepia.png (2.7 MB) - Sepia background for GUI
- knoeLogoBlueprint.png (3.1 MB) - Blueprint variant
- knoeIconblueprint.png (1.6 MB) - Blueprint icon variant
2. Binary Executables
- scan/network-agent (6.8 MB) - Network scanner
- Universal binary (x86_64 + arm64)
- Used by network scan screen
- Detects Kerberos, Active Directory, etc.
3. Application Bundles
- knoe-app/dist/Knoe Tools.app (~12 MB) - Pre-built Knoe Tools
- Complete macOS .app bundle
- Used by installer creation screen
- Can be copied to DMG or USB installer
How Embedding Works
Resource Path Resolution
All resources use the get_resource_path() helper function:
def get_resource_path(relative_path):
"""Get absolute path to resource, works for dev and for PyInstaller."""
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = Path(sys._MEIPASS)
except AttributeError:
# Running from source
base_path = PROJECT_ROOT
return base_path / relative_path
Usage Examples
Image Loading:
bg_path = get_resource_path('img/knoeLogoSepia.png')
if bg_path.exists():
image = Image.open(str(bg_path))
Binary Execution:
scan_binary = get_resource_path("scan/network-agent")
if scan_binary.exists():
process = subprocess.Popen([str(scan_binary)], ...)
App Bundle Access:
app_src = get_resource_path('knoe-app/dist/Knoe Tools.app')
if app_src.exists():
# Copy to destination
shutil.copytree(app_src, dest)
PyInstaller Configuration
Spec File (scripts/generate_spec.py)
Data Files:
datas = [
('installer', 'installer'),
('conf', 'conf'),
('etc', 'etc'),
('img', 'img'),
('docs', 'docs'),
('knoe-app/dist/Knoe Tools.app', 'knoe-app/dist/Knoe Tools.app'),
]
Binaries:
binaries = [
('scan/network-agent', 'scan'),
]
The binaries list ensures executable permissions are preserved.
File Sizes
Total embedded resources: ~30-35 MB
Breakdown:
- Images: ~11 MB
- network-agent: 6.8 MB
- Knoe Tools.app: ~12 MB
- Other resources: ~5-10 MB
Final installer bundle: ~50-100 MB (includes Python runtime)
Usage in Installer
Network Scan Screen
The network scan screen uses network-agent to detect services:
scan_binary = get_resource_path("scan/network-agent")
process = subprocess.Popen([str(scan_binary)],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True)
Output is captured in real-time and displayed in the scan results text widget.
Installer Creation Screen
The installer creation screen copies Knoe Tools.app to DMG:
app_src = get_resource_path('knoe-app/dist/Knoe Tools.app')
if app_src.exists():
shutil.copytree(app_src, staging / 'Knoe Tools.app')
Testing
Verify Resources from Source
python3 test_embedded_resources.sh
This tests:
- ✓ All image files exist
- ✓ network-agent binary exists and is executable
- ✓ Knoe Tools.app bundle exists
- ✓ Spec file includes all resources
- ✓ Resource sizes
Verify Resources in Built Package
After building:
# Check extracted resources
./dist/knoe-installer --help
# In another terminal, while installer is running:
ls -la /tmp/_MEI*/scan/
ls -la /tmp/_MEI*/img/
ls -la "/tmp/_MEI*/knoe-app/dist/Knoe Tools.app"
Troubleshooting
Binary Not Found Error
Error: Scan binary not found at /var/folders/.../scan/network-agent
Cause: Binary not included in package or path not using get_resource_path()
Solution:
- Verify spec includes binary:
grep network-agent installer.spec - Check code uses
get_resource_path():grep "get_resource_path.*network-agent" install.py - Rebuild:
make clean && make package
Binary Not Executable
Error: Permission denied when running network-agent
Cause: Binary permissions not preserved in package
Solution:
Ensure binary is in binaries list (not datas) in spec file:
binaries = [
('scan/network-agent', 'scan'), # Correct - preserves +x
]
# NOT in datas:
# datas = [('scan/network-agent', 'scan')] # Wrong - loses +x
App Bundle Not Found
Error: Knoe Tools.app not found
Cause: App not built before packaging installer
Solution:
- Build Knoe Tools.app first (in knoe-app directory)
- Verify it exists:
ls "knoe-app/dist/Knoe Tools.app" - Then build installer:
make package
Large Bundle Size
Cause: Including large binary files significantly increases bundle size
Optimization Options:
-
Compress app bundle:
cd knoe-app/dist zip -r "Knoe Tools.zip" "Knoe Tools.app" # Include zip instead of .app -
Download on demand: Instead of embedding, download from server when needed
-
Exclude debug symbols: Strip binaries before packaging:
strip scan/network-agent
Build Process
When running make package:
- Icon conversion: PNG → ICNS
- Spec generation: Creates installer.spec with all resources
- PyInstaller:
- Analyzes install.py
- Collects dependencies
- Copies data files (preserves directory structure)
- Copies binaries (preserves execute permissions)
- Creates single-file executable
- Bundle creation: Packages into .app with icon
Resource Extraction at Runtime
When the packaged installer runs:
- PyInstaller extracts resources to
/tmp/_MEI<random>/ - Sets
sys._MEIPASSto extraction directory get_resource_path()uses_MEIPASSto find resources- Resources deleted automatically when installer exits
Security Considerations
Code Signing
Embedded binaries should be code signed:
codesign --force --sign "Developer ID Application: Your Name" scan/network-agent
Then build the installer - the signed binary will be included.
Verification
Users can verify embedded binaries:
# Check signature of network-agent after extraction
codesign --verify --verbose /tmp/_MEI*/scan/network-agent
# Check installer bundle signature
codesign --verify --verbose "dist/Knoe Installer.app"
Future Enhancements
Lazy Loading
For large resources, consider lazy loading:
def get_knoe_tools_app():
"""Download or extract Knoe Tools.app only when needed."""
app_path = get_resource_path('knoe-app/dist/Knoe Tools.app')
if not app_path.exists():
# Download from server
download_knoe_tools(app_path)
return app_path
Compression
Compress large resources:
# In spec file
datas = [
('knoe-app/dist/Knoe Tools.zip', 'knoe-app/dist'), # Compressed
]
# In code
def extract_knoe_tools():
zip_path = get_resource_path('knoe-app/dist/Knoe Tools.zip')
extract_dir = Path(tempfile.mkdtemp())
shutil.unpack_archive(zip_path, extract_dir)
return extract_dir / 'Knoe Tools.app'