blob: aee29508e88fbe8bb8807d11b9af7f27af4feefe [file] [log] [blame]
// Copyright 2014 The Bazel 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.
package com.google.devtools.build.xcode.xcodegen;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.xcode.util.Containing;
import com.google.devtools.build.xcode.util.Equaling;
import com.google.devtools.build.xcode.util.Mapping;
import com.facebook.buck.apple.xcode.xcodeproj.PBXGroup;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference;
import com.facebook.buck.apple.xcode.xcodeproj.PBXReference.SourceTree;
import com.facebook.buck.apple.xcode.xcodeproj.PBXVariantGroup;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* A {@link PBXReference} processor to group self-contained PBXReferences into PBXGroups. Grouping
* is done to make it easier to navigate the files of the project in Xcode's Project Navigator.
*
* <p>A <em>self-contained</em> reference is one that is not a member of a PBXVariantGroup or other
* aggregate group, although a self-contained reference may contain such a reference as a child.
*
* <p>This implementation arranges the {@code PBXFileReference}s into a hierarchy of
* {@code PBXGroup}s that mirrors the actual location of the files on disk.
*
* <p>When using this grouper, the top-level items are the following:
* <ul>
* <li>BUILT_PRODUCTS_DIR - a group containing items in the SourceRoot of this name
* <li>SDKROOT - a group containing items that are part of the Xcode install, such as SDK
* frameworks
* <li>workspace_root - a group containing items within the root of the workspace of the client
* <li>miscellaneous - anything that does not belong in one of the above groups is placed directly
* in the main group.
* </ul>
*/
public class PbxReferencesGrouper implements PbxReferencesProcessor {
private final FileSystem fileSystem;
public PbxReferencesGrouper(FileSystem fileSystem) {
this.fileSystem = Preconditions.checkNotNull(fileSystem, "fileSystem");
}
/**
* Converts a {@code String} to a {@code Path} using this instance's file system.
*/
private Path path(String pathString) {
return RelativePaths.fromString(fileSystem, pathString);
}
/**
* Returns the deepest directory that contains both paths.
*/
private Path deepestCommonContainer(Path path1, Path path2) {
Path container = path("");
int nameIndex = 0;
while ((nameIndex < Math.min(path1.getNameCount(), path2.getNameCount()))
&& Equaling.of(path1.getName(nameIndex), path2.getName(nameIndex))) {
container = container.resolve(path1.getName(nameIndex));
nameIndex++;
}
return container;
}
/**
* Returns the parent of the given path. This is similar to {@link Path#getParent()}, but is
* nullable-phobic. {@link Path#getParent()} considers the root of the filesystem to be the null
* Path. This method uses {@code path("")} for the root. This is also how the implementation of
* {@link PbxReferencesGrouper} expresses <em>root</em> in general.
*/
private Path parent(Path path) {
return (path.getNameCount() == 1) ? path("") : path.getParent();
}
/**
* The directory of the PBXGroup that will contain the given reference. For most references, this
* is just the actual parent directory. For {@code PBXVariantGroup}s, whose children are not
* guaranteed to be in any common directory except the client root, this returns the deepest
* common container of each child in the group.
*/
private Path dirOfContainingPbxGroup(PBXReference reference) {
if (reference instanceof PBXVariantGroup) {
PBXVariantGroup variantGroup = (PBXVariantGroup) reference;
Path path = Paths.get(variantGroup.getChildren().get(0).getPath());
for (PBXReference child : variantGroup.getChildren()) {
path = deepestCommonContainer(path, path(child.getPath()));
}
return path;
} else {
return parent(path(reference.getPath()));
}
}
/**
* Contains the populated PBXGroups for a certain source tree.
*/
private class Groups {
/**
* Map of paths to the PBXGroup that is used to contain all files and groups in that path.
*/
final Map<Path, PBXGroup> groupCache;
Groups(String rootGroupName, SourceTree sourceTree) {
groupCache = new HashMap<>();
groupCache.put(path(""), new PBXGroup(rootGroupName, "" /* path */, sourceTree));
}
PBXGroup rootGroup() {
return Mapping.of(groupCache, path("")).get();
}
void add(Path dirOfContainingPbxGroup, PBXReference reference) {
for (PBXGroup container : Mapping.of(groupCache, dirOfContainingPbxGroup).asSet()) {
container.getChildren().add(reference);
return;
}
PBXGroup newGroup = new PBXGroup(dirOfContainingPbxGroup.getFileName().toString(),
null /* path */, SourceTree.GROUP);
newGroup.getChildren().add(reference);
add(parent(dirOfContainingPbxGroup), newGroup);
groupCache.put(dirOfContainingPbxGroup, newGroup);
}
}
@Override
public Iterable<PBXReference> process(Iterable<PBXReference> references) {
Map<SourceTree, Groups> groupsBySourceTree = ImmutableMap.of(
SourceTree.GROUP, new Groups("workspace_root", SourceTree.GROUP),
SourceTree.SDKROOT, new Groups("SDKROOT", SourceTree.SDKROOT),
SourceTree.BUILT_PRODUCTS_DIR,
new Groups("BUILT_PRODUCTS_DIR", SourceTree.BUILT_PRODUCTS_DIR));
ImmutableList.Builder<PBXReference> result = new ImmutableList.Builder<>();
for (PBXReference reference : references) {
if (Containing.key(groupsBySourceTree, reference.getSourceTree())) {
Path containingDir = dirOfContainingPbxGroup(reference);
Mapping.of(groupsBySourceTree, reference.getSourceTree())
.get()
.add(containingDir, reference);
} else {
// The reference is not inside any expected source tree, so don't try anything clever. Just
// add it to the main group directly (not in a nested PBXGroup).
result.add(reference);
}
}
for (Groups groupsRoot : groupsBySourceTree.values()) {
if (!groupsRoot.rootGroup().getChildren().isEmpty()) {
result.add(groupsRoot.rootGroup());
}
}
return result.build();
}
}