Skip to content
Open
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
78 changes: 77 additions & 1 deletion Sources/Commands/PackageCommands/AddDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ extension SwiftPackageCommand {
@Option(help: "Specify dependency type.")
var type: DependencyType = .url

@Option(name: .customLong("filter-manifests"), help: "Filter manifests by name pattern")
var manifestFilter: [String] = []

enum DependencyType: String, Codable, CaseIterable, ExpressibleByArgument {
case url
case path
Expand Down Expand Up @@ -231,14 +234,87 @@ extension SwiftPackageCommand {
)
}

private func findAllManifests(packagePath: Basics.AbsolutePath, fileSystem: Basics.FileSystem) -> [Basics.AbsolutePath] {
var manifests: [Basics.AbsolutePath] = []

// Add standard manifest if it exists
let standardManifest = packagePath.appending(component: Manifest.filename)
if fileSystem.isFile(standardManifest) {
manifests.append(standardManifest)
}

// Find version specific manifests
do {
let packageContents = try fileSystem.getDirectoryContents(packagePath)
let regexManifestFile = try! RegEx(pattern: #"^Package@swift-(\d+)(?:\.(\d+))?(?:\.(\d+))?.swift$"#)
Copy link
Member

Choose a reason for hiding this comment

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

question: Is there some reusable regex or function that can find all of the package manifests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, sadly there is not a reusable regex. Furthermore, the only function that is used for finding package manifests only returns one (which is required for the manifest loader, as you would not load multiple manifests)


for file in packageContents {
if regexManifestFile.matchGroups(in: file).first != nil {
manifests.append(packagePath.appending(component: file))
}
}
} catch {
// If we cannot read directory, just use standard manifest
}

// Filter manifests by name patterns if specified
if !manifestFilter.isEmpty {
manifests = manifests.filter { manifestPath in
let fileName = manifestPath.basename
return manifestFilter.contains { pattern in
fileName == pattern
}
}
}

return manifests
}

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 packageManifests = findAllManifests(packagePath: packagePath, fileSystem: workspace.fileSystem)

guard !packageManifests.isEmpty else {
throw StringError("cannot find package manifest in \(packagePath)")
}

var successCount = 0
var errors: [String] = []

for manifest in packageManifests {
do {
try applyEditsToSingleManifest(manifestPath: manifest, fileSystem: fileSystem, packageDependency: packageDependency)
successCount += 1
} catch {
// For single manifest scenarios, rethrow error
if packageManifests.count == 1 {
throw error
}
// For multiple manifests, collect error message
let errorMessage = "Failed to update \(manifest.basename)"
errors.append(errorMessage)
}
}

if successCount == 0 {
throw StringError("Failed to update any manifest files:\n" + errors.joined(separator: "\n"))
} else if !errors.isEmpty {
print("Successfully updated \(successCount)/\(packageManifests.count) manifest files")
print("Warnings/Errors occured:\n" + errors.joined(separator: "\n"))
}
}

private func applyEditsToSingleManifest(
manifestPath: Basics.AbsolutePath,
fileSystem: FileSystem,
packageDependency: PackageDependency
) throws {
// Load the manifest file
let manifestContents: ByteString
do {
manifestContents = try fileSystem.readFileContents(manifestPath)
Expand Down
199 changes: 187 additions & 12 deletions Tests/CommandsTests/PackageCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,34 @@ import enum TSCBasic.JSON
fileprivate func execute(
_ args: [String] = [],
packagePath: AbsolutePath? = nil,
manifest: String? = nil,
manifests: [String]? = nil,
env: Environment? = nil,
configuration: BuildConfiguration,
buildSystem: BuildSystemProvider.Kind
) async throws -> (stdout: String, stderr: String) {
var environment = env ?? [:]
if let manifest, let packagePath {
try localFileSystem.writeFileContents(packagePath.appending("Package.swift"), string: manifest)
if let manifests, let packagePath {
if manifests.count > 1 {
for (index, manifest) in manifests.enumerated() {

guard index != manifests.count - 1 else {
let marker = "// swift-tools-version: "
guard let markerRange = manifest.range(of: marker) else {
continue
}
let startIndex = markerRange.upperBound
let endIndex = manifest.index(startIndex, offsetBy: 3, limitedBy: manifest.endIndex) ?? manifest.endIndex

let toolsVersion = manifest[startIndex..<endIndex]
try localFileSystem.writeFileContents(packagePath.appending("Package@swift-\(toolsVersion).swift"), string: manifest)

continue
}
try localFileSystem.writeFileContents(packagePath.appending("Package.swift"), string: manifest)
}
} else {
try localFileSystem.writeFileContents(packagePath.appending("Package.swift"), string: manifests[0])
}
}

// don't ignore local packages when caching
Expand All @@ -66,14 +86,37 @@ private func expectManifest(_ packagePath: AbsolutePath, _ callback: (String) th
// Helper function to assert content exists in the manifest
private func expectManifestContains(_ packagePath: AbsolutePath, _ expected: String) throws {
try expectManifest(packagePath) { manifestContents in
#expect(manifestContents.contains(expected))
#expect(manifestContents.contains(expected),"expected: \(expected), and contents: \(manifestContents)")
}
}

// Helper function to check all manifest files in a package directory
private func expectAllManifests(_ packagePath: AbsolutePath, _ callback: ([String]) throws -> Void) throws {
let fs = localFileSystem
var manifestContents: [String] = []

let mainManifest = packagePath.appending("Package.swift")
if fs.exists(mainManifest) {
let contents: String = try fs.readFileContents(mainManifest)
manifestContents.append(contents)
}

let packageFiles = try fs.getDirectoryContents(packagePath)
let manifestFiles = packageFiles.filter { $0.hasPrefix("Package@swift-") && $0.hasSuffix(".swift") }

for manifestFile in manifestFiles.sorted() {
let manifestPath = packagePath.appending(manifestFile)
let contents: String = try fs.readFileContents(manifestPath)
manifestContents.append(contents)
}

try callback(manifestContents)
}

// Helper function to test adding a URL dependency and asserting the result
private func executeAddURLDependencyAndAssert(
packagePath: AbsolutePath,
initialManifest: String? = nil,
initialManifests: [String]? = nil,
url: String,
requirementArgs: [String],
expectedManifestString: String,
Expand All @@ -82,7 +125,7 @@ private func executeAddURLDependencyAndAssert(
_ = try await execute(
["add-dependency", url] + requirementArgs,
packagePath: packagePath,
manifest: initialManifest,
manifests: initialManifests,
configuration: buildData.config,
buildSystem: buildData.buildSystem,
)
Expand Down Expand Up @@ -2005,6 +2048,138 @@ struct PackageCommandTests {
}
}

@Test(
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
)
func packageAddDependencyToMultipleManifests(
data: BuildData,
) async throws {
try await testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("PackageA")
try fs.createDirectory(path)

let url = "https://github.com/swiftlang/swift-syntax.git"

let manifestA = """
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "client",
dependencies: [
.package(url: "\(url)", exact: "601.0.1"),
],
targets: [ .target(name: "client", dependencies: [ "library" ]) ]
)
"""
let manifestB = """
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "client",
dependencies: [
.package(url: "\(url)", exact: "601.0.1"),
],
targets: [ .target(name: "client", dependencies: [ "library" ]) ]
)
"""

let expected =
#".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"),"#

let manifests = [manifestA, manifestB]
try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifests: manifests,
url: url,
requirementArgs: ["--exact", "601.0.1"],
expectedManifestString: expected,
buildData: data,
)

try expectAllManifests(path) { manifestContents in
#expect(manifestContents.count == 2)
for manifest in manifestContents {
let components = manifest.components(separatedBy: expected)
#expect(components.count == 2, "Expected dependency to appear exactly once in each manifest, but found \(components.count - 1) occurrences")
}
}

}
}

@Test(
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
)
func packageAddDependencyToMultipleManifestsWithFilter(
data: BuildData,
) async throws {
try await testWithTemporaryDirectory { tmpPath in
let fs = localFileSystem
let path = tmpPath.appending("PackageA")
try fs.createDirectory(path)

let url = "https://github.com/swiftlang/swift-syntax.git"

let manifestA = """
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "client",
dependencies: [
.package(url: "\(url)", exact: "601.0.1"),
],
targets: [ .target(name: "client", dependencies: [ "library" ]) ]
)
"""
let manifestB = """
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "client",
dependencies: [
.package(url: "\(url)", exact: "601.0.1"),
],
targets: [ .target(name: "client", dependencies: [ "library" ]) ]
)
"""

let expected =
#".package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"),"#

let manifests = [manifestA, manifestB]
try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifests: manifests,
url: url,
requirementArgs: ["--exact", "601.0.1", "--filter-manifests", "Package@swift-6.1.swift"],
expectedManifestString: expected,
buildData: data,
)

try expectAllManifests(path) { manifestContents in
#expect(manifestContents.count == 2)

var mainManifestCount = 0
var filteredManifestCount = 0

for manifest in manifestContents {
let components = manifest.components(separatedBy: expected)
if manifest.contains("// swift-tools-version: 5.9") {
mainManifestCount = components.count - 1
} else if manifest.contains("// swift-tools-version: 6.1") {
filteredManifestCount = components.count - 1
}
}

#expect(mainManifestCount == 1, "Main manifest (5.9) should have exactly 1 dependency occurrence")
#expect(filteredManifestCount == 1, "Filtered manifest (6.1) should have exactly 1 dependency occurrence")
}

}
}


@Test(
arguments: getBuildData(for: SupportedBuildSystemOnAllPlatforms),
)
Expand Down Expand Up @@ -2033,7 +2208,7 @@ struct PackageCommandTests {

try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: url,
requirementArgs: ["--exact", "601.0.1"],
expectedManifestString: expected,
Expand Down Expand Up @@ -2074,7 +2249,7 @@ struct PackageCommandTests {
let expected = #".package(path: "../foo")"#
try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: depPath,
requirementArgs: ["--type", "path"],
expectedManifestString: expected,
Expand Down Expand Up @@ -2115,7 +2290,7 @@ struct PackageCommandTests {
let expected = #".package(id: "foo", exact: "1.0.0")"#
try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: registryId,
requirementArgs: ["--type", "registry", "--exact", "1.0.0"],
expectedManifestString: expected,
Expand Down Expand Up @@ -2215,7 +2390,7 @@ struct PackageCommandTests {

try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: testData.url,
requirementArgs: testData.requirementArgs,
expectedManifestString: testData.expectedManifestString,
Expand Down Expand Up @@ -2260,7 +2435,7 @@ struct PackageCommandTests {

try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: testData.url,
requirementArgs: testData.requirementArgs,
expectedManifestString: testData.expectedManifestString,
Expand Down Expand Up @@ -2325,7 +2500,7 @@ struct PackageCommandTests {
"""
try await executeAddURLDependencyAndAssert(
packagePath: path,
initialManifest: manifest,
initialManifests: [manifest],
url: testData.url,
requirementArgs: testData.requirementArgs,
expectedManifestString: testData.expectedManifestString,
Expand Down