mirror of
https://github.com/krzysztofzablocki/Sourcery.git
synced 2026-04-07 19:17:40 +00:00
430 lines
20 KiB
Swift
430 lines
20 KiB
Swift
//
|
|
// main.swift
|
|
// Sourcery
|
|
//
|
|
// Created by Krzysztof Zablocki on 09/12/2016.
|
|
// Copyright © 2016 Pixle. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import Commander
|
|
import PathKit
|
|
import SourceryRuntime
|
|
import SourceryFramework
|
|
import SourceryUtils
|
|
import SourceryJS
|
|
import SourceryLib
|
|
|
|
extension Path: ArgumentConvertible {
|
|
/// :nodoc:
|
|
public init(parser: ArgumentParser) throws {
|
|
if let path = parser.shift() {
|
|
self.init(path)
|
|
} else {
|
|
throw ArgumentError.missingValue(argument: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum Validators {
|
|
static func isReadable(path: Path) -> Path {
|
|
if !path.isReadable {
|
|
Log.error("'\(path)' does not exist or is not readable.")
|
|
exit(.invalidePath)
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
static func isFileOrDirectory(path: Path) -> Path {
|
|
_ = isReadable(path: path)
|
|
|
|
if !(path.isDirectory || path.isFile) {
|
|
Log.error("'\(path)' isn't a directory or proper file.")
|
|
exit(.invalidePath)
|
|
}
|
|
|
|
return path
|
|
}
|
|
|
|
static func isWritable(path: Path) -> Path {
|
|
if path.exists && !path.isWritable {
|
|
Log.error("'\(path)' isn't writable.")
|
|
exit(.invalidePath)
|
|
}
|
|
return path
|
|
}
|
|
}
|
|
|
|
extension Configuration {
|
|
|
|
func validate() {
|
|
guard !source.isEmpty else {
|
|
Log.error("No sources provided.")
|
|
exit(.invalidConfig)
|
|
}
|
|
if case let .sources(sources) = source {
|
|
_ = sources.allPaths.map(Validators.isReadable(path:))
|
|
}
|
|
|
|
guard !templates.isEmpty else {
|
|
Log.error("No templates provided.")
|
|
exit(.invalidConfig)
|
|
}
|
|
_ = templates.allPaths.map(Validators.isReadable(path:))
|
|
_ = output.path.map(Validators.isWritable(path:))
|
|
}
|
|
|
|
}
|
|
|
|
enum ExitCode: Int32 {
|
|
case invalidePath = 1
|
|
case invalidConfig
|
|
case other
|
|
}
|
|
|
|
private func exit(_ code: ExitCode) -> Never {
|
|
exit(code.rawValue)
|
|
}
|
|
|
|
#if canImport(ObjectiveC)
|
|
func runCLI() {
|
|
command(
|
|
Flag("watch", flag: "w", description: "Watch template for changes and regenerate as needed."),
|
|
Flag("disableCache", description: "Stops using cache."),
|
|
Flag("verbose", flag: "v", description: "Turn on verbose logging, this causes to log everything we can."),
|
|
Flag("logAST", description: "Log AST messages"),
|
|
Flag("logBenchmarks", description: "Log time benchmark info"),
|
|
Flag("parseDocumentation", description: "Include documentation comments for all declarations."),
|
|
Flag("quiet", flag: "q", description: "Turn off any logging, only emmit errors."),
|
|
Flag("prune", flag: "p", description: "Remove empty generated files"),
|
|
Flag("serialParse", description: "Parses the specified sources in serial, rather than in parallel (the default), which can address stability issues in SwiftSyntax."),
|
|
VariadicOption<Path>("sources", description: "Path to a source swift files. File or Directory."),
|
|
VariadicOption<Path>("exclude-sources", description: "Path to a source swift files to exclude. File or Directory."),
|
|
VariadicOption<Path>("templates", description: "Path to templates. File or Directory."),
|
|
VariadicOption<Path>("exclude-templates", description: "Path to templates to exclude. File or Directory."),
|
|
Option<Path>("output", default: "", description: "Path to output. File or Directory. Default is current path."),
|
|
Flag("dry", default: false, description: "Dry run, without file system modifications, will output result and errors in JSON format. Not working with --watch."),
|
|
VariadicOption<Path>("config", default: ["."], description: "Path to config file. File or Directory. Default is current path."),
|
|
VariadicOption<String>("force-parse", description: "File extensions that Sourcery will be forced to parse, even if they were generated by Sourcery."),
|
|
Option<Int>("base-indentation", default: 0, description: "Base indendation to add to sourcery:auto fragments."),
|
|
VariadicOption<String>("args", description:
|
|
"""
|
|
Additional arguments to pass to templates. Each argument can have an explicit value or will have \
|
|
an implicit `true` value. Arguments should be comma-separated without spaces (e.g. --args arg1=value,arg2) \
|
|
or should be passed one by one (e.g. --args arg1=value --args arg2). Arguments are accessible in templates \
|
|
via `argument.<name>`. To pass in string you should use escaped quotes (\\").
|
|
"""),
|
|
Option<Path>("ejsPath", default: "", description: "Path to EJS file for JavaScript templates."),
|
|
Option<Path>("cacheBasePath", default: "", description: "Base path to Sourcery's cache directory"),
|
|
Option<Path>("buildPath", default: "", description: "Sets a custom build path"),
|
|
Flag("hideVersionHeader", description: "Do not include Sourcery version in the generated files headers."),
|
|
Option<String?>("headerPrefix", default: nil, description: "Additional prefix for headers.")
|
|
) { watcherEnabled, disableCache, verboseLogging, logAST, logBenchmark, parseDocumentation, quiet, prune, serialParse, sources, excludeSources, templates, excludeTemplates, output, isDryRun, configPaths, forceParse, baseIndentation, args, ejsPath, cacheBasePath, buildPath, hideVersionHeader, headerPrefix in
|
|
do {
|
|
let logConfiguration = Log.Configuration(
|
|
isDryRun: isDryRun,
|
|
isQuiet: quiet,
|
|
isVerboseLoggingEnabled: verboseLogging,
|
|
isLogBenchmarkEnabled: logBenchmark,
|
|
shouldLogAST: logAST
|
|
)
|
|
Log.setup(using: logConfiguration)
|
|
|
|
// if ejsPath is not provided use default value or executable path
|
|
EJSTemplate.ejsPath = ejsPath.string.isEmpty
|
|
? (EJSTemplate.ejsPath ?? Path(ProcessInfo.processInfo.arguments[0]).parent() + "ejs.js")
|
|
: ejsPath
|
|
|
|
let configurations = configPaths.flatMap { configPath -> [Configuration] in
|
|
let yamlPath: Path = configPath.isDirectory ? configPath + ".sourcery.yml" : configPath
|
|
|
|
if !yamlPath.exists {
|
|
Log.info("No config file provided or it does not exist. Using command line arguments.")
|
|
let args = args.joined(separator: ",")
|
|
let arguments = AnnotationsParser.parse(line: args)
|
|
return [
|
|
Configuration(
|
|
sources: Paths(include: sources, exclude: excludeSources) ,
|
|
templates: Paths(include: templates, exclude: excludeTemplates),
|
|
output: output.string.isEmpty ? "." : output,
|
|
cacheBasePath: cacheBasePath.string.isEmpty ? Path.defaultBaseCachePath : cacheBasePath,
|
|
forceParse: forceParse,
|
|
parseDocumentation: parseDocumentation,
|
|
baseIndentation: baseIndentation,
|
|
args: arguments
|
|
)
|
|
]
|
|
} else {
|
|
_ = Validators.isFileOrDirectory(path: configPath)
|
|
_ = Validators.isReadable(path: yamlPath)
|
|
|
|
do {
|
|
let relativePath: Path = configPath.isDirectory ? configPath : configPath.parent()
|
|
|
|
// Check if the user is passing parameters
|
|
// that are ignored cause read from the yaml file
|
|
let hasAnyYamlDuplicatedParameter = (
|
|
!sources.isEmpty ||
|
|
!excludeSources.isEmpty ||
|
|
!templates.isEmpty ||
|
|
!excludeTemplates.isEmpty ||
|
|
!forceParse.isEmpty ||
|
|
output != "" ||
|
|
!args.isEmpty
|
|
)
|
|
|
|
if hasAnyYamlDuplicatedParameter {
|
|
Log.info("Using configuration file at '\(yamlPath)'. WARNING: Ignoring the parameters passed in the command line.")
|
|
} else {
|
|
Log.info("Using configuration file at '\(yamlPath)'")
|
|
}
|
|
|
|
return try Configurations.make(
|
|
path: yamlPath,
|
|
relativePath: relativePath,
|
|
env: ProcessInfo.processInfo.environment
|
|
)
|
|
} catch {
|
|
Log.error("while reading .yml '\(yamlPath)'. '\(error)'")
|
|
exit(.invalidConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
let start = currentTimestamp()
|
|
|
|
let keepAlive = try configurations.flatMap { configuration -> [FolderWatcher.Local] in
|
|
configuration.validate()
|
|
|
|
let shouldUseCacheBasePathArg = configuration.cacheBasePath == Path.defaultBaseCachePath && !cacheBasePath.string.isEmpty
|
|
|
|
let sourcery = Sourcery(verbose: verboseLogging,
|
|
watcherEnabled: watcherEnabled,
|
|
cacheDisabled: disableCache,
|
|
cacheBasePath: shouldUseCacheBasePathArg ? cacheBasePath : configuration.cacheBasePath,
|
|
buildPath: buildPath.string.isEmpty ? nil : buildPath,
|
|
prune: prune,
|
|
serialParse: serialParse,
|
|
hideVersionHeader: hideVersionHeader,
|
|
arguments: configuration.args,
|
|
headerPrefix: headerPrefix)
|
|
|
|
if isDryRun, watcherEnabled {
|
|
throw "--dry not compatible with --watch"
|
|
}
|
|
|
|
return try sourcery.processFiles(
|
|
configuration.source,
|
|
usingTemplates: configuration.templates,
|
|
output: configuration.output,
|
|
isDryRun: isDryRun,
|
|
forceParse: configuration.forceParse,
|
|
parseDocumentation: configuration.parseDocumentation,
|
|
baseIndentation: configuration.baseIndentation
|
|
) ?? []
|
|
}
|
|
|
|
if keepAlive.isEmpty {
|
|
Log.info(String(format: "Processing time %.2f seconds", currentTimestamp() - start))
|
|
} else {
|
|
RunLoop.current.run()
|
|
withExtendedLifetime(keepAlive) {
|
|
_ = keepAlive
|
|
}
|
|
}
|
|
} catch {
|
|
if isDryRun {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .prettyPrinted
|
|
let data = try? encoder.encode(DryOutputFailure(error: "\(error)",
|
|
log: Log.messagesStack))
|
|
data.flatMap { Log.output(String(data: $0, encoding: .utf8) ?? "") }
|
|
} else {
|
|
Log.error("\(error)")
|
|
}
|
|
|
|
exit(.other)
|
|
}
|
|
}.run(Sourcery.version)
|
|
}
|
|
|
|
import AppKit
|
|
|
|
if !inUnitTests {
|
|
runCLI()
|
|
} else {
|
|
// ! Need to run something for tests to work
|
|
final class TestApplicationController: NSObject, NSApplicationDelegate {
|
|
let window = NSWindow()
|
|
|
|
func applicationDidFinishLaunching(aNotification: NSNotification) {
|
|
window.setFrame(CGRect(x: 0, y: 0, width: 0, height: 0), display: false)
|
|
window.makeKeyAndOrderFront(self)
|
|
}
|
|
|
|
func applicationWillTerminate(aNotification: NSNotification) {
|
|
}
|
|
|
|
}
|
|
|
|
autoreleasepool { () -> Void in
|
|
let app = NSApplication.shared
|
|
let controller = TestApplicationController()
|
|
|
|
app.delegate = controller
|
|
app.run()
|
|
}
|
|
}
|
|
#else
|
|
func runCLI() {
|
|
command(
|
|
Flag("disableCache", description: "Stops using cache."),
|
|
Flag("verbose", flag: "v", description: "Turn on verbose logging, this causes to log everything we can."),
|
|
Flag("logAST", description: "Log AST messages"),
|
|
Flag("logBenchmarks", description: "Log time benchmark info"),
|
|
Flag("parseDocumentation", description: "Include documentation comments for all declarations."),
|
|
Flag("quiet", flag: "q", description: "Turn off any logging, only emmit errors."),
|
|
Flag("prune", flag: "p", description: "Remove empty generated files"),
|
|
Flag("serialParse", description: "Parses the specified sources in serial, rather than in parallel (the default), which can address stability issues in SwiftSyntax."),
|
|
VariadicOption<Path>("sources", description: "Path to a source swift files. File or Directory."),
|
|
VariadicOption<Path>("exclude-sources", description: "Path to a source swift files to exclude. File or Directory."),
|
|
VariadicOption<Path>("templates", description: "Path to templates. File or Directory."),
|
|
VariadicOption<Path>("exclude-templates", description: "Path to templates to exclude. File or Directory."),
|
|
Option<Path>("output", default: "", description: "Path to output. File or Directory. Default is current path."),
|
|
Flag("dry", default: false, description: "Dry run, without file system modifications, will output result and errors in JSON format. Not working with --watch."),
|
|
VariadicOption<Path>("config", default: ["."], description: "Path to config file. File or Directory. Default is current path."),
|
|
VariadicOption<String>("force-parse", description: "File extensions that Sourcery will be forced to parse, even if they were generated by Sourcery."),
|
|
Option<Int>("base-indentation", default: 0, description: "Base indendation to add to sourcery:auto fragments."),
|
|
VariadicOption<String>("args", description:
|
|
"""
|
|
Additional arguments to pass to templates. Each argument can have an explicit value or will have \
|
|
an implicit `true` value. Arguments should be comma-separated without spaces (e.g. --args arg1=value,arg2) \
|
|
or should be passed one by one (e.g. --args arg1=value --args arg2). Arguments are accessible in templates \
|
|
via `argument.<name>`. To pass in string you should use escaped quotes (\\").
|
|
"""),
|
|
Option<Path>("cacheBasePath", default: "", description: "Base path to Sourcery's cache directory"),
|
|
Option<Path>("buildPath", default: "", description: "Sets a custom build path"),
|
|
Flag("hideVersionHeader", description: "Do not include Sourcery version in the generated files headers."),
|
|
Option<String?>("headerPrefix", default: nil, description: "Additional prefix for headers.")
|
|
) { disableCache, verboseLogging, logAST, logBenchmark, parseDocumentation, quiet, prune, serialParse, sources, excludeSources, templates, excludeTemplates, output, isDryRun, configPaths, forceParse, baseIndentation, args, cacheBasePath, buildPath, hideVersionHeader, headerPrefix in
|
|
do {
|
|
let logConfiguration = Log.Configuration(
|
|
isDryRun: isDryRun,
|
|
isQuiet: quiet,
|
|
isVerboseLoggingEnabled: verboseLogging,
|
|
isLogBenchmarkEnabled: logBenchmark,
|
|
shouldLogAST: logAST
|
|
)
|
|
Log.setup(using: logConfiguration)
|
|
|
|
let configurations = configPaths.flatMap { configPath -> [Configuration] in
|
|
let yamlPath: Path = configPath.isDirectory ? configPath + ".sourcery.yml" : configPath
|
|
|
|
if !yamlPath.exists {
|
|
Log.info("No config file provided or it does not exist. Using command line arguments.")
|
|
let args = args.joined(separator: ",")
|
|
let arguments = AnnotationsParser.parse(line: args)
|
|
return [
|
|
Configuration(
|
|
sources: Paths(include: sources, exclude: excludeSources) ,
|
|
templates: Paths(include: templates, exclude: excludeTemplates),
|
|
output: output.string.isEmpty ? "." : output,
|
|
cacheBasePath: cacheBasePath.string.isEmpty ? Path.defaultBaseCachePath : cacheBasePath,
|
|
forceParse: forceParse,
|
|
parseDocumentation: parseDocumentation,
|
|
baseIndentation: baseIndentation,
|
|
args: arguments
|
|
)
|
|
]
|
|
} else {
|
|
_ = Validators.isFileOrDirectory(path: configPath)
|
|
_ = Validators.isReadable(path: yamlPath)
|
|
|
|
do {
|
|
let relativePath: Path = configPath.isDirectory ? configPath : configPath.parent()
|
|
|
|
// Check if the user is passing parameters
|
|
// that are ignored cause read from the yaml file
|
|
let hasAnyYamlDuplicatedParameter = (
|
|
!sources.isEmpty ||
|
|
!excludeSources.isEmpty ||
|
|
!templates.isEmpty ||
|
|
!excludeTemplates.isEmpty ||
|
|
!forceParse.isEmpty ||
|
|
output != "" ||
|
|
!args.isEmpty
|
|
)
|
|
|
|
if hasAnyYamlDuplicatedParameter {
|
|
Log.info("Using configuration file at '\(yamlPath)'. WARNING: Ignoring the parameters passed in the command line.")
|
|
} else {
|
|
Log.info("Using configuration file at '\(yamlPath)'")
|
|
}
|
|
|
|
return try Configurations.make(
|
|
path: yamlPath,
|
|
relativePath: relativePath,
|
|
env: ProcessInfo.processInfo.environment
|
|
)
|
|
} catch {
|
|
Log.error("while reading .yml '\(yamlPath)'. '\(error)'")
|
|
exit(.invalidConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
let start = currentTimestamp()
|
|
|
|
let keepAlive = try configurations.flatMap { configuration -> [FolderWatcher.Local] in
|
|
configuration.validate()
|
|
|
|
let shouldUseCacheBasePathArg = configuration.cacheBasePath == Path.defaultBaseCachePath && !cacheBasePath.string.isEmpty
|
|
|
|
let sourcery = Sourcery(verbose: verboseLogging,
|
|
watcherEnabled: false,
|
|
cacheDisabled: disableCache,
|
|
cacheBasePath: shouldUseCacheBasePathArg ? cacheBasePath : configuration.cacheBasePath,
|
|
buildPath: buildPath.string.isEmpty ? nil : buildPath,
|
|
prune: prune,
|
|
serialParse: serialParse,
|
|
hideVersionHeader: hideVersionHeader,
|
|
arguments: configuration.args,
|
|
headerPrefix: headerPrefix)
|
|
|
|
return try sourcery.processFiles(
|
|
configuration.source,
|
|
usingTemplates: configuration.templates,
|
|
output: configuration.output,
|
|
isDryRun: isDryRun,
|
|
forceParse: configuration.forceParse,
|
|
parseDocumentation: configuration.parseDocumentation,
|
|
baseIndentation: configuration.baseIndentation
|
|
) ?? []
|
|
}
|
|
|
|
if keepAlive.isEmpty {
|
|
Log.info(String(format: "Processing time %.2f seconds", currentTimestamp() - start))
|
|
} else {
|
|
RunLoop.current.run()
|
|
_ = keepAlive
|
|
}
|
|
} catch {
|
|
if isDryRun {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .prettyPrinted
|
|
let data = try? encoder.encode(DryOutputFailure(error: "\(error)",
|
|
log: Log.messagesStack))
|
|
data.flatMap { Log.output(String(data: $0, encoding: .utf8) ?? "") }
|
|
} else {
|
|
Log.error("\(error)")
|
|
}
|
|
|
|
exit(.other)
|
|
}
|
|
}.run(Sourcery.version)
|
|
}
|
|
|
|
runCLI()
|
|
#endif
|