melankolıa18 1
melankolıa18
romegames 1
romegames
Krutzo 1
Krutzo
shrpnl 1
shrpnl
Best Studio 1
Best Studio
D 1
delimuratt
Aliyldrim 1
Aliyldrim
Mt2Hizmet 1
Mt2Hizmet
Hikaye Ekle
Reklam vermek için turkmmo@gmail.com

[Araç] Lib Builder Pro - Otomatik Kütüphane Derleme Aracı (libjpeg, DevIL, Crypto++)

  • Konuyu başlatan Konuyu başlatan dormammu
  • Başlangıç tarihi Başlangıç tarihi
  • Cevaplar Cevaplar 8
  • Görüntüleme Görüntüleme 690

dormammu

Level 4
TM Üye
Üye
Katılım
6 Eyl 2017
Konular
53
Mesajlar
358
Çözüm
1
Online süresi
1mo 8d
Reaksiyon Skoru
169
Altın Konu
1
TM Yaşı
8 Yıl 9 Ay 3 Gün
Başarım Puanı
129
Yaş
32
MmoLira
6,460
DevLira
78
Ticaret - 100%
1   0   0

ROHAN2 WORLD 1-120 TR TİPİ OFFICIAL YOHARA, BALATHOR VE AMON! 80. GÜNÜNDE! +10.000 ONLİNE! HİLE VE BOT %100 ENGELLİ HEMEN TIKLA!

GÜNCELLENDİ
1762291070706.png
Selamlar Turkmmo ailesi,​

Metin2 source derleme sürecinin en baş ağrıtan kısımlarından birinin kütüphaneleri (library) derlemek olduğunu hepimiz biliyoruz. libjpeg, DevIL, Crypto++ gibi temel bağımlılıkları doğru ayarlarla, doğru mimaride (32-bit client için!) derlemek, özellikle bu işe yeni başlayanlar için tam bir kabusa dönüşebiliyor.

Forumda "kütüphane hatası alıyorum", "build edemiyorum" gibi onlarca konu gördükten sonra bu süreci tamamen otomatikleştiren bir araç geliştirdim! Artık tek bir programla, hiçbir ayarla boğuşmadan, tüm kütüphanelerinizi dakikalar içinde hazır hale getirebileceksiniz.




💣 Bu Araç Ne İşe Yarıyor? Neden Kullanmalısınız? 💣



Bu program, Metin2 client ve server derlemek için gereken en temel 3 kütüphaneyi sizin için sıfırdan oluşturur.

  • Zahmetsiz Kurulum: Derleme için gereken tüm programları (Git, CMake, Ninja vb.) tek tıkla kendisi indirir. Size sadece Visual Studio kurmak kalır.
  • Metin2 Uyumlu: Client için zorunlu olan 32-bit (x86) mimarisini ve isteğe bağlı olarak 64-bit (x64) mimarisini destekler.
  • Tam Otomasyon: "Tüm Kütüphaneleri Derle" butonu ile bağımlılıkları (DevIL için libjpeg gibi) kendi gözeterek tüm kütüphaneleri sırayla derler.
  • Düzenli ve Hazır Çıktı: Derlenen tüm dosyaları (.lib, .h, .dll) alıp doğrudan source'unuzun extern klasörüne atabileceğiniz şekilde hazırlar. Artık extern/include ve extern/lib klasörlerini aramakla uğraşmak yok!



⚠️ Kurulum Öncesi GEREKSİNİMLER (MUTLAKA OKUYUN!) ⚠️



Programı çalıştırmadan önce sisteminizde iki şeyin kurulu ve ayarlı olması ŞARTTIR!



1. Visual Studio 2022 (C++ İş Yükü)



Bu adım olmadan program KESİNLİKLE ÇALIŞMAZ!

  • Visual Studio 2022 kurulu olmalı.
  • Visual Studio Installer üzerinden "Masaüstü geliştirme (C++)" (Desktop development with C++) iş yükünün seçili olduğundan emin olun. Bu, derleyicinin kendisidir.


2. Python ve Gerekli Paketler



Bu araç Python ile yazılmıştır. Bu yüzden sisteminizde Python kurulu olmalıdır.

  • Python Sürümü: Python 3.14 veya daha yenibir sürüm gereklidir.
    • PowerShell'i açıp python --version komutuyla sürümünüzü kontrol edebilirsiniz.
    • Kurulu değilse, indirip kurarken "Add Python to PATH" seçeneğini işaretlemeyi UNUTMAYIN!
  • Gerekli Python Paketleri: Programın çalışması için birkaç ek pakete ihtiyacı var. PowerShell'i açıp aşağıdaki komutu çalıştırarak hepsini tek seferde kurun:

    [CODE title="Powershell"]pip install requests psutil ttkbootstrap[/CODE]



🚀 Kurulum ve Çalıştırma (SIFIRDAN ANLATIM) 🚀



İndirme linki yok! Kodu doğrudan buraya yapıştıracağım. Aşağıdaki adımları sırayla takip edin:

1. Boş Bir Klasör Oluşturun:Masaüstünüzde veya istediğiniz bir yerde LibBuilder gibi bir isimle yeni bir klasör oluşturun.

2. Metin Belgesi Oluşturun:Oluşturduğunuz bu LibBuilder klasörünün içine girin. Sağ tıklayıp Yeni -> Metin Belgesi deyin.

3. Dosyanın Adını ve Uzantısını Değiştirin:Oluşan Yeni Metin Belgesi.txt dosyasının adını LibBuilder.py olarak değiştirin.

  • DİKKAT: Dosya uzantılarının görünür olduğundan emin olun! Eğer .txt kısmını göremiyorsanız, klasör seçeneklerinden "Bilinen dosya türleri için uzantıları gizle" seçeneğinin tikini kaldırın. Dosyanın adı tam olarak derleyici.py olmalıdır.
4. Kodu Yapıştırın:Aşağıda --- KOD BLOĞU --- başlığı altında verdiğim kodun tamamını kopyalayın. Oluşturduğunuz derleyici.py dosyasına sağ tıklayıp "Düzenle" deyin (veya Notepad++ ile açın), kopyaladığınız kodu içine yapıştırın ve dosyayı kaydedip kapatın.

5. Programı Çalıştırın:

  • LibBuilder klasörünün içindeyken, üstteki adres çubuğuna powershell yazıp Enter'a basın. Bu, o klasörde bir PowerShell penceresi açacaktır.
  • Açılan PowerShell ekranına aşağıdaki komutu yazın ve Enter'a basın:
[CODE lang="python" title="powershell"]python .\LibBuilder.py[/CODE]
Tebrikler! Programın arayüzü karşınıza gelecek.




