Skip to content

Commit 2e0c761

Browse files
neonichuMaxDesiatov
authored andcommitted
Apply mirrors to root dependencies (#6765)
We weren't applying mirrors to root dependencies so far. rdar://110869499
1 parent 01203f6 commit 2e0c761

18 files changed

+368
-206
lines changed

Sources/PackageGraph/PackageGraphRoot.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,39 @@ public struct PackageGraphRoot {
4646
return self.packages.values.map { $0.reference }
4747
}
4848

49+
private let _dependencies: [PackageDependency]
50+
4951
/// The top level dependencies.
50-
public let dependencies: [PackageDependency]
52+
public var dependencies: [PackageDependency] {
53+
guard let dependencyMapper else {
54+
return self._dependencies
55+
}
56+
57+
return self._dependencies.map { dependency in
58+
do {
59+
return try dependencyMapper.mappedDependency(for: dependency, fileSystem: localFileSystem)
60+
} catch {
61+
observabilityScope.emit(warning: "could not map dependency \(dependency.identity): \(error.interpolationDescription)")
62+
return dependency
63+
}
64+
}
65+
}
66+
67+
private let dependencyMapper: DependencyMapper?
68+
private let observabilityScope: ObservabilityScope
5169

5270
/// Create a package graph root.
5371
/// Note this quietly skip inputs for which manifests are not found. this could be because the manifest failed to load or for some other reasons
5472
// FIXME: This API behavior wrt to non-found manifests is fragile, but required by IDEs
5573
// it may lead to incorrect assumption in downstream code which may expect an error if a manifest was not found
5674
// we should refactor this API to more clearly return errors for inputs that do not have a corresponding manifest
57-
public init(input: PackageGraphRootInput, manifests: [AbsolutePath: Manifest], explicitProduct: String? = nil) {
75+
public init(
76+
input: PackageGraphRootInput,
77+
manifests: [AbsolutePath: Manifest],
78+
explicitProduct: String? = nil,
79+
dependencyMapper: DependencyMapper? = nil,
80+
observabilityScope: ObservabilityScope
81+
) {
5882
self.packages = input.packages.reduce(into: .init(), { partial, inputPath in
5983
if let manifest = manifests[inputPath] {
6084
let packagePath = manifest.path.parentDirectory
@@ -77,7 +101,9 @@ public struct PackageGraphRoot {
77101
}
78102
}
79103

80-
self.dependencies = adjustedDependencies
104+
self._dependencies = adjustedDependencies
105+
self.dependencyMapper = dependencyMapper
106+
self.observabilityScope = observabilityScope
81107
}
82108

83109
/// Returns the constraints imposed by root manifests + dependencies.

Sources/PackageLoading/ManifestJSONParser.swift

Lines changed: 44 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import struct TSCBasic.StringError
2626
import struct TSCUtility.Version
2727

2828
enum ManifestJSONParser {
29-
private static let filePrefix = "file://"
30-
3129
struct Input: Codable {
3230
let package: Serialization.Package
3331
let errors: [String]
@@ -56,6 +54,7 @@ enum ManifestJSONParser {
5654
toolsVersion: ToolsVersion,
5755
packageKind: PackageReference.Kind,
5856
identityResolver: IdentityResolver,
57+
dependencyMapper: DependencyMapper,
5958
fileSystem: FileSystem
6059
) throws -> ManifestJSONParser.Result {
6160
let decoder = JSONDecoder.makeWithDefaults()
@@ -84,6 +83,7 @@ enum ManifestJSONParser {
8483
toolsVersion: toolsVersion,
8584
packageKind: packageKind,
8685
identityResolver: identityResolver,
86+
dependencyMapper: dependencyMapper,
8787
fileSystem: fileSystem
8888
)
8989
}
@@ -148,180 +148,56 @@ enum ManifestJSONParser {
148148
toolsVersion: ToolsVersion,
149149
packageKind: PackageReference.Kind,
150150
identityResolver: IdentityResolver,
151+
dependencyMapper: DependencyMapper,
151152
fileSystem: FileSystem
152153
) throws -> PackageDependency {
153154
switch dependency.kind {
154155
case .registry(let identity, let requirement):
155-
return try Self.parseRegistryDependency(
156-
identity: .plain(identity),
157-
requirement: .init(requirement),
158-
identityResolver: identityResolver
159-
)
160-
case .sourceControl(let name, let location, let requirement):
161-
return try Self.parseSourceControlDependency(
162-
packageKind: packageKind,
163-
at: location,
164-
name: name,
165-
requirement: .init(requirement),
166-
identityResolver: identityResolver,
167-
fileSystem: fileSystem
168-
)
169-
case .fileSystem(let name, let path):
170-
return try Self.parseFileSystemDependency(
171-
packageKind: packageKind,
172-
at: path,
173-
name: name,
174-
identityResolver: identityResolver,
175-
fileSystem: fileSystem
176-
)
177-
}
178-
}
179-
180-
private static func parseFileSystemDependency(
181-
packageKind: PackageReference.Kind,
182-
at location: String,
183-
name: String?,
184-
identityResolver: IdentityResolver,
185-
fileSystem: FileSystem
186-
) throws -> PackageDependency {
187-
let location = try sanitizeDependencyLocation(fileSystem: fileSystem, packageKind: packageKind, dependencyLocation: location)
188-
let path: AbsolutePath
189-
do {
190-
path = try AbsolutePath(validating: location)
191-
} catch PathValidationError.invalidAbsolutePath(let path) {
192-
throw ManifestParseError.invalidManifestFormat("'\(path)' is not a valid path for path-based dependencies; use relative or absolute path instead.", diagnosticFile: nil, compilerCommandLine: nil)
193-
}
194-
let identity = try identityResolver.resolveIdentity(for: path)
195-
return .fileSystem(identity: identity,
196-
nameForTargetDependencyResolutionOnly: name,
197-
path: path,
198-
productFilter: .everything)
199-
}
200-
201-
private static func parseSourceControlDependency(
202-
packageKind: PackageReference.Kind,
203-
at location: String,
204-
name: String?,
205-
requirement: PackageDependency.SourceControl.Requirement,
206-
identityResolver: IdentityResolver,
207-
fileSystem: FileSystem
208-
) throws -> PackageDependency {
209-
// cleans up variants of path based location
210-
var location = try sanitizeDependencyLocation(fileSystem: fileSystem, packageKind: packageKind, dependencyLocation: location)
211-
// location mapping (aka mirrors) if any
212-
location = identityResolver.mappedLocation(for: location)
213-
if PackageIdentity.plain(location).isRegistry {
214-
// re-mapped to registry
215-
let identity = PackageIdentity.plain(location)
216-
let registryRequirement: PackageDependency.Registry.Requirement
217-
switch requirement {
218-
case .branch, .revision:
219-
throw StringError("invalid mapping of source control to registry, requirement information mismatch: cannot map branch or revision based dependencies to registry.")
220-
case .exact(let value):
221-
registryRequirement = .exact(value)
222-
case .range(let value):
223-
registryRequirement = .range(value)
224-
}
225-
return .registry(
226-
identity: identity,
227-
requirement: registryRequirement,
228-
productFilter: .everything
229-
)
230-
} else if let localPath = try? AbsolutePath(validating: location) {
231-
// a package in a git location, may be a remote URL or on disk
232-
// in the future this will check with the registries for the identity of the URL
233-
let identity = try identityResolver.resolveIdentity(for: localPath)
234-
return .localSourceControl(
235-
identity: identity,
236-
nameForTargetDependencyResolutionOnly: name,
237-
path: localPath,
238-
requirement: requirement,
239-
productFilter: .everything
240-
)
241-
} else {
242-
let url = SourceControlURL(location)
243-
// in the future this will check with the registries for the identity of the URL
244-
let identity = try identityResolver.resolveIdentity(for: url)
245-
return .remoteSourceControl(
246-
identity: identity,
247-
nameForTargetDependencyResolutionOnly: name,
248-
url: url,
249-
requirement: requirement,
250-
productFilter: .everything
251-
)
252-
}
253-
}
254-
255-
private static func parseRegistryDependency(
256-
identity: PackageIdentity,
257-
requirement: PackageDependency.Registry.Requirement,
258-
identityResolver: IdentityResolver
259-
) throws -> PackageDependency {
260-
// location mapping (aka mirrors) if any
261-
let location = identityResolver.mappedLocation(for: identity.description)
262-
if PackageIdentity.plain(location).isRegistry {
263-
// re-mapped to registry
264-
let identity = PackageIdentity.plain(location)
265-
return .registry(
266-
identity: identity,
267-
requirement: requirement,
268-
productFilter: .everything
269-
)
270-
} else if let url = URL(string: location){
271-
let SourceControlURL = SourceControlURL(url)
272-
// in the future this will check with the registries for the identity of the URL
273-
let identity = try identityResolver.resolveIdentity(for: SourceControlURL)
274-
let sourceControlRequirement: PackageDependency.SourceControl.Requirement
275-
switch requirement {
276-
case .exact(let value):
277-
sourceControlRequirement = .exact(value)
278-
case .range(let value):
279-
sourceControlRequirement = .range(value)
156+
do {
157+
return try dependencyMapper.mappedDependency(
158+
packageKind: .registry(.plain(identity)),
159+
at: identity,
160+
nameForTargetDependencyResolutionOnly: nil,
161+
requirement: .init(requirement),
162+
productFilter: .everything,
163+
fileSystem: fileSystem
164+
)
165+
} catch let error as TSCBasic.PathValidationError {
166+
throw error
167+
} catch {
168+
throw ManifestParseError.invalidManifestFormat("\(error.interpolationDescription)", diagnosticFile: nil, compilerCommandLine: nil)
280169
}
281-
return .remoteSourceControl(
282-
identity: identity,
283-
nameForTargetDependencyResolutionOnly: identity.description,
284-
url: SourceControlURL,
285-
requirement: sourceControlRequirement,
286-
productFilter: .everything
287-
)
288-
} else {
289-
throw StringError("invalid location: \(location)")
290-
}
291-
}
292-
293-
private static func sanitizeDependencyLocation(fileSystem: FileSystem, packageKind: PackageReference.Kind, dependencyLocation: String) throws -> String {
294-
if dependencyLocation.hasPrefix("~/") {
295-
// If the dependency URL starts with '~/', try to expand it.
296-
return try AbsolutePath(validating: String(dependencyLocation.dropFirst(2)), relativeTo: fileSystem.homeDirectory).pathString
297-
} else if dependencyLocation.hasPrefix(filePrefix) {
298-
// FIXME: SwiftPM can't handle file locations with file:// scheme so we need to
299-
// strip that. We need to design a Location data structure for SwiftPM.
300-
let location = String(dependencyLocation.dropFirst(filePrefix.count))
301-
let hostnameComponent = location.prefix(while: { $0 != "/" })
302-
guard hostnameComponent.isEmpty else {
303-
if hostnameComponent == ".." {
304-
throw ManifestParseError.invalidManifestFormat(
305-
"file:// URLs cannot be relative, did you mean to use '.package(path:)'?", diagnosticFile: nil, compilerCommandLine: nil
170+
case .sourceControl(let name, let location, let requirement):
171+
do {
172+
return try dependencyMapper.mappedDependency(
173+
packageKind: packageKind,
174+
at: location,
175+
nameForTargetDependencyResolutionOnly: name,
176+
requirement: .init(requirement),
177+
productFilter: .everything,
178+
fileSystem: fileSystem
306179
)
307-
}
308-
throw ManifestParseError.invalidManifestFormat(
309-
"file:// URLs with hostnames are not supported, are you missing a '/'?", diagnosticFile: nil, compilerCommandLine: nil
310-
)
180+
} catch let error as TSCBasic.PathValidationError {
181+
throw error
182+
} catch {
183+
throw ManifestParseError.invalidManifestFormat("\(error.interpolationDescription)", diagnosticFile: nil, compilerCommandLine: nil)
311184
}
312-
return try AbsolutePath(validating: location).pathString
313-
} else if parseScheme(dependencyLocation) == nil {
314-
// If the URL has no scheme, we treat it as a path (either absolute or relative to the base URL).
315-
switch packageKind {
316-
case .root(let packagePath), .fileSystem(let packagePath), .localSourceControl(let packagePath):
317-
return try AbsolutePath(validating: dependencyLocation, relativeTo: packagePath).pathString
318-
case .remoteSourceControl, .registry:
319-
// nothing to "fix"
320-
return dependencyLocation
185+
case .fileSystem(let name, let path):
186+
// FIXME: This case should really also be handled by `mappedDependency()` but that is currently impossible because `sanitizeDependencyLocation()` relies on the fact that we're calling it with an incorrect (file-system) `packageKind` for SCM-based dependencies, so we have no ability to distinguish between actual file-system dependencies and SCM-based ones without introducing some secondary package kind or other flag to pick the different behaviors. That seemed much worse than having this extra code path be here and `DefaultIdentityResolver.sanitizeDependencyLocation()` being public, but it should eventually be cleaned up. It seems to me as if that will mostly be a case of fixing the test suite to not rely on these fairly arbitrary behaviors.
187+
188+
// cleans up variants of path based location
189+
let location = try DefaultDependencyMapper.sanitizeDependencyLocation(fileSystem: fileSystem, packageKind: packageKind, dependencyLocation: path)
190+
let path: AbsolutePath
191+
do {
192+
path = try AbsolutePath(validating: location)
193+
} catch PathValidationError.invalidAbsolutePath(let path) {
194+
throw ManifestParseError.invalidManifestFormat("'\(path)' is not a valid path for path-based dependencies; use relative or absolute path instead.", diagnosticFile: nil, compilerCommandLine: nil)
321195
}
322-
} else {
323-
// nothing to "fix"
324-
return dependencyLocation
196+
let identity = try identityResolver.resolveIdentity(for: path)
197+
return .fileSystem(identity: identity,
198+
nameForTargetDependencyResolutionOnly: name,
199+
path: path,
200+
productFilter: .everything)
325201
}
326202
}
327203

@@ -392,33 +268,6 @@ enum ManifestJSONParser {
392268
return settings
393269
}
394270

395-
/// Parses the URL type of a git repository
396-
/// e.g. https://github.com/apple/swift returns "https"
397-
/// e.g. git@github.com:apple/swift returns "git"
398-
///
399-
/// This is *not* a generic URI scheme parser!
400-
private static func parseScheme(_ location: String) -> String? {
401-
func prefixOfSplitBy(_ delimiter: String) -> String? {
402-
let (head, tail) = location.spm_split(around: delimiter)
403-
if tail == nil {
404-
//not found
405-
return nil
406-
} else {
407-
//found, return head
408-
//lowercase the "scheme", as specified by the URI RFC (just in case)
409-
return head.lowercased()
410-
}
411-
}
412-
413-
for delim in ["://", "@"] {
414-
if let found = prefixOfSplitBy(delim), !found.contains("/") {
415-
return found
416-
}
417-
}
418-
419-
return nil
420-
}
421-
422271
/// Looks for Xcode-style build setting macros "$()".
423272
fileprivate static let invalidValueRegex = try! RegEx(pattern: #"(\$\(.*?\))"#)
424273
}

0 commit comments

Comments
 (0)