blob: 1d802ad6ad56c64a2601d603c5f119ae7b0ca331 [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 a Tulsiproj bundle.
struct HeadlessTulsiProjectCreator {
/// Provides functionality to signal a semaphore when the "processing" key on some object is set
/// to false.
private class ProcessingCompletedObserver: NSObject {
let semaphore: DispatchSemaphore
init(semaphore: DispatchSemaphore) {
self.semaphore = semaphore
}
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
if context != &HeadlessTulsiProjectCreator.KVOContext {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
if keyPath == "processing", let newValue = change?[NSKeyValueChangeKey.newKey] as? Bool {
if (!newValue) {
semaphore.signal()
}
}
}
}
let arguments: TulsiCommandlineParser.Arguments
private static var KVOContext: Int = 0
init(arguments: TulsiCommandlineParser.Arguments) {
self.arguments = arguments
}
/// Performs project generation.
func generate() throws {
guard let bazelPath = arguments.bazel else {
throw HeadlessModeError.missingBazelPath
}
let defaultFileManager = FileManager.default
if !defaultFileManager.isExecutableFile(atPath: bazelPath) {
throw HeadlessModeError.invalidBazelPath
}
guard let tulsiprojName = arguments.tulsiprojName else {
fatalError("HeadlessTulsiProjectCreator invoked without a valid tulsiprojName")
}
guard let targets = arguments.buildTargets else {
throw HeadlessModeError.missingBuildTargets
}
guard let outputFolderPath = arguments.outputFolder else {
throw HeadlessModeError.explicitOutputOptionRequired
}
let (projectURL, projectName) = try buildOutputPath(outputFolderPath,
projectBundleName: tulsiprojName)
let workspaceRootURL: URL
if let explicitWorkspaceRoot = arguments.workspaceRootOverride {
workspaceRootURL = URL(fileURLWithPath: explicitWorkspaceRoot, isDirectory: true)
} else {
workspaceRootURL = URL(fileURLWithPath: defaultFileManager.currentDirectoryPath,
isDirectory: true)
}
let workspaceFileURL = try buildWORKSPACEFileURL(workspaceRootURL)
TulsiProjectDocument.showAlertsOnErrors = false
defer {
TulsiProjectDocument.showAlertsOnErrors = true
}
try createTulsiProject(projectName,
workspaceFileURL: workspaceFileURL,
targets: targets,
atURL: projectURL)
}
// MARK: - Private methods
private func createTulsiProject(_ projectName: String,
workspaceFileURL: URL,
targets: [String],
atURL projectURL: URL) throws {
let document = TulsiProjectDocument()
document.createNewProject(projectName, workspaceFileURL: workspaceFileURL)
let bazelPackages = processBazelPackages(document, targets: targets)
if document.ruleInfos.isEmpty {
throw HeadlessModeError.bazelTargetProcessingFailed
}
if let buildStartupOptions = arguments.buildStartupOptions {
guard let optionSet = document.optionSet else {
fatalError("Failed to retrieve option set.")
}
optionSet[.BazelBuildStartupOptionsDebug].projectValue = buildStartupOptions
optionSet[.BazelBuildStartupOptionsRelease].projectValue = buildStartupOptions
}
if let buildOptions = arguments.buildOptions {
guard let optionSet = document.optionSet else {
fatalError("Failed to retrieve option set.")
}
optionSet[.BazelBuildOptionsDebug].projectValue = buildOptions
optionSet[.BazelBuildOptionsRelease].projectValue = buildOptions
}
document.fileURL = projectURL
try document.writeSafely(to: projectURL,
ofType: "com.google.tulsi.project",
for: .saveOperation)
try addDefaultConfig(document,
named: projectName,
bazelPackages: bazelPackages,
targets: targets,
additionalSourcePaths: arguments.additionalPathFilters)
}
private func processBazelPackages(_ document: TulsiProjectDocument,
targets: [String]) -> Set<String> {
let bazelPackages = extractBazelPackages(targets)
// Updating the project's bazelPackages will cause it to go into processing, observe the
// processing key and block further execution until it is completed.
let semaphore = DispatchSemaphore(value: 0)
let observer = ProcessingCompletedObserver(semaphore: semaphore)
document.addObserver(observer,
forKeyPath: "processing",
options: .new,
context: &HeadlessTulsiProjectCreator.KVOContext)
document.bazelPackages = Array(bazelPackages)
// Wait until processing completes.
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
document.removeObserver(observer, forKeyPath: "processing")
return bazelPackages
}
private func addDefaultConfig(_ projectDocument: TulsiProjectDocument,
named projectName: String,
bazelPackages: Set<String>,
targets: [String],
additionalSourcePaths: Set<String>? = nil) throws {
let additionalFilePaths = bazelPackages.map() { "\($0)/BUILD" }
guard let generatorConfigFolderURL = projectDocument.generatorConfigFolderURL else {
fatalError("Config folder unexpectedly nil")
}
let configDocument = try TulsiGeneratorConfigDocument.makeDocumentWithProjectRuleEntries(projectDocument.ruleInfos,
optionSet: projectDocument.optionSet!,
projectName: projectName,
saveFolderURL: generatorConfigFolderURL,
infoExtractor: projectDocument.infoExtractor,
messageLog: projectDocument,
additionalFilePaths: additionalFilePaths,
bazelURL: projectDocument.bazelURL)
projectDocument.trackChildConfigDocument(configDocument)
let targetLabels = Set(targets.map() { BuildLabel($0, normalize: true) })
// Select appropriate rule infos in the config.
for info in configDocument.uiRuleInfos {
info.selected = targetLabels.contains(info.ruleInfo.label)
}
// Add a single source path including every possible source.
configDocument.sourcePaths = [UISourcePath(path: ".", selected: true, recursive: true)]
if let sourcePaths = additionalSourcePaths {
// TODO(thomasmarsh@github): This currently assumes that the paths are recursive. A more robust solution
// would be preferred to handle both recursive and non-recursive cases.
configDocument.sourcePaths += sourcePaths.map { UISourcePath(path: $0, selected: false, recursive: true) }
}
configDocument.headlessSave(projectName)
}
private func extractBazelPackages(_ targets: [String]) -> Set<String> {
var buildFiles = Set<String>()
for target in targets {
guard let range = target.range(of: ":"), !range.isEmpty else { continue }
let package = String(target[..<range.lowerBound])
buildFiles.insert(package)
}
return buildFiles
}
/// Processes the "outputFolder" argument, returning the Tulsi project bundle URL and project
/// name.
private func buildOutputPath(_ outputFolderPath: String,
projectBundleName: String) throws -> (URL, String) {
let outputFolderURL = URL(fileURLWithPath: outputFolderPath, isDirectory: true)
guard projectBundleName == (projectBundleName as NSString).lastPathComponent else {
throw HeadlessModeError.invalidProjectBundleName
}
let projectName = (projectBundleName as NSString).deletingPathExtension
let normalizedProjectBundleName = "\(projectName).\(TulsiProjectDocument.getTulsiBundleExtension())"
let projectBundleURL = outputFolderURL.appendingPathComponent(normalizedProjectBundleName,
isDirectory: false)
return (projectBundleURL, projectName)
}
private func buildWORKSPACEFileURL(_ workspaceRootURL: URL) throws -> URL {
let workspaceFile = workspaceRootURL.appendingPathComponent("WORKSPACE", isDirectory: false)
var isDirectory = ObjCBool(false)
if !FileManager.default.fileExists(atPath: workspaceFile.path,
isDirectory: &isDirectory) || isDirectory.boolValue {
throw HeadlessModeError.missingWORKSPACEFile(workspaceRootURL.path)
}
return workspaceFile
}
}