Skip to content

Add AbsolutePath and SourceControlURL parsing to swift package add-dependency #7769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions Sources/Basics/SourceControlURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,42 @@ public struct SourceControlURL: Codable, Equatable, Hashable, Sendable {
self.urlString = url.absoluteString
}

/// Initialize with string, returning nil if the URL is not https://domain or git@domain
///
/// The following URL are valid
/// e.g. https://github.com/apple/swift
/// e.g. git@github.com:apple/swift
public init?(absoluteString url: String) {
guard let regex = try? NSRegularExpression(pattern: "^(?:https://|git@)", options: .caseInsensitive) else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use Swift regex literals instead?

return nil
}

if regex.firstMatch(in: url, options: [], range: NSRange(location: 0, length: url.utf16.count)) != nil {
self.init(url)
} else {
return nil
}
}

public var absoluteString: String {
return self.urlString
self.urlString
}

public var lastPathComponent: String {
return (self.urlString as NSString).lastPathComponent
(self.urlString as NSString).lastPathComponent
}

public var url: URL? {
return URL(string: self.urlString)
URL(string: self.urlString)
}
}

extension SourceControlURL: CustomStringConvertible {
public var description: String {
return self.urlString
self.urlString
}
}

extension SourceControlURL: ExpressibleByStringInterpolation {
}
extension SourceControlURL: ExpressibleByStringInterpolation {}

