blob: 34b730a0243005ab21d70c5d66fba727daa027c8 [file] [log] [blame]
// Copyright 2017 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
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
// Provides methods to patch up Bazel specific PBX objects and references before project generation.
// This will remove any invalid .xcassets in order to make Xcode happy, as well as apply a
// BazelPBXReferencePatcher to any files that can't be found. As a backup, paths are set to be
// relative to the Bazel exec root for non-generated files.
final class BazelXcodeProjectPatcher {
// FileManager used to check for presence of PBXFileReferences when patching.
let fileManager: FileManager
// Secondary patcher for the PBXFileReferences and primary patcher for the @external references.
let fileReferencePatcher: BazelPBXReferencePatcher
init(fileManager: FileManager) {
self.fileManager = fileManager
self.fileReferencePatcher = BazelPBXReferencePatcher(fileManager: fileManager)
// Resolves the given Bazel exec-root relative path to a filesystem path.
// This is intended to be used to resolve "@external_repo" style labels to paths usable by Xcode
// as well as any other paths that must be relative to the Bazel exec root.
private func resolvePathFromBazelExecRoot(_ path: String) -> String {
return "\(PBXTargetGenerator.TulsiWorkspacePath)/\(path)"
// Rewrites the path for file references that it believes to be relative to Bazel's exec root.
// This should be called before patching external references.
private func patchFileReference(file: PBXFileReference, url: URL, workspaceRootURL: URL) {
// We only want to modify the path if the current path doesn't point to a valid file.
guard !fileManager.fileExists(atPath: url.path) else { return }
// Don't patch anything that isn't group relative.
guard file.sourceTree == .Group else { return }
// Remove .xcassets that are not present. Unfortunately, Xcode's handling of .xcassets has
// quite a number of issues with Tulsi and readonly files.
// .xcassets references in a project that are not present on disk will present a warning after
// opening the main target.
// Readonly (not writeable) .xcassets that contain an .appiconset with a Contents.json will
// trigger Xcode into an endless loop of
// "You don’t have permission to save the file “Contents.json” in the folder <X>."
// This is present in Xcode 8.3.3 and Xcode 9b2.
guard !url.path.hasSuffix(".xcassets") else {
if let parent = file.parent as? PBXGroup {
// Default to be relative to the bazel exec root if the FileReferencePatcher doesn't handle the
// patch. This is for both source files as well as generated files (which always need to be
// relative to the bazel exec root).
if !fileReferencePatcher.patchNonPresentFileReference(file: file,
url: url,
workspaceRootURL: workspaceRootURL) {
file.path = resolvePathFromBazelExecRoot(file.path!)
// Handles patching PBXFileReferences that are not present on disk. This should be called before
// calling patchExternalRepositoryReferences.
func patchBazelRelativeReferences(_ xcodeProject: PBXProject,
_ workspaceRootURL : URL) {
// Exclude external references that have yet to be patched in.
var queue = xcodeProject.mainGroup.children.filter{ $ != "external" }
while !queue.isEmpty {
let ref = queue.remove(at: 0)
if let group = ref as? PBXGroup {
// Queue up all children of the group so we can find all of their FileReferences.
queue.append(contentsOf: group.children)
} else if let file = ref as? PBXFileReference,
let fileURL = URL(string: file.path!, relativeTo: workspaceRootURL) {
self.patchFileReference(file: file, url: fileURL, workspaceRootURL: workspaceRootURL)
// Handles patching any groups that were generated under Bazel's magical "external" container to
// proper filesystem references. This should be called after patchBazelRelativeReferences.
func patchExternalRepositoryReferences(_ xcodeProject: PBXProject) {