Skip to content

Commit 04dafa1

Browse files
committed
WIP: Ability to write out package manifests as source code, providing more flexibility for things like mechanical package manifest generation.
Note that this is _not_ suitable for editing the package manifest, since that would need to preserve whitespace and comments. Rather, this is for things like automatically generating packages from test sets, or for generating the initial configuration of packages (e.g. a more flexible `package init`). This is still a WIP, and still needs support for: - resources - build settings - binary targets Additionally, there are FIXMEs to resolve and many more unit tests to add. rdar://70271810
1 parent 90ff0b7 commit 04dafa1

File tree

2 files changed

+407
-0
lines changed

2 files changed

+407
-0
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 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 TSCBasic
12+
import TSCUtility
13+
import Foundation
14+
15+
/// Extensions on Manifest for generating source code expressing its contents
16+
/// in canonical declarative form. Note that this bakes in the results of any
17+
/// algorithmically generated manifest content, so it is not suitable for the
18+
/// mechanical editing of package manifests. Rather, it is intended for such
19+
/// tasks as manifest creation as part of package instantiation, etc.
20+
extension Manifest {
21+
22+
/// Generates and returns a string containing the contents of the manifest
23+
/// in canonical declarative form.
24+
public var generatedManifestFileContents: String {
25+
return """
26+
// swift-tools-version: \(toolsVersion.description)
27+
import PackageDescription
28+
29+
let package = \(SourceCodeFragment(from: self).generateSourceCode())
30+
"""
31+
}
32+
}
33+
34+
35+
/// Convenience initializers for package manifest structures.
36+
fileprivate extension SourceCodeFragment {
37+
38+
/// Instantiates a SourceCodeFragment to represent an entire manifest.
39+
convenience init(from manifest: Manifest) {
40+
var params: [SourceCodeFragment] = []
41+
42+
params.append(SourceCodeFragment(key: "name", string: manifest.name))
43+
44+
if let defaultLoc = manifest.defaultLocalization {
45+
// FIXME: The value being emitted here is probably not correct.
46+
let locName = String(describing: defaultLoc)
47+
params.append(SourceCodeFragment(key: "defaultLocalization", string: locName))
48+
}
49+
50+
if !manifest.platforms.isEmpty {
51+
let nodes = manifest.platforms.map{ SourceCodeFragment(from: $0) }
52+
params.append(SourceCodeFragment(key: "platforms", subnodes: nodes))
53+
}
54+
55+
// FIXME: Add support for `pkgConfig` and `providers` properties.
56+
57+
if !manifest.products.isEmpty {
58+
let nodes = manifest.products.map{ SourceCodeFragment(from: $0) }
59+
params.append(SourceCodeFragment(key: "products", subnodes: nodes))
60+
}
61+
62+
if !manifest.dependencies.isEmpty {
63+
let nodes = manifest.dependencies.map{ SourceCodeFragment(from: $0) }
64+
params.append(SourceCodeFragment(key: "dependencies", subnodes: nodes))
65+
}
66+
67+
if !manifest.targets.isEmpty {
68+
let nodes = manifest.targets.map{ SourceCodeFragment(from: $0) }
69+
params.append(SourceCodeFragment(key: "targets", subnodes: nodes))
70+
}
71+
72+
if let swiftLanguageVersions = manifest.swiftLanguageVersions {
73+
let nodes = swiftLanguageVersions.map{ SourceCodeFragment(from: $0) }
74+
params.append(SourceCodeFragment(key: "swiftLanguageVersions", subnodes: nodes))
75+
}
76+
77+
if let cLanguageStandard = manifest.cLanguageStandard {
78+
// FIXME: The value being emitted here is probably not correct.
79+
params.append(SourceCodeFragment(key: "cLanguageStandard", string: cLanguageStandard))
80+
}
81+
82+
if let cxxLanguageStandard = manifest.cxxLanguageStandard {
83+
// FIXME: The value being emitted here is probably not correct.
84+
params.append(SourceCodeFragment(key: "cxxLanguageStandard", string: cxxLanguageStandard))
85+
}
86+
87+
self.init("Package", delimiters: .parentheses, subnodes: params)
88+
}
89+
90+
/// Instantiates a SourceCodeFragment to represent a single platform.
91+
convenience init(from platform: PlatformDescription) {
92+
// FIXME: The name and value being emitted here is probably not correct.
93+
let name = platform.platformName
94+
let version = platform.version
95+
self.init(".\(name)(\(version.quotedForPackageManifest))")
96+
}
97+
98+
/// Instantiates a SourceCodeFragment to represent a single package dependency.
99+
convenience init(from dependency: PackageDependencyDescription) {
100+
var params: [SourceCodeFragment] = []
101+
102+
if let explicitName = dependency.explicitName {
103+
params.append(SourceCodeFragment(key: "name", string: explicitName))
104+
}
105+
switch dependency.requirement {
106+
case .exact(let version):
107+
params.append(SourceCodeFragment(key: "url", string: dependency.url))
108+
params.append(SourceCodeFragment(".exact(\(version.description.quotedForPackageManifest))"))
109+
case .range(let range):
110+
params.append(SourceCodeFragment(key: "url", string: dependency.url))
111+
params.append(SourceCodeFragment(".range(\(range.description.quotedForPackageManifest))"))
112+
case .revision(let revision):
113+
params.append(SourceCodeFragment(key: "url", string: dependency.url))
114+
params.append(SourceCodeFragment(".revision(\(revision.quotedForPackageManifest)"))
115+
case .branch(let branch):
116+
params.append(SourceCodeFragment(key: "url", string: dependency.url))
117+
params.append(SourceCodeFragment(".branch(\(branch.quotedForPackageManifest)"))
118+
case .localPackage:
119+
params.append(SourceCodeFragment(key: "path", string: dependency.url))
120+
}
121+
self.init(".package", delimiters: .parentheses, multiline: false, subnodes: params)
122+
}
123+
124+
/// Instantiates a SourceCodeFragment to represent a single product.
125+
convenience init(from product: ProductDescription) {
126+
let literal: String
127+
var params: [SourceCodeFragment] = []
128+
129+
params.append(SourceCodeFragment(key: "name", string: product.name))
130+
switch product.type {
131+
case .library(let type):
132+
literal = ".library"
133+
if type != .automatic {
134+
params.append(SourceCodeFragment(key: "type", enum: type.rawValue))
135+
}
136+
case .executable:
137+
literal = ".executable"
138+
case .test:
139+
literal = ".test"
140+
}
141+
if !product.targets.isEmpty {
142+
params.append(SourceCodeFragment(key: "targets", strings: product.targets))
143+
}
144+
self.init(literal, delimiters: .parentheses, subnodes: params)
145+
}
146+
147+
/// Instantiates a SourceCodeFragment to represent a single target.
148+
convenience init(from target: TargetDescription) {
149+
let literal: String
150+
var params: [SourceCodeFragment] = []
151+
152+
params.append(SourceCodeFragment(key: "name", string: target.name))
153+
switch target.type {
154+
case .regular:
155+
literal = ".target"
156+
case .test:
157+
literal = ".testTarget"
158+
case .system:
159+
literal = ".systemTarget"
160+
case .binary:
161+
literal = ".binaryTarget"
162+
}
163+
if !target.dependencies.isEmpty {
164+
let nodes = target.dependencies.map{ SourceCodeFragment(from: $0) }
165+
params.append(SourceCodeFragment(key: "dependencies", subnodes: nodes))
166+
}
167+
// FIXME: Add build settings, etc, etc
168+
self.init(literal, delimiters: .parentheses, subnodes: params)
169+
}
170+
171+
/// Instantiates a SourceCodeFragment to represent a single target dependency.
172+
convenience init(from dependency: TargetDescription.Dependency) {
173+
var params: [SourceCodeFragment] = []
174+
175+
switch dependency {
176+
case .target(name: let name, condition: let condition):
177+
params.append(SourceCodeFragment(key: "name", string: name))
178+
if let condition = condition {
179+
let configNode = SourceCodeFragment(from: condition)
180+
params.append(SourceCodeFragment("condition: \(configNode.generateSourceCode())"))
181+
}
182+
self.init(".target", delimiters: .parentheses, subnodes: params)
183+
184+
case .product(name: let name, package: let packageName, condition: let condition):
185+
params.append(SourceCodeFragment(key: "name", string: name))
186+
if let packageName = packageName {
187+
params.append(SourceCodeFragment(key: "package", string: packageName))
188+
}
189+
if let condition = condition {
190+
let configNode = SourceCodeFragment(from: condition)
191+
params.append(SourceCodeFragment("condition: \(configNode.generateSourceCode())"))
192+
}
193+
self.init(".product", delimiters: .parentheses, subnodes: params)
194+
195+
case .byName(name: let name, condition: let condition):
196+
if let condition = condition {
197+
params.append(SourceCodeFragment(key: "name", string: name))
198+
let configNode = SourceCodeFragment(from: condition)
199+
params.append(SourceCodeFragment("condition: \(configNode.generateSourceCode())"))
200+
self.init(".product", delimiters: .parentheses, multiline: false, subnodes: params)
201+
}
202+
else {
203+
self.init(name.quotedForPackageManifest)
204+
}
205+
}
206+
}
207+
208+
/// Instantiates a SourceCodeFragment to represent a single dependency condition.
209+
convenience init(from condition: PackageConditionDescription) {
210+
var params: [SourceCodeFragment] = []
211+
212+
if !condition.platformNames.isEmpty {
213+
let nodes = condition.platformNames.map{ SourceCodeFragment(".\($0)") }
214+
params.append(SourceCodeFragment(key: "platforms", multiline: false, subnodes: nodes))
215+
}
216+
if let configName = condition.config {
217+
params.append(SourceCodeFragment(key: "configuration", enum: configName))
218+
}
219+
self.init(".when", delimiters: .parentheses, multiline: false, subnodes: params)
220+
}
221+
222+
/// Instantiates a SourceCodeFragment to represent a single Swift language version.
223+
convenience init(from version: SwiftLanguageVersion) {
224+
let version = String(describing: version) // FIXME
225+
self.init(".version(\(version.quotedForPackageManifest))")
226+
}
227+
}
228+
229+
230+
/// Convenience initializers for key-value pairs of simple types.
231+
fileprivate extension SourceCodeFragment {
232+
233+
/// Initializes a SourceCodeFragment for a boolean in a generated manifest.
234+
convenience init(key: String, boolean: Bool) {
235+
self.init(key + ": " + (boolean ? "true" : "false"))
236+
}
237+
238+
/// Initializes a SourceCodeFragment for a quoted string in a generated manifest.
239+
convenience init(key: String, string: String) {
240+
self.init(key + ": " + string.quotedForPackageManifest)
241+
}
242+
243+
/// Initializes a SourceCodeFragment for an enum in a generated manifest.
244+
convenience init(key: String, enum: String) {
245+
self.init(key + ": ." + `enum`)
246+
}
247+
248+
/// Initializes a SourceCodeFragment for a string list in a generated manifest.
249+
convenience init(key: String, strings: [String]) {
250+
let subnodes = strings.map{ SourceCodeFragment($0.quotedForPackageManifest) }
251+
self.init(key + ": ", delimiters: .brackets, subnodes: subnodes)
252+
}
253+
254+
/// Initializes a SourceCodeFragment for a list of nodes in a generated manifest.
255+
convenience init(key: String, multiline: Bool = true, subnodes: [SourceCodeFragment]) {
256+
self.init(key + ": ", delimiters: .brackets, multiline: multiline, subnodes: subnodes)
257+
}
258+
}
259+
260+
261+
262+
/// Helper type to emit source code. Represents one node of source code, as a
263+
/// arbitrary string followed by an optional child list, optionally enclosed in
264+
/// a pair of delimiters. The code generation works by creating source code
265+
/// fragments and then rendering them as source code with proper formatting.
266+
fileprivate class SourceCodeFragment {
267+
let literal: String
268+
var delimiters: Delimiters
269+
var multiline: Bool
270+
var subnodes: [SourceCodeFragment]
271+
272+
enum Delimiters {
273+
case none
274+
case brackets
275+
case parentheses
276+
}
277+
278+
init(_ literal: String, delimiters: Delimiters = .none,
279+
multiline: Bool = true, subnodes: [SourceCodeFragment] = []) {
280+
self.literal = literal
281+
self.delimiters = delimiters
282+
self.multiline = multiline
283+
self.subnodes = subnodes
284+
}
285+
286+
func generateSourceCode(indent: String = "") -> String {
287+
var string = literal
288+
if !subnodes.isEmpty {
289+
switch delimiters {
290+
case .none: break
291+
case .brackets: string.append("[")
292+
case .parentheses: string.append("(")
293+
}
294+
if multiline { string.append("\n") }
295+
for (idx, subnode) in subnodes.enumerated() {
296+
let subindent = indent + " "
297+
if multiline { string.append(subindent) }
298+
string.append(subnode.generateSourceCode(indent: subindent))
299+
if idx < subnodes.count-1 {
300+
string.append(multiline ? ",\n" : ", ")
301+
}
302+
}
303+
if multiline {
304+
string.append("\n")
305+
string.append(indent)
306+
}
307+
switch delimiters {
308+
case .none: break
309+
case .brackets: string.append("]")
310+
case .parentheses: string.append(")")
311+
}
312+
}
313+
return string
314+
}
315+
}
316+
317+
318+
fileprivate extension String {
319+
320+
var quotedForPackageManifest: String {
321+
return "\"" + self
322+
.replacingOccurrences(of: "\\", with: "\\\\")
323+
.replacingOccurrences(of: "\"", with: "\\\"")
324+
+ "\""
325+
}
326+
}

0 commit comments

Comments
 (0)