-
-
Notifications
You must be signed in to change notification settings - Fork 0
Asynchronous Build System
CppLab IDE uses a fully asynchronous build system to ensure the UI remains responsive during compilation. This document explains the architecture and implementation details.
Traditional IDEs often execute builds synchronously:
# ❌ Blocking approach (old method)
def on_build_project(self):
result = build_project(config, toolchains) # BLOCKS for 1-3 seconds
update_ui(result)Issues:
- UI freezes during compilation (1-3 seconds)
- User cannot interact with menus/windows
- Feels unresponsive and unprofessional
- Cannot cancel or monitor progress
┌─────────────────┐
│ MainWindow │ ← Main Thread (Qt Event Loop)
│ │
│ build_current()│
└────────┬────────┘
│ creates
↓
┌─────────────────┐
│ QThread │ ← Background Thread
│ │
│ BuildWorker │
│ .run() │
└────────┬────────┘
│ calls
↓
┌─────────────────┐
│ builder.py │ ← Core Build Logic
│ │
│ build_project() │
│ build_single_ │
│ _file() │
└─────────────────┘
Location: src/cpplab/app.py
class BuildWorker(QObject):
"""Worker that runs build/check operations in a background thread."""
# Signals (thread-safe communication)
started = pyqtSignal()
finished = pyqtSignal(object) # BuildResult
error = pyqtSignal(str)
def __init__(self, toolchains, project_config=None, source_path=None,
force_rebuild=False, check_only=False):
super().__init__()
self.toolchains = toolchains
self.project_config = project_config
self.source_path = source_path
self.force_rebuild = force_rebuild
self.check_only = check_only
@pyqtSlot()
def run(self):
"""Execute the build/check operation."""
try:
self.started.emit()
# Determine operation
if self.check_only:
if self.project_config:
result = check_project(self.project_config, self.toolchains)
else:
result = check_single_file(self.source_path, self.toolchains)
else:
if self.project_config:
result = build_project(self.project_config,
self.toolchains,
self.force_rebuild)
else:
result = build_single_file(self.source_path, self.toolchains)
self.finished.emit(result)
except Exception as e:
self.error.emit(str(e))Key Points:
- Inherits
QObject(not QThread) - modern PyQt6 pattern - Uses Qt signals for thread-safe communication
-
@pyqtSlot()decorator for proper slot connection - Handles both project and standalone builds
- Supports syntax-only checks (
check_only=True)
Location: src/cpplab/app.py
def start_build_task(self, *, project_config=None, source_path=None,
force_rebuild=False, check_only=False):
"""Start a background build/check task if none is running."""
# Prevent concurrent builds
if self.build_in_progress:
QMessageBox.information(self, "Build In Progress",
"A build is already running. Please wait for it to complete.")
return
# Save files before building
if not check_only:
self.on_save_all()
# Create thread and worker
thread = QThread(self)
worker = BuildWorker(
toolchains=self.toolchains,
project_config=project_config,
source_path=source_path,
force_rebuild=force_rebuild,
check_only=check_only
)
# Move worker to thread (critical!)
worker.moveToThread(thread)
# Connect signals
thread.started.connect(worker.run) # Start work when thread starts
worker.started.connect(self.on_build_started) # Update UI
worker.finished.connect(self.on_build_finished) # Handle result
worker.error.connect(self.on_build_error) # Handle errors
worker.finished.connect(thread.quit) # Stop thread
worker.finished.connect(worker.deleteLater) # Clean up worker
thread.finished.connect(thread.deleteLater) # Clean up thread
# Store reference and start
self.current_build_thread = thread
self.build_in_progress = True
thread.start()Thread Safety Pattern:
- Create
QThreadobject - Create
BuildWorker(QObject) -
Move worker to thread with
moveToThread() - Connect signals/slots
- Start thread with
thread.start()
@pyqtSlot()
def on_build_started(self):
"""Handle build start - update UI to show build in progress."""
self.statusBuildLabel.setText("Building...")
# Disable actions to prevent spam
self.buildProjectAction.setEnabled(False)
self.buildAndRunAction.setEnabled(False)
self.runProjectAction.setEnabled(False)
# Clear output and switch to Build tab
self.output_panel.clear_output()
self.output_panel.append_output("=== Build Started ===\n")
self.outputDockWidget.setVisible(True)
self.outputTabWidget.setCurrentIndex(0)@pyqtSlot(object)
def on_build_finished(self, result: BuildResult):
"""Handle build completion - update UI with results."""
# Re-enable actions
self.buildProjectAction.setEnabled(True)
self.buildAndRunAction.setEnabled(True)
self.runProjectAction.setEnabled(True)
self.build_in_progress = False
self.current_build_thread = None
# Display output
if result.command:
self.output_panel.append_output(f"\nCommand: {' '.join(result.command)}\n")
if result.stdout:
self.output_panel.append_output("\n--- Standard Output ---\n")
self.output_panel.append_output(result.stdout)
if result.stderr:
self.output_panel.append_output("\n--- Standard Error ---\n")
self.output_panel.append_output(result.stderr)
# Update status bar with timing
if result.success:
msg = "Build succeeded"
else:
msg = "Build failed"
if hasattr(result, "elapsed_ms") and self.settings.show_build_elapsed:
msg += f" in {result.elapsed_ms:.0f} ms"
if result.skipped:
msg = "Build skipped (up to date)"
self.statusBuildLabel.setText(msg)
# Handle Build & Run workflow
if result.success and self._pending_run_after_build:
self._pending_run_after_build = False
self.run_current()@pyqtSlot(str)
def on_build_error(self, message: str):
"""Handle build error."""
self.build_in_progress = False
self.current_build_thread = None
# Re-enable actions
self.buildProjectAction.setEnabled(True)
self.buildAndRunAction.setEnabled(True)
self.runProjectAction.setEnabled(True)
self.statusBuildLabel.setText("Build error")
QMessageBox.critical(self, "Build Error", message)How to build first, then run if successful?
def on_build_and_run(self):
"""Build and run current project or standalone file."""
self._pending_run_after_build = True
self.build_current()
# Later in on_build_finished:
if result.success and self._pending_run_after_build:
self._pending_run_after_build = False
self.run_current()Flow:
- User presses F5 (Build & Run)
- Set
_pending_run_after_build = True - Start async build
- Build completes →
on_build_finished() - Check flag, if true → call
run_current() -
run_current()uses non-blockingPopen(already async)
# State flag
self.build_in_progress: bool = False
# Check before starting
if self.build_in_progress:
QMessageBox.information(self, "Build In Progress",
"A build is already running. Please wait for it to complete.")
return
# Set flag
self.build_in_progress = True
thread.start()
# Clear flag when done
def on_build_finished(self, result):
self.build_in_progress = False# Worker auto-deletes
worker.finished.connect(worker.deleteLater)
# Thread auto-deletes
thread.finished.connect(thread.deleteLater)
# Reference cleared
self.current_build_thread = Nonedef closeEvent(self, event):
"""Handle application close event."""
if self.build_in_progress:
reply = QMessageBox.question(
self, "Build in progress",
"A build is currently running. Do you really want to exit?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
event.ignore()
return
super().closeEvent(event)def _setup_widgets(self):
# ...
# Add build status label to status bar
self.statusBuildLabel = QLabel("Ready")
self.statusbar.addPermanentWidget(self.statusBuildLabel)Ready
↓ (build starts)
Building...
↓ (build completes)
Build succeeded in 1234 ms
↓ (or if failed)
Build failed in 567 ms
↓ (or if skipped)
Build skipped (up to date)
Controlled by user settings:
if self.settings.show_build_elapsed:
msg += f" in {result.elapsed_ms:.0f} ms"[x] No freezing during compilation
[x] User can resize/move windows
[x] Menus remain accessible
[x] Professional user experience
[x] Real-time status updates
[x] Build timing visible
[x] Clear success/failure indication
[x] Elapsed time transparency
[x] Cannot start multiple builds
[x] Warns before closing during build
[x] Threads properly cleaned up
[x] Exception handling in worker
[x] Build & Run works seamlessly
[x] Can queue Run after Build
[x] Non-blocking Run (Popen)
[x] Build/Run buttons disabled during build
- Thread creation: ~10ms (one-time)
- Signal emission: <1ms (negligible)
- UI updates: ~5ms (batched by Qt)
- Total overhead: ~15ms (vs 1000-3000ms build time = <2%)
-
QThread: ~50KB per thread -
BuildWorker: ~10KB - Only 1 thread active at a time
- Auto-cleanup prevents leaks
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| UI Responsiveness | ❌ Freezes 1-3s | [x] Always responsive |
| User Experience | Poor | Professional |
| Build Timing | Hidden | Visible in status |
| Concurrent Builds | Possible (bad) | Prevented (good) |
| Code Complexity | Simple | Moderate |
| Thread Overhead | None | ~15ms (<2%) |
- Open large project
- Press F7 (Build)
- During build:
- Try resizing window [x]
- Try opening menus [x]
- Try starting another build (should be blocked) [x]
- Check status bar shows "Building..." [x]
- After build, check timing appears [x]
# Future: Test signal emissions
def test_build_worker_signals():
worker = BuildWorker(toolchains, project_config=config)
started_emitted = False
finished_emitted = False
def on_started():
nonlocal started_emitted
started_emitted = True
def on_finished(result):
nonlocal finished_emitted
finished_emitted = True
assert result.success
worker.started.connect(on_started)
worker.finished.connect(on_finished)
worker.run()
assert started_emitted
assert finished_emitted# Old PyQt style - NOT RECOMMENDED
class BuildThread(QThread):
def run(self):
result = build_project(...)Problem: Tight coupling, harder to test
# Modern PyQt6 style - RECOMMENDED
class BuildWorker(QObject):
def run(self):
result = build_project(...)
worker.moveToThread(thread)Benefit: Loose coupling, easier to test
# WRONG - crashes or undefined behavior
def run(self):
self.main_window.statusLabel.setText("Building...") # ❌# CORRECT - thread-safe
def run(self):
self.started.emit() # [x] Signal triggers UI update in main threadCurrently: Output displayed after build completes
Future: Stream stdout/stderr line-by-line during build
# Future: Real-time output
class BuildWorker(QObject):
output_line = pyqtSignal(str) # Emit each line
def run(self):
process = subprocess.Popen(cmd, stdout=PIPE, ...)
for line in process.stdout:
self.output_line.emit(line.decode())Currently: Build runs to completion
Future: Cancel button to terminate build
# Future: Cancellable builds
class BuildWorker(QObject):
def __init__(self):
self._cancelled = False
def cancel(self):
self._cancelled = True
if self._process:
self._process.terminate()Currently: Indeterminate "Building..." message
Future: Progress percentage for multi-file projects
Currently: One build at a time
Future: Queue multiple build requests
Next: Build System Details
Previous: Architecture Overview
💡 Found this wiki useful?
⭐ Star the repo
·
💖 Sponsor this project
·
📦 Latest release
·
🐞 Report an issue