Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added protocol plugin support for Xcode targets #58

Merged
merged 1 commit into from Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions Plugins/MetaProtocolCodable/Config.swift
Expand Up @@ -22,6 +22,11 @@ struct Config {
///
/// Files from the target which includes plugin and target dependencies
/// present in current package manifest are checked.
case direct
/// Represents to check all local targets.
///
/// Files from the target which includes plugin and all targets
/// that are in the same project/package.
case local
/// Represents to check current target and all dependencies.
///
Expand Down
116 changes: 56 additions & 60 deletions Plugins/MetaProtocolCodable/Plugin.swift
Expand Up @@ -17,28 +17,20 @@ struct MetaProtocolCodable: BuildToolPlugin {
///
/// - Parameter target: The target including plugin.
/// - Returns: The config if provided, otherwise default config.
func fetchConfig(for target: SourceModuleTarget) async throws -> Config {
let fileManager = FileManager.default
let directory = target.directory.string
let contents = try fileManager.contentsOfDirectory(atPath: directory)
let file = contents.first { file in
let path = Path(file)
let name = path.stem
.components(separatedBy: .alphanumerics.inverted)
.joined(separator: "")
.lowercased()
return name == "metacodableconfig"
}
guard let file else { return .init(scan: .target) }
let pathStr = target.directory.appending([file]).string
func fetchConfig<Target: MetaProtocolCodableSourceTarget>(
for target: Target
) throws -> Config {
let pathStr = try target.configPath(named: "metacodableconfig")
guard let pathStr else { return .init(scan: .target) }
let path = Config.url(forFilePath: pathStr)
let conf = try Data(contentsOf: path)
let pConf = try? PropertyListDecoder().decode(Config.self, from: conf)
let config = try pConf ?? JSONDecoder().decode(Config.self, from: conf)
return config
}

/// Invoked by SwiftPM to create build commands for a particular target.
/// Invoked by build systems to create build commands for a particular
/// target.
///
/// Creates build commands that produces intermediate files scanning
/// swift source files according to configuration. Final build command
Expand All @@ -49,14 +41,14 @@ struct MetaProtocolCodable: BuildToolPlugin {
/// - target: The target including plugin.
///
/// - Returns: The commands to be executed during build.
func createBuildCommands(
context: PluginContext, target: Target
) async throws -> [Command] {
guard let target = target as? SourceModuleTarget else { return [] }
func createBuildCommands<Context>(
in context: Context, for target: Context.Target
) throws -> [Command] where Context: MetaProtocolCodablePluginContext {
// Get config
let tool = try context.tool(named: "ProtocolGen")
// Get Config
let config = try await fetchConfig(for: target)
let (allTargets, imports) = config.scanInput(for: target)
let config = try fetchConfig(for: target)
let (allTargets, imports) = config.scanInput(for: target, in: context)

// Setup folder
let genFolder = context.pluginWorkDirectory.appending(["ProtocolGen"])
try FileManager.default.createDirectory(
Expand Down Expand Up @@ -115,45 +107,49 @@ struct MetaProtocolCodable: BuildToolPlugin {
}
}

extension Config {
/// Returns targets to scan and import modules based on current
/// configuration.
extension MetaProtocolCodable {
/// Invoked by SwiftPM to create build commands for a particular target.
///
/// Based on configuration, the targets for which source files need
/// to be checked and the modules that will be imported in final syntax
/// generated is returned.
/// Creates build commands that produces intermediate files scanning
/// swift source files according to configuration. Final build command
/// generates syntax aggregating all intermediate files.
///
/// - Parameter target: The target including plugin.
/// - Returns: The targets to scan and modules to import.
func scanInput(
for target: SourceModuleTarget
) -> (targets: [SourceModuleTarget], modules: [String]) {
let allTargets: [SourceModuleTarget]
let modules: [String]
switch scan {
case .target:
allTargets = [target]
modules = []
case .local:
var targets = target.dependencies.compactMap { dependency in
return switch dependency {
case .target(let target):
target.sourceModule
default:
nil
}
}
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
case .recursive:
var targets = target.recursiveTargetDependencies.compactMap {
return $0 as? SourceModuleTarget
}
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
}
return (allTargets, modules)
/// - Parameters:
/// - context: The package and environmental inputs context.
/// - target: The target including plugin.
///
/// - Returns: The commands to be executed during build.
func createBuildCommands(
context: PluginContext, target: Target
) async throws -> [Command] {
guard let target = target as? SourceModuleTarget else { return [] }
return try self.createBuildCommands(
in: context, for: SwiftPackageTarget(module: target)
)
}
}

#if canImport(XcodeProjectPlugin)
@_implementationOnly import XcodeProjectPlugin

extension MetaProtocolCodable: XcodeBuildToolPlugin {
/// Invoked by Xcode to create build commands for a particular target.
///
/// Creates build commands that produces intermediate files scanning
/// swift source files according to configuration. Final build command
/// generates syntax aggregating all intermediate files.
///
/// - Parameters:
/// - context: The package and environmental inputs context.
/// - target: The target including plugin.
///
/// - Returns: The commands to be executed during build.
func createBuildCommands(
context: XcodePluginContext, target: XcodeTarget
) throws -> [Command] {
return try self.createBuildCommands(
in: context, for: target
)
}
}
#endif
64 changes: 64 additions & 0 deletions Plugins/MetaProtocolCodable/PluginContext.swift
@@ -0,0 +1,64 @@
@_implementationOnly import PackagePlugin

/// Provides information about the package for which the plugin is invoked,
/// as well as contextual information based on the plugin's stated intent
/// and requirements.
///
/// Build systems can provide their own conformance implementations.
protocol MetaProtocolCodablePluginContext {
/// The source code module type associated with this context.
///
/// Build can customize target type based on build context.
associatedtype Target: MetaProtocolCodableSourceTarget
/// The path of a writable directory into which the plugin or the build
/// commands it constructs can write anything it wants. This could include
/// any generated source files that should be processed further, and it
/// could include any caches used by the build tool or the plugin itself.
///
/// The plugin is in complete control of what is written under this
/// directory, and the contents are preserved between builds.
///
/// A plugin would usually create a separate subdirectory of this directory
/// for each command it creates, and the command would be configured to
/// write its outputs to that directory. The plugin may also create other
/// directories for cache files and other file system content that either
/// it or the command will need.
var pluginWorkDirectory: Path { get }
/// The targets which are local to current context.
///
/// These targets are included in the same package/project as this context.
/// These targets are scanned if `local` scan mode provided in config.
var localTargets: [Target] { get }
/// Looks up and returns the path of a named command line executable tool.
///
/// The executable must be provided by an executable target or a binary
/// target on which the package plugin target depends. This function throws
/// an error if the tool cannot be found. The lookup is case sensitive.
///
/// - Parameter name: The executable tool name.
/// - Returns: The executable tool.
func tool(named name: String) throws -> PluginContext.Tool
}

extension PluginContext: MetaProtocolCodablePluginContext {
/// The targets which are local to current context.
///
/// Includes all the source code targets of the package.
var localTargets: [SwiftPackageTarget] {
return `package`.targets.compactMap { target in
guard let sourceModule = target.sourceModule else { return nil }
return SwiftPackageTarget(module: sourceModule)
}
}
}

#if canImport(XcodeProjectPlugin)
@_implementationOnly import XcodeProjectPlugin

extension XcodePluginContext: MetaProtocolCodablePluginContext {
/// The targets which are local to current context.
///
/// Includes all the targets of the Xcode project.
var localTargets: [XcodeTarget] { xcodeProject.targets }
}
#endif
@@ -0,0 +1,85 @@
@_implementationOnly import PackagePlugin

/// Represents a target consisting of a source code module,
/// containing `Swift` source files.
///
/// Targets from multiple build system can support this plugin
/// by providing conformance.
protocol MetaProtocolCodableSourceTarget {
/// Type representing sequence of files.
associatedtype FileSequence: Sequence
where FileSequence.Element == FileList.Element

/// The name of the module produced
/// by the target.
///
/// This is used as additional imports in
/// plugin generated code.
var moduleName: String { get }
/// The targets on which the current target depends on.
///
/// These targets are scanned if `direct` scan mode
/// provided in config.
var dependencyTargets: [Self] { get }
/// All the targets on which current target depends on.
///
/// These targets are scanned if `recursive` scan mode
/// provided in config.
var recursiveTargets: [Self] { get }

/// A list of source files in the target that have the given
/// filename suffix.
///
/// The list can possibly be empty if no file matched.
///
/// - Parameter suffix: The name suffix.
/// - Returns: The matching files.
func sourceFiles(withSuffix suffix: String) -> FileSequence
/// The absolute path to config file if provided.
///
/// The file name comparison is case-insensitive
/// and if no match found `nil` is returned.
///
/// - Parameter name: The config file name.
/// - Returns: The config file path.
func configPath(named name: String) throws -> String?
}

extension Config {
/// Returns targets to scan and import modules based on current
/// configuration.
///
/// Based on configuration, the targets for which source files need
/// to be checked and the modules that will be imported in final syntax
/// generated is returned.
///
/// - Parameter target: The target including plugin.
/// - Returns: The targets to scan and modules to import.
func scanInput<Context: MetaProtocolCodablePluginContext>(
for target: Context.Target, in context: Context
) -> (targets: [Context.Target], modules: [String]) {
let allTargets: [Context.Target]
let modules: [String]
switch scan {
case .target:
allTargets = [target]
modules = []
case .direct:
var targets = target.dependencyTargets
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
case .local:
allTargets = context.localTargets
modules = allTargets.lazy.map(\.moduleName).filter { module in
return module != target.moduleName
}
case .recursive:
var targets = target.recursiveTargets
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
}
return (allTargets, modules)
}
}
79 changes: 79 additions & 0 deletions Plugins/MetaProtocolCodable/SourceTarget/SwiftPackageTarget.swift
@@ -0,0 +1,79 @@
@_implementationOnly import Foundation
@_implementationOnly import PackagePlugin

/// Represents an SwiftPM target.
///
/// Uses `SourceModuleTarget` to provide conformances.
struct SwiftPackageTarget {
/// The actual module for this target.
///
/// The conformances provided uses this module.
let module: any SourceModuleTarget
}

extension SwiftPackageTarget: MetaProtocolCodableSourceTarget {
/// The name of the module produced
/// by the target.
///
/// This is derived from target name or SwiftPM customized name.
var moduleName: String { module.moduleName }

/// The targets on which the current target depends on.
///
/// Represents direct dependencies of the target.
var dependencyTargets: [Self] {
return module.dependencies.lazy.compactMap { dependency in
return switch dependency {
case .target(let target):
target.sourceModule
default:
nil
}
}.map { Self.init(module: $0) }
}

/// All the targets on which current target depends on.
///
/// Represents direct and transient dependencies of the target.
var recursiveTargets: [Self] {
return module.recursiveTargetDependencies.lazy
.compactMap { $0.sourceModule }
.map { Self.init(module: $0) }
}

/// A list of source files in the target that have the given
/// filename suffix.
///
/// The list can possibly be empty if no file matched.
///
/// - Parameter suffix: The name suffix.
/// - Returns: The matching files.
func sourceFiles(withSuffix suffix: String) -> FileList {
return module.sourceFiles(withSuffix: suffix)
}

/// The absolute path to config file if provided.
///
/// The file name comparison is case-insensitive
/// and if no match found `nil` is returned.
///
/// The file is checked only in the module directory
/// and not in any of its sub-directories.
///
/// - Parameter name: The config file name.
/// - Returns: The config file path.
func configPath(named name: String) throws -> String? {
let fileManager = FileManager.default
let directory = module.directory.string
let contents = try fileManager.contentsOfDirectory(atPath: directory)
let file = contents.first { file in
let path = Path(file)
return name.lowercased() == path.stem
.components(separatedBy: .alphanumerics.inverted)
.joined(separator: "")
.lowercased()
}
guard let file else { return nil }
return module.directory.appending([file]).string
}
}