@@ -940,6 +940,255 @@ public final class PluginLoader: @unchecked Sendable {
940940 }
941941 }
942942
943+ // MARK: - Parallel Plugin Compilation
944+
945+ /// Compile legacy plugins concurrently using a TaskGroup.
946+ ///
947+ /// Independent plugins share no build artefacts, so their `swiftc` / `swift build`
948+ /// invocations can run in parallel. Compilation warnings are collected and printed
949+ /// after all tasks complete; individual failures do not abort other compilations.
950+ public func compilePluginsParallel( from sourceDirectory: URL , to outputDirectory: URL ) async throws {
951+ let sourcePluginsDir = sourceDirectory
952+ let outputPluginsDir = outputDirectory
953+
954+ #if os(Windows)
955+ let libraryExtension = " dll "
956+ #elseif os(Linux)
957+ let libraryExtension = " so "
958+ #else
959+ let libraryExtension = " dylib "
960+ #endif
961+
962+ guard FileManager . default. fileExists ( atPath: sourcePluginsDir. path) else { return }
963+ try FileManager . default. createDirectory ( at: outputPluginsDir, withIntermediateDirectories: true )
964+
965+ let contents = try FileManager . default. contentsOfDirectory (
966+ at: sourcePluginsDir,
967+ includingPropertiesForKeys: [ . isDirectoryKey] ,
968+ options: [ . skipsHiddenFiles]
969+ )
970+
971+ // Build a list of compilation work items
972+ struct CompileJob : Sendable {
973+ let name : String
974+ let source : URL
975+ let output : URL
976+ let isPackage : Bool
977+ }
978+
979+ var jobs : [ CompileJob ] = [ ]
980+
981+ // Single-file Swift plugins
982+ for swiftFile in contents where swiftFile. pathExtension == " swift " {
983+ let name = swiftFile. deletingPathExtension ( ) . lastPathComponent
984+ let output = outputPluginsDir. appendingPathComponent ( " \( name) . \( libraryExtension) " )
985+ jobs. append ( CompileJob ( name: name, source: swiftFile, output: output, isPackage: false ) )
986+ }
987+
988+ // Swift package plugins
989+ for item in contents {
990+ var isDir : ObjCBool = false
991+ guard FileManager . default. fileExists ( atPath: item. path, isDirectory: & isDir) ,
992+ isDir. boolValue else { continue }
993+ let packageSwift = item. appendingPathComponent ( " Package.swift " )
994+ guard FileManager . default. fileExists ( atPath: packageSwift. path) else { continue }
995+ let name = item. lastPathComponent
996+ let output = outputPluginsDir. appendingPathComponent ( " lib \( name) . \( libraryExtension) " )
997+ jobs. append ( CompileJob ( name: name, source: item, output: output, isPackage: true ) )
998+ }
999+
1000+ guard !jobs. isEmpty else { return }
1001+
1002+ // Run all compilations concurrently
1003+ await withTaskGroup ( of: String ? . self) { group in
1004+ for job in jobs {
1005+ group. addTask {
1006+ do {
1007+ if job. isPackage {
1008+ try self . compilePackagePlugin ( source: job. source, output: job. output)
1009+ } else {
1010+ try self . compilePlugin ( source: job. source, output: job. output)
1011+ }
1012+ return nil // success
1013+ } catch {
1014+ return " [PluginLoader] Warning: Failed to compile \( job. name) : \( error) "
1015+ }
1016+ }
1017+ }
1018+ for await warning in group {
1019+ if let warning { print ( warning) }
1020+ }
1021+ }
1022+ }
1023+
1024+ /// Compile managed plugins concurrently using a TaskGroup.
1025+ ///
1026+ /// Each plugin in `Plugins/` is compiled independently, so all builds run in parallel.
1027+ /// Directory setup and file copies happen inside each task (they target separate output dirs).
1028+ public func compileManagedPluginsParallel( from sourceDirectory: URL , to outputDirectory: URL ) async throws {
1029+ #if os(Windows)
1030+ let libraryExtension = " dll "
1031+ #elseif os(Linux)
1032+ let libraryExtension = " so "
1033+ #else
1034+ let libraryExtension = " dylib "
1035+ #endif
1036+
1037+ guard FileManager . default. fileExists ( atPath: sourceDirectory. path) else { return }
1038+
1039+ let contents = try FileManager . default. contentsOfDirectory (
1040+ at: sourceDirectory,
1041+ includingPropertiesForKeys: [ . isDirectoryKey] ,
1042+ options: [ . skipsHiddenFiles]
1043+ )
1044+
1045+ // Filter to valid plugin directories (must contain plugin.yaml)
1046+ let pluginDirs = contents. filter { item -> Bool in
1047+ var isDir : ObjCBool = false
1048+ guard FileManager . default. fileExists ( atPath: item. path, isDirectory: & isDir) ,
1049+ isDir. boolValue else { return false }
1050+ return FileManager . default. fileExists (
1051+ atPath: item. appendingPathComponent ( " plugin.yaml " ) . path
1052+ )
1053+ }
1054+
1055+ guard !pluginDirs. isEmpty else { return }
1056+
1057+ // Run all plugin compilations concurrently
1058+ await withTaskGroup ( of: String ? . self) { group in
1059+ for item in pluginDirs {
1060+ let libExt = libraryExtension
1061+ let outDir = outputDirectory
1062+ group. addTask {
1063+ do {
1064+ try self . compileSingleManagedPlugin (
1065+ item: item,
1066+ outputDirectory: outDir,
1067+ libraryExtension: libExt
1068+ )
1069+ return nil
1070+ } catch {
1071+ return " [PluginLoader] Warning: Failed to compile managed plugin \( item. lastPathComponent) : \( error) "
1072+ }
1073+ }
1074+ }
1075+ for await warning in group {
1076+ if let warning { print ( warning) }
1077+ }
1078+ }
1079+ }
1080+
1081+ /// Compile a single managed plugin (extracted from the sequential loop body).
1082+ private func compileSingleManagedPlugin(
1083+ item: URL ,
1084+ outputDirectory: URL ,
1085+ libraryExtension: String
1086+ ) throws {
1087+ let pluginName = item. lastPathComponent
1088+ let outputPluginDir = outputDirectory. appendingPathComponent ( pluginName)
1089+ try FileManager . default. createDirectory ( at: outputPluginDir, withIntermediateDirectories: true )
1090+
1091+ // Copy plugin.yaml
1092+ let manifestPath = item. appendingPathComponent ( " plugin.yaml " )
1093+ let outputManifestPath = outputPluginDir. appendingPathComponent ( " plugin.yaml " )
1094+ if !FileManager. default. fileExists ( atPath: outputManifestPath. path) {
1095+ try FileManager . default. copyItem ( at: manifestPath, to: outputManifestPath)
1096+ }
1097+
1098+ // Check for Package.swift (Swift Package)
1099+ let packageSwift = item. appendingPathComponent ( " Package.swift " )
1100+ if FileManager . default. fileExists ( atPath: packageSwift. path) {
1101+ let outputLibPath = outputPluginDir. appendingPathComponent ( " Sources " )
1102+ . appendingPathComponent ( " lib \( pluginName) . \( libraryExtension) " )
1103+ try FileManager . default. createDirectory (
1104+ at: outputLibPath. deletingLastPathComponent ( ) ,
1105+ withIntermediateDirectories: true
1106+ )
1107+ try compilePackagePlugin ( source: item, output: outputLibPath)
1108+ return
1109+ }
1110+
1111+ // Check for Sources/ directory with Swift files
1112+ let sourcesDir = item. appendingPathComponent ( " Sources " )
1113+ if FileManager . default. fileExists ( atPath: sourcesDir. path) {
1114+ let sourceContents = try ? FileManager . default. contentsOfDirectory (
1115+ at: sourcesDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles]
1116+ )
1117+ let swiftFiles = sourceContents? . filter { $0. pathExtension == " swift " } ?? [ ]
1118+
1119+ if !swiftFiles. isEmpty {
1120+ let outputSourcesDir = outputPluginDir. appendingPathComponent ( " Sources " )
1121+ try FileManager . default. createDirectory ( at: outputSourcesDir, withIntermediateDirectories: true )
1122+ let outputLibPath = outputSourcesDir. appendingPathComponent ( " \( pluginName) . \( libraryExtension) " )
1123+ if swiftFiles. count == 1 {
1124+ try compilePlugin ( source: swiftFiles [ 0 ] , output: outputLibPath)
1125+ } else {
1126+ try compileMultipleSwiftFiles ( sources: swiftFiles, output: outputLibPath)
1127+ }
1128+ }
1129+ }
1130+
1131+ // Check for src/ directory with Rust or C files
1132+ let srcDir = item. appendingPathComponent ( " src " )
1133+ if FileManager . default. fileExists ( atPath: srcDir. path) {
1134+ let srcContents = try ? FileManager . default. contentsOfDirectory (
1135+ at: srcDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles]
1136+ )
1137+
1138+ let cargoInSrc = srcDir. appendingPathComponent ( " Cargo.toml " )
1139+ if FileManager . default. fileExists ( atPath: cargoInSrc. path) {
1140+ let outputTargetDir = outputPluginDir. appendingPathComponent ( " target/release " )
1141+ try compileRustPlugin ( projectDir: srcDir, outputDir: outputTargetDir, pluginName: pluginName)
1142+ return
1143+ }
1144+
1145+ let cFiles = srcContents? . filter { $0. pathExtension == " c " } ?? [ ]
1146+ if !cFiles. isEmpty {
1147+ let outputSrcDir = outputPluginDir. appendingPathComponent ( " src " )
1148+ try FileManager . default. createDirectory ( at: outputSrcDir, withIntermediateDirectories: true )
1149+ let outputLibPath = outputSrcDir. appendingPathComponent ( " \( pluginName) . \( libraryExtension) " )
1150+ try compileCPlugin ( sources: cFiles, output: outputLibPath)
1151+ }
1152+ }
1153+
1154+ // Check for Cargo.toml in root
1155+ let rootCargoToml = item. appendingPathComponent ( " Cargo.toml " )
1156+ if FileManager . default. fileExists ( atPath: rootCargoToml. path) {
1157+ let outputTargetDir = outputPluginDir. appendingPathComponent ( " target/release " )
1158+ try compileRustPlugin ( projectDir: item, outputDir: outputTargetDir, pluginName: pluginName)
1159+ }
1160+
1161+ // Python plugins — copy only
1162+ if FileManager . default. fileExists ( atPath: srcDir. path) {
1163+ let srcContents = try ? FileManager . default. contentsOfDirectory (
1164+ at: srcDir, includingPropertiesForKeys: nil , options: [ . skipsHiddenFiles]
1165+ )
1166+ let pyFiles = srcContents? . filter { $0. pathExtension == " py " } ?? [ ]
1167+ if !pyFiles. isEmpty {
1168+ let outputSrcDir = outputPluginDir. appendingPathComponent ( " src " )
1169+ if !FileManager. default. fileExists ( atPath: outputSrcDir. path) {
1170+ try FileManager . default. copyItem ( at: srcDir, to: outputSrcDir)
1171+ }
1172+ let requirementsFile = item. appendingPathComponent ( " requirements.txt " )
1173+ if FileManager . default. fileExists ( atPath: requirementsFile. path) {
1174+ let outputReq = outputPluginDir. appendingPathComponent ( " requirements.txt " )
1175+ if !FileManager. default. fileExists ( atPath: outputReq. path) {
1176+ try FileManager . default. copyItem ( at: requirementsFile, to: outputReq)
1177+ }
1178+ }
1179+ }
1180+ }
1181+
1182+ // Copy features/ directory if present
1183+ let featuresDir = item. appendingPathComponent ( " features " )
1184+ if FileManager . default. fileExists ( atPath: featuresDir. path) {
1185+ let outputFeaturesDir = outputPluginDir. appendingPathComponent ( " features " )
1186+ if !FileManager. default. fileExists ( atPath: outputFeaturesDir. path) {
1187+ try FileManager . default. copyItem ( at: featuresDir, to: outputFeaturesDir)
1188+ }
1189+ }
1190+ }
1191+
9431192 /// Compile multiple Swift files to a dynamic library
9441193 private func compileMultipleSwiftFiles( sources: [ URL ] , output: URL ) throws {
9451194 guard let swiftc = findSwiftCompiler ( ) else {
0 commit comments