Skip to content

Commit

Permalink
Add support for dependencies as binary targets
Browse files Browse the repository at this point in the history
  • Loading branch information
albertodebortoli committed Dec 17, 2024
1 parent 3a91f22 commit c344094
Show file tree
Hide file tree
Showing 18 changed files with 571 additions and 130 deletions.
16 changes: 16 additions & 0 deletions .swiftpm/xcode/xcshareddata/xcschemes/GeneratePackage.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@
argument = "--template Templates/Package.stencil"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--dependencies-as-binary-targets"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--relative-dependencies-path &quot;.xcframeworks&quot;"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--hashing-paths Package.swift"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--hashing-paths Source"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription

let package = Package(
name: "PackageGenerator",
platforms: [.macOS(.v12)],
platforms: [.macOS(.v13)],
products: [
.executable(name: "PackageGenerator", targets: ["PackageGenerator"])
],
Expand Down
21 changes: 21 additions & 0 deletions Sources/Commands/CachingFlags.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// CachingFlags.swift

import ArgumentParser

struct CachingFlags: ParsableArguments {

@Flag(name: .long, help: "Whether to use binary targets for dependencies.")
var dependenciesAsBinaryTargets: Bool = false

@Option(name: .long, help: "Path to a folder containing dependencies. Required if --dependencies-as-binary-targets is set.")
var relativeDependenciesPath: String?

@Option(name: .long, help: "List of required relative paths to use when generating the hash for local dependencies. Required if --dependencies-as-binary-targets is set.")
var requiredHashingPaths: [String] = []

@Option(name: .long, help: "List of optional relative paths to use when generating the hash for local dependencies.")
var optionalHashingPaths: [String] = []

@Option(name: .long, help: "List of dependencies to exclude from the list of binary targets.")
var exclusions: [String] = []
}
79 changes: 52 additions & 27 deletions Sources/Commands/GeneratePackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,61 @@
import ArgumentParser
import Foundation

struct GeneratePackage: ParsableCommand {
struct GeneratePackage: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Generate a Package.swift file from a spec.")

@Option(name: .long, help: "Path to a package spec file (supported formats: json, yaml)")
private var spec: String

@Option(name: .long, help: "Path to s dependencies file (supported formats: json, yaml)")
private var dependencies: String

@Option(name: .long, help: "Path to a template file (supported formats: stencil)")
private var template: String

func run() throws {
let content = try generatePackageContent()
let path = try write(content: content)
print("✅ File successfully saved at \(path).")
}

private func generatePackageContent() throws -> Content {
let specUrl = URL(fileURLWithPath: spec, isDirectory: false)
let dependenciesUrl = URL(fileURLWithPath: dependencies, isDirectory: false)
let specGenerator = SpecGenerator(specUrl: specUrl, dependenciesUrl: dependenciesUrl)
let spec = try specGenerator.makeSpec()
let templater = Templater(templatePath: template)
return try templater.renderTemplate(context: spec.makeContext())
@Option(name: .long, help: "Path to a package spec file (supported formats: json, yaml).")
var spec: String

@Option(name: .long, help: "Path to s dependencies file (supported formats: json, yaml).")
var dependencies: String

@Option(name: .long, help: "Path to a template file (supported formats: stencil).")
var template: String

@OptionGroup()
var cachingFlags: CachingFlags

func run() async throws {
let generator = Generator(
specUrl: URL(filePath: spec, directoryHint: .notDirectory),
templateUrl: URL(filePath: template, directoryHint: .notDirectory),
dependenciesUrl: URL(fileURLWithPath: dependencies, isDirectory: false),
fileManager: .default
)
let dependencyTreatment: Generator.DependencyTreatment = try {
if cachingFlags.dependenciesAsBinaryTargets {
guard let relativeDependenciesPath = cachingFlags.relativeDependenciesPath else {
throw ValidationError("--dependencies-as-binary-targets is set but --relative-dependencies-path is not specified")
}
return .binaryTargets(
relativeDependenciesPath: relativeDependenciesPath,
requiredHashingPaths: cachingFlags.requiredHashingPaths,
optionalHashingPaths: cachingFlags.optionalHashingPaths,
exclusions: cachingFlags.exclusions
)
}
return .standard
}()
try await generator.generatePackage(dependencyTreatment: dependencyTreatment)
}

private func write(content: Content) throws -> String {
let specUrl = URL(fileURLWithPath: spec, isDirectory: false)
let packageFolder = specUrl.deletingLastPathComponent()
return try Writer().writePackageFile(content: content, to: packageFolder)
func validate() throws {
if cachingFlags.dependenciesAsBinaryTargets {
if cachingFlags.relativeDependenciesPath == nil {
throw ValidationError("--dependencies-as-binary-targets is set but --relative-dependencies-path is not specified")
}
if cachingFlags.requiredHashingPaths.isEmpty {
throw ValidationError("--dependencies-as-binary-targets is set but --required-hashing-paths is not specified")
}
}
if !cachingFlags.dependenciesAsBinaryTargets {
if cachingFlags.relativeDependenciesPath != nil {
throw ValidationError("--relative-dependencies-path specified but --dependencies-as-binary-targets is unset")
}
if !cachingFlags.requiredHashingPaths.isEmpty {
throw ValidationError("--required-hashing-paths specified but --dependencies-as-binary-targets is unset")
}
}
}
}
11 changes: 11 additions & 0 deletions Sources/Core/ContentGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// ContentGenerator.swift

import Foundation

struct ContentGenerator {

func content(for spec: Spec, templateUrl: URL) throws -> Content {
let templater = Templater(templateUrl: templateUrl)
return try templater.renderTemplate(context: spec.makeContext())
}
}
25 changes: 25 additions & 0 deletions Sources/Core/DTOLoader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// DTOLoader.swift

import Foundation
import Yams

final class DTOLoader {

enum GeneratorError: Error {
case invalidFormat(String)
}

func loadDto<T: Decodable>(url: URL) throws -> T {
let data = try Data(contentsOf: url)
switch url.pathExtension {
case "json":
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
case "yaml", "yml":
let decoder = YAMLDecoder()
return try decoder.decode(T.self, from: data)
default:
throw GeneratorError.invalidFormat(url.pathExtension)
}
}
}
98 changes: 98 additions & 0 deletions Sources/Core/DependencyFinder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// DependencyFinder.swift

import Foundation

final class DependencyFinder {

enum DependencyFinderError: Error, LocalizedError {
case failedCollectingDependencies(packageUrl: URL)

var errorDescription: String? {
switch self {
case .failedCollectingDependencies(let packageUrl):
return "Failed collecting dependencies at \(packageUrl.path)"
}
}
}

private let fileManager: FileManager

private lazy var dependencyHasher: DependencyHasher = {
DependencyHasher(fileManager: fileManager)
}()

// MARK: - Inits

init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}

// MARK: - Functions

func findPackageDependencies(at url: URL, requiredHashingPaths: [String], optionalHashingPaths: [String] = []) async throws -> [PackageDependency] {
let process = Process()
process.executableURL = URL(filePath: "/usr/bin/swift", directoryHint: .notDirectory)
process.arguments = ["package", "show-dependencies", "--format", "json"]
process.currentDirectoryURL = url

let pipe = Pipe()
process.standardOutput = pipe
process.standardError = Pipe()

try process.run()
process.waitUntilExit()

guard process.terminationStatus == 0 else {
throw DependencyFinderError.failedCollectingDependencies(packageUrl: url)
}

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let packageDescription = try JSONDecoder().decode(PackageSpec.self, from: data)
return try parseDependencies(
packageDescription.dependencies,
requiredHashingPaths: requiredHashingPaths,
optionalHashingPaths: optionalHashingPaths
)
}

// MARK: - Helper Functions

private func parseDependencies(_ dependencies: [DependencySpec], requiredHashingPaths: [String], optionalHashingPaths: [String] = []) throws -> [PackageDependency] {
var result = [PackageDependency]()

for dependency in dependencies {
if dependency.version == "unspecified" {
let url = URL(filePath: dependency.path, directoryHint: .isDirectory)
let hash = try dependencyHasher.hashForPackage(
at: url,
requiredSubpaths: requiredHashingPaths,
optionalSubpaths: optionalHashingPaths
)
result.append(
PackageDependency(
name: dependency.name,
type: .local(hash: hash)
)
)
}
else {
result.append(
PackageDependency(
name: dependency.name,
type: .remote(tag: dependency.version)
)
)
}

let nestedDependencies = try parseDependencies(
dependency.dependencies,
requiredHashingPaths: requiredHashingPaths,
optionalHashingPaths: optionalHashingPaths
)
result.append(contentsOf: nestedDependencies)
}

return Set(result)
.sorted { $0.name.lowercased() < $1.name.lowercased() }
}
}
Loading

0 comments on commit c344094

Please sign in to comment.