Skip to content

Commit 3d215a5

Browse files
authored
consolidate and improve manifest validation (#4203)
1 parent 9e02bd6 commit 3d215a5

17 files changed

+570
-532
lines changed

Sources/Basics/Observability.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ public protocol DiagnosticsEmitterProtocol {
160160
}
161161

162162
extension DiagnosticsEmitterProtocol {
163+
public func emit(_ diagnostics: [Diagnostic]) {
164+
for diagnostic in diagnostics {
165+
self.emit(diagnostic)
166+
}
167+
}
168+
163169
public func emit(severity: Diagnostic.Severity, message: String, metadata: ObservabilityMetadata? = .none) {
164170
self.emit(.init(severity: severity, message: message, metadata: metadata))
165171
}

Sources/PackageLoading/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ add_library(PackageLoading
1111
Diagnostics.swift
1212
IdentityResolver.swift
1313
ManifestLoader.swift
14+
ManifestLoader+Validation.swift
1415
ModuleMapGenerator.swift
1516
PackageBuilder.swift
1617
ManifestJSONParser.swift
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Basics
14+
import Foundation
15+
import PackageModel
16+
import TSCBasic
17+
18+
public struct ManifestValidator {
19+
static var supportedLocalBinaryDependencyExtensions: [String] {
20+
["zip"] + BinaryTarget.Kind.allCases.filter{ $0 != .unknown }.map { $0.fileExtension }
21+
}
22+
static var supportedRemoteBinaryDependencyExtensions: [String] {
23+
["zip", "artifactbundleindex"]
24+
}
25+
26+
private let manifest: Manifest
27+
private let sourceControlValidator: ManifestSourceControlValidator
28+
private let fileSystem: FileSystem
29+
30+
public init(manifest: Manifest, sourceControlValidator: ManifestSourceControlValidator, fileSystem: FileSystem) {
31+
self.manifest = manifest
32+
self.sourceControlValidator = sourceControlValidator
33+
self.fileSystem = fileSystem
34+
}
35+
36+
/// Validate the provided manifest.
37+
public func validate() -> [Basics.Diagnostic] {
38+
var diagnostics = [Basics.Diagnostic]()
39+
40+
diagnostics += self.validateTargets()
41+
diagnostics += self.validateProducts()
42+
diagnostics += self.validateDependencies()
43+
44+
// Checks reserved for tools version 5.2 features
45+
if self.manifest.toolsVersion >= .v5_2 {
46+
diagnostics += self.validateTargetDependencyReferences()
47+
diagnostics += self.validateBinaryTargets()
48+
}
49+
50+
return diagnostics
51+
}
52+
53+
private func validateTargets() -> [Basics.Diagnostic] {
54+
var diagnostics = [Basics.Diagnostic]()
55+
56+
let duplicateTargetNames = self.manifest.targets.map({ $0.name }).spm_findDuplicates()
57+
for name in duplicateTargetNames {
58+
diagnostics.append(.duplicateTargetName(targetName: name))
59+
}
60+
61+
return diagnostics
62+
}
63+
64+
private func validateProducts() -> [Basics.Diagnostic] {
65+
var diagnostics = [Basics.Diagnostic]()
66+
67+
for product in self.manifest.products {
68+
// Check that the product contains targets.
69+
guard !product.targets.isEmpty else {
70+
diagnostics.append(.emptyProductTargets(productName: product.name))
71+
continue
72+
}
73+
74+
// Check that the product references existing targets.
75+
for target in product.targets {
76+
if !self.manifest.targetMap.keys.contains(target) {
77+
diagnostics.append(.productTargetNotFound(productName: product.name, targetName: target, validTargets: self.manifest.targetMap.keys.sorted()))
78+
}
79+
}
80+
81+
// Check that products that reference only binary targets don't define a type.
82+
let areTargetsBinary = product.targets.allSatisfy { self.manifest.targetMap[$0]?.type == .binary }
83+
if areTargetsBinary && product.type != .library(.automatic) {
84+
diagnostics.append(.invalidBinaryProductType(productName: product.name))
85+
}
86+
}
87+
88+
return diagnostics
89+
}
90+
91+
private func validateDependencies() -> [Basics.Diagnostic] {
92+
var diagnostics = [Basics.Diagnostic]()
93+
94+
// validate dependency requirements
95+
for dependency in self.manifest.dependencies {
96+
switch dependency {
97+
case .sourceControl(let sourceControl):
98+
diagnostics += validateSourceControlDependency(sourceControl)
99+
default:
100+
break
101+
}
102+
}
103+
104+
return diagnostics
105+
}
106+
107+
private func validateBinaryTargets() -> [Basics.Diagnostic] {
108+
var diagnostics = [Basics.Diagnostic]()
109+
110+
// Check that binary targets point to the right file type.
111+
for target in self.manifest.targets where target.type == .binary {
112+
if target.isLocal {
113+
guard let path = target.path else {
114+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
115+
continue
116+
}
117+
118+
guard let path = path.spm_chuzzle(), !path.isEmpty else {
119+
diagnostics.append(.invalidLocalBinaryPath(path: path, targetName: target.name))
120+
continue
121+
}
122+
123+
guard let relativePath = try? RelativePath(validating: path) else {
124+
diagnostics.append(.invalidLocalBinaryPath(path: path, targetName: target.name))
125+
continue
126+
}
127+
128+
let validExtensions = Self.supportedLocalBinaryDependencyExtensions
129+
guard let fileExtension = relativePath.extension, validExtensions.contains(fileExtension) else {
130+
diagnostics.append(.unsupportedBinaryLocationExtension(
131+
targetName: target.name,
132+
validExtensions: validExtensions
133+
))
134+
continue
135+
}
136+
} else if target.isRemote {
137+
guard let url = target.url else {
138+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
139+
continue
140+
}
141+
142+
guard let url = url.spm_chuzzle(), !url.isEmpty else {
143+
diagnostics.append(.invalidBinaryURL(url: url, targetName: target.name))
144+
continue
145+
}
146+
147+
guard let url = URL(string: url) else {
148+
diagnostics.append(.invalidBinaryURL(url: url, targetName: target.name))
149+
continue
150+
}
151+
152+
let validSchemes = ["https"]
153+
guard url.scheme.map({ validSchemes.contains($0) }) ?? false else {
154+
diagnostics.append(.invalidBinaryURLScheme(
155+
targetName: target.name,
156+
validSchemes: validSchemes
157+
))
158+
continue
159+
}
160+
161+
guard Self.supportedRemoteBinaryDependencyExtensions.contains(url.pathExtension) else {
162+
diagnostics.append(.unsupportedBinaryLocationExtension(
163+
targetName: target.name,
164+
validExtensions: Self.supportedRemoteBinaryDependencyExtensions
165+
))
166+
continue
167+
}
168+
169+
} else {
170+
diagnostics.append(.invalidBinaryLocation(targetName: target.name))
171+
continue
172+
}
173+
}
174+
175+
return diagnostics
176+
}
177+
178+
/// Validates that product target dependencies reference an existing package.
179+
private func validateTargetDependencyReferences() -> [Basics.Diagnostic] {
180+
var diagnostics = [Basics.Diagnostic]()
181+
182+
for target in self.manifest.targets {
183+
for targetDependency in target.dependencies {
184+
switch targetDependency {
185+
case .target:
186+
// If this is a target dependency, we don't need to check anything.
187+
break
188+
case .product(_, let packageName, _, _):
189+
if self.manifest.packageDependency(referencedBy: targetDependency) == nil {
190+
diagnostics.append(.unknownTargetPackageDependency(
191+
packageName: packageName ?? "unknown package name",
192+
targetName: target.name,
193+
validPackages: self.manifest.dependencies.map { $0.nameForTargetDependencyResolutionOnly }
194+
))
195+
}
196+
case .byName(let name, _):
197+
// Don't diagnose root manifests so we can emit a better diagnostic during package loading.
198+
if !self.manifest.packageKind.isRoot &&
199+
!self.manifest.targetMap.keys.contains(name) &&
200+
self.manifest.packageDependency(referencedBy: targetDependency) == nil
201+
{
202+
diagnostics.append(.unknownTargetDependency(
203+
dependency: name,
204+
targetName: target.name,
205+
validDependencies: self.manifest.dependencies.map { $0.nameForTargetDependencyResolutionOnly }
206+
))
207+
}
208+
}
209+
}
210+
}
211+
212+
return diagnostics
213+
}
214+
215+
func validateSourceControlDependency(_ dependency: PackageDependency.SourceControl) -> [Basics.Diagnostic] {
216+
var diagnostics = [Basics.Diagnostic]()
217+
218+
// validate source control ref
219+
switch dependency.requirement {
220+
case .branch(let name):
221+
if !self.sourceControlValidator.isValidRefFormat(name) {
222+
diagnostics.append(.invalidSourceControlBranchName(name))
223+
}
224+
case .revision(let revision):
225+
if !self.sourceControlValidator.isValidRefFormat(revision) {
226+
diagnostics.append(.invalidSourceControlRevision(revision))
227+
}
228+
default:
229+
break
230+
}
231+
// if a location is on file system, validate it is in fact a git repo
232+
// there is a case to be made to throw early (here) if the path does not exists
233+
// but many of our tests assume they can pass a non existent path
234+
if case .local(let localPath) = dependency.location, self.fileSystem.exists(localPath) {
235+
if !self.sourceControlValidator.isValidDirectory(localPath) {
236+
// Provides better feedback when mistakingly using url: for a dependency that
237+
// is a local package. Still allows for using url with a local package that has
238+
// also been initialized by git
239+
diagnostics.append(.invalidSourceControlDirectory(localPath))
240+
}
241+
}
242+
return diagnostics
243+
}
244+
}
245+
246+
public protocol ManifestSourceControlValidator {
247+
func isValidRefFormat(_ revision: String) -> Bool
248+
func isValidDirectory(_ path: AbsolutePath) -> Bool
249+
}
250+
251+
extension Basics.Diagnostic {
252+
static func duplicateTargetName(targetName: String) -> Self {
253+
.error("duplicate target named '\(targetName)'")
254+
}
255+
256+
static func emptyProductTargets(productName: String) -> Self {
257+
.error("product '\(productName)' doesn't reference any targets")
258+
}
259+
260+
static func productTargetNotFound(productName: String, targetName: String, validTargets: [String]) -> Self {
261+
.error("target '\(targetName)' referenced in product '\(productName)' could not be found; valid targets are: '\(validTargets.joined(separator: "', '"))'")
262+
}
263+
264+
static func invalidBinaryProductType(productName: String) -> Self {
265+
.error("invalid type for binary product '\(productName)'; products referencing only binary targets must have a type of 'library'")
266+
}
267+
268+
/*static func duplicateDependency(dependencyIdentity: PackageIdentity) -> Self {
269+
.error("duplicate dependency '\(dependencyIdentity)'")
270+
}
271+
272+
static func duplicateDependencyName(dependencyName: String) -> Self {
273+
.error("duplicate dependency named '\(dependencyName)'; consider differentiating them using the 'name' argument")
274+
}*/
275+
276+
static func unknownTargetDependency(dependency: String, targetName: String, validDependencies: [String]) -> Self {
277+
.error("unknown dependency '\(dependency)' in target '\(targetName)'; valid dependencies are: '\(validDependencies.joined(separator: "', '"))'")
278+
}
279+
280+
static func unknownTargetPackageDependency(packageName: String, targetName: String, validPackages: [String]) -> Self {
281+
.error("unknown package '\(packageName)' in dependencies of target '\(targetName)'; valid packages are: '\(validPackages.joined(separator: "', '"))'")
282+
}
283+
284+
static func invalidBinaryLocation(targetName: String) -> Self {
285+
.error("invalid location for binary target '\(targetName)'")
286+
}
287+
288+
static func invalidBinaryURL(url: String, targetName: String) -> Self {
289+
.error("invalid URL '\(url)' for binary target '\(targetName)'")
290+
}
291+
292+
static func invalidLocalBinaryPath(path: String, targetName: String) -> Self {
293+
.error("invalid local path '\(path)' for binary target '\(targetName)', path expected to be relative to package root.")
294+
}
295+
296+
static func invalidBinaryURLScheme(targetName: String, validSchemes: [String]) -> Self {
297+
.error("invalid URL scheme for binary target '\(targetName)'; valid schemes are: '\(validSchemes.joined(separator: "', '"))'")
298+
}
299+
300+
static func unsupportedBinaryLocationExtension(targetName: String, validExtensions: [String]) -> Self {
301+
.error("unsupported extension for binary target '\(targetName)'; valid extensions are: '\(validExtensions.joined(separator: "', '"))'")
302+
}
303+
304+
static func invalidLanguageTag(_ languageTag: String) -> Self {
305+
.error("""
306+
invalid language tag '\(languageTag)'; the pattern for language tags is groups of latin characters and \
307+
digits separated by hyphens
308+
""")
309+
}
310+
311+
static func invalidSourceControlBranchName(_ name: String) -> Self {
312+
.error("invalid branch name: '\(name)'")
313+
}
314+
315+
static func invalidSourceControlRevision(_ revision: String) -> Self {
316+
.error("invalid revision: '\(revision)'")
317+
}
318+
319+
static func invalidSourceControlDirectory(_ path: AbsolutePath) -> Self {
320+
.error("cannot clone from local directory \(path)\nPlease git init or use \"path:\" for \(path)")
321+
}
322+
}
323+
324+
extension TargetDescription {
325+
fileprivate var isRemote: Bool { url != nil }
326+
fileprivate var isLocal: Bool { path != nil }
327+
}

0 commit comments

Comments
 (0)