diff --git a/Package.swift b/Package.swift index a9f02ac0..ecb6de32 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,9 @@ let package = Package( ], targets: [ .target(name: "RswiftResources"), + .target(name: "RswiftShared"), .target(name: "RswiftGenerators", dependencies: ["RswiftResources"]), - .target(name: "RswiftParsers", dependencies: ["RswiftResources", "XcodeEdit"]), + .target(name: "RswiftParsers", dependencies: ["RswiftResources", "RswiftShared", "XcodeEdit"]), .testTarget(name: "RswiftGeneratorsTests", dependencies: ["RswiftGenerators"]), .testTarget(name: "RswiftParsersTests", dependencies: ["RswiftParsers"]), @@ -33,6 +34,7 @@ let package = Package( .executableTarget(name: "rswift", dependencies: [ .target(name: "RswiftParsers"), .target(name: "RswiftGenerators"), + .target(name: "RswiftShared"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), diff --git a/Plugins/RswiftGenerateInternalResources/RswiftGenerateInternalResources.swift b/Plugins/RswiftGenerateInternalResources/RswiftGenerateInternalResources.swift index bdefb5b9..1aba9118 100644 --- a/Plugins/RswiftGenerateInternalResources/RswiftGenerateInternalResources.swift +++ b/Plugins/RswiftGenerateInternalResources/RswiftGenerateInternalResources.swift @@ -13,33 +13,56 @@ struct RswiftGenerateInternalResources: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { guard let target = target as? SourceModuleTarget else { return [] } - let outputDirectoryPath = context.pluginWorkDirectory - .appending(subpath: target.name) + let defaultOutputDirectoryPath = context.pluginWorkDirectory.appending(subpath: target.name) + let rswiftPath = defaultOutputDirectoryPath.appending(subpath: "R.generated.swift") - try FileManager.default.createDirectory(atPath: outputDirectoryPath.string, withIntermediateDirectories: true) + let optionsFile = URL(fileURLWithPath: target.directory.appending(subpath: ".rswiftoptions").string) - let rswiftPath = outputDirectoryPath.appending(subpath: "R.generated.swift") + // Our base set of options contains only an access level of `internal` given that + // this is the "internal" resources plugin, hence the access level should always be + // `internal`. + let options = RswiftOptions(accessLevel: .internalLevel) - let sourceFiles = target.sourceFiles + // Next we load and merge any options that may be present in an options file. These + // options don't override any of the previous options provided, but supplements + // those options. + .merging(with: try .init(contentsOf: optionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(bundleSource: target.kind == .generic ? .module : .finder, + outputPath: rswiftPath.string)) + + // Get a concrete reference to the file we'll be writing out to + let outputPath = options.outputPath.map { $0.hasPrefix("/") ? Path($0) : defaultOutputDirectoryPath.appending(subpath: $0) } ?? rswiftPath + + // Create the output directory, if needed + try FileManager.default.createDirectory(atPath: outputPath.removingLastComponent().string, + withIntermediateDirectories: true) + + // Get the input files for the current target being processed + let sourceFiles: [String] = target.sourceFiles .filter { $0.type == .resource || $0.type == .unknown } .map(\.path.string) - let inputFilesArguments = sourceFiles + let inputFilesArguments: [String] = sourceFiles .flatMap { ["--input-files", $0 ] } - let bundleSource = target.kind == .generic ? "module" : "finder" - let description = "\(target.kind) module \(target.name)" + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: target.directory.string), + outputDirectory: URL(fileURLWithPath: outputPath.removingLastComponent().string)) + + ["--input-type", "input-files"] + inputFilesArguments + // Return the build command to execute return [ .buildCommand( - displayName: "R.swift generate resources for \(description)", + displayName: "R.swift generate resources for \(target.kind) module \(target.name)", executable: try context.tool(named: "rswift").path, - arguments: [ - "generate", rswiftPath.string, - "--input-type", "input-files", - "--bundle-source", bundleSource, - ] + inputFilesArguments, - outputFiles: [rswiftPath] + arguments: arguments, + outputFiles: [outputPath] ), ] } @@ -51,13 +74,50 @@ import XcodeProjectPlugin extension RswiftGenerateInternalResources: XcodeBuildToolPlugin { func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { - let resourcesDirectoryPath = context.pluginWorkDirectory + let defaultOutputDirectoryPath = context.pluginWorkDirectory .appending(subpath: target.displayName) .appending(subpath: "Resources") - try FileManager.default.createDirectory(atPath: resourcesDirectoryPath.string, withIntermediateDirectories: true) + let rswiftPath = defaultOutputDirectoryPath.appending(subpath: "R.generated.swift") + + let projectOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: ".rswiftoptions").string) + let targetOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: target.displayName).appending(subpath: ".rswiftoptions").string) + + // Our base set of options contains an access level of `internal` given that this + // is the "internal" resources plugin, hence the access level should always be + // `internal`, as well as the bundle source, which is always `finder` for Xcode + // projects. + let options = RswiftOptions(accessLevel: .internalLevel, + bundleSource: .finder) + + // Next we load and merge any options that may be present in an options file + // specific to the target being processed. These options don't override any of the + // previous options provided, but supplements those options. + .merging(with: try .init(contentsOf: targetOptionsFile)) + + // Next we load and merge any options that may be present in an options file that + // applies to the entire project. These options don't override any of the previous + // options provided, but supplements those options. + .merging(with: try .init(contentsOf: projectOptionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(outputPath: rswiftPath.string)) + + // Get a concrete reference to the file we'll be writing out to + let outputPath = options.outputPath.map { $0.hasPrefix("/") ? Path($0) : defaultOutputDirectoryPath.appending(subpath: $0) } ?? rswiftPath + + // Create the output directory, if needed + try FileManager.default.createDirectory(atPath: outputPath.removingLastComponent().string, + withIntermediateDirectories: true) - let rswiftPath = resourcesDirectoryPath.appending(subpath: "R.generated.swift") + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: context.xcodeProject.directory.string), + outputDirectory: URL(fileURLWithPath: outputPath.removingLastComponent().string)) + + ["--input-type", "xcodeproj"] let description: String if let product = target.product { @@ -66,17 +126,13 @@ extension RswiftGenerateInternalResources: XcodeBuildToolPlugin { description = target.displayName } + // Return the build command to execute return [ .buildCommand( displayName: "R.swift generate resources for \(description)", executable: try context.tool(named: "rswift").path, - arguments: [ - "generate", rswiftPath.string, - "--target", target.displayName, - "--input-type", "xcodeproj", - "--bundle-source", "finder", - ], - outputFiles: [rswiftPath] + arguments: arguments, + outputFiles: [outputPath] ), ] } diff --git a/Plugins/RswiftGenerateInternalResources/RswiftShared b/Plugins/RswiftGenerateInternalResources/RswiftShared new file mode 120000 index 00000000..b892221a --- /dev/null +++ b/Plugins/RswiftGenerateInternalResources/RswiftShared @@ -0,0 +1 @@ +../../Sources/RswiftShared \ No newline at end of file diff --git a/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift b/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift index 9efb45e4..3066aafe 100644 --- a/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift +++ b/Plugins/RswiftGeneratePublicResources/RswiftGeneratePublicResources.swift @@ -13,34 +13,56 @@ struct RswiftGeneratePublicResources: BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { guard let target = target as? SourceModuleTarget else { return [] } - let outputDirectoryPath = context.pluginWorkDirectory - .appending(subpath: target.name) + let defaultOutputDirectoryPath = context.pluginWorkDirectory.appending(subpath: target.name) + let rswiftPath = defaultOutputDirectoryPath.appending(subpath: "R.generated.swift") - try FileManager.default.createDirectory(atPath: outputDirectoryPath.string, withIntermediateDirectories: true) + let optionsFile = URL(fileURLWithPath: target.directory.appending(subpath: ".rswiftoptions").string) - let rswiftPath = outputDirectoryPath.appending(subpath: "R.generated.swift") + // Our base set of options contains only an access level of `public` given that + // this is the "public" resources plugin, hence the access level should always be + // `public`. + let options = RswiftOptions(accessLevel: .publicLevel) - let sourceFiles = target.sourceFiles + // Next we load and merge any options that may be present in an options file. These + // options don't override any of the previous options provided, but supplements + // those options. + .merging(with: try .init(contentsOf: optionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(bundleSource: target.kind == .generic ? .module : .finder, + outputPath: rswiftPath.string)) + + // Get a concrete reference to the file we'll be writing out to + let outputPath = options.outputPath.map { $0.hasPrefix("/") ? Path($0) : defaultOutputDirectoryPath.appending(subpath: $0) } ?? rswiftPath + + // Create the output directory, if needed + try FileManager.default.createDirectory(atPath: outputPath.removingLastComponent().string, + withIntermediateDirectories: true) + + // Get the input files for the current target being processed + let sourceFiles: [String] = target.sourceFiles .filter { $0.type == .resource || $0.type == .unknown } .map(\.path.string) - let inputFilesArguments = sourceFiles + let inputFilesArguments: [String] = sourceFiles .flatMap { ["--input-files", $0 ] } - let bundleSource = target.kind == .generic ? "module" : "finder" - let description = "\(target.kind) module \(target.name)" + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: target.directory.string), + outputDirectory: URL(fileURLWithPath: outputPath.removingLastComponent().string)) + + ["--input-type", "input-files"] + inputFilesArguments + // Return the build command to execute return [ .buildCommand( - displayName: "R.swift generate resources for \(description)", + displayName: "R.swift generate resources for \(target.kind) module \(target.name)", executable: try context.tool(named: "rswift").path, - arguments: [ - "generate", rswiftPath.string, - "--input-type", "input-files", - "--bundle-source", bundleSource, - "--access-level", "public", - ] + inputFilesArguments, - outputFiles: [rswiftPath] + arguments: arguments, + outputFiles: [outputPath] ), ] } @@ -52,13 +74,49 @@ import XcodeProjectPlugin extension RswiftGeneratePublicResources: XcodeBuildToolPlugin { func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] { - let resourcesDirectoryPath = context.pluginWorkDirectory + let defaultOutputDirectoryPath = context.pluginWorkDirectory .appending(subpath: target.displayName) .appending(subpath: "Resources") - try FileManager.default.createDirectory(atPath: resourcesDirectoryPath.string, withIntermediateDirectories: true) + let rswiftPath = defaultOutputDirectoryPath.appending(subpath: "R.generated.swift") + + let projectOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: ".rswiftoptions").string) + let targetOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: target.displayName).appending(subpath: ".rswiftoptions").string) + + // Our base set of options contains an access level of `public` given that this is + // the "public" resources plugin, hence the access level should always be `public`, + // as well as the bundle source, which is always `finder` for Xcode projects. + let options = RswiftOptions(accessLevel: .publicLevel, + bundleSource: .finder) + + // Next we load and merge any options that may be present in an options file + // specific to the target being processed. These options don't override any of the + // previous options provided, but supplements those options. + .merging(with: try .init(contentsOf: targetOptionsFile)) + + // Next we load and merge any options that may be present in an options file that + // applies to the entire project. These options don't override any of the previous + // options provided, but supplements those options. + .merging(with: try .init(contentsOf: projectOptionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(outputPath: rswiftPath.string)) + + // Get a concrete reference to the file we'll be writing out to + let outputPath = options.outputPath.map { $0.hasPrefix("/") ? Path($0) : defaultOutputDirectoryPath.appending(subpath: $0) } ?? rswiftPath + + // Create the output directory, if needed + try FileManager.default.createDirectory(atPath: outputPath.removingLastComponent().string, + withIntermediateDirectories: true) - let rswiftPath = resourcesDirectoryPath.appending(subpath: "R.generated.swift") + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: context.xcodeProject.directory.string), + outputDirectory: URL(fileURLWithPath: outputPath.removingLastComponent().string)) + + ["--input-type", "xcodeproj"] let description: String if let product = target.product { @@ -67,18 +125,13 @@ extension RswiftGeneratePublicResources: XcodeBuildToolPlugin { description = target.displayName } + // Return the build command to execute return [ .buildCommand( displayName: "R.swift generate resources for \(description)", executable: try context.tool(named: "rswift").path, - arguments: [ - "generate", rswiftPath.string, - "--target", target.displayName, - "--input-type", "xcodeproj", - "--bundle-source", "finder", - "--access-level", "public", - ], - outputFiles: [rswiftPath] + arguments: arguments, + outputFiles: [outputPath] ), ] } diff --git a/Plugins/RswiftGeneratePublicResources/RswiftShared b/Plugins/RswiftGeneratePublicResources/RswiftShared new file mode 120000 index 00000000..b892221a --- /dev/null +++ b/Plugins/RswiftGeneratePublicResources/RswiftShared @@ -0,0 +1 @@ +../../Sources/RswiftShared \ No newline at end of file diff --git a/Plugins/RswiftGenerateResourcesCommand/RswiftGenerateResourcesCommand.swift b/Plugins/RswiftGenerateResourcesCommand/RswiftGenerateResourcesCommand.swift index a9b05815..c786f6ab 100644 --- a/Plugins/RswiftGenerateResourcesCommand/RswiftGenerateResourcesCommand.swift +++ b/Plugins/RswiftGenerateResourcesCommand/RswiftGenerateResourcesCommand.swift @@ -14,29 +14,49 @@ struct RswiftGenerateResourcesCommand: CommandPlugin { let rswift = try context.tool(named: "rswift") let parsedArguments = ParsedArguments.parse(arguments: externalArgs) - let outputSubpath = parsedArguments.outputFile ?? "R.generated.swift" for target in context.package.targets { guard let target = target as? SourceModuleTarget else { continue } guard parsedArguments.targets.contains(target.name) || parsedArguments.targets.isEmpty else { continue } - let outputPath = target.directory.appending(subpath: outputSubpath) + let optionsFile = URL(fileURLWithPath: target.directory.appending(subpath: ".rswiftoptions").string) - let sourceFiles = target.sourceFiles + // Our base set of options contains just the output file. We do this since if when + // running the plugin an explicit output file was provided, this should take + // precedence any other argument that might have been provided by other means. + let options = RswiftOptions(outputPath: parsedArguments.outputFile) + + // Next we merge in the "remaining" arguments that were provided when invoking the + // plugin. Like with the output file, these should take precedence over any other + // arguments provided by other means. + .merging(with: try .init(from: parsedArguments.remaining)) + + // Next we load and merge any options that may be present in an options file. These + // options don't override any of the previous options provided, but supplements + // those options. + .merging(with: try .init(contentsOf: optionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(bundleSource: target.kind == .generic ? .module : .finder, + outputPath: "R.generated.swift")) + + // Get the input files for the current target being processed + let sourceFiles: [String] = target.sourceFiles .filter { $0.type == .resource || $0.type == .unknown } .map(\.path.string) - let inputFilesArguments = sourceFiles + let inputFilesArguments: [String] = sourceFiles .flatMap { ["--input-files", $0 ] } - let bundleSource = target.kind == .generic ? "module" : "finder" - - let arguments: [String] = [ - "generate", outputPath.string, - "--input-type", "input-files", - "--bundle-source", bundleSource, - ] + inputFilesArguments + parsedArguments.remaining + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: target.directory.string)) + + ["--input-type", "input-files"] + inputFilesArguments + // Finally, run the `rswift` utility do { try rswift.run(arguments: arguments, environment: nil) } catch let error as RunError { @@ -55,25 +75,52 @@ extension RswiftGenerateResourcesCommand: XcodeCommandPlugin { let rswift = try context.tool(named: "rswift") let parsedArguments = ParsedArguments.parse(arguments: externalArgs) - let outputSubpath = parsedArguments.outputFile ?? "R.generated.swift" for target in context.xcodeProject.targets { guard parsedArguments.targets.contains(target.displayName) || parsedArguments.targets.isEmpty else { continue } - let outputPath = context.xcodeProject.directory.appending(subpath: outputSubpath) + let projectOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: ".rswiftoptions").string) + let targetOptionsFile = URL(fileURLWithPath: context.xcodeProject.directory.appending(subpath: target.displayName).appending(subpath: ".rswiftoptions").string) + + // Our base set of options contains just the output file. We do this since if when + // running the plugin an explicit output file was provided, this should take + // precedence any other argument that might have been provided by other means. + let options = RswiftOptions(outputPath: parsedArguments.outputFile) + + // Next we merge in the "remaining" arguments that were provided when invoking the + // plugin. Like with the output file, these should take precedence over any other + // arguments provided by other means. + .merging(with: try .init(from: parsedArguments.remaining)) + + // Next we load and merge any options that may be present in an options file + // specific to the target being processed. These options don't override any of the + // previous options provided, but supplements those options. + .merging(with: try .init(contentsOf: targetOptionsFile)) + + // Next we load and merge any options that may be present in an options file that + // applies to the entire project. These options don't override any of the previous + // options provided, but supplements those options. + .merging(with: try .init(contentsOf: projectOptionsFile)) + + // Lastly, we provide a fallback bundle source and output file to ensure that these + // values are always set should no other values be provided + .merging(with: .init(bundleSource: .finder, + outputPath: "R.generated.swift")) - let sourceFiles = target.inputFiles + // Get the input files for the current target being processed + let sourceFiles: [String] = target.inputFiles .filter { $0.type == .resource || $0.type == .unknown } .map(\.path.string) - let inputFilesArguments = sourceFiles + let inputFilesArguments: [String] = sourceFiles .flatMap { ["--input-files", $0 ] } - let arguments: [String] = [ - "generate", outputPath.string, - "--input-type", "input-files", - "--bundle-source", "finder", - ] + inputFilesArguments + parsedArguments.remaining + // Lastly, convert the options struct into an array of arguments, subsequently + // appending the input type and files, all of which will be provided to the + // `rswift` utility that we'll invoke. + let arguments: [String] = + options.makeArguments(sourceDirectory: URL(fileURLWithPath: context.xcodeProject.directory.string)) + + ["--input-type", "input-files"] + inputFilesArguments var environment: [String: String] = [ "SOURCE_ROOT": context.xcodeProject.directory.string, diff --git a/Plugins/RswiftGenerateResourcesCommand/RswiftShared b/Plugins/RswiftGenerateResourcesCommand/RswiftShared new file mode 120000 index 00000000..b892221a --- /dev/null +++ b/Plugins/RswiftGenerateResourcesCommand/RswiftShared @@ -0,0 +1 @@ +../../Sources/RswiftShared \ No newline at end of file diff --git a/Sources/RswiftParsers/ProjectResources.swift b/Sources/RswiftParsers/ProjectResources.swift index 2fdee3a6..b41dda85 100644 --- a/Sources/RswiftParsers/ProjectResources.swift +++ b/Sources/RswiftParsers/ProjectResources.swift @@ -8,23 +8,7 @@ import Foundation import XcodeEdit import RswiftResources - -public enum ResourceType: String, CaseIterable { - case image - case string - case color - case data - case file - case font - case nib - case segue - case storyboard - case reuseIdentifier - case entitlements - case info - case id - case project -} +import RswiftShared public struct ProjectResources { public let assetCatalogs: [AssetCatalog] diff --git a/Sources/RswiftShared/AccessLevel.swift b/Sources/RswiftShared/AccessLevel.swift new file mode 100644 index 00000000..dad82acb --- /dev/null +++ b/Sources/RswiftShared/AccessLevel.swift @@ -0,0 +1,13 @@ +// +// AccessLevel.swift +// R.swift +// +// Created by Joe Newton on 2024-07-11. +// + +public enum AccessLevel: String, Decodable { + case publicLevel = "public" + case internalLevel = "internal" + case filePrivate = "fileprivate" + case privateLevel = "private" +} diff --git a/Sources/RswiftShared/BundleSource.swift b/Sources/RswiftShared/BundleSource.swift new file mode 100644 index 00000000..d4d0edb6 --- /dev/null +++ b/Sources/RswiftShared/BundleSource.swift @@ -0,0 +1,11 @@ +// +// BundleSource.swift +// R.swift +// +// Created by Joe Newton on 2024-07-11. +// + +public enum BundleSource: String, Decodable { + case module + case finder +} diff --git a/Sources/RswiftShared/ResourceType.swift b/Sources/RswiftShared/ResourceType.swift new file mode 100644 index 00000000..e8b567dc --- /dev/null +++ b/Sources/RswiftShared/ResourceType.swift @@ -0,0 +1,23 @@ +// +// ResourceType.swift +// R.swift +// +// Created by Joe Newton on 2024-07-11. +// + +public enum ResourceType: String, CaseIterable, Decodable { + case image + case string + case color + case data + case file + case font + case nib + case segue + case storyboard + case reuseIdentifier + case entitlements + case info + case id + case project +} diff --git a/Sources/RswiftShared/RswiftOptions.swift b/Sources/RswiftShared/RswiftOptions.swift new file mode 100644 index 00000000..84026420 --- /dev/null +++ b/Sources/RswiftShared/RswiftOptions.swift @@ -0,0 +1,132 @@ +// +// RswiftOptions.swift +// R.swift +// +// Created by Joe Newton on 2024-07-11. +// + +import Foundation + +struct RswiftOptions: Decodable { + let generators: [ResourceType]? + let omitMainLet: Bool? + let imports: [String]? + let accessLevel: AccessLevel? + let rswiftignore: String? + let bundleSource: BundleSource? + let outputPath: String? + let additionalArguments: [String]? + + init?(contentsOf url: URL) throws { + guard FileManager.default.fileExists(atPath: url.path) else { return nil } + self = try JSONDecoder().decode(RswiftOptions.self, from: Data(contentsOf: url)) + } + + init(from arguments: [String]) throws { + var structuredArguments: [String: Any] = [:] + + // We first iterate over all of the provided arguments to put them into a + // dictionary structure. + var i = arguments.startIndex + while i < arguments.endIndex { + let argument = arguments[i] + if argument == "--omit-main-let" { + structuredArguments["omit-main-let"] = true + } else if argument.hasPrefix("--") { + if arguments.index(after: i) == arguments.endIndex { + // This is the very last argument provided + structuredArguments["additional-arguments"] = (structuredArguments["additional-arguments"] as? [String] ?? []) + [argument] + } else { + // We have at least one argument that comes after this one + let key = argument == "--import" ? "imports" : String(argument.dropFirst(2)) + i = arguments.index(after: i) // Move the index to the argument's value + + if key == "imports" || key == "generators" { + // These keys represent arrays, so we'll append to that array + structuredArguments[key] = (structuredArguments[key] as? [String] ?? []) + [arguments[i]] + } else { + structuredArguments[key] = arguments[i] + } + } + } else { + structuredArguments["additional-arguments"] = (structuredArguments["additional-arguments"] as? [String] ?? []) + [argument] + } + + i = arguments.index(after: i) + } + + if structuredArguments.isEmpty { + // No options parsed out, simply delegate to the default initializer + self.init() + } else { + // Now that we have a dictionary structure, we can attempt to serialize the + // dictionary into data that we can then attempt to decode. + let encodedArguments = try JSONSerialization.data(withJSONObject: structuredArguments) + self = try JSONDecoder().decode(RswiftOptions.self, from: encodedArguments) + } + } + + init(generators: [ResourceType]? = nil, + omitMainLet: Bool? = nil, + imports: [String]? = nil, + accessLevel: AccessLevel? = nil, + rswiftignore: String? = nil, + bundleSource: BundleSource? = nil, + outputPath: String? = nil, + additionalArguments: [String]? = nil) { + self.generators = generators + self.omitMainLet = omitMainLet + self.imports = imports + self.accessLevel = accessLevel + self.rswiftignore = rswiftignore + self.bundleSource = bundleSource + self.outputPath = outputPath + self.additionalArguments = additionalArguments + } + + func merging(with options: RswiftOptions?) -> RswiftOptions { + guard let options else { return self } + return RswiftOptions( + generators: generators.flatMap { $0.isEmpty ? nil : $0 } ?? options.generators, + omitMainLet: omitMainLet ?? options.omitMainLet, + imports: imports.flatMap { $0.isEmpty ? nil : $0 } ?? options.imports, + accessLevel: accessLevel ?? options.accessLevel, + rswiftignore: rswiftignore ?? options.rswiftignore, + bundleSource: bundleSource ?? options.bundleSource, + outputPath: outputPath ?? options.outputPath, + additionalArguments: additionalArguments.map { $0 + (options.additionalArguments ?? []) } ?? options.additionalArguments + ) + } + + func makeArguments(command: String = "generate", + sourceDirectory: URL, + outputDirectory: URL? = nil, + fallbackOutputPath: String = "R.generated.swift") -> [String] { + let outputDirectory = outputDirectory ?? sourceDirectory + let outputPath = outputPath ?? fallbackOutputPath + var arguments: [String] = [ + command, outputPath.hasPrefix("/") ? outputPath : outputDirectory.appendingPathComponent(outputPath).path + ] + + arguments += (generators ?? []).flatMap { ["--generators", $0.rawValue] } + arguments += omitMainLet == true ? ["--omit-main-let"] : [] + arguments += (imports ?? []).flatMap { ["--import", $0] } + arguments += accessLevel.map { ["--access-level", $0.rawValue] } ?? [] + arguments += rswiftignore.map { ["--rswiftignore", $0.starts(with: "/") ? $0 : sourceDirectory.appendingPathComponent($0).path] } ?? [] + arguments += bundleSource.map { ["--bundle-source", $0.rawValue] } ?? [] + arguments += additionalArguments ?? [] + + return arguments + } + + enum CodingKeys: String, CodingKey { + case generators + case omitMainLet = "omit-main-let" + case imports + case accessLevel = "access-level" + case rswiftignore + case bundleSource = "bundle-source" + case outputPath = "output-path" + case additionalArguments = "additional-arguments" + } +} diff --git a/Sources/rswift/App.swift b/Sources/rswift/App.swift index 3c7321c1..d86130be 100644 --- a/Sources/rswift/App.swift +++ b/Sources/rswift/App.swift @@ -8,9 +8,9 @@ import ArgumentParser import Foundation import RswiftParsers +import RswiftShared import XcodeEdit - @main struct App: ParsableCommand { static var configuration = CommandConfiguration( @@ -211,3 +211,7 @@ extension ProcessInfo { return value } } + +extension ResourceType: ExpressibleByArgument {} +extension AccessLevel: ExpressibleByArgument {} +extension BundleSource: ExpressibleByArgument {} diff --git a/Sources/rswift/RswiftCore.swift b/Sources/rswift/RswiftCore.swift index 91c1799d..add02306 100644 --- a/Sources/rswift/RswiftCore.swift +++ b/Sources/rswift/RswiftCore.swift @@ -11,20 +11,7 @@ import XcodeEdit import RswiftParsers import RswiftResources import RswiftGenerators - -extension ResourceType: ExpressibleByArgument {} - -public enum AccessLevel: String, ExpressibleByArgument { - case publicLevel = "public" - case internalLevel = "internal" - case filePrivate = "fileprivate" - case privateLevel = "private" -} - -public enum BundleSource: String, ExpressibleByArgument { - case module - case finder -} +import RswiftShared public struct RswiftCore { let outputURL: URL