Skip to content

Commit 5cc5919

Browse files
authored
feat: added protocol plugin support for Xcode targets (#58)
1 parent a6011c3 commit 5cc5919

File tree

7 files changed

+365
-60
lines changed

7 files changed

+365
-60
lines changed

Plugins/MetaProtocolCodable/Config.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ struct Config {
2222
///
2323
/// Files from the target which includes plugin and target dependencies
2424
/// present in current package manifest are checked.
25+
case direct
26+
/// Represents to check all local targets.
27+
///
28+
/// Files from the target which includes plugin and all targets
29+
/// that are in the same project/package.
2530
case local
2631
/// Represents to check current target and all dependencies.
2732
///

Plugins/MetaProtocolCodable/Plugin.swift

Lines changed: 56 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,20 @@ struct MetaProtocolCodable: BuildToolPlugin {
1717
///
1818
/// - Parameter target: The target including plugin.
1919
/// - Returns: The config if provided, otherwise default config.
20-
func fetchConfig(for target: SourceModuleTarget) async throws -> Config {
21-
let fileManager = FileManager.default
22-
let directory = target.directory.string
23-
let contents = try fileManager.contentsOfDirectory(atPath: directory)
24-
let file = contents.first { file in
25-
let path = Path(file)
26-
let name = path.stem
27-
.components(separatedBy: .alphanumerics.inverted)
28-
.joined(separator: "")
29-
.lowercased()
30-
return name == "metacodableconfig"
31-
}
32-
guard let file else { return .init(scan: .target) }
33-
let pathStr = target.directory.appending([file]).string
20+
func fetchConfig<Target: MetaProtocolCodableSourceTarget>(
21+
for target: Target
22+
) throws -> Config {
23+
let pathStr = try target.configPath(named: "metacodableconfig")
24+
guard let pathStr else { return .init(scan: .target) }
3425
let path = Config.url(forFilePath: pathStr)
3526
let conf = try Data(contentsOf: path)
3627
let pConf = try? PropertyListDecoder().decode(Config.self, from: conf)
3728
let config = try pConf ?? JSONDecoder().decode(Config.self, from: conf)
3829
return config
3930
}
4031

41-
/// Invoked by SwiftPM to create build commands for a particular target.
32+
/// Invoked by build systems to create build commands for a particular
33+
/// target.
4234
///
4335
/// Creates build commands that produces intermediate files scanning
4436
/// swift source files according to configuration. Final build command
@@ -49,14 +41,14 @@ struct MetaProtocolCodable: BuildToolPlugin {
4941
/// - target: The target including plugin.
5042
///
5143
/// - Returns: The commands to be executed during build.
52-
func createBuildCommands(
53-
context: PluginContext, target: Target
54-
) async throws -> [Command] {
55-
guard let target = target as? SourceModuleTarget else { return [] }
44+
func createBuildCommands<Context>(
45+
in context: Context, for target: Context.Target
46+
) throws -> [Command] where Context: MetaProtocolCodablePluginContext {
47+
// Get config
5648
let tool = try context.tool(named: "ProtocolGen")
57-
// Get Config
58-
let config = try await fetchConfig(for: target)
59-
let (allTargets, imports) = config.scanInput(for: target)
49+
let config = try fetchConfig(for: target)
50+
let (allTargets, imports) = config.scanInput(for: target, in: context)
51+
6052
// Setup folder
6153
let genFolder = context.pluginWorkDirectory.appending(["ProtocolGen"])
6254
try FileManager.default.createDirectory(
@@ -115,45 +107,49 @@ struct MetaProtocolCodable: BuildToolPlugin {
115107
}
116108
}
117109

118-
extension Config {
119-
/// Returns targets to scan and import modules based on current
120-
/// configuration.
110+
extension MetaProtocolCodable {
111+
/// Invoked by SwiftPM to create build commands for a particular target.
121112
///
122-
/// Based on configuration, the targets for which source files need
123-
/// to be checked and the modules that will be imported in final syntax
124-
/// generated is returned.
113+
/// Creates build commands that produces intermediate files scanning
114+
/// swift source files according to configuration. Final build command
115+
/// generates syntax aggregating all intermediate files.
125116
///
126-
/// - Parameter target: The target including plugin.
127-
/// - Returns: The targets to scan and modules to import.
128-
func scanInput(
129-
for target: SourceModuleTarget
130-
) -> (targets: [SourceModuleTarget], modules: [String]) {
131-
let allTargets: [SourceModuleTarget]
132-
let modules: [String]
133-
switch scan {
134-
case .target:
135-
allTargets = [target]
136-
modules = []
137-
case .local:
138-
var targets = target.dependencies.compactMap { dependency in
139-
return switch dependency {
140-
case .target(let target):
141-
target.sourceModule
142-
default:
143-
nil
144-
}
145-
}
146-
modules = targets.map(\.moduleName)
147-
targets.append(target)
148-
allTargets = targets
149-
case .recursive:
150-
var targets = target.recursiveTargetDependencies.compactMap {
151-
return $0 as? SourceModuleTarget
152-
}
153-
modules = targets.map(\.moduleName)
154-
targets.append(target)
155-
allTargets = targets
156-
}
157-
return (allTargets, modules)
117+
/// - Parameters:
118+
/// - context: The package and environmental inputs context.
119+
/// - target: The target including plugin.
120+
///
121+
/// - Returns: The commands to be executed during build.
122+
func createBuildCommands(
123+
context: PluginContext, target: Target
124+
) async throws -> [Command] {
125+
guard let target = target as? SourceModuleTarget else { return [] }
126+
return try self.createBuildCommands(
127+
in: context, for: SwiftPackageTarget(module: target)
128+
)
129+
}
130+
}
131+
132+
#if canImport(XcodeProjectPlugin)
133+
@_implementationOnly import XcodeProjectPlugin
134+
135+
extension MetaProtocolCodable: XcodeBuildToolPlugin {
136+
/// Invoked by Xcode to create build commands for a particular target.
137+
///
138+
/// Creates build commands that produces intermediate files scanning
139+
/// swift source files according to configuration. Final build command
140+
/// generates syntax aggregating all intermediate files.
141+
///
142+
/// - Parameters:
143+
/// - context: The package and environmental inputs context.
144+
/// - target: The target including plugin.
145+
///
146+
/// - Returns: The commands to be executed during build.
147+
func createBuildCommands(
148+
context: XcodePluginContext, target: XcodeTarget
149+
) throws -> [Command] {
150+
return try self.createBuildCommands(
151+
in: context, for: target
152+
)
158153
}
159154
}
155+
#endif
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@_implementationOnly import PackagePlugin
2+
3+
/// Provides information about the package for which the plugin is invoked,
4+
/// as well as contextual information based on the plugin's stated intent
5+
/// and requirements.
6+
///
7+
/// Build systems can provide their own conformance implementations.
8+
protocol MetaProtocolCodablePluginContext {
9+
/// The source code module type associated with this context.
10+
///
11+
/// Build can customize target type based on build context.
12+
associatedtype Target: MetaProtocolCodableSourceTarget
13+
/// The path of a writable directory into which the plugin or the build
14+
/// commands it constructs can write anything it wants. This could include
15+
/// any generated source files that should be processed further, and it
16+
/// could include any caches used by the build tool or the plugin itself.
17+
///
18+
/// The plugin is in complete control of what is written under this
19+
/// directory, and the contents are preserved between builds.
20+
///
21+
/// A plugin would usually create a separate subdirectory of this directory
22+
/// for each command it creates, and the command would be configured to
23+
/// write its outputs to that directory. The plugin may also create other
24+
/// directories for cache files and other file system content that either
25+
/// it or the command will need.
26+
var pluginWorkDirectory: Path { get }
27+
/// The targets which are local to current context.
28+
///
29+
/// These targets are included in the same package/project as this context.
30+
/// These targets are scanned if `local` scan mode provided in config.
31+
var localTargets: [Target] { get }
32+
/// Looks up and returns the path of a named command line executable tool.
33+
///
34+
/// The executable must be provided by an executable target or a binary
35+
/// target on which the package plugin target depends. This function throws
36+
/// an error if the tool cannot be found. The lookup is case sensitive.
37+
///
38+
/// - Parameter name: The executable tool name.
39+
/// - Returns: The executable tool.
40+
func tool(named name: String) throws -> PluginContext.Tool
41+
}
42+
43+
extension PluginContext: MetaProtocolCodablePluginContext {
44+
/// The targets which are local to current context.
45+
///
46+
/// Includes all the source code targets of the package.
47+
var localTargets: [SwiftPackageTarget] {
48+
return `package`.targets.compactMap { target in
49+
guard let sourceModule = target.sourceModule else { return nil }
50+
return SwiftPackageTarget(module: sourceModule)
51+
}
52+
}
53+
}
54+
55+
#if canImport(XcodeProjectPlugin)
56+
@_implementationOnly import XcodeProjectPlugin
57+
58+
extension XcodePluginContext: MetaProtocolCodablePluginContext {
59+
/// The targets which are local to current context.
60+
///
61+
/// Includes all the targets of the Xcode project.
62+
var localTargets: [XcodeTarget] { xcodeProject.targets }
63+
}
64+
#endif
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
@_implementationOnly import PackagePlugin
2+
3+
/// Represents a target consisting of a source code module,
4+
/// containing `Swift` source files.
5+
///
6+
/// Targets from multiple build system can support this plugin
7+
/// by providing conformance.
8+
protocol MetaProtocolCodableSourceTarget {
9+
/// Type representing sequence of files.
10+
associatedtype FileSequence: Sequence
11+
where FileSequence.Element == FileList.Element
12+
13+
/// The name of the module produced
14+
/// by the target.
15+
///
16+
/// This is used as additional imports in
17+
/// plugin generated code.
18+
var moduleName: String { get }
19+
/// The targets on which the current target depends on.
20+
///
21+
/// These targets are scanned if `direct` scan mode
22+
/// provided in config.
23+
var dependencyTargets: [Self] { get }
24+
/// All the targets on which current target depends on.
25+
///
26+
/// These targets are scanned if `recursive` scan mode
27+
/// provided in config.
28+
var recursiveTargets: [Self] { get }
29+
30+
/// A list of source files in the target that have the given
31+
/// filename suffix.
32+
///
33+
/// The list can possibly be empty if no file matched.
34+
///
35+
/// - Parameter suffix: The name suffix.
36+
/// - Returns: The matching files.
37+
func sourceFiles(withSuffix suffix: String) -> FileSequence
38+
/// The absolute path to config file if provided.
39+
///
40+
/// The file name comparison is case-insensitive
41+
/// and if no match found `nil` is returned.
42+
///
43+
/// - Parameter name: The config file name.
44+
/// - Returns: The config file path.
45+
func configPath(named name: String) throws -> String?
46+
}
47+
48+
extension Config {
49+
/// Returns targets to scan and import modules based on current
50+
/// configuration.
51+
///
52+
/// Based on configuration, the targets for which source files need
53+
/// to be checked and the modules that will be imported in final syntax
54+
/// generated is returned.
55+
///
56+
/// - Parameter target: The target including plugin.
57+
/// - Returns: The targets to scan and modules to import.
58+
func scanInput<Context: MetaProtocolCodablePluginContext>(
59+
for target: Context.Target, in context: Context
60+
) -> (targets: [Context.Target], modules: [String]) {
61+
let allTargets: [Context.Target]
62+
let modules: [String]
63+
switch scan {
64+
case .target:
65+
allTargets = [target]
66+
modules = []
67+
case .direct:
68+
var targets = target.dependencyTargets
69+
modules = targets.map(\.moduleName)
70+
targets.append(target)
71+
allTargets = targets
72+
case .local:
73+
allTargets = context.localTargets
74+
modules = allTargets.lazy.map(\.moduleName).filter { module in
75+
return module != target.moduleName
76+
}
77+
case .recursive:
78+
var targets = target.recursiveTargets
79+
modules = targets.map(\.moduleName)
80+
targets.append(target)
81+
allTargets = targets
82+
}
83+
return (allTargets, modules)
84+
}
85+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
@_implementationOnly import Foundation
2+
@_implementationOnly import PackagePlugin
3+
4+
/// Represents an SwiftPM target.
5+
///
6+
/// Uses `SourceModuleTarget` to provide conformances.
7+
struct SwiftPackageTarget {
8+
/// The actual module for this target.
9+
///
10+
/// The conformances provided uses this module.
11+
let module: any SourceModuleTarget
12+
}
13+
14+
extension SwiftPackageTarget: MetaProtocolCodableSourceTarget {
15+
/// The name of the module produced
16+
/// by the target.
17+
///
18+
/// This is derived from target name or SwiftPM customized name.
19+
var moduleName: String { module.moduleName }
20+
21+
/// The targets on which the current target depends on.
22+
///
23+
/// Represents direct dependencies of the target.
24+
var dependencyTargets: [Self] {
25+
return module.dependencies.lazy.compactMap { dependency in
26+
return switch dependency {
27+
case .target(let target):
28+
target.sourceModule
29+
default:
30+
nil
31+
}
32+
}.map { Self.init(module: $0) }
33+
}
34+
35+
/// All the targets on which current target depends on.
36+
///
37+
/// Represents direct and transient dependencies of the target.
38+
var recursiveTargets: [Self] {
39+
return module.recursiveTargetDependencies.lazy
40+
.compactMap { $0.sourceModule }
41+
.map { Self.init(module: $0) }
42+
}
43+
44+
/// A list of source files in the target that have the given
45+
/// filename suffix.
46+
///
47+
/// The list can possibly be empty if no file matched.
48+
///
49+
/// - Parameter suffix: The name suffix.
50+
/// - Returns: The matching files.
51+
func sourceFiles(withSuffix suffix: String) -> FileList {
52+
return module.sourceFiles(withSuffix: suffix)
53+
}
54+
55+
/// The absolute path to config file if provided.
56+
///
57+
/// The file name comparison is case-insensitive
58+
/// and if no match found `nil` is returned.
59+
///
60+
/// The file is checked only in the module directory
61+
/// and not in any of its sub-directories.
62+
///
63+
/// - Parameter name: The config file name.
64+
/// - Returns: The config file path.
65+
func configPath(named name: String) throws -> String? {
66+
let fileManager = FileManager.default
67+
let directory = module.directory.string
68+
let contents = try fileManager.contentsOfDirectory(atPath: directory)
69+
let file = contents.first { file in
70+
let path = Path(file)
71+
return name.lowercased() == path.stem
72+
.components(separatedBy: .alphanumerics.inverted)
73+
.joined(separator: "")
74+
.lowercased()
75+
}
76+
guard let file else { return nil }
77+
return module.directory.appending([file]).string
78+
}
79+
}

0 commit comments

Comments
 (0)