basic package graph implemented

This commit is contained in:
2024-11-11 21:06:37 +11:00
parent af87395545
commit cf5e1a3f35
10 changed files with 240 additions and 61 deletions

View File

@ -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)"
}
} }
} }

View 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
}
}
}

View 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
}
}

View File

@ -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)"
}
} }
} }

View File

@ -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
} }
} }

View File

@ -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
} }

View File

@ -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
} }
} }

View File

@ -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))"
}
} }
} }

View File

@ -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 {

View 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()
}
}