Skip to content

Commit b036851

Browse files
authored
Code generation build plugin (#28)
## Overview New build plugin to generate gRPC services and protobuf messages The SwiftPM build plugin will locate protobuf files in the `Sources` directory (with the extension `.proto`) and attempt to invoke both the `protoc-gen-swift` and `protoc-gen-grpc-swift` `protoc` plugins on them to automatically generate Swift source. Behavior can be modified by specifying one or more configuration files. * For a given protobuf definition file the tool will search for configuration files in the same and all parent directories and will use the file lowest in the hierarchy. * Most configuration if not specified will use the protoc plugin's own defaults.
1 parent 390183c commit b036851

File tree

7 files changed

+765
-0
lines changed

7 files changed

+765
-0
lines changed

Package.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ let products: [Product] = [
2626
name: "protoc-gen-grpc-swift",
2727
targets: ["protoc-gen-grpc-swift"]
2828
),
29+
.plugin(
30+
name: "GRPCProtobufGenerator",
31+
targets: ["GRPCProtobufGenerator"]
32+
),
2933
]
3034

3135
let dependencies: [Package.Dependency] = [
@@ -101,6 +105,16 @@ let targets: [Target] = [
101105
],
102106
swiftSettings: defaultSwiftSettings
103107
),
108+
109+
// Code generator build plugin
110+
.plugin(
111+
name: "GRPCProtobufGenerator",
112+
capability: .buildTool(),
113+
dependencies: [
114+
.target(name: "protoc-gen-grpc-swift"),
115+
.product(name: "protoc-gen-swift", package: "swift-protobuf"),
116+
]
117+
),
104118
]
105119

