From bb5a65e9abc99e68ae39ea3f7544b2f78ec4514a Mon Sep 17 00:00:00 2001 From: Kruszoneq <83231322+Kruszoneq@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:13:14 +0200 Subject: [PATCH 1/4] Refactor analysis logic into focused modules with macOS separation This change restructures the analysis layer into smaller, focused files while preserving existing behavior and UI contracts. The main AnalysisLogic type remains the facade used by the view, with file selection, analysis flow, USB handling, lifecycle, and notifications split into dedicated units. macOS-specific analysis responsibilities were moved into a dedicated macOS logic folder and renamed consistently to prepare cleanly for future Linux-specific counterparts without altering runtime rules. --- .github/workflows/development_build_debug.yml | 33 + docs/AGENTS.md | 33 + docs/reference/core/FILE_STRUCTURE.md | 16 + .../analysis/ANALYSIS_COMPATIBILITY.md | 2 + macUSB.xcodeproj/project.pbxproj | 10 + macUSB/Features/Analysis/AnalysisLogic.swift | 1131 +---------------- .../Analysis/AnalysisNotifications.swift | 10 + .../Analysis/AnalysisSelectionHandoff.swift | 19 + .../Logic/AnalysisLogicAnalysisFlow.swift | 139 ++ .../Logic/AnalysisLogicFileSelection.swift | 113 ++ .../Logic/AnalysisLogicUsbDrives.swift | 99 ++ .../AnalysisLogicMacOSCompatibility.swift | 319 +++++ .../AnalysisLogicMacOSImageMounting.swift | 223 ++++ .../AnalysisLogicMacOSInstallerIcon.swift | 82 ++ .../AnalysisLogicMacOSInstallerMetadata.swift | 53 + .../macOS/AnalysisLogicMacOSLifecycle.swift | 104 ++ macUSB/Resources/Localizable.xcstrings | 1 + 17 files changed, 1262 insertions(+), 1125 deletions(-) create mode 100644 .github/workflows/development_build_debug.yml create mode 100644 macUSB/Features/Analysis/AnalysisNotifications.swift create mode 100644 macUSB/Features/Analysis/AnalysisSelectionHandoff.swift create mode 100644 macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift create mode 100644 macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift create mode 100644 macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift create mode 100644 macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift create mode 100644 macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift create mode 100644 macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerIcon.swift create mode 100644 macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift create mode 100644 macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift diff --git a/.github/workflows/development_build_debug.yml b/.github/workflows/development_build_debug.yml new file mode 100644 index 0000000..e6e846d --- /dev/null +++ b/.github/workflows/development_build_debug.yml @@ -0,0 +1,33 @@ +name: Debug Build (development) + +on: + push: + branches: + - development + pull_request: + branches: + - development + +concurrency: + group: debug-build-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + debug-build: + name: macUSB - Debug Build + runs-on: macos-latest + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build macUSB (Debug) + run: | + xcodebuild \ + -project macUSB.xcodeproj \ + -scheme macUSB \ + -configuration Debug \ + -destination 'platform=macOS' \ + CODE_SIGNING_ALLOWED=NO \ + build diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 13770b8..d9818e5 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -174,6 +174,18 @@ xcodebuild -project macUSB.xcodeproj -scheme macUSB -configuration Debug -destin xcodebuild -project macUSB.xcodeproj -scheme macUSB -configuration Debug -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build ``` +## CI required check for development + +For `development`, a GitHub Actions Debug build check is required for both: +- push to `development`, +- pull requests targeting `development`. + +Required workflow file: +- `.github/workflows/development_build_debug.yml` + +Required status check (branch protection): +- `Build Debug app` + ## Change classification Use these rules to decide required documentation updates: @@ -224,6 +236,27 @@ Minor helper changes that do not alter behavior may proceed, but must still be r - Keep one clear orchestrator per workflow and move helper policies/utilities/IO/details to focused companion files. - Treat structural refactors as behavior-preserving by default (no runtime/UI changes unless explicitly requested). +## File naming rules + +- File names must use letters only (`A-Z`, `a-z`) by default. +- Use `PascalCase` and compose names from: + - feature/domain prefix, + - concrete responsibility, + - role suffix. +- Match existing repository patterns, for example: + - `AnalysisLogicMacOSCompatibility.swift` + - `MacOSDiscoveryCatalogParser.swift` + - `MacOSDownloadOrchestrator.swift` + - `MacOSDownloaderListView.swift` +- The underscore (`_`) is allowed only when an external or required structure mandates it (for example GitHub workflow conventions). +- Do not use spaces, hyphens, digits, or other special characters in file names unless required by external format constraints. + +## Large file split rule + +- If a file becomes materially larger after a change (many additional lines), the agent must explicitly evaluate whether splitting it into smaller focused files makes sense. +- When a split is reasonable, the agent should propose the split before implementation or include the split in the same change when safe. +- Splits should preserve behavior and separate concerns (orchestrator vs helpers/policies/parsers/ui parts). + ## Branch naming convention When branch creation is requested: diff --git a/docs/reference/core/FILE_STRUCTURE.md b/docs/reference/core/FILE_STRUCTURE.md index 1173409..4e2456e 100644 --- a/docs/reference/core/FILE_STRUCTURE.md +++ b/docs/reference/core/FILE_STRUCTURE.md @@ -13,6 +13,22 @@ - `macUSB/Features/Finish/*` — result and cleanup UX. - `macUSB/Features/Downloader/*` — downloader coordinator + UI + logic split. +### Analysis layout + +- `macUSB/Features/Analysis/SystemAnalysisView.swift` — analysis UI screen. +- `macUSB/Features/Analysis/AnalysisLogic.swift` — analysis state + facade API for UI bindings. +- `macUSB/Features/Analysis/AnalysisSelectionHandoff.swift` — handoff bridge for pending installer URL from downloader flow. +- `macUSB/Features/Analysis/AnalysisNotifications.swift` — shared `Notification.Name` constants used by analysis/flow wiring. +- `macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift` — file selection/drop/open-panel logic. +- `macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift` — orchestration of analysis execution for `.app` and image sources. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift` — macOS-only compatibility/version-family detection rules and flag mapping. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift` — image mounting + mounted-source guard + legacy image read logic. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift` — installer metadata/version parsing and USB capacity mapping helpers. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerIcon.swift` — installer icon discovery. +- `macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift` — USB drive enumeration/refresh/capacity checks. +- `macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift` — reset/cleanup/manual Tiger flow helpers. +- Reserved naming for future non-macOS support: `AnalysisLogicLinuxCompatibility.swift` (not implemented yet). + ### Downloader layout - `macUSB/Features/Downloader/MacOSDownloaderCoordinator.swift` diff --git a/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md b/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md index e590377..3bd2be0 100644 --- a/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md +++ b/docs/reference/features/analysis/ANALYSIS_COMPATIBILITY.md @@ -1,5 +1,7 @@ # Analysis and Compatibility Contract +Current implementation scope of this contract is the macOS analysis path (see `AnalysisLogicMacOSCompatibility.swift`). + ## Detection Source of Truth Analysis flags are the source of truth for workflow branch selection. diff --git a/macUSB.xcodeproj/project.pbxproj b/macUSB.xcodeproj/project.pbxproj index 7cc7ff6..782cbaa 100644 --- a/macUSB.xcodeproj/project.pbxproj +++ b/macUSB.xcodeproj/project.pbxproj @@ -107,6 +107,16 @@ App/ContentView.swift, App/macUSBApp.swift, Features/Analysis/AnalysisLogic.swift, + Features/Analysis/AnalysisNotifications.swift, + Features/Analysis/AnalysisSelectionHandoff.swift, + Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift, + Features/Analysis/Logic/AnalysisLogicFileSelection.swift, + Features/Analysis/Logic/AnalysisLogicUsbDrives.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerIcon.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift, + Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift, Features/Analysis/SystemAnalysisView.swift, Features/Downloader/Logic/Assembly/MacOSAssemblyFileUtils.swift, Features/Downloader/Logic/Assembly/MacOSAssemblyLegacyFlow.swift, diff --git a/macUSB/Features/Analysis/AnalysisLogic.swift b/macUSB/Features/Analysis/AnalysisLogic.swift index 58ed63e..fd567bc 100644 --- a/macUSB/Features/Analysis/AnalysisLogic.swift +++ b/macUSB/Features/Analysis/AnalysisLogic.swift @@ -1,16 +1,9 @@ import SwiftUI import AppKit -import UniformTypeIdentifiers -import Combine import Foundation +import Combine final class AnalysisLogic: ObservableObject { - private enum ImageReadResult { - case success(name: String, rawVersion: String, appURL: URL, mountPath: String) - case sourceAlreadyMounted(mountPath: String) - case failure - } - // MARK: - Published State (moved from SystemAnalysisView) @Published var selectedFilePath: String = "" @Published var selectedFileUrl: URL? @@ -90,18 +83,6 @@ final class AnalysisLogic: ObservableObject { requiredUSBCapacityGB.map(String.init) ?? "--" } - private var requiredUSBCapacityBytes: Int? { - guard let requiredGB = requiredUSBCapacityGB else { return nil } - switch requiredGB { - case 16: - return 15_000_000_000 - case 32: - return 28_000_000_000 - default: - return requiredGB * 1_000_000_000 - } - } - // Computed: true only when app has recognized a supported system and can proceed normally var isRecognizedAndSupported: Bool { // Recognized and supported when analysis finished, a valid source exists or PPC flow is selected, @@ -113,1116 +94,16 @@ final class AnalysisLogic: ObservableObject { return recognized && hasValidSourceOrPPC && detected && !unsupported } - private func updateRequiredUSBCapacity(rawVersion: String, name: String) { - guard let majorVersion = marketingMajorVersion(raw: rawVersion, name: name) else { - requiredUSBCapacityGB = nil - return - } - requiredUSBCapacityGB = (majorVersion >= 15) ? 32 : 16 - } - - private func marketingMajorVersion(raw: String, name: String) -> Int? { - let marketingVersion = formatMarketingVersion(raw: raw, name: name) - guard let majorToken = marketingVersion.split(separator: ".").first else { return nil } - return Int(majorToken) - } - - // MARK: - Helper to enumerate external hard drives (non-removable) - private func enumerateExternalUSBHardDrives() -> [USBDrive] { - let keys: [URLResourceKey] = [ - .volumeNameKey, - .volumeIsRemovableKey, - .volumeIsInternalKey, - .volumeTotalCapacityKey - ] - guard let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: .skipHiddenVolumes) else { return [] } - - let candidates: [USBDrive] = urls.compactMap { url -> USBDrive? in - guard let v = try? url.resourceValues(forKeys: Set(keys)) else { return nil } - // Only external (non-internal), non-network, non-removable volumes (HDD/SSD) - if (v.volumeIsInternal ?? true) { return nil } - // Filter out obvious network-mounted volumes by scheme (e.g., afp, smb, nfs) - let scheme = url.scheme?.lowercased() - if let scheme = scheme, ["afp", "smb", "nfs", "ftp", "webdav"].contains(scheme) { return nil } - if (v.volumeIsRemovable ?? false) { return nil } - guard let name = v.volumeName else { return nil } - let bsd = USBDriveLogic.getBSDName(from: url) - guard !bsd.isEmpty && bsd != "unknown" else { return nil } - let totalCapacity = Int64(v.volumeTotalCapacity ?? 0) - let size = ByteCountFormatter.string(fromByteCount: totalCapacity, countStyle: .file) - let whole = USBDriveLogic.wholeDiskName(from: bsd) - let speed = USBDriveLogic.detectUSBSpeed(forBSDName: whole) - let partitionScheme = USBDriveLogic.detectPartitionScheme(forBSDName: whole) - let fileSystemFormat = USBDriveLogic.detectFileSystemFormat(forVolumeURL: url) - return USBDrive( - name: name, - device: bsd, - size: size, - url: url, - usbSpeed: speed, - partitionScheme: partitionScheme, - fileSystemFormat: fileSystemFormat - ) - } - return candidates - } - // MARK: - Logging - private func log(_ message: String, category: String = "FileAnalysis") { + func log(_ message: String, category: String = "FileAnalysis") { AppLogging.info(message, category: category) } - private func logError(_ message: String, category: String = "FileAnalysis") { - AppLogging.error(message, category: category) - } - private func stage(_ title: String) { - AppLogging.stage(title) - } - - // MARK: - Logic (moved from SystemAnalysisView) - func handleDrop(providers: [NSItemProvider]) -> Bool { - self.log("Odebrano przeciągnięcie pliku (providers=\(providers.count)). Szukam URL...") - if let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) }) { - provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, error) in - if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { - self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - self.processDroppedURL(url) - } - } - else if let url = item as? URL { - self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - self.processDroppedURL(url) - } - } - } - return true - } - return false - } - - func processDroppedURL(_ url: URL) { - DispatchQueue.main.async { - let ext = url.pathExtension.lowercased() - self.log("Wybrano plik w formacie .\(ext). Resetuję stan i przygotowuję analizę.") - if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { - withAnimation { - self.selectedFilePath = url.path - self.selectedFileUrl = url - self.recognizedVersion = "" - self.isSystemDetected = false - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - } - self.log("Lokalizacja wybranego pliku: \(url.path)") - self.log("Źródło do rozpoznania wersji: \(url.path)") - } - } - } - - func applySelectedURLAndStartAnalysis(_ url: URL) { - let ext = url.pathExtension.lowercased() - guard ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" else { - logError("Pominięto automatyczne podstawienie ścieżki. Nieobsługiwany format: .\(ext)") - return - } - - processDroppedURL(url) - DispatchQueue.main.async { [weak self] in - self?.startAnalysis() - } - } - - func selectDMGFile() { - self.log("Otwieram panel wyboru pliku…") - let p = NSOpenPanel() - p.allowedContentTypes = [.diskImage, .applicationBundle] - // Dodajemy obsługę .iso i .cdr, które nie mają jeszcze UTType w UniformTypeIdentifiers, więc rozszerzamy allowedFileTypes - p.allowedFileTypes = ["dmg", "iso", "cdr", "app"] - p.allowsMultipleSelection = false - p.begin { if $0 == .OK, let url = p.url { - let ext = url.pathExtension.lowercased() - guard ext == "dmg" || ext == "iso" || ext == "cdr" || ext == "app" else { return } - withAnimation { - self.selectedFilePath = url.path - self.selectedFileUrl = url - self.recognizedVersion = "" - self.isSystemDetected = false - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - } - self.log("Wybrano plik w formacie .\(ext)") - self.log("Lokalizacja wybranego pliku: \(url.path)") - self.log("Źródło do rozpoznania wersji: \(url.path)") - } else { - self.log("Anulowano wybór pliku") - } } - } - - func startAnalysis() { - guard let url = selectedFileUrl else { return } - self.stage("Analiza pliku — start") - self.log("Rozpoczynam analizę pliku") - self.log("Źródło pliku do odczytu wersji: \(url.path)") - withAnimation { isAnalyzing = true } - detectedSystemIcon = nil - selectedDrive = nil; capacityCheckFinished = false - showUSBSection = false; showUnsupportedMessage = false - isUnsupportedSierra = false - isPPC = false - isMavericks = false - shouldShowAlreadyMountedSourceAlert = false - requiredUSBCapacityGB = nil - - let ext = url.pathExtension.lowercased() - self.log("Wykryto rozszerzenie: \(ext)") - if ext == "dmg" || ext == "iso" || ext == "cdr" { - self.stage("Analiza obrazu (DMG/ISO/CDR) — start") - self.log("Analiza obrazu (DMG/ISO/CDR): montowanie obrazu przez hdiutil (attach -plist -nobrowse -readonly), odczyt Info.plist z aplikacji oraz wykrywanie wersji i trybu instalacji.") - let oldMountPath = self.mountedDMGPath - DispatchQueue.global(qos: .userInitiated).async { - if let path = oldMountPath { - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil"); task.arguments = ["detach", path, "-force"]; try? task.run(); task.waitUntilExit() - } - let shouldDetectAlreadyMountedSource = (ext == "cdr" || ext == "iso") - let result = self.mountAndReadInfo(dmgUrl: url, detectPreMountedSource: shouldDetectAlreadyMountedSource) - DispatchQueue.main.async { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - self.isAnalyzing = false - let mountedReadInfo: (String, String, URL, String)? - let sourceAlreadyMounted: Bool - switch result { - case .success(let name, let rawVersion, let appURL, let mountPath): - mountedReadInfo = (name, rawVersion, appURL, mountPath) - sourceAlreadyMounted = false - case .sourceAlreadyMounted(let mountPath): - mountedReadInfo = nil - sourceAlreadyMounted = true - self.log("Wykryto, że wybrany obraz źródłowy jest już zamontowany: \(mountPath)") - case .failure: - mountedReadInfo = nil - sourceAlreadyMounted = false - } - if let (_, _, _, mp) = mountedReadInfo { self.mountedDMGPath = mp } else { self.mountedDMGPath = nil } - if let (name, rawVer, appURL, _) = mountedReadInfo { - let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) - var cleanName = name - cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") - cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") - cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") - cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") - let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") - - self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" - self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) - self.sourceAppURL = appURL - self.updateDetectedSystemIcon(from: appURL) - - // Try to read ProductUserVisibleVersion from mounted image (Tiger/Leopard) - var userVisibleVersionFromMounted: String? = nil - if let mountPath = self.mountedDMGPath { - let sysVerPlist = URL(fileURLWithPath: mountPath).appendingPathComponent("System/Library/CoreServices/SystemVersion.plist") - if let data = try? Data(contentsOf: sysVerPlist), - let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], - let userVisible = dict["ProductUserVisibleVersion"] as? String { - userVisibleVersionFromMounted = userVisible - } - } - - // Use lowercase name for detection - let nameLower = name.lowercased() - - // Leopard/Tiger detection (PowerPC) using name, raw version, or mounted SystemVersion.plist - let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") || (userVisibleVersionFromMounted?.hasPrefix("10.5") ?? false) - let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") || (userVisibleVersionFromMounted?.hasPrefix("10.4") ?? false) - let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") || (userVisibleVersionFromMounted?.hasPrefix("10.3") ?? false) - let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") || (userVisibleVersionFromMounted?.hasPrefix("10.6") ?? false) - - // Disable kernel arch detection for Panther (10.3), Tiger (10.4), Leopard (10.5) and Snow Leopard (10.6). Always mark PPC flow for legacy, but do not set legacyArchInfo. - if (isLeopard || isTiger || isPanther || isSnowLeopard) { - self.isPPC = true // niezależnie od architektury, proces USB taki sam - self.legacyArchInfo = nil - } - - // Legacy versions exact recognition for mounted userVisibleVersion or fallback for legacy systems - if isLeopard { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Leopard \(userVisible)" - } else if rawVer.starts(with: "10.5") { - self.recognizedVersion = "Mac OS X Leopard \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Leopard" - } - } - if isTiger { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Tiger \(userVisible)" - } else if rawVer.starts(with: "10.4") { - self.recognizedVersion = "Mac OS X Tiger \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Tiger" - } - } - if isPanther { - self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Panther \(userVisible)" - } else if rawVer.starts(with: "10.3") { - self.recognizedVersion = "Mac OS X Panther \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Panther" - } - } - if isSnowLeopard { - if let userVisible = userVisibleVersionFromMounted { - self.recognizedVersion = "Mac OS X Snow Leopard \(userVisible)" - } else if rawVer.starts(with: "10.6") { - self.recognizedVersion = "Mac OS X Snow Leopard \(rawVer)" - } else { - self.recognizedVersion = "Mac OS X Snow Leopard" - } - } - // If Panther is detected, mark as unsupported and block further processing - if isPanther { - self.isSystemDetected = false - self.showUSBSection = false - self.isPPC = false - self.legacyArchInfo = nil - // Show unsupported message immediately - withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { - self.showUnsupportedMessage = true - } - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - return - } - - // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ - let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") - - // Catalina detection - let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") - - // Sierra detection (supported only for installer version 12.6.06) - let isSierra = (rawVer == "12.6.06") - let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") - let isUnsupportedSierraVersion = isSierraName && !isSierra - self.isUnsupportedSierra = isUnsupportedSierraVersion - if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } - - let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") - - // Modern (Big Sur+) - let isModern = - nameLower.contains("tahoe") || // Dodano Tahoe - nameLower.contains("sur") || - nameLower.contains("monterey") || - nameLower.contains("ventura") || - nameLower.contains("sonoma") || - nameLower.contains("sequoia") || - rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) - rawVer.starts(with: "11.") || - (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || - (rawVer.starts(with: "13.") && !nameLower.contains("high")) || - (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || - (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) - - // Old Supported (Mojave + High Sierra) - let isOldSupported = - nameLower.contains("mojave") || - nameLower.contains("high sierra") || - rawVer.starts(with: "10.14") || - rawVer.starts(with: "10.13") || - (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || - (rawVer.starts(with: "13.") && nameLower.contains("high")) - - // Legacy No Codesign (Yosemite + El Capitan) - let isLegacyDetected = - nameLower.contains("yosemite") || - nameLower.contains("el capitan") || - rawVer.starts(with: "10.10") || - rawVer.starts(with: "10.11") - - // Legacy Restore (Lion + Mountain Lion) - let isRestoreLegacy = - nameLower.contains("mountain lion") || - nameLower.contains("lion") || - rawVer.starts(with: "10.8") || - rawVer.starts(with: "10.7") - - // ZMIANA: Dodanie isCatalina do isSystemDetected - self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra || isMavericks - - // Catalina ma swój własny codesign, więc tu wyłączamy standardowy 'needsCodesign' - self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected - self.isLegacyDetected = isLegacyDetected - self.isRestoreLegacy = isRestoreLegacy - self.isCatalina = isCatalina - self.isSierra = isSierra - self.isMavericks = isMavericks - if isMavericks { - self.shouldShowMavericksDialog = true - } - if isSierra { - self.recognizedVersion = "macOS Sierra 10.12" - self.needsCodesign = false - } - // Dla Leoparda/Tigera już ustawione na true powyżej, pozostaw - self.isPPC = self.isPPC || false - - let trueFlags = [ - self.isCatalina ? "isCatalina" : nil, - self.isSierra ? "isSierra" : nil, - self.isLegacyDetected ? "isLegacyDetected" : nil, - self.isRestoreLegacy ? "isRestoreLegacy" : nil, - self.isPPC ? "isPPC" : nil, - self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, - self.isMavericks ? "isMavericks" : nil - ].compactMap { $0 }.joined(separator: ", ") - self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") - self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") - AppLogging.separator() - - if self.isSystemDetected { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } - } - } else if sourceAlreadyMounted { - self.recognizedVersion = "" - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.isSystemDetected = false - self.showUSBSection = false - self.showUnsupportedMessage = false - self.needsCodesign = true - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.userSkippedAnalysis = false - self.requiredUSBCapacityGB = nil - self.shouldShowAlreadyMountedSourceAlert = true - AppLogging.separator() - } else { - // Użyto String(localized:) aby ten ciąg został wykryty, mimo że jest przypisywany do zmiennej - self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") - self.requiredUSBCapacityGB = nil - self.log("Analiza zakończona: nie rozpoznano instalatora.") - AppLogging.separator() - } - } - } - } - } - else if ext == "app" { - self.stage("Analiza aplikacji (.app) — start") - self.log("Analiza aplikacji (.app): odczyt Info.plist (CFBundleDisplayName, CFBundleShortVersionString) oraz wykrywanie wersji i trybu instalacji.") - self.log("Źródło pliku do odczytu wersji: \(url.path)") - DispatchQueue.global(qos: .userInitiated).async { - let result = self.readAppInfo(appUrl: url) - DispatchQueue.main.async { - withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { - self.isAnalyzing = false - self.mountedDMGPath = nil - if let (name, rawVer, appURL) = result { - let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) - var cleanName = name - cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") - cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") - cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") - cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") - let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") - - self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" - self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) - self.sourceAppURL = appURL - self.updateDetectedSystemIcon(from: appURL) - - let nameLower = name.lowercased() - - // Leopard detection (PowerPC) - let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") - let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") - let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") - let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") - - self.legacyArchInfo = nil - - // Dla Snow Leoparda/Leoparda/Tigera zawsze traktujemy jako PPC flow - if isLeopard || isTiger || isSnowLeopard { - self.isPPC = true - } - - // Ustal dokładną wersję dla Panther/Tiger/Leopard/Snow Leopard (dla .app) - if isPanther || isTiger || isLeopard || isSnowLeopard { - let isExact = rawVer.starts(with: "10.3") || rawVer.starts(with: "10.4") || rawVer.starts(with: "10.5") || rawVer.starts(with: "10.6") - let exactSuffix = isExact ? " \(rawVer)" : "" - if isPanther { - self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") - self.recognizedVersion = "Mac OS X Panther\(exactSuffix)" - } else if isTiger { - self.recognizedVersion = "Mac OS X Tiger\(exactSuffix)" - } else if isLeopard { - self.recognizedVersion = "Mac OS X Leopard\(exactSuffix)" - } else if isSnowLeopard { - self.recognizedVersion = "Mac OS X Snow Leopard\(exactSuffix)" - } - } - // If Panther is detected, mark as unsupported and block further processing - if isPanther { - self.isSystemDetected = false - self.showUSBSection = false - self.isPPC = false - self.legacyArchInfo = nil - // Show unsupported message immediately - withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { - self.showUnsupportedMessage = true - } - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - return - } - - // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ - let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") - - // Catalina detection - let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") - - // Sierra detection (supported only for installer version 12.6.06) - let isSierra = (rawVer == "12.6.06") - let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") - let isUnsupportedSierraVersion = isSierraName && !isSierra - self.isUnsupportedSierra = isUnsupportedSierraVersion - if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } - - let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") - - // Modern (Big Sur+) - let isModern = - nameLower.contains("tahoe") || // Dodano Tahoe - nameLower.contains("sur") || - nameLower.contains("monterey") || - nameLower.contains("ventura") || - nameLower.contains("sonoma") || - nameLower.contains("sequoia") || - rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) - rawVer.starts(with: "11.") || - (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || - (rawVer.starts(with: "13.") && !nameLower.contains("high")) || - (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || - (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) - - // Old Supported (Mojave + High Sierra) - let isOldSupported = - nameLower.contains("mojave") || - nameLower.contains("high sierra") || - rawVer.starts(with: "10.14") || - rawVer.starts(with: "10.13") || - (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || - (rawVer.starts(with: "13.") && nameLower.contains("high")) - - // Legacy No Codesign (Yosemite + El Capitan) - let isLegacyDetected = - nameLower.contains("yosemite") || - nameLower.contains("el capitan") || - rawVer.starts(with: "10.10") || - rawVer.starts(with: "10.11") - - // Legacy Restore (Lion + Mountain Lion) - let isRestoreLegacy = - nameLower.contains("mountain lion") || - nameLower.contains("lion") || - rawVer.starts(with: "10.8") || - rawVer.starts(with: "10.7") - - self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra - - self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected - self.isLegacyDetected = isLegacyDetected - self.isRestoreLegacy = isRestoreLegacy - self.isCatalina = isCatalina - self.isSierra = isSierra - self.isMavericks = isMavericks - if isMavericks { - self.shouldShowMavericksDialog = true - } - if isSierra { - self.recognizedVersion = "macOS Sierra 10.12" - self.needsCodesign = false - } - // isPPC zostało ustawione wcześniej dla Snow Leoparda/Leoparda/Tigera; dla pozostałych pozostaje false - self.isPPC = self.isPPC || false - - let trueFlags = [ - self.isCatalina ? "isCatalina" : nil, - self.isSierra ? "isSierra" : nil, - self.isLegacyDetected ? "isLegacyDetected" : nil, - self.isRestoreLegacy ? "isRestoreLegacy" : nil, - self.isPPC ? "isPPC" : nil, - self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, - self.isMavericks ? "isMavericks" : nil - ].compactMap { $0 }.joined(separator: ", ") - self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") - self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") - AppLogging.separator() - - if self.isSystemDetected { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } - } else { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } - } - } else { - self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") - self.requiredUSBCapacityGB = nil - self.log("Analiza zakończona: nie rozpoznano instalatora.") - AppLogging.separator() - } - } - } - } - } - } - - func forceTigerMultiDVDSelection() { - self.log("Ręcznie wybrano tryb Tiger Multi DVD") - let fileURL = self.selectedFileUrl - DispatchQueue.global(qos: .userInitiated).async { - var mountPoint: String? = self.mountedDMGPath - var effectiveSourceAppURL: URL? = nil - if let url = fileURL { - let ext = url.pathExtension.lowercased() - if ext == "dmg" || ext == "iso" || ext == "cdr" { - if mountPoint == nil { - mountPoint = self.mountImageForPPC(dmgUrl: url) - } - if let mp = mountPoint { - effectiveSourceAppURL = URL(fileURLWithPath: mp).appendingPathComponent("Install") - } - } else if ext == "app" { - effectiveSourceAppURL = url - } - } - DispatchQueue.main.async { - withAnimation { - self.isAnalyzing = false - self.userSkippedAnalysis = true - self.recognizedVersion = "Mac OS X Tiger 10.4" - self.sourceAppURL = effectiveSourceAppURL - self.updateDetectedSystemIcon(from: effectiveSourceAppURL) - self.mountedDMGPath = mountPoint - self.isSystemDetected = true - self.showUnsupportedMessage = false - self.showUSBSection = true - self.needsCodesign = false - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = true - self.legacyArchInfo = nil - self.selectedDrive = nil - self.capacityCheckFinished = false - self.requiredUSBCapacityGB = 16 - } - let flags = [self.isPPC ? "isPPC" : nil].compactMap { $0 }.joined(separator: ", ") - self.log("Ustawiono Tiger Multi DVD: recognizedVersion=\(self.recognizedVersion). Flagi: \(flags.isEmpty ? "brak" : flags)") - } - } - } - - func formatMarketingVersion(raw: String, name: String) -> String { - let n = name.lowercased() - if n.contains("tahoe") { return "26" } // Dodano Tahoe - if n.contains("sequoia") { return "15" } - if n.contains("sonoma") { return "14" } - if n.contains("ventura") { return "13" } - if n.contains("monterey") { return "12" } - if n.contains("big sur") { return "11" } - if n.contains("catalina") { return "10.15" } - if n.contains("mojave") { return "10.14" } - if n.contains("high sierra") { return "10.13" } - if n.contains("sierra") && !n.contains("high") { return "10.12" } - if n.contains("el capitan") { return "10.11" } - if n.contains("yosemite") { return "10.10" } - if n.contains("mavericks") { return "10.9" } - if n.contains("mountain lion") { return "10.8" } - if n.contains("lion") { return "10.7" } - if n.contains("snow leopard") { return "10.6" } - if n.contains("panther") { return "10.3" } - return raw - } - - func readAppInfo(appUrl: URL) -> (String, String, URL)? { - let plistUrl = appUrl.appendingPathComponent("Contents/Info.plist") - self.log("Odczyt Info.plist: \(plistUrl.path)") - if let d = try? Data(contentsOf: plistUrl), - let dict = try? PropertyListSerialization.propertyList(from: d, format: nil) as? [String: Any] { - let name = (dict["CFBundleDisplayName"] as? String) ?? appUrl.lastPathComponent - let ver = (dict["CFBundleShortVersionString"] as? String) ?? "?" - self.log("Odczytano Info.plist: name=\(name), version=\(ver)") - return (name, ver, appUrl) - } - self.logError("Nie udało się odczytać Info.plist") - return nil - } - - private func updateDetectedSystemIcon(from appURL: URL?) { - guard let appURL = appURL else { - self.detectedSystemIcon = nil - return - } - - let iconFileCandidates = [ - "ProductPageIcon.icns", - "InstallAssistant.icns", - "Install Mac OS X.icns" - ] - - for installerURL in self.candidateInstallerLocations(from: appURL) { - let resourcesURL = installerURL.appendingPathComponent("Contents/Resources", isDirectory: true) - self.log("Próba odczytu ikony systemu z katalogu: \(resourcesURL.path)") - guard let iconURL = self.findIconURL(in: resourcesURL, preferredFileNames: iconFileCandidates), - let icon = NSImage(contentsOf: iconURL) else { - continue - } - - self.detectedSystemIcon = icon - self.log("Odczytano ikonę systemu z pliku: \(iconURL.path)") - return - } - - self.detectedSystemIcon = nil - self.log("Nie znaleziono ikony instalatora (\(iconFileCandidates.joined(separator: ", "))) dla: \(appURL.path)") - } - private func candidateInstallerLocations(from url: URL) -> [URL] { - var result: [URL] = [] - let fm = FileManager.default - - func appendUnique(_ candidate: URL) { - let normalized = candidate.standardizedFileURL - guard !result.contains(where: { $0.standardizedFileURL == normalized }) else { return } - result.append(candidate) - } - - appendUnique(url) - - if url.pathExtension.lowercased() != "app" { - // Część starych obrazów ma instalator pod klasyczną nazwą "Install Mac OS X". - appendUnique(url.appendingPathComponent("Install Mac OS X.app", isDirectory: true)) - appendUnique(url.appendingPathComponent("Install OS X.app", isDirectory: true)) - appendUnique(url.appendingPathComponent("Install macOS.app", isDirectory: true)) - - if let children = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { - for child in children where child.pathExtension.lowercased() == "app" { - appendUnique(child) - } - } - } - - return result.filter { fm.fileExists(atPath: $0.path) } - } - - private func findIconURL(in resourcesURL: URL, preferredFileNames: [String]) -> URL? { - let fm = FileManager.default - guard fm.fileExists(atPath: resourcesURL.path), - let files = try? fm.contentsOfDirectory(at: resourcesURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { - return nil - } - - var byLowercasedName: [String: URL] = [:] - for file in files { - byLowercasedName[file.lastPathComponent.lowercased()] = file - } - - for fileName in preferredFileNames { - if let match = byLowercasedName[fileName.lowercased()] { - return match - } - } - - return nil - } - - private func readLegacyInstallMacOSXInfo(from mountURL: URL) -> (String, String, URL)? { - let legacyInstallers = [ - "Install Mac OS X", - "Install Mac OS X.app" - ] - - var foundLegacyPath = false - for installerName in legacyInstallers { - let installerURL = mountURL.appendingPathComponent(installerName, isDirectory: true) - let plistURL = installerURL.appendingPathComponent("Contents/Info.plist") - guard FileManager.default.fileExists(atPath: plistURL.path) else { - continue - } - - foundLegacyPath = true - self.log("Znaleziono legacy installer path: \(installerURL.path)") - self.log("Odczyt Info.plist (legacy): \(plistURL.path)") - - guard let data = try? Data(contentsOf: plistURL), - let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { - self.logError("Nie udało się odczytać Info.plist (legacy): \(plistURL.path)") - continue - } - - let name = (dict["CFBundleDisplayName"] as? String) ?? installerURL.lastPathComponent - let version = (dict["CFBundleShortVersionString"] as? String) ?? "?" - self.log("Odczytano Info.plist (legacy): name=\(name), version=\(version)") - return (name, version, installerURL) - } - - if !foundLegacyPath { - self.log("Nie znaleziono legacy path instalatora 'Install Mac OS X' w: \(mountURL.path)") - } - - return nil - } - - private func normalizedImagePath(_ path: String) -> String { - URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path - } - - private func mountedPathForAlreadyAttachedImage(sourceURL: URL) -> String? { - let task = Process() - task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["info", "-plist"] - let outputPipe = Pipe() - let errorPipe = Pipe() - task.standardOutput = outputPipe - task.standardError = errorPipe - - do { - try task.run() - } catch { - self.logError("Nie udało się uruchomić hdiutil info: \(error.localizedDescription)") - return nil - } - task.waitUntilExit() - - let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - if task.terminationStatus != 0 { - let stderrText = String(data: errorData, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if stderrText.isEmpty { - self.logError("hdiutil info zakończył się błędem (kod \(task.terminationStatus)).") - } else { - self.logError("hdiutil info zakończył się błędem: \(stderrText)") - } - return nil - } - - guard let plist = try? PropertyListSerialization.propertyList(from: outputData, options: [], format: nil) as? [String: Any], - let images = plist["images"] as? [[String: Any]] else { - return nil - } - - let sourcePath = normalizedImagePath(sourceURL.path) - for image in images { - guard let imagePath = image["image-path"] as? String else { continue } - guard normalizedImagePath(imagePath) == sourcePath else { continue } - guard let entities = image["system-entities"] as? [[String: Any]], - let mountPoint = entities.compactMap({ $0["mount-point"] as? String }).first else { - continue - } - return mountPoint - } - - return nil - } - - private func mountAndReadInfo(dmgUrl: URL, detectPreMountedSource: Bool = false) -> ImageReadResult { - self.log("Montowanie obrazu (DMG/ISO/CDR)") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Wybrany obraz .\(dmgUrl.pathExtension.lowercased()) jest już zamontowany w systemie: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] - let pipe = Pipe() - let errorPipe = Pipe() - task.standardOutput = pipe - task.standardError = errorPipe - - do { - try task.run() - } catch { - self.logError("Nie udało się uruchomić hdiutil attach: \(error.localizedDescription)") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po błędzie uruchomienia attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - task.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() - let stderrText = String(data: errorData, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if task.terminationStatus != 0 { - if stderrText.isEmpty { - self.logError("hdiutil attach zakończył się błędem (kod \(task.terminationStatus)).") - } else { - self.logError("hdiutil attach zakończył się błędem: \(stderrText)") - } - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po błędzie attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - - guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { - self.logError("Nie udało się odczytać informacji z obrazu") - if detectPreMountedSource, - let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { - self.log("Po nieudanym odczycie plist wykryto już zamontowany obraz źródłowy: \(mountPoint)") - return .sourceAlreadyMounted(mountPath: mountPoint) - } - return .failure - } - self.log("Przetwarzanie wyników hdiutil attach (\(entities.count) encji)") - for e in entities { - if let mp = e["mount-point"] as? String { - let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) - var mountId = "unknown" - if let dev = devEntry { - let bsd = URL(fileURLWithPath: dev).lastPathComponent // e.g. disk9s1 - if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { - mountId = String(bsd[.. String? { - self.log("Montowanie obrazu (PPC)") - let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") - task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] - let pipe = Pipe(); task.standardOutput = pipe; try? task.run(); task.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { - self.logError("Nie udało się zamontować obrazu (PPC)") - return nil - } - for e in entities { - if let mp = e["mount-point"] as? String { - let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) - var mountId = "unknown" - if let dev = devEntry { - let bsd = URL(fileURLWithPath: dev).lastPathComponent - if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { - mountId = String(bsd[..= minCapacity; capacityCheckFinished = true } - } else { isCapacitySufficient = false; capacityCheckFinished = true } - } - - func resetAll() { - let oldMount = self.mountedDMGPath - if let path = oldMount { - let task = Process() - task.launchPath = "/usr/bin/hdiutil" - task.arguments = ["detach", path, "-force"] - try? task.run() - task.waitUntilExit() - } - DispatchQueue.main.async { - withAnimation { - self.selectedFilePath = "" - self.selectedFileUrl = nil - self.recognizedVersion = "" - self.sourceAppURL = nil - self.detectedSystemIcon = nil - self.mountedDMGPath = nil - - self.isAnalyzing = false - self.isSystemDetected = false - self.showUSBSection = false - self.showUnsupportedMessage = false - - self.needsCodesign = true - self.isLegacyDetected = false - self.isRestoreLegacy = false - self.isCatalina = false - self.isSierra = false - self.isMavericks = false - self.isUnsupportedSierra = false - self.isPPC = false - self.legacyArchInfo = nil - self.shouldShowAlreadyMountedSourceAlert = false - self.userSkippedAnalysis = false - self.shouldShowMavericksDialog = false - self.requiredUSBCapacityGB = nil - - self.availableDrives = [] - self.selectedDrive = nil - - self.isCapacitySufficient = false - self.capacityCheckFinished = false - } - } - } - - // Call this from the UI when the user presses the "Przejdź dalej" button - func recordProceedPressed() { - self.log("Użytkownik nacisnął przycisk 'Przejdź dalej'. Wybrany nośnik: \(self.selectedDrive?.url.path ?? "brak"), źródło: \(self.sourceAppURL?.path ?? "brak"), rozpoznano: \(self.recognizedVersion)") - } -} - -@MainActor -final class AnalysisSelectionHandoff { - static let shared = AnalysisSelectionHandoff() - - private var pendingInstallerURL: URL? - - private init() {} - - func setPendingInstallerURL(_ url: URL) { - pendingInstallerURL = url.standardizedFileURL + func logError(_ message: String, category: String = "FileAnalysis") { + AppLogging.error(message, category: category) } - func consumePendingInstallerURL() -> URL? { - defer { pendingInstallerURL = nil } - return pendingInstallerURL + func stage(_ title: String) { + AppLogging.stage(title) } } - -extension Notification.Name { - static let macUSBResetToStart = Notification.Name("macUSB.resetToStart") - static let macUSBStartTigerMultiDVD = Notification.Name("macUSB.startTigerMultiDVD") - static let macUSBDebugGoToBigSurSummary = Notification.Name("macUSB.debugGoToBigSurSummary") - static let macUSBDebugGoToTigerSummary = Notification.Name("macUSB.debugGoToTigerSummary") - static let macUSBNavigateToAnalysis = Notification.Name("macUSB.navigateToAnalysis") - static let macUSBApplyPendingDownloaderInstaller = Notification.Name("macUSB.applyPendingDownloaderInstaller") -} diff --git a/macUSB/Features/Analysis/AnalysisNotifications.swift b/macUSB/Features/Analysis/AnalysisNotifications.swift new file mode 100644 index 0000000..18bffc7 --- /dev/null +++ b/macUSB/Features/Analysis/AnalysisNotifications.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Notification.Name { + static let macUSBResetToStart = Notification.Name("macUSB.resetToStart") + static let macUSBStartTigerMultiDVD = Notification.Name("macUSB.startTigerMultiDVD") + static let macUSBDebugGoToBigSurSummary = Notification.Name("macUSB.debugGoToBigSurSummary") + static let macUSBDebugGoToTigerSummary = Notification.Name("macUSB.debugGoToTigerSummary") + static let macUSBNavigateToAnalysis = Notification.Name("macUSB.navigateToAnalysis") + static let macUSBApplyPendingDownloaderInstaller = Notification.Name("macUSB.applyPendingDownloaderInstaller") +} diff --git a/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift b/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift new file mode 100644 index 0000000..423914d --- /dev/null +++ b/macUSB/Features/Analysis/AnalysisSelectionHandoff.swift @@ -0,0 +1,19 @@ +import Foundation + +@MainActor +final class AnalysisSelectionHandoff { + static let shared = AnalysisSelectionHandoff() + + private var pendingInstallerURL: URL? + + private init() {} + + func setPendingInstallerURL(_ url: URL) { + pendingInstallerURL = url.standardizedFileURL + } + + func consumePendingInstallerURL() -> URL? { + defer { pendingInstallerURL = nil } + return pendingInstallerURL + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift new file mode 100644 index 0000000..fb0c550 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicAnalysisFlow.swift @@ -0,0 +1,139 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func startAnalysis() { + guard let url = selectedFileUrl else { return } + self.stage("Analiza pliku — start") + self.log("Rozpoczynam analizę pliku") + self.log("Źródło pliku do odczytu wersji: \(url.path)") + withAnimation { isAnalyzing = true } + detectedSystemIcon = nil + selectedDrive = nil; capacityCheckFinished = false + showUSBSection = false; showUnsupportedMessage = false + isUnsupportedSierra = false + isPPC = false + isMavericks = false + shouldShowAlreadyMountedSourceAlert = false + requiredUSBCapacityGB = nil + + let ext = url.pathExtension.lowercased() + self.log("Wykryto rozszerzenie: \(ext)") + if ext == "dmg" || ext == "iso" || ext == "cdr" { + self.stage("Analiza obrazu (DMG/ISO/CDR) — start") + self.log("Analiza obrazu (DMG/ISO/CDR): montowanie obrazu przez hdiutil (attach -plist -nobrowse -readonly), odczyt Info.plist z aplikacji oraz wykrywanie wersji i trybu instalacji.") + let oldMountPath = self.mountedDMGPath + DispatchQueue.global(qos: .userInitiated).async { + if let path = oldMountPath { + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil"); task.arguments = ["detach", path, "-force"]; try? task.run(); task.waitUntilExit() + } + let shouldDetectAlreadyMountedSource = (ext == "cdr" || ext == "iso") + let result = self.mountAndReadInfo(dmgUrl: url, detectPreMountedSource: shouldDetectAlreadyMountedSource) + DispatchQueue.main.async { + withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { + self.isAnalyzing = false + let mountedReadInfo = result?.mountedReadInfo + let sourceAlreadyMountedPath = result?.sourceAlreadyMountedPath + let sourceAlreadyMounted = sourceAlreadyMountedPath != nil + if let mountPath = sourceAlreadyMountedPath { + self.log("Wykryto, że wybrany obraz źródłowy jest już zamontowany: \(mountPath)") + } + if let (_, _, _, mp) = mountedReadInfo { self.mountedDMGPath = mp } else { self.mountedDMGPath = nil } + if let (name, rawVer, appURL, _) = mountedReadInfo { + let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) + var cleanName = name + cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") + cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") + cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") + cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") + let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") + + self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" + self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) + self.sourceAppURL = appURL + self.updateDetectedSystemIcon(from: appURL) + + // Try to read ProductUserVisibleVersion from mounted image (Tiger/Leopard) + var userVisibleVersionFromMounted: String? = nil + if let mountPath = self.mountedDMGPath { + let sysVerPlist = URL(fileURLWithPath: mountPath).appendingPathComponent("System/Library/CoreServices/SystemVersion.plist") + if let data = try? Data(contentsOf: sysVerPlist), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], + let userVisible = dict["ProductUserVisibleVersion"] as? String { + userVisibleVersionFromMounted = userVisible + } + } + + if !self.applyMacosCompatibilityForMountedInstaller(name: name, rawVer: rawVer, userVisibleVersionFromMounted: userVisibleVersionFromMounted) { + return + } + } else if sourceAlreadyMounted { + self.recognizedVersion = "" + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.isSystemDetected = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.needsCodesign = true + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.requiredUSBCapacityGB = nil + self.shouldShowAlreadyMountedSourceAlert = true + AppLogging.separator() + } else { + // Użyto String(localized:) aby ten ciąg został wykryty, mimo że jest przypisywany do zmiennej + self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") + self.requiredUSBCapacityGB = nil + self.log("Analiza zakończona: nie rozpoznano instalatora.") + AppLogging.separator() + } + } + } + } + } + else if ext == "app" { + self.stage("Analiza aplikacji (.app) — start") + self.log("Analiza aplikacji (.app): odczyt Info.plist (CFBundleDisplayName, CFBundleShortVersionString) oraz wykrywanie wersji i trybu instalacji.") + self.log("Źródło pliku do odczytu wersji: \(url.path)") + DispatchQueue.global(qos: .userInitiated).async { + let result = self.readAppInfo(appUrl: url) + DispatchQueue.main.async { + withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) { + self.isAnalyzing = false + self.mountedDMGPath = nil + if let (name, rawVer, appURL) = result { + let friendlyVer = self.formatMarketingVersion(raw: rawVer, name: name) + var cleanName = name + cleanName = cleanName.replacingOccurrences(of: "Install ", with: "") + cleanName = cleanName.replacingOccurrences(of: "macOS ", with: "") + cleanName = cleanName.replacingOccurrences(of: "Mac OS X ", with: "") + cleanName = cleanName.replacingOccurrences(of: "OS X ", with: "") + let prefix = name.contains("macOS") ? "macOS" : (name.contains("OS X") ? "OS X" : "macOS") + + self.recognizedVersion = "\(prefix) \(cleanName) \(friendlyVer)" + self.updateRequiredUSBCapacity(rawVersion: rawVer, name: name) + self.sourceAppURL = appURL + self.updateDetectedSystemIcon(from: appURL) + + if !self.applyMacosCompatibilityForAppInstaller(name: name, rawVer: rawVer) { + return + } + } else { + self.recognizedVersion = String(localized: "Nie rozpoznano instalatora") + self.requiredUSBCapacityGB = nil + self.log("Analiza zakończona: nie rozpoznano instalatora.") + AppLogging.separator() + } + } + } + } + } + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift new file mode 100644 index 0000000..48a44fc --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicFileSelection.swift @@ -0,0 +1,113 @@ +import SwiftUI +import AppKit +import UniformTypeIdentifiers + +extension AnalysisLogic { + func handleDrop(providers: [NSItemProvider]) -> Bool { + self.log("Odebrano przeciągnięcie pliku (providers=\(providers.count)). Szukam URL...") + if let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) }) { + provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, _) in + if let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { + self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + self.processDroppedURL(url) + } + } + else if let url = item as? URL { + self.log("Przeciągnięto plik w formacie .\(url.pathExtension.lowercased())") + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + self.processDroppedURL(url) + } + } + } + return true + } + return false + } + + func processDroppedURL(_ url: URL) { + DispatchQueue.main.async { + let ext = url.pathExtension.lowercased() + self.log("Wybrano plik w formacie .\(ext). Resetuję stan i przygotowuję analizę.") + if ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" { + withAnimation { + self.selectedFilePath = url.path + self.selectedFileUrl = url + self.recognizedVersion = "" + self.isSystemDetected = false + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + } + self.log("Lokalizacja wybranego pliku: \(url.path)") + self.log("Źródło do rozpoznania wersji: \(url.path)") + } + } + } + + func applySelectedURLAndStartAnalysis(_ url: URL) { + let ext = url.pathExtension.lowercased() + guard ext == "dmg" || ext == "app" || ext == "iso" || ext == "cdr" else { + logError("Pominięto automatyczne podstawienie ścieżki. Nieobsługiwany format: .\(ext)") + return + } + + processDroppedURL(url) + DispatchQueue.main.async { [weak self] in + self?.startAnalysis() + } + } + + func selectDMGFile() { + self.log("Otwieram panel wyboru pliku…") + let p = NSOpenPanel() + p.allowedContentTypes = [.diskImage, .applicationBundle] + // Dodajemy obsługę .iso i .cdr, które nie mają jeszcze UTType w UniformTypeIdentifiers, więc rozszerzamy allowedFileTypes + p.allowedFileTypes = ["dmg", "iso", "cdr", "app"] + p.allowsMultipleSelection = false + p.begin { + if $0 == .OK, let url = p.url { + let ext = url.pathExtension.lowercased() + guard ext == "dmg" || ext == "iso" || ext == "cdr" || ext == "app" else { return } + withAnimation { + self.selectedFilePath = url.path + self.selectedFileUrl = url + self.recognizedVersion = "" + self.isSystemDetected = false + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.showUSBSection = false + self.showUnsupportedMessage = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + } + self.log("Wybrano plik w formacie .\(ext)") + self.log("Lokalizacja wybranego pliku: \(url.path)") + self.log("Źródło do rozpoznania wersji: \(url.path)") + } else { + self.log("Anulowano wybór pliku") + } + } + } +} diff --git a/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift b/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift new file mode 100644 index 0000000..fd739cc --- /dev/null +++ b/macUSB/Features/Analysis/Logic/AnalysisLogicUsbDrives.swift @@ -0,0 +1,99 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + private var requiredUSBCapacityBytes: Int? { + guard let requiredGB = requiredUSBCapacityGB else { return nil } + switch requiredGB { + case 16: + return 15_000_000_000 + case 32: + return 28_000_000_000 + default: + return requiredGB * 1_000_000_000 + } + } + + // MARK: - Helper to enumerate external hard drives (non-removable) + private func enumerateExternalUSBHardDrives() -> [USBDrive] { + let keys: [URLResourceKey] = [ + .volumeNameKey, + .volumeIsRemovableKey, + .volumeIsInternalKey, + .volumeTotalCapacityKey + ] + guard let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: .skipHiddenVolumes) else { return [] } + + let candidates: [USBDrive] = urls.compactMap { url -> USBDrive? in + guard let v = try? url.resourceValues(forKeys: Set(keys)) else { return nil } + // Only external (non-internal), non-network, non-removable volumes (HDD/SSD) + if (v.volumeIsInternal ?? true) { return nil } + // Filter out obvious network-mounted volumes by scheme (e.g., afp, smb, nfs) + let scheme = url.scheme?.lowercased() + if let scheme = scheme, ["afp", "smb", "nfs", "ftp", "webdav"].contains(scheme) { return nil } + if (v.volumeIsRemovable ?? false) { return nil } + guard let name = v.volumeName else { return nil } + let bsd = USBDriveLogic.getBSDName(from: url) + guard !bsd.isEmpty && bsd != "unknown" else { return nil } + let totalCapacity = Int64(v.volumeTotalCapacity ?? 0) + let size = ByteCountFormatter.string(fromByteCount: totalCapacity, countStyle: .file) + let whole = USBDriveLogic.wholeDiskName(from: bsd) + let speed = USBDriveLogic.detectUSBSpeed(forBSDName: whole) + let partitionScheme = USBDriveLogic.detectPartitionScheme(forBSDName: whole) + let fileSystemFormat = USBDriveLogic.detectFileSystemFormat(forVolumeURL: url) + return USBDrive( + name: name, + device: bsd, + size: size, + url: url, + usbSpeed: speed, + partitionScheme: partitionScheme, + fileSystemFormat: fileSystemFormat + ) + } + return candidates + } + + func refreshDrives() { + let currentSelectedURL = selectedDrive?.url + var foundDrives = USBDriveLogic.enumerateAvailableDrives() + let allowExternal = UserDefaults.standard.bool(forKey: "AllowExternalDrives") + if allowExternal { + let extra = enumerateExternalUSBHardDrives() + // Merge unique by URL + for d in extra { + if !foundDrives.contains(where: { $0.url == d.url }) { + foundDrives.append(d) + } + } + } + self.availableDrives = foundDrives + if let currentURL = currentSelectedURL { + if let stillConnectedDrive = foundDrives.first(where: { $0.url == currentURL }) { + self.selectedDrive = stillConnectedDrive + } else { + self.selectedDrive = nil + self.capacityCheckFinished = false + } + } else { + if self.selectedDrive != nil { + self.selectedDrive = nil + self.capacityCheckFinished = false + } + } + } + + func checkCapacity() { + guard let drive = selectedDrive, let minCapacity = requiredUSBCapacityBytes else { + isCapacitySufficient = false + capacityCheckFinished = false + return + } + if let values = try? drive.url.resourceValues(forKeys: [.volumeTotalCapacityKey]), let capacity = values.volumeTotalCapacity { + withAnimation { isCapacitySufficient = capacity >= minCapacity; capacityCheckFinished = true } + } else { + isCapacitySufficient = false + capacityCheckFinished = true + } + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift new file mode 100644 index 0000000..cc0c8af --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSCompatibility.swift @@ -0,0 +1,319 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func applyMacosCompatibilityForMountedInstaller(name: String, rawVer: String, userVisibleVersionFromMounted: String?) -> Bool { + // Use lowercase name for detection + let nameLower = name.lowercased() + + // Leopard/Tiger detection (PowerPC) using name, raw version, or mounted SystemVersion.plist + let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") || (userVisibleVersionFromMounted?.hasPrefix("10.5") ?? false) + let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") || (userVisibleVersionFromMounted?.hasPrefix("10.4") ?? false) + let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") || (userVisibleVersionFromMounted?.hasPrefix("10.3") ?? false) + let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") || (userVisibleVersionFromMounted?.hasPrefix("10.6") ?? false) + + // Disable kernel arch detection for Panther (10.3), Tiger (10.4), Leopard (10.5) and Snow Leopard (10.6). Always mark PPC flow for legacy, but do not set legacyArchInfo. + if (isLeopard || isTiger || isPanther || isSnowLeopard) { + self.isPPC = true // niezależnie od architektury, proces USB taki sam + self.legacyArchInfo = nil + } + + // Legacy versions exact recognition for mounted userVisibleVersion or fallback for legacy systems + if isLeopard { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Leopard \(userVisible)" + } else if rawVer.starts(with: "10.5") { + self.recognizedVersion = "Mac OS X Leopard \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Leopard" + } + } + if isTiger { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Tiger \(userVisible)" + } else if rawVer.starts(with: "10.4") { + self.recognizedVersion = "Mac OS X Tiger \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Tiger" + } + } + if isPanther { + self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Panther \(userVisible)" + } else if rawVer.starts(with: "10.3") { + self.recognizedVersion = "Mac OS X Panther \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Panther" + } + } + if isSnowLeopard { + if let userVisible = userVisibleVersionFromMounted { + self.recognizedVersion = "Mac OS X Snow Leopard \(userVisible)" + } else if rawVer.starts(with: "10.6") { + self.recognizedVersion = "Mac OS X Snow Leopard \(rawVer)" + } else { + self.recognizedVersion = "Mac OS X Snow Leopard" + } + } + + // If Panther is detected, mark as unsupported and block further processing + if isPanther { + self.isSystemDetected = false + self.showUSBSection = false + self.isPPC = false + self.legacyArchInfo = nil + // Show unsupported message immediately + withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { + self.showUnsupportedMessage = true + } + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + return false + } + + // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ + let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") + + // Catalina detection + let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") + + // Sierra detection (supported only for installer version 12.6.06) + let isSierra = (rawVer == "12.6.06") + let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") + let isUnsupportedSierraVersion = isSierraName && !isSierra + self.isUnsupportedSierra = isUnsupportedSierraVersion + if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } + + let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") + + // Modern (Big Sur+) + let isModern = + nameLower.contains("tahoe") || // Dodano Tahoe + nameLower.contains("sur") || + nameLower.contains("monterey") || + nameLower.contains("ventura") || + nameLower.contains("sonoma") || + nameLower.contains("sequoia") || + rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) + rawVer.starts(with: "11.") || + (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || + (rawVer.starts(with: "13.") && !nameLower.contains("high")) || + (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || + (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) + + // Old Supported (Mojave + High Sierra) + let isOldSupported = + nameLower.contains("mojave") || + nameLower.contains("high sierra") || + rawVer.starts(with: "10.14") || + rawVer.starts(with: "10.13") || + (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || + (rawVer.starts(with: "13.") && nameLower.contains("high")) + + // Legacy No Codesign (Yosemite + El Capitan) + let isLegacyDetected = + nameLower.contains("yosemite") || + nameLower.contains("el capitan") || + rawVer.starts(with: "10.10") || + rawVer.starts(with: "10.11") + + // Legacy Restore (Lion + Mountain Lion) + let isRestoreLegacy = + nameLower.contains("mountain lion") || + nameLower.contains("lion") || + rawVer.starts(with: "10.8") || + rawVer.starts(with: "10.7") + + // ZMIANA: Dodanie isCatalina do isSystemDetected + self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra || isMavericks + + // Catalina ma swój własny codesign, więc tu wyłączamy standardowy 'needsCodesign' + self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected + self.isLegacyDetected = isLegacyDetected + self.isRestoreLegacy = isRestoreLegacy + self.isCatalina = isCatalina + self.isSierra = isSierra + self.isMavericks = isMavericks + if isMavericks { + self.shouldShowMavericksDialog = true + } + if isSierra { + self.recognizedVersion = "macOS Sierra 10.12" + self.needsCodesign = false + } + // Dla Leoparda/Tigera już ustawione na true powyżej, pozostaw + self.isPPC = self.isPPC || false + + let trueFlags = [ + self.isCatalina ? "isCatalina" : nil, + self.isSierra ? "isSierra" : nil, + self.isLegacyDetected ? "isLegacyDetected" : nil, + self.isRestoreLegacy ? "isRestoreLegacy" : nil, + self.isPPC ? "isPPC" : nil, + self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, + self.isMavericks ? "isMavericks" : nil + ].compactMap { $0 }.joined(separator: ", ") + self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") + self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") + AppLogging.separator() + + if self.isSystemDetected { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } + } + + return true + } + + func applyMacosCompatibilityForAppInstaller(name: String, rawVer: String) -> Bool { + let nameLower = name.lowercased() + + // Leopard detection (PowerPC) + let isLeopard = nameLower.contains("leopard") || rawVer.starts(with: "10.5") + let isTiger = nameLower.contains("tiger") || rawVer.starts(with: "10.4") + let isPanther = nameLower.contains("panther") || rawVer.starts(with: "10.3") + let isSnowLeopard = nameLower.contains("snow leopard") || rawVer.starts(with: "10.6") + + self.legacyArchInfo = nil + + // Dla Snow Leoparda/Leoparda/Tigera zawsze traktujemy jako PPC flow + if isLeopard || isTiger || isSnowLeopard { + self.isPPC = true + } + + // Ustal dokładną wersję dla Panther/Tiger/Leopard/Snow Leopard (dla .app) + if isPanther || isTiger || isLeopard || isSnowLeopard { + let isExact = rawVer.starts(with: "10.3") || rawVer.starts(with: "10.4") || rawVer.starts(with: "10.5") || rawVer.starts(with: "10.6") + let exactSuffix = isExact ? " \(rawVer)" : "" + if isPanther { + self.logError("Wykryto niewspierany system: Mac OS X Panther (10.3). Przerywam analizę.") + self.recognizedVersion = "Mac OS X Panther\(exactSuffix)" + } else if isTiger { + self.recognizedVersion = "Mac OS X Tiger\(exactSuffix)" + } else if isLeopard { + self.recognizedVersion = "Mac OS X Leopard\(exactSuffix)" + } else if isSnowLeopard { + self.recognizedVersion = "Mac OS X Snow Leopard\(exactSuffix)" + } + } + + // If Panther is detected, mark as unsupported and block further processing + if isPanther { + self.isSystemDetected = false + self.showUSBSection = false + self.isPPC = false + self.legacyArchInfo = nil + // Show unsupported message immediately + withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { + self.showUnsupportedMessage = true + } + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + return false + } + + // Systemy niewspierane (Explicit) - USUNIĘTO CATALINĘ + let isExplicitlyUnsupported = nameLower.contains("sierra") && !nameLower.contains("high") + + // Catalina detection + let isCatalina = nameLower.contains("catalina") || rawVer.starts(with: "10.15") + + // Sierra detection (supported only for installer version 12.6.06) + let isSierra = (rawVer == "12.6.06") + let isSierraName = nameLower.contains("sierra") && !nameLower.contains("high") + let isUnsupportedSierraVersion = isSierraName && !isSierra + self.isUnsupportedSierra = isUnsupportedSierraVersion + if isUnsupportedSierraVersion { self.logError("Ta wersja systemu macOS Sierra nie jest wspierana (wymagana 12.6.06).") } + + let isMavericks = nameLower.contains("mavericks") || rawVer.starts(with: "10.9") + + // Modern (Big Sur+) + let isModern = + nameLower.contains("tahoe") || // Dodano Tahoe + nameLower.contains("sur") || + nameLower.contains("monterey") || + nameLower.contains("ventura") || + nameLower.contains("sonoma") || + nameLower.contains("sequoia") || + rawVer.starts(with: "21.") || // Dodano Tahoe (v26/21.x) + rawVer.starts(with: "11.") || + (rawVer.starts(with: "12.") && !isExplicitlyUnsupported) || + (rawVer.starts(with: "13.") && !nameLower.contains("high")) || + (rawVer.starts(with: "14.") && !nameLower.contains("mojave")) || + (rawVer.starts(with: "15.") && !isExplicitlyUnsupported) + + // Old Supported (Mojave + High Sierra) + let isOldSupported = + nameLower.contains("mojave") || + nameLower.contains("high sierra") || + rawVer.starts(with: "10.14") || + rawVer.starts(with: "10.13") || + (rawVer.starts(with: "14.") && nameLower.contains("mojave")) || + (rawVer.starts(with: "13.") && nameLower.contains("high")) + + // Legacy No Codesign (Yosemite + El Capitan) + let isLegacyDetected = + nameLower.contains("yosemite") || + nameLower.contains("el capitan") || + rawVer.starts(with: "10.10") || + rawVer.starts(with: "10.11") + + // Legacy Restore (Lion + Mountain Lion) + let isRestoreLegacy = + nameLower.contains("mountain lion") || + nameLower.contains("lion") || + rawVer.starts(with: "10.8") || + rawVer.starts(with: "10.7") + + self.isSystemDetected = isModern || isOldSupported || isLegacyDetected || isRestoreLegacy || isCatalina || isSierra + + self.needsCodesign = isOldSupported && !isModern && !isLegacyDetected + self.isLegacyDetected = isLegacyDetected + self.isRestoreLegacy = isRestoreLegacy + self.isCatalina = isCatalina + self.isSierra = isSierra + self.isMavericks = isMavericks + if isMavericks { + self.shouldShowMavericksDialog = true + } + if isSierra { + self.recognizedVersion = "macOS Sierra 10.12" + self.needsCodesign = false + } + // isPPC zostało ustawione wcześniej dla Snow Leoparda/Leoparda/Tigera; dla pozostałych pozostaje false + self.isPPC = self.isPPC || false + + let trueFlags = [ + self.isCatalina ? "isCatalina" : nil, + self.isSierra ? "isSierra" : nil, + self.isLegacyDetected ? "isLegacyDetected" : nil, + self.isRestoreLegacy ? "isRestoreLegacy" : nil, + self.isPPC ? "isPPC" : nil, + self.isUnsupportedSierra ? "isUnsupportedSierra" : nil, + self.isMavericks ? "isMavericks" : nil + ].compactMap { $0 }.joined(separator: ", ") + self.log("Analiza zakończona. Rozpoznano: \(self.recognizedVersion)") + self.log("Przypisane flagi: \(trueFlags.isEmpty ? "brak" : trueFlags)") + AppLogging.separator() + + if self.isSystemDetected { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUSBSection = true } } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { withAnimation(.spring(response: 0.7, dampingFraction: 0.8)) { self.showUnsupportedMessage = true } } + } + + return true + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift new file mode 100644 index 0000000..2f9583f --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSImageMounting.swift @@ -0,0 +1,223 @@ +import Foundation + +extension AnalysisLogic { + private func readLegacyInstallMacOSXInfo(from mountURL: URL) -> (String, String, URL)? { + let legacyInstallers = [ + "Install Mac OS X", + "Install Mac OS X.app" + ] + + var foundLegacyPath = false + for installerName in legacyInstallers { + let installerURL = mountURL.appendingPathComponent(installerName, isDirectory: true) + let plistURL = installerURL.appendingPathComponent("Contents/Info.plist") + guard FileManager.default.fileExists(atPath: plistURL.path) else { + continue + } + + foundLegacyPath = true + self.log("Znaleziono legacy installer path: \(installerURL.path)") + self.log("Odczyt Info.plist (legacy): \(plistURL.path)") + + guard let data = try? Data(contentsOf: plistURL), + let dict = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { + self.logError("Nie udało się odczytać Info.plist (legacy): \(plistURL.path)") + continue + } + + let name = (dict["CFBundleDisplayName"] as? String) ?? installerURL.lastPathComponent + let version = (dict["CFBundleShortVersionString"] as? String) ?? "?" + self.log("Odczytano Info.plist (legacy): name=\(name), version=\(version)") + return (name, version, installerURL) + } + + if !foundLegacyPath { + self.log("Nie znaleziono legacy path instalatora 'Install Mac OS X' w: \(mountURL.path)") + } + + return nil + } + + private func normalizedImagePath(_ path: String) -> String { + URL(fileURLWithPath: path).resolvingSymlinksInPath().standardizedFileURL.path + } + + private func mountedPathForAlreadyAttachedImage(sourceURL: URL) -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["info", "-plist"] + let outputPipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = outputPipe + task.standardError = errorPipe + + do { + try task.run() + } catch { + self.logError("Nie udało się uruchomić hdiutil info: \(error.localizedDescription)") + return nil + } + task.waitUntilExit() + + let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + if task.terminationStatus != 0 { + let stderrText = String(data: errorData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if stderrText.isEmpty { + self.logError("hdiutil info zakończył się błędem (kod \(task.terminationStatus)).") + } else { + self.logError("hdiutil info zakończył się błędem: \(stderrText)") + } + return nil + } + + guard let plist = try? PropertyListSerialization.propertyList(from: outputData, options: [], format: nil) as? [String: Any], + let images = plist["images"] as? [[String: Any]] else { + return nil + } + + let sourcePath = normalizedImagePath(sourceURL.path) + for image in images { + guard let imagePath = image["image-path"] as? String else { continue } + guard normalizedImagePath(imagePath) == sourcePath else { continue } + guard let entities = image["system-entities"] as? [[String: Any]], + let mountPoint = entities.compactMap({ $0["mount-point"] as? String }).first else { + continue + } + return mountPoint + } + + return nil + } + + func mountAndReadInfo(dmgUrl: URL, detectPreMountedSource: Bool = false) -> (mountedReadInfo: (String, String, URL, String)?, sourceAlreadyMountedPath: String?)? { + self.log("Montowanie obrazu (DMG/ISO/CDR)") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Wybrany obraz .\(dmgUrl.pathExtension.lowercased()) jest już zamontowany w systemie: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] + let pipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = pipe + task.standardError = errorPipe + + do { + try task.run() + } catch { + self.logError("Nie udało się uruchomić hdiutil attach: \(error.localizedDescription)") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po błędzie uruchomienia attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + let stderrText = String(data: errorData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if task.terminationStatus != 0 { + if stderrText.isEmpty { + self.logError("hdiutil attach zakończył się błędem (kod \(task.terminationStatus)).") + } else { + self.logError("hdiutil attach zakończył się błędem: \(stderrText)") + } + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po błędzie attach wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + + guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { + self.logError("Nie udało się odczytać informacji z obrazu") + if detectPreMountedSource, + let mountPoint = mountedPathForAlreadyAttachedImage(sourceURL: dmgUrl) { + self.log("Po nieudanym odczycie plist wykryto już zamontowany obraz źródłowy: \(mountPoint)") + return (mountedReadInfo: nil, sourceAlreadyMountedPath: mountPoint) + } + return nil + } + self.log("Przetwarzanie wyników hdiutil attach (\(entities.count) encji)") + for e in entities { + if let mp = e["mount-point"] as? String { + let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) + var mountId = "unknown" + if let dev = devEntry { + let bsd = URL(fileURLWithPath: dev).lastPathComponent // e.g. disk9s1 + if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { + mountId = String(bsd[.. String? { + self.log("Montowanie obrazu (PPC)") + let task = Process(); task.executableURL = URL(fileURLWithPath: "/usr/bin/hdiutil") + task.arguments = ["attach", dmgUrl.path, "-plist", "-nobrowse", "-readonly"] + let pipe = Pipe(); task.standardOutput = pipe; try? task.run(); task.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], let entities = plist["system-entities"] as? [[String: Any]] else { + self.logError("Nie udało się zamontować obrazu (PPC)") + return nil + } + for e in entities { + if let mp = e["mount-point"] as? String { + let devEntry = (e["dev-entry"] as? String) ?? (e["devname"] as? String) + var mountId = "unknown" + if let dev = devEntry { + let bsd = URL(fileURLWithPath: dev).lastPathComponent + if let range = bsd.range(of: #"s\d+$"#, options: .regularExpression) { + mountId = String(bsd[.. [URL] { + var result: [URL] = [] + let fm = FileManager.default + + func appendUnique(_ candidate: URL) { + let normalized = candidate.standardizedFileURL + guard !result.contains(where: { $0.standardizedFileURL == normalized }) else { return } + result.append(candidate) + } + + appendUnique(url) + + if url.pathExtension.lowercased() != "app" { + // Część starych obrazów ma instalator pod klasyczną nazwą "Install Mac OS X". + appendUnique(url.appendingPathComponent("Install Mac OS X.app", isDirectory: true)) + appendUnique(url.appendingPathComponent("Install OS X.app", isDirectory: true)) + appendUnique(url.appendingPathComponent("Install macOS.app", isDirectory: true)) + + if let children = try? fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) { + for child in children where child.pathExtension.lowercased() == "app" { + appendUnique(child) + } + } + } + + return result.filter { fm.fileExists(atPath: $0.path) } + } + + private func findIconURL(in resourcesURL: URL, preferredFileNames: [String]) -> URL? { + let fm = FileManager.default + guard fm.fileExists(atPath: resourcesURL.path), + let files = try? fm.contentsOfDirectory(at: resourcesURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { + return nil + } + + var byLowercasedName: [String: URL] = [:] + for file in files { + byLowercasedName[file.lastPathComponent.lowercased()] = file + } + + for fileName in preferredFileNames { + if let match = byLowercasedName[fileName.lowercased()] { + return match + } + } + + return nil + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift new file mode 100644 index 0000000..8fde3f6 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSInstallerMetadata.swift @@ -0,0 +1,53 @@ +import Foundation + +extension AnalysisLogic { + func updateRequiredUSBCapacity(rawVersion: String, name: String) { + guard let majorVersion = marketingMajorVersion(raw: rawVersion, name: name) else { + requiredUSBCapacityGB = nil + return + } + requiredUSBCapacityGB = (majorVersion >= 15) ? 32 : 16 + } + + func marketingMajorVersion(raw: String, name: String) -> Int? { + let marketingVersion = formatMarketingVersion(raw: raw, name: name) + guard let majorToken = marketingVersion.split(separator: ".").first else { return nil } + return Int(majorToken) + } + + func formatMarketingVersion(raw: String, name: String) -> String { + let n = name.lowercased() + if n.contains("tahoe") { return "26" } // Dodano Tahoe + if n.contains("sequoia") { return "15" } + if n.contains("sonoma") { return "14" } + if n.contains("ventura") { return "13" } + if n.contains("monterey") { return "12" } + if n.contains("big sur") { return "11" } + if n.contains("catalina") { return "10.15" } + if n.contains("mojave") { return "10.14" } + if n.contains("high sierra") { return "10.13" } + if n.contains("sierra") && !n.contains("high") { return "10.12" } + if n.contains("el capitan") { return "10.11" } + if n.contains("yosemite") { return "10.10" } + if n.contains("mavericks") { return "10.9" } + if n.contains("mountain lion") { return "10.8" } + if n.contains("lion") { return "10.7" } + if n.contains("snow leopard") { return "10.6" } + if n.contains("panther") { return "10.3" } + return raw + } + + func readAppInfo(appUrl: URL) -> (String, String, URL)? { + let plistUrl = appUrl.appendingPathComponent("Contents/Info.plist") + self.log("Odczyt Info.plist: \(plistUrl.path)") + if let d = try? Data(contentsOf: plistUrl), + let dict = try? PropertyListSerialization.propertyList(from: d, format: nil) as? [String: Any] { + let name = (dict["CFBundleDisplayName"] as? String) ?? appUrl.lastPathComponent + let ver = (dict["CFBundleShortVersionString"] as? String) ?? "?" + self.log("Odczytano Info.plist: name=\(name), version=\(ver)") + return (name, ver, appUrl) + } + self.logError("Nie udało się odczytać Info.plist") + return nil + } +} diff --git a/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift new file mode 100644 index 0000000..f375ff4 --- /dev/null +++ b/macUSB/Features/Analysis/Logic/macOS/AnalysisLogicMacOSLifecycle.swift @@ -0,0 +1,104 @@ +import SwiftUI +import Foundation + +extension AnalysisLogic { + func forceTigerMultiDVDSelection() { + self.log("Ręcznie wybrano tryb Tiger Multi DVD") + let fileURL = self.selectedFileUrl + DispatchQueue.global(qos: .userInitiated).async { + var mountPoint: String? = self.mountedDMGPath + var effectiveSourceAppURL: URL? = nil + if let url = fileURL { + let ext = url.pathExtension.lowercased() + if ext == "dmg" || ext == "iso" || ext == "cdr" { + if mountPoint == nil { + mountPoint = self.mountImageForPPC(dmgUrl: url) + } + if let mp = mountPoint { + effectiveSourceAppURL = URL(fileURLWithPath: mp).appendingPathComponent("Install") + } + } else if ext == "app" { + effectiveSourceAppURL = url + } + } + DispatchQueue.main.async { + withAnimation { + self.isAnalyzing = false + self.userSkippedAnalysis = true + self.recognizedVersion = "Mac OS X Tiger 10.4" + self.sourceAppURL = effectiveSourceAppURL + self.updateDetectedSystemIcon(from: effectiveSourceAppURL) + self.mountedDMGPath = mountPoint + self.isSystemDetected = true + self.showUnsupportedMessage = false + self.showUSBSection = true + self.needsCodesign = false + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = true + self.legacyArchInfo = nil + self.selectedDrive = nil + self.capacityCheckFinished = false + self.requiredUSBCapacityGB = 16 + } + let flags = [self.isPPC ? "isPPC" : nil].compactMap { $0 }.joined(separator: ", ") + self.log("Ustawiono Tiger Multi DVD: recognizedVersion=\(self.recognizedVersion). Flagi: \(flags.isEmpty ? "brak" : flags)") + } + } + } + + func resetAll() { + let oldMount = self.mountedDMGPath + if let path = oldMount { + let task = Process() + task.launchPath = "/usr/bin/hdiutil" + task.arguments = ["detach", path, "-force"] + try? task.run() + task.waitUntilExit() + } + DispatchQueue.main.async { + withAnimation { + self.selectedFilePath = "" + self.selectedFileUrl = nil + self.recognizedVersion = "" + self.sourceAppURL = nil + self.detectedSystemIcon = nil + self.mountedDMGPath = nil + + self.isAnalyzing = false + self.isSystemDetected = false + self.showUSBSection = false + self.showUnsupportedMessage = false + + self.needsCodesign = true + self.isLegacyDetected = false + self.isRestoreLegacy = false + self.isCatalina = false + self.isSierra = false + self.isMavericks = false + self.isUnsupportedSierra = false + self.isPPC = false + self.legacyArchInfo = nil + self.shouldShowAlreadyMountedSourceAlert = false + self.userSkippedAnalysis = false + self.shouldShowMavericksDialog = false + self.requiredUSBCapacityGB = nil + + self.availableDrives = [] + self.selectedDrive = nil + + self.isCapacitySufficient = false + self.capacityCheckFinished = false + } + } + } + + // Call this from the UI when the user presses the "Przejdź dalej" button + func recordProceedPressed() { + self.log("Użytkownik nacisnął przycisk 'Przejdź dalej'. Wybrany nośnik: \(self.selectedDrive?.url.path ?? "brak"), źródło: \(self.sourceAppURL?.path ?? "brak"), rozpoznano: \(self.recognizedVersion)") + } +} diff --git a/macUSB/Resources/Localizable.xcstrings b/macUSB/Resources/Localizable.xcstrings index 133cc49..c212cb0 100644 --- a/macUSB/Resources/Localizable.xcstrings +++ b/macUSB/Resources/Localizable.xcstrings @@ -10868,6 +10868,7 @@ } }, "Lokalizacja aplikacji: poza /Applications" : { + "extractionState" : "stale", "localizations" : { "de" : { "stringUnit" : { From c9f5e2ad68e80f7df0a4d6b9e099bc000dd42f20 Mon Sep 17 00:00:00 2001 From: Kruszoneq <83231322+Kruszoneq@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:46:59 +0200 Subject: [PATCH 2/4] Fix debug CI compatibility and rename development workflow Remove the direct Glass type reference from LiquidGlassCompatibility by selecting glass effects inline per tone, which keeps UI behavior while restoring compatibility on GitHub-hosted runners. Also rename the development workflow/check labels to the requested macUSB - development / Debug Build format for clearer PR status presentation. --- .github/workflows/development_build_debug.yml | 4 +- .../Shared/UI/LiquidGlassCompatibility.swift | 39 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.github/workflows/development_build_debug.yml b/.github/workflows/development_build_debug.yml index e6e846d..a132ed6 100644 --- a/.github/workflows/development_build_debug.yml +++ b/.github/workflows/development_build_debug.yml @@ -1,4 +1,4 @@ -name: Debug Build (development) +name: macUSB - development on: push: @@ -14,7 +14,7 @@ concurrency: jobs: debug-build: - name: macUSB - Debug Build + name: Debug Build runs-on: macos-latest timeout-minutes: 30 diff --git a/macUSB/Shared/UI/LiquidGlassCompatibility.swift b/macUSB/Shared/UI/LiquidGlassCompatibility.swift index 64c2a51..e218560 100644 --- a/macUSB/Shared/UI/LiquidGlassCompatibility.swift +++ b/macUSB/Shared/UI/LiquidGlassCompatibility.swift @@ -78,23 +78,6 @@ private extension MacUSBSurfaceTone { } } - @available(macOS 26.0, *) - var glass: Glass { - switch self { - case .neutral, .subtle: - return .regular.interactive(false) - case .info: - return .regular.tint(.blue.opacity(0.14)).interactive(false) - case .success: - return .regular.tint(.green.opacity(0.14)).interactive(false) - case .warning: - return .regular.tint(.orange.opacity(0.16)).interactive(false) - case .error: - return .regular.tint(.red.opacity(0.16)).interactive(false) - case .active: - return .regular.tint(.accentColor.opacity(0.18)).interactive(false) - } - } } private struct MacUSBTopRoundedRectangle: Shape { @@ -131,8 +114,26 @@ private struct MacUSBPanelSurfaceModifier: ViewModifier { let shape = RoundedRectangle(cornerRadius: radius, style: .continuous) if #available(macOS 26.0, *), !tone.usesStableSurfaceOnLiquidGlass { - content - .glassEffect(tone.glass, in: shape) + switch tone { + case .neutral, .subtle: + content + .glassEffect(.regular.interactive(false), in: shape) + case .info: + content + .glassEffect(.regular.tint(.blue.opacity(0.14)).interactive(false), in: shape) + case .success: + content + .glassEffect(.regular.tint(.green.opacity(0.14)).interactive(false), in: shape) + case .warning: + content + .glassEffect(.regular.tint(.orange.opacity(0.16)).interactive(false), in: shape) + case .error: + content + .glassEffect(.regular.tint(.red.opacity(0.16)).interactive(false), in: shape) + case .active: + content + .glassEffect(.regular.tint(.accentColor.opacity(0.18)).interactive(false), in: shape) + } } else { content .background( From 33d0b4dc88304bed04bf51c7853cba6a757d9557 Mon Sep 17 00:00:00 2001 From: Kruszoneq <83231322+Kruszoneq@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:49:43 +0200 Subject: [PATCH 3/4] Fix CI by removing unavailable glass style APIs Replace glassEffect and glass button style usages in LiquidGlassCompatibility with the existing cross-version fallback styles so the project builds on GitHub-hosted Xcode 16.4 runners. --- .../Shared/UI/LiquidGlassCompatibility.swift | 88 +++++-------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/macUSB/Shared/UI/LiquidGlassCompatibility.swift b/macUSB/Shared/UI/LiquidGlassCompatibility.swift index e218560..b331bfc 100644 --- a/macUSB/Shared/UI/LiquidGlassCompatibility.swift +++ b/macUSB/Shared/UI/LiquidGlassCompatibility.swift @@ -113,38 +113,15 @@ private struct MacUSBPanelSurfaceModifier: ViewModifier { let radius = cornerRadius ?? MacUSBDesignTokens.panelCornerRadius(for: currentVisualMode()) let shape = RoundedRectangle(cornerRadius: radius, style: .continuous) - if #available(macOS 26.0, *), !tone.usesStableSurfaceOnLiquidGlass { - switch tone { - case .neutral, .subtle: - content - .glassEffect(.regular.interactive(false), in: shape) - case .info: - content - .glassEffect(.regular.tint(.blue.opacity(0.14)).interactive(false), in: shape) - case .success: - content - .glassEffect(.regular.tint(.green.opacity(0.14)).interactive(false), in: shape) - case .warning: - content - .glassEffect(.regular.tint(.orange.opacity(0.16)).interactive(false), in: shape) - case .error: - content - .glassEffect(.regular.tint(.red.opacity(0.16)).interactive(false), in: shape) - case .active: - content - .glassEffect(.regular.tint(.accentColor.opacity(0.18)).interactive(false), in: shape) - } - } else { - content - .background( - shape - .fill(tone.fallbackFillColor(for: colorScheme)) - ) - .overlay( - shape - .stroke(tone.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) - ) - } + content + .background( + shape + .fill(tone.fallbackFillColor(for: colorScheme)) + ) + .overlay( + shape + .stroke(tone.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) + ) } } @@ -157,16 +134,11 @@ private struct MacUSBDockedBarSurfaceModifier: ViewModifier { cornerRadius: MacUSBDesignTokens.dockedBarTopCornerRadius(for: mode) ) - if #available(macOS 26.0, *) { - content - .glassEffect(.regular.interactive(false), in: shape) - } else { - content - .background(shape.fill(MacUSBSurfaceTone.subtle.fallbackFillColor(for: colorScheme))) - .overlay( - shape.stroke(MacUSBSurfaceTone.subtle.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) - ) - } + content + .background(shape.fill(MacUSBSurfaceTone.subtle.fallbackFillColor(for: colorScheme))) + .overlay( + shape.stroke(MacUSBSurfaceTone.subtle.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) + ) } } @@ -174,18 +146,11 @@ private struct MacUSBPrimaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { - if #available(macOS 26.0, *) { - content - .buttonStyle(.glassProminent) - .controlSize(.large) - .opacity(isEnabled ? 1.0 : 0.55) - } else { - content - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(.accentColor) - .opacity(isEnabled ? 1.0 : 0.55) - } + content + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.accentColor) + .opacity(isEnabled ? 1.0 : 0.55) } } @@ -193,17 +158,10 @@ private struct MacUSBSecondaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { - if #available(macOS 26.0, *) { - content - .buttonStyle(.glass) - .controlSize(.large) - .opacity(isEnabled ? 1.0 : 0.55) - } else { - content - .buttonStyle(.bordered) - .controlSize(.large) - .opacity(isEnabled ? 1.0 : 0.55) - } + content + .buttonStyle(.bordered) + .controlSize(.large) + .opacity(isEnabled ? 1.0 : 0.55) } } From 4995fbe24e596b81d883ed45482e373e408e893d Mon Sep 17 00:00:00 2001 From: Kruszoneq <83231322+Kruszoneq@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:52:58 +0200 Subject: [PATCH 4/4] Revert development debug workflow and related CI adjustments Roll back the recent CI-specific changes and remove the development debug workflow setup, returning the branch to the previous behavior and documentation alignment. --- .github/workflows/development_build_debug.yml | 33 ------- docs/AGENTS.md | 12 --- .../Shared/UI/LiquidGlassCompatibility.swift | 88 ++++++++++++++----- 3 files changed, 65 insertions(+), 68 deletions(-) delete mode 100644 .github/workflows/development_build_debug.yml diff --git a/.github/workflows/development_build_debug.yml b/.github/workflows/development_build_debug.yml deleted file mode 100644 index a132ed6..0000000 --- a/.github/workflows/development_build_debug.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: macUSB - development - -on: - push: - branches: - - development - pull_request: - branches: - - development - -concurrency: - group: debug-build-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - debug-build: - name: Debug Build - runs-on: macos-latest - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build macUSB (Debug) - run: | - xcodebuild \ - -project macUSB.xcodeproj \ - -scheme macUSB \ - -configuration Debug \ - -destination 'platform=macOS' \ - CODE_SIGNING_ALLOWED=NO \ - build diff --git a/docs/AGENTS.md b/docs/AGENTS.md index d9818e5..b8ce38f 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -174,18 +174,6 @@ xcodebuild -project macUSB.xcodeproj -scheme macUSB -configuration Debug -destin xcodebuild -project macUSB.xcodeproj -scheme macUSB -configuration Debug -destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build ``` -## CI required check for development - -For `development`, a GitHub Actions Debug build check is required for both: -- push to `development`, -- pull requests targeting `development`. - -Required workflow file: -- `.github/workflows/development_build_debug.yml` - -Required status check (branch protection): -- `Build Debug app` - ## Change classification Use these rules to decide required documentation updates: diff --git a/macUSB/Shared/UI/LiquidGlassCompatibility.swift b/macUSB/Shared/UI/LiquidGlassCompatibility.swift index b331bfc..e218560 100644 --- a/macUSB/Shared/UI/LiquidGlassCompatibility.swift +++ b/macUSB/Shared/UI/LiquidGlassCompatibility.swift @@ -113,15 +113,38 @@ private struct MacUSBPanelSurfaceModifier: ViewModifier { let radius = cornerRadius ?? MacUSBDesignTokens.panelCornerRadius(for: currentVisualMode()) let shape = RoundedRectangle(cornerRadius: radius, style: .continuous) - content - .background( - shape - .fill(tone.fallbackFillColor(for: colorScheme)) - ) - .overlay( - shape - .stroke(tone.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) - ) + if #available(macOS 26.0, *), !tone.usesStableSurfaceOnLiquidGlass { + switch tone { + case .neutral, .subtle: + content + .glassEffect(.regular.interactive(false), in: shape) + case .info: + content + .glassEffect(.regular.tint(.blue.opacity(0.14)).interactive(false), in: shape) + case .success: + content + .glassEffect(.regular.tint(.green.opacity(0.14)).interactive(false), in: shape) + case .warning: + content + .glassEffect(.regular.tint(.orange.opacity(0.16)).interactive(false), in: shape) + case .error: + content + .glassEffect(.regular.tint(.red.opacity(0.16)).interactive(false), in: shape) + case .active: + content + .glassEffect(.regular.tint(.accentColor.opacity(0.18)).interactive(false), in: shape) + } + } else { + content + .background( + shape + .fill(tone.fallbackFillColor(for: colorScheme)) + ) + .overlay( + shape + .stroke(tone.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) + ) + } } } @@ -134,11 +157,16 @@ private struct MacUSBDockedBarSurfaceModifier: ViewModifier { cornerRadius: MacUSBDesignTokens.dockedBarTopCornerRadius(for: mode) ) - content - .background(shape.fill(MacUSBSurfaceTone.subtle.fallbackFillColor(for: colorScheme))) - .overlay( - shape.stroke(MacUSBSurfaceTone.subtle.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) - ) + if #available(macOS 26.0, *) { + content + .glassEffect(.regular.interactive(false), in: shape) + } else { + content + .background(shape.fill(MacUSBSurfaceTone.subtle.fallbackFillColor(for: colorScheme))) + .overlay( + shape.stroke(MacUSBSurfaceTone.subtle.fallbackStrokeColor(for: colorScheme), lineWidth: 0.5) + ) + } } } @@ -146,11 +174,18 @@ private struct MacUSBPrimaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { - content - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(.accentColor) - .opacity(isEnabled ? 1.0 : 0.55) + if #available(macOS 26.0, *) { + content + .buttonStyle(.glassProminent) + .controlSize(.large) + .opacity(isEnabled ? 1.0 : 0.55) + } else { + content + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.accentColor) + .opacity(isEnabled ? 1.0 : 0.55) + } } } @@ -158,10 +193,17 @@ private struct MacUSBSecondaryButtonStyleModifier: ViewModifier { let isEnabled: Bool func body(content: Content) -> some View { - content - .buttonStyle(.bordered) - .controlSize(.large) - .opacity(isEnabled ? 1.0 : 0.55) + if #available(macOS 26.0, *) { + content + .buttonStyle(.glass) + .controlSize(.large) + .opacity(isEnabled ? 1.0 : 0.55) + } else { + content + .buttonStyle(.bordered) + .controlSize(.large) + .opacity(isEnabled ? 1.0 : 0.55) + } } }