Skip to content

Commit 3b1eeb8

Browse files
committed
Add a new swift-package-collections CLI
This implements the proposed CLI for package-collections. Requires #3028
1 parent 7801f56 commit 3b1eeb8

File tree

6 files changed

+399
-3
lines changed

6 files changed

+399
-3
lines changed

Package.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ let package = Package(
171171
.target(
172172
/** High-level commands */
173173
name: "Commands",
174-
dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser"]),
174+
dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections"]),
175175
.target(
176176
/** The main executable provided by SwiftPM */
177177
name: "swift-package",
@@ -188,6 +188,10 @@ let package = Package(
188188
/** Runs an executable product */
189189
name: "swift-run",
190190
dependencies: ["Commands"]),
191+
.target(
192+
/** Interacts with package collections */
193+
name: "swift-package-collections",
194+
dependencies: ["Commands"]),
191195
.target(
192196
/** Shim tool to find test names on OS X */
193197
name: "swiftpm-xctest-helper",
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import ArgumentParser
12+
import Foundation
13+
import PackageCollections
14+
import PackageModel
15+
import TSCBasic
16+
import TSCUtility
17+
18+
private enum CollectionsError: Swift.Error {
19+
case invalidVersionString(String)
20+
case missingArgument(String)
21+
case noCollectionMatchingURL(String)
22+
}
23+
24+
extension CollectionsError: CustomStringConvertible {
25+
var description: String {
26+
switch self {
27+
case .invalidVersionString(let versionString):
28+
return "invalid version string '\(versionString)'"
29+
case .missingArgument(let argumentName):
30+
return "missing argument '\(argumentName)'"
31+
case .noCollectionMatchingURL(let url):
32+
return "no collection matching URL '\(url)'"
33+
}
34+
}
35+
}
36+
37+
struct JSONOptions: ParsableArguments {
38+
@Flag(name: .long, help: "Output as JSON")
39+
var json: Bool = false
40+
}
41+
42+
struct ProfileOptions: ParsableArguments {
43+
@Option(name: .long, help: "Profile to use for the given command")
44+
var profile: String?
45+
46+
var usedProfile: PackageCollectionsModel.Profile {
47+
if let profile = profile {
48+
return .init(name: profile)
49+
} else {
50+
return .default
51+
}
52+
}
53+
}
54+
55+
public struct SwiftPackageCollectionsTool: ParsableCommand {
56+
public static var configuration = CommandConfiguration(
57+
commandName: "package-collections",
58+
_superCommandName: "swift",
59+
abstract: "Interact with package collections",
60+
discussion: "SEE ALSO: swift build, swift package, swift run, swift test",
61+
version: Versioning.currentVersion.completeDisplayString,
62+
subcommands: [
63+
Add.self,
64+
Describe.self,
65+
DescribeCollection.self,
66+
List.self,
67+
ProfileList.self,
68+
Refresh.self,
69+
Remove.self,
70+
Search.self
71+
],
72+
helpNames: [.short, .long, .customLong("help", withSingleDash: true)])
73+
74+
public init() {
75+
}
76+
77+
// MARK: Profiles
78+
79+
struct ProfileList: ParsableCommand {
80+
static let configuration = CommandConfiguration(abstract: "List configured profiles")
81+
82+
@OptionGroup
83+
var jsonOptions: JSONOptions
84+
85+
mutating func run() throws {
86+
let profiles: [PackageCollectionsModel.Profile] = try await { self.collections.listProfiles(callback: $0) }
87+
88+
if jsonOptions.json {
89+
try JSONEncoder().print(profiles)
90+
} else {
91+
profiles.forEach {
92+
print($0)
93+
}
94+
}
95+
}
96+
}
97+
98+
// MARK: Collections
99+
100+
struct List: ParsableCommand {
101+
static let configuration = CommandConfiguration(abstract: "List configured collections")
102+
103+
@OptionGroup
104+
var jsonOptions: JSONOptions
105+
106+
@OptionGroup
107+
var profileOptions: ProfileOptions
108+
109+
mutating func run() throws {
110+
let collections = try await { self.collections.listCollections(identifiers: nil, in: profileOptions.usedProfile, callback: $0) }
111+
112+
if jsonOptions.json {
113+
try JSONEncoder().print(collections)
114+
} else {
115+
collections.forEach {
116+
print("\($0.name) - \($0.source.url)")
117+
}
118+
}
119+
}
120+
}
121+
122+
struct Refresh: ParsableCommand {
123+
static let configuration = CommandConfiguration(abstract: "Refresh configured collections")
124+
125+
@OptionGroup
126+
var profileOptions: ProfileOptions
127+
128+
mutating func run() throws {
129+
let collections = try await { self.collections.refreshCollections(in: profileOptions.usedProfile, callback: $0) }
130+
print("Refreshed \(collections.count) configured package collections.")
131+
}
132+
}
133+
134+
struct Add: ParsableCommand {
135+
static let configuration = CommandConfiguration(abstract: "Add a new collection")
136+
137+
@Argument(help: "URL of the collection to add")
138+
var collectionUrl: String?
139+
140+
@Option(name: .long, help: "Sort order for the added collection")
141+
var order: Int?
142+
143+
@OptionGroup
144+
var profileOptions: ProfileOptions
145+
146+
mutating func run() throws {
147+
guard let collectionUrlString = collectionUrl, let collectionUrl = URL(string: collectionUrlString) else {
148+
throw CollectionsError.missingArgument("collectionUrl")
149+
}
150+
151+
let source = PackageCollectionsModel.CollectionSource(url: collectionUrl)
152+
let collection = try await { self.collections.addCollection(source, order: order, to: profileOptions.usedProfile, callback: $0) }
153+
154+
print("Added \"\(collection.name)\" to your package collections.")
155+
}
156+
}
157+
158+
struct Remove: ParsableCommand {
159+
static let configuration = CommandConfiguration(abstract: "Remove a configured collection")
160+
161+
@OptionGroup
162+
var profileOptions: ProfileOptions
163+
164+
@Argument(help: "URL of the collection to remove")
165+
var collectionUrl: String?
166+
167+
mutating func run() throws {
168+
guard let collectionUrlString = collectionUrl, let collectionUrl = URL(string: collectionUrlString) else {
169+
throw CollectionsError.missingArgument("collectionUrl")
170+
}
171+
172+
let collections = try await { self.collections.listCollections(identifiers: nil, in: profileOptions.usedProfile, callback: $0) }
173+
let source = PackageCollectionsModel.CollectionSource(url: collectionUrl)
174+
175+
guard let collection = collections.first(where: { $0.source == source }) else {
176+
throw CollectionsError.noCollectionMatchingURL(collectionUrlString)
177+
}
178+
179+
_ = try await { self.collections.removeCollection(source, from: profileOptions.usedProfile, callback: $0) }
180+
print("Removed \"\(collection.name)\" from your package collections.")
181+
}
182+
}
183+
184+
struct DescribeCollection: ParsableCommand {
185+
static let configuration = CommandConfiguration(abstract: "Get metadata for a configured collection")
186+
187+
@Argument(help: "URL of the collection to describe")
188+
var collectionUrl: String?
189+
190+
@OptionGroup
191+
var profileOptions: ProfileOptions
192+
193+
mutating func run() throws {
194+
guard let collectionUrlString = collectionUrl, let collectionUrl = URL(string: collectionUrlString) else {
195+
throw CollectionsError.missingArgument("collectionUrl")
196+
}
197+
198+
let collections = try await { self.collections.listCollections(identifiers: nil, in: profileOptions.usedProfile, callback: $0) }
199+
let source = PackageCollectionsModel.CollectionSource(url: collectionUrl)
200+
201+
guard let collection = collections.first(where: { $0.source == source }) else {
202+
throw CollectionsError.noCollectionMatchingURL(collectionUrlString)
203+
}
204+
205+
let description = optionalRow("Description", collection.description)
206+
let keywords = optionalRow("Keywords", collection.keywords?.joined(separator: ", "))
207+
let createdAt = DateFormatter().string(from: collection.createdAt)
208+
let packages = collection.packages.map { "\($0.repository.url)" }.joined(separator: "\n")
209+
210+
print("""
211+
Name: \(collection.name)
212+
Source: \(collection.source.url)\(description)\(keywords)
213+
Created At: \(createdAt)
214+
Packages:
215+
\(packages)
216+
""")
217+
}
218+
}
219+
220+
// MARK: Search
221+
222+
enum SearchMethod: String, EnumerableFlag {
223+
case keywords
224+
case module
225+
}
226+
227+
struct Search: ParsableCommand {
228+
static var configuration = CommandConfiguration(abstract: "Search for packages by keywords or module names")
229+
230+
@OptionGroup
231+
var jsonOptions: JSONOptions
232+
233+
@OptionGroup
234+
var profileOptions: ProfileOptions
235+
236+
@Flag(help: "Pick the method for searching")
237+
var searchMethod: SearchMethod
238+
239+
@Argument(help: "Search query")
240+
var searchQuery: String?
241+
242+
mutating func run() throws {
243+
guard let searchQuery = searchQuery else {
244+
throw CollectionsError.missingArgument("searchQuery")
245+
}
246+
247+
switch searchMethod {
248+
case .keywords:
249+
let results = try await { collections.findPackages(searchQuery, collections: nil, profile: profileOptions.usedProfile, callback: $0) }
250+
251+
results.items.forEach {
252+
print("\($0.package.repository.url): \($0.package.description ?? "")")
253+
}
254+
255+
case .module:
256+
let results = try await { collections.findTargets(searchQuery, searchType: .exactMatch, collections: nil, profile: profileOptions.usedProfile, callback: $0) }
257+
258+
results.items.forEach {
259+
$0.packages.forEach {
260+
print("\($0.repository.url): \($0.description ?? "")")
261+
}
262+
}
263+
}
264+
}
265+
}
266+
267+
// MARK: Packages
268+
269+
struct Describe: ParsableCommand {
270+
static var configuration = CommandConfiguration(abstract: "Get metadata for a single package")
271+
272+
@OptionGroup
273+
var jsonOptions: JSONOptions
274+
275+
@OptionGroup
276+
var profileOptions: ProfileOptions
277+
278+
@Argument(help: "URL of the package to get information for")
279+
var packageUrl: String?
280+
281+
@Option(name: .long, help: "Version of the package to get information for")
282+
var version: String?
283+
284+
private func printVersion(_ version: PackageCollectionsModel.Package.Version?) -> String? {
285+
guard let version = version else {
286+
return nil
287+
}
288+
289+
let modules = version.targets.compactMap { $0.moduleName }.joined(separator: ", ")
290+
let platforms = optionalRow("Supported Platforms", version.verifiedPlatforms?.map { $0.name }.joined(separator: ", "))
291+
let swiftVersions = optionalRow("Supported Swift Versions", version.verifiedSwiftVersions?.map { $0.rawValue }.joined(separator: ", "))
292+
let license = optionalRow("License", version.license?.type.description)
293+
let cves = optionalRow("CVEs", version.cves?.map { $0.identifier }.joined(separator: ", "))
294+
295+
return """
296+
\(version.version)
297+
Package Name: \(version.packageName)
298+
Modules: \(modules)\(platforms)\(swiftVersions)\(license)\(cves)
299+
"""
300+
}
301+
302+
mutating func run() throws {
303+
guard let packageUrl = packageUrl else {
304+
throw CollectionsError.missingArgument("packageUrl")
305+
}
306+
307+
let identity = PackageReference.computeIdentity(packageURL: packageUrl)
308+
let reference = PackageReference(identity: identity, path: packageUrl)
309+
310+
let result = try await { self.collections.getPackageMetadata(reference, profile: profileOptions.usedProfile, callback: $0) }
311+
312+
if let versionString = version {
313+
guard let version = TSCUtility.Version(string: versionString), let result = result.package.versions.first(where: { $0.version == version }), let printedResult = printVersion(result) else {
314+
throw CollectionsError.invalidVersionString(versionString)
315+
}
316+
317+
print("Version: \(printedResult)")
318+
} else {
319+
let description = optionalRow("Description", result.package.description)
320+
let versions = result.package.versions.map { "\($0.version)" }.joined(separator: ", ")
321+
let watchers = optionalRow("Watchers", result.package.watchersCount?.description)
322+
let readme = optionalRow("Readme", result.package.readmeURL?.absoluteString)
323+
let authors = optionalRow("Authors", result.package.authors?.map { $0.username }.joined(separator: ", "))
324+
let latestVersion = optionalRow("--------------------------------------------------------------\nLatest Version", printVersion(result.package.latestVersion))
325+
326+
print("""
327+
\(description)Available Versions: \(versions)\(watchers)\(readme)\(authors)\(latestVersion)
328+
""")
329+
}
330+
}
331+
}
332+
}
333+
334+
private func optionalRow(_ title: String, _ contents: String?) -> String {
335+
if let contents = contents {
336+
return "\n\(title): \(contents)\n"
337+
} else {
338+
return ""
339+
}
340+
}
341+
342+
private extension JSONEncoder {
343+
func print<T>(_ value: T) throws where T : Encodable {
344+
if #available(macOS 10.13, *) {
345+
self.outputFormatting = [.prettyPrinted, .sortedKeys]
346+
}
347+
348+
let jsonData = try self.encode(value)
349+
let jsonString = String(data: jsonData, encoding: .utf8)!
350+
Swift.print(jsonString)
351+
}
352+
}
353+
354+
private extension ParsableCommand {
355+
var collections: PackageCollectionsProtocol {
356+
fatalError("not implemented")
357+
}
358+
}

Sources/PackageCollections/Model/Profile.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@
1010

1111
extension PackageCollectionsModel {
1212
/// A `PackageGroupProfile` is a grouping of `PackageGroup`s.
13-
public struct Profile: Hashable {
13+
public struct Profile: Hashable, Codable {
1414
/// The default profile; this should be used when a profile is required but not specified.
1515
public static let `default` = Profile(name: "default")
1616

1717
/// Profile name
1818
public let name: String
19+
20+
public init(name: String) {
21+
self.name = name
22+
}
1923
}
2024
}

0 commit comments

Comments
 (0)