106120
let package = Package(
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright 2024, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
let configFileName = "grpc-swift-proto-generator-config.json"
20+
21+
/// The config of the build plugin.
22+
struct BuildPluginConfig: Codable {
23+
/// Config defining which components should be considered when generating source.
24+
struct Generate {
25+
/// Whether server code is generated.
26+
///
27+
/// Defaults to `true`.
28+
var servers: Bool
29+
/// Whether client code is generated.
30+
///
31+
/// Defaults to `true`.
32+
var clients: Bool
33+
/// Whether message code is generated.
34+
///
35+
/// Defaults to `true`.
36+
var messages: Bool
37+
38+
static let defaults = Self(
39+
servers: true,
40+
clients: true,
41+
messages: true
42+
)
43+
44+
private init(servers: Bool, clients: Bool, messages: Bool) {
45+
self.servers = servers
46+
self.clients = clients
47+
self.messages = messages
48+
}
49+
}
50+
51+
/// Config relating to the generated code itself.
52+
struct GeneratedSource {
53+
/// The visibility of the generated files.
54+
///
55+
/// Defaults to `Internal`.
56+
var accessLevel: GenerationConfig.AccessLevel
57+
/// Whether imports should have explicit access levels.
58+
///
59+
/// Defaults to `false`.
60+
var useAccessLevelOnImports: Bool
61+
62+
static let defaults = Self(
63+
accessLevel: .internal,
64+
useAccessLevelOnImports: false
65+
)
66+
67+
private init(accessLevel: GenerationConfig.AccessLevel, useAccessLevelOnImports: Bool) {
68+
self.accessLevel = accessLevel
69+
self.useAccessLevelOnImports = useAccessLevelOnImports
70+
}
71+
}
72+
73+
/// Config relating to the protoc invocation.
74+
struct Protoc {
75+
/// Specify the directory in which to search for imports.
76+
///
77+
/// Paths are relative to the location of the specifying config file.
78+
/// Build plugins only have access to files within the target's source directory.
79+
/// May be specified multiple times; directories will be searched in order.
80+
/// The target source directory is always appended
81+
/// to the import paths.
82+
var importPaths: [String]
83+
84+
/// The path to the `protoc` executable binary.
85+
///
86+
/// If this is not set, Swift Package Manager will try to find the tool itself.
87+
var executablePath: String?
88+
89+
static let defaults = Self(
90+
importPaths: [],
91+
executablePath: nil
92+
)
93+
94+
private init(importPaths: [String], executablePath: String?) {
95+
self.importPaths = importPaths
96+
self.executablePath = executablePath
97+
}
98+
}
99+
100+
/// Config defining which components should be considered when generating source.
101+
var generate: Generate
102+
/// Config relating to the nature of the generated code.
103+
var generatedSource: GeneratedSource
104+
/// Config relating to the protoc invocation.
105+
var protoc: Protoc
106+
107+
static let defaults = Self(
108+
generate: Generate.defaults,
109+
generatedSource: GeneratedSource.defaults,
110+
protoc: Protoc.defaults
111+
)
112+
private init(generate: Generate, generatedSource: GeneratedSource, protoc: Protoc) {
113+
self.generate = generate
114+
self.generatedSource = generatedSource
115+
self.protoc = protoc
116+
}
117+
118+
// Codable conformance with defaults
119+
enum CodingKeys: String, CodingKey {
120+
case generate
121+
case generatedSource
122+
case protoc
123+
}
124+
125+
init(from decoder: any Decoder) throws {
126+
let container = try decoder.container(keyedBy: CodingKeys.self)
127+
128+
self.generate =
129+
try container.decodeIfPresent(Generate.self, forKey: .generate) ?? Self.defaults.generate
130+
self.generatedSource =
131+
try container.decodeIfPresent(GeneratedSource.self, forKey: .generatedSource)
132+
?? Self.defaults.generatedSource
133+
self.protoc =
134+
try container.decodeIfPresent(Protoc.self, forKey: .protoc) ?? Self.defaults.protoc
135+
}
136+
}
137+
138+
extension BuildPluginConfig.Generate: Codable {
139+
// Codable conformance with defaults
140+
enum CodingKeys: String, CodingKey {
141+
case servers
142+
case clients
143+
case messages
144+
}
145+
146+
init(from decoder: any Decoder) throws {
147+
let container = try decoder.container(keyedBy: CodingKeys.self)
148+
149+
self.servers =
150+
try container.decodeIfPresent(Bool.self, forKey: .servers) ?? Self.defaults.servers
151+
self.clients =
152+
try container.decodeIfPresent(Bool.self, forKey: .clients) ?? Self.defaults.clients
153+
self.messages =
154+
try container.decodeIfPresent(Bool.self, forKey: .messages) ?? Self.defaults.messages
155+
}
156+
}
157+
158+
extension BuildPluginConfig.GeneratedSource: Codable {
159+
// Codable conformance with defaults
160+
enum CodingKeys: String, CodingKey {
161+
case accessLevel
162+
case useAccessLevelOnImports
163+
}
164+
165+
init(from decoder: any Decoder) throws {
166+
let container = try decoder.container(keyedBy: CodingKeys.self)
167+
168+
self.accessLevel =
169+
try container.decodeIfPresent(GenerationConfig.AccessLevel.self, forKey: .accessLevel)
170+
?? Self.defaults.accessLevel
171+
self.useAccessLevelOnImports =
172+
try container.decodeIfPresent(Bool.self, forKey: .useAccessLevelOnImports)
173+
?? Self.defaults.useAccessLevelOnImports
174+
}
175+
}
176+
177+
extension BuildPluginConfig.Protoc: Codable {
178+
// Codable conformance with defaults
179+
enum CodingKeys: String, CodingKey {
180+
case importPaths
181+
case executablePath
182+
}
183+
184+
init(from decoder: any Decoder) throws {
185+
let container = try decoder.container(keyedBy: CodingKeys.self)
186+
187+
self.importPaths =
188+
try container.decodeIfPresent([String].self, forKey: .importPaths)
189+
?? Self.defaults.importPaths
190+
self.executablePath = try container.decodeIfPresent(String.self, forKey: .executablePath)
191+
}
192+
}
193+
194+
extension GenerationConfig {
195+
init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) {
196+
self.server = buildPluginConfig.generate.servers
197+
self.client = buildPluginConfig.generate.clients
198+
self.message = buildPluginConfig.generate.messages
199+
// hard-code full-path to avoid collisions since this goes into a temporary directory anyway
200+
self.fileNaming = .fullPath
201+
self.visibility = buildPluginConfig.generatedSource.accessLevel
202+
self.useAccessLevelOnImports = buildPluginConfig.generatedSource.useAccessLevelOnImports
203+
// Generate absolute paths for the imports relative to the config file in which they are specified
204+
self.importPaths = buildPluginConfig.protoc.importPaths.map { relativePath in
205+
configFilePath.deletingLastPathComponent().absoluteStringNoScheme + "/" + relativePath
206+
}
207+
self.protocPath = buildPluginConfig.protoc.executablePath
208+
self.outputPath = outputPath.absoluteStringNoScheme
209+
}
210+
}

0 commit comments

Comments
 (0)