blob: 36819de696c15410422e329f523da9f4ed7efb0d [file] [log] [blame]
// Copyright 2016 The Tulsi Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Cocoa
import TulsiGenerator
/// Provides functionality to generate an Xcode project from a TulsiGeneratorConfig
struct HeadlessXcodeProjectGenerator {
let arguments: TulsiCommandlineParser.Arguments
/// Performs project generation.
func generate() throws {
TulsiProjectDocument.showAlertsOnErrors = false
let explicitBazelURL: URL?
if let bazelPath = arguments.bazel {
if !FileManager.default.isExecutableFile(atPath: bazelPath) {
throw HeadlessModeError.invalidBazelPath
}
explicitBazelURL = URL(fileURLWithPath: bazelPath)
TulsiProjectDocument.suppressRuleEntryUpdateOnLoad = true
} else {
explicitBazelURL = nil
}
defer {
TulsiProjectDocument.showAlertsOnErrors = true
TulsiProjectDocument.suppressRuleEntryUpdateOnLoad = false
}
guard let configPath = arguments.generatorConfig else {
fatalError("HeadlessXcodeProjectGenerator invoked without a valid generatorConfig")
}
let (projectURL, configURL, defaultOutputFolderURL) = try resolveConfigPath(configPath)
let documentController = NSDocumentController.shared
let doc: NSDocument
do {
doc = try documentController.makeDocument(withContentsOf: projectURL,
ofType: "com.google.tulsi.project")
} catch TulsiProjectDocument.DocumentError.invalidWorkspace(let info) {
throw HeadlessModeError.invalidProjectFileContents("Failed to load project due to invalid workspace: \(info)")
} catch let e as NSError {
throw HeadlessModeError.invalidProjectFileContents("Failed to load project due to unexpected exception: \(e)")
} catch {
throw HeadlessModeError.invalidProjectFileContents("Failed to load project due to unexpected exception.")
}
guard let projectDocument = doc as? TulsiProjectDocument else {
throw HeadlessModeError.invalidProjectFileContents("\(doc) is not of the expected type.")
}
let outputFolderURL: URL
if let option = arguments.outputFolder {
outputFolderURL = URL(fileURLWithPath: option, isDirectory: true)
} else if let defaultOutputFolderURL = defaultOutputFolderURL {
outputFolderURL = defaultOutputFolderURL
} else {
throw HeadlessModeError.explicitOutputOptionRequired
}
var config = try loadConfig(configURL, bazelURL: explicitBazelURL)
config = config.configByAppendingPathFilters(arguments.additionalPathFilters)
if let project = projectDocument.project {
config = config.configByResolvingInheritedSettingsFromProject(project);
}
let workspaceRootURL: URL
let projectWorkspaceRootURL = projectDocument.workspaceRootURL
if let workspaceRootOverride = arguments.workspaceRootOverride {
workspaceRootURL = URL(fileURLWithPath: workspaceRootOverride, isDirectory: true)
if !isExistingDirectory(workspaceRootURL) {
throw HeadlessModeError.invalidWorkspaceRootOverride
}
if projectWorkspaceRootURL != nil {
print("Overriding project workspace root (\(projectWorkspaceRootURL!.path)) with " +
"command-line parameter (\(workspaceRootOverride))")
}
} else {
guard let projectWorkspaceRootURL = projectWorkspaceRootURL else {
throw HeadlessModeError.invalidProjectFileContents("Invalid workspaceRoot")
}
workspaceRootURL = projectWorkspaceRootURL as URL
}
print("Generating project into '\(outputFolderURL.path)' using:\n" +
"\tconfig at '\(configURL.path)'\n" +
"\tBazel workspace at '\(workspaceRootURL.path)'\n" +
"\tBazel at '\(config.bazelURL.path)'.\n" +
"This may take a while.")
let result = TulsiGeneratorConfigDocument.generateXcodeProjectInFolder(outputFolderURL,
withGeneratorConfig: config,
workspaceRootURL: workspaceRootURL,
messageLog: nil)
switch result {
case .success(let url):
print("Generated project at \(url.path)")
if arguments.openXcodeOnSuccess {
print("Opening generated project in Xcode")
NSWorkspace.shared.open(url)
}
case .failure:
throw HeadlessModeError.generationFailed
}
}
// MARK: - Private methods
private func resolveConfigPath(_ path: String) throws -> (projectURL: URL,
configURL: URL,
defaultOutputFolderURL: URL?) {
let tulsiProjExtension = TulsiProjectDocument.getTulsiBundleExtension()
let components = path.components(separatedBy: ":")
if components.count == 2 {
var pathString = components[0] as NSString
let projectExtension = pathString.pathExtension
if projectExtension != tulsiProjExtension {
pathString = pathString.appendingPathExtension(tulsiProjExtension)! as NSString
}
let projectURL = URL(fileURLWithPath: pathString as String)
let (configURL, defaultOutputFolderURL) = try locateConfigNamed(components[1],
inTulsiProject: pathString as String)
return (projectURL, configURL, defaultOutputFolderURL)
}
var pathString = path as NSString
let pathExtension = pathString.pathExtension
var isProject = pathExtension == tulsiProjExtension
if !isProject && pathExtension.isEmpty {
// See if the user provided a Tulsiproj bundle without the extension or if there is a
// tulsiproj bundle with the same name as the given directory.
let projectPath = pathString.appendingPathExtension(tulsiProjExtension)!
if isExistingDirectory(URL(fileURLWithPath: projectPath, isDirectory: true)) {
isProject = true
pathString = projectPath as NSString
} else {
let projectName = (pathString.lastPathComponent as NSString).appendingPathExtension(tulsiProjExtension)!
let projectWithinPath = pathString.appendingPathComponent(projectName)
if isExistingDirectory(URL(fileURLWithPath: projectWithinPath, isDirectory: true)) {
isProject = true
pathString = projectWithinPath as NSString
}
}
}
if isProject {
let project = pathString.lastPathComponent as NSString
let projectName = project.deletingPathExtension
let (configURL, defaultOutputFolderURL) = try locateConfigNamed(projectName,
inTulsiProject: pathString as String)
let projectURL = URL(fileURLWithPath: pathString as String)
return (projectURL, configURL, defaultOutputFolderURL)
}
throw HeadlessModeError.invalidConfigPath("The given config is invalid")
}
private func locateConfigNamed(_ configName: String,
inTulsiProject tulsiProj: String) throws -> (configURL: URL, defaultOutputFolderURL: URL?) {
let tulsiProjectURL = URL(fileURLWithPath: tulsiProj, isDirectory: true)
if !isExistingDirectory(tulsiProjectURL) {
throw HeadlessModeError.invalidConfigPath("The given Tulsi project does not exist")
}
let configDirectoryURL = tulsiProjectURL.appendingPathComponent(TulsiProjectDocument.ProjectConfigsSubpath)
if !isExistingDirectory(configDirectoryURL) {
throw HeadlessModeError.invalidConfigPath("The given Tulsi project does not contain any configs")
}
let configFilename: String
if configName.hasSuffix(TulsiGeneratorConfig.FileExtension) {
configFilename = configName
} else {
configFilename = "\(configName).\(TulsiGeneratorConfig.FileExtension)"
}
let configFileURL = configDirectoryURL.appendingPathComponent(configFilename)
if FileManager.default.isReadableFile(atPath: configFileURL.path) {
return (configFileURL, tulsiProjectURL.deletingLastPathComponent())
}
throw HeadlessModeError.invalidConfigPath("The given Tulsi project does not contain a Tulsi config named \(configName).")
}
private func isExistingDirectory(_ url: URL) -> Bool {
var isDirectory = ObjCBool(false)
if !FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) {
return false
}
return isDirectory.boolValue
}
private func loadConfig(_ url: URL, bazelURL: URL?) throws -> TulsiGeneratorConfig {
let config: TulsiGeneratorConfig
do {
config = try TulsiGeneratorConfig.load(url, bazelURL: bazelURL)
} catch TulsiGeneratorConfig.ConfigError.badInputFilePath {
throw HeadlessModeError.invalidConfigFileContents("Failed to read config file at \(url.path)")
} catch TulsiGeneratorConfig.ConfigError.failedToReadAdditionalOptionsData(let info) {
throw HeadlessModeError.invalidConfigFileContents("Failed to read per-user config file: \(info)")
} catch TulsiGeneratorConfig.ConfigError.deserializationFailed(let info) {
throw HeadlessModeError.invalidConfigFileContents("Config file at \(url.path) is invalid: \(info)")
} catch {
throw HeadlessModeError.invalidConfigFileContents("Unexpected exception reading config file at \(url.path)")
}
if !config.bazelURL.isFileURL {
throw HeadlessModeError.invalidBazelPath
}
return config
}
}