com.facebook.buck.features.apple.project.SchemeGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.features.apple.project.SchemeGenerator.java

Source

/*
 * 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.features.apple.project;

import com.facebook.buck.apple.xcode.XCScheme;
import com.facebook.buck.apple.xcode.XCScheme.AdditionalActions;
import com.facebook.buck.apple.xcode.xcodeproj.PBXTarget;
import com.facebook.buck.core.util.log.Logger;
import com.facebook.buck.io.MoreProjectFilesystems;
import com.facebook.buck.io.filesystem.ProjectFilesystem;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
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.Objects;
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 boolean wasCreatedForAppExtension;
    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 Optional<XCScheme.LaunchAction.WatchInterface> watchInterface;
    private final Optional<String> notificationPayloadFile;
    private final XCScheme.LaunchAction.LaunchStyle launchStyle;
    private final Optional<ImmutableMap<SchemeActionType, ImmutableMap<String, String>>> environmentVariables;
    private Optional<ImmutableMap<SchemeActionType, ImmutableMap<XCScheme.AdditionalActions, ImmutableList<String>>>> additionalSchemeActions;

    public SchemeGenerator(ProjectFilesystem projectFilesystem, Optional<PBXTarget> primaryTarget,
            ImmutableSet<PBXTarget> orderedBuildTargets, ImmutableSet<PBXTarget> orderedBuildTestTargets,
            ImmutableSet<PBXTarget> orderedRunTestTargets, String schemeName, Path outputDirectory,
            boolean parallelizeBuild, Optional<Boolean> wasCreatedForAppExtension, Optional<String> runnablePath,
            Optional<String> remoteRunnablePath, ImmutableMap<SchemeActionType, String> actionConfigNames,
            ImmutableMap<PBXTarget, Path> targetToProjectPathMap,
            Optional<ImmutableMap<SchemeActionType, ImmutableMap<String, String>>> environmentVariables,
            Optional<ImmutableMap<SchemeActionType, ImmutableMap<XCScheme.AdditionalActions, ImmutableList<String>>>> additionalSchemeActions,
            XCScheme.LaunchAction.LaunchStyle launchStyle,
            Optional<XCScheme.LaunchAction.WatchInterface> watchInterface,
            Optional<String> notificationPayloadFile) {
        this.projectFilesystem = projectFilesystem;
        this.primaryTarget = primaryTarget;
        this.watchInterface = watchInterface;
        this.launchStyle = launchStyle;
        this.orderedBuildTargets = orderedBuildTargets;
        this.orderedBuildTestTargets = orderedBuildTestTargets;
        this.orderedRunTestTargets = orderedRunTestTargets;
        this.schemeName = schemeName;
        this.outputDirectory = outputDirectory;
        this.parallelizeBuild = parallelizeBuild;
        this.wasCreatedForAppExtension = wasCreatedForAppExtension.orElse(false);
        this.runnablePath = runnablePath;
        this.remoteRunnablePath = remoteRunnablePath;
        this.actionConfigNames = actionConfigNames;
        this.targetToProjectPathMap = targetToProjectPathMap;
        this.environmentVariables = environmentVariables;
        this.additionalSchemeActions = additionalSchemeActions;
        this.notificationPayloadFile = notificationPayloadFile;

        LOG.debug("Generating scheme with build targets %s, test build targets %s, test bundle targets %s",
                orderedBuildTargets, orderedBuildTestTargets, orderedRunTestTargets);
    }

    @VisibleForTesting
    private Optional<ImmutableList<XCScheme.SchemePrePostAction>> additionalCommandsForSchemeAction(
            SchemeActionType schemeActionType, XCScheme.AdditionalActions actionType,
            Optional<XCScheme.BuildableReference> primaryTarget) {

        Optional<ImmutableList<String>> commands = this.additionalSchemeActions
                .map(input -> Optional.ofNullable(input.get(schemeActionType))).filter(Optional::isPresent)
                .map(input -> Optional.ofNullable(input.get().get(actionType))).orElse(Optional.empty());
        if (commands.isPresent()) {
            ImmutableList<XCScheme.SchemePrePostAction> actions = commands.get().stream()
                    .map(command -> new XCScheme.SchemePrePostAction(primaryTarget, command))
                    .collect(ImmutableList.toImmutableList());
            return Optional.of(actions);
        } else {
            return Optional.empty();
        }
    }

    @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 = Objects.requireNonNull(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,
                    Objects.requireNonNull(target.getGlobalID()),
                    target.getProductReference() != null ? target.getProductReference().getName()
                            : Objects.requireNonNull(target.getProductName()),
                    blueprintName);
            buildTargetToBuildableReferenceMap.put(target, buildableReference);
        }

        Optional<XCScheme.BuildableReference> primaryBuildReference = Optional.empty();
        if (primaryTarget.isPresent()) {
            primaryBuildReference = Optional
                    .ofNullable(buildTargetToBuildableReferenceMap.get(primaryTarget.get()));
        }

        XCScheme.BuildAction buildAction = new XCScheme.BuildAction(parallelizeBuild,
                additionalCommandsForSchemeAction(SchemeActionType.BUILD,
                        XCScheme.AdditionalActions.PRE_SCHEME_ACTIONS, primaryBuildReference),
                additionalCommandsForSchemeAction(SchemeActionType.BUILD,
                        XCScheme.AdditionalActions.POST_SCHEME_ACTIONS, primaryBuildReference));

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

        ImmutableMap<SchemeActionType, ImmutableMap<String, String>> envVariables = ImmutableMap.of();
        if (environmentVariables.isPresent()) {
            envVariables = environmentVariables.get();
        }

        XCScheme.TestAction testAction = new XCScheme.TestAction(
                Objects.requireNonNull(actionConfigNames.get(SchemeActionType.TEST)),
                Optional.ofNullable(envVariables.get(SchemeActionType.TEST)),
                additionalCommandsForSchemeAction(SchemeActionType.TEST, AdditionalActions.PRE_SCHEME_ACTIONS,
                        primaryBuildReference),
                additionalCommandsForSchemeAction(SchemeActionType.TEST, AdditionalActions.POST_SCHEME_ACTIONS,
                        primaryBuildReference));

        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,
                        Objects.requireNonNull(actionConfigNames.get(SchemeActionType.LAUNCH)), runnablePath,
                        remoteRunnablePath, watchInterface, launchStyle,
                        Optional.ofNullable(envVariables.get(SchemeActionType.LAUNCH)),
                        additionalCommandsForSchemeAction(SchemeActionType.LAUNCH,
                                AdditionalActions.PRE_SCHEME_ACTIONS, primaryBuildReference),
                        additionalCommandsForSchemeAction(SchemeActionType.LAUNCH,
                                AdditionalActions.POST_SCHEME_ACTIONS, primaryBuildReference),
                        notificationPayloadFile));

                profileAction = Optional.of(new XCScheme.ProfileAction(primaryBuildableReference,
                        Objects.requireNonNull(actionConfigNames.get(SchemeActionType.PROFILE)),
                        Optional.ofNullable(envVariables.get(SchemeActionType.PROFILE)),
                        additionalCommandsForSchemeAction(SchemeActionType.PROFILE,
                                AdditionalActions.PRE_SCHEME_ACTIONS, primaryBuildReference),
                        additionalCommandsForSchemeAction(SchemeActionType.PROFILE,
                                AdditionalActions.POST_SCHEME_ACTIONS, primaryBuildReference)));
            }
        }
        XCScheme.AnalyzeAction analyzeAction = new XCScheme.AnalyzeAction(
                Objects.requireNonNull(actionConfigNames.get(SchemeActionType.ANALYZE)),
                additionalCommandsForSchemeAction(SchemeActionType.ANALYZE, AdditionalActions.PRE_SCHEME_ACTIONS,
                        primaryBuildReference),
                additionalCommandsForSchemeAction(SchemeActionType.ANALYZE, AdditionalActions.POST_SCHEME_ACTIONS,
                        primaryBuildReference));

        XCScheme.ArchiveAction archiveAction = new XCScheme.ArchiveAction(
                Objects.requireNonNull(actionConfigNames.get(SchemeActionType.ARCHIVE)),
                additionalCommandsForSchemeAction(SchemeActionType.ARCHIVE, AdditionalActions.PRE_SCHEME_ACTIONS,
                        primaryBuildReference),
                additionalCommandsForSchemeAction(SchemeActionType.ARCHIVE, AdditionalActions.POST_SCHEME_ACTIONS,
                        primaryBuildReference));

        XCScheme scheme = new XCScheme(schemeName, wasCreatedForAppExtension, 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 serializeEnvironmentVariables(Document doc,
            ImmutableMap<String, String> environmentVariables) {
        Element rootElement = doc.createElement("EnvironmentVariables");
        for (String variableKey : environmentVariables.keySet()) {
            Element variableElement = doc.createElement("EnvironmentVariable");
            variableElement.setAttribute("key", variableKey);
            variableElement.setAttribute("value", environmentVariables.get(variableKey));
            variableElement.setAttribute("isEnabled", "YES");
            rootElement.appendChild(variableElement);
        }
        return rootElement;
    }

    public static Element serializeBuildAction(Document doc, XCScheme.BuildAction buildAction) {
        Element buildActionElem = doc.createElement("BuildAction");
        serializePrePostActions(doc, buildAction, buildActionElem);

        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");
        serializePrePostActions(doc, testAction, testActionElem);

        // unless otherwise specified, use the Launch scheme's env variables like the xcode default
        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);
        }

        if (testAction.getEnvironmentVariables().isPresent()) {
            // disable the default override that makes Test use Launch's environment variables
            testActionElem.setAttribute("shouldUseLaunchSchemeArgsEnv", "NO");
            Element environmentVariablesElement = serializeEnvironmentVariables(doc,
                    testAction.getEnvironmentVariables().get());
            testActionElem.appendChild(environmentVariablesElement);
        }

        return testActionElem;
    }

    public static Element serializeLaunchAction(Document doc, XCScheme.LaunchAction launchAction) {
        Element launchActionElem = doc.createElement("LaunchAction");
        serializePrePostActions(doc, launchAction, launchActionElem);

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

        Optional<XCScheme.LaunchAction.WatchInterface> watchInterface = launchAction.getWatchInterface();
        if (watchInterface.isPresent()) {
            Optional<String> watchInterfaceValue = Optional.empty();
            switch (watchInterface.get()) {
            case MAIN:
                // excluded from scheme
            case COMPLICATION:
                watchInterfaceValue = Optional.of("32");
                break;
            case DYNAMIC_NOTIFICATION:
                watchInterfaceValue = Optional.of("8");
                break;
            case STATIC_NOTIFICATION:
                watchInterfaceValue = Optional.of("16");
                break;
            }

            if (watchInterfaceValue.isPresent()) {
                launchActionElem.setAttribute("launchAutomaticallySubstyle", watchInterfaceValue.get());
            }
        }

        Optional<String> notificationPayloadFile = launchAction.getNotificationPayloadFile();
        if (notificationPayloadFile.isPresent()) {
            launchActionElem.setAttribute("notificationPayloadFile", notificationPayloadFile.get());
        }

        XCScheme.LaunchAction.LaunchStyle launchStyle = launchAction.getLaunchStyle();
        launchActionElem.setAttribute("launchStyle",
                launchStyle == XCScheme.LaunchAction.LaunchStyle.AUTO ? "0" : "1");

        if (launchAction.getEnvironmentVariables().isPresent()) {
            Element environmentVariablesElement = serializeEnvironmentVariables(doc,
                    launchAction.getEnvironmentVariables().get());
            launchActionElem.appendChild(environmentVariablesElement);
        }

        return launchActionElem;
    }

    public static Element serializeProfileAction(Document doc, XCScheme.ProfileAction profileAction) {
        Element profileActionElem = doc.createElement("ProfileAction");
        serializePrePostActions(doc, profileAction, profileActionElem);

        // unless otherwise specified, use the Launch scheme's env variables like the xcode default
        profileActionElem.setAttribute("shouldUseLaunchSchemeArgsEnv", "YES");

        Element productRunnableElem = doc.createElement("BuildableProductRunnable");
        profileActionElem.appendChild(productRunnableElem);

        Element refElem = serializeBuildableReference(doc, profileAction.getBuildableReference());
        productRunnableElem.appendChild(refElem);

        if (profileAction.getEnvironmentVariables().isPresent()) {
            // disable the default override that makes Profile use Launch's environment variables
            profileActionElem.setAttribute("shouldUseLaunchSchemeArgsEnv", "NO");
            Element environmentVariablesElement = serializeEnvironmentVariables(doc,
                    profileAction.getEnvironmentVariables().get());
            profileActionElem.appendChild(environmentVariablesElement);
        }

        return profileActionElem;
    }

    private static void serializePrePostActions(Document doc, XCScheme.SchemeAction action, Element actionElem) {
        if (action.getPreActions().isPresent()) {
            actionElem.appendChild(serializePrePostAction(doc, "PreActions", action.getPreActions().get()));
        }
        if (action.getPostActions().isPresent()) {
            actionElem.appendChild(serializePrePostAction(doc, "PostActions", action.getPostActions().get()));
        }
    }

    private static Element serializePrePostAction(Document doc, String tagName,
            ImmutableList<XCScheme.SchemePrePostAction> actions) {
        Element executionActions = doc.createElement(tagName);
        for (XCScheme.SchemePrePostAction action : actions) {
            executionActions.appendChild(serializeExecutionAction(doc, action));
        }
        return executionActions;
    }

    public static Element serializeExecutionAction(Document doc, XCScheme.SchemePrePostAction action) {
        Element executionAction = doc.createElement("ExecutionAction");
        executionAction.setAttribute("ActionType",
                "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction");

        Element actionContent = doc.createElement("ActionContent");
        actionContent.setAttribute("title", "Run Script");
        actionContent.setAttribute("scriptText", action.getCommand());
        actionContent.setAttribute("shellToInvoke", "/bin/bash");

        Optional<XCScheme.BuildableReference> buildableReference = action.getBuildableReference();
        if (buildableReference.isPresent()) {
            Element environmentBuildable = doc.createElement("EnvironmentBuildable");
            environmentBuildable.appendChild(serializeBuildableReference(doc, buildableReference.get()));
            actionContent.appendChild(environmentBuildable);
        }

        executionAction.appendChild(actionContent);
        return executionAction;
    }

    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");
        if (scheme.getWasCreatedForExtension()) {
            rootElem.setAttribute("wasCreatedForAppExtension", "YES");
        }
        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");
            serializePrePostActions(doc, analyzeAction.get(), analyzeActionElem);
            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");
            serializePrePostActions(doc, archiveAction.get(), archiveActionElem);
            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);
        }
    }
}