mirror of
https://github.com/GayPizzaSpecifications/darwin-apk.git
synced 2025-08-03 13:31:32 +00:00
basic package graph implemented
This commit is contained in:
@ -5,8 +5,16 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
internal struct ApkRequirement {
|
internal struct ApkRequirement: Hashable {
|
||||||
static func extract(blob extract: String) throws(ParseError) -> (String, ApkVersionSpecification) {
|
let name: String
|
||||||
|
let versionSpec: ApkVersionSpecification
|
||||||
|
|
||||||
|
init(name: String, spec: ApkVersionSpecification) {
|
||||||
|
self.name = name
|
||||||
|
self.versionSpec = spec
|
||||||
|
}
|
||||||
|
|
||||||
|
init(extract: String) throws(ParseError) {
|
||||||
var comparer: ComparatorBits = []
|
var comparer: ComparatorBits = []
|
||||||
var dependStr = extract[...]
|
var dependStr = extract[...]
|
||||||
let nameEnd: String.Index, versionStart: String.Index
|
let nameEnd: String.Index, versionStart: String.Index
|
||||||
@ -30,7 +38,7 @@ internal struct ApkRequirement {
|
|||||||
}
|
}
|
||||||
(nameEnd, versionStart) = (range.lowerBound, range.upperBound)
|
(nameEnd, versionStart) = (range.lowerBound, range.upperBound)
|
||||||
} else {
|
} else {
|
||||||
//
|
// Lack of conflict flag indicates any version
|
||||||
if !comparer.contains(.conflict) {
|
if !comparer.contains(.conflict) {
|
||||||
comparer.formUnion(.any)
|
comparer.formUnion(.any)
|
||||||
}
|
}
|
||||||
@ -38,9 +46,18 @@ internal struct ApkRequirement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse version specification
|
// Parse version specification
|
||||||
let spec = try ApkVersionSpecification(comparer, version: dependStr[versionStart...])
|
self.versionSpec = try ApkVersionSpecification(comparer, version: dependStr[versionStart...])
|
||||||
let name = String(dependStr[..<nameEnd])
|
self.name = String(dependStr[..<nameEnd])
|
||||||
return (name, spec)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApkRequirement: CustomStringConvertible {
|
||||||
|
var description: String {
|
||||||
|
switch self.versionSpec {
|
||||||
|
case .any: self.name
|
||||||
|
case .conflict: "!\(self.name)"
|
||||||
|
case .constraint(let op, let version): "\(self.name)\(op)\(version)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
64
Sources/apk/Graph/ApkPackageGraph.swift
Normal file
64
Sources/apk/Graph/ApkPackageGraph.swift
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* darwin-apk © 2024 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ApkPackageGraph {
|
||||||
|
let pkgIndex: ApkIndex
|
||||||
|
|
||||||
|
private var _nodes = [ApkPackageGraphNode]()
|
||||||
|
|
||||||
|
var nodes: [ApkPackageGraphNode] { self._nodes }
|
||||||
|
var shallowIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.parents.isEmpty) }
|
||||||
|
var deepIsolates: [ApkPackageGraphNode] { self._nodes.filter(\.children.isEmpty) }
|
||||||
|
|
||||||
|
init(index: ApkIndex) {
|
||||||
|
self.pkgIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildGraphNode() {
|
||||||
|
var provides = [String: Int]()
|
||||||
|
|
||||||
|
for (idx, package) in self.pkgIndex.packages.enumerated() {
|
||||||
|
provides[package.name] = idx
|
||||||
|
for provision in package.provides {
|
||||||
|
provides[provision.name] = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for package in pkgIndex.packages {
|
||||||
|
self._nodes.append(.init(
|
||||||
|
package: package,
|
||||||
|
children: package.dependencies.compactMap { dependency in
|
||||||
|
guard let id = provides[dependency.requirement.name] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(self, id: id, constraint: .dep(version: dependency.requirement.versionSpec))
|
||||||
|
} + package.provides.compactMap { provision in
|
||||||
|
guard let id = provides[provision.name] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(self, id: id, constraint: .provision)
|
||||||
|
} + package.installIf.compactMap { installIf in
|
||||||
|
guard let id = provides[installIf.requirement.name] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(self, id: id, constraint: .installIf(version: installIf.requirement.versionSpec ))
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
var reverseDependencies = [ApkIndexRequirementRef: [ApkIndexRequirementRef]]()
|
||||||
|
for (index, node) in self._nodes.enumerated() {
|
||||||
|
for child in node.children {
|
||||||
|
reverseDependencies[child, default: []].append(
|
||||||
|
.init(self, id: index, constraint: child.constraint)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ref, parents) in reverseDependencies {
|
||||||
|
self._nodes[ref.packageID].parents = parents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
Sources/apk/Graph/ApkPackageGraphNode.swift
Normal file
35
Sources/apk/Graph/ApkPackageGraphNode.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* darwin-apk © 2024 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ApkPackageGraphNode {
|
||||||
|
private weak var graph: ApkPackageGraph!
|
||||||
|
let package: ApkIndexPackage
|
||||||
|
|
||||||
|
//private var _parents = NSHashTable<ApkPackageGraphNode>.weakObjects()
|
||||||
|
//private var _children = NSHashTable<ApkPackageGraphNode>.weakObjects()
|
||||||
|
var parents = [ApkIndexRequirementRef]()
|
||||||
|
var children: [ApkIndexRequirementRef]
|
||||||
|
|
||||||
|
internal init(package: ApkIndexPackage, children: [ApkIndexRequirementRef]) {
|
||||||
|
self.package = package
|
||||||
|
self.children = children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApkPackageGraphNode: CustomStringConvertible {
|
||||||
|
var description: String {
|
||||||
|
var result = "node[\(self.package.name)]"
|
||||||
|
if !self.parents.isEmpty {
|
||||||
|
result += ", parents[\(self.parents.lazy.map(\.description).joined(separator: ", "))]"
|
||||||
|
}
|
||||||
|
if !self.children.isEmpty {
|
||||||
|
result += ", children[\(self.children.lazy.map(\.description).joined(separator: ", "))]"
|
||||||
|
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -5,22 +5,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ApkIndexDependency: ApkIndexRequirementRef {
|
struct ApkIndexDependency: Hashable {
|
||||||
let name: String
|
let requirement: ApkRequirement
|
||||||
let versionSpec: ApkVersionSpecification
|
|
||||||
|
|
||||||
init(name: String, version spec: ApkVersionSpecification) {
|
init(requirement: ApkRequirement) {
|
||||||
self.name = name
|
self.requirement = requirement
|
||||||
self.versionSpec = spec
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ApkIndexDependency: CustomStringConvertible {
|
|
||||||
var description: String {
|
|
||||||
switch self.versionSpec {
|
|
||||||
case .any: self.name
|
|
||||||
case .conflict: "!\(self.name)"
|
|
||||||
case .constraint(let op, let version): "\(self.name)\(op)\(version)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,10 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct ApkIndexInstallIf: ApkIndexRequirementRef {
|
struct ApkIndexInstallIf: Hashable {
|
||||||
let name: String
|
let requirement: ApkRequirement
|
||||||
let versionSpec: ApkVersionSpecification
|
|
||||||
|
|
||||||
init(name: String, version spec: ApkVersionSpecification) {
|
init(requirement: ApkRequirement) {
|
||||||
self.name = name
|
self.requirement = requirement
|
||||||
self.versionSpec = spec
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ApkIndexPackage: ApkIndexRequirementRef {
|
struct ApkIndexPackage: Hashable {
|
||||||
let indexChecksum: ApkIndexDigest
|
let indexChecksum: ApkIndexDigest
|
||||||
let name: String
|
let name: String
|
||||||
let version: String
|
let version: String
|
||||||
@ -74,8 +74,10 @@ extension ApkIndexPackage {
|
|||||||
case "A":
|
case "A":
|
||||||
architecture = record.value
|
architecture = record.value
|
||||||
case "D":
|
case "D":
|
||||||
do { dependencies = try ApkIndexDependency.extract(record.value) }
|
do {
|
||||||
catch { throw .badValue(key: record.key) }
|
dependencies = try record.value.components(separatedBy: " ")
|
||||||
|
.map { .init(requirement: try .init(extract: $0)) }
|
||||||
|
} catch { throw .badValue(key: record.key) }
|
||||||
case "C":
|
case "C":
|
||||||
guard let digest = ApkIndexDigest(decode: record.value) else {
|
guard let digest = ApkIndexDigest(decode: record.value) else {
|
||||||
throw .badValue(key: record.key)
|
throw .badValue(key: record.key)
|
||||||
@ -92,11 +94,15 @@ extension ApkIndexPackage {
|
|||||||
}
|
}
|
||||||
installedSize = value
|
installedSize = value
|
||||||
case "p":
|
case "p":
|
||||||
do { provides = try ApkIndexProvides.extract(record.value) }
|
do {
|
||||||
catch { throw .badValue(key: record.key) }
|
provides = try record.value.components(separatedBy: " ")
|
||||||
|
.map { .init(requirement: try .init(extract: $0)) }
|
||||||
|
} catch { throw .badValue(key: record.key) }
|
||||||
case "i":
|
case "i":
|
||||||
do { installIf = try ApkIndexInstallIf.extract(record.value) }
|
do {
|
||||||
catch { throw .badValue(key: record.key) }
|
installIf = try record.value.components(separatedBy: " ")
|
||||||
|
.map { .init(requirement: try .init(extract: $0)) }
|
||||||
|
} catch { throw .badValue(key: record.key) }
|
||||||
case "o":
|
case "o":
|
||||||
origin = record.value
|
origin = record.value
|
||||||
case "m":
|
case "m":
|
||||||
@ -191,13 +197,13 @@ extension ApkIndexPackage: CustomStringConvertible {
|
|||||||
s += "provider prio: \(providerPrio)\n"
|
s += "provider prio: \(providerPrio)\n"
|
||||||
}
|
}
|
||||||
if !self.dependencies.isEmpty {
|
if !self.dependencies.isEmpty {
|
||||||
s += "dependencies: - \(self.dependencies.map(String.init).joined(separator: " "))\n"
|
s += "dependencies: - \(self.dependencies.map(\.requirement.description).joined(separator: " "))\n"
|
||||||
}
|
}
|
||||||
if !self.provides.isEmpty {
|
if !self.provides.isEmpty {
|
||||||
s += "provides: ----- \(self.provides.map { $0.name }.joined(separator: " "))\n"
|
s += "provides: ----- \(self.provides.map(\.name).joined(separator: " "))\n"
|
||||||
}
|
}
|
||||||
if !self.installIf.isEmpty {
|
if !self.installIf.isEmpty {
|
||||||
s += "install if: --- \(self.installIf.map { $0.name }.joined(separator: " "))\n"
|
s += "install if: --- \(self.installIf.map(\.requirement.description).joined(separator: " "))\n"
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
struct ApkIndexProvides: ApkIndexRequirementRef {
|
struct ApkIndexProvides: Hashable {
|
||||||
let name: String
|
let name: String
|
||||||
|
|
||||||
init(name: String, version _: ApkVersionSpecification) {
|
init(requirement: ApkRequirement) {
|
||||||
self.name = name
|
self.name = requirement.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,24 +3,58 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
protocol ApkIndexRequirementRef: Equatable, Hashable {
|
struct ApkIndexRequirementRef {
|
||||||
var name: String { get }
|
private weak var _graph: ApkPackageGraph?
|
||||||
var invert: Bool { get }
|
|
||||||
|
|
||||||
init(name: String, version spec: ApkVersionSpecification)
|
let packageID: Int
|
||||||
|
let constraint: Constraint
|
||||||
|
|
||||||
func satisfied(by other: ApkIndexPackage) -> Bool
|
init(_ graph: ApkPackageGraph, id: Int, constraint: Constraint) {
|
||||||
|
self._graph = graph
|
||||||
|
self.packageID = id
|
||||||
|
self.constraint = constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
var package: ApkIndexPackage {
|
||||||
|
self._graph!.pkgIndex.packages[self.packageID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func satisfied(by other: ApkRequirement) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApkIndexRequirementRef: Equatable, Hashable {
|
||||||
|
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||||
|
lhs.packageID == rhs.packageID && lhs.constraint == rhs.constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
self.packageID.hash(into: &hasher)
|
||||||
|
self.constraint.hash(into: &hasher)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ApkIndexRequirementRef {
|
extension ApkIndexRequirementRef {
|
||||||
var invert: Bool { false }
|
enum Constraint: Hashable {
|
||||||
func satisfied(by _: ApkIndexPackage) -> Bool { true }
|
case dep(version: ApkVersionSpecification)
|
||||||
|
case provision
|
||||||
static func extract<T: ApkIndexRequirementRef>(_ blob: String) throws(ApkRequirement.ParseError) -> [T] {
|
case installIf(version: ApkVersionSpecification)
|
||||||
return try blob.components(separatedBy: " ")
|
}
|
||||||
.map { token throws(ApkRequirement.ParseError) in
|
}
|
||||||
let (name, versionSpec) = try ApkRequirement.extract(blob: token)
|
|
||||||
return .init(name: name, version: versionSpec)
|
extension ApkIndexRequirementRef: CustomStringConvertible {
|
||||||
}
|
var description: String {
|
||||||
|
guard let package = self._graph?.pkgIndex.packages[self.packageID] else {
|
||||||
|
return String()
|
||||||
|
}
|
||||||
|
return switch self.constraint {
|
||||||
|
case .dep(let version):
|
||||||
|
"dep=\(ApkRequirement(name: package.name, spec: version))"
|
||||||
|
case .provision:
|
||||||
|
"provides=\(package.name)"
|
||||||
|
case .installIf(let version):
|
||||||
|
"installIf=\(ApkRequirement(name: package.name, spec: version))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@ public struct ApkIndexUpdater {
|
|||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
self.repositories = [
|
self.repositories = [
|
||||||
"https://dl-cdn.alpinelinux.org/alpine/v3.21/main",
|
"https://dl-cdn.alpinelinux.org/alpine/v3.20/main",
|
||||||
"https://dl-cdn.alpinelinux.org/alpine/edge/community"
|
"https://dl-cdn.alpinelinux.org/alpine/v3.20/community"
|
||||||
]
|
]
|
||||||
// other archs: "armhf", "armv7", "loongarch64", "ppc64le", "riscv64", "s390x", "x86"
|
// other archs: "armhf", "armv7", "loongarch64", "ppc64le", "riscv64", "s390x", "x86"
|
||||||
self.architectures = [ "aarch64", "x86_64" ]
|
self.architectures = [ "aarch64" /*, "x86_64" */ ]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update() {
|
public func update() {
|
||||||
@ -41,14 +41,23 @@ public struct ApkIndexUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let index: ApkIndex
|
let graph: ApkPackageGraph
|
||||||
do {
|
do {
|
||||||
let tables = try repositories.map { try readIndex(URL(filePath: $0.localName)) }
|
let tables = try repositories.map { try readIndex(URL(filePath: $0.localName)) }
|
||||||
index = ApkIndex.merge(tables)
|
graph = ApkPackageGraph(index: ApkIndex.merge(tables))
|
||||||
try index.description.write(to: URL(fileURLWithPath: "packages.txt"), atomically: false, encoding: .utf8)
|
graph.buildGraphNode()
|
||||||
|
|
||||||
|
try graph.pkgIndex.description.write(to: URL(filePath: "packages.txt"), atomically: false, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
fatalError(error.localizedDescription)
|
fatalError(error.localizedDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if var out = TextFileWriter(URL(filePath: "shallowIsolates.txt")) {
|
||||||
|
for node in graph.shallowIsolates { print(node, to: &out) }
|
||||||
|
}
|
||||||
|
if var out = TextFileWriter(URL(filePath: "deepIsolates.txt")) {
|
||||||
|
for node in graph.deepIsolates { print(node, to: &out) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func readIndex(_ indexURL: URL) throws -> ApkIndex {
|
private func readIndex(_ indexURL: URL) throws -> ApkIndex {
|
||||||
|
28
Sources/apk/Utility/TextFileWriter.swift
Normal file
28
Sources/apk/Utility/TextFileWriter.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* darwin-apk © 2024 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TextFileWriter: TextOutputStream {
|
||||||
|
private var _hnd: FileHandle
|
||||||
|
|
||||||
|
init?(_ to: URL) {
|
||||||
|
let file = open(to.path(), O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, 0o644)
|
||||||
|
guard file >= 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self._hnd = FileHandle(fileDescriptor: file, closeOnDealloc: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func write(_ string: String) {
|
||||||
|
if let data = string.data(using: .utf8) {
|
||||||
|
try? self._hnd.write(contentsOf: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func close() throws {
|
||||||
|
try self._hnd.close()
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user