diff --git a/.github/workflows/Project.yml b/.github/workflows/Project.yml
new file mode 100644
index 000000000..a24247d22
--- /dev/null
+++ b/.github/workflows/Project.yml
@@ -0,0 +1,28 @@
+name: Project Build
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ build:
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Install XcodeGen
+ run: brew install xcodegen
+
+ - name: Generate Xcode project
+ run: xcodegen generate
+
+ - name: List generated files
+ run: ls -R
+
+ - name: Upload generated project
+ uses: actions/upload-artifact@v4
+ with:
+ name: xcodeproj
+ path: "*.xcodeproj"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1d0d0bea9..db21709b9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,6 +19,12 @@ jobs:
with:
fetch-depth: 0
+ - name: setup xcodegen
+ run: brew install xcodegen
+
+ - name: make project
+ run: xcodegen generate
+
- name: setup xcode
uses: maxim-lobanov/setup-xcode@v1
with:
@@ -41,7 +47,7 @@ jobs:
with:
name: lara-ipa
path: |
- build/lara.ipa
+ ./lara.ipa
build/xcodebuild.log
- name: upload release ipa
@@ -49,7 +55,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: release-ipa
- path: build/lara.ipa
+ path: ./lara.ipa
release:
diff --git a/README.md b/README.md
index bfebaf349..17430e423 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@ Important Notes:
- OTA Update Disabler
- Screen Time Disabler
- App Decrypt
+- Clean Cache
### Coming Soon
diff --git a/lara.xcodeproj/.gitignore b/lara.xcodeproj/.gitignore
deleted file mode 100644
index 21166e6fd..000000000
--- a/lara.xcodeproj/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-xcuserdata/
\ No newline at end of file
diff --git a/lara/Info.plist b/lara/Info.plist
index 7f18d78f0..2db184d53 100644
--- a/lara/Info.plist
+++ b/lara/Info.plist
@@ -6,6 +6,18 @@
audio
+ CFBundleIdentifier
+ com.roooot.lara
+ CFBundleExecutable
+ lara
+ CFBundleName
+ Lara
+ CFBundlePackageType
+ APPL
+ CFBundleVersion
+ 2
+ CFBundleShortVersionString
+ 0.2
UIFileSharingEnabled
diff --git a/lara/classes/laramgr.swift b/lara/classes/laramgr.swift
index d9004c037..8a3163af6 100644
--- a/lara/classes/laramgr.swift
+++ b/lara/classes/laramgr.swift
@@ -88,6 +88,7 @@ final class laramgr: ObservableObject {
@Published var rcready: Bool = false
@Published var rcfailed: Bool = false
@Published var showrespring: Bool = false
+ @Published var developer: Bool = false
@Published var showLogs: Bool = false
diff --git a/lara/kexploit/offsets.h b/lara/kexploit/offsets.h
index c49f1462f..3b4868b86 100644
--- a/lara/kexploit/offsets.h
+++ b/lara/kexploit/offsets.h
@@ -57,6 +57,7 @@ extern uint32_t off_proc_p_name;
extern uint32_t off_proc_ro_pr_task;
extern uint32_t off_proc_ro_p_ucred;
extern uint32_t off_ucred_cr_label;
+extern uint32_t off_ucred_cr_ref;
extern uint32_t off_task_itk_space;
extern uint32_t off_task_threads_next;
extern uint32_t off_task_task_exc_guard;
diff --git a/lara/kexploit/offsets.m b/lara/kexploit/offsets.m
index 604190147..e9786f773 100644
--- a/lara/kexploit/offsets.m
+++ b/lara/kexploit/offsets.m
@@ -67,6 +67,7 @@
uint32_t off_proc_ro_pr_task = 0;
uint32_t off_proc_ro_p_ucred = 0;
uint32_t off_ucred_cr_label = 0;
+uint32_t off_ucred_cr_ref = 0;
uint32_t off_task_itk_space = 0;
uint32_t off_task_threads_next = 0;
uint32_t off_task_task_exc_guard = 0;
@@ -168,6 +169,7 @@
OFFSET32(off_proc_ro_pr_task),
OFFSET32(off_proc_ro_p_ucred),
OFFSET32(off_ucred_cr_label),
+ OFFSET32(off_ucred_cr_ref),
OFFSET32(off_task_itk_space),
OFFSET32(off_task_threads_next),
OFFSET32(off_task_task_exc_guard),
@@ -354,6 +356,7 @@ void savealloffsets(void) {
@"off_proc_p_name": @(off_proc_p_name),
@"off_proc_ro_pr_task": @(off_proc_ro_pr_task),
@"off_ucred_cr_label": @(off_ucred_cr_label),
+ @"off_ucred_cr_ref": @(off_ucred_cr_ref),
@"off_task_itk_space": @(off_task_itk_space),
@"off_task_threads_next": @(off_task_threads_next),
@"off_task_task_exc_guard": @(off_task_task_exc_guard),
diff --git a/lara/kexploit/pe/sbx.h b/lara/kexploit/pe/sbx.h
index 7956e8c49..fc139486d 100644
--- a/lara/kexploit/pe/sbx.h
+++ b/lara/kexploit/pe/sbx.h
@@ -10,6 +10,7 @@
#include
+uint64_t sbx_ucredbyproc(uint64_t proc);
int sbx_escape(uint64_t self_proc);
void sbx_setlogcallback(void (*callback)(const char *message));
uint64_t sbx_gettoken(pid_t pid);
diff --git a/lara/kexploit/utils.h b/lara/kexploit/utils.h
index 19a7f6c9c..f82fad67d 100644
--- a/lara/kexploit/utils.h
+++ b/lara/kexploit/utils.h
@@ -68,6 +68,8 @@ uint64_t proc_self(void);
uint64_t task_self(void);
int crashproc(const char* pid);
+int proc_pause_resume(const char *name, bool resume);
+int count_pids(uint64_t allproc);
#ifdef __cplusplus
}
diff --git a/lara/kexploit/utils.m b/lara/kexploit/utils.m
index ec7f8d26a..2a1f725cb 100644
--- a/lara/kexploit/utils.m
+++ b/lara/kexploit/utils.m
@@ -10,6 +10,7 @@
#import "xpf.h"
#import "offsets.h"
#import "xpaci.h"
+#import "pe/sbx.h"
#import
#import
@@ -47,6 +48,7 @@
static const uint32_t ARM_SS_OFFSET = 0x8;
uint32_t TASK_TNEXT_OFFSET;
uint32_t THREAD_MUPCB_OFFSET;
+bool launchd = true;
struct arm_saved_state64 {
uint64_t x[29];
@@ -853,3 +855,50 @@ int crashproc(const char* name) {
ds_kwrite64(state + offsetof(struct arm_saved_state64, sp), 0x1337133713371337);
return 0;
}
+
+int proc_pause_resume(const char *name, bool resume) {
+ if (!name) {
+ return -1;
+ }
+
+ uint64_t proc = procbyname(name);
+ if (!proc) {
+ printf("(signal) process not found: %s\n", name);
+ return -1;
+ }
+
+ uint32_t pid = ds_kread32(proc + PROC_PID_OFFSET);
+ int result;
+
+ if (resume) {
+ result = kill(pid, SIGCONT);
+ } else {
+ result = kill(pid, SIGSTOP);
+ }
+
+ if (result != 0) {
+ perror("(signal) kill failed");
+ return -1;
+ }
+
+ printf("(signal) %s %s\n",
+ name,
+ resume ? "resumed" : "paused");
+ return 0;
+}
+
+int count_pids(uint64_t allproc) {
+ int count = 0;
+ uint64_t proc = allproc;
+
+ for (int i = 0; i < 12000 && proc; i++) {
+ int pid = ds_kread32(proc + off_proc_p_pid);
+ if (pid > 0 && pid < 99999)
+ count++;
+ uint64_t next = ds_kread64(proc + off_proc_p_list_le_next);
+ if (next == 0 || next == proc)
+ break;
+ proc = next;
+ }
+ return count;
+}
diff --git a/lara/lara-Bridging-Header.h b/lara/lara-Bridging-Header.h
index f092a4222..8103570aa 100644
--- a/lara/lara-Bridging-Header.h
+++ b/lara/lara-Bridging-Header.h
@@ -20,6 +20,7 @@
#import "persistence.h"
#import "ota.h"
#import "screentime.h"
+#import "themer.h"
#import
diff --git a/lara/views/app/settings/SettingsView.swift b/lara/views/app/settings/SettingsView.swift
index 2a9b54de2..a08d4af32 100644
--- a/lara/views/app/settings/SettingsView.swift
+++ b/lara/views/app/settings/SettingsView.swift
@@ -8,6 +8,7 @@
import SwiftUI
import UIKit
import UniformTypeIdentifiers
+import Combine
enum method: String, CaseIterable {
case vfs = "VFS"
@@ -254,6 +255,10 @@ struct SettingsView: View {
Toggle("Allow >10 dock icons", isOn: $rcDockUnlimited)
}
#endif
+
+ Section(header: HeaderLabel(text: "Developer", icon: "gear")) {
+ Toggle("Developer Mode", isOn: $mgr.developer)
+ }
}
.navigationTitle("Settings")
.fileImporter(isPresented: $showkcacheimport, allowedContentTypes: [.data], allowsMultipleSelection: false) { result in
diff --git a/lara/views/tweaks/ToolsView.swift b/lara/views/tweaks/ToolsView.swift
index d69d8eb77..1141bb075 100644
--- a/lara/views/tweaks/ToolsView.swift
+++ b/lara/views/tweaks/ToolsView.swift
@@ -24,6 +24,8 @@ struct ToolsView: View {
@State private var pid: pid_t = getpid()
@State private var status: String?
@State private var crashname: String = "SpringBoard"
+ @State private var pausedProcesses: Set = []
+ @State private var proc_sbx: UInt64 = 0
private enum tokenclass: String, CaseIterable, Identifiable {
case read = "com.apple.app-sandbox.read"
@@ -156,10 +158,37 @@ struct ToolsView: View {
}
}
.disabled(crashname.isEmpty)
+ Button("Pause") {
+ crashname.withCString { _ = proc_pause_resume($0, false) }
+ pausedProcesses.insert(crashname)
+ }
+ .disabled(crashname.isEmpty || pausedProcesses.contains(crashname))
+
+ Button("Resume") {
+ crashname.withCString { _ = proc_pause_resume($0, true) }
+ pausedProcesses.remove(crashname)
+ }
+ .disabled(crashname.isEmpty || !pausedProcesses.contains(crashname))
+
+ Button("SBX Escape Helper") {
+ crashname.withCString { cstr in
+ proc_sbx = procbyname(cstr)
+ }
+
+ if proc_sbx == 0 {
+ status = "Failed to get proc"
+ return
+ }
+
+ let errorcheck = sbx_escape(proc_sbx)
+ status = errorcheck == 0 ? nil : "Failure"
+ }
+ .disabled(crashname.isEmpty)
+
} header: {
- Text("Crasher")
+ Text("Task Manager")
} footer: {
- Text("Crashes the selected process")
+ Text("Manages The Selected Process")
}
Section {
diff --git a/lara/views/tweaks/TweaksView.swift b/lara/views/tweaks/TweaksView.swift
index 443948457..7a16461d0 100644
--- a/lara/views/tweaks/TweaksView.swift
+++ b/lara/views/tweaks/TweaksView.swift
@@ -59,11 +59,12 @@ struct TweaksView: View {
.disabled(!mgr.vfsready)
NavigationLink("OTA Updates", destination: OTAView(mgr: mgr))
NavigationLink("Screen Time", destination: ScreenTimeView(mgr: mgr))
+ NavigationLink("Clean Cache", destination: CacheView())
}
Section(header: HeaderLabel(text: "Broken", icon: "exclamationmark.triangle.fill")) {
NavigationLink("DarkBoard", destination: DarkBoardView())
- .disabled(true)
+ .disabled(!mgr.developer)
}
NavigationLink("Extra Tools", destination: ToolsView())
diff --git a/lara/views/tweaks/cacheclean/BundleResolver.swift b/lara/views/tweaks/cacheclean/BundleResolver.swift
new file mode 100644
index 000000000..931e3a6dd
--- /dev/null
+++ b/lara/views/tweaks/cacheclean/BundleResolver.swift
@@ -0,0 +1,222 @@
+//
+// BundleResolver.swift
+// lara
+//
+
+import Foundation
+import UIKit
+
+// MARK: - Model
+
+struct ResolvedApp {
+ let dataUUID: String
+ let bundleID: String
+ let bundlePath: String
+ let name: String
+ let icon: UIImage?
+}
+
+// MARK: - Resolver
+
+final class BundleResolver {
+
+ private let fm = FileManager.default
+
+ private let bundleRoot = "/var/containers/Bundle/Application"
+ private let dataRoot = "/var/mobile/Containers/Data/Application"
+
+ // MARK: Public
+
+ func resolveAll() -> [ResolvedApp] {
+
+ let bundleMap = buildBundleMap()
+
+ guard let dataContainers = try? fm.contentsOfDirectory(atPath: dataRoot) else {
+ return []
+ }
+
+ var results: [ResolvedApp] = []
+
+ for dataUUID in dataContainers {
+
+ let dataPath = dataRoot + "/" + dataUUID
+ let metaPath = dataPath + "/.com.apple.mobile_container_manager.metadata.plist"
+
+ // MARK: STEP 1 - metadata
+ var bundleID =
+ NSDictionary(contentsOfFile: metaPath)?["MCMMetadataIdentifier"] as? String
+
+ // MARK: STEP 2 - fallback scan inside container
+ if bundleID == nil {
+ bundleID = findBundleIDInDataContainer(dataPath)
+ }
+
+ // ❗ DO NOT DROP APP
+ let finalBundleID = bundleID ?? "unknown.\(dataUUID)"
+
+ // MARK: STEP 3 - resolve bundle path
+ let resolvedBundlePath =
+ bundleID != nil
+ ? (bundleMap[finalBundleID] ?? findBundlePathFallback(bundleID: finalBundleID))
+ : nil
+
+ let finalBundlePath = resolvedBundlePath ?? ""
+
+ // MARK: STEP 4 - name resolution (NEVER FAIL)
+ let finalName = readSafeName(
+ bundlePath: finalBundlePath.isEmpty ? dataPath : finalBundlePath,
+ fallback: dataUUID
+ )
+
+ // MARK: STEP 5 - icon resolution (NEVER FAIL)
+ let finalIcon =
+ finalBundlePath.isEmpty
+ ? UIImage(systemName: "app")
+ : readIcon(finalBundlePath)
+
+ results.append(
+ ResolvedApp(
+ dataUUID: dataUUID,
+ bundleID: finalBundleID,
+ bundlePath: finalBundlePath,
+ name: finalName,
+ icon: finalIcon
+ )
+ )
+ }
+
+ return results
+ }
+
+ // MARK: Bundle Map
+
+ private func buildBundleMap() -> [String: String] {
+
+ var map: [String: String] = [:]
+
+ guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else {
+ return map
+ }
+
+ for root in roots {
+
+ let rootPath = bundleRoot + "/" + root
+
+ guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else {
+ continue
+ }
+
+ for item in items where item.hasSuffix(".app") {
+
+ let appPath = rootPath + "/" + item
+
+ // PRIMARY
+ if let meta = NSDictionary(contentsOfFile: appPath + "/.com.apple.mobile_container_manager.metadata.plist"),
+ let bundleID = meta["MCMMetadataIdentifier"] as? String {
+ map[bundleID] = appPath
+ continue
+ }
+
+ // FALLBACK
+ if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"),
+ let bundleID = info["CFBundleIdentifier"] as? String {
+ map[bundleID] = appPath
+ }
+ }
+ }
+
+ return map
+ }
+
+ // MARK: Data container scan
+
+ private func findBundleIDInDataContainer(_ dataPath: String) -> String? {
+
+ guard let items = try? fm.contentsOfDirectory(atPath: dataPath) else {
+ return nil
+ }
+
+ for item in items where item.hasSuffix(".app") {
+
+ let infoPath = dataPath + "/" + item + "/Info.plist"
+
+ if let info = NSDictionary(contentsOfFile: infoPath),
+ let bundleID = info["CFBundleIdentifier"] as? String {
+ return bundleID
+ }
+ }
+
+ return nil
+ }
+
+ // MARK: Bundle fallback
+
+ private func findBundlePathFallback(bundleID: String) -> String? {
+
+ guard let roots = try? fm.contentsOfDirectory(atPath: bundleRoot) else {
+ return nil
+ }
+
+ for root in roots {
+
+ let rootPath = bundleRoot + "/" + root
+
+ guard let items = try? fm.contentsOfDirectory(atPath: rootPath) else {
+ continue
+ }
+
+ for item in items where item.hasSuffix(".app") {
+
+ let appPath = rootPath + "/" + item
+
+ if let info = NSDictionary(contentsOfFile: appPath + "/Info.plist"),
+ let id = info["CFBundleIdentifier"] as? String,
+ id == bundleID {
+ return appPath
+ }
+ }
+ }
+
+ return nil
+ }
+
+ // MARK: SAFE NAME (never empty)
+
+ private func readSafeName(bundlePath: String, fallback: String) -> String {
+
+ let infoPath = bundlePath + "/Info.plist"
+
+ guard let info = NSDictionary(contentsOfFile: infoPath) else {
+ return fallback
+ }
+
+ return info["CFBundleDisplayName"] as? String ??
+ info["CFBundleName"] as? String ??
+ fallback
+ }
+
+ // MARK: Icon
+
+ private func readIcon(_ bundlePath: String) -> UIImage? {
+
+ let infoPath = bundlePath + "/Info.plist"
+
+ guard let info = NSDictionary(contentsOfFile: infoPath) else {
+ return UIImage(systemName: "app")
+ }
+
+ if let icons = info["CFBundleIcons"] as? [String: Any],
+ let primary = icons["CFBundlePrimaryIcon"] as? [String: Any],
+ let files = primary["CFBundleIconFiles"] as? [String],
+ let iconName = files.last {
+
+ let path = bundlePath + "/" + iconName
+
+ return UIImage(contentsOfFile: path)
+ ?? UIImage(contentsOfFile: path + "@2x.png")
+ ?? UIImage(contentsOfFile: path + ".png")
+ }
+
+ return UIImage(systemName: "app")
+ }
+}
diff --git a/lara/views/tweaks/cacheclean/CacheView.swift b/lara/views/tweaks/cacheclean/CacheView.swift
new file mode 100644
index 000000000..f7294dad6
--- /dev/null
+++ b/lara/views/tweaks/cacheclean/CacheView.swift
@@ -0,0 +1,356 @@
+//
+//
+// CacheView.swift
+//
+// lara
+//
+
+import SwiftUI
+import UIKit
+import WebKit
+import Combine
+
+// MARK: - Models
+
+struct CacheApp: Identifiable {
+ let id: String
+ let name: String
+ let bundleID: String?
+
+ let appBundlePath: String?
+ let dataContainerPath: String
+
+ let icon: UIImage?
+
+ let cacheSize: Int64
+ let tmpSize: Int64
+ let documentsSize: Int64
+
+ let cachePath: String
+ let tmpPath: String
+ let documentsPath: String
+}
+
+struct StorageSnapshot: Identifiable {
+ let id = UUID()
+ let date = Date()
+ let totalBytes: Int64
+}
+
+struct AppRecord {
+ let dataUUID: String
+ let bundleID: String
+ let bundlePath: String
+ let name: String
+ let icon: UIImage?
+}
+
+// MARK: - Manager
+
+final class CleanerManager: ObservableObject {
+
+ @Published var apps: [CacheApp] = []
+ @Published var snapshots: [StorageSnapshot] = []
+
+ @Published var isScanning = false
+ @Published var scanProgress: Double = 0
+ @Published var statusText = ""
+
+ @Published var totalCacheBytes: Int64 = 0
+
+ private let fm = FileManager.default
+ private let dataRoot = "/var/mobile/Containers/Data/Application"
+
+ // Resolver (optional enrichment layer)
+ private let resolver = BundleResolver()
+ private var appDB: [String: AppRecord] = [:]
+
+ // MARK: Build DB (SAFE)
+ private func buildDatabase() {
+ let resolved = resolver.resolveAll()
+
+ var db: [String: AppRecord] = [:]
+ for app in resolved {
+ db[app.dataUUID] = AppRecord(
+ dataUUID: app.dataUUID,
+ bundleID: app.bundleID,
+ bundlePath: app.bundlePath,
+ name: app.name,
+ icon: app.icon
+ )
+ }
+
+ self.appDB = db
+ }
+
+ // MARK: Scan
+
+ func startScan(minSizeMB: Int64 = 1) {
+
+ guard !isScanning else { return }
+
+ isScanning = true
+ apps.removeAll()
+ scanProgress = 0
+ totalCacheBytes = 0
+ statusText = "Scanning apps..."
+
+ DispatchQueue.global(qos: .userInitiated).async {
+
+ self.buildDatabase()
+
+ let containers = (try? self.fm.contentsOfDirectory(atPath: self.dataRoot)) ?? []
+
+ let total = max(containers.count, 1)
+ var processed = 0
+
+ var results: [CacheApp] = []
+
+ for uuid in containers {
+
+ let dataPath = self.dataRoot + "/" + uuid
+
+ let cachePath = dataPath + "/Library/Caches"
+ let tmpPath = dataPath + "/tmp"
+ let docsPath = dataPath + "/Documents"
+
+ let cacheSize = self.folderSize(cachePath)
+ let tmpSize = self.folderSize(tmpPath)
+ let docsSize = self.folderSize(docsPath)
+
+ let totalSize = cacheSize + tmpSize + docsSize
+
+ // Only skip extremely small containers
+ if totalSize < minSizeMB * 1024 * 1024 {
+ processed += 1
+ continue
+ }
+
+ let appInfo = self.appDB[uuid]
+
+ let name = appInfo?.name ?? "App \(uuid.suffix(6))"
+ let bundleID = appInfo?.bundleID
+ let bundlePath = appInfo?.bundlePath
+ let icon = appInfo?.icon
+
+ results.append(CacheApp(
+ id: uuid,
+ name: name,
+ bundleID: bundleID,
+ appBundlePath: bundlePath,
+ dataContainerPath: dataPath,
+ icon: icon,
+ cacheSize: cacheSize,
+ tmpSize: tmpSize,
+ documentsSize: docsSize,
+ cachePath: cachePath,
+ tmpPath: tmpPath,
+ documentsPath: docsPath
+ ))
+
+ processed += 1
+
+ DispatchQueue.main.async {
+ self.scanProgress = Double(processed) / Double(total)
+ self.statusText = "Scanning \(processed)/\(total)"
+ }
+ }
+
+ let totalBytes = results.reduce(0) {
+ $0 + $1.cacheSize + $1.tmpSize + $1.documentsSize
+ }
+
+ DispatchQueue.main.async {
+ self.apps = results.sorted { $0.cacheSize > $1.cacheSize }
+ self.totalCacheBytes = totalBytes
+
+ self.snapshots.append(StorageSnapshot(totalBytes: totalBytes))
+
+ self.isScanning = false
+ self.scanProgress = 1.0
+ self.statusText = "Completed (\(results.count) apps)"
+ }
+ }
+ }
+
+ // MARK: Delete
+
+ func deleteCache(_ app: CacheApp) {
+ try? fm.removeItem(atPath: app.cachePath)
+ try? fm.removeItem(atPath: app.tmpPath)
+ }
+
+ func deleteAll() {
+ for app in apps {
+ deleteCache(app)
+ }
+ }
+
+ // MARK: Web cleanup
+
+ func cleanWKWebView() {
+
+ let types: Set = [
+ WKWebsiteDataTypeDiskCache,
+ WKWebsiteDataTypeMemoryCache,
+ WKWebsiteDataTypeCookies,
+ WKWebsiteDataTypeLocalStorage,
+ WKWebsiteDataTypeSessionStorage,
+ WKWebsiteDataTypeIndexedDBDatabases,
+ WKWebsiteDataTypeWebSQLDatabases
+ ]
+
+ WKWebsiteDataStore.default().removeData(
+ ofTypes: types,
+ modifiedSince: Date(timeIntervalSince1970: 0)
+ ) {
+ DispatchQueue.main.async {
+ self.statusText = "WKWebView cleared"
+ }
+ }
+ }
+
+ func cleanURLCache() {
+ URLCache.shared.removeAllCachedResponses()
+ }
+
+ // MARK: Size
+
+ private func folderSize(_ path: String) -> Int64 {
+ guard let e = fm.enumerator(atPath: path) else { return 0 }
+
+ var size: Int64 = 0
+
+ for case let file as String in e {
+
+ let full = (path as NSString).appendingPathComponent(file)
+
+ if let attrs = try? fm.attributesOfItem(atPath: full),
+ let fileSize = attrs[.size] as? NSNumber {
+ size += fileSize.int64Value
+ }
+ }
+
+ return size
+ }
+}
+
+// MARK: - UI
+
+struct CacheView: View {
+
+ @StateObject var mgr = CleanerManager()
+
+ var body: some View {
+
+ NavigationStack {
+
+ VStack {
+
+ Text("Clean Cache")
+ .font(.title2).bold()
+
+ Text("\(mgr.totalCacheBytes / 1024 / 1024) MB Total")
+ .font(.title)
+
+ ProgressView(value: mgr.scanProgress)
+
+ Text(mgr.statusText)
+ .font(.caption)
+
+ List {
+
+ Section("Apps") {
+
+ ForEach(mgr.apps) { app in
+
+ HStack {
+
+ if let icon = app.icon {
+ Image(uiImage: icon)
+ .resizable()
+ .frame(width: 40, height: 40)
+ .cornerRadius(8)
+ } else {
+ Image(systemName: "app")
+ }
+
+ VStack(alignment: .leading) {
+ Text(app.name).bold()
+ Text("Cache \(app.cacheSize / 1024 / 1024) MB")
+ Text("Tmp \(app.tmpSize / 1024 / 1024) MB")
+ Text("Docs \(app.documentsSize / 1024 / 1024) MB")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ .swipeActions {
+ Button(role: .destructive) {
+ mgr.deleteCache(app)
+ } label: {
+ Text("Delete")
+ }
+ }
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ mgr.deleteCache(app)
+ } label: {
+ Text("Cache")
+ }
+ }
+ .swipeActions(edge: .leading) {
+ Button(role: .destructive) {
+ try? FileManager.default.removeItem(atPath: app.documentsPath)
+ if let index = mgr.apps.firstIndex(where: { $0.id == app.id }) {
+ mgr.apps[index] = CacheApp(
+ id: app.id,
+ name: app.name,
+ bundleID: app.bundleID,
+ appBundlePath: app.appBundlePath,
+ dataContainerPath: app.dataContainerPath,
+ icon: app.icon,
+ cacheSize: app.cacheSize,
+ tmpSize: app.tmpSize,
+ documentsSize: 0,
+ cachePath: app.cachePath,
+ tmpPath: app.tmpPath,
+ documentsPath: app.documentsPath
+
+ )
+ }
+
+ } label: {
+ Text("Docs")
+ }
+ .tint(.orange)
+ }
+ }
+ }
+
+ Section("Tools") {
+
+ Button("Delete ALL Cache") {
+ mgr.deleteAll()
+ }
+ .foregroundStyle(.red)
+
+ Button("Clear WKWebView") {
+ mgr.cleanWKWebView()
+ }
+
+ Button("Clear URLCache") {
+ mgr.cleanURLCache()
+ }
+
+ Button("Rescan") {
+ mgr.startScan()
+ }
+ }
+ }
+ }
+ .onAppear {
+ mgr.startScan()
+ }
+ }
+ }
+}
diff --git a/lara/views/tweaks/mobilegestalt/GestaltView.swift b/lara/views/tweaks/mobilegestalt/GestaltView.swift
index 5c843b041..ca96a1d0c 100644
--- a/lara/views/tweaks/mobilegestalt/GestaltView.swift
+++ b/lara/views/tweaks/mobilegestalt/GestaltView.swift
@@ -28,6 +28,7 @@ enum fileloc: String, CaseIterable {
let mgCurrentPath = "/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist"
+@MainActor
struct GestaltView: View {
@AppStorage("gestaltwarn") private var gestaltwarn: Bool = true
@AppStorage("mgDeviceName") private var mgDeviceName: String = ""
@@ -743,19 +744,23 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data {
let attrs = try fm.attributesOfItem(atPath: targetPath)
if let current = attrs[.size] as? NSNumber,
current.intValue == 0 {
- Alertinator.shared.alert(
- title: "Dangerous Plist State Detected",
- body: "The current plist file is already 0 bytes. Overwriting has been aborted to prevent corruption."
- )
+ Task { @MainActor in
+ Alertinator.shared.alert(
+ title: "Dangerous Plist State Detected",
+ body: "The current plist file is already 0 bytes. Overwriting has been aborted to prevent corruption."
+ )
+ }
throw "Current MobileGestalt file is 0 bytes."
}
}
guard PropertyListSerialization.propertyList(plist, isValidFor: .binary) else {
- Alertinator.shared.alert(
- title: "Invalid Property List",
- body: "The plist is invalid and cannot be written safely."
- )
+ Task { @MainActor in
+ Alertinator.shared.alert(
+ title: "Invalid Property List",
+ body: "The plist is invalid and cannot be written safely."
+ )
+ }
throw "Invalid plist structure."
}
@@ -766,10 +771,12 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data {
)
if data.isEmpty || data.count == 0 {
- Alertinator.shared.alert(
- title: "Refusing Empty Plist Write",
- body: "The generated plist would become 0 bytes after overwrite. Operation cancelled."
- )
+ Task { @MainActor in
+ Alertinator.shared.alert(
+ title: "Refusing Empty Plist Write",
+ body: "The generated plist would become 0 bytes after overwrite. Operation cancelled."
+ )
+ }
throw "Serialized plist data is empty."
}
@@ -780,10 +787,12 @@ func verifyPlist(_ plist: Any, targetPath: String) throws -> Data {
format: nil
)
} catch {
- Alertinator.shared.alert(
- title: "Invalid Serialized Property List",
- body: "The generated plist failed validation after serialization."
- )
+ Task { @MainActor in
+ Alertinator.shared.alert(
+ title: "Invalid Serialized Property List",
+ body: "The generated plist failed validation after serialization."
+ )
+ }
throw "Serialized plist validation failed."
}
diff --git a/lara.xcodeproj/project.pbxproj b/lara1.xcodeproj/project.pbxproj
similarity index 100%
rename from lara.xcodeproj/project.pbxproj
rename to lara1.xcodeproj/project.pbxproj
diff --git a/lara.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/lara1.xcodeproj/project.xcworkspace/contents.xcworkspacedata
similarity index 100%
rename from lara.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename to lara1.xcodeproj/project.xcworkspace/contents.xcworkspacedata
diff --git a/project.yml b/project.yml
new file mode 100644
index 000000000..6d32a4a38
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,28 @@
+name: lara
+
+options:
+ bundleIdPrefix: com.roooot
+ deploymentTarget:
+ iOS: 17.0
+
+targets:
+ lara:
+ type: application
+ platform: iOS
+
+ sources:
+ - path: lara
+
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.roooot.lara
+ INFOPLIST_FILE: lara/Info.plist
+ SWIFT_OBJC_BRIDGING_HEADER: lara/lara-Bridging-Header.h
+ LIBRARY_SEARCH_PATHS: $(inherited) lara/lib/
+ OTHER_LDFLAGS: $(inherited) -lxpf -lgrabkernel2
+ SWIFT_VERSION: 5.0
+ SWIFT_STRICT_CONCURRENCY: minimal
+
+ dependencies:
+ - sdk: UIKit.framework
+ - sdk: Foundation.framework
diff --git a/scripts/build_ipa.sh b/scripts/build_ipa.sh
index cb166dc40..0ab17f1cf 100755
--- a/scripts/build_ipa.sh
+++ b/scripts/build_ipa.sh
@@ -8,7 +8,7 @@ echo "Build Started!"
echo
xcodebuild \
- -project lara.xcodeproj \
+ -project lara1.xcodeproj \
-scheme lara \
-configuration Debug \
-sdk iphoneos \
@@ -18,27 +18,77 @@ xcodebuild \
CODE_SIGN_IDENTITY="" \
CODE_SIGN_ENTITLEMENTS="Config/lara.entitlements" \
archive \
- -archivePath "$PWD/build/lara.xcarchive" 2>&1 | xcpretty
+ -archivePath "$PWD/build/lara.xcarchive"
+
+# -----------------------------------
+# FIND APP FROM XCODE OUTPUT
+# -----------------------------------
+
+APP_PATH=$(find "$PWD/build/lara.xcarchive/Products/Applications" -name "*.app" -type d | head -n 1)
-APP_PATH="$PWD/build/lara.xcarchive/Products/Applications/lara.app"
if [ ! -d "$APP_PATH" ]; then
- echo "Missing app at $APP_PATH"
+ echo "Missing .app in archive"
exit 1
fi
-rm -rf "$PWD/build/Payload"
-mkdir -p "$PWD/build/Payload"
-cp -R "$APP_PATH" "$PWD/build/Payload/"
-plutil -replace UIFileSharingEnabled -bool YES "$PWD/build/Payload/lara.app/Info.plist"
+echo "Found app: $APP_PATH"
+
+# -----------------------------------
+# COPY TO PROJECT ROOT (NEW BEHAVIOR)
+# -----------------------------------
+
+APP_ROOT="$PWD/lara.app"
+
+rm -rf "$APP_ROOT"
+cp -R "$APP_PATH" "$APP_ROOT"
+
+echo "Copied app to project root: $APP_ROOT"
+
+# -----------------------------------
+# MODIFY INFO.PLIST
+# -----------------------------------
+
+plutil -replace UIFileSharingEnabled -bool YES "$APP_ROOT/Info.plist"
+
+# -----------------------------------
+# DETECT EXECUTABLE NAME
+# -----------------------------------
+
+EXEC_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleExecutable" "$APP_ROOT/Info.plist")
+
+if [ -z "$EXEC_NAME" ]; then
+ echo "Failed to read CFBundleExecutable"
+ exit 1
+fi
+
+echo "Executable: $EXEC_NAME"
+
+# -----------------------------------
+# SIGN (ldid)
+# -----------------------------------
if ! command -v ldid >/dev/null 2>&1; then
echo "ERROR: ldid not installed. Install with: brew install ldid" >&2
exit 1
fi
-ldid -SConfig/lara.entitlements "$PWD/build/Payload/lara.app/lara"
-(cd "$PWD/build" && /usr/bin/zip -qry lara.ipa Payload)
+
+ldid -SConfig/lara.entitlements "$APP_ROOT/$EXEC_NAME"
+#cd "$APP_ROOT"
+#mkdir Frameworks
+#mv libgrabkernel2.dylib Frameworks/
+#mv libxpf.dylib Frameworks/
+
+# -----------------------------------
+# BUILD IPA
+# -----------------------------------
+
+rm -rf Payload
+mkdir -p Payload
+cp -R "$APP_ROOT" Payload/
+
+(cd "$PWD" && /usr/bin/zip -qry lara.ipa Payload)
echo
echo "build successful!"
-echo "ipa at: build/lara.ipa"
+echo "ipa at: $PWD/lara.ipa"
exit 0