Initial experiments with LLBCASFileTree

+10 -3
Package.swift
···
targets: ["PterodactylSyntax"]
),
.library(
-
name: "PterodactylServer",
-
targets: ["PterodactylServer"]
+
name: "PterodactylBuild",
+
targets: ["PterodactylBuild"]
)
],
dependencies: [
···
name: "PterodactylSyntax"
),
.target(
-
name: "PterodactylServer",
+
name: "PterodactylBuild",
+
dependencies: [
+
.product(name: "llbuild2fx", package: "swift-llbuild2")
+
]
+
),
+
.testTarget(
+
name: "PterodactylBuildTests",
dependencies: [
+
"PterodactylBuild",
.product(name: "llbuild2fx", package: "swift-llbuild2")
]
),
+51
Sources/PterodactylBuild/BuildContext.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
/// A simplified API for `llbuild2fx` that bundles the arguments of `FXKey.computeValue(_:_)`.
+
public struct BuildContext<Key: FXKey> {
+
fileprivate let functionInterface: FXFunctionInterface<Key>
+
fileprivate let context: Context
+
+
var db: any LLBCASDatabase { context.db }
+
+
func request<X: FXKey>(_ key: X) async throws -> X.ValueType {
+
try await functionInterface.request(key, context)
+
}
+
+
func load(_ id: LLBDataID, type hint: LLBFileType? = nil) async throws -> LLBCASFSNode {
+
try await LLBCASFSClient(context.db).load(id, type: hint, context).get()
+
}
+
+
func read(blob: LLBCASBlob) async throws -> LLBByteBufferView {
+
try await blob.read(context).get()
+
}
+
}
+
+
public protocol BuildKey: AsyncFXKey {
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType
+
}
+
+
extension BuildKey {
+
func computeValue(_ fi: FXFunctionInterface<Self>, _ ctx: Context) async throws -> ValueType {
+
try await computeValue(BuildContext(functionInterface: fi, context: ctx))
+
}
+
}
+
+
extension LLBCASFileTree {
+
static func load<X: FXKey>(id: LLBDataID, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
+
try await load(id: id, from: ctx.db, ctx.context).get()
+
}
+
+
func remove<X: FXKey>(path: AbsolutePath, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
+
try await remove(path: path, in: ctx.db, ctx.context).get()
+
}
+
+
func traverse<X: FXKey>(root: AbsolutePath, in ctx: BuildContext<X>, _ callback: (AbsolutePath, LLBDataID, LLBDirectoryEntry) async throws -> Void) async throws {
+
try await traverse(root: root, in: ctx.db, ctx.context, callback)
+
}
+
}
+34
Sources/PterodactylBuild/Keys/AnalyseImports.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Keys {
+
struct AnalyseImports: BuildKey {
+
typealias ValueType = [UnitName]
+
let blobId: LLBDataID
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> [UnitName] {
+
let contents = try await ctx.load(blobId)
+
let code = try await String(decoding: Data(ctx.read(blob: contents.blob!)), as: UTF8.self)
+
+
var results: [UnitName] = []
+
let lines = code.split(separator: "\n", omittingEmptySubsequences: false)
+
+
for line in lines {
+
let trimmed = line.trimmingCharacters(in: .whitespaces)
+
guard trimmed.hasPrefix("import ") else { continue }
+
let parts = trimmed.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: true)
+
if parts.count == 2 {
+
let name = parts[1].trimmingCharacters(in: .whitespaces)
+
results.append(UnitName(name: name))
+
}
+
}
+
+
return results
+
}
+
}
+
}
+33
Sources/PterodactylBuild/Keys/DependencyGraphOfSourceTree.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Graph: FXValue where Vertex: Codable {}
+
+
extension Keys {
+
struct DependencyGraphOfSourceTree: BuildKey {
+
typealias ValueType = Graph<UnitName>
+
+
let sourceTreeId: LLBDataID
+
static let versionDependencies: [any FXVersioning.Type] = [Keys.UnitMapOfSourceTree.self, Keys.AnalyseImports.self]
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> Graph<UnitName> {
+
let unitMap = try await ctx.request(Keys.UnitMapOfSourceTree(sourceTreeId: sourceTreeId))
+
var edges: [UnitName: Set<UnitName>] = [:]
+
+
for (unitName, unitInfo) in unitMap.units {
+
if edges[unitName] == nil { edges[unitName] = [] }
+
let imports = try await ctx.request(Keys.AnalyseImports(blobId: unitInfo.blobId))
+
for importedUnitName in imports {
+
edges[unitName]!.insert(importedUnitName)
+
}
+
}
+
+
return Graph(edges: edges)
+
}
+
}
+
}
+7
Sources/PterodactylBuild/Keys/Keys.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
enum Keys {}
+35
Sources/PterodactylBuild/Keys/NarrowSourceTree.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Keys {
+
/// Narrows a source tree to just the transitive dependencies of a given unit
+
struct NarrowSourceTree: BuildKey {
+
struct ValueType: Codable, FXValue {
+
let sourceTreeId: LLBDataID
+
}
+
+
let sourceTreeId: LLBDataID
+
let unitName: UnitName
+
+
static let versionDependencies: [any FXVersioning.Type] = [TransitiveDependencies.self, UnitMapOfSourceTree.self]
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
+
let dependencies = try await ctx.request(TransitiveDependencies(sourceTreeId: sourceTreeId, unitName: unitName)).dependencies
+
let unitMap = try await ctx.request(UnitMapOfSourceTree(sourceTreeId: sourceTreeId))
+
+
var sourceTree = try await LLBCASFileTree.load(id: sourceTreeId, in: ctx)
+
for (unitName, unitInfo) in unitMap.units {
+
if !dependencies.contains(unitName) {
+
sourceTree = try await sourceTree.remove(path: unitInfo.path, in: ctx)
+
}
+
}
+
+
return ValueType(sourceTreeId: sourceTree.id)
+
}
+
}
+
}
+32
Sources/PterodactylBuild/Keys/SourceCode.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Keys {
+
struct SourceCode: BuildKey {
+
struct ValueType: Codable, FXValue {
+
let code: String
+
}
+
+
enum SourceCodeError: Error {
+
case unitNotFound
+
}
+
+
let sourceTreeId: LLBDataID
+
let unitName: UnitName
+
+
static let versionDependencies: [any FXVersioning.Type] = [UnitMapOfSourceTree.self]
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
+
let unitMap = try await ctx.request(UnitMapOfSourceTree(sourceTreeId: sourceTreeId))
+
guard let unitInfo = unitMap.units[unitName] else { throw SourceCodeError.unitNotFound }
+
let contents = try await ctx.load(unitInfo.blobId)
+
let code = try await String(decoding: Data(ctx.read(blob: contents.blob!)), as: UTF8.self)
+
return ValueType(code: code)
+
}
+
}
+
}
+25
Sources/PterodactylBuild/Keys/TransitiveDependencies.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Keys {
+
struct TransitiveDependencies: BuildKey {
+
struct ValueType: Codable, FXValue {
+
var dependencies: Set<UnitName>
+
}
+
+
let sourceTreeId: LLBDataID
+
let unitName: UnitName
+
+
static let versionDependencies: [any FXVersioning.Type] = [Keys.DependencyGraphOfSourceTree.self]
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
+
let graph = try await ctx.request(Keys.DependencyGraphOfSourceTree(sourceTreeId: sourceTreeId))
+
return ValueType(dependencies: graph.verticesReachableFrom(unitName))
+
}
+
}
+
}
+25
Sources/PterodactylBuild/Keys/UnitMapOfSourceTree.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension Keys {
+
struct UnitMapOfSourceTree: BuildKey {
+
typealias ValueType = UnitMap
+
let sourceTreeId: LLBDataID
+
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> UnitMap {
+
let sourceTree = try await LLBCASFileTree.load(id: sourceTreeId, in: ctx)
+
var units: [UnitName: UnitInfo] = [:]
+
try await sourceTree.traverse(root: .root, in: ctx) { path, blobID, directoryEntry in
+
let unitName = UnitName.fromPath(path)
+
units[unitName] = UnitInfo(path: path, blobId: blobID)
+
}
+
+
return UnitMap(units: units)
+
}
+
}
+
}
+24
Sources/PterodactylBuild/LLBCASFileTree+Traversal.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension LLBCASFileTree {
+
func traverse(root: AbsolutePath, in db: any LLBCASDatabase, _ ctx: Context, _ callback: (AbsolutePath, LLBDataID, LLBDirectoryEntry) async throws -> Void) async throws {
+
for (index, fileId) in object.refs.enumerated() {
+
let directoryEntry = files[index]
+
switch directoryEntry.type {
+
case .directory:
+
let root = root.appending(component: directoryEntry.name)
+
let subtree = try await Self.load(id: fileId, from: db, ctx).get()
+
try await subtree.traverse(root: root, in: db, ctx, callback)
+
case .plainFile:
+
try await callback(root.appending(component: directoryEntry.name), fileId, directoryEntry)
+
default: continue
+
}
+
}
+
}
+
}
+35
Sources/PterodactylBuild/Types/Graph.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
struct Graph<Vertex: Hashable> {
+
var edges: [Vertex: Set<Vertex>]
+
}
+
+
extension Graph: Codable where Vertex: Codable {}
+
extension Graph: Sendable where Vertex: Sendable {}
+
+
extension Graph {
+
func verticesReachableFrom(_ start: Vertex) -> Set<Vertex> {
+
var visited: Set<Vertex> = []
+
var stack: [Vertex] = []
+
+
if let neighbors = edges[start] {
+
stack.append(contentsOf: neighbors)
+
}
+
+
while let vertex = stack.popLast() {
+
guard !visited.contains(vertex) else { continue }
+
visited.insert(vertex)
+
if let neighbors = edges[vertex] {
+
for neighbor in neighbors where !visited.contains(neighbor) {
+
stack.append(neighbor)
+
}
+
}
+
}
+
+
return visited
+
}
+
}
+12
Sources/PterodactylBuild/Types/UnitInfo.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
struct UnitInfo: Equatable, Codable, FXValue {
+
let path: AbsolutePath
+
let blobId: LLBDataID
+
}
+10
Sources/PterodactylBuild/Types/UnitMap.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import llbuild2fx
+
+
struct UnitMap: Codable, FXValue {
+
let units: [UnitName: UnitInfo]
+
}
+19
Sources/PterodactylBuild/Types/UnitName.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import llbuild2fx
+
import TSCBasic
+
+
struct UnitName: Codable, Equatable, Hashable {
+
var name: String
+
}
+
+
extension UnitName: FXValue {}
+
+
extension UnitName {
+
static func fromPath(_ path: AbsolutePath) -> Self {
+
Self(name: path.basenameWithoutExt)
+
}
+
}
-2
Sources/PterodactylServer/PterodactylServer.swift
···
-
import Foundation
-
import llbuild2fx
+49
Tests/PterodactylBuildTests/Test.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Testing
+
+
@testable import PterodactylBuild
+
@testable import llbuild2fx
+
+
struct BuildTests {
+
@Test
+
func testImports() async throws {
+
let group = LLBMakeDefaultDispatchGroup()
+
let db = LLBInMemoryCASDatabase(group: group)
+
let functionCache = FXInMemoryFunctionCache(group: group)
+
let executor = FXLocalExecutor()
+
let engine = FXEngine(group: group, db: db, functionCache: functionCache, executor: executor)
+
let client = LLBCASFSClient(db)
+
let ctx = Context()
+
+
let declTree = LLBDeclFileTree.dir(
+
[
+
"src": .dir([
+
"bar.ext": .file(contents: Array("hello world".utf8)),
+
"foo.ext": .file(contents: Array("import bar".utf8)),
+
"baz.ext": .file(contents: Array("import foo".utf8))
+
])
+
]
+
)
+
+
let treeID: LLBDataID = try await client.store(declTree, ctx).get()
+
let dependencyGraph = try await engine.build(key: Keys.DependencyGraphOfSourceTree(sourceTreeId: treeID), ctx).get()
+
let foo = UnitName(name: "foo")
+
let bar = UnitName(name: "bar")
+
let baz = UnitName(name: "baz")
+
+
#expect(
+
dependencyGraph.edges == [
+
bar: [],
+
foo: [bar],
+
baz: [foo]
+
]
+
)
+
+
let dependenciesOfBaz = try await engine.build(key: Keys.TransitiveDependencies(sourceTreeId: treeID, unitName: baz), ctx).get().dependencies
+
#expect(dependenciesOfBaz == [foo, bar])
+
return
+
}
+
}