Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Applite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1400;
LastUpgradeCheck = 1630;
LastUpgradeCheck = 1631;
TargetAttributes = {
414074F028DF53E80073EB22 = {
CreatedOnToolsVersion = 14.0;
Expand Down Expand Up @@ -1025,6 +1025,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
Expand Down Expand Up @@ -1090,6 +1091,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
Expand Down Expand Up @@ -1151,12 +1153,13 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Applite/AppliteDebug.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 19;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Applite/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -1173,6 +1176,7 @@
MARKETING_VERSION = 1.3.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.aerolite.Applite;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
};
Expand Down
2 changes: 1 addition & 1 deletion Applite.xcodeproj/xcshareddata/xcschemes/Applite.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1630"
LastUpgradeVersion = "1631"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
294 changes: 294 additions & 0 deletions Applite/Model/Package Manager/BrewPackageManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
//
// BrewPackageManager.swift
// Applite
//
// Created by Subham mahesh
// licensed under the MIT
//

import Foundation
import OSLog

/// Package manager implementation for Homebrew
@MainActor
final class BrewPackageManager: PackageManagerProtocol {
typealias Package = GenericPackage

let name = "Homebrew"

private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: BrewPackageManager.self)
)

var isAvailable: Bool {
get async {
do {
_ = try await Shell.runBrewCommand(["--version"])
return true
} catch {
return false
}
}
}

// MARK: - Package Operations

func install(_ package: Package) async throws {
Self.logger.info("Installing package: \(package.id)")

let arguments = ["install", package.id]

do {
_ = try await Shell.runBrewCommand(arguments)
Self.logger.info("Successfully installed package: \(package.id)")
} catch {
Self.logger.error("Failed to install package \(package.id): \(error.localizedDescription)")
throw PackageManagerError.installationFailed(package.id, error.localizedDescription)
}
}

func uninstall(_ package: Package) async throws {
Self.logger.info("Uninstalling package: \(package.id)")

let arguments = ["uninstall", package.id]

do {
_ = try await Shell.runBrewCommand(arguments)
Self.logger.info("Successfully uninstalled package: \(package.id)")
} catch {
Self.logger.error("Failed to uninstall package \(package.id): \(error.localizedDescription)")
throw PackageManagerError.uninstallationFailed(package.id, error.localizedDescription)
}
}

func update(_ package: Package) async throws {
Self.logger.info("Updating package: \(package.id)")

let arguments = ["upgrade", package.id]

do {
_ = try await Shell.runBrewCommand(arguments)
Self.logger.info("Successfully updated package: \(package.id)")
} catch {
Self.logger.error("Failed to update package \(package.id): \(error.localizedDescription)")
throw PackageManagerError.updateFailed(package.id, error.localizedDescription)
}
}

func updateAll() async throws {
Self.logger.info("Updating all Homebrew packages")

do {
// First update homebrew itself
_ = try await Shell.runBrewCommand(["update"])

// Then upgrade all packages
_ = try await Shell.runBrewCommand(["upgrade"])

Self.logger.info("Successfully updated all Homebrew packages")
} catch {
Self.logger.error("Failed to update all packages: \(error.localizedDescription)")
throw PackageManagerError.commandExecutionFailed(error.localizedDescription)
}
}

// MARK: - Package Information

func getInstalledPackages() async throws -> [Package] {
Self.logger.info("Fetching installed Homebrew packages")

do {
let output = try await Shell.runBrewCommand(["list", "--formula", "--versions"])
return parseInstalledPackages(output)
} catch {
Self.logger.error("Failed to get installed packages: \(error.localizedDescription)")
throw PackageManagerError.commandExecutionFailed(error.localizedDescription)
}
}

func getOutdatedPackages() async throws -> [Package] {
Self.logger.info("Fetching outdated Homebrew packages")

do {
let output = try await Shell.runBrewCommand(["outdated", "--formula"])
return parseOutdatedPackages(output)
} catch {
Self.logger.error("Failed to get outdated packages: \(error.localizedDescription)")
throw PackageManagerError.commandExecutionFailed(error.localizedDescription)
}
}

func searchPackages(_ query: String) async throws -> [Package] {
Self.logger.info("Searching for packages with query: \(query)")

guard !query.isEmpty else {
return []
}

do {
let output = try await Shell.runBrewCommand(["search", "--desc", query])
return parseSearchResultsWithDescriptions(output, query: query)
} catch {
Self.logger.error("Failed to search packages: \(error.localizedDescription)")
throw PackageManagerError.commandExecutionFailed(error.localizedDescription)
}
}

