/*
* Copyright 2012-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.project_generator;
import com.facebook.buck.apple.SchemeActionType;
import com.facebook.buck.apple.xcode.XCScheme;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.io.MoreProjectFilesystems;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.log.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
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;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Collects target references and generates an xcscheme.
*
* <p>To register entries in the scheme, clients must add:
*
* <ul>
* <li>associations between buck targets and Xcode targets
* <li>associations between Xcode targets and the projects that contain them
* </ul>
*
* <p>Both of these values can be pulled out of {@link ProjectGenerator}.
*/
class SchemeGenerator {
private static final Logger LOG = Logger.get(SchemeGenerator.class);
private final ProjectFilesystem projectFilesystem;
private final Optional<PBXTarget> primaryTarget;
private final ImmutableSet<PBXTarget> orderedBuildTargets;
private final ImmutableSet<PBXTarget> orderedBuildTestTargets;
private final ImmutableSet<PBXTarget> orderedRunTestTargets;
private final String schemeName;
private final Path outputDirectory;
private final boolean parallelizeBuild;
private final Optional<String> runnablePath;
private final Optional<String> remoteRunnablePath;
private final ImmutableMap<SchemeActionType, String> actionConfigNames;
private final ImmutableMap<PBXTarget, Path> targetToProjectPathMap;
private Optional<XCScheme> outputScheme = Optional.empty();
private final XCScheme.LaunchAction.LaunchStyle launchStyle;
public SchemeGenerator(
ProjectFilesystem projectFilesystem,
Optional<PBXTarget> primaryTarget,
ImmutableSet<PBXTarget> orderedBuildTargets,
ImmutableSet<PBXTarget> orderedBuildTestTargets,
ImmutableSet<PBXTarget> orderedRunTestTargets,
String schemeName,
Path outputDirectory,
boolean parallelizeBuild,
Optional<String> runnablePath,
Optional<String> remoteRunnablePath,
ImmutableMap<SchemeActionType, String> actionConfigNames,
ImmutableMap<PBXTarget, Path> targetToProjectPathMap,
XCScheme.LaunchAction.LaunchStyle launchStyle) {
this.projectFilesystem = projectFilesystem;
this.primaryTarget = primaryTarget;
this.launchStyle = launchStyle;
this.orderedBuildTargets = orderedBuildTargets;
this.orderedBuildTestTargets = orderedBuildTestTargets;
this.orderedRunTestTargets = orderedRunTestTargets;
this.schemeName = schemeName;
this.outputDirectory = outputDirectory;
this.parallelizeBuild = parallelizeBuild;
this.runnablePath = runnablePath;
this.remoteRunnablePath = remoteRunnablePath;
this.actionConfigNames = actionConfigNames;
this.targetToProjectPathMap = targetToProjectPathMap;
LOG.debug(
"Generating scheme with build targets %s, test build targets %s, test bundle targets %s",
orderedBuildTargets, orderedBuildTestTargets, orderedRunTestTargets);
}
@VisibleForTesting
Optional<XCScheme> getOutputScheme() {
return outputScheme;
}
public Path writeScheme() throws IOException {
Map<PBXTarget, XCScheme.BuildableReference> buildTargetToBuildableReferenceMap =
new HashMap<>();
for (PBXTarget target : Iterables.concat(orderedBuildTargets, orderedBuildTestTargets)) {
String blueprintName = target.getProductName();
if (blueprintName == null) {
blueprintName = target.getName();
}
Path outputPath = outputDirectory.getParent();
String buildableReferencePath;
Path projectPath = Preconditions.checkNotNull(targetToProjectPathMap.get(target));
if (outputPath == null) {
//Root directory project
buildableReferencePath = projectPath.toString();
} else {
buildableReferencePath = outputPath.relativize(projectPath).toString();
}
XCScheme.BuildableReference buildableReference =
new XCScheme.BuildableReference(
buildableReferencePath,
Preconditions.checkNotNull(target.getGlobalID()),
target.getProductReference() != null
? target.getProductReference().getName()
: Preconditions.checkNotNull(target.getProductName()),
blueprintName);
buildTargetToBuildableReferenceMap.put(target, buildableReference);
}
XCScheme.BuildAction buildAction = new XCScheme.BuildAction(parallelizeBuild);
// For aesthetic reasons put all non-test build actions before all test build actions.
for (PBXTarget target : orderedBuildTargets) {
addBuildActionForBuildTarget(
buildTargetToBuildableReferenceMap.get(target),
XCScheme.BuildActionEntry.BuildFor.DEFAULT,
buildAction);
}
for (PBXTarget target : orderedBuildTestTargets) {
addBuildActionForBuildTarget(
buildTargetToBuildableReferenceMap.get(target),
XCScheme.BuildActionEntry.BuildFor.TEST_ONLY,
buildAction);
}
XCScheme.TestAction testAction =
new XCScheme.TestAction(
Preconditions.checkNotNull(actionConfigNames.get(SchemeActionType.TEST)));
for (PBXTarget target : orderedRunTestTargets) {
XCScheme.BuildableReference buildableReference =
buildTargetToBuildableReferenceMap.get(target);
XCScheme.TestableReference testableReference =
new XCScheme.TestableReference(buildableReference);
testAction.addTestableReference(testableReference);
}
Optional<XCScheme.LaunchAction> launchAction = Optional.empty();
Optional<XCScheme.ProfileAction> profileAction = Optional.empty();
if (primaryTarget.isPresent()) {
XCScheme.BuildableReference primaryBuildableReference =
buildTargetToBuildableReferenceMap.get(primaryTarget.get());
if (primaryBuildableReference != null) {
launchAction =
Optional.of(
new XCScheme.LaunchAction(
primaryBuildableReference,
Preconditions.checkNotNull(actionConfigNames.get(SchemeActionType.LAUNCH)),
runnablePath,
remoteRunnablePath,
launchStyle));
profileAction =
Optional.of(
new XCScheme.ProfileAction(
primaryBuildableReference,
Preconditions.checkNotNull(actionConfigNames.get(SchemeActionType.PROFILE))));
}
}
XCScheme.AnalyzeAction analyzeAction =
new XCScheme.AnalyzeAction(
Preconditions.checkNotNull(actionConfigNames.get(SchemeActionType.ANALYZE)));
XCScheme.ArchiveAction archiveAction =
new XCScheme.ArchiveAction(
Preconditions.checkNotNull(actionConfigNames.get(SchemeActionType.ARCHIVE)));
XCScheme scheme =
new XCScheme(
schemeName,
Optional.of(buildAction),
Optional.of(testAction),
launchAction,
profileAction,
Optional.of(analyzeAction),
Optional.of(archiveAction));
outputScheme = Optional.of(scheme);
Path schemeDirectory = outputDirectory.resolve("xcshareddata/xcschemes");
projectFilesystem.mkdirs(schemeDirectory);
Path schemePath = schemeDirectory.resolve(schemeName + ".xcscheme");
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
serializeScheme(scheme, outputStream);
String contentsToWrite = outputStream.toString();
if (MoreProjectFilesystems.fileContentsDiffer(
new ByteArrayInputStream(contentsToWrite.getBytes(Charsets.UTF_8)),
schemePath,
projectFilesystem)) {
projectFilesystem.writeContentsToPath(outputStream.toString(), schemePath);
}
}
return schemePath;
}
private static void addBuildActionForBuildTarget(
XCScheme.BuildableReference buildableReference,
EnumSet<XCScheme.BuildActionEntry.BuildFor> buildFor,
XCScheme.BuildAction buildAction) {
XCScheme.BuildActionEntry entry = new XCScheme.BuildActionEntry(buildableReference, buildFor);
buildAction.addBuildAction(entry);
}
public static Element serializeBuildableReference(
Document doc, XCScheme.BuildableReference buildableReference) {
Element refElem = doc.createElement("BuildableReference");
refElem.setAttribute("BuildableIdentifier", "primary");
refElem.setAttribute("BlueprintIdentifier", buildableReference.getBlueprintIdentifier());
refElem.setAttribute("BuildableName", buildableReference.getBuildableName());
refElem.setAttribute("BlueprintName", buildableReference.getBlueprintName());
String referencedContainer = "container:" + buildableReference.getContainerRelativePath();
refElem.setAttribute("ReferencedContainer", referencedContainer);
return refElem;
}
public static Element serializeBuildAction(Document doc, XCScheme.BuildAction buildAction) {
Element buildActionElem = doc.createElement("BuildAction");
buildActionElem.setAttribute(
"parallelizeBuildables", buildAction.getParallelizeBuild() ? "YES" : "NO");
buildActionElem.setAttribute(
"buildImplicitDependencies", buildAction.getParallelizeBuild() ? "YES" : "NO");
Element buildActionEntriesElem = doc.createElement("BuildActionEntries");
buildActionElem.appendChild(buildActionEntriesElem);
for (XCScheme.BuildActionEntry entry : buildAction.getBuildActionEntries()) {
Element entryElem = doc.createElement("BuildActionEntry");
buildActionEntriesElem.appendChild(entryElem);
EnumSet<XCScheme.BuildActionEntry.BuildFor> buildFor = entry.getBuildFor();
boolean buildForRunning = buildFor.contains(XCScheme.BuildActionEntry.BuildFor.RUNNING);
entryElem.setAttribute("buildForRunning", buildForRunning ? "YES" : "NO");
boolean buildForTesting = buildFor.contains(XCScheme.BuildActionEntry.BuildFor.TESTING);
entryElem.setAttribute("buildForTesting", buildForTesting ? "YES" : "NO");
boolean buildForProfiling = buildFor.contains(XCScheme.BuildActionEntry.BuildFor.PROFILING);
entryElem.setAttribute("buildForProfiling", buildForProfiling ? "YES" : "NO");
boolean buildForArchiving = buildFor.contains(XCScheme.BuildActionEntry.BuildFor.ARCHIVING);
entryElem.setAttribute("buildForArchiving", buildForArchiving ? "YES" : "NO");
boolean buildForAnalyzing = buildFor.contains(XCScheme.BuildActionEntry.BuildFor.ANALYZING);
entryElem.setAttribute("buildForAnalyzing", buildForAnalyzing ? "YES" : "NO");
Element refElem = serializeBuildableReference(doc, entry.getBuildableReference());
entryElem.appendChild(refElem);
}
return buildActionElem;
}
public static Element serializeTestAction(Document doc, XCScheme.TestAction testAction) {
Element testActionElem = doc.createElement("TestAction");
testActionElem.setAttribute("shouldUseLaunchSchemeArgsEnv", "YES");
Element testablesElem = doc.createElement("Testables");
testActionElem.appendChild(testablesElem);
for (XCScheme.TestableReference testable : testAction.getTestables()) {
Element testableElem = doc.createElement("TestableReference");
testablesElem.appendChild(testableElem);
testableElem.setAttribute("skipped", "NO");
Element refElem = serializeBuildableReference(doc, testable.getBuildableReference());
testableElem.appendChild(refElem);
}
return testActionElem;
}
public static Element serializeLaunchAction(Document doc, XCScheme.LaunchAction launchAction) {
Element launchActionElem = doc.createElement("LaunchAction");
Optional<String> runnablePath = launchAction.getRunnablePath();
Optional<String> remoteRunnablePath = launchAction.getRemoteRunnablePath();
if (remoteRunnablePath.isPresent()) {
Element remoteRunnableElem = doc.createElement("RemoteRunnable");
remoteRunnableElem.setAttribute("runnableDebuggingMode", "2");
remoteRunnableElem.setAttribute("BundleIdentifier", "com.apple.springboard");
remoteRunnableElem.setAttribute("RemotePath", remoteRunnablePath.get());
launchActionElem.appendChild(remoteRunnableElem);
Element refElem = serializeBuildableReference(doc, launchAction.getBuildableReference());
remoteRunnableElem.appendChild(refElem);
// Yes, this appears to be duplicated in Xcode as well..
Element refElem2 = serializeBuildableReference(doc, launchAction.getBuildableReference());
launchActionElem.appendChild(refElem2);
} else if (runnablePath.isPresent()) {
Element pathRunnableElem = doc.createElement("PathRunnable");
launchActionElem.appendChild(pathRunnableElem);
pathRunnableElem.setAttribute("FilePath", runnablePath.get());
} else {
Element productRunnableElem = doc.createElement("BuildableProductRunnable");
launchActionElem.appendChild(productRunnableElem);
Element refElem = serializeBuildableReference(doc, launchAction.getBuildableReference());
productRunnableElem.appendChild(refElem);
}
XCScheme.LaunchAction.LaunchStyle launchStyle = launchAction.getLaunchStyle();
launchActionElem.setAttribute(
"launchStyle", launchStyle == XCScheme.LaunchAction.LaunchStyle.AUTO ? "0" : "1");
return launchActionElem;
}
public static Element serializeProfileAction(Document doc, XCScheme.ProfileAction profileAction) {
Element profileActionElem = doc.createElement("ProfileAction");
Element productRunnableElem = doc.createElement("BuildableProductRunnable");
profileActionElem.appendChild(productRunnableElem);
Element refElem = serializeBuildableReference(doc, profileAction.getBuildableReference());
productRunnableElem.appendChild(refElem);
return profileActionElem;
}
private static void serializeScheme(XCScheme scheme, OutputStream stream) {
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();
Document doc = domImplementation.createDocument(null, "Scheme", null);
doc.setXmlVersion("1.0");
Element rootElem = doc.getDocumentElement();
rootElem.setAttribute("LastUpgradeVersion", "9999");
rootElem.setAttribute("version", "1.7");
Optional<XCScheme.BuildAction> buildAction = scheme.getBuildAction();
if (buildAction.isPresent()) {
Element buildActionElem = serializeBuildAction(doc, buildAction.get());
rootElem.appendChild(buildActionElem);
}
Optional<XCScheme.TestAction> testAction = scheme.getTestAction();
if (testAction.isPresent()) {
Element testActionElem = serializeTestAction(doc, testAction.get());
testActionElem.setAttribute(
"buildConfiguration", scheme.getTestAction().get().getBuildConfiguration());
rootElem.appendChild(testActionElem);
}
Optional<XCScheme.LaunchAction> launchAction = scheme.getLaunchAction();
if (launchAction.isPresent()) {
Element launchActionElem = serializeLaunchAction(doc, launchAction.get());
launchActionElem.setAttribute(
"buildConfiguration", launchAction.get().getBuildConfiguration());
rootElem.appendChild(launchActionElem);
} else {
Element launchActionElem = doc.createElement("LaunchAction");
launchActionElem.setAttribute("buildConfiguration", "Debug");
rootElem.appendChild(launchActionElem);
}
Optional<XCScheme.ProfileAction> profileAction = scheme.getProfileAction();
if (profileAction.isPresent()) {
Element profileActionElem = serializeProfileAction(doc, profileAction.get());
profileActionElem.setAttribute(
"buildConfiguration", profileAction.get().getBuildConfiguration());
rootElem.appendChild(profileActionElem);
} else {
Element profileActionElem = doc.createElement("ProfileAction");
profileActionElem.setAttribute("buildConfiguration", "Release");
rootElem.appendChild(profileActionElem);
}
Optional<XCScheme.AnalyzeAction> analyzeAction = scheme.getAnalyzeAction();
if (analyzeAction.isPresent()) {
Element analyzeActionElem = doc.createElement("AnalyzeAction");
analyzeActionElem.setAttribute(
"buildConfiguration", analyzeAction.get().getBuildConfiguration());
rootElem.appendChild(analyzeActionElem);
} else {
Element analyzeActionElem = doc.createElement("AnalyzeAction");
analyzeActionElem.setAttribute("buildConfiguration", "Debug");
rootElem.appendChild(analyzeActionElem);
}
Optional<XCScheme.ArchiveAction> archiveAction = scheme.getArchiveAction();
if (archiveAction.isPresent()) {
Element archiveActionElem = doc.createElement("ArchiveAction");
archiveActionElem.setAttribute(
"buildConfiguration", archiveAction.get().getBuildConfiguration());
archiveActionElem.setAttribute("revealArchiveInOrganizer", "YES");
rootElem.appendChild(archiveActionElem);
} else {
Element archiveActionElem = doc.createElement("ArchiveAction");
archiveActionElem.setAttribute("buildConfiguration", "Release");
rootElem.appendChild(archiveActionElem);
}
// write out
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(stream);
try {
transformer.transform(source, result);
} catch (TransformerException e) {
throw new RuntimeException(e);
}
}
}