| // 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(); |
| } |
| } |