blob: fd7d79bee70041a98bbadda2ab683724a5122b79 [file] [log] [blame]
/*
* Copyright 2014-present Facebook, Inc.
*
* 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.facebook.buck.apple;
import com.facebook.buck.io.MorePaths;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.Stack;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Collects file references and generates an xcworkspace.
*/
public class WorkspaceGenerator {
private final ProjectFilesystem projectFilesystem;
private final String workspaceName;
private final Path outputDirectory;
private final SortedMap<String, WorkspaceNode> children;
private static class WorkspaceNode {
}
private static class WorkspaceGroup extends WorkspaceNode {
private final SortedMap<String, WorkspaceNode> children;
WorkspaceGroup() {
this.children = Maps.newTreeMap();
}
public Map<String, WorkspaceNode> getChildren() {
return children;
}
}
private static class WorkspaceFileRef extends WorkspaceNode {
private final Path path;
WorkspaceFileRef(Path path) {
this.path = path;
}
public Path getPath() {
return path;
}
}
public WorkspaceGenerator(
ProjectFilesystem projectFilesystem,
String workspaceName,
Path outputDirectory) {
this.projectFilesystem = projectFilesystem;
this.workspaceName = workspaceName;
this.outputDirectory = outputDirectory;
this.children = Maps.newTreeMap();
}
/**
* Adds a reference to a project file to the generated workspace.
*
* @param path Path to the referenced project file in the repository.
*/
public void addFilePath(Path path) {
path = path.normalize();
Optional<Path> groupPath = Optional.absent();
// We skip the last name before the file name as it's usually the same as the project name, and
// adding a group would add an unnecessary level of nesting. We don't check whether it's the
// same or not to avoid inconsistent behaviour: this will result in all projects to show up in a
// group with the path of their grandparent directory in all cases.
if (path.getNameCount() > 2) {
groupPath = Optional.of(path.subpath(0, path.getNameCount() - 2));
}
addFilePath(path, groupPath);
}
/**
* Adds a reference to a project file to the group hierarchy of the generated workspace.
*
* @param path Path to the referenced project file in the repository.
* @param groupPath Path in the group hierarchy of the generated workspace where
* the reference will be placed.
* If absent, the project reference is placed to the root of the workspace.
*/
public void addFilePath(Path path, Optional<Path> groupPath) {
Map<String, WorkspaceNode> children = this.children;
if (groupPath.isPresent()) {
for (Path groupPathPart : groupPath.get()) {
String groupName = groupPathPart.toString();
WorkspaceNode node = children.get(groupName);
WorkspaceGroup group;
if (node instanceof WorkspaceFileRef) {
throw new HumanReadableException(
"Invalid workspace: a group and a project have the same name: %s", groupName);
} else if (node == null) {
group = new WorkspaceGroup();
children.put(groupName, group);
} else if (node instanceof WorkspaceGroup) {
group = (WorkspaceGroup) node;
} else {
// Unreachable
throw new HumanReadableException(
"Expected a workspace to only contain groups and file references");
}
children = group.getChildren();
}
}
children.put(path.toString(), new WorkspaceFileRef(path));
}
private void walkNodeTree(FileVisitor<Map.Entry<String, WorkspaceNode>> visitor)
throws IOException{
Stack<Iterator<Map.Entry<String, WorkspaceNode>>> iterators = new Stack<>();
Stack<Map.Entry<String, WorkspaceNode>> groups = new Stack<>();
iterators.push(this.children.entrySet().iterator());
while (!iterators.isEmpty()) {
if (!iterators.peek().hasNext()) {
if (groups.isEmpty()) {
break;
}
visitor.postVisitDirectory(groups.pop(), null);
iterators.pop();
continue;
}
Map.Entry<String, WorkspaceNode> nextEntry = iterators.peek().next();
WorkspaceNode nextNode = nextEntry.getValue();
if (nextNode instanceof WorkspaceGroup) {
visitor.preVisitDirectory(nextEntry, null);
WorkspaceGroup nextGroup = (WorkspaceGroup) nextNode;
groups.push(nextEntry);
iterators.push(nextGroup.getChildren().entrySet().iterator());
} else if (nextNode instanceof WorkspaceFileRef) {
visitor.visitFile(nextEntry, null);
} else {
// Unreachable
throw new HumanReadableException(
"Expected a workspace to only contain groups and file references");
}
}
}
public Path writeWorkspace() throws IOException {
DocumentBuilder docBuilder;
Transformer transformer;
try {
docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
transformer = TransformerFactory.newInstance().newTransformer();
} catch (ParserConfigurationException | TransformerConfigurationException e) {
throw new RuntimeException(e);
}
DOMImplementation domImplementation = docBuilder.getDOMImplementation();
final Document doc = domImplementation.createDocument(
/* namespaceURI */ null,
"Workspace",
/* docType */ null);
doc.setXmlVersion("1.0");
Element rootElem = doc.getDocumentElement();
rootElem.setAttribute("version", "1.0");
final Stack<Element> groups = new Stack<>();
groups.push(rootElem);
FileVisitor<Map.Entry<String, WorkspaceNode>> visitor =
new FileVisitor<Map.Entry<String, WorkspaceNode>>() {
@Override
public FileVisitResult preVisitDirectory(
Map.Entry<String, WorkspaceNode> dir,
BasicFileAttributes attrs) throws IOException {
Preconditions.checkArgument(dir.getValue() instanceof WorkspaceGroup);
Element element = doc.createElement("Group");
element.setAttribute("location", "container:");
element.setAttribute("name", dir.getKey());
groups.peek().appendChild(element);
groups.push(element);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(
Map.Entry<String, WorkspaceNode> file,
BasicFileAttributes attrs) throws IOException {
Preconditions.checkArgument(file.getValue() instanceof WorkspaceFileRef);
WorkspaceFileRef fileRef = (WorkspaceFileRef) file.getValue();
Element element = doc.createElement("FileRef");
element.setAttribute(
"location",
"container:" +
outputDirectory.normalize().relativize(fileRef.getPath()).toString());
groups.peek().appendChild(element);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(
Map.Entry<String, WorkspaceNode> file,
IOException exc) throws IOException {
return FileVisitResult.TERMINATE;
}
@Override
public FileVisitResult postVisitDirectory(
Map.Entry<String, WorkspaceNode> dir, IOException exc) throws IOException {
groups.pop();
return FileVisitResult.CONTINUE;
}
};
walkNodeTree(visitor);
Path projectWorkspaceDir = outputDirectory.resolve(workspaceName + ".xcworkspace");
projectFilesystem.mkdirs(projectWorkspaceDir);
Path serializedWorkspace = projectWorkspaceDir.resolve("contents.xcworkspacedata");
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(outputStream);
transformer.transform(source, result);
String contentsToWrite = outputStream.toString();
if (MorePaths.fileContentsDiffer(
new ByteArrayInputStream(contentsToWrite.getBytes(Charsets.UTF_8)),
serializedWorkspace,
projectFilesystem)) {
projectFilesystem.writeContentsToPath(contentsToWrite, serializedWorkspace);
}
} catch (TransformerException e) {
throw new RuntimeException(e);
}
return projectWorkspaceDir;
}
}