Python:
# -*- 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, field
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 Arch(str, Enum):
        X86 = "x86"
        X64 = "x64"

    class Runtime(str, Enum):
        MT = "MT"
        MD = "MD"

    class BuildType(str, Enum):
        RELEASE = "Release"
        DEBUG = "Debug"

    class ToolMode(str, Enum):
        BUNDLED = "bundled"
        SYSTEM = "system"
     
    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"
    CONFIG_FILE = ROOT / "config.json"
 
    INST_LIBJPEG_TURBO = INST / "libjpeg-turbo"
    INST_DEVIL = INST / "devil"
    INST_CRYPTOPP = INST / "cryptopp"
    INST_LZO = INST / "lzo"
    INST_SPDLOG = INST / "spdlog"
    INST_NLOHMANN_JSON = INST / "nlohmann-json"
    INST_PYTHON = INST / "python"

    LIB_META = {
        "libjpeg-turbo": {"name": "libjpeg-turbo", "type": "git", "repo": "https://github.com/libjpeg-turbo/libjpeg-turbo", "tag_prefix": "", "tag_filter": re.compile(r"^\d+\.\d+\.\d+$")},
        "devil": {"name": "DevIL", "type": "git", "repo": "https://github.com/DentonW/DevIL", "tag_prefix": "v", "tag_filter": re.compile(r"^v\d+\.\d+\.\d+$")},
        "cryptopp": {"name": "Crypto++", "type": "git", "repo": "https://github.com/weidai11/cryptopp", "tag_prefix": "CRYPTOPP_", "tag_filter": re.compile(r"^CRYPTOPP_\d+_\d+(_\d+)?$")},
        "lzo": {"name": "LZO", "type": "url", "url": "https://www.oberhumer.com/opensource/lzo/download/lzo-2.10.tar.gz", "version": "2.10"},
        "spdlog": {"name": "spdlog", "type": "git", "repo": "https://github.com/gabime/spdlog", "tag_prefix": "v", "tag_filter": re.compile(r"^v\d+\.\d+\.\d+$")},
        "nlohmann-json": {"name": "nlohmann/json", "type": "git", "repo": "https://github.com/nlohmann/json", "tag_prefix": "v", "tag_filter": re.compile(r"^v\d+\.\d+\.\d+$")},
        "python": {"name": "Python", "type": "git", "repo": "https://github.com/python/cpython", "tag_prefix": "v", "tag_filter": re.compile(r"^v\d+\.\d+(\.\d+)?$")},
    }

    BUILD_ORDER = ["libjpeg-turbo", "devil", "cryptopp", "lzo", "spdlog", "nlohmann-json", "python"]

    DEFAULT_JOBS = max(1, (os.cpu_count() or 4) - 2)
    DISK_MIN_GB = 2.0
    MAX_LOG_LINES = 5000
    _RE_NINJA = re.compile(r"\[(\d+)\s*/\s*(\d+)\]")

    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",
        "nasm": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/win64/nasm-2.16.03-win64.zip",
    }
    BUNDLED_PATHS = [TOOLS, TOOLS / "7zip", TOOLS / "cmake" / "bin", TOOLS / "mingit" / "cmd", TOOLS / "nasm"]

@dataclass
class BuildConfig:
    lib_key: str
    tag: str
    runtime: C.Runtime
    tool_mode: C.ToolMode
    jobs: int
    prefer_clean: bool
    architectures: list[C.Arch]
    build_configs: list[C.BuildType]
    extra_opts: dict = field(default_factory=dict)

    @property
    def source_dir(self) -> Path: return C.SRC / self.lib_key / self.tag
    def build_dir_for(self, arch: C.Arch, build_type: C.BuildType) -> Path: return C.BUILD / self.lib_key / self.tag / arch.value / self.runtime.value / build_type.value.lower()
    def install_dir_for(self, arch: C.Arch, build_type: C.BuildType) -> Path: return getattr(C, f"INST_{self.lib_key.upper().replace('-', '_')}") / arch.value / self.runtime.value / build_type.value.lower()

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))
    def work_mode(self, mode: str): self.q.put(("work_mode", mode))
    def work_control(self, action: str): self.q.put(("work_control", action))
    def work_text(self, text: str): self.q.put(("work_text", text))