func getPackageInfo(_ packageId: String) async throws -> Package {
Self.logger.info("Getting info for package: \(packageId)")

do {
let output = try await Shell.runBrewCommand(["info", "--json", packageId])
return try parsePackageInfo(output, packageId: packageId)
} catch {
Self.logger.error("Failed to get package info for \(packageId): \(error.localizedDescription)")
throw PackageManagerError.packageNotFound(packageId)
}
}

// MARK: - Parsing Methods

private func parseInstalledPackages(_ output: String) -> [Package] {
let lines = output.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }

return lines.compactMap { line in
let components = line.components(separatedBy: " ")
guard let packageName = components.first else { return nil }

let version = components.count > 1 ? components[1] : nil

return GenericPackage(
id: packageName,
name: packageName,
version: version,
manager: .homebrew,
isInstalled: true
)
}
}

private func parseOutdatedPackages(_ output: String) -> [Package] {
let lines = output.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }

return lines.compactMap { line in
// Format: package (current_version) < latest_version
let components = line.components(separatedBy: " ")
guard let packageName = components.first else { return nil }

// Extract versions if present
var currentVersion: String?
var latestVersion: String?

if let currentVersionMatch = line.range(of: #"\([^)]+\)"#, options: .regularExpression) {
currentVersion = String(line[currentVersionMatch])
.trimmingCharacters(in: CharacterSet(charactersIn: "()"))
}

if let latestVersionIndex = components.lastIndex(of: "<"),
latestVersionIndex + 1 < components.count {
latestVersion = components[latestVersionIndex + 1]
}

return GenericPackage(
id: packageName,
name: packageName,
version: currentVersion,
manager: .homebrew,
isInstalled: true,
isOutdated: true,
latestVersion: latestVersion
)
}
}

private func parseSearchResults(_ output: String, query: String) -> [Package] {
let lines = output.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
.filter { !$0.hasPrefix("==>") } // Remove section headers

return lines.compactMap { line in
let packageName = line.trimmingCharacters(in: .whitespaces)
guard !packageName.isEmpty else { return nil }

return GenericPackage(
id: packageName,
name: packageName,
manager: .homebrew,
isInstalled: false
)
}
}

private func parseSearchResultsWithDescriptions(_ output: String, query: String) -> [Package] {
let lines = output.components(separatedBy: .newlines)
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
.filter { !$0.hasPrefix("==>") } // Remove section headers
.filter { !$0.contains("Warning:") } // Remove warnings
.filter { $0.contains(":") } // Only lines with descriptions

return lines.compactMap { line in
let trimmedLine = line.trimmingCharacters(in: .whitespaces)
guard !trimmedLine.isEmpty else { return nil }

// Format: "packagename: description"
let components = trimmedLine.components(separatedBy: ": ")
guard components.count >= 2 else { return nil }

let packageName = components[0].trimmingCharacters(in: .whitespaces)
let description = components.dropFirst().joined(separator: ": ").trimmingCharacters(in: .whitespaces)

guard !packageName.isEmpty && !description.isEmpty else { return nil }

return GenericPackage(
id: packageName,
name: packageName,
description: description,
manager: .homebrew,
isInstalled: false
)
}
}

private func parsePackageInfo(_ jsonOutput: String, packageId: String) throws -> Package {
guard let data = jsonOutput.data(using: .utf8) else {
throw PackageManagerError.parseError("Invalid JSON data")
}

do {
if let jsonArray = try JSONSerialization.jsonObject(with: data) as? [[String: Any]],
let packageInfo = jsonArray.first {

let name = packageInfo["name"] as? String ?? packageId
let description = packageInfo["desc"] as? String
let homepage = packageInfo["homepage"] as? String
let version = packageInfo["versions"] as? [String: String]
let currentVersion = version?["stable"]

// Check if package is installed
let installedVersions = packageInfo["installed"] as? [[String: Any]]
let isInstalled = !(installedVersions?.isEmpty ?? true)

// Get dependencies
let dependencies = packageInfo["dependencies"] as? [String] ?? []

return GenericPackage(
id: packageId,
name: name,
version: currentVersion,
description: description,
manager: .homebrew,
isInstalled: isInstalled,
homepage: homepage,
dependencies: dependencies
)
}
} catch {
throw PackageManagerError.parseError("Failed to parse package info JSON: \(error.localizedDescription)")
}

throw PackageManagerError.packageNotFound(packageId)
}
}
Loading