add build button

Build button is for linux only and needs testing
This commit is contained in:
Mattscreative 2025-04-04 18:50:43 -03:00
parent 1d25762097
commit 14b73f8acb

View file

@ -10,13 +10,116 @@ import psutil
import time
import threading
from concurrent.futures import ThreadPoolExecutor
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLineEdit, QLabel,
QFileDialog, QMessageBox, QTextEdit, QFrame,
QTabWidget, QProgressBar, QGroupBox, QGridLayout,
QCheckBox)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt6.QtGui import QPalette, QColor, QScreen
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
from PyQt5.QtGui import QPalette, QColor, QScreen
from pathlib import Path
import json
import shutil
def check_admin():
"""Check if the program is running with admin privileges."""
try:
return os.geteuid() == 0
except AttributeError:
# Windows platform
try:
return bool(os.getuid() & 0x4000) # Check for admin bit
except AttributeError:
return False
def restart_as_admin():
"""Restart the application with admin privileges."""
if platform.system() == "Windows":
import ctypes
if not ctypes.windll.shell32.IsUserAnAdmin():
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, " ".join(sys.argv), None, 1
)
return True
else:
if os.geteuid() != 0:
os.execvp('sudo', ['sudo', 'python3'] + sys.argv)
return True
return False
def get_linux_distro():
"""Detect the Linux distribution."""
try:
with open('/etc/os-release', 'r') as f:
lines = f.readlines()
info = {}
for line in lines:
if '=' in line:
key, value = line.strip().split('=', 1)
info[key] = value.strip('"')
# Get the ID and ID_LIKE fields
distro_id = info.get('ID', '').lower()
distro_like = info.get('ID_LIKE', '').lower()
# Handle special cases
if 'cachyos' in distro_id:
return 'cachyos'
elif 'pikaos' in distro_id:
return 'pikaos'
elif 'nobara' in distro_id:
return 'nobara'
return distro_id
except:
return ''
def get_package_manager_commands():
"""Get the appropriate package manager commands for the detected distro."""
distro = get_linux_distro()
commands = {
'ubuntu': {
'update': 'sudo apt update',
'install': 'sudo apt install -y build-essential curl git cmake pkg-config libvulkan-dev vulkan-tools spirv-tools libclang-dev clang llvm-dev python3-pip ninja-build',
'rust': 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
},
'debian': {
'update': 'sudo apt update',
'install': 'sudo apt install -y build-essential curl git cmake pkg-config libvulkan-dev vulkan-tools spirv-tools libclang-dev clang llvm-dev python3-pip ninja-build',
'rust': 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
},
'pikaos': {
'update': 'sudo apt update',
'install': 'sudo apt install -y build-essential curl git cmake pkg-config libvulkan-dev vulkan-tools spirv-tools libclang-dev clang llvm-dev python3-pip ninja-build',
'rust': 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y'
},
'arch': {
'update': 'sudo pacman -S --noconfirm base-devel git cmake vulkan-devel vulkan-tools spirv-tools clang llvm python-pip ninja rust',
'install': '', # Empty since we do it all in the update command
'rust': '' # Empty since rust is included in the update command
},
'cachyos': {
'update': 'sudo pacman -S --noconfirm base-devel git cmake vulkan-devel vulkan-tools spirv-tools clang llvm python-pip ninja rust',
'install': '', # Empty since we do it all in the update command
'rust': '' # Empty since rust is included in the update command
},
'manjaro': {
'update': 'sudo pacman -S --noconfirm base-devel git cmake vulkan-devel vulkan-tools spirv-tools clang llvm python-pip ninja rust',
'install': '', # Empty since we do it all in the update command
'rust': '' # Empty since rust is included in the update command
},
'fedora': {
'update': 'sudo dnf update -y',
'install': 'sudo dnf install -y gcc gcc-c++ git cmake pkgconfig vulkan-devel vulkan-tools spirv-tools clang-devel llvm-devel python3-pip ninja-build',
'rust': 'sudo dnf install -y rust cargo'
},
'nobara': {
'update': 'sudo dnf update -y',
'install': 'sudo dnf install -y gcc gcc-c++ git cmake pkgconfig vulkan-devel vulkan-tools spirv-tools clang-devel llvm-devel python3-pip ninja-build',
'rust': 'sudo dnf install -y rust cargo'
}
}
return commands.get(distro, {})
class DownloadThread(QThread):
"""Thread for downloading ZLUDA with progress tracking"""
@ -119,6 +222,219 @@ class ProcessMonitor(QThread):
def stop(self):
self.running = False
class BuildWorker(QThread):
progress = pyqtSignal(str)
finished = pyqtSignal(bool, str)
def __init__(self, parent=None):
super().__init__(parent)
self.process = None
def run_command(self, command, env=None, timeout=None):
"""Run a command and stream its output to the progress signal."""
try:
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
text=True,
bufsize=1,
universal_newlines=True
)
# Create threads to read stdout and stderr
def read_output(pipe, prefix):
for line in pipe:
line = line.strip()
if line: # Only emit non-empty lines
if "Cloning into" in line:
self.progress.emit(f"\n{prefix}Starting clone: {line}")
elif any(x in line for x in ["Receiving objects:", "Resolving deltas:", "Updating files:", "remote: Counting", "remote: Compressing"]):
self.progress.emit(f"\r{prefix}{line}") # Use \r for progress updates
else:
self.progress.emit(f"{prefix}{line}")
# Start threads for reading output
stdout_thread = threading.Thread(target=read_output, args=(process.stdout, ""))
stderr_thread = threading.Thread(target=read_output, args=(process.stderr, "[Info] "))
stdout_thread.start()
stderr_thread.start()
# Wait for process to complete
returncode = process.wait()
# Wait for output threads to finish
stdout_thread.join()
stderr_thread.join()
return returncode == 0
except Exception as e:
self.progress.emit(f"[Error] Failed to run command: {str(e)}")
return False
def run(self):
try:
# Get distro-specific commands
commands = get_package_manager_commands()
if not commands:
self.finished.emit(False, "Unsupported Linux distribution")
return
# Update package manager
self.progress.emit("=== Updating package manager ===")
if not self.run_command(commands['update']):
self.finished.emit(False, "Failed to update package manager")
return
# Install dependencies
self.progress.emit("\n=== Installing dependencies ===")
if not self.run_command(commands['install']):
self.finished.emit(False, "Failed to install dependencies")
return
# Install Rust if needed
self.progress.emit("\n=== Installing Rust ===")
if not self.run_command(commands['rust']):
self.finished.emit(False, "Failed to install Rust")
return
# Source cargo environment
self.progress.emit("\n=== Setting up Rust environment ===")
os.environ["PATH"] = f"{str(Path.home())}/.cargo/bin:{os.environ['PATH']}"
# Set up build directory in the GUI folder
gui_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(gui_dir, "zluda_build")
# Clean up any existing build directory
if os.path.exists(build_dir):
self.progress.emit("\n=== Cleaning up previous build ===")
try:
shutil.rmtree(build_dir)
except Exception as e:
self.progress.emit(f"[Warning] Failed to clean up previous build: {str(e)}")
# Create build directory
os.makedirs(build_dir, exist_ok=True)
# Clone ZLUDA into the build directory
self.progress.emit("\n=== Cloning ZLUDA repository ===")
if not self.run_command(f"git clone --progress https://github.com/vosen/ZLUDA.git {build_dir}"):
self.progress.emit("\n=== Cleaning up failed clone ===")
try:
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, "Failed to clone ZLUDA")
return
# Initialize and update git submodules
self.progress.emit("\n=== Initializing git submodules ===")
os.chdir(build_dir)
# Clone LLVM submodule with progress
self.progress.emit("\n=== Cloning LLVM (this may take a while) ===")
if not self.run_command("git submodule init ext/llvm-project && git submodule update --progress --depth 1 ext/llvm-project"):
self.progress.emit("\n=== Cleaning up failed LLVM clone ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, "Failed to clone LLVM")
return
# Update other submodules
self.progress.emit("\n=== Updating other submodules ===")
if not self.run_command("git submodule update --init --recursive --progress --depth 1 -- $(ls -d ext/* | grep -v llvm-project)"):
self.progress.emit("\n=== Cleaning up failed submodule initialization ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, "Failed to initialize other submodules")
return
# Build ZLUDA
self.progress.emit("\n=== Building ZLUDA ===")
build_env = os.environ.copy()
build_env["RUST_LOG"] = "debug"
build_env["RUST_BACKTRACE"] = "1"
if not self.run_command("cargo build --release", env=build_env):
self.progress.emit("\n=== Cleaning up failed build ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, "Failed to build ZLUDA")
return
# Find the built library
self.progress.emit("\n=== Locating built library ===")
lib_path = None
for path in Path(build_dir).rglob("*.so"):
if "target/release" in str(path) and "libzluda" in str(path):
lib_path = str(path)
self.progress.emit(f"Found library at: {lib_path}")
break
if not lib_path:
self.progress.emit("\n=== Cleaning up failed build ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, "Could not find built ZLUDA library")
return
# Copy the built library to the GUI directory
target_lib = os.path.join(gui_dir, "libzluda.so")
try:
shutil.copy2(lib_path, target_lib)
self.progress.emit(f"\nCopied library to: {target_lib}")
except Exception as e:
self.progress.emit("\n=== Cleaning up failed copy ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, f"Failed to copy library: {str(e)}")
return
# Clean up build directory
self.progress.emit("\n=== Cleaning up build directory ===")
try:
os.chdir(gui_dir)
shutil.rmtree(build_dir)
except Exception as e:
self.progress.emit(f"[Warning] Failed to clean up build directory: {str(e)}")
self.progress.emit("\n=== Build completed successfully! ===")
self.finished.emit(True, target_lib)
except Exception as e:
self.progress.emit(f"\n[Error] Build failed: {str(e)}")
# Try to clean up on any unexpected error
try:
gui_dir = os.path.dirname(os.path.abspath(__file__))
build_dir = os.path.join(gui_dir, "zluda_build")
os.chdir(gui_dir)
if os.path.exists(build_dir):
self.progress.emit("\n=== Cleaning up after error ===")
shutil.rmtree(build_dir)
except:
pass
self.finished.emit(False, str(e))
class ZLUDA_GUI(QMainWindow):
def __init__(self):
# Enable high DPI scaling
@ -129,31 +445,12 @@ class ZLUDA_GUI(QMainWindow):
super().__init__()
self.setWindowTitle("ZLUDA GUI")
# Get the primary screen
screen = QApplication.primaryScreen()
if screen:
# Get the screen's geometry and scale factor
geometry = screen.geometry()
scale_factor = screen.devicePixelRatio()
# Adjust scale factor to be more reasonable
scale_factor = min(scale_factor, 1.5) # Cap the scaling
# Set minimum size scaled according to DPI
self.setMinimumSize(900, 700)
# Calculate the window size based on screen size and scale factor
width = int(geometry.width() * 0.7) # 70% of screen width
height = int(geometry.height() * 0.7) # 70% of screen height
# Set the window size
self.resize(width, height)
# Center the window on screen
self.move(
int((geometry.width() - width) / 2),
int((geometry.height() - height) / 2)
)
# Initialize variables first
self.current_process = None
self.process_monitor = None
self.download_thread = None
self.download_progress = None
self.build_worker = None
# Initialize widgets
self.zluda_path = QLineEdit()
@ -161,212 +458,12 @@ class ZLUDA_GUI(QMainWindow):
self.libs_path = QLineEdit()
self.download_button = QPushButton("Download ZLUDA")
self.run_button = QPushButton("Run Application")
self.build_button = QPushButton("Build ZLUDA")
# Connect signals
self.download_button.clicked.connect(self.download_zluda)
self.run_button.clicked.connect(self.run_application)
# Adjust font sizes based on DPI
base_font_size = int(11 * scale_factor)
title_font_size = int(18 * scale_factor)
button_font_size = int(12 * scale_factor)
# Update the style sheet with adjusted sizes
self.setStyleSheet(f"""
/* Main window and widget backgrounds */
QMainWindow {{
background-color: #1a1a1a;
}}
QWidget {{
background-color: transparent;
color: #ffffff;
}}
/* Labels */
QLabel {{
color: #ffffff;
font-size: {base_font_size}px;
}}
/* Group boxes */
QGroupBox {{
background-color: #212121;
border: 1px solid #2a2a2a;
border-radius: 6px;
margin-top: 12px;
padding: 15px;
font-size: {base_font_size}px;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: 10px;
padding: 3px 6px;
color: #00e5ff;
font-weight: bold;
background-color: #212121;
}}
/* Frames */
QFrame {{
background-color: #212121;
border: 1px solid #2a2a2a;
border-radius: 6px;
margin: 2px 0;
}}
/* Line edits */
QLineEdit {{
background-color: #2a2a2a;
color: #ffffff;
border: 1px solid #333333;
border-radius: 4px;
padding: 6px 10px;
font-size: {base_font_size}px;
selection-background-color: #006064;
min-height: 24px;
}}
QLineEdit:focus {{
border: 2px solid #00e5ff;
background-color: #2d2d2d;
}}
QLineEdit:hover {{
background-color: #2d2d2d;
border: 1px solid #404040;
}}
/* Buttons */
QPushButton {{
border: none;
border-radius: 4px;
padding: 6px 16px;
font-size: {button_font_size}px;
font-weight: bold;
color: white;
background-color: #424242;
min-height: 28px;
}}
QPushButton:hover {{
background-color: #4a4a4a;
}}
QPushButton:pressed {{
background-color: #383838;
}}
QPushButton[cssClass="browse"] {{
background-color: #333333;
padding: 6px 12px;
min-width: 70px;
}}
QPushButton[cssClass="browse"]:hover {{
background-color: #3d3d3d;
}}
QPushButton[cssClass="browse"]:pressed {{
background-color: #2a2a2a;
}}
/* Checkboxes */
QCheckBox {{
spacing: 6px;
color: #ffffff;
font-size: {base_font_size}px;
min-height: 20px;
padding: 2px;
}}
QCheckBox::indicator {{
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid #404040;
background-color: #2a2a2a;
}}
QCheckBox::indicator:hover {{
border-color: #00e5ff;
background-color: #2d2d2d;
}}
QCheckBox::indicator:checked {{
background-color: #00e5ff;
border-color: #00e5ff;
}}
/* Tab widget */
QTabWidget::pane {{
border: 1px solid #2a2a2a;
border-radius: 6px;
background-color: #212121;
top: -1px;
}}
QTabBar::tab {{
background-color: #2a2a2a;
color: #b0b0b0;
padding: 8px 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
margin-right: 2px;
font-size: {base_font_size}px;
min-height: 24px;
min-width: 80px;
}}
QTabBar::tab:selected {{
background-color: #212121;
color: #00e5ff;
border: 1px solid #2a2a2a;
border-bottom: 2px solid #00e5ff;
}}
QTabBar::tab:hover:!selected {{
background-color: #333333;
color: #ffffff;
}}
/* Progress bar */
QProgressBar {{
border: 1px solid #2a2a2a;
border-radius: 4px;
background-color: #2a2a2a;
text-align: center;
font-size: {int(10 * scale_factor)}px;
color: white;
min-height: 20px;
}}
QProgressBar::chunk {{
background-color: #00e5ff;
border-radius: 3px;
}}
/* Text edit */
QTextEdit {{
background-color: #2a2a2a;
color: #ffffff;
border: 1px solid #333333;
border-radius: 4px;
padding: 8px;
font-size: {base_font_size}px;
line-height: 1.4;
selection-background-color: #006064;
}}
QTextEdit:focus {{
border: 2px solid #00e5ff;
}}
/* Scrollbars */
QScrollBar:vertical {{
border: none;
background: #2a2a2a;
width: 10px;
margin: 2px;
border-radius: 5px;
}}
QScrollBar::handle:vertical {{
background: #404040;
min-height: 20px;
border-radius: 5px;
}}
QScrollBar::handle:vertical:hover {{
background: #4a4a4a;
}}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
height: 0;
}}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{
background: none;
}}
""")
self.build_button.clicked.connect(self.build_zluda)
# Create main widget and layout
main_widget = QWidget()
@ -380,15 +477,15 @@ class ZLUDA_GUI(QMainWindow):
header.setSpacing(6)
title = QLabel("ZLUDA GUI")
title.setStyleSheet(f"""
font-size: {title_font_size}px;
title.setStyleSheet("""
font-size: 18px;
font-weight: bold;
color: #00e5ff;
""")
version = QLabel("v4.0")
version.setStyleSheet(f"""
font-size: {button_font_size}px;
version.setStyleSheet("""
font-size: 12px;
color: #808080;
padding-top: 4px;
padding-left: 6px;
@ -491,6 +588,7 @@ class ZLUDA_GUI(QMainWindow):
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(10)
# Set button styles
self.download_button.setStyleSheet("""
QPushButton {
background-color: #00796b;
@ -525,14 +623,39 @@ class ZLUDA_GUI(QMainWindow):
}
""")
self.build_button.setStyleSheet("""
QPushButton {
background-color: #2196f3;
min-width: 150px;
}
QPushButton:hover {
background-color: #42a5f5;
}
QPushButton:pressed {
background-color: #1976d2;
}
QPushButton:disabled {
background-color: #2d3436;
color: #666666;
}
""")
buttons_layout.addWidget(self.download_button)
buttons_layout.addWidget(self.run_button)
buttons_layout.addWidget(self.build_button)
buttons_layout.addStretch()
main_layout.addLayout(buttons_layout)
# Progress bar
# Progress bars
self.progress_bar = QProgressBar()
main_layout.addWidget(self.progress_bar)
self.build_progress = QProgressBar()
self.build_progress.setTextVisible(False)
self.build_progress.hide()
main_layout.addWidget(self.build_progress)
# Log areas
log_frame = QFrame()
log_layout = QVBoxLayout(log_frame)
@ -557,6 +680,9 @@ class ZLUDA_GUI(QMainWindow):
main_layout.addWidget(log_frame)
# Add main tab
tabs.addTab(main_tab, "Main")
# Debug tab
debug_tab = QWidget()
debug_layout = QVBoxLayout(debug_tab)
@ -586,27 +712,76 @@ class ZLUDA_GUI(QMainWindow):
debug_layout.addWidget(debug_log_frame)
# Add tabs
tabs.addTab(main_tab, "Main")
# Add debug tab
tabs.addTab(debug_tab, "Debug")
# Set window flags for proper window controls
self.setWindowFlags(
Qt.WindowType.Window |
Qt.WindowType.CustomizeWindowHint |
Qt.WindowType.WindowCloseButtonHint |
Qt.WindowType.WindowMinimizeButtonHint |
Qt.WindowType.WindowMaximizeButtonHint
Qt.Window |
Qt.CustomizeWindowHint |
Qt.WindowCloseButtonHint |
Qt.WindowMinimizeButtonHint |
Qt.WindowMaximizeButtonHint
)
# Maximize the window by default
self.setWindowState(Qt.WindowState.WindowMaximized)
# Apply dark theme stylesheet
self.setStyleSheet("""
QMainWindow, QWidget {
background-color: #1a1a1a;
color: #ffffff;
}
QGroupBox {
background-color: #212121;
border: 1px solid #2a2a2a;
border-radius: 6px;
margin-top: 12px;
padding: 15px;
}
QFrame {
background-color: #212121;
border: 1px solid #2a2a2a;
border-radius: 6px;
}
QLineEdit {
background-color: #2a2a2a;
border: 1px solid #333333;
border-radius: 4px;
padding: 6px 10px;
color: white;
}
QTextEdit {
background-color: #2a2a2a;
border: 1px solid #333333;
border-radius: 4px;
padding: 8px;
color: white;
}
QPushButton[cssClass="browse"] {
background-color: #333333;
padding: 6px 12px;
min-width: 70px;
}
QTabWidget::pane {
border: 1px solid #2a2a2a;
border-radius: 6px;
background-color: #212121;
}
QTabBar::tab {
background-color: #2a2a2a;
color: #b0b0b0;
padding: 8px 16px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
QTabBar::tab:selected {
background-color: #212121;
color: #00e5ff;
border-bottom: 2px solid #00e5ff;
}
""")
# Initialize variables
self.current_process = None
self.process_monitor = None
self.download_thread = None
self.download_progress = None
# Maximize the window by default
self.setWindowState(Qt.WindowMaximized)
def log(self, message):
"""Add a message to the main log area"""
@ -892,6 +1067,77 @@ class ZLUDA_GUI(QMainWindow):
# Reset process reference
self.current_process = None
def build_zluda(self):
"""Build ZLUDA from source."""
if not sys.platform.startswith('linux'):
QMessageBox.warning(self, "Error", "ZLUDA building is only supported on Linux")
return
# Check for admin privileges
if not check_admin():
reply = QMessageBox.question(
self,
"Admin Privileges Required",
"Installing dependencies requires administrator privileges. Would you like to restart the application as admin?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.log("Restarting with admin privileges...")
if restart_as_admin():
sys.exit(0) # Exit current instance
else:
QMessageBox.critical(self, "Error", "Failed to restart with admin privileges")
return
else:
return
reply = QMessageBox.question(
self,
"Build ZLUDA",
"This will install required dependencies and build ZLUDA from source. Continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
return
# Disable buttons during build
self.build_button.setEnabled(False)
self.download_button.setEnabled(False)
self.run_button.setEnabled(False)
# Show and start progress bar
self.build_progress.setRange(0, 0) # Indeterminate mode
self.build_progress.show()
# Start build process
self.build_worker = BuildWorker()
self.build_worker.progress.connect(self.update_build_progress)
self.build_worker.finished.connect(self.build_finished)
self.build_worker.start()
def update_build_progress(self, message):
"""Update build progress in log area."""
self.log_area.append(f"[Build] {message}")
def build_finished(self, success, result):
"""Handle build completion."""
# Re-enable buttons
self.build_button.setEnabled(True)
self.download_button.setEnabled(True)
self.run_button.setEnabled(True)
# Hide progress bar
self.build_progress.hide()
if success:
self.log_area.append("[Build] ZLUDA built successfully!")
self.zluda_path.setText(result)
else:
self.log_area.append(f"[Build] Error: {result}")
QMessageBox.warning(self, "Build Error", f"Failed to build ZLUDA: {result}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ZLUDA_GUI()