@contextlib.contextmanager
def temp_env_path(extra_paths: list[Path]):
    old_path = os.environ.get("PATH", "")
    new_path = ";".join([str(p) for p in extra_paths if p.exists()] + [old_path])
    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=30).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 = subprocess.CREATE_NO_WINDOW | (subprocess.BELOW_NORMAL_PRIORITY_CLASS if low_pri else 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)
                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()
     
        active_pids = self.app.build_manager.get_active_pids()
        if active_pids:
            for pid in active_pids:
                try: psutil.Process(pid).kill()
                except psutil.Error: pass
        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.lib_tabs: dict[str, dict[str, Any]] = {}
        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)
        common_panel = ttk.Labelframe(parent, text="Ortak Derleme Ayarları", padding=10); common_panel.grid(row=0, column=0, sticky="ew")
        self.notebook = ttk.Notebook(parent, padding=0); self.notebook.grid(row=1, column=0, sticky="nsew", pady=(5,0))
        self._create_common_panel_content(common_panel)
        for lib_key, meta in C.LIB_META.items():
            tab = ttk.Frame(self.notebook, padding=10)
            self.notebook.add(tab, text=meta["name"])
            self.lib_tabs[lib_key] = {"_tab_id": tab}
            self._create_library_tab(tab, lib_key)
        tools_panel = ttk.Labelframe(parent, text="Araç Durumu", padding=10); tools_panel.grid(row=2, column=0, sticky="ew", pady=(10,0))
        dash = ttk.Labelframe(parent, text="İşlem Durumu", padding=10); dash.grid(row=3, column=0, sticky="ew", pady=(10, 0))
        self._create_tools_panel(tools_panel)
        self._create_status_panel_content(dash)

    def _create_library_tab(self, parent: ttk.Frame, lib_key: str):
        parent.grid_columnconfigure(1, weight=1)
        f_ver = ttk.Frame(parent); f_ver.grid(row=0, column=0, columnspan=3, sticky="ew", pady=2)
        meta = C.LIB_META[lib_key]
        ttk.Label(f_ver, text=f"{meta['name']} Sürümü:", width=20).pack(side=LEFT)

        if meta.get('type') == 'url':
            version_str = meta.get('version', 'Sabit Sürüm')
            ttk.Label(f_ver, text=version_str, font="-weight bold").pack(side=LEFT, padx=5, fill=X, expand=True)
            self.lib_tabs[lib_key]['fixed_version'] = version_str
        else:
            cmb_tag = self._register_control(ttk.Combobox(f_ver, width=25, state="readonly"))
            cmb_tag.pack(side=LEFT, padx=5, fill=X, expand=True)
            self.lib_tabs[lib_key]['cmb_tag'] = cmb_tag
            btn_fetch = ttk.Button(f_ver, text="Listele", command=lambda k=lib_key, c=cmb_tag: self.app.build_manager.fetch_tags_async(k, c), width=8, bootstyle=(SECONDARY, OUTLINE))
            btn_fetch.pack(side=LEFT)
       
        f_opts = ttk.Labelframe(parent, text="Özel Ayarlar", padding=5); f_opts.grid(row=1, column=0, columnspan=3, sticky="nsew", pady=(10,0))
     
        opts_dict = {}
        if lib_key in ['libjpeg-turbo', 'devil', 'lzo', 'python']:
            var_static = self._register_control(ttk.BooleanVar(value=True))
            var_shared = self._register_control(ttk.BooleanVar(value=True))
            self._register_control(ttk.Checkbutton(f_opts, text="Statik Kütüphane (.lib) Derle", variable=var_static)).pack(anchor=W)
            self._register_control(ttk.Checkbutton(f_opts, text="Dinamik Kütüphane (.dll) Derle", variable=var_shared)).pack(anchor=W)
            opts_dict = {'static': var_static, 'shared': var_shared}
        else:
            ttk.Label(f_opts, text="Bu kütüphane için özel bir ayar bulunmuyor.").pack(padx=5, pady=5)
     
        self.lib_tabs[lib_key]['opts'] = opts_dict

    def _create_common_panel_content(self, parent: ttk.Frame):
        parent.grid_columnconfigure(1, weight=1); parent.grid_columnconfigure(3, weight=1)
     
        ttk.Label(parent, text="Runtime:").grid(row=0, 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(parent, textvariable=self.runtime, values=[r.value for r in C.Runtime], width=10, state="readonly")).grid(row=0, column=1, sticky="ew", padx=5)
     
        ttk.Label(parent, text="Paralel İş:").grid(row=0, column=2, sticky="w", padx=(15, 5), pady=2)
        self.jobs = self._register_control(ttk.IntVar(value=C.DEFAULT_JOBS))
        self._register_control(ttk.Spinbox(parent, from_=1, to=128, textvariable=self.jobs, width=6)).grid(row=0, column=3, sticky="ew", padx=5)
     
        ttk.Label(parent, text="Araç Modu:").grid(row=1, column=0, sticky="w", padx=5, pady=2)
        f_tools = ttk.Frame(parent); f_tools.grid(row=1, column=1, sticky="w")
        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)
     
        ttk.Label(parent, text="Mimari:").grid(row=1, column=2, sticky="w", padx=(15, 5), pady=2)
        f_arch = ttk.Frame(parent); f_arch.grid(row=1, column=3, sticky="w")
        self.build_x64 = self._register_control(ttk.BooleanVar(value=True))
        self._register_control(ttk.Checkbutton(f_arch, text="x64 (64-bit)", variable=self.build_x64, bootstyle=PRIMARY)).pack(side=LEFT)
        self.build_x86 = self._register_control(ttk.BooleanVar(value=True))
        self._register_control(ttk.Checkbutton(f_arch, text="x86 (32-bit)", variable=self.build_x86, bootstyle=PRIMARY)).pack(side=LEFT, padx=10)

        f_opts = ttk.Frame(parent); f_opts.grid(row=0, column=4, rowspan=2, sticky="e", padx=(20,0))
        self.build_release = self._register_control(ttk.BooleanVar(value=True))
        self._register_control(ttk.Checkbutton(f_opts, 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(f_opts, text="Debug Derle", variable=self.build_debug, bootstyle=PRIMARY)).pack(anchor="w", pady=1)
        self.prefer_clean = self._register_control(ttk.BooleanVar(value=False))
        self._register_control(ttk.Checkbutton(f_opts, text="Temiz başlangıç", variable=self.prefer_clean, bootstyle=PRIMARY)).pack(anchor="w", pady=1)

    def _create_tools_panel(self, parent: ttk.Frame) -> None:
        parent.grid_columnconfigure(0, weight=1)
        self.tree = ttk.Treeview(parent, columns=("tool", "status", "ver", "src", "path"), show="headings", height=4, bootstyle=INFO)
        for c, t, w in (("tool","Araç",120), ("status","Durum",80), ("ver","Sürüm",100), ("src","Kaynak",70), ("path","Yol",300)):
            self.tree.heading(c, text=t); self.tree.column(c, width=w, anchor=W)
        self.tree.grid(row=0, column=0, sticky="nsew")
        btn_frame = ttk.Frame(parent); 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_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="nse", padx=(20, 0))
        self.btn_start = ttk.Button(btns, text="Seçili Kütüphaneyi Derle", bootstyle=SUCCESS, width=22, command=self.app.build_manager.start_build); self.btn_start.pack(pady=2, fill=X)
        self.btn_build_all = ttk.Button(btns, text="Tüm Kütüphaneleri Derle", bootstyle=(SUCCESS, OUTLINE), width=22, command=self.app.build_manager.start_build_all); self.btn_build_all.pack(pady=2, fill=X)
        self.btn_stop = ttk.Button(btns, text="Durdur (Acil)", bootstyle=(DANGER, OUTLINE), width=22, 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="Tüm 'build' Klasörünü Temizle", bootstyle=(WARNING, OUTLINE), width=22, command=self.app.build_manager.clear_build_folder); 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=22, command=self._open_install_folder); self.btn_open_install.pack(pady=2, fill=X)
        self.btn_info = ttk.Button(btns, text="ℹ️ Bilgi & Yardım", command=self._show_info_modal, bootstyle=(SECONDARY, OUTLINE)); self.btn_info.pack(fill=X, pady=(8, 0))

    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")
        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(): self.log.tag_config(level.value, foreground=fg, background=bg)

    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:
        try:
            while not self.app.q.empty():
                kind, *rest = self.app.q.get_nowait()
                if kind == "log":
                    timestamp = datetime.datetime.now().strftime('%H:%M:%S')
                    message, level = cast(str, rest[0]), cast(str, rest[1])
                    is_scrolled_to_bottom = self.log.yview()[1] >= 1.0
                    for ln in message.splitlines():
                        start_index = self.log.index("end-1c")
                        self.log.insert(END, f"[{timestamp}] {ln}\n")
                        end_index = self.log.index("end-1c")
                        self.log.tag_add(level, start_index, end_index)
                    if is_scrolled_to_bottom: self.log.see(END)
                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
                elif kind == "work_mode": self.pb_work.config(mode=rest[0])
                elif kind == "work_control" and rest[0] == "start": self.pb_work.start()
                elif kind == "work_control" and rest[0] == "stop": self.pb_work.stop()
                elif kind == "work_text": self.lbl_work_txt.config(text=rest[0])
        except queue.Empty: pass
        finally: self.app.after(100, self.pump_queue)

    def set_controls_state(self, state: str) -> None:
        self.btn_start.config(state=state)
        self.btn_build_all.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
        self.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:
                total_mb_s = (current_io.read_bytes - self.last_disk_io.read_bytes + current_io.write_bytes - self.last_disk_io.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): pass
        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**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.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.build_release.set(settings.get("build_release", True))
                self.build_debug.set(settings.get("build_debug", True))
                self.build_x64.set(settings.get("build_x64", True))
                self.build_x86.set(settings.get("build_x86", True))
                self.prefer_clean.set(settings.get("prefer_clean", False))
                for lib_key, tab_widgets in self.lib_tabs.items():
                    meta = C.LIB_META.get(lib_key, {})
                    if meta.get('type') != 'url':
                        last_tag = settings.get(f"last_tag_{lib_key}")
                        if last_tag: tab_widgets['last_tag_from_config'] = last_tag
                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 = {
            "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(),
            "build_x64": self.build_x64.get(), "build_x86": self.build_x86.get(),
            "prefer_clean": self.prefer_clean.get()
        }
        for lib_key, tab_widgets in self.lib_tabs.items():
            if 'cmb_tag' in tab_widgets:
                tag = tab_widgets['cmb_tag'].get()
                if tag: settings[f"last_tag_{lib_key}"] = tag
        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 _open_install_folder(self) -> None:
        C.INST.mkdir(parents=True, exist_ok=True)
        try: os.startfile(C.INST)
        except Exception as e: 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 = """Lib Builder Pro v7.4 (Final) Kullanım Rehberi
============================================

Bu araç libjpeg-turbo, DevIL, Crypto++, LZO, spdlog ve nlohmann/json kütüphanelerini Windows ortamında 32-bit ve 64-bit olarak derleme/kurma sürecini otomatikleştirmek için tasarlanmıştır.

---
ÖN GEREKSİNİMLER
---
- **Windows 10/11 (64-bit)**
- **Visual Studio 2022:** "Masaüstü geliştirme (C++)" iş yükünün kurulu olması ZORUNLUDUR.

---
ADIM ADIM KULLANIM
---
1.  **Araç Kurulumu:** Uygulamayı ilk açtığınızda "Araç Durumu" panelinden **"Eksikleri Kur"** butonuna tıklayarak gerekli araçları (CMake, Ninja vb.) kurun.
2.  **Derleme Ayarları:**
    -   Sürüm seçimi gerektiren kütüphaneler için derlemek istediğiniz sürümü **"Listele"** butonu ile seçin.
    -   **Ortak Ayarlar** bölümünden `Runtime`, `Mimari` ve `Release/Debug` gibi seçenekleri belirleyin.
3.  **Derlemeyi Başlatma:**
    -   **"Seçili Kütüphaneyi Derle":** Aktif olan sekmeyi derler.
    -   **"Tüm Kütüphaneleri Derle":** Tüm kütüphaneleri sırayla (bağımlılıkları gözeterek) derler.
4.  **Sonuçlar:** Derleme bittiğinde, dosyalar `install/<kütüphane_adı>/<mimari>/...` klasörüne kaydedilir.

---
KÜTÜPHANEYE ÖZEL NOTLAR
---
-   **DevIL:** Bu kütüphane, libjpeg-turbo'ya bağımlıdır. "Tümünü Derle" seçeneği bu sırayı otomatik yönetir. Tek tek derliyorsanız, **önce libjpeg-turbo'yu derlemeniz GEREKİR.**
-   **Crypto++, spdlog, Python:** MSBuild/CMake ile statik kütüphane (.lib) olarak derlenirler.
-   **LZO:** CMake ile derlenir.
-   **nlohmann/json:** Bu kütüphane sadece başlık dosyalarından oluşur (header-only). Derleme işlemi yerine sadece ilgili dosyalar `install` klasörüne kopyalanır.
"""
        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)

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": ["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":"nasm", "name":"NASM", "exe":"nasm", "ensure":lambda bus, se: self._ensure_zip_tool("nasm", C.URLS["nasm"], "nasm", bus, se), "vercmd":["nasm", "-v"]},
        ]
     
    def vs_env_path(self) -> str | None:
        vswhere = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe")
        if vswhere.exists():
            cmd = [str(vswhere), "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"]
            base_path = run_out(subprocess.list2cmdline(cmd)).splitlines()
            base = Path((base_path[0] if base_path else "").strip())
            if base.is_dir():
                candidate = base / "VC/Auxiliary/Build/vcvarsall.bat"
                if candidate.exists(): return str(candidate)
        for ver in ("2022", "2019"):
            for ed in ("Community", "Professional", "Enterprise", "BuildTools"):
                p = Path(rf"C:\Program Files\Microsoft Visual Studio\{ver}\{ed}\VC\Auxiliary\Build\vcvarsall.bat")
                if p.exists(): return str(p)
        return None

    def which(self, name: str, bundled_only: bool) -> str | None:
        paths = C.BUNDLED_PATHS if bundled_only else (os.environ.get("PATH", "").split(os.pathsep) + [str(p) for p in C.BUNDLED_PATHS])
        for folder in paths:
            if not folder: continue
            p = Path(folder)
            if not p.is_dir(): continue
            for ext in ("", ".exe"):
                fp = p / (name + ext)
                if fp.exists() and fp.is_file(): return str(fp)
        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 = next((path for exe in executables if (path := self.which(exe, mode == C.ToolMode.BUNDLED))), None)
        if not p and mode == C.ToolMode.SYSTEM:
            p = next((path for exe in executables if (path := shutil.which(exe))), None)
        if not p: return None, None, None
     
        src = "Bundled" if Path(p).resolve().is_relative_to(C.TOOLS.resolve()) else "System"
        vercmd = [p] + meta.get("vercmd", ["--version"])[1:]
        out = run_out(vercmd, cwd=Path(p).parent)
        if out.startswith("ERR:"): return p, "Hata", src
        ver = next((m.group(1) for pat in [r"v?(\d+\.\d+(?:\.\d+)?)", r"version[\s:]*(\d+\.\d+(?:\.\d+)?)"] if (m := re.search(pat, out, re.I))), out.splitlines()[0][:40])
        return p, 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)
            ui.tree.insert("", END, iid=meta["key"], values=(meta["name"], "✔️ Yüklü" if path else "❌ Yok", ver or "-", src or "-", path or "-"), tags=() if path else ("missing",))
        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. Devam 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 = [m for m in self.TOOLS_META if not self.detect_tool(m, C.ToolMode.BUNDLED)[0]] if missing else self.TOOLS_META
            if not metas_to_install and missing:
                self.bus.log("Tüm araçlar zaten .tools klasöründe yüklü.", C.LogLevel.INFO)
                messagebox.showinfo("Araçlar", "Eksik araç bulunamadı."); return
            with ThreadPoolExecutor() as executor:
                futures = [executor.submit(m["ensure"], self.bus, stop_event) for m in metas_to_install if "ensure" in m]
                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(): 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 _http_get(self, url: str, dest: Path, bus: Bus) -> bool:
        bus.log(f"[DL] Başlıyor: {Path(url).name}", C.LogLevel.STAGE)
        dest.parent.mkdir(parents=True, exist_ok=True)
        try:
            with requests.get(url, stream=True, timeout=60, verify=True) as r:
                r.raise_for_status()
                total_size = int(r.headers.get("Content-Length", 0))
                done, last_pc = 0, -1
                with open(dest, "wb") as f:
                    for chunk in r.iter_content(256 * 1024):
                        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"); return True
        except requests.exceptions.SSLError as e:
             bus.log(f"[DL] SSL Hatası: {e}. Sunucu sertifikasıyla ilgili bir sorun olabilir.", C.LogLevel.ERR)
             return False
        except requests.RequestException as e:
            bus.log(f"[DL] Hata: {e}", C.LogLevel.ERR)
            return False

    def _seven_extract(self, arc: Path, out: Path, bus: Bus) -> bool:
        seven = self.which("7z", False) or self.which("7zr", False)
        if not seven:
            try:
                self._ensure_7zip(bus, None)
                seven = self.which("7z", False) or self.which("7zr", False)
            except Exception as e:
                bus.log(f"7-Zip otomatik olarak kurulamadı: {e}", C.LogLevel.ERR)
        if not seven:
            bus.log("7-Zip (7z.exe or 7zr.exe) bulunamadı.", C.LogLevel.ERR)
            return False
        parser = SevenZipProgressParser(bus)
        cmd = f'"{seven}" x "{arc}" -o"{out}" -y -bsp1'
        return stream_proc(cmd, os.environ.copy(), on_line=parser.parse_line)

    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
        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() and 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)
        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ı")
        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 (zdir / "7z.exe").exists(): return

        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)
        if not (zdir / "7z.exe").exists():
            raise RuntimeError("7-Zip extraction failed. 7z.exe was not found after extraction.")
         
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: self.active_pids.add(pid)
    def remove_pid(self, pid: int) -> None: self.active_pids.discard(pid)
    def get_active_pids(self) -> set[int]: return self.active_pids.copy()
    def clear_active_pids(self) -> None: self.active_pids.clear()

    def _get_common_build_config_parts(self) -> dict | None:
        ui = self.app.ui_manager
        architectures = [arch for arch, var in [(C.Arch.X64, ui.build_x64), (C.Arch.X86, ui.build_x86)] if var.get()]
        if not architectures: messagebox.showwarning("Seçim Gerekli", "En az bir mimari (x86/x64) seçin."); return None
     
        build_configs = [btype for btype, var in [(C.BuildType.RELEASE, ui.build_release), (C.BuildType.DEBUG, ui.build_debug)] if var.get()]
        if not build_configs: messagebox.showwarning("Seçim Gerekli", "En az bir derleme konfigürasyonu (Release/Debug) seçin."); return None
     
        return {
            "runtime": C.Runtime(ui.runtime.get()), "tool_mode": C.ToolMode(ui.tool_mode.get()),
            "jobs": ui.jobs.get(), "prefer_clean": ui.prefer_clean.get(),
            "architectures": architectures, "build_configs": build_configs
        }

    def start_build(self) -> None:
        if self.app._is_busy: return
        ui = self.app.ui_manager
     
        selected_tab_id = ui.notebook.index(ui.notebook.select())
        if selected_tab_id is None: messagebox.showwarning("Seçim Gerekli", "Lütfen derlemek için bir kütüphane sekmesi seçin."); return
        lib_key = list(C.LIB_META.keys())[selected_tab_id]
        meta = C.LIB_META[lib_key]

        common_parts = self._get_common_build_config_parts()
        if not common_parts: return

        tab_widgets = ui.lib_tabs.get(lib_key)
       
        tag = ""
        if meta.get('type') == 'url':
            tag = meta.get('version')
        else:
            tag = tab_widgets['cmb_tag'].get()

        if not tag: messagebox.showwarning("Seçim Gerekli", f"{meta['name']} için bir sürüm seçin."); return
     
        extra_opts = {key: var.get() for key, var in tab_widgets.get('opts', {}).items()}
        config = BuildConfig(lib_key=lib_key, tag=tag, **common_parts, extra_opts=extra_opts)
     
        stop_event = threading.Event()
        self.app.task_manager.register(
            name=f"Ana Derleme Yöneticisi ({meta['name']})",
            target=self._build_manager_worker, args=([config], stop_event),
            stop_event=stop_event, is_build_task=False
        )

    def start_build_all(self) -> None:
        if self.app._is_busy: return
        ui = self.app.ui_manager
     
        common_parts = self._get_common_build_config_parts()
        if not common_parts: return

        configs_to_build = []
        for lib_key in C.BUILD_ORDER:
            meta = C.LIB_META[lib_key]
            tab_widgets = ui.lib_tabs.get(lib_key)
           
            tag = ""
            if meta.get('type') == 'url':
                tag = meta.get('version')
            else:
                tag = tab_widgets['cmb_tag'].get()
           
            if not tag: messagebox.showwarning("Seçim Gerekli", f"Tümünü derlemek için önce {meta['name']} sekmesinden bir sürüm seçmelisiniz."); return
           
            extra_opts = {key: var.get() for key, var in tab_widgets.get('opts', {}).items()}
            configs_to_build.append(BuildConfig(lib_key=lib_key, tag=tag, **common_parts, extra_opts=extra_opts))
     
        stop_event = threading.Event()
        self.app.task_manager.register(
            name="Tüm Kütüphaneleri Derleme Yöneticisi",
            target=self._build_manager_worker, args=(configs_to_build, stop_event),
            stop_event=stop_event, is_build_task=False
        )

    def _build_manager_worker(self, configs: list[BuildConfig], stop_event: threading.Event, task_id: str) -> None:
        self.app.set_busy(True)
        self.app.ui_manager.log.delete("1.0", END)
        overall_success = True
     
        try:
            if configs and configs[0].prefer_clean:
                self._do_clean_start()

            for config in configs:
                if stop_event.is_set(): overall_success = False; break
                self.bus.log(f"====== {C.LIB_META[config.lib_key]['name'].upper()} İÇİN DERLEME SÜRECİ BAŞLIYOR ======", C.LogLevel.STAGE)
                if not self._run_preflight_checks(config):
                    overall_success = False; break
             
                success = self._build_single_library(config, stop_event)
                if not success:
                    overall_success = False
                    self.bus.log(f"{C.LIB_META[config.lib_key]['name']} derlemesi başarısız oldu. Tüm işlemler durduruluyor.", C.LogLevel.ERR)
                    break
         
        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)
                messagebox.showerror("Kritik Hata", f"Beklenmedik hata:\n\n{e}")
            overall_success = False
        finally:
            self._finalize_build_process(overall_success)
            self.app.task_manager.task_registry[task_id]['success'] = overall_success
            self.app.set_busy(False)
         
    def _build_single_library(self, config: BuildConfig, stop_event: threading.Event) -> bool:
        source_dir = self._fetch_source(config.lib_key, config.tag, stop_event)
        if not source_dir: return False

        build_actions = {
            "libjpeg-turbo": self._build_libjpeg, "devil": self._build_devil,
            "cryptopp": self._build_cryptopp, "lzo": self._build_lzo,
            "spdlog": self._build_spdlog, "nlohmann-json": self._build_nlohmann_json,
            "python": self._build_python
        }
        action = build_actions.get(config.lib_key)
        if not action: return False

        for arch in config.architectures:
            if stop_event.is_set(): return False
            self.bus.log(f"===== {arch.value.upper()} MİMARİSİ İÇİN DERLEME BAŞLIYOR =====", C.LogLevel.STAGE)
            if not action(config, source_dir, arch, stop_event):
                return False
        return True

    def _run_command_in_vs_env(self, cmd_list: list[str], cwd: Path, log_name_prefix: str, arch: C.Arch, stop_event: threading.Event, on_line: Callable[[str],None]=None) -> bool:
        vsbat = self.app.tool_manager.vs_env_path()
        if not vsbat: self.bus.log("VS Geliştirici Ortamı (vcvarsall.bat) bulunamadı.", C.LogLevel.ERR); return False

        arch_param = "amd64" if arch == C.Arch.X64 else "x86"
        log_path = C.LOGS / f"{log_name_prefix}_{arch.value}_{datetime.datetime.now():%Y%m%d-%H%M%S}.log"
        self.bus.log(f"Çıktı şu dosyaya loglanıyor: {log_path.name}", C.LogLevel.STAGE)

        bat_path = C.DEPS / f"_{log_name_prefix}_runner.bat"
        with open(bat_path, "w", encoding="utf-8", newline="\r\n") as f:
            f.write(f'@echo off\r\nchcp 65001 > nul\r\n')
            f.write(f'call "{vsbat}" {arch_param}\r\n')
            f.write(f'{" ".join(cmd_list)}\r\n')

        env = os.environ.copy()
        if self.app.ui_manager.tool_mode.get() == C.ToolMode.BUNDLED:
            env["PATH"] = ";".join([str(p) for p in C.BUNDLED_PATHS if p.exists()] + [env.get('PATH', '')])

        ok = stream_proc(f'cmd /d /c "{bat_path}"', env, on_line=on_line if on_line else lambda s: self.bus.log(s, C.LogLevel.CMD),
                         log_file_path=log_path, stop_event=stop_event, cwd=cwd, on_start=lambda p: self.add_pid(p.pid))
     
        bat_path.unlink(missing_ok=True)
        if not ok: self.bus.log(f"Komut başarısız oldu. Detaylar için logu inceleyin: {log_path}", C.LogLevel.ERR)
        return ok

    def _build_libjpeg(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- libjpeg-turbo ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
            build_dir = config.build_dir_for(arch, build_type); install_dir = config.install_dir_for(arch, build_type)
         
            cmake_exe = self.app.tool_manager.which("cmake", config.tool_mode == C.ToolMode.BUNDLED)
            if not cmake_exe: return False
         
            cmake_args = [f'"{cmake_exe}"', "-S", f'"{source_dir}"', "-B", f'"{build_dir}"', "-G", "Ninja",
                          f"-DCMAKE_INSTALL_PREFIX={install_dir}", f"-DCMAKE_BUILD_TYPE={build_type.value}",
                          f"-DENABLE_STATIC={'ON' if config.extra_opts.get('static') else 'OFF'}",
                          f"-DENABLE_SHARED={'ON' if config.extra_opts.get('shared') else 'OFF'}",
                          f"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded{'Debug' if build_type == C.BuildType.DEBUG else ''}{'DLL' if config.runtime == C.Runtime.MD else ''}"]
            if not self._run_command_in_vs_env(cmake_args, source_dir, f"ljt_config_{build_type.value.lower()}", arch, stop_event): return False

            build_args = [f'"{cmake_exe}"', "--build", f'"{build_dir}"', "--config", build_type.value, "--target", "install", "--parallel", str(config.jobs)]
            if not self._run_command_in_vs_env(build_args, source_dir, f"ljt_build_{build_type.value.lower()}", arch, stop_event, on_line=lambda s: self.app.ui_manager.update_build_stats(s, 'build_task')): return False
        return True

    def _patch_devil_rc_file(self, source_dir: Path) -> bool:
        rc_file = source_dir / "DevIL" / "src-IL" / "msvc" / "IL.rc"
        if not rc_file.exists():
            self.bus.log(f"DevIL RC dosyası bulunamadı, patch atlanıyor: {rc_file}", C.LogLevel.WARN)
            return True
     
        try:
            self.bus.log(f"DevIL RC dosyası ({rc_file.name}) MFC bağımlılığını kaldırmak için yamalanıyor...", C.LogLevel.STAGE)
            content = rc_file.read_text(encoding='cp1252', errors='ignore')
            if '#include "afxres.h"' in content:
                content = content.replace('#include "afxres.h"', '// #include "afxres.h" -- Patched by LibBuilder')
                rc_file.write_text(content, encoding='cp1252')
                self.bus.log("Yama başarıyla uygulandı.", C.LogLevel.INFO)
            else:
                self.bus.log("afxres.h bağımlılığı bulunamadı, yama gerekli değil.", C.LogLevel.INFO)
            return True
        except Exception as e:
            self.bus.log(f"DevIL RC dosyası yamalanırken hata: {e}", C.LogLevel.ERR)
            return False

    def _build_devil(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        if not self._patch_devil_rc_file(source_dir):
            return False

        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- DevIL ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
            build_dir = config.build_dir_for(arch, build_type); install_dir = config.install_dir_for(arch, build_type)
         
            ljt_install_dir = C.INST_LIBJPEG_TURBO / arch.value / config.runtime.value / build_type.value.lower()
            ljt_lib = ljt_install_dir / "lib" / "jpeg-static.lib"; ljt_inc = ljt_install_dir / "include"
            if config.extra_opts.get('static') and not (ljt_lib.exists() and ljt_inc.exists()):
                msg = f"DevIL bağımlılığı (libjpeg-turbo statik) bulunamadı: {ljt_install_dir}. Lütfen önce libjpeg-turbo'yu aynı ayarlarla ({arch.value}/{config.runtime.value}/{build_type.value}) statik olarak derleyin."
                self.bus.log(msg, C.LogLevel.ERR); messagebox.showerror("Bağımlılık Hatası", msg); return False

            cmake_exe = self.app.tool_manager.which("cmake", config.tool_mode == C.ToolMode.BUNDLED)
            if not cmake_exe: return False

            cmake_source_dir = source_dir / "DevIL"
            if not (cmake_source_dir / "CMakeLists.txt").exists():
                self.bus.log(f"DevIL için CMakeLists.txt beklenen yolda bulunamadı: {cmake_source_dir}", C.LogLevel.ERR)
                return False

            cmake_args = [f'"{cmake_exe}"', "-S", f'"{cmake_source_dir}"', "-B", f'"{build_dir}"', "-G", "Ninja",
                          f"-DCMAKE_INSTALL_PREFIX={install_dir}", f"-DCMAKE_BUILD_TYPE={build_type.value}",
                          f"-DBUILD_STATIC_LIBS={'ON' if config.extra_opts.get('static') else 'OFF'}",
                          f"-DBUILD_SHARED_LIBS={'ON' if config.extra_opts.get('shared') else 'OFF'}",
                          f"-DJPEG_INCLUDE_DIR={ljt_inc}", f"-DJPEG_LIBRARY={ljt_lib}", "-DIL_USE_JPEG=ON",
                          "-DIL_USE_PNG=OFF", "-DIL_USE_TIFF=OFF", "-DIL_USE_LCMS=OFF", "-DIL_USE_JASPER=OFF",
                          f"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded{'Debug' if build_type == C.BuildType.DEBUG else ''}{'DLL' if config.runtime == C.Runtime.MD else ''}"]
            if not self._run_command_in_vs_env(cmake_args, cmake_source_dir, f"devil_config_{build_type.value.lower()}", arch, stop_event): return False

            build_args = [f'"{cmake_exe}"', "--build", f'"{build_dir}"', "--config", build_type.value, "--target", "install", "--parallel", str(config.jobs)]
            if not self._run_command_in_vs_env(build_args, cmake_source_dir, f"devil_build_{build_type.value.lower()}", arch, stop_event, on_line=lambda s: self.app.ui_manager.update_build_stats(s, 'build_task')): return False
        return True
   
    def _build_cryptopp(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        vs_env = self.app.tool_manager.vs_env_path()
        if not vs_env: return False
        msbuild_exe = Path(vs_env).parent.parent.parent.parent / "MSBuild/Current/Bin/MSBuild.exe"
        if not msbuild_exe.exists(): self.bus.log(f"MSBuild.exe bulunamadı: {msbuild_exe}", C.LogLevel.ERR); return False
     
        sln_file = source_dir / "cryptest.sln"
        if not sln_file.exists(): self.bus.log(f"Crypto++ solution dosyası bulunamadı: {sln_file}", C.LogLevel.ERR); return False
     
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- Crypto++ ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
            install_dir = config.install_dir_for(arch, build_type)
         
            runtime_flag = "MultiThreaded" if config.runtime == C.Runtime.MT else "MultiThreadedDLL"
            if build_type == C.BuildType.DEBUG: runtime_flag += "Debug"
            platform = "x64" if arch == C.Arch.X64 else "Win32"

            build_args = [f'"{msbuild_exe}"', f'"{sln_file}"', "/t:cryptlib", f"/p:Configuration={build_type.value}",
                          f"/p:Platform={platform}", f"/p:RuntimeLibrary={runtime_flag}", f"/p:UseMultiToolTask=true",
                          f"/p:BuildInParallel=true", f"/m:{config.jobs}"]
         
            self.bus.work_mode("indeterminate")
            self.bus.work_control("start")
            def on_line_msbuild(line: str):
                self.bus.log(line, C.LogLevel.CMD)
                if ".cpp" in line or ".asm" in line:
                    self.bus.work_text(Path(line.split(' ')[0]).name)

            build_ok = self._run_command_in_vs_env(build_args, source_dir, f"cryptopp_build_{build_type.value.lower()}", arch, stop_event, on_line=on_line_msbuild)
         
            self.bus.work_control("stop")
            self.bus.work_mode("determinate")
            self.bus.progress("work", 100 if build_ok else 0, "Tamamlandı" if build_ok else "Hata")

            if not build_ok: return False
         
            self.bus.log("Derleme sonrası kopyalama işlemi...", C.LogLevel.STAGE)
            try:
                output_dir = source_dir / ("x64" if arch == C.Arch.X64 else "Win32") / "Output" / build_type.value
                install_dir.mkdir(parents=True, exist_ok=True)
                (install_dir / "lib").mkdir(exist_ok=True); (install_dir / "include").mkdir(exist_ok=True)
                for f in output_dir.glob("*.lib"): shutil.copy2(f, install_dir / "lib")
                for header in source_dir.glob('*.h'): shutil.copy2(header, install_dir / "include")
            except Exception as e: self.bus.log(f"Crypto++ dosyaları kopyalanırken hata: {e}", C.LogLevel.ERR); return False
        return True
   
    def _build_lzo(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- LZO ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
            build_dir = config.build_dir_for(arch, build_type)
            install_dir = config.install_dir_for(arch, build_type)
           
            cmake_exe = self.app.tool_manager.which("cmake", config.tool_mode == C.ToolMode.BUNDLED)
            if not cmake_exe: return False
           
            cmake_args = [
                f'"{cmake_exe}"', "-S", f'"{source_dir}"', "-B", f'"{build_dir}"', "-G", "Ninja",
                f"-DCMAKE_INSTALL_PREFIX={install_dir}",
                f"-DCMAKE_BUILD_TYPE={build_type.value}",
                f"-DBUILD_SHARED_LIBS={'ON' if config.extra_opts.get('shared') else 'OFF'}",
                f"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded{'Debug' if build_type == C.BuildType.DEBUG else ''}{'DLL' if config.runtime == C.Runtime.MD else ''}"
            ]
            if not self._run_command_in_vs_env(cmake_args, source_dir, f"lzo_config_{build_type.value.lower()}", arch, stop_event):
                return False

            build_args = [f'"{cmake_exe}"', "--build", f'"{build_dir}"', "--config", build_type.value, "--target", "install", "--parallel", str(config.jobs)]
            if not self._run_command_in_vs_env(build_args, source_dir, f"lzo_build_{build_type.value.lower()}", arch, stop_event, on_line=lambda s: self.app.ui_manager.update_build_stats(s, 'build_task')):
                return False
        return True

    def _build_spdlog(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- spdlog ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
           
            build_dir = config.build_dir_for(arch, build_type)
            install_dir = config.install_dir_for(arch, build_type)

            cmake_exe = self.app.tool_manager.which("cmake", config.tool_mode == C.ToolMode.BUNDLED)
            if not cmake_exe: return False

            cmake_args = [
                f'"{cmake_exe}"', "-S", f'"{source_dir}"', "-B", f'"{build_dir}"', "-G", "Ninja",
                f"-DCMAKE_INSTALL_PREFIX={install_dir}",
                f"-DCMAKE_BUILD_TYPE={build_type.value}",
                f"-DSPDLOG_BUILD_STATIC=ON",
                f"-DSPDLOG_BUILD_SHARED=OFF",
                f"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded{'Debug' if build_type == C.BuildType.DEBUG else ''}{'DLL' if config.runtime == C.Runtime.MD else ''}",
                "-DSPDLOG_BUILD_EXAMPLES=OFF",
                "-DSPDLOG_BUILD_TESTS=OFF",
                "-DSPDLOG_BUILD_BENCH=OFF"
            ]
           
            if not self._run_command_in_vs_env(cmake_args, source_dir, f"spdlog_config_{build_type.value.lower()}", arch, stop_event):
                return False

            build_args = [f'"{cmake_exe}"', "--build", f'"{build_dir}"', "--config", build_type.value, "--target", "install", "--parallel", str(config.jobs)]
            if not self._run_command_in_vs_env(build_args, source_dir, f"spdlog_build_{build_type.value.lower()}", arch, stop_event, on_line=lambda s: self.app.ui_manager.update_build_stats(s, 'build_task')):
                return False
        return True

    def _build_nlohmann_json(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- nlohmann/json ({arch.value} / {build_type.value} / {config.runtime.value}) Kurulumu Başlıyor ---", C.LogLevel.STAGE)
            install_dir = config.install_dir_for(arch, build_type)
           
            try:
                src_include_path = source_dir / "include"
                if not src_include_path.exists():
                    self.bus.log(f"Kaynak 'include' klasörü bulunamadı: {src_include_path}", C.LogLevel.ERR)
                    return False
               
                dest_include_path = install_dir / "include"
                self.bus.log(f"'{src_include_path}' kopyalanıyor -> '{dest_include_path}'", C.LogLevel.CMD)
               
                if dest_include_path.exists():
                    shutil.rmtree(dest_include_path)
                shutil.copytree(src_include_path, dest_include_path)
               
                self.bus.log("nlohmann/json başlık dosyaları başarıyla kopyalandı.", C.LogLevel.INFO)
            except Exception as e:
                self.bus.log(f"nlohmann/json dosyaları kopyalanırken hata: {e}", C.LogLevel.ERR)
                return False
        return True
     
    def _build_python(self, config: BuildConfig, source_dir: Path, arch: C.Arch, stop_event: threading.Event) -> bool:
        vs_env = self.app.tool_manager.vs_env_path()
        if not vs_env: return False
        msbuild_exe = Path(vs_env).parent.parent.parent.parent / "MSBuild/Current/Bin/MSBuild.exe"
        if not msbuild_exe.exists(): self.bus.log(f"MSBuild.exe bulunamadı: {msbuild_exe}", C.LogLevel.ERR); return False
     
        sln_file = source_dir / "PCbuild" / "pcbuild.sln"
        if not sln_file.exists(): self.bus.log(f"Python solution dosyası bulunamadı: {sln_file}", C.LogLevel.ERR); return False
     
        for build_type in config.build_configs:
            if stop_event.is_set(): return False
            self.bus.log(f"--- Python ({arch.value} / {build_type.value} / {config.runtime.value}) Derlemesi Başlıyor ---", C.LogLevel.STAGE)
            install_dir = config.install_dir_for(arch, build_type)
         
            runtime_flag = "MultiThreaded" if config.runtime == C.Runtime.MT else "MultiThreadedDLL"
            if build_type == C.BuildType.DEBUG: runtime_flag += "Debug"
            platform = "x64" if arch == C.Arch.X64 else "Win32"

            targets = []
            if config.extra_opts.get('shared'): targets.append("pythoncore")
            if config.extra_opts.get('static'): targets.append("pythoncore_static")
           
            if not targets:
                self.bus.log("Python için 'static' veya 'shared' seçilmedi, bu konfigürasyon atlanıyor.", C.LogLevel.WARN)
                continue

            # CPython'un kendi build sistemi, çıktıları $(SolutionDir)$(Platform)\$(Configuration) içine koyar
            output_dir = source_dir / "PCbuild" / platform / build_type.value
            build_args = [f'"{msbuild_exe}"', f'"{sln_file}"', f"/t:{';'.join(targets)}", f"/p:Configuration={build_type.value}",
                          f"/p:Platform={platform}", f"/p:RuntimeLibrary={runtime_flag}", f"/p:UseMultiToolTask=true",
                          f"/p:BuildInParallel=true", f"/m:{config.jobs}", f"/p:OutDir={output_dir}\\"]
         
            self.bus.work_mode("indeterminate")
            self.bus.work_control("start")
            def on_line_msbuild(line: str):
                self.bus.log(line, C.LogLevel.CMD)
                if ".cpp" in line or ".c" in line:
                    self.bus.work_text(Path(line.split(' ')[0]).name)

            build_ok = self._run_command_in_vs_env(build_args, source_dir / "PCbuild", f"python_build_{build_type.value.lower()}", arch, stop_event, on_line=on_line_msbuild)
         
            self.bus.work_control("stop")
            self.bus.work_mode("determinate")
            self.bus.progress("work", 100 if build_ok else 0, "Tamamlandı" if build_ok else "Hata")

            if not build_ok: return False
         
            self.bus.log("Derleme sonrası kopyalama işlemi...", C.LogLevel.STAGE)
            try:
                lib_dir = (install_dir / "lib"); lib_dir.mkdir(parents=True, exist_ok=True)
                inc_dir = (install_dir / "include"); inc_dir.mkdir(parents=True, exist_ok=True)
                dll_dir = (install_dir / "bin"); dll_dir.mkdir(parents=True, exist_ok=True)

                self.bus.log("Kopyalanıyor: Başlık Dosyaları (Include, PC)", C.LogLevel.CMD)
                # Dirs_exist_ok=True, var olan bir hedefe kopyalamaya izin verir
                shutil.copytree(source_dir / "Include", inc_dir / "Include", dirs_exist_ok=True)
                shutil.copytree(source_dir / "PC", inc_dir / "PC", dirs_exist_ok=True)

                if config.extra_opts.get('shared'):
                    self.bus.log("Kopyalanıyor: Paylaşımlı Kütüphane (.lib, .dll)", C.LogLevel.CMD)
                    for f in output_dir.glob("python*.lib"): shutil.copy2(f, lib_dir)
                    for f in output_dir.glob("python*.dll"): shutil.copy2(f, dll_dir)
               
                if config.extra_opts.get('static'):
                    self.bus.log("Kopyalanıyor: Statik Kütüphane (_s.lib)", C.LogLevel.CMD)
                    for f in output_dir.glob("python*_s.lib"): shutil.copy2(f, lib_dir)

            except Exception as e: self.bus.log(f"Python dosyaları kopyalanırken hata: {e}", C.LogLevel.ERR); return False
        return True

    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("Lib Builder", "Derleme işlemi başarıyla tamamlandı.")
        else:
            self.bus.status("Derleme bir veya daha fazla hatayla tamamlandı.")
            messagebox.showwarning("Derleme Başarısız", "Derleme görevi başarısız oldu. Detaylar için logları inceleyin.")

    def _run_preflight_checks(self, config: BuildConfig) -> bool:
        self.bus.log("--- Uçuş Öncesi Kontroller Başlatıldı ---", C.LogLevel.STAGE)
        if not self.app.tool_manager.vs_env_path():
            messagebox.showerror("Ortam Hatası", "Visual Studio 'Desktop development with C++' bileşeni kurulu değil."); return False
     
        required = ["cmake", "ninja"]
        if config.lib_key == "libjpeg-turbo": required.append("nasm")
     
        missing = []
        for tool_key in required:
            meta = next((m for m in self.app.tool_manager.TOOLS_META if m["key"] == tool_key), None)
            if not meta or not self.app.tool_manager.detect_tool(meta, config.tool_mode)[0]:
                missing.append(meta['name'] if meta else tool_key)

        if missing:
            msg = f"Eksik araçlar: {', '.join(missing)}. Lütfen 'Araç Durumu' sekmesinden kurun."
            messagebox.showwarning("Eksik Araçlar", msg); 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: pass
        self.bus.log("--- Uçuş Öncesi Kontroller Başarıyla Tamamlandı ---", C.LogLevel.STAGE)
        return True

    def clear_build_folder(self) -> None:
        if self.app._is_busy: return
        if not C.BUILD.exists() or not any(C.BUILD.iterdir()): 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(); messagebox.showinfo("Başarılı", "'build' klasörü temizlendi.")
            except OSError as e: messagebox.showerror("Hata", f"Silinemedi:\n{e}")

    def fetch_tags_async(self, lib_key: str, combobox: ttk.Combobox) -> None:
        if self.app._is_busy: return
        meta = C.LIB_META[lib_key]
        self.app.task_manager.register(name=f"{meta['name']} Sürüm Listeleme",
                                       target=self._fetch_tags_worker, args=(meta, combobox), is_build_task=False)
     
    def _fetch_tags_worker(self, meta: dict, combobox: ttk.Combobox, task_id: str) -> None:
        self.bus.log(f"{meta['name']} GitHub etiketleri çekiliyor...", C.LogLevel.INFO)
        tags = []
        try:
            git = self.app.tool_manager.which("git", False)
            if git:
                out = run_out([git, "ls-remote", "--tags", meta['repo']])
                versions = []
                for ln in out.splitlines():
                    m = re.search(r"refs/tags/(" + re.escape(meta['tag_prefix']) + r".*)$", ln)
                    if m and meta['tag_filter'].match(m.group(1)):
                        versions.append(m.group(1))
                tags = sorted(versions, key=lambda v: list(map(int, re.findall(r'\d+', v))), reverse=True)[:50]
        except Exception as e: self.bus.log(f"Git ile etiket çekme hatası: {e}", C.LogLevel.WARN)
         
        if not tags: messagebox.showerror("Hata", "Sürümler listelenemedi. İnternet bağlantınızı veya Git kurulumunu kontrol edin.")
     
        lib_key_from_meta = next(key for key, m in C.LIB_META.items() if m['name'] == meta['name'])
        last_tag = self.app.ui_manager.lib_tabs.get(lib_key_from_meta, {}).get('last_tag_from_config')
        selected_tag = (last_tag if last_tag in tags else tags[0]) if tags else ""
        self.app.after(0, lambda: (combobox.config(values=tags), combobox.set(selected_tag)))
        self.app.task_manager.task_registry[task_id]['success'] = True

    def _fetch_source(self, lib_key: str, tag: str, stop_event: threading.Event) -> Path | None:
        meta = C.LIB_META[lib_key]
        dst = C.SRC / lib_key / tag
        if stop_event.is_set(): return None
        if dst.exists() and any(dst.iterdir()):
            self.bus.log("Kaynaklar zaten çıkarılmış, atlandı.", C.LogLevel.STAGE); return dst
         
        if meta.get('type') == 'url':
            url = meta['url']
            archive_name = Path(url).name
            zip_path = C.DL / archive_name.replace(":", "_")
            if not zip_path.exists():
                if not self.app.tool_manager._http_get(url, zip_path, self.bus):
                    raise RuntimeError("Kaynak arşivi indirilemedi.")
        else: # git type
            zip_name = tag.replace('/', '_')
            zip_path = C.DL / f"{lib_key}-{zip_name}.zip"
            if not zip_path.exists():
                url = f"{meta['repo']}/archive/refs/tags/{tag}.zip"
                if not self.app.tool_manager._http_get(url, zip_path, self.bus):
                    raise RuntimeError("Kaynak ZIP alınamadı.")

        if stop_event.is_set(): return None
     
        extract_dir = C.SRC / lib_key / "_tmp"
        if extract_dir.exists(): shutil.rmtree(extract_dir)
        extract_dir.mkdir(parents=True)
     
        try:
            self.bus.log(f"'{zip_path.name}' arşivi çıkarılıyor...", C.LogLevel.STAGE)
            shutil.unpack_archive(zip_path, extract_dir)
            self.bus.log("Arşiv başarıyla çıkarıldı.", C.LogLevel.INFO)
        except Exception as e:
            self.bus.log(f"Arşiv çıkarılırken hata oluştu: {e}", C.LogLevel.ERR)
            self.bus.log(traceback.format_exc(), C.LogLevel.ERR)
            raise RuntimeError("Arşiv dosyası çıkarılamadı.")

        items_in_extract_dir = list(extract_dir.iterdir())
        if len(items_in_extract_dir) == 1 and items_in_extract_dir[0].is_dir():
            self.bus.log("Tek bir kök dizin algılandı, taşınıyor...", C.LogLevel.INFO)
            source_root = items_in_extract_dir[0]
            if dst.exists():
                shutil.rmtree(dst)
            shutil.move(str(source_root), str(dst))
            shutil.rmtree(extract_dir)
        else:
            self.bus.log("Kök dizin bulunamadı, geçici klasörün kendisi kaynak olarak kullanılıyor.", C.LogLevel.INFO)
            if dst.exists():
                shutil.rmtree(dst)
            shutil.move(str(extract_dir), str(dst))

        if not dst.exists():
             raise RuntimeError("Kaynak çıkarma sonrası klasör bulunamadı.")
        return dst

    def initial_trash_cleanup(self) -> None:
        if C.TRASH.exists() and 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)); p.mkdir(exist_ok=True)
                except OSError as e: self.bus.log(f"'{p}' taşınamadı: {e}.", C.LogLevel.ERR); raise
                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. Devam etmek istiyor musunuz?"):
            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="Lib Builder Pro v7.4 (Final) - turkmmo.com - dormammu", size=(1320, 960), 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)
        for lib_key, widgets in self.ui_manager.lib_tabs.items():
             if 'cmb_tag' in widgets:
                self.after(200, lambda k=lib_key, c=widgets['cmb_tag']: self.build_manager.fetch_tags_async(k, c))
        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.protocol("WM_DELETE_WINDOW", self.on_closing)

    def on_closing(self) -> None:
        if self._is_busy and 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()

    def set_busy(self, busy: bool) -> None:
        self._is_busy = busy
        self.ui_manager.set_controls_state('disabled' if busy else 'normal')
        try:
            priority = psutil.IDLE_PRIORITY_CLASS if busy else psutil.NORMAL_PRIORITY_CLASS
            self.process.nice(priority)
        except psutil.Error: pass

if __name__ == "__main__":
    for p in (C.LOGS, C.TOOLS, C.DL, C.SRC, C.BUILD, C.INST, C.DEPS, C.TRASH,
              C.INST_LIBJPEG_TURBO, C.INST_DEVIL, C.INST_CRYPTOPP, C.INST_LZO,
              C.INST_SPDLOG, C.INST_NLOHMANN_JSON, C.INST_PYTHON):
        p.mkdir(parents=True, exist_ok=True)
    app = App()
    app.mainloop()




🛠️ Program Nasıl Kullanılır? 🛠️



  1. Araçları Kur: İlk olarak, "Araç Durumu" panelindeki eksikleri "Eksikleri Kur" butonuyla tamamlayın.
  2. Ayarları Yap:
    • "Ortak Ayarlar" kısmından Metin2 client için zorunlu olan x86 (32-bit) mimarisinin seçili olduğundan emin olun.
    • Runtime olarak genellikle MT seçilir.
    • Her kütüphane sekmesine gidip "Listele" butonuna basarak en güncel sürümü seçin (veya istediğiniz başka bir sürümü).
  3. Derlemeyi Başlat: Sağdaki "Tüm Kütüphaneleri Derle" butonuna basın. Bu en kolay yöntemdir, çünkü bağımlılık sırasını kendi ayarlar.
  4. Kullanıma Hazır: İşlem bitince "Install Klasörünü Aç" butonuna basın. Açılan klasörde, her kütüphane için ayrı ayrı klasörler göreceksiniz. Örneğin install\devil\x86\MT\Release klasörünün içindeki include ve lib klasörlerini, kendi source'unuzun extern klasöründeki ilgili yerlere kopyalayın.
Artık kütüphane derleme hataları olmadan, keyifle source'unuzu derleyebilirsiniz!

Tüm soru, sorun ve önerilerinizi konu altından bekliyorum.

Saygılarımla,dormammu
 

Ekli dosyalar

  • 1760439983937.png
    1760439983937.png
    85.4 KB · Görüntüleme: 0
Son düzenleme:
Paylaşım için teşekkürler, Elinize sağlık.
 
Eline sağlık çok iyi olmuş çok fazla problem yaşayan vardı bu konuda.
 
Paylaşım için teşekkürler
 
paylaşım için teşekkürler
 

Şu an konuyu görüntüleyenler (Toplam : 0, Üye: 0, Misafir: 0)

Geri
Üst