extension SourceControlURL: ExpressibleByStringLiteral {
}
extension SourceControlURL: ExpressibleByStringLiteral {}
119 changes: 86 additions & 33 deletions Sources/Commands/PackageCommands/AddDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import Workspace
extension SwiftPackageCommand {
struct AddDependency: SwiftCommand {
package static let configuration = CommandConfiguration(
abstract: "Add a package dependency to the manifest")
abstract: "Add a package dependency to the manifest"
)

@Argument(help: "The URL or directory of the package to add")
var dependency: String
Expand Down Expand Up @@ -52,31 +53,56 @@ extension SwiftPackageCommand {

func run(_ swiftCommandState: SwiftCommandState) throws {
let workspace = try swiftCommandState.getActiveWorkspace()

guard let packagePath = try swiftCommandState.getWorkspaceRoot().packages.first else {
throw StringError("unknown package")
}

// Load the manifest file
let fileSystem = workspace.fileSystem
let manifestPath = packagePath.appending("Package.swift")
let manifestContents: ByteString
do {
manifestContents = try fileSystem.readFileContents(manifestPath)
} catch {
throw StringError("cannot find package manifest in \(manifestPath)")
if let url = SourceControlURL(absoluteString: dependency) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have a CanonicalPackageURL that @francescomikulis is changing a little in #7741. What if instead of the new SourceControlURL here, we use CanonicalPackageURL and then check if the scheme is either file or nil?

try self.createSourceControlPackage(
packagePath: packagePath,
workspace: workspace,
url: url
)
} else if let path = try? Basics.AbsolutePath(validating: dependency) {
try self.createFileSystemPackage(
packagePath: packagePath,
workspace: workspace,
path: path
)
} else if let _ = try? Basics.RelativePath(validating: dependency) {
throw StringError("relative paths not supported")
} else {
throw StringError("must provide valid url or path")
}
}

// Parse the manifest.
let manifestSyntax = manifestContents.withData { data in
data.withUnsafeBytes { buffer in
buffer.withMemoryRebound(to: UInt8.self) { buffer in
Parser.parse(source: buffer)
}
}
}
private func createFileSystemPackage(
packagePath: Basics.AbsolutePath,
workspace: Workspace,
path: Basics.AbsolutePath
) throws {
let identity = PackageIdentity(path: path)
let packageDependency: PackageDependency = .fileSystem(
identity: identity,
nameForTargetDependencyResolutionOnly: nil,
path: path,
productFilter: .everything,
traits: []
)

try applyEdits(
packagePath: packagePath,
workspace: workspace,
packageDependency: packageDependency
)
}

let identity = PackageIdentity(url: .init(dependency))
private func createSourceControlPackage(
packagePath: Basics.AbsolutePath,
workspace: Workspace,
url: SourceControlURL
) throws {
let identity = PackageIdentity(url: url)

// Collect all of the possible version requirements.
var requirements: [PackageDependency.SourceControl.Requirement] = []
Expand All @@ -101,45 +127,72 @@ extension SwiftPackageCommand {
}

if requirements.count > 1 {
throw StringError("must specify at most one of --exact, --branch, --revision, --from, or --up-to-next-minor-from")
throw StringError(
"must specify at most one of --exact, --branch, --revision, --from, or --up-to-next-minor-from"
)
}

guard let firstRequirement = requirements.first else {
throw StringError("must specify one of --exact, --branch, --revision, --from, or --up-to-next-minor-from")
throw StringError(
"must specify one of --exact, --branch, --revision, --from, or --up-to-next-minor-from"
)
}

let requirement: PackageDependency.SourceControl.Requirement
if case .range(let range) = firstRequirement {
if let to {
requirement = .range(range.lowerBound..<to)
requirement = .range(range.lowerBound ..< to)
} else {
requirement = .range(range)
}
} else {
requirement = firstRequirement

if to != nil {
if self.to != nil {
throw StringError("--to can only be specified with --from or --up-to-next-minor-from")
}
}

// Figure out the location of the package.
let location: PackageDependency.SourceControl.Location
if let path = try? Basics.AbsolutePath(validating: dependency) {
location = .local(path)
} else {
location = .remote(.init(dependency))
}

let packageDependency: PackageDependency = .sourceControl(
identity: identity,
nameForTargetDependencyResolutionOnly: nil,
location: location,
location: .remote(url),
requirement: requirement,
productFilter: .everything,
traits: []
)

try applyEdits(
packagePath: packagePath,
workspace: workspace,
packageDependency: packageDependency
)
}

private func applyEdits(
packagePath: Basics.AbsolutePath,
workspace: Workspace,
packageDependency: PackageDependency
) throws {
// Load the manifest file
let fileSystem = workspace.fileSystem
let manifestPath = packagePath.appending(component: Manifest.filename)
let manifestContents: ByteString
do {
manifestContents = try fileSystem.readFileContents(manifestPath)
} catch {
throw StringError("cannot find package manifest in \(manifestPath)")
}

// Parse the manifest.
let manifestSyntax = manifestContents.withData { data in
data.withUnsafeBytes { buffer in
buffer.withMemoryRebound(to: UInt8.self) { buffer in
Parser.parse(source: buffer)
}
}
}

let editResult = try AddPackageDependency.addPackageDependency(
packageDependency,
to: manifestSyntax
Expand All @@ -149,7 +202,7 @@ extension SwiftPackageCommand {
to: fileSystem,
manifest: manifestSyntax,
manifestPath: manifestPath,
verbose: !globalOptions.logging.quiet
verbose: !self.globalOptions.logging.quiet
)
}
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/PackageModelSyntax/PackageDependency+Syntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

import Basics
import PackageModel
import SwiftSyntax
import SwiftParser
import SwiftSyntax
import struct TSCUtility.Version

extension PackageDependency: ManifestSyntaxRepresentable {
Expand All @@ -28,7 +28,7 @@ extension PackageDependency: ManifestSyntaxRepresentable {

extension PackageDependency.FileSystem: ManifestSyntaxRepresentable {
func asSyntax() -> ExprSyntax {
fatalError()
".package(path: \(literal: path.description))"
}
}

Expand All @@ -37,8 +37,8 @@ extension PackageDependency.SourceControl: ManifestSyntaxRepresentable {
// TODO: Not handling identity, nameForTargetDependencyResolutionOnly,
// or productFilter yet.
switch location {
case .local(let path):
".package(path: \(literal: path.description), \(requirement.asSyntax()))"
case .local:
fatalError()
case .remote(let url):
".package(url: \(literal: url.description), \(requirement.asSyntax()))"
}
Expand Down Expand Up @@ -88,6 +88,6 @@ extension PackageDependency.SourceControl.Requirement: ManifestSyntaxRepresentab

extension Version: ManifestSyntaxRepresentable {
func asSyntax() -> ExprSyntax {
return "\(literal: description)"
"\(literal: description)"
}
}
53 changes: 50 additions & 3 deletions Tests/CommandsTests/PackageCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,47 @@ final class PackageCommandTests: CommandsTestCase {
}
}

func testPackageAddDependency() async throws {
func testPackageAddDependency_URL() async throws {
try await testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("PackageB")
try fs.createDirectory(path)

try fs.writeFileContents(
path.appending("Package.swift"),
string:
"""
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "client",
targets: [ .target(name: "client", dependencies: [ "library" ]) ]
)
"""
)

_ = try await self.execute(
[
"add-dependency",
"https://github.com/swiftlang/swift-syntax.git",
"--branch",
"main",
],
packagePath: path
)

let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let contents: String = try fs.readFileContents(manifest)

XCTAssertMatch(
contents,
.contains(#".package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"),"#)
)
}
}

func testPackageAddDependency_absolutePath() async throws {
try await testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("PackageB")
Expand All @@ -811,16 +851,23 @@ final class PackageCommandTests: CommandsTestCase {
"""
)

_ = try await execute(["add-dependency", "--branch", "main", "https://github.com/swiftlang/swift-syntax.git"], packagePath: path)
_ = try await execute(
[
"add-dependency",
"/directory"
],
packagePath: path
)

let manifest = path.appending("Package.swift")
XCTAssertFileExists(manifest)
let contents: String = try fs.readFileContents(manifest)

XCTAssertMatch(contents, .contains(#".package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"),"#))
XCTAssertMatch(contents, .contains(#".package(path: "/directory"),"#))
}
}


func testPackageAddTarget() async throws {
try await testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
Expand Down