aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/objc_tools/xcodegen/java/com/google/devtools/build/xcode/xcodegen/PbxReferencesGrouper.java
blob: 91901332c1c9fb35f71aec4eb54c037f680594cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// Copyright 2014 Google Inc. 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();
  }
}