- Katılım
- 6 Mayıs 2022
- Konular
- 48,291
- Mesajlar
- 48,601
- Tepkime puanı
- 75
- M2 Yaşı
- 3 yıl 11 ay 10 gün
- Trophy Puan
- 48
- M2 Yang
- 488,879
Merhaba TurkMMO Ailesi,
Metin2 sunucu dosyalarını Windows üzerinde derlerken hepimizin kabusu olan bir adıma kesin çözüm getiren bir araçla karşınızdayım: MySQL mysqlclient.lib kütüphanesini derlemek.
Artık saatlerce süren denemeler, anlamsız CMake hataları, eksik OpenSSL bağımlılıkları ve uyumsuz Visual Studio sürümleriyle uğraşmaya son! Bu araç, tüm bu karmaşık süreci sizin için tek tıkla hallediyor.
MySQL Builder Pro Nedir?
Bu program, MySQL kaynak kodunu (özellikle Metin2 için gerekli olan mysqlclient.lib dosyasını) Windows ortamında derleme işlemini otomatikleştiren, grafik arayüzlü (GUI) bir araçtır.
Ana Özellikleri:
Bu adımları eksiksiz yapmanız, programın sorunsuz çalışması için kritiktir.
A. Visual Studio 2022 Kurulumu
Derleme işlemleri için Microsoft'un C++ derleyicisi zorunludur.
Paylaştığım bu araç, Python ile yazılmıştır. Çalıştırmak için sisteminizde Python kurulu olmalıdır.
Tüm ön kurulumları tamamladıysanız, artık programı kullanmaya hazırsınız.
Adım 1: Programı Çalıştırma
Aşağıda paylaştığım kaynak kodunun tamamını kopyalayın. Masaüstünde yeni bir metin belgesi oluşturun, içine yapıştırın ve adını mysql_builder.py olarak kaydedin. Daha sonra bu dosyaya çift tıklayarak çalıştırın.
Adım 2: Gerekli Derleme Araçlarını Kurma (İlk Kullanım)
Program açıldığında, "Araç Durumu" panelinde birçok aracın "❌ Yok" olarak göründüğünü fark edeceksiniz.
MySQL derlemesi için OpenSSL kütüphanesi zorunludur.
Artık her şey hazır olduğuna göre, hangi mysqlclient.lib dosyasını istediğimizi seçeceğiz.
MySQL Builder Pro ile Metin2 Geliştirme Sürecini Kolaylaştırın
Metin2 özel sunucu geliştirme sürecinde karşılaşılan en yaygın sorunlardan birisi derleme süreçleridir. Özellikle C++ tabanlı sistemlerde, MySQL bağlantıları sırasında 'mysqlclient.lib' hatası gibi derleme sorunları geliştiricileri zorlayabilir. Bu yazıda, [Dev Paylaşım] MySQL mysqlclient.lib Derleme Sorununa Son! adlı projeyi tanıtarak, bu sorunu kalıcı olarak nasıl çözeceğinizi anlatıyoruz.
Metin2 Geliştirme Ortamında MySQL Entegrasyonu
Metin2 özel sunucularında genellikle auth server ve game server olmak üzere iki temel yapı bulunur. Bu yapılar genellikle MySQL veritabanı ile haberleşir. Ancak, özellikle C++ kaynak kodlar üzerinde çalışırken MySQL bağlantısı sırasında bazı derleme hatalarıyla karşılaşılabilir. Bunlardan birisi 'mysqlclient.lib' dosyasının bulunamaması veya eksik derlenmesidir. Bu durum, proje derlenirken linker hatasına neden olur.
MySQL Builder Pro Nedir?
MySQL Builder Pro, Metin2 özel sunucu geliştiricileri için özel olarak tasarlanmış bir araçtır. Bu aracın temel amacı, MySQL kütüphanelerini doğru şekilde derlemek ve geliştiricilere kolayca entegre edilebilir bir yapı sunmaktır. mysqlclient.lib dosyasının derleme sürecini otomatikleştirerek, geliştiricilerin zaman kaybetmeden çalışmaya devam etmelerini sağlar.
mysqlclient.lib Derleme Sorunu Neden Oluşur?
C++ projelerinde MySQL ile bağlantı kurarken, derleyici mysqlclient.lib dosyasını bekler. Bu dosya, MySQL istemci kütüphanesinin derlenmiş halidir. Eğer doğru bir şekilde yapılandırılmazsa, linker hatası alınır. Bu durum özellikle farklı MySQL sürümleriyle çalışılırken daha sık ortaya çıkar. Ayrıca, 32-bit ve 64-bit sistem uyumsuzlukları da bu hatayı tetikleyebilir.
MySQL Builder Pro ile Çözüm
MySQL Builder Pro, bu sorunu adım adım otomatikleştirerek çözer. Kullanıcı dostu arayüzü sayesinde, sadece birkaç tıklama ile mysqlclient.lib dosyası doğru şekilde derlenebilir. Bu sayede Metin2 sunucularında auth-server ve game-server bağlantıları daha hızlı ve güvenli bir şekilde sağlanabilir. Arayüz üzerinden MySQL versiyon seçimi, derleme türü (debug/release) ve sistem mimarisi (x86/x64) ayarlanabilmektedir.
Metin2 Sunucu Geliştirme İçin Önerilen Yapılandırmalar
Metin2 özel sunucu geliştirme yaparken dikkat etmeniz gereken bazı önemli konfigürasyonlar vardır. Öncelikle, MySQL sürümünüzle derleme sırasında kullandığınız kütüphane sürümlerinin uyumlu olması gerekir. MySQL Builder Pro bu uyumu sağlamanız için rehberlik eder. Ayrıca, Visual Studio sürümü, Windows SDK ve compiler ayarları da önemli rol oynar. mysqlclient.lib dosyasının doğru yola yerleştirilmesi, linker ayarlarının yapılması ve include dizinlerinin belirlenmesi gibi işlemler, MySQL Builder Pro sayesinde otomatik hale getirilmiştir.
Metin2 Geliştiricileri İçin Kaynak Kod Paylaşımları
Metin2 geliştirme dünyasında kaynak kod paylaşımı, topluluk tarafından büyük önem taşır. Martysama gibi isimlerin sağladığı C++ sistemler, Python GUI uygulamaları ve Py Root yapıları, birçok geliştiriciye ilham vermiştir. MySQL Builder Pro da bu doğrultuda, geliştiricilerin daha az derleme stresi yaşaması için geliştirilmiş değerli bir araçtır. Bu tip paylaşımlar sayesinde, Metin2 özel sunucu geliştirme süreci hız kazanır ve hata oranı düşer.
Sonuç
Metin2 özel sunucu geliştirme sürecinde mysqlclient.lib gibi derleme hataları, geliştirici motivasyonunu düşürebilir. Ancak, MySQL Builder Pro gibi araçlar sayesinde bu tür sorunlar minimize edilebilir. Hem yeni başlayanlar hem de deneyimli geliştiriciler için büyük kolaylık sağlayan bu araç, Metin2 gelişimine katkı sağlarken, geliştirme süresini kısaltmaktadır. Eğer Metin2 özel sunucu geliştiriyorsanız ve MySQL ile ilgili derleme sorunları yaşıyorsanız, mutlaka MySQL Builder Pro'yu denemelisiniz.
Kaynak: Metin2 Lobby
Simplify Your Metin2 Development Process with MySQL Builder Pro
One of the most common issues encountered during Metin2 private server development is related to compilation processes. Especially in C++ based systems, errors like 'mysqlclient.lib' during MySQL connections can be challenging for developers. In this article, we introduce the project [Dev Sharing] End to MySQL mysqlclient.lib Compilation Issue! and explain how to permanently resolve this issue.
MySQL Integration in Metin2 Development Environment
In Metin2 private servers, two main structures usually exist: the auth server and the game server. These structures often communicate with MySQL databases. However, while working on C++ source codes, certain compilation errors may occur during MySQL connections. One such error is the failure to locate or incorrectly compile the 'mysqlclient.lib' file, which causes a linker error during project compilation.
What is MySQL Builder Pro?
MySQL Builder Pro is a tool specifically designed for Metin2 private server developers. Its main purpose is to correctly compile MySQL libraries and provide an easily integrable structure for developers. By automating the compilation process of the mysqlclient.lib file, it allows developers to continue their work without wasting time.
Why Does the mysqlclient.lib Compilation Error Occur?
When connecting to MySQL in C++ projects, the compiler expects the mysqlclient.lib file. This file is the compiled version of the MySQL client library. If not configured properly, a linker error occurs. This situation arises more frequently when working with different MySQL versions. Additionally, 32-bit and 64-bit system incompatibilities can trigger this error.
Solution with MySQL Builder Pro
MySQL Builder Pro resolves this issue by automatically streamlining the process with just a few clicks. Thanks to its user-friendly interface, the mysqlclient.lib file can be compiled correctly in no time. This enables faster and safer connections between auth-server and game-server in Metin2 servers. Users can select MySQL versions, compilation types (debug/release), and system architectures (x86/x64) through the interface.
Recommended Configurations for Metin2 Server Development
There are several important configurations to consider when developing Metin2 private servers. First, ensure that your MySQL version matches the library versions used during compilation. MySQL Builder Pro guides you in achieving this compatibility. Additionally, the Visual Studio version, Windows SDK, and compiler settings play crucial roles. With MySQL Builder Pro, tasks such as placing the mysqlclient.lib file in the correct path, setting up linker options, and defining include directories are automated.
Source Code Sharing for Metin2 Developers
In the world of Metin2 development, sharing source code is highly valued by the community. Systems provided by figures like Martysama, including C++ systems, Python GUI applications, and Py Root structures, have inspired many developers. MySQL Builder Pro, following this direction, helps reduce compilation stress for developers. Such shares accelerate the development process and decrease error rates in Metin2 private server creation.
Conclusion
Compilation errors like mysqlclient.lib during Metin2 private server development can lower developer motivation. However, tools like MySQL Builder Pro can minimize these issues. Providing great convenience for both newcomers and experienced developers, this tool contributes to Metin2's development while shortening the overall development timeline. If you're developing Metin2 private servers and experiencing MySQL-related compilation issues, give MySQL Builder Pro a try.
Source: Metin2 Lobby
Metin2 sunucu dosyalarını Windows üzerinde derlerken hepimizin kabusu olan bir adıma kesin çözüm getiren bir araçla karşınızdayım: MySQL mysqlclient.lib kütüphanesini derlemek.
Artık saatlerce süren denemeler, anlamsız CMake hataları, eksik OpenSSL bağımlılıkları ve uyumsuz Visual Studio sürümleriyle uğraşmaya son! Bu araç, tüm bu karmaşık süreci sizin için tek tıkla hallediyor.
MySQL Builder Pro Nedir?
Bu program, MySQL kaynak kodunu (özellikle Metin2 için gerekli olan mysqlclient.lib dosyasını) Windows ortamında derleme işlemini otomatikleştiren, grafik arayüzlü (GUI) bir araçtır.
Ana Özellikleri:
- <li data-xf-list-type="ul">Tam Otomasyon: Git, CMake, Ninja, Perl gibi gerekli tüm araçları kendisi indirir ve kurar. <li data-xf-list-type="ul">Grafik Arayüz: Tüm ayarları (Release/Debug, MT/MD, MySQL sürümü) kolayca seçmenizi sağlar. <li data-xf-list-type="ul">OpenSSL Desteği: Derleme için zorunlu olan OpenSSL kütüphanesini kaynak kodundan sizin için derler ve yapılandırır. <li data-xf-list-type="ul">Hata Kontrolü: Süreci anlık olarak log ekranından takip edebilir, olası hataları anında görebilirsiniz. <li data-xf-list-type="ul">Esneklik: Hem statik (/MT - redist paketi gerektirmez) hem de dinamik (/MD) runtime seçenekleriyle derleme yapabilirsiniz.
1. Gerekli Kurulumlar (Programa Geçmeden Önce Mutlaka Yapılmalı!)
Bu adımları eksiksiz yapmanız, programın sorunsuz çalışması için kritiktir.
A. Visual Studio 2022 Kurulumu
Derleme işlemleri için Microsoft'un C++ derleyicisi zorunludur.
- <li data-xf-list-type="ol">İndirme: linkinden "Community" sürümünü indirin. <li data-xf-list-type="ol">Kurulum: İndirdiğiniz yükleyiciyi çalıştırın. Karşınıza gelen "İş Yükleri" (Workloads) ekranında mutlaka aşağıdaki seçeneği işaretleyin:
- <li data-xf-list-type="ul">C++ ile masaüstü geliştirme (Desktop development with C++) <li data-xf-list-type="ul">Bu seçeneği işaretlemezseniz program derleme yapamaz! Kurulum tamamlandıktan sonra başka bir işlem yapmanıza gerek yok.
Paylaştığım bu araç, Python ile yazılmıştır. Çalıştırmak için sisteminizde Python kurulu olmalıdır.
- <li data-xf-list-type="ol">İndirme: linkinden en güncel Python sürümünü (örn: 3.12.x) indirin. <li data-xf-list-type="ol">Kurulum: Yükleyiciyi çalıştırın. ÇOK ÖNEMLİ: Kurulumun ilk ekranında, altta bulunan "Add Python.exe to PATH" kutucuğunu mutlaka işaretleyin. Bu adımı atlarsanız program çalışmaz.
- <li data-xf-list-type="ol">Windows'ta Başlat menüsünü açın. <li data-xf-list-type="ol">cmd yazarak "Komut İstemi"ni (Command Prompt) çalıştırın. <li data-xf-list-type="ol">Açılan siyah ekrana aşağıdaki komutu kopyalayıp yapıştırın ve Enter'a basın:
Kod:pip install requests ttkbootstrap psutil
Bu komut, programın ihtiyaç duyduğu ek kütüphaneleri otomatik olarak yükleyecektir.
2. MySQL Builder Pro Kullanımı
Tüm ön kurulumları tamamladıysanız, artık programı kullanmaya hazırsınız.
Adım 1: Programı Çalıştırma
Aşağıda paylaştığım kaynak kodunun tamamını kopyalayın. Masaüstünde yeni bir metin belgesi oluşturun, içine yapıştırın ve adını mysql_builder.py olarak kaydedin. Daha sonra bu dosyaya çift tıklayarak çalıştırın.
Adım 2: Gerekli Derleme Araçlarını Kurma (İlk Kullanım)
Program açıldığında, "Araç Durumu" panelinde birçok aracın "❌ Yok" olarak göründüğünü fark edeceksiniz.
- <li data-xf-list-type="ol">Sağ taraftaki "Eksikleri Kur" butonuna tıklayın. <li data-xf-list-type="ol">Program, CMake, Git, Ninja gibi tüm gerekli araçları otomatik olarak programın bulunduğu dizine .tools adında bir klasör açıp içine indirecektir. Bu işlem internet hızınıza bağlı olarak birkaç dakika sürebilir. <li data-xf-list-type="ol">İşlem bittiğinde tablo güncellenecek ve tüm araçlar "✔️ Yüklü" olarak görünecektir.
MySQL derlemesi için OpenSSL kütüphanesi zorunludur.
- <li data-xf-list-type="ol">"OpenSSL Durumu" paneline bakın. <li data-xf-list-type="ol">Sağdaki "OpenSSL Kur (R+D)" butonuna tıklayın. <li data-xf-list-type="ol">Program, OpenSSL'i hem Release hem de Debug modları için derleyecektir. Bu işlem de biraz zaman alabilir. Bittiğinde durum tablosu "✔️ OK" olarak güncellenecektir.
Artık her şey hazır olduğuna göre, hangi mysqlclient.lib dosyasını istediğimizi seçeceğiz.
- <li data-xf-list-type="ul">MySQL Sürümü: Metin2 sunucu dosyalarınızla uyumlu olan MySQL sürümünü seçin. Genellikle mysql-5.x veya mysql-8.x sürümleri kullanılır. "Listele" butonu güncel sürümleri çeker. <li data-xf-list-type="ul">Runtime (ÇOK ÖNEMLİ):
- <li data-xf-list-type="ul">MT: Statik derleme yapar. Oluşturulan .lib dosyası, C++ runtime kütüphanelerini içinde barındırır. Bu sayede sunucunuza vcredist.exe gibi ek paketler kurmanıza gerek kalmaz. Genellikle en sorunsuz ve tavsiye edilen seçenektir. <li data-xf-list-type="ul">MD: Dinamik derleme yapar. Bu seçeneği kullanırsanız, oyun sunucunuzu çalıştıracağınız makinede uygun Visual C++ Redistributable paketinin kurulu olması gerekir.
- <li data-xf-list-type="ul">Release Derle: Canlı sunucular için kullanacağınız optimize edilmiş .lib dosyasını oluşturur. (İşaretli kalmalı) <li data-xf-list-type="ul">Debug Derle: Hata ayıklama modunda derleme yapar. Geliştiriciler için gereklidir. (İsteğe bağlı) <li data-xf-list-type="ul">Yalnız client kütüphanesi: Bu seçenek mutlaka işaretli kalmalıdır! Amacımız zaten sadece mysqlclient.lib derlemek.
- <li data-xf-list-type="ol">Tüm ayarları yaptıktan sonra sağ alttaki yeşil "Başlat" butonuna tıklayın. <li data-xf-list-type="ol">Program, seçtiğiniz MySQL sürümünü indirip derlemeye başlayacaktır. Süreci log ekranından ve ilerleme çubuklarından takip edebilirsiniz. <li data-xf-list-type="ol">Derleme başarıyla tamamlandığında bir bildirim alacaksınız. <li data-xf-list-type="ol">"Install Klasörünü Aç" butonuna tıklayın. <li data-xf-list-type="ol">Karşınıza gelen install klasörünün içinde, yaptığınız seçimlere göre (örn: MT/mysql-8.0.38/release/lib/) mysqlclient.lib dosyanızı bulabilirsiniz.
3. Programın Kaynak Kodu
Kod:
# -*- coding: utf-8 -*- import os import re import sys import time import shutil import uuid import zipfile import threading import queue import subprocess import tarfile import contextlib import json import datetime import traceback from pathlib import Path from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from enum import Enum from typing import Callable, Any, cast try: import requests import psutil import ttkbootstrap as ttk import tkinter as tk from ttkbootstrap.constants import * from tkinter import font as tkFont, messagebox, scrolledtext, filedialog except ImportError: raise SystemExit("Gerekli paketler eksik. Lütfen çalıştırın: pip install requests ttkbootstrap psutil") class C: class BuildType(str, Enum): RELEASE = "Release" DEBUG = "Debug" RELWITHDEBINFO = "RelWithDebInfo" class Runtime(str, Enum): MT = "MT" MD = "MD" class ToolMode(str, Enum): BUNDLED = "bundled" SYSTEM = "system" class Generator(str, Enum): NINJA = "Ninja" VS17 = "VS17" class LogLevel(str, Enum): STAGE = "stage" CMD = "cmd" WARN = "warn" ERR = "err" INFO = "info" ROOT = Path.cwd() LOGS = ROOT / "logs" TOOLS = ROOT / ".tools" DL = ROOT / "downloads" SRC = ROOT / "src" BUILD = ROOT / "build" INST = ROOT / "install" DEPS = ROOT / ".deps" TRASH = ROOT / ".trash" OPENSSL_BASE = DEPS / "openssl" OPENSSL_SRC = DEPS / "openssl-src" CONFIG_FILE = ROOT / "config.json" DEFAULT_JOBS = max(1, (os.cpu_count() or 4) - 2) DISK_MIN_GB = 5.0 MAX_LOG_LINES = 5000 _RE_TAG = re.compile(r"^mysql-\d+\.\d+(?:\.\d+)?$") _RE_NINJA = re.compile(r"\[(\d+)\s*/\s*(\d+)\]") _RE_ERR_MULTI_RULE = re.compile(r"multiple rules generate", re.I) URLS = { "7zr": "https://www.7-zip.org/a/7zr.exe", "7zextra": "https://www.7-zip.org/a/7z2408-extra.7z", "mingit": "https://github.com/git-for-windows/git/releases/download/v2.46.0.windows.1/MinGit-2.46.0-64-bit.zip", "cmake": "https://github.com/Kitware/CMake/releases/download/v3.30.3/cmake-3.30.3-windows-x86_64.zip", "ninja": "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-win.zip", "perl": "https://strawberryperl.com/download/5.32.1.1/strawberry-perl-5.32.1.1-64bit-portable.zip", "nasm": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/win64/nasm-2.16.03-win64.zip", "openssl": "https://github.com/openssl/openssl/archive/refs/tags/openssl-3.3.1.zip", "sccache": "https://github.com/mozilla/sccache/releases/download/v0.8.0/sccache-v0.8.0-x86_64-pc-windows-msvc.tar.gz", } BUNDLED_PATHS = [TOOLS, TOOLS / "7zip", TOOLS / "cmake" / "bin", TOOLS / "mingit" / "cmd", TOOLS / "perl" / "perl" / "bin", TOOLS / "perl" / "c" / "bin", TOOLS / "nasm"] @dataclass class BuildConfig: build_type: C.BuildType source_dir: Path tag: str tool_mode: C.ToolMode generator: C.Generator runtime: C.Runtime cxx_version: str jobs: int extra_defs: str only_client: bool @property def build_dir(self) -> Path: return C.BUILD / self.runtime.value / self.tag / self.build_type.value.lower().replace(' ', '_') @property def install_dir(self) -> Path: return C.INST / self.runtime.value / self.tag / self.build_type.value.lower().replace(' ', '_') @property def ssl_root_dir(self) -> Path: return self._openssl_root(self.runtime, self.build_type) @staticmethod def _openssl_root(runtime: C.Runtime, build_type: C.BuildType) -> Path: is_debug = build_type == C.BuildType.DEBUG base = C.OPENSSL_BASE / runtime.value root = base / ("debug" if is_debug else "release") root.mkdir(parents=True, exist_ok=True) return root class Bus: def __init__(self, app: 'App'): self.app = app; self.q = app.q def log(self, message: str, level: C.LogLevel = C.LogLevel.INFO) -> None: self.q.put(("log", message, level.value)) def progress(self, kind: str, pc: int, text: str = "") -> None: self.q.put((kind, pc, text)) def status(self, text: str) -> None: self.q.put(("status", text)) def update_task_progress(self, task_id: str, progress_text: str) -> None: self.q.put(("task_progress", task_id, progress_text)) @contextlib.contextmanager def temp_env_path(extra_paths: list[Path]): old_path = os.environ.get("PATH", "") new_path_parts = [str(p) for p in extra_paths if p.exists()] new_path_parts.append(old_path) new_path = ";".join(new_path_parts) try: os.environ["PATH"] = new_path yield finally: os.environ["PATH"] = old_path def run_out(cmd: list[str] | str, cwd: Path | None = None) -> str: try: return subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True, errors="replace", shell=isinstance(cmd, str), creationflags=subprocess.CREATE_NO_WINDOW, cwd=cwd, timeout=15).strip() except subprocess.TimeoutExpired: return "ERR:Timeout" except Exception as e: return f"ERR:{e}" def stream_proc(cmd: list[str] | str, env: dict, on_line: Callable[[str], None], log_file_path: Path | None = None, low_pri: bool = True, stop_event: threading.Event | None = None, cwd: str | Path | None = None, on_start: Callable[[subprocess.Popen], None] | None = None) -> bool: log_file = None try: if log_file_path: log_file_path.parent.mkdir(parents=True, exist_ok=True) log_file = open(log_file_path, "w", encoding="utf-8", errors="replace") creation_flags = 0 if os.name == "nt" and low_pri: creation_flags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0) creation_flags |= getattr(subprocess, "CREATE_NO_WINDOW", 0) p = subprocess.Popen(cmd, shell=isinstance(cmd, str), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, encoding="utf-8", errors="replace", bufsize=1, env=env, creationflags=creation_flags, cwd=cwd) if on_start: on_start(p) for line in iter(p.stdout.readline, ''): if stop_event and stop_event.is_set(): p.terminate() on_line("--- Process terminated by user request. ---") break if line: clean_line = line.rstrip() on_line(clean_line) if log_file: log_file.write(clean_line + '\n') return p.wait() == 0 finally: if log_file: log_file.close() class SevenZipProgressParser: _RE_PERCENT = re.compile(r"(\d+)\s*\%") def __init__(self, bus: Bus): self.bus = bus; self.last_progress_update = -1 def parse_line(self, line: str) -> None: match = self._RE_PERCENT.search(line) if match: pc = int(match.group(1)) if pc > self.last_progress_update: self.bus.progress("work", pc, f"%{pc} çıkarılıyor...") self.last_progress_update = pc if pc % 10 == 0: self.bus.log(line, C.LogLevel.CMD) else: self.bus.log(line, C.LogLevel.CMD) class TaskManager: def __init__(self, app: 'App'): self.app = app self.bus = app.bus self.task_registry: dict[str, dict[str, Any]] = {} def register(self, name: str, target: Callable, args: tuple = (), kwargs: dict | None = None, stop_event: threading.Event | None = None, is_build_task: bool = True, task_id: str | None = None) -> threading.Thread: task_id = task_id or f"task_{uuid.uuid4().hex[:8]}" kwargs = kwargs or {} if "task_id" in target.__code__.co_varnames: kwargs['task_id'] = task_id thread = threading.Thread(target=target, args=args, kwargs=kwargs, daemon=True, name=task_id) self.task_registry[task_id] = {'thread': thread, 'name': name, 'start_time': time.time(), 'stop_event': stop_event, 'is_build_task': is_build_task, 'progress': 'Başlatılıyor...', 'status': 'Başlatılıyor', 'success': None} thread.start() self.bus.log(f"Yeni görev başlatıldı: {name} (ID: {task_id})", C.LogLevel.INFO) return thread def update_monitor(self) -> None: task_tree = self.app.ui_manager.task_tree finished_tasks = {tid: info for tid, info in self.task_registry.items() if not info['thread'].is_alive()} running_tasks = {tid: info for tid, info in self.task_registry.items() if info['thread'].is_alive()} for task_id, info in finished_tasks.items(): if info.get('status') == 'Bitti': continue self.task_registry[task_id]['status'] = 'Bitti' if task_tree.exists(task_id): final_status = "Başarısız" if info.get('success') is False else "Bitti" task_tree.set(task_id, "status", final_status) task_tree.set(task_id, "progress", "Tamamlandı") if final_status == "Başarısız": task_tree.item(task_id, tags=("failed",)) if not info.get('is_build_task'): self.app.after(5000, lambda tid=task_id: task_tree.delete(tid) if task_tree.exists(tid) else None) for task_id, info in running_tasks.items(): elapsed = time.time() - info['start_time'] runtime_str = str(datetime.timedelta(seconds=int(elapsed))) status = "Durduruluyor" if info.get('stop_event') and info['stop_event'].is_set() else "Çalışıyor" values = (info['name'], status, info.get('progress', '...'), runtime_str) if task_tree.exists(task_id): task_tree.item(task_id, values=values) else: task_tree.insert("", "end", iid=task_id, values=values) task_tree.tag_configure("failed", background="#f8d7da", foreground="#842029") self.app.after(1000, self.update_monitor) def terminate_selected_task(self) -> None: task_tree = self.app.ui_manager.task_tree selected_items = task_tree.selection() if not selected_items: messagebox.showwarning("Uyarı", "Lütfen durdurmak için bir görev seçin.") return task_id = selected_items[0] if task_id in self.task_registry: task_info = self.task_registry[task_id] if task_info['thread'].is_alive(): if task_info.get('stop_event'): task_info['stop_event'].set() self.bus.log(f"Durdurma sinyali gönderildi: {task_info['name']}", C.LogLevel.WARN) messagebox.showinfo("Başarılı", f"'{task_info['name']}' görevine durdurma sinyali gönderildi.") else: messagebox.showerror("Hata", "Bu görev, güvenli bir şekilde durdurulmayı desteklemiyor.") else: messagebox.showwarning("Bilgi", "Seçilen görev artık çalışmıyor veya tamamlanmış.") def stop_all(self) -> None: self.bus.log("DURDURMA KOMUTU GÖNDERİLDİ! Tüm aktif görevler durduruluyor.", C.LogLevel.ERR) for task_info in self.task_registry.values(): if task_info['thread'].is_alive() and task_info.get('stop_event'): task_info['stop_event'].set() self.bus.log(f"GÖREV DURDURULDU: {task_info['name']}", C.LogLevel.WARN) active_pids = self.app.build_manager.get_active_pids() if active_pids: self.bus.log(f"Sonlandırılacak {len(active_pids)} alt süreç bulundu.", C.LogLevel.ERR) for pid in active_pids: try: proc = psutil.Process(pid) proc.kill() self.bus.log(f"Süreç {pid} ({proc.name()}) başarıyla sonlandırıldı.", C.LogLevel.WARN) except psutil.NoSuchProcess: pass except psutil.Error as e: self.bus.log(f"Süreç {pid} sonlandırılamadı: {e}", C.LogLevel.ERR) self.app.build_manager.clear_active_pids() self.app.set_busy(False) class UIManager: def __init__(self, app: 'App'): self.app = app self.root = app self.bus = app.bus self.control_widgets: list[tk.Widget | ttk.Variable] = [] self.log_filter_vars: dict[str, tk.BooleanVar] = {} self.last_tag_from_config = None self._create_main_layout() self._create_config_widgets(self.top_frame) self._create_log_widgets(self.log_frame) self._create_task_panel(self.task_frame) self.load_settings() self.last_disk_io = psutil.disk_io_counters() self.last_update_time = time.time() def _create_main_layout(self) -> None: main_pane = ttk.PanedWindow(self.root, orient=VERTICAL) main_pane.pack(fill=BOTH, expand=True, padx=10, pady=10) self.top_frame = ttk.Frame(main_pane, padding=5) main_pane.add(self.top_frame, weight=0) bottom_pane = ttk.PanedWindow(main_pane, orient=HORIZONTAL) main_pane.add(bottom_pane, weight=1) self.log_frame = ttk.LabelFrame(bottom_pane, text="Log", padding=5) bottom_pane.add(self.log_frame, weight=1) self.task_frame = ttk.LabelFrame(bottom_pane, text="Arka Plan Görev Yöneticisi", padding=5) bottom_pane.add(self.task_frame, weight=0) def _register_control(self, widget: tk.Widget | ttk.Variable) -> Any: self.control_widgets.append(widget) return widget def _create_config_widgets(self, parent: ttk.Frame) -> None: parent.grid_columnconfigure(0, weight=1) build_panel = ttk.LabelFrame(parent, text="Derleme Ayarları", padding=10) build_panel.grid(row=0, column=0, sticky="ew") info_panels_frame = ttk.Frame(parent) info_panels_frame.grid(row=1, column=0, sticky="ew") info_panels_frame.grid_columnconfigure(0, weight=1) info_panels_frame.grid_columnconfigure(1, weight=1) dash = ttk.LabelFrame(parent, text="İşlem Durumu", padding=10) dash.grid(row=2, column=0, sticky="ew", pady=(10, 0)) self._create_build_panel_content(build_panel) self._create_tools_panel(info_panels_frame) self._create_ssl_panel(info_panels_frame) self._create_status_panel_content(dash) def _create_tools_panel(self, parent: ttk.Frame) -> None: tools_frame = ttk.LabelFrame(parent, text="Araç Durumu", padding=10) tools_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 5)) tools_frame.grid_columnconfigure(0, weight=1) self.tree = ttk.Treeview(tools_frame, columns=("tool", "status", "ver", "src", "path"), show="headings", height=6, bootstyle=INFO) for c, t, w in (("tool","Araç",100), ("status","Durum",80), ("ver","Sürüm",100), ("src","Kaynak",70), ("path","Yol",200)): self.tree.heading(c, text=t) self.tree.column(c, width=w, anchor=W) self.tree.grid(row=0, column=0, sticky="nsew") ysb = ttk.Scrollbar(tools_frame, orient="vertical", command=self.tree.yview) ysb.grid(row=0, column=1, sticky="ns") self.tree.configure(yscrollcommand=ysb.set) btn_frame = ttk.Frame(tools_frame) btn_frame.grid(row=0, column=2, sticky="ns", padx=(10, 0)) ttk.Button(btn_frame, text="Yenile", command=self.app.tool_manager.refresh_tools, width=20, bootstyle=SECONDARY).pack(fill=X, pady=2) ttk.Button(btn_frame, text="Eksikleri Kur", command=lambda: self.app.tool_manager.install_tools_async(missing=True), width=20).pack(fill=X, pady=2) ttk.Button(btn_frame, text="Hepsini Yeniden Kur", command=self.app.tool_manager.confirm_reinstall_all, width=20, bootstyle=OUTLINE).pack(fill=X, pady=2) def _create_ssl_panel(self, parent: ttk.Frame) -> None: ssl_frame = ttk.LabelFrame(parent, text="OpenSSL Durumu", padding=10) ssl_frame.grid(row=0, column=1, sticky="nsew", padx=(5, 0)) ssl_frame.grid_columnconfigure(0, weight=1) self.ssl_tree = ttk.Treeview(ssl_frame, columns=("component", "status"), show="headings", height=6, bootstyle=INFO) self.ssl_tree.heading("component", text="Yapılandırma") self.ssl_tree.column("component", width=250, anchor=W) self.ssl_tree.heading("status", text="Durum") self.ssl_tree.column("status", width=100, anchor=W) self.ssl_tree.grid(row=0, column=0, sticky="nsew") btn_frame = ttk.Frame(ssl_frame) btn_frame.grid(row=0, column=1, sticky="n", padx=(10, 0)) ttk.Button(btn_frame, text="OpenSSL Kur (R+D)", command=lambda: self.app.build_manager.build_openssl_async(reinstall=False), width=20).pack(fill=X, pady=2) ttk.Button(btn_frame, text="OpenSSL Yeniden Kur", command=self.app.build_manager.confirm_reinstall_openssl, width=20, bootstyle=OUTLINE).pack(fill=X, pady=2) ttk.Button(btn_frame, text="Klasörü Aç", command=lambda: os.startfile(C.OPENSSL_BASE), width=20, bootstyle=SECONDARY).pack(fill=X, pady=2) def _create_build_panel_content(self, p2: ttk.LabelFrame) -> None: p2.grid_columnconfigure(0, weight=2) p2.grid_columnconfigure(1, weight=2) p2.grid_columnconfigure(2, weight=1) source_frame = ttk.LabelFrame(p2, text="Kaynak ve Araçlar", padding=10) source_frame.grid(row=0, column=0, sticky="nsew", padx=2, pady=2) f_mysql = ttk.Frame(source_frame) f_mysql.pack(fill=X, pady=2) ttk.Label(f_mysql, text="MySQL Sürümü:").pack(side=LEFT) self.cmb_tag = self._register_control(ttk.Combobox(f_mysql, width=20, state="readonly")) self.cmb_tag.pack(side=LEFT, padx=5, fill=X, expand=True) ttk.Button(f_mysql, text="Listele", command=self.app.build_manager.fetch_tags_async, width=8, bootstyle=(SECONDARY, OUTLINE)).pack(side=LEFT) f_tools = ttk.Frame(source_frame) f_tools.pack(fill=X, pady=2) ttk.Label(f_tools, text="Araç Modu:").pack(side=LEFT, padx=(0, 10)) self.tool_mode = self._register_control(ttk.StringVar(value=C.ToolMode.BUNDLED.value)) self._register_control(ttk.Radiobutton(f_tools, text=".tools", variable=self.tool_mode, value=C.ToolMode.BUNDLED.value, command=self.app.tool_manager.refresh_tools)).pack(side=LEFT) self._register_control(ttk.Radiobutton(f_tools, text="Sistem", variable=self.tool_mode, value=C.ToolMode.SYSTEM.value, command=self.app.tool_manager.refresh_tools)).pack(side=LEFT, padx=5) params_frame = ttk.LabelFrame(p2, text="Parametreler", padding=10) params_frame.grid(row=0, column=1, sticky="nsew", padx=2, pady=2) params_frame.grid_columnconfigure(1, weight=1) ttk.Label(params_frame, text="C++ Sürümü:").grid(row=0, column=0, sticky="w", padx=5, pady=2) self.cxx_version = self._register_control(ttk.StringVar(value="Latest")) self._register_control(ttk.Combobox(params_frame, textvariable=self.cxx_version, values=["14", "17", "20", "Preview", "Latest"], width=12, state="readonly")).grid(row=0, column=1, sticky="ew", padx=5) ttk.Label(params_frame, text="Generator:").grid(row=1, column=0, sticky="w", padx=5, pady=2) self.generator = self._register_control(ttk.StringVar(value=C.Generator.NINJA.value)) self._register_control(ttk.Combobox(params_frame, textvariable=self.generator, values=[g.value for g in C.Generator], width=12, state="readonly")).grid(row=1, column=1, sticky="ew", padx=5) ttk.Label(params_frame, text="Runtime:").grid(row=2, column=0, sticky="w", padx=5, pady=2) self.runtime = self._register_control(ttk.StringVar(value=C.Runtime.MT.value)) self._register_control(ttk.Combobox(params_frame, textvariable=self.runtime, values=[r.value for r in C.Runtime], width=12, state="readonly")).grid(row=2, column=1, sticky="ew", padx=5) ttk.Label(params_frame, text="Paralel İş:").grid(row=2, column=2, sticky="w", padx=5, pady=2) self.jobs = self._register_control(ttk.IntVar(value=C.DEFAULT_JOBS)) self._register_control(ttk.Spinbox(params_frame, from_=1, to=128, textvariable=self.jobs, width=6)).grid(row=2, column=3, padx=5) options_frame = ttk.LabelFrame(p2, text="Seçenekler", padding=10) options_frame.grid(row=0, column=2, sticky="nsew", padx=2, pady=2) self.build_release = self._register_control(ttk.BooleanVar(value=True)) self._register_control(ttk.Checkbutton(options_frame, text="Release Derle", variable=self.build_release, bootstyle=PRIMARY)).pack(anchor=W, pady=1) self.build_debug = self._register_control(ttk.BooleanVar(value=True)) self._register_control(ttk.Checkbutton(options_frame, text="Debug Derle", variable=self.build_debug, bootstyle=PRIMARY)).pack(anchor=W, pady=1) ttk.Separator(options_frame, orient=HORIZONTAL).pack(fill=X, pady=5) self.only_client = self._register_control(ttk.BooleanVar(value=True)) self._register_control(ttk.Checkbutton(options_frame, text="Yalnız client kütüphanesi", variable=self.only_client, bootstyle=PRIMARY)).pack(anchor=W, pady=1) self.prefer_clean = self._register_control(ttk.BooleanVar(value=False)) self._register_control(ttk.Checkbutton(options_frame, text="Temiz başlangıç (.trash'e taşınır)", variable=self.prefer_clean, bootstyle=PRIMARY)).pack(anchor=W, pady=1) self.auto_cleanup = self._register_control(ttk.BooleanVar(value=False)) self._register_control(ttk.Checkbutton(options_frame, text="'build' klasörlerini temizle", variable=self.auto_cleanup, bootstyle=PRIMARY)).pack(anchor=W, pady=1) self.btn_info = ttk.Button(options_frame, text="ℹ️ Bilgi", command=self._show_info_modal, bootstyle=(INFO, OUTLINE)) self.btn_info.pack(fill=X, pady=(8, 0)) extra_frame = ttk.Frame(p2) extra_frame.grid(row=1, column=0, columnspan=3, sticky="ew", padx=2, pady=2) ttk.Label(extra_frame, text="Ekstra -D Bayrakları:").pack(side=LEFT, padx=(0, 10)) self.extra = self._register_control(ttk.Entry(extra_frame)) self.extra.insert(0, "") self.extra.pack(side=LEFT, fill=X, expand=True) def _create_status_panel_content(self, dash: ttk.LabelFrame) -> None: dash.grid_columnconfigure(0, weight=1) self.lbl_status_main = ttk.Label(dash, text="Beklemede...", font="-weight bold -size 10") self.lbl_status_main.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 5)) pb_frame = ttk.Frame(dash) pb_frame.grid(row=1, column=0, sticky="ew", pady=2) pb_frame.grid_columnconfigure(1, weight=3) pb_frame.grid_columnconfigure(3, weight=2) ttk.Label(pb_frame, text="İndirme:", width=10).grid(row=0, column=0, sticky="e", padx=5) self.pb_dl = ttk.Progressbar(pb_frame, length=100) self.pb_dl.grid(row=0, column=1, sticky="ew") self.lbl_dl_pct = ttk.Label(pb_frame, text="0%", width=5) self.lbl_dl_pct.grid(row=0, column=2, sticky="w", padx=5) self.lbl_dl_txt = ttk.Label(pb_frame, text="—", width=30) self.lbl_dl_txt.grid(row=0, column=3, sticky="ew", padx=5) ttk.Label(pb_frame, text="Genel İşlem:", width=10).grid(row=1, column=0, sticky="e", padx=5) self.pb_work = ttk.Progressbar(pb_frame, length=100) self.pb_work.grid(row=1, column=1, sticky="ew") self.lbl_work_pct = ttk.Label(pb_frame, text="0%", width=5) self.lbl_work_pct.grid(row=1, column=2, sticky="w", padx=5) self.lbl_work_txt = ttk.Label(pb_frame, text="—", width=30) self.lbl_work_txt.grid(row=1, column=3, sticky="ew", padx=5) metrics_frame = ttk.Frame(dash) metrics_frame.grid(row=2, column=0, sticky="ew", pady=5) ttk.Label(metrics_frame, text="CPU:", font="-weight bold").pack(side=LEFT, padx=(15, 5)) self.lbl_cpu = ttk.Label(metrics_frame, text="0%") self.lbl_cpu.pack(side=LEFT) ttk.Label(metrics_frame, text="RAM:", font="-weight bold").pack(side=LEFT, padx=(15, 5)) self.lbl_ram = ttk.Label(metrics_frame, text="0%") self.lbl_ram.pack(side=LEFT) ttk.Label(metrics_frame, text="Disk:", font="-weight bold").pack(side=LEFT, padx=(15, 5)) self.lbl_disk = ttk.Label(metrics_frame, text="0 MB/s") self.lbl_disk.pack(side=LEFT) ttk.Label(metrics_frame, text=".trash:", font="-weight bold").pack(side=LEFT, padx=(15, 5)) self.lbl_trash_size = ttk.Label(metrics_frame, text="0 B") self.lbl_trash_size.pack(side=LEFT) self.btn_empty_trash = ttk.Button(metrics_frame, text="Boşalt", command=self.app.build_manager.empty_trash_folder, bootstyle=(SECONDARY, OUTLINE), width=6) self.btn_empty_trash.pack(side=LEFT, padx=5) btns = ttk.Frame(dash) btns.grid(row=0, column=1, rowspan=3, sticky="ne", padx=(20, 0)) self.btn_start = ttk.Button(btns, text="Başlat", bootstyle=SUCCESS, width=18, command=self.app.build_manager.start_build) self.btn_start.pack(pady=2, fill=X) self.btn_stop = ttk.Button(btns, text="Durdur (Acil)", bootstyle=(DANGER, OUTLINE), width=18, command=self.app.task_manager.stop_all, state=DISABLED) self.btn_stop.pack(pady=2, fill=X) self.btn_clear_cache = ttk.Button(btns, text="CMake Önbelleğini Temizle", bootstyle=(WARNING, OUTLINE), width=18, command=self.app.build_manager.clear_cmake_cache) self.btn_clear_cache.pack(pady=2, fill=X) self.btn_open_install = ttk.Button(btns, text="Install Klasörünü Aç", bootstyle=(INFO, OUTLINE), width=18, command=self._open_install_folder) self.btn_open_install.pack(pady=2, fill=X) def _create_log_widgets(self, parent: ttk.LabelFrame) -> None: parent.rowconfigure(1, weight=1) parent.columnconfigure(0, weight=1) filter_frame = ttk.Frame(parent, padding=(0, 5)) filter_frame.grid(row=0, column=0, sticky="ew") ttk.Label(filter_frame, text="Filtrele:").pack(side=LEFT, padx=(0, 5)) for level in C.LogLevel: var = tk.BooleanVar(value=True) chk = ttk.Checkbutton(filter_frame, text=level.name.capitalize(), variable=var, command=lambda l=level.value, v=var: self.toggle_log_filter(l, v.get()), bootstyle="outline-toolbutton") chk.pack(side=LEFT, padx=2) self.log_filter_vars[level.value] = var log_text_frame = ttk.Frame(parent) log_text_frame.grid(row=1, column=0, sticky="nsew") log_text_frame.rowconfigure(0, weight=1) log_text_frame.columnconfigure(0, weight=1) self.log = scrolledtext.ScrolledText(log_text_frame, font=("Consolas", 10), wrap=WORD, undo=True) self.log.grid(row=0, column=0, sticky="nsew") self.log_context_menu = tk.Menu(self.log, tearoff=0) self.log_context_menu.add_command(label="Kopyala", command=self.copy_log_selection) self.log_context_menu.add_command(label="Tümünü Kopyala", command=self.copy_all_logs) self.log_context_menu.add_separator() self.log_context_menu.add_command(label="Tümünü Temizle", command=self._clear_log) self.log.bind("<Button-3>", self.show_log_context_menu) colors = { C.LogLevel.STAGE: ("#198754", None), C.LogLevel.CMD: ("#0d6efd", None), C.LogLevel.WARN: ("#664d03", "#fff3cd"), C.LogLevel.ERR: ("#842029", "#f8d7da"), C.LogLevel.INFO: ("#6c757d", None) } for level, (fg, bg) in colors.items(): if bg: self.log.tag_config(level.value, foreground=fg, background=bg) else: self.log.tag_config(level.value, foreground=fg) def _create_task_panel(self, parent: ttk.LabelFrame) -> None: parent.rowconfigure(0, weight=1) parent.columnconfigure(0, weight=1) self.task_tree = ttk.Treeview(parent, columns=("name", "status", "progress", "runtime"), show="headings", height=4) for c, t, w in (("name","Görev",200), ("status","Durum",100), ("progress","İlerleme",120), ("runtime","Süre",100)): self.task_tree.heading(c, text=t) self.task_tree.column(c, width=w, anchor=W) self.task_tree.grid(row=0, column=0, sticky="nsew") btn_frame = ttk.Frame(parent) btn_frame.grid(row=1, column=0, sticky="ew", pady=(5,0)) ttk.Button(btn_frame, text="Seçili Görevi Durdur", command=self.app.task_manager.terminate_selected_task, bootstyle=(WARNING, OUTLINE)).pack(fill=X) def pump_queue(self) -> None: log_batch = [] try: while not self.app.q.empty(): log_batch.append(self.app.q.get_nowait()) if log_batch: self._process_log_batch(log_batch) except queue.Empty: pass finally: self.app.after(100, self.pump_queue) def _process_log_batch(self, batch: list[tuple]) -> None: full_text_to_insert = "" is_scrolled_to_bottom = self.log.yview()[1] >= 1.0 for kind, *rest in batch: if kind == "log": timestamp = datetime.datetime.now().strftime('%H:%M:%S') message, level = cast(str, rest[0]), cast(str, rest[1]) for ln in message.splitlines(): start_index = self.log.index(f"end-1c") line_content = f"[{timestamp}] {ln}\n" full_text_to_insert += line_content end_index = f"{start_index}+{len(line_content)}c" self.log.tag_add(level, start_index, end_index) elif kind == "status": self.lbl_status_main.config(text=rest[0]) elif kind == "dl": val, txt = int(rest[0]), str(rest[1]) self.pb_dl['value'] = val self.lbl_dl_pct.config(text=f"{val}%") self.lbl_dl_txt.config(text=txt) elif kind == "work": val, txt = int(rest[0]), str(rest[1]) self.pb_work['value'] = val self.lbl_work_pct.config(text=f"{val}%") self.lbl_work_txt.config(text=txt) elif kind == "task_progress": task_id, progress_text = cast(str, rest[0]), cast(str, rest[1]) if task_id in self.app.task_manager.task_registry: self.app.task_manager.task_registry[task_id]['progress'] = progress_text if full_text_to_insert: self.log.insert(END, full_text_to_insert) num_lines = int(self.log.index('end-1c').split('.')[0]) if num_lines > C.MAX_LOG_LINES: self.log.delete("1.0", f"{num_lines - C.MAX_LOG_LINES}.0") if is_scrolled_to_bottom: self.log.see(END) def set_controls_state(self, state: str) -> None: self.btn_start.config(state=state) self.btn_stop.config(state='normal' if state == 'disabled' else 'disabled') self.btn_clear_cache.config(state=state) self.btn_open_install.config(state=state) for widget in self.control_widgets: try: if isinstance(widget, ttk.Variable): continue widget.config(state="readonly" if isinstance(widget, ttk.Combobox) and state == 'normal' else state) except tk.TclError: pass for tree in (self.tree, self.ssl_tree): tree.config(selectmode='browse' if state == 'normal' else 'none') def update_build_stats(self, last_line: str, task_id: str) -> None: m = C._RE_NINJA.search(last_line) if m: d, t = int(m.group(1)), int(m.group(2)) pct = int(100.0 * d / max(1, t)) progress_text = f"[{d}/{t}] {pct}%" self.bus.update_task_progress(task_id, progress_text) self.bus.progress("work", pct, progress_text) else: self.bus.log(last_line, C.LogLevel.CMD) def update_resource_monitor(self) -> None: try: self.lbl_cpu.config(text=f"{psutil.cpu_percent(interval=None):.1f}%") self.lbl_ram.config(text=f"{psutil.virtual_memory().percent:.1f}%") current_time, current_io = time.time(), psutil.disk_io_counters() delta_time = current_time - self.last_update_time if delta_time > 0: read_bytes = current_io.read_bytes - self.last_disk_io.read_bytes write_bytes = current_io.write_bytes - self.last_disk_io.write_bytes total_mb_s = (read_bytes + write_bytes) / (1024 * 1024) / delta_time self.lbl_disk.config(text=f"{total_mb_s:.1f} MB/s") self.last_update_time, self.last_disk_io = current_time, current_io except (psutil.Error, ZeroDivisionError): for label in (self.lbl_cpu, self.lbl_ram, self.lbl_disk): label.config(text="N/A") finally: self.app.after(1000, self.update_resource_monitor) def update_trash_monitor(self) -> None: try: if C.TRASH.exists(): total_size = sum(f.stat().st_size for f in C.TRASH.glob('**/*') if f.is_file()) if total_size < 1024: size_str = f"{total_size} B" elif total_size < 1024**2: size_str = f"{total_size/1024:.1f} KB" elif total_size < 1024**3: size_str = f"{total_size/1024**2:.1f} MB" else: size_str = f"{total_size/1024**3:.1f} GB" self.lbl_trash_size.config(text=size_str) else: self.lbl_trash_size.config(text="0 B") except Exception: self.lbl_trash_size.config(text="N/A") finally: self.app.after(5000, self.update_trash_monitor) def load_settings(self) -> None: try: if C.CONFIG_FILE.exists(): with open(C.CONFIG_FILE, 'r', encoding='utf-8') as f: settings = json.load(f) self.cxx_version.set(settings.get("cxx_version", "Latest")) self.generator.set(settings.get("generator", C.Generator.NINJA.value)) self.runtime.set(settings.get("runtime", C.Runtime.MT.value)) self.jobs.set(settings.get("jobs", C.DEFAULT_JOBS)) self.tool_mode.set(settings.get("tool_mode", C.ToolMode.BUNDLED.value)) self.last_tag_from_config = settings.get("last_tag") for var, key in ((self.build_release,"build_release"),(self.build_debug,"build_debug"),(self.only_client,"only_client"), (self.prefer_clean,"prefer_clean"),(self.auto_cleanup,"auto_cleanup")): var.set(settings.get(key, var.get())) self.extra.delete(0, END) self.extra.insert(0, settings.get("extra_defs", "")) self.bus.log("Ayarlar 'config.json' dosyasından yüklendi.", C.LogLevel.INFO) except (json.JSONDecodeError, TypeError, KeyError) as e: self.bus.log(f"'config.json' dosyası okunurken hata: {e}", C.LogLevel.WARN) def save_settings(self) -> None: settings = {"cxx_version": self.cxx_version.get(), "generator": self.generator.get(), "runtime": self.runtime.get(), "jobs": self.jobs.get(), "tool_mode": self.tool_mode.get(), "build_release": self.build_release.get(), "build_debug": self.build_debug.get(), "only_client": self.only_client.get(), "prefer_clean": self.prefer_clean.get(), "auto_cleanup": self.auto_cleanup.get(), "extra_defs": self.extra.get(), "last_tag": self.cmb_tag.get()} try: with open(C.CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(settings, f, indent=4) except OSError as e: self.bus.log(f"Ayarlar kaydedilemedi: {e}", C.LogLevel.ERR) def _clear_log(self) -> None: self.log.delete("1.0", END) def _save_log(self) -> None: log_content = self.log.get("1.0", END) if not log_content.strip(): messagebox.showinfo("Bilgi", "Kaydedilecek log içeriği bulunmuyor.") return filename = filedialog.asksaveasfilename(title="Log Dosyasını Kaydet", initialfile=f"mysql_builder_pro_log_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt", defaultextension=".txt", filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")]) if filename: try: with open(filename, 'w', encoding='utf-8') as f: f.write(log_content) self.bus.log(f"Loglar başarıyla '{filename}' dosyasına kaydedildi.", C.LogLevel.INFO) except OSError as e: self.bus.log(f"Log dosyası kaydedilemedi: {e}", C.LogLevel.ERR) messagebox.showerror("Hata", f"Dosya kaydedilemedi:\n{e}") def _open_install_folder(self) -> None: C.INST.mkdir(parents=True, exist_ok=True) try: os.startfile(C.INST) self.bus.log(f"'{C.INST}' klasörü açıldı.", C.LogLevel.INFO) except Exception as e: self.bus.log(f"Klasör açılamadı: {e}", C.LogLevel.ERR) messagebox.showerror("Hata", f"'{C.INST}' klasörü açılamadı:\n{e}") def _show_info_modal(self) -> None: info_window = ttk.Toplevel(title="Uygulama Kullanım Rehberi") info_window.geometry("850x750") info_window.transient(self.root) info_window.grab_set() text_area = scrolledtext.ScrolledText(info_window, wrap=WORD, font=("Segoe UI", 10), relief="flat", padx=10, pady=10) text_area.pack(fill=BOTH, expand=True) info_text = """MySQL Builder Pro Kullanım Rehberi (v1.0.0) ========================================== Bu araç, MySQL kaynak kodunu (özellikle C++ connector/client kütüphanesini) Windows ortamında derleme sürecini otomatikleştirmek için tasarlanmıştır. Normalde karmaşık ve çok adımlı olan bu işlemi, kullanıcı dostu bir arayüz üzerinden yönetmenizi sağlar. --- ÖN GEREKSİNİMLER --- - **Windows 10/11 (64-bit)** - **Visual Studio 2022:** "Masaüstü geliştirme (C++)" (Desktop development with C++) iş yükünün kurulu olması ZORUNLUDUR. Bu, derleme için gerekli olan C++ derleyicisini (cl.exe), linker'ı ve Windows SDK'larını içerir. --- ADIM ADIM KULLANIM --- **Adım 1: Gerekli Araçların Kurulumu** Uygulama, derleme için Git, CMake, Ninja, Perl gibi bir dizi araca ihtiyaç duyar. Bu araçları sizin için otomatik olarak indirip kurabilir. 1. Uygulamayı ilk açtığınızda **"Araç Durumu"** paneline bakın. 2. Eğer listede "❌ Yok" olarak işaretlenmiş araçlar varsa, sağ taraftaki **"Eksikleri Kur"** butonuna tıklayın. 3. Uygulama, gerekli tüm araçları programın yanındaki `.tools` klasörüne indirecek ve kuracaktır. İşlem tamamlandığında bir bildirim alacaksınız. 4. Eğer mevcut kurulumda bir sorun olduğunu düşünüyorsanız, **"Hepsini Yeniden Kur"** butonu ile tüm araçları silip baştan kurabilirsiniz. **Adım 2: OpenSSL Kütüphanesinin Derlenmesi** MySQL, güvenlik katmanı için OpenSSL kütüphanesine ihtiyaç duyar. Bu araç, OpenSSL'i de sizin için kaynak kodundan derler. 1. **"OpenSSL Durumu"** paneline bakın. Başlangıçta tüm konfigürasyonlar "❌ Eksik" görünecektir. 2. Sağ taraftaki **"OpenSSL Kur (R+D)"** butonuna tıklayın. - **(R+D)**, hem **Release** hem de **Debug** konfigürasyonlarının derleneceği anlamına gelir. Bu, her iki modda da MySQL derlemesi yapabilmeniz için gereklidir. 3. Bu işlem biraz zaman alabilir. İşlem tamamlandığında durum tablosu güncellenecek ve "✔️ OK" olarak görünecektir. **Adım 3: Derleme Ayarlarını Yapılandırma** Tüm araçlar ve bağımlılıklar hazır olduğunda, neyi ve nasıl derlemek istediğinizi seçebilirsiniz. - **MySQL Sürümü:** Derlemek istediğiniz MySQL versiyonunu seçin. **"Listele"** butonu en güncel sürümleri GitHub'dan çeker. - **Araç Modu:** Genellikle **".tools"** seçeneğinde kalmalıdır. Bu, uygulamanın kendi kurduğu araçları kullanmasını sağlar. - **Parametreler:** - **C++ Sürümü:** Genellikle "Latest" olarak bırakılabilir. - **Generator:** "Ninja" genellikle daha hızlıdır ve varsayılan olarak seçilidir. - **Runtime:** - **MT:** Statik Runtime. Derlenen kütüphane, C++ runtime'ını kendi içinde barındırır. Dağıtımı daha kolaydır. - **MD:** Dinamik Runtime. Derlenen kütüphane, sistemde Visual C++ Redistributable paketinin kurulu olmasını gerektirir. - **Seçenekler:** - **Release/Debug Derle:** Hangi konfigürasyonları derlemek istediğinizi seçin. Genellikle ikisi de seçilir. - **Yalnız client kütüphanesi:** Bu seçenek, tüm MySQL sunucusunu değil, sadece C++ uygulamalarınıza bağlayacağınız `mysqlclient.lib` ve ilgili başlık dosyalarını derler. **Çoğu kullanım durumu için bu seçenek işaretli kalmalıdır.** - **Temiz başlangıç:** Derlemeye başlamadan önce `src`, `build`, `install` klasörlerini `.trash` klasörüne taşıyarak temiz bir başlangıç yapar. - **'build' klasörlerini temizle:** Her bir derleme bittikten sonra, diskte yer kaplayan geçici derleme klasörünü otomatik olarak siler. **Adım 4: Derlemeyi Başlatma** 1. Tüm ayarları yaptıktan sonra sağ alttaki yeşil **"Başlat"** butonuna tıklayın. 2. Uygulama, seçtiğiniz MySQL sürümünün kaynak kodunu indirecek, yapılandıracak ve derleyecektir. 3. İlerleme çubuklarından ve log penceresinden süreci takip edebilirsiniz. 4. Derleme sırasında bir sorun olursa, kırmızı **"Durdur (Acil)"** butonu ile işlemi sonlandırabilirsiniz. **Adım 5: Sonuçları Kullanma** 1. Derleme başarıyla tamamlandığında bir bildirim alacaksınız. 2. Tüm nihai dosyalar (`.lib`, `.dll`, `.h` dosyaları) programın yanındaki **`install`** klasörünün içine, seçtiğiniz Runtime ve Sürüm'e göre alt klasörlere yerleştirilir. 3. **"Install Klasörünü Aç"** butonuna tıklayarak bu klasöre kolayca erişebilirsiniz. """ text_area.insert(END, info_text.strip()) text_area.config(state=DISABLED) ttk.Button(info_window, text="Kapat", command=info_window.destroy, bootstyle=SECONDARY).pack(pady=10) def toggle_log_filter(self, tag_name: str, is_visible: bool) -> None: self.log.tag_config(tag_name, elide=not is_visible) def show_log_context_menu(self, event: tk.Event) -> None: if self.log.tag_ranges("sel"): self.log_context_menu.entryconfig("Kopyala", state="normal") else: self.log_context_menu.entryconfig("Kopyala", state="disabled") self.log_context_menu.tk_popup(event.x_root, event.y_root) def copy_log_selection(self) -> None: try: selected_text = self.log.get(tk.SEL_FIRST, tk.SEL_LAST) self.root.clipboard_clear() self.root.clipboard_append(selected_text) self.bus.log("Seçili loglar panoya kopyalandı.", C.LogLevel.INFO) except tk.TclError: pass def copy_all_logs(self) -> None: all_text = self.log.get("1.0", tk.END) self.root.clipboard_clear() self.root.clipboard_append(all_text) self.bus.log("Tüm loglar panoya kopyalandı.", C.LogLevel.INFO) class ToolManager: def __init__(self, app: 'App'): self.app = app self.bus = app.bus self.TOOLS_META = [ {"key":"7zip","name":"7-Zip","exe":"7z","altexe":["7za","7zr"],"ensure":self._ensure_7zip,"vercmd":["7z"]}, {"key":"git","name":"Git (MinGit)","exe":"git","ensure":lambda bus, se: self._ensure_zip_tool("mingit",C.URLS["mingit"],"mingit",bus, se),"vercmd":["git","--version"]}, {"key":"cmake","name":"CMake","exe":"cmake","ensure":lambda bus, se: self._ensure_zip_tool("cmake",C.URLS["cmake"],"cmake",bus, se),"vercmd":["cmake","--version"]}, {"key":"ninja","name":"Ninja","exe":"ninja","ensure":lambda bus, se: self._ensure_zip_tool("ninja",C.URLS["ninja"],None,bus, se),"vercmd":["ninja","--version"]}, {"key":"perl","name":"Strawberry Perl","exe":"perl","ensure":lambda bus, se: self._ensure_zip_tool("perl",C.URLS["perl"],"perl",bus, se),"vercmd":["perl","-e","print $^V"]}, {"key":"nasm","name":"NASM","exe":"nasm","ensure":lambda bus, se: self._ensure_zip_tool("nasm",C.URLS["nasm"],"nasm",bus, se),"vercmd":["nasm","-v"]}, {"key":"sccache","name":"sccache (Önerilir)","exe":"sccache","ensure":self._ensure_sccache,"vercmd":["sccache","--version"]}, ] def vs_env_paths(self) -> str | None: vswhere = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe") candidates = [] if vswhere.exists(): cmd_str = subprocess.list2cmdline([str(vswhere), "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"]) base_path = run_out(cmd_str).splitlines() base = Path((base_path[0] if base_path else "").strip()) if base.is_dir(): candidates += [base / "VC/Auxiliary/Build/vcvars64.bat", base / "Common7/Tools/VsDevCmd.bat"] root = Path(r"C:\Program Files\Microsoft Visual Studio\2022") for ed in ("Community", "Professional", "Enterprise", "BuildTools"): base = root / ed candidates += [base / "VC/Auxiliary/Build/vcvars64.bat", base / "Common7/Tools/VsDevCmd.bat"] return next((str(p) for p in candidates if p.exists()), None) def which(self, name: str) -> str | None: return shutil.which(name) def which_only_bundled(self, name: str) -> str | None: for folder in C.BUNDLED_PATHS: for ext in ["", ".exe", ".bat", ".cmd"]: p = folder / (name + ext) if p.exists() and p.is_file(): return str(p) return None def detect_tool(self, meta: dict, mode: C.ToolMode) -> tuple[str | None, str | None, str | None]: executables = [meta['exe']] + meta.get('altexe', []) p, src = (None, None) if mode == C.ToolMode.BUNDLED: p = next((path for exe in executables if (path := self.which_only_bundled(exe))), None) if p: src = "Bundled" else: p = next((path for exe in executables if (path := self.which(exe))), None) if p: src = "System" if not p: return None, None, None exe_path = Path(p) try: is_bundled = exe_path.resolve().is_relative_to(C.TOOLS.resolve()) if mode == C.ToolMode.BUNDLED and not is_bundled: src = "System" if mode == C.ToolMode.SYSTEM and is_bundled: src = "Bundled" except (ValueError, OSError): pass vercmd = meta.get("vercmd", [])[:] if vercmd: vercmd[0] = str(exe_path) else: vercmd = [str(exe_path), "--version"] env_paths = C.BUNDLED_PATHS if src == "Bundled" else [] with temp_env_path(env_paths): out = run_out(vercmd, cwd=exe_path.parent) if out.startswith("ERR:"): return str(exe_path), "Hata", src ver_patterns = [r"v?(\d+\.\d+(?:\.\d+)?(?:\.\d+)?)", r"version[\s:]*(\d+\.\d+(?:\.\d+)?)"] ver = next((m.group(1) for pat in ver_patterns if (m := re.search(pat, out, re.I))), out.splitlines()[0][:40] if out else "") return str(exe_path), ver, src def refresh_tools(self) -> None: ui = self.app.ui_manager ui.tree.delete(*ui.tree.get_children()) mode = C.ToolMode(ui.tool_mode.get()) for meta in self.TOOLS_META: path, ver, src = self.detect_tool(meta, mode) tags = () if path else ("missing",) ui.tree.insert("", END, iid=meta["key"], values=(meta["name"], "✔️ Yüklü" if path else "❌ Yok", ver or "-", src or "-", path or "-"), tags=tags) ui.tree.tag_configure("missing", background="#f8d7da", foreground="#842029") def confirm_reinstall_all(self) -> None: if messagebox.askyesno("Onay", "Tüm araçlar .tools klasöründen silinip yeniden indirilecektir. Bu işlem zaman alabilir.\n\nDevam etmek istiyor musunuz?"): self.install_tools_async(reinstall=True) def install_tools_async(self, missing: bool = False, reinstall: bool = False) -> None: if self.app._is_busy: return self.app.set_busy(True) stop_event = threading.Event() self.app.task_manager.register(name="Araç Kurulum Yöneticisi", target=self._install_tools_worker, args=(missing, reinstall, stop_event), stop_event=stop_event, is_build_task=False) def _install_tools_worker(self, missing: bool, reinstall: bool, stop_event: threading.Event, task_id: str) -> None: try: metas_to_install = self.TOOLS_META if missing: metas_to_install = [m for m in self.TOOLS_META if not self.detect_tool(m, C.ToolMode.BUNDLED)[0]] if not metas_to_install: self.bus.log("Tüm araçlar zaten .tools klasöründe yüklü.", C.LogLevel.INFO) messagebox.showinfo("Araçlar", "Eksik araç bulunamadı.") self.app.task_manager.task_registry[task_id]['success'] = True self.app.set_busy(False) return with ThreadPoolExecutor() as executor: futures = [executor.submit(self._install_tool, mt, reinstall, stop_event) for mt in metas_to_install] for future in futures: future.result() if not stop_event.is_set(): messagebox.showinfo("Araçlar", "Seçilen araçların kurulumu tamamlandı.") self.app.task_manager.task_registry[task_id]['success'] = True except Exception as e: if not stop_event.is_set(): self.bus.log(f"Araç kurulum hatası: {e}\n{traceback.format_exc()}", C.LogLevel.ERR) messagebox.showerror("Hata", f"Araç kurulumu başarısız oldu: {e}") self.app.task_manager.task_registry[task_id]['success'] = False finally: self.app.after(0, self.refresh_tools) self.app.set_busy(False) def _install_tool(self, meta: dict, reinstall: bool, stop_event: threading.Event) -> None: if stop_event.is_set(): return self.bus.log(f"{meta['name']} kurulumu başlıyor...", C.LogLevel.STAGE) if reinstall: if meta['key'] == '7zip' and (C.TOOLS / "7zip").exists(): shutil.rmtree(C.TOOLS / "7zip", ignore_errors=True) elif meta.get('subfolder') and (C.TOOLS / meta['subfolder']).exists(): shutil.rmtree(C.TOOLS / meta['subfolder'], ignore_errors=True) elif meta['key'] == 'sccache' and (C.TOOLS / 'sccache.exe').exists(): (C.TOOLS / 'sccache.exe').unlink(missing_ok=True) meta["ensure"](self.bus, stop_event) def _http_get(self, url: str, dest: Path, bus: Bus) -> bool: bus.log(f"[DL] Başlıyor: {url} → {dest}", C.LogLevel.STAGE) dest.parent.mkdir(parents=True, exist_ok=True) try: with requests.get(url, stream=True, timeout=60) as r: r.raise_for_status() total_size = int(r.headers.get("Content-Length", 0)) done = 0 last_pc = -1 with open(dest, "wb") as f: for chunk in r.iter_content(256 * 1024): if not chunk: continue f.write(chunk) done += len(chunk) if total_size: pc = int(done * 100 / total_size) if pc != last_pc: bus.progress("dl", pc, f"{done/1e6:.1f}/{total_size/1e6:.1f} MB") last_pc = pc bus.progress("dl", 100, "OK") bus.log("[DL] Tamamlandı", C.LogLevel.STAGE) return True except requests.RequestException as e: bus.log(f"[DL] Hata: {e}", C.LogLevel.ERR) dest.unlink(missing_ok=True) return False def _seven_extract(self, arc: Path, out: Path, bus: Bus) -> bool: seven = next((p for n in ("7z", "7zr") for p in (self.which_only_bundled(n), self.which(n)) if p), None) if not seven: self._ensure_7zip(bus, None) seven = next((p for n in ("7z", "7zr") for p in (self.which_only_bundled(n), self.which(n)) if p), None) if not seven: return False parser = SevenZipProgressParser(bus) lines = [] cmd = f'"{seven}" x "{arc}" -o"{out}" -y -bsp1 -bso1 -bse1' ok = stream_proc(cmd, os.environ.copy(), on_line=lambda line: (lines.append(line), parser.parse_line(line))) output_text = "\n".join(lines) if "Everything is Ok".lower() in output_text.lower(): return True return ok and out.exists() and any(out.rglob("*")) def _python_unzip(self, z: Path, out: Path, bus: Bus) -> bool: with zipfile.ZipFile(z) as zp: infolist = zp.infolist() n = len(infolist) last_pc = -1 for i, info in enumerate(infolist, 1): zp.extract(info, out) pc = int(i * 100 / n) if n else 0 if pc != last_pc: bus.progress("work", pc, f"{i}/{n}") last_pc = pc bus.progress("work", 100, "Tamamlandı") return True def _ensure_zip_tool(self, name: str, url: str, subfolder: str | None, bus: Bus, stop_event: threading.Event | None): if stop_event and stop_event.is_set(): return zip_path = C.DL / f"{name}.zip" if not zip_path.exists(): bus.progress("dl", 0, f"{name} indiriliyor") if not self._http_get(url, zip_path, bus): raise RuntimeError(f"{name} indirilemedi") out = C.TOOLS / subfolder if subfolder else C.TOOLS out.mkdir(parents=True, exist_ok=True) bus.status(f"'{name}' arşivi çıkarılıyor...") bus.progress("work", 0, "Çıkarma başlıyor...") if not (self._seven_extract(zip_path, out, bus) or self._python_unzip(zip_path, out, bus)): raise RuntimeError(f"{name} arşivi açılamadı") bus.progress("work", 100, f"{name} hazır") if subfolder: kids = list(out.glob("*")) if len(kids) == 1 and kids[0].is_dir(): tmp = out.parent / f"_{subfolder}" shutil.move(str(kids[0]), str(tmp)) shutil.rmtree(out, ignore_errors=True) tmp.rename(out) def _ensure_7zip(self, bus: Bus, stop_event: threading.Event | None) -> None: zdir = C.TOOLS / "7zip" zdir.mkdir(exist_ok=True) if not (zdir / "7z.exe").exists(): zr, ze = zdir / "7zr.exe", C.DL / "7zextra.7z" if not zr.exists(): self._http_get(C.URLS["7zr"], zr, bus) if not ze.exists(): self._http_get(C.URLS["7zextra"], ze, bus) subprocess.call(f'"{zr}" x "{ze}" -o"{zdir}" -y', shell=True, creationflags=subprocess.CREATE_NO_WINDOW) def _ensure_sccache(self, bus: Bus, stop_event: threading.Event | None) -> None: archive = C.DL / "sccache.tar.gz" if not archive.exists(): self._http_get(C.URLS["sccache"], archive, bus) bus.status("sccache arşivi çıkarılıyor...") with tarfile.open(archive, "r:gz") as tar: exe_member = next((m for m in tar.getmembers() if "sccache.exe" in m.name), None) if not exe_member: raise RuntimeError("sccache.exe arşivde bulunamadı.") exe_member.name = Path(exe_member.name).name tar.extract(exe_member, path=C.TOOLS) if not (C.TOOLS / "sccache.exe").exists(): raise RuntimeError("sccache.exe .tools'a taşınamadı.") bus.log("✔️ sccache başarıyla kuruldu.", C.LogLevel.STAGE) class BuildManager: def __init__(self, app: 'App'): self.app = app self.bus = app.bus self.active_pids = set() self.pids_lock = threading.Lock() def add_pid(self, pid: int) -> None: with self.pids_lock: self.active_pids.add(pid) def remove_pid(self, pid: int) -> None: with self.pids_lock: self.active_pids.discard(pid) def get_active_pids(self) -> set[int]: with self.pids_lock: return self.active_pids.copy() def clear_active_pids(self) -> None: with self.pids_lock: self.active_pids.clear() def start_build(self) -> None: ui = self.app.ui_manager if self.app._is_busy: self.bus.log("Zaten devam eden bir derleme var.", C.LogLevel.WARN) return ui.log.delete("1.0", END) if not self._run_preflight_checks(): self.bus.log("Ön kontrol başarısız, derleme iptal.", C.LogLevel.ERR) return if not ui.build_release.get() and not ui.build_debug.get(): messagebox.showwarning("Seçim Gerekli", "En az bir derleme konfigürasyonu seçin.") return ui.pb_dl['value'] = 0 ui.pb_work['value'] = 0 ui.lbl_dl_pct.config(text="0%") ui.lbl_work_pct.config(text="0%") self.app.set_busy(True) try: tag = ui.cmb_tag.get().strip() shared_config = {"tag": tag, "tool_mode": C.ToolMode(ui.tool_mode.get()), "generator": C.Generator(ui.generator.get()), "runtime": C.Runtime(ui.runtime.get()), "cxx_version": ui.cxx_version.get(), "jobs": ui.jobs.get(), "extra_defs": ui.extra.get(), "only_client": ui.only_client.get(), "source_dir": C.SRC / tag} except Exception as e: self.bus.log(f"Yapılandırma okunurken hata: {e}", C.LogLevel.ERR) self.app.set_busy(False) return stop_event = threading.Event() self.app.task_manager.register(name="Ana Derleme Yöneticisi", target=self._start_build_manager, args=(shared_config, stop_event), stop_event=stop_event, is_build_task=False) def _start_build_manager(self, shared_config: dict, stop_event: threading.Event, task_id: str) -> None: ui = self.app.ui_manager try: configs = [] if ui.build_release.get(): configs.append(BuildConfig(build_type=C.BuildType.RELEASE, **shared_config)) if ui.build_debug.get(): configs.append(BuildConfig(build_type=C.BuildType.DEBUG, **shared_config)) if not configs: raise RuntimeError("Derlenecek yapılandırma bulunamadı.") self.bus.update_task_progress(task_id, "Temizlik...") if ui.prefer_clean.get(): self._do_clean_start() if stop_event.is_set(): return self.bus.update_task_progress(task_id, "OpenSSL Derleniyor...") self._build_openssl_sequence(C.Runtime(shared_config['runtime']), stop_event) if stop_event.is_set(): return self.bus.update_task_progress(task_id, "MySQL Kaynakları Getiriliyor...") src_path = self._fetch_source(shared_config['tag'], stop_event) if not src_path: raise RuntimeError("MySQL kaynakları alınamadı.") for bc in configs: bc.source_dir = src_path if stop_event.is_set(): return all_success = True for config in configs: self.bus.update_task_progress(task_id, f"{config.build_type.value} derleniyor...") sub_task_id = f"build_{config.build_type.value.lower()}_{uuid.uuid4().hex[:6]}" worker_thread = self.app.task_manager.register(name=f"MySQL Derleme: {config.build_type.value}", target=self._build_worker, args=(config, stop_event), stop_event=stop_event, task_id=sub_task_id) worker_thread.join() if stop_event.is_set(): return if not self.app.task_manager.task_registry.get(sub_task_id, {}).get('success', False): all_success = False self.bus.log(f"{config.build_type.value} derlemesi başarısız, süreç durduruldu.", C.LogLevel.ERR) break self._finalize_build_process(all_success) except Exception as e: if not stop_event.is_set(): self.bus.log(f"ANA YÖNETİCİDE KRİTİK HATA: {e}\n{traceback.format_exc()}", C.LogLevel.ERR) self.bus.status(f"Kritik Hata: {e}") messagebox.showerror("Kritik Hata", f"Beklenmedik hata:\n\n{e}") finally: self.app.task_manager.task_registry[task_id]['success'] = True self.app.set_busy(False) def _finalize_build_process(self, all_success: bool) -> None: if all_success: self.bus.status("Tüm işlemler başarıyla tamamlandı!") messagebox.showinfo("MySQL Builder", "Tüm derleme işlemleri başarıyla tamamlandı.") self.app.q.put(("auto_open_folder", str(C.INST))) else: self.bus.status("Derleme bir veya daha fazla hatayla tamamlandı.") messagebox.showwarning("Derleme Başarısız", "Bir veya daha fazla derleme görevi başarısız oldu. Detaylar için logları inceleyin.") def _build_worker(self, config: BuildConfig, stop_event: threading.Event, task_id: str) -> None: ui = self.app.ui_manager try: config.build_dir.mkdir(parents=True, exist_ok=True) config.install_dir.mkdir(parents=True, exist_ok=True) if not self._configure_mysql(config, stop_event=stop_event): raise RuntimeError("CMake yapılandırması başarısız.") if stop_event.is_set(): return ok_build, _, _ = self._build_mysql(config, task_id, stop_event=stop_event) if not ok_build: raise RuntimeError("MySQL derleme adımı başarısız.") if ui.auto_cleanup.get(): self.bus.log(f"[{config.build_type.value}] 'build' klasörü temizleniyor: {config.build_dir}", C.LogLevel.INFO) shutil.rmtree(config.build_dir, ignore_errors=True) self.app.task_manager.task_registry[task_id]['success'] = True except Exception as e: self.app.task_manager.task_registry[task_id]['success'] = False self.app.task_manager.task_registry[task_id]['error'] = str(e) if not stop_event.is_set(): self.bus.log(f"[{config.build_type.value}] KRİTİK HATA: {e}\n{traceback.format_exc()}", C.LogLevel.ERR) def _run_preflight_checks(self) -> bool: ui = self.app.ui_manager tool_manager = self.app.tool_manager self.bus.log("--- Uçuş Öncesi Kontroller Başlatıldı ---", C.LogLevel.STAGE) if not tool_manager.vs_env_paths(): self.bus.log("Visual Studio C++ ortamı bulunamadı.", C.LogLevel.ERR) messagebox.showerror("Ortam Hatası", "Visual Studio 'Desktop development with C++' bileşeni kurulu değil.") return False mode = C.ToolMode(ui.tool_mode.get()) required = ["cmake", "perl", "nasm"] if ui.generator.get() == C.Generator.NINJA.value: required.append("ninja") missing = [] for tool_key in required: meta = next(m for m in tool_manager.TOOLS_META if m["key"] == tool_key) if not tool_manager.detect_tool(meta, mode)[0]: missing.append(meta['name']) if missing: msg = f"Eksik araçlar: {', '.join(missing)}. Lütfen 'Araç Durumu' sekmesinden kurun." if mode == C.ToolMode.BUNDLED else f"Eksik sistem araçları: {', '.join(missing)}." self.bus.log(msg, C.LogLevel.ERR) messagebox.showwarning("Eksik Araçlar", msg) return False if not C._RE_TAG.match(ui.cmb_tag.get() or ""): self.bus.log("Geçerli MySQL sürümü seçilmedi.", C.LogLevel.ERR) messagebox.showwarning("Geçersiz Sürüm", "Geçerli bir MySQL sürüm etiketi seçilmedi.") return False try: free_gb = shutil.disk_usage(C.ROOT).free / (1024**3) if free_gb < C.DISK_MIN_GB: if not messagebox.askyesno("Disk Alanı Uyarısı", f"Yetersiz disk alanı ({free_gb:.1f} GB). En az {C.DISK_MIN_GB} GB önerilir.\nYine de devam edilsin mi?"): return False except OSError as e: self.bus.log(f"Disk alanı kontrol edilemedi: {e}", C.LogLevel.WARN) self.bus.log("--- Uçuş Öncesi Kontroller Başarıyla Tamamlandı ---", C.LogLevel.STAGE) return True def clear_cmake_cache(self) -> None: if self.app._is_busy: self.bus.log("Derleme çalışırken önbellek temizlenemez.", C.LogLevel.WARN) return if not C.BUILD.exists() or not any(C.BUILD.iterdir()): self.bus.log("'build' klasörü zaten boş.", C.LogLevel.INFO) messagebox.showinfo("Bilgi", "'build' klasörü zaten boş.") return if messagebox.askyesno("Onay", "Tüm 'build' klasörünün içeriği silinecektir. Devam etmek istiyor musunuz?"): try: shutil.rmtree(C.BUILD) C.BUILD.mkdir(parents=True, exist_ok=True) messagebox.showinfo("Başarılı", "'build' klasörü temizlendi.") except OSError as e: self.bus.log(f"'build' klasörü silinirken hata: {e}", C.LogLevel.ERR) messagebox.showerror("Hata", f"Silinemedi:\n{e}") def fetch_tags_async(self) -> None: if self.app._is_busy: return self.app.task_manager.register(name="MySQL Etiket Listeleme", target=self._fetch_tags_worker, is_build_task=False) def _fetch_tags_worker(self, task_id: str) -> None: ui = self.app.ui_manager tool_manager = self.app.tool_manager self.bus.log("MySQL GitHub etiketleri çekiliyor...", C.LogLevel.INFO) tags, latest = [], None try: git = tool_manager.which_only_bundled("git") or tool_manager.which("git") if git: with temp_env_path(C.BUNDLED_PATHS): out = run_out([git, "ls-remote", "--tags", "https://github.com/mysql/mysql-server.git"]) versions = [] for ln in out.splitlines(): m = re.search(r"refs/tags/(mysql-\d+\.\d+(?:\.\d+)?)$", ln) if m: t = m.group(1) v_match = re.match(r"mysql-(\d+)\.(\d+)(?:\.(\d+))?$", t) if v_match: v_groups = v_match.groups() versions.append(((int(v_groups[0]), int(v_groups[1]), int(v_groups[2] or 0)), t)) versions.sort(reverse=True) tags = [t for _, t in versions[:50]] except Exception as e: self.bus.log(f"Git ile etiket çekme hatası: {e}", C.LogLevel.WARN) if not tags: try: r = requests.get("https://github.com/mysql/mysql-server/tags", timeout=30) r.raise_for_status() found = set(re.findall(r"(mysql-\d+\.\d+(?:\.\d+)?)", r.text)) tags = sorted(list(found), key=lambda t: [int(v or 0) for v in re.match(r"mysql-(\d+)\.(\d+)(?:\.(\d+))?$", t).groups()], reverse=True)[:50] except requests.RequestException as e: self.bus.log(f"API/Scraping ile etiket çekme hatası: {e}", C.LogLevel.ERR) if not tags: tags = ["mysql-8.4.0", "mysql-8.0.38"] self.bus.log("Yedek etiket listesi yüklendi.", C.LogLevel.WARN) selected_tag = tags[0] if tags else "mysql-8.4.0" if ui.last_tag_from_config and ui.last_tag_from_config in tags: selected_tag = ui.last_tag_from_config self.bus.log(f"En son MySQL etiketi: {selected_tag}", C.LogLevel.STAGE) self.app.after(0, lambda: (ui.cmb_tag.config(values=tags), ui.cmb_tag.set(selected_tag))) self.app.task_manager.task_registry[task_id]['success'] = True def _fetch_source(self, tag: str, stop_event: threading.Event) -> Path | None: tool_manager = self.app.tool_manager dst = C.SRC / tag if stop_event.is_set(): return None if dst.exists() and (dst / "extra").exists(): self.bus.log("Kaynaklar zaten çıkarılmış, atlandı.", C.LogLevel.STAGE) return dst zip_path = C.DL / f"{tag}.zip" download_needed = True def _zip_ok(zp: Path) -> bool: try: with zipfile.ZipFile(zp) as zf: return zf.testzip() is None except Exception: return False if zip_path.exists(): if _zip_ok(zip_path): self.bus.log("Geçerli ZIP arşivi bulundu, indirme atlandı.", C.LogLevel.STAGE) download_needed = False else: self.bus.log("Bozuk ZIP arşivi, yeniden indirilecek.", C.LogLevel.WARN) zip_path.unlink(missing_ok=True) if stop_event.is_set(): return None if download_needed: urls = [f"https://github.com/mysql/mysql-server/archive/refs/tags/{tag}.zip", f"https://codeload.github.com/mysql/mysql-server/zip/refs/tags/{tag}"] download_ok = False for u in urls: if tool_manager._http_get(u, zip_path, self.bus) and _zip_ok(zip_path): download_ok = True break if not download_ok: raise RuntimeError("Tüm URL'lerden kaynak ZIP alınamadı.") if stop_event.is_set(): return None self.bus.status(f"Arşivden çıkarılıyor: {zip_path.name}...") self.bus.progress("work", 0, "Çıkarma başlıyor...") if tool_manager._seven_extract(zip_path, C.SRC, self.bus) or tool_manager._python_unzip(zip_path, C.SRC, self.bus): inner = next((p for p in C.SRC.iterdir() if p.is_dir() and tag.replace('mysql-', '') in p.name), None) if inner: if dst.exists(): shutil.rmtree(dst, ignore_errors=True) inner.rename(dst) if not dst.exists(): raise RuntimeError(f"Kaynak çıkarma sonrası klasör bulunamadı: {dst}") return dst def _run_cmake(self, args: list[str], need_vs_env: bool, tool_mode: C.ToolMode, stop_event: threading.Event | None) -> tuple[bool, str, Path | None]: tool_manager = self.app.tool_manager cmake_exe = tool_manager.which_only_bundled("cmake") if tool_mode == C.ToolMode.BUNDLED else tool_manager.which("cmake") if not cmake_exe: self.bus.log("cmake bulunamadı.", C.LogLevel.ERR) return False, "", None vsbat = tool_manager.vs_env_paths() if need_vs_env else None if need_vs_env and not vsbat: self.bus.log("VS DevCmd/vcvars64 yok.", C.LogLevel.ERR) return False, "", None env = os.environ.copy() if tool_mode == C.ToolMode.BUNDLED: env["PATH"] = ";".join([str(p) for p in C.BUNDLED_PATHS if p.exists()] + [os.environ.get("PATH", "")]) config_type = next((arg for arg in args if "DCMAKE_BUILD_TYPE" in arg), "unknown").split("=")[-1] log_path = C.LOGS / f"cmake_configure_{config_type}_{datetime.datetime.now():%Y%m%d-%H%M%S}.log" self.bus.log(f"CMake yapılandırma çıktısı şu dosyaya loglanıyor: {log_path}", C.LogLevel.STAGE) output_lines: list[str] = [] def on_line(s: str) -> None: output_lines.append(s) ok = False on_start_cb = lambda p: self.add_pid(p.pid) if need_vs_env: bat_path = C.DEPS / "_cmk.bat" with open(bat_path, "w", encoding="utf-8", newline="\r\n") as f: bat_name = Path(vsbat).name.lower() f.write(f'@echo off\r\nchcp 65001 > nul\r\n') f.write(f'call "{vsbat}" {"x64" if "vcvars" in bat_name else "-no_logo -arch=x64"}\r\n') quoted_args = " ".join(f'"{a}"' if " " in a else a for a in args) f.write(f'"{cmake_exe}" {quoted_args}\r\n') ok = stream_proc(f'cmd /d /c "{bat_path}"', env, on_line=on_line, log_file_path=log_path, stop_event=stop_event, on_start=on_start_cb) bat_path.unlink(missing_ok=True) else: ok = stream_proc([cmake_exe, *args], env, on_line=on_line, log_file_path=log_path, stop_event=stop_event, on_start=on_start_cb) if not ok: self.bus.log(f"HATA: CMake yapılandırması başarısız. Detaylar için logu inceleyin:\n--> {log_path}", C.LogLevel.ERR) if messagebox.askyesno("CMake Hatası", f"CMake yapılandırması başarısız oldu.\n\nDetayları içeren log dosyasını açmak ister misiniz?\n({log_path.name})"): try: os.startfile(log_path) except Exception as e: messagebox.showerror("Hata", f"Log dosyası açılamadı:\n{e}") return ok, "\n".join(output_lines[-200:]), log_path def _configure_mysql(self, cfg_obj: BuildConfig, stop_event: threading.Event | None) -> bool: tool_manager = self.app.tool_manager if cfg_obj.build_type == C.BuildType.RELEASE and not cfg_obj.only_client: self.bus.log("[Install] Release -> RelWithDebInfo (PDB için gerekli).", C.LogLevel.WARN) cfg_obj.build_type = C.BuildType.RELWITHDEBINFO cfg, bld = cfg_obj.build_type.value, cfg_obj.build_dir if stop_event and stop_event.is_set(): return False self.bus.log(f"[{cfg}] Yapılandırma başlıyor → {bld}", C.LogLevel.STAGE) sccache_path = tool_manager.which_only_bundled('sccache') or tool_manager.which('sccache') is_ninja = cfg_obj.generator == C.Generator.NINJA if (bld / "CMakeCache.txt").exists() and ((bld / "build.ninja").exists() if is_ninja else any(bld.glob("*.sln"))): self.bus.log(f"[{cfg}] Geçerli cache bulundu, configure atlandı.", C.LogLevel.STAGE) return True gen_args = (["-G", "Ninja"], True) if is_ninja else (["-G", "Visual Studio 17 2022", "-A", "x64"], True) cmake_rt = ("MultiThreaded" if cfg_obj.runtime == C.Runtime.MT else "MultiThreadedDLL") + ("Debug" if cfg in (C.BuildType.DEBUG.value, C.BuildType.RELWITHDEBINFO.value) else "") common_args = ["-DWITH_SSL=system", *self._openssl_hint_args(cfg_obj.ssl_root_dir)] if cfg_obj.cxx_version not in ("Latest", ""): common_args.extend([f"-DCMAKE_CXX_STANDARD={cfg_obj.cxx_version}", "-DCMAKE_CXX_STANDARD_REQUIRED=ON"]) if sccache_path: common_args.extend([f"-DCMAKE_C_COMPILER_LAUNCHER={sccache_path}", f"-DCMAKE_CXX_COMPILER_LAUNCHER={sccache_path}"]) if cfg_obj.runtime == C.Runtime.MT: common_args.append("-DSTATIC_MSVCRT=ON") if cfg_obj.extra_defs.strip(): common_args.extend(d for d in re.split(r"\s+", cfg_obj.extra_defs.strip()) if d.startswith("-D")) args = [*gen_args[0], "-S", str(cfg_obj.source_dir), "-B", str(bld), f"-DCMAKE_BUILD_TYPE={cfg}", f"-DCMAKE_INSTALL_PREFIX={cfg_obj.install_dir}", f"-DCMAKE_MSVC_RUNTIME_LIBRARY={cmake_rt}", *common_args] ok, tail, _ = self._run_cmake(args, gen_args[1], cfg_obj.tool_mode, stop_event) if ok and not ((bld/"build.ninja").exists() if is_ninja else any(bld.glob("*.sln"))): self.bus.log(f"[{cfg}] HATA: CMake hatasız bitti ama derleme dosyaları (build.ninja/sln) OLUŞTURULMADI.", C.LogLevel.ERR) messagebox.showerror("CMake Hatası", "CMake başarıyla tamamlandı ancak derleme dosyaları (örn. build.ninja) oluşturulmadı. Lütfen Visual Studio kurulumunuzu ve CMake loglarını kontrol edin.") ok = False if not ok: self.bus.log(f"\n--- [{cfg}] CMake Son 200 Satır ---\n{tail}\n---------------------------\n", C.LogLevel.ERR) return ok def _build_mysql(self, cfg_obj: BuildConfig, task_id: str, stop_event: threading.Event | None) -> tuple[bool, bool, str]: ui = self.app.ui_manager tool_manager = self.app.tool_manager cfg, bld = cfg_obj.build_type.value, cfg_obj.build_dir is_ninja = cfg_obj.generator == C.Generator.NINJA if stop_event and stop_event.is_set(): return False, False, "stopped" if not bld.exists() or not ((bld/"build.ninja").exists() if is_ninja else any(bld.glob("*.sln"))): self.bus.log(f"[{cfg}] HATA: Derleme başlatılamıyor, yapılandırma dosyaları eksik.", C.LogLevel.ERR) return False, False, "config_missing" cmake_exe = tool_manager.which_only_bundled("cmake") if cfg_obj.tool_mode == C.ToolMode.BUNDLED else tool_manager.which("cmake") if not cmake_exe: self.bus.log("cmake bulunamadı.", C.LogLevel.ERR) return False, False, "" env = os.environ.copy() jobs = str(max(1, min(C.DEFAULT_JOBS, cfg_obj.jobs))) env["CL"] = f"/MP{jobs}" env["CMAKE_BUILD_PARALLEL_LEVEL"] = jobs target = "mysqlclient" if cfg_obj.only_client else "install" self.bus.log(f"[{cfg}] Derleme başlıyor: target={target}, jobs={jobs}", C.LogLevel.STAGE) multi_rules = False def on_line(s: str) -> None: nonlocal multi_rules ui.update_build_stats(s, task_id) if C._RE_ERR_MULTI_RULE.search(s): multi_rules = True ok = False on_start_cb = lambda p: self.add_pid(p.pid) if is_ninja: vsbat = tool_manager.vs_env_paths() bat_path = C.DEPS/f"_build_{cfg.lower()}.bat" with open(bat_path, "w", encoding="utf-8", newline="\r\n") as f: bat_name = Path(vsbat).name.lower() f.write(f'@echo off\r\nchcp 65001 > nul\r\n') f.write(f'call "{vsbat}" {"x64" if "vcvars" in bat_name else "-no_logo -arch=x64"}\r\n') f.write(f'"{cmake_exe}" --build "{bld}" --config "{cfg}" --parallel {jobs} --target {target}\r\n') ok = stream_proc(f'cmd /d /c "{bat_path}"', env, on_line=on_line, stop_event=stop_event, on_start=on_start_cb) bat_path.unlink(missing_ok=True) else: args = [cmake_exe, "--build", str(bld), "--config", cfg, "--parallel", jobs, "--target", target] ok = stream_proc(args, env, on_line=on_line, stop_event=stop_event, on_start=on_start_cb) self.bus.log(f"[{cfg}] Derleme sonucu: {'OK' if ok else 'FAIL'}", C.LogLevel.STAGE if ok else C.LogLevel.ERR) if not ok and multi_rules: return False, True, "multi_rules" if ok: self._post_build_copy(cfg_obj) return ok, False, "" def _post_build_copy(self, cfg_obj: BuildConfig) -> None: try: ssl_lib, inst_lib = cfg_obj.ssl_root_dir / "lib", cfg_obj.install_dir / "lib" inst_lib.mkdir(parents=True, exist_ok=True) if ssl_lib.exists(): for dll in ssl_lib.glob("*.dll"): shutil.copy2(dll, inst_lib) if cfg_obj.only_client: lib_file = next(cfg_obj.build_dir.rglob("mysqlclient.lib"), None) or next(cfg_obj.build_dir.rglob("libmysql.lib"), None) if lib_file: shutil.copy2(lib_file, inst_lib / "mysqlclient.lib") src_inc, inst_inc = cfg_obj.source_dir / "include", cfg_obj.install_dir / "include" if src_inc.exists(): shutil.copytree(src_inc, inst_inc, dirs_exist_ok=True) except Exception as e: self.bus.log(f"Nihai kopyalama işleminde hata: {e}", C.LogLevel.WARN) def _openssl_hint_args(self, root: Path) -> list[str]: inc, lib = root / "include", root / "lib" libssl, libcrypto = next(lib.glob("libssl*.lib"), None), next(lib.glob("libcrypto*.lib"), None) args = [f"-DOPENSSL_ROOT_DIR={root}", f"-DOPENSSL_INCLUDE_DIR={inc}"] if libssl and libcrypto: args.extend([f"-DOPENSSL_LIBRARIES={libssl};{libcrypto}", f"-DOPENSSL_CRYPTO_LIBRARY={libcrypto}", f"-DOPENSSL_SSL_LIBRARY={libssl}"]) applink = root / "ms" / "applink.c" if applink.exists(): args.append(f"-DOPENSSL_APPLINK_C={applink}") return args def confirm_reinstall_openssl(self) -> None: if messagebox.askyesno("Onay", "Mevcut OpenSSL derlemeleri silinip kaynak kodundan yeniden derlenecektir. Bu işlem zaman alabilir.\n\nDevam etmek istiyor musunuz?"): self.build_openssl_async(reinstall=True) def build_openssl_async(self, reinstall: bool) -> None: if self.app._is_busy: return self.app.set_busy(True) stop_event = threading.Event() self.app.task_manager.register(name=f"OpenSSL Kurulumu", target=self._openssl_action_worker, args=(reinstall, stop_event), stop_event=stop_event, is_build_task=False) def _openssl_action_worker(self, reinstall: bool, stop_event: threading.Event, task_id: str) -> None: ui = self.app.ui_manager success = False try: rt = C.Runtime(ui.runtime.get()) self.bus.log(f"OpenSSL derlemesi başlatılıyor (Runtime: {rt.value}, Yeniden Kur: {reinstall})", C.LogLevel.STAGE) success = self._build_openssl_sequence(rt, stop_event, reinstall) if not stop_event.is_set(): messagebox.showinfo("OpenSSL", f"OpenSSL kurulumu {'başarıyla' if success else 'hatalarla'} tamamlandı.") except Exception as e: if not stop_event.is_set(): self.bus.log(f"OpenSSL kurulumunda kritik hata: {e}\n{traceback.format_exc()}", C.LogLevel.ERR) messagebox.showerror("Hata", f"OpenSSL kurulumu başarısız oldu: {e}") finally: self.app.task_manager.task_registry[task_id]['success'] = success self.app.set_busy(False) def _build_openssl_sequence(self, runtime: C.Runtime, stop_event: threading.Event, reinstall: bool = False) -> bool: if stop_event.is_set(): return False if not self._openssl_tool_check(): raise RuntimeError("OpenSSL için gerekli araçlar eksik.") self._ensure_openssl_source(reinstall) success = True for build_type in (C.BuildType.RELEASE, C.BuildType.DEBUG): if stop_event.is_set(): success = False break try: self._build_single_openssl_cfg(runtime, build_type, reinstall, stop_event) except Exception as e: success = False self.bus.log(f"OpenSSL {build_type.value} ({runtime.value}) derlemesinde hata: {e}", C.LogLevel.ERR) self.app.after(0, self.refresh_ssl_status) return success def _ensure_openssl_source(self, reinstall: bool = False) -> None: tool_manager = self.app.tool_manager zip_path = C.DL / "openssl.zip" if reinstall: self.bus.log("Yeniden kurulum: Eski OpenSSL kaynakları ve arşivi siliniyor...", C.LogLevel.WARN) zip_path.unlink(missing_ok=True) shutil.rmtree(C.OPENSSL_SRC, ignore_errors=True) C.OPENSSL_SRC.mkdir(parents=True, exist_ok=True) if any(p.is_dir() and (p / "Configure").exists() for p in C.OPENSSL_SRC.glob("openssl-openssl-*")): return if not zip_path.exists(): self.bus.status("OpenSSL kaynak ZIP indiriliyor...") tool_manager._http_get(C.URLS["openssl"], zip_path, self.bus) self.bus.status("OpenSSL kaynakları çıkarılıyor...") if not (tool_manager._seven_extract(zip_path, C.OPENSSL_SRC, self.bus) or tool_manager._python_unzip(zip_path, C.OPENSSL_SRC, self.bus)): raise RuntimeError("OpenSSL arşivi açılamadı.") def _build_single_openssl_cfg(self, runtime: C.Runtime, build_type: C.BuildType, reinstall: bool, stop_event: threading.Event) -> None: ui = self.app.ui_manager tool_manager = self.app.tool_manager if stop_event.is_set(): return root = BuildConfig._openssl_root(runtime, build_type) if not reinstall and self._openssl_ok(root): self.bus.log(f"OpenSSL {build_type.value} ({runtime.value}) zaten derlenmiş, atlandı.", C.LogLevel.STAGE) return self.bus.log(f"--- OpenSSL {build_type.value} ({runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE) if reinstall and root.exists(): shutil.rmtree(root, ignore_errors=True) root.mkdir(parents=True, exist_ok=True) tree = next(C.OPENSSL_SRC.glob("openssl-openssl-*"), None) if not tree or not (tree / "Configure").exists(): raise RuntimeError("OpenSSL kaynak klasörü/Configure betiği bulunamadı.") vsbat = tool_manager.vs_env_paths() perl_exe = tool_manager.which_only_bundled("perl") or tool_manager.which("perl") if not vsbat or not perl_exe: raise RuntimeError("VS Ortamı veya Perl bulunamadı.") cl_flag = ("/MT" if runtime == C.Runtime.MT else "/MD") + ("d" if build_type == C.BuildType.DEBUG else "") jobs = max(1, min(128, ui.jobs.get())) bat_path = C.DEPS / f"_ossl_{runtime.value}_{build_type.value.lower()}.bat" prefix_path = root openssldir_path = root / "ssl" with open(bat_path, "w", encoding="utf-8", newline="\r\n") as f: bat_name = Path(vsbat).name.lower() f.write(f'@echo off\r\nchcp 65001 > nul\r\n') f.write(f'call "{vsbat}" {"x64" if "vcvars" in bat_name else "-no_logo -arch=x64"}\r\n') f.write(f'nmake clean > nul 2>&1\r\nset CL={cl_flag} /MP{jobs}\r\n') f.write(f'"{perl_exe}" Configure VC-WIN64A no-tests no-zlib --prefix="{prefix_path}" --openssldir="{openssldir_path}"\r\nif errorlevel 1 exit /b 10\r\n') f.write(f'nmake\r\nif errorlevel 1 exit /b 11\r\n') f.write(f'nmake install_sw\r\nif errorlevel 1 exit /b 12\r\n') env = os.environ.copy() if C.ToolMode(ui.tool_mode.get()) == C.ToolMode.BUNDLED: env["PATH"] = ";".join([str(p) for p in C.BUNDLED_PATHS if p.exists()] + [env.get('PATH', '')]) on_start_cb = lambda p: self.add_pid(p.pid) ok = stream_proc(f'cmd /d /c "{bat_path}"', env, on_line=lambda s: self.bus.log(s, C.LogLevel.CMD), stop_event=stop_event, cwd=str(tree), on_start=on_start_cb) bat_path.unlink(missing_ok=True) if stop_event.is_set() or not ok: raise RuntimeError(f"OpenSSL {build_type.value} derlemesi başarısız oldu.") self._ensure_applink_stub(root) if not self._openssl_ok(root): raise RuntimeError(f"OpenSSL {build_type.value} derlemesi sonrası doğrulama başarısız.") self.bus.log(f"✔️ OpenSSL {build_type.value} ({runtime.value}) derlemesi tamamlandı.", C.LogLevel.STAGE) def _openssl_tool_check(self) -> bool: tool_manager = self.app.tool_manager ui = self.app.ui_manager mode = C.ToolMode(ui.tool_mode.get()) missing = [] for tool_key in ("perl", "nasm"): meta = next(m for m in tool_manager.TOOLS_META if m["key"] == tool_key) if not tool_manager.detect_tool(meta, mode)[0]: missing.append(meta['name']) if missing: msg = f"OpenSSL derlemesi için eksik araçlar: {', '.join(missing)}. {'Lütfen önce bunları kurun.' if mode == C.ToolMode.BUNDLED else 'Lütfen sisteme yükleyip PATH\'e ekleyin.'}" self.bus.log(msg, C.LogLevel.ERR) messagebox.showwarning("Eksik OpenSSL Araçları", msg) return False return True def refresh_ssl_status(self) -> None: ui = self.app.ui_manager ui.ssl_tree.delete(*ui.ssl_tree.get_children()) selected_rt = C.Runtime(ui.runtime.get()) for rt in C.Runtime: rt_id = ui.ssl_tree.insert("", "end", values=(f"► Runtime: {rt.value} (Seçili: {'EVET' if selected_rt == rt else 'HAYIR'})", "—"), tags=("rt_summary",)) for bt in (C.BuildType.RELEASE, C.BuildType.DEBUG): root = BuildConfig._openssl_root(rt, bt) status = "✔️ OK" if self._openssl_ok(root) else "❌ Eksik" cfg_id = ui.ssl_tree.insert(rt_id, "end", values=(f" └─ {bt.value} Yapılandırması", status), tags=("cfg_summary",)) for name, path in (("ssl.h",root/"include/openssl/ssl.h"), ("libssl.lib",root/"lib/libssl.lib"), ("libcrypto.lib",root/"lib/libcrypto.lib")): ui.ssl_tree.insert(cfg_id, "end", values=(f" ├─ {name}", "Var" if path.exists() else "Yok")) ui.ssl_tree.tag_configure("rt_summary", font="-weight bold -size 9") ui.ssl_tree.tag_configure("cfg_summary", font="-weight normal -size 9") def _openssl_ok(self, root: Path) -> bool: return (root/"include/openssl/ssl.h").exists() and (root/"lib/libssl.lib").exists() and (root/"lib/libcrypto.lib").exists() def _ensure_applink_stub(self, root: Path) -> Path: ms = root/"ms" ms.mkdir(exist_ok=True) dst = ms/"applink.c" if not dst.exists(): dst.write_text("/* applink stub */\n#ifndef OPENSSL_NO_APPLINK\n#define OPENSSL_NO_APPLINK 1\n#endif\n", encoding="utf-8") return dst def initial_trash_cleanup(self) -> None: if any(C.TRASH.iterdir()): self.bus.log("Başlangıç: Dolu .trash klasörü bulundu. Arka planda temizleniyor...", C.LogLevel.WARN) for item in C.TRASH.iterdir(): self._cleanup_trash_folder_async(item) def _cleanup_trash_folder_async(self, folder: Path) -> None: self.app.task_manager.register(name=f".trash Temizliği ({folder.name})", target=shutil.rmtree, args=(folder,), kwargs={'ignore_errors': True}, is_build_task=False) def _do_clean_start(self) -> None: self.bus.log("Temiz başlangıç: Klasörler .trash'e taşınıyor...", C.LogLevel.STAGE) for p in (C.SRC, C.BUILD, C.INST): if p.exists() and any(p.iterdir()): trash_dest = C.TRASH / f"{p.name}-{int(time.time())}" try: shutil.move(str(p), str(trash_dest)) self.bus.log(f"Taşındı: {p} -> {trash_dest}", C.LogLevel.CMD) except OSError as e: self.bus.log(f"'{p}' taşınamadı, hata: {e}. Lütfen elle silin.", C.LogLevel.ERR) raise p.mkdir(exist_ok=True) self._cleanup_trash_folder_async(trash_dest) def empty_trash_folder(self): if not C.TRASH.exists() or not any(C.TRASH.iterdir()): messagebox.showinfo("Bilgi", ".trash klasörü zaten boş.") return if messagebox.askyesno("Onay", ".trash klasörünün tüm içeriği kalıcı olarak silinecektir. Bu işlem geri alınamaz.\n\nDevam etmek istiyor musunuz?"): self.bus.log(".trash klasörü temizleniyor...", C.LogLevel.WARN) for item in C.TRASH.iterdir(): self._cleanup_trash_folder_async(item) self.bus.log(".trash temizleme görevi başlatıldı.", C.LogLevel.INFO) class App(ttk.Window): def __init__(self): super().__init__(themename="litera", title="MySQL Builder Pro v1.0.0 - turkmmo.com - dormammu", size=(1320, 920), minsize=(1200, 860)) self.process = psutil.Process() self.q: queue.Queue = queue.Queue() self._is_busy = False self.bus = Bus(self) self.task_manager = TaskManager(self) self.tool_manager = ToolManager(self) self.build_manager = BuildManager(self) self.ui_manager = UIManager(self) self.after(100, self.ui_manager.pump_queue) self.after(10, self.tool_manager.refresh_tools) self.after(10, self.build_manager.refresh_ssl_status) self.after(10, self.build_manager.fetch_tags_async) self.after(500, self.build_manager.initial_trash_cleanup) self.after(1000, self.ui_manager.update_resource_monitor) self.after(1000, self.task_manager.update_monitor) self.after(1100, self.ui_manager.update_trash_monitor) self.ui_manager.runtime.trace_add("write", lambda *a: self.build_manager.refresh_ssl_status()) self.protocol("WM_DELETE_WINDOW", self.on_closing) def on_closing(self) -> None: if self._is_busy: if messagebox.askyesno("Çıkış Onayı", "Bir işlem çalışıyor. Çıkmak istediğinizden emin misiniz?"): self.task_manager.stop_all() self.ui_manager.save_settings() self.destroy() else: self.ui_manager.save_settings() self.destroy() def set_busy(self, busy: bool) -> None: self._is_busy = busy self.ui_manager.set_controls_state('disabled' if busy else 'normal') try: priority_class = psutil.IDLE_PRIORITY_CLASS if busy else psutil.NORMAL_PRIORITY_CLASS io_priority = psutil.IOPRIO_LOW if busy else psutil.IOPRIO_NORMAL self.process.nice(priority_class) if hasattr(self.process, "ionice"): self.process.ionice(io_priority) self.bus.log(f"Uygulama önceliği {'düşürüldü' if busy else 'normale döndü'}.", C.LogLevel.INFO) except psutil.Error as e: self.bus.log(f"Uygulama önceliği ayarlanırken hata: {e}", C.LogLevel.WARN) if __name__ == "__main__": for p in (C.LOGS, C.TOOLS, C.DL, C.SRC, C.BUILD, C.INST, C.DEPS, C.TRASH, C.OPENSSL_BASE, C.OPENSSL_SRC): p.mkdir(parents=True, exist_ok=True) app = App() app.mainloop()
MySQL Builder Pro ile Metin2 Geliştirme Sürecini Kolaylaştırın
Metin2 özel sunucu geliştirme sürecinde karşılaşılan en yaygın sorunlardan birisi derleme süreçleridir. Özellikle C++ tabanlı sistemlerde, MySQL bağlantıları sırasında 'mysqlclient.lib' hatası gibi derleme sorunları geliştiricileri zorlayabilir. Bu yazıda, [Dev Paylaşım] MySQL mysqlclient.lib Derleme Sorununa Son! adlı projeyi tanıtarak, bu sorunu kalıcı olarak nasıl çözeceğinizi anlatıyoruz.
Metin2 Geliştirme Ortamında MySQL Entegrasyonu
Metin2 özel sunucularında genellikle auth server ve game server olmak üzere iki temel yapı bulunur. Bu yapılar genellikle MySQL veritabanı ile haberleşir. Ancak, özellikle C++ kaynak kodlar üzerinde çalışırken MySQL bağlantısı sırasında bazı derleme hatalarıyla karşılaşılabilir. Bunlardan birisi 'mysqlclient.lib' dosyasının bulunamaması veya eksik derlenmesidir. Bu durum, proje derlenirken linker hatasına neden olur.
MySQL Builder Pro Nedir?
MySQL Builder Pro, Metin2 özel sunucu geliştiricileri için özel olarak tasarlanmış bir araçtır. Bu aracın temel amacı, MySQL kütüphanelerini doğru şekilde derlemek ve geliştiricilere kolayca entegre edilebilir bir yapı sunmaktır. mysqlclient.lib dosyasının derleme sürecini otomatikleştirerek, geliştiricilerin zaman kaybetmeden çalışmaya devam etmelerini sağlar.
mysqlclient.lib Derleme Sorunu Neden Oluşur?
C++ projelerinde MySQL ile bağlantı kurarken, derleyici mysqlclient.lib dosyasını bekler. Bu dosya, MySQL istemci kütüphanesinin derlenmiş halidir. Eğer doğru bir şekilde yapılandırılmazsa, linker hatası alınır. Bu durum özellikle farklı MySQL sürümleriyle çalışılırken daha sık ortaya çıkar. Ayrıca, 32-bit ve 64-bit sistem uyumsuzlukları da bu hatayı tetikleyebilir.
MySQL Builder Pro ile Çözüm
MySQL Builder Pro, bu sorunu adım adım otomatikleştirerek çözer. Kullanıcı dostu arayüzü sayesinde, sadece birkaç tıklama ile mysqlclient.lib dosyası doğru şekilde derlenebilir. Bu sayede Metin2 sunucularında auth-server ve game-server bağlantıları daha hızlı ve güvenli bir şekilde sağlanabilir. Arayüz üzerinden MySQL versiyon seçimi, derleme türü (debug/release) ve sistem mimarisi (x86/x64) ayarlanabilmektedir.
Metin2 Sunucu Geliştirme İçin Önerilen Yapılandırmalar
Metin2 özel sunucu geliştirme yaparken dikkat etmeniz gereken bazı önemli konfigürasyonlar vardır. Öncelikle, MySQL sürümünüzle derleme sırasında kullandığınız kütüphane sürümlerinin uyumlu olması gerekir. MySQL Builder Pro bu uyumu sağlamanız için rehberlik eder. Ayrıca, Visual Studio sürümü, Windows SDK ve compiler ayarları da önemli rol oynar. mysqlclient.lib dosyasının doğru yola yerleştirilmesi, linker ayarlarının yapılması ve include dizinlerinin belirlenmesi gibi işlemler, MySQL Builder Pro sayesinde otomatik hale getirilmiştir.
Metin2 Geliştiricileri İçin Kaynak Kod Paylaşımları
Metin2 geliştirme dünyasında kaynak kod paylaşımı, topluluk tarafından büyük önem taşır. Martysama gibi isimlerin sağladığı C++ sistemler, Python GUI uygulamaları ve Py Root yapıları, birçok geliştiriciye ilham vermiştir. MySQL Builder Pro da bu doğrultuda, geliştiricilerin daha az derleme stresi yaşaması için geliştirilmiş değerli bir araçtır. Bu tip paylaşımlar sayesinde, Metin2 özel sunucu geliştirme süreci hız kazanır ve hata oranı düşer.
Sonuç
Metin2 özel sunucu geliştirme sürecinde mysqlclient.lib gibi derleme hataları, geliştirici motivasyonunu düşürebilir. Ancak, MySQL Builder Pro gibi araçlar sayesinde bu tür sorunlar minimize edilebilir. Hem yeni başlayanlar hem de deneyimli geliştiriciler için büyük kolaylık sağlayan bu araç, Metin2 gelişimine katkı sağlarken, geliştirme süresini kısaltmaktadır. Eğer Metin2 özel sunucu geliştiriyorsanız ve MySQL ile ilgili derleme sorunları yaşıyorsanız, mutlaka MySQL Builder Pro'yu denemelisiniz.
Kaynak: Metin2 Lobby
Simplify Your Metin2 Development Process with MySQL Builder Pro
One of the most common issues encountered during Metin2 private server development is related to compilation processes. Especially in C++ based systems, errors like 'mysqlclient.lib' during MySQL connections can be challenging for developers. In this article, we introduce the project [Dev Sharing] End to MySQL mysqlclient.lib Compilation Issue! and explain how to permanently resolve this issue.
MySQL Integration in Metin2 Development Environment
In Metin2 private servers, two main structures usually exist: the auth server and the game server. These structures often communicate with MySQL databases. However, while working on C++ source codes, certain compilation errors may occur during MySQL connections. One such error is the failure to locate or incorrectly compile the 'mysqlclient.lib' file, which causes a linker error during project compilation.
What is MySQL Builder Pro?
MySQL Builder Pro is a tool specifically designed for Metin2 private server developers. Its main purpose is to correctly compile MySQL libraries and provide an easily integrable structure for developers. By automating the compilation process of the mysqlclient.lib file, it allows developers to continue their work without wasting time.
Why Does the mysqlclient.lib Compilation Error Occur?
When connecting to MySQL in C++ projects, the compiler expects the mysqlclient.lib file. This file is the compiled version of the MySQL client library. If not configured properly, a linker error occurs. This situation arises more frequently when working with different MySQL versions. Additionally, 32-bit and 64-bit system incompatibilities can trigger this error.
Solution with MySQL Builder Pro
MySQL Builder Pro resolves this issue by automatically streamlining the process with just a few clicks. Thanks to its user-friendly interface, the mysqlclient.lib file can be compiled correctly in no time. This enables faster and safer connections between auth-server and game-server in Metin2 servers. Users can select MySQL versions, compilation types (debug/release), and system architectures (x86/x64) through the interface.
Recommended Configurations for Metin2 Server Development
There are several important configurations to consider when developing Metin2 private servers. First, ensure that your MySQL version matches the library versions used during compilation. MySQL Builder Pro guides you in achieving this compatibility. Additionally, the Visual Studio version, Windows SDK, and compiler settings play crucial roles. With MySQL Builder Pro, tasks such as placing the mysqlclient.lib file in the correct path, setting up linker options, and defining include directories are automated.
Source Code Sharing for Metin2 Developers
In the world of Metin2 development, sharing source code is highly valued by the community. Systems provided by figures like Martysama, including C++ systems, Python GUI applications, and Py Root structures, have inspired many developers. MySQL Builder Pro, following this direction, helps reduce compilation stress for developers. Such shares accelerate the development process and decrease error rates in Metin2 private server creation.
Conclusion
Compilation errors like mysqlclient.lib during Metin2 private server development can lower developer motivation. However, tools like MySQL Builder Pro can minimize these issues. Providing great convenience for both newcomers and experienced developers, this tool contributes to Metin2's development while shortening the overall development timeline. If you're developing Metin2 private servers and experiencing MySQL-related compilation issues, give MySQL Builder Pro a try.
Source: Metin2 Lobby
