com.google.idea.blaze.cpp.BlazeConfigurationResolver.java Source code

Java tutorial

Introduction

Here is the source code for com.google.idea.blaze.cpp.BlazeConfigurationResolver.java

Source

/*
 * Copyright 2016 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.idea.blaze.cpp;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.base.async.executor.BlazeExecutor;
import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
import com.google.idea.blaze.base.ideinfo.TargetIdeInfo;
import com.google.idea.blaze.base.ideinfo.TargetKey;
import com.google.idea.blaze.base.ideinfo.TargetMap;
import com.google.idea.blaze.base.io.FileAttributeProvider;
import com.google.idea.blaze.base.model.BlazeProjectData;
import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
import com.google.idea.blaze.base.model.primitives.LanguageClass;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.Scope;
import com.google.idea.blaze.base.scope.ScopedFunction;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.google.idea.blaze.base.scope.scopes.TimingScope;
import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
import com.google.idea.blaze.base.targetmaps.SourceToTargetMap;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import javax.annotation.Nullable;

final class BlazeConfigurationResolver {
    private static final class MapEntry {
        public final TargetKey targetKey;
        public final BlazeResolveConfiguration configuration;

        public MapEntry(TargetKey targetKey, BlazeResolveConfiguration configuration) {
            this.targetKey = targetKey;
            this.configuration = configuration;
        }
    }

    private static final Logger LOG = Logger.getInstance(BlazeConfigurationResolver.class);
    private final Project project;

    private ImmutableMap<TargetKey, BlazeResolveConfiguration> resolveConfigurations = ImmutableMap.of();

    public BlazeConfigurationResolver(Project project) {
        this.project = project;
    }

    public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
        ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap = BlazeResolveConfiguration
                .buildToolchainLookupMap(context, blazeProjectData.targetMap, blazeProjectData.reverseDependencies);
        ImmutableMap<File, VirtualFile> headerRoots = collectHeaderRoots(context, blazeProjectData,
                toolchainLookupMap);
        resolveConfigurations = buildBlazeConfigurationMap(context, blazeProjectData, toolchainLookupMap,
                headerRoots);
    }

    private static ImmutableMap<File, VirtualFile> collectHeaderRoots(BlazeContext parentContext,
            BlazeProjectData blazeProjectData, ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
        // Type specification needed to avoid incorrect type inference during command line build.
        return Scope.push(parentContext, (ScopedFunction<ImmutableMap<File, VirtualFile>>) context -> {
            context.push(new TimingScope("Resolve header include roots"));
            Set<ExecutionRootPath> paths = collectExecutionRootPaths(blazeProjectData.targetMap,
                    toolchainLookupMap);
            return doCollectHeaderRoots(context, blazeProjectData, paths);
        });
    }

    private static ImmutableMap<File, VirtualFile> doCollectHeaderRoots(BlazeContext context,
            BlazeProjectData projectData, Set<ExecutionRootPath> rootPaths) {
        ConcurrentMap<File, VirtualFile> rootsMap = Maps.newConcurrentMap();
        List<ListenableFuture<Void>> futures = Lists.newArrayListWithCapacity(rootPaths.size());
        for (ExecutionRootPath path : rootPaths) {
            futures.add(submit(() -> {
                ImmutableList<File> possibleDirectories = projectData.workspacePathResolver
                        .resolveToIncludeDirectories(path);
                for (File file : possibleDirectories) {
                    VirtualFile vf = getVirtualFile(file);
                    if (vf != null) {
                        rootsMap.put(file, vf);
                    } else if (!projectData.blazeRoots.isOutputArtifact(path)
                            && FileAttributeProvider.getInstance().exists(file)) {
                        // If it's not a blaze output file, we expect it to always resolve.
                        LOG.info(String.format("Unresolved header root %s", file.getAbsolutePath()));
                    }
                }
                return null;
            }));
        }
        try {
            Futures.allAsList(futures).get();
            return ImmutableMap.copyOf(rootsMap);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            context.setCancelled();
        } catch (ExecutionException e) {
            IssueOutput.error("Error resolving header include roots: " + e).submit(context);
            LOG.error("Error resolving header include roots", e);
        }
        return ImmutableMap.of();
    }

    private static Set<ExecutionRootPath> collectExecutionRootPaths(TargetMap targetMap,
            ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap) {
        Set<ExecutionRootPath> paths = Sets.newHashSet();
        for (TargetIdeInfo target : targetMap.targets()) {
            if (target.cIdeInfo != null) {
                paths.addAll(target.cIdeInfo.transitiveSystemIncludeDirectories);
                paths.addAll(target.cIdeInfo.transitiveIncludeDirectories);
                paths.addAll(target.cIdeInfo.transitiveQuoteIncludeDirectories);
            }
        }
        for (CToolchainIdeInfo toolchain : toolchainLookupMap.values()) {
            paths.addAll(toolchain.builtInIncludeDirectories);
            paths.addAll(toolchain.unfilteredToolchainSystemIncludes);
        }
        return paths;
    }

    @Nullable
    private static VirtualFile getVirtualFile(File file) {
        LocalFileSystem fileSystem = LocalFileSystem.getInstance();
        VirtualFile vf = fileSystem.findFileByPathIfCached(file.getPath());
        if (vf == null) {
            vf = fileSystem.findFileByIoFile(file);
        }
        return vf;
    }

    private ImmutableMap<TargetKey, BlazeResolveConfiguration> buildBlazeConfigurationMap(
            BlazeContext parentContext, BlazeProjectData blazeProjectData,
            ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
            ImmutableMap<File, VirtualFile> headerRoots) {
        // Type specification needed to avoid incorrect type inference during command line build.
        return Scope.push(parentContext,
                (ScopedFunction<ImmutableMap<TargetKey, BlazeResolveConfiguration>>) context -> {
                    context.push(new TimingScope("Build C configuration map"));

                    ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache = Maps.newConcurrentMap();
                    List<ListenableFuture<MapEntry>> mapEntryFutures = Lists.newArrayList();

                    for (TargetIdeInfo target : blazeProjectData.targetMap.targets()) {
                        if (target.kind.getLanguageClass() == LanguageClass.C) {
                            ListenableFuture<MapEntry> future = submit(() -> createResolveConfiguration(target,
                                    toolchainLookupMap, headerRoots, compilerWrapperCache, blazeProjectData));
                            mapEntryFutures.add(future);
                        }
                    }

                    ImmutableMap.Builder<TargetKey, BlazeResolveConfiguration> newResolveConfigurations = ImmutableMap
                            .builder();
                    List<MapEntry> mapEntries;
                    try {
                        mapEntries = Futures.allAsList(mapEntryFutures).get();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        context.setCancelled();
                        return ImmutableMap.of();
                    } catch (ExecutionException e) {
                        IssueOutput.error("Could not build C resolve configurations: " + e).submit(context);
                        LOG.error("Could not build C resolve configurations", e);
                        return ImmutableMap.of();
                    }

                    for (MapEntry mapEntry : mapEntries) {
                        // Skip over labels that don't have C configuration data.
                        if (mapEntry != null) {
                            newResolveConfigurations.put(mapEntry.targetKey, mapEntry.configuration);
                        }
                    }
                    return newResolveConfigurations.build();
                });
    }

    private static <T> ListenableFuture<T> submit(Callable<T> callable) {
        return BlazeExecutor.getInstance().submit(callable);
    }

    @Nullable
    private MapEntry createResolveConfiguration(TargetIdeInfo target,
            ImmutableMap<TargetKey, CToolchainIdeInfo> toolchainLookupMap,
            ImmutableMap<File, VirtualFile> headerRoots,
            ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache, BlazeProjectData blazeProjectData) {
        TargetKey targetKey = target.key;

        CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(targetKey);
        if (toolchainIdeInfo != null) {
            File compilerWrapper = findOrCreateCompilerWrapperScript(compilerWrapperCache, toolchainIdeInfo,
                    blazeProjectData.workspacePathResolver, targetKey);
            if (compilerWrapper != null) {
                BlazeResolveConfiguration config = BlazeResolveConfiguration.createConfigurationForTarget(project,
                        blazeProjectData.workspacePathResolver, headerRoots,
                        blazeProjectData.targetMap.get(targetKey), toolchainIdeInfo, compilerWrapper);
                if (config != null) {
                    return new MapEntry(targetKey, config);
                }
            }
        }
        return null;
    }

    @Nullable
    private static File findOrCreateCompilerWrapperScript(Map<CToolchainIdeInfo, File> compilerWrapperCache,
            CToolchainIdeInfo toolchainIdeInfo, WorkspacePathResolver workspacePathResolver, TargetKey targetKey) {
        File compilerWrapper = compilerWrapperCache.get(toolchainIdeInfo);
        if (compilerWrapper == null) {
            File cppExecutable = toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile();
            if (cppExecutable != null && !cppExecutable.isAbsolute()) {
                cppExecutable = workspacePathResolver.resolveToFile(cppExecutable.getPath());
            }
            if (cppExecutable == null) {
                String errorMessage = String.format("Unable to find compiler executable: %s for rule %s",
                        toolchainIdeInfo.cppExecutable.toString(), targetKey);
                LOG.warn(errorMessage);
                compilerWrapper = null;
            } else {
                compilerWrapper = createCompilerExecutableWrapper(cppExecutable);
                if (compilerWrapper != null) {
                    compilerWrapperCache.put(toolchainIdeInfo, compilerWrapper);
                }
            }
        }
        return compilerWrapper;
    }

    /**
     * Create a wrapper script that transforms the CLion compiler invocation into a safe invocation of
     * the compiler script that blaze uses.
     *
     * <p>CLion passes arguments to the compiler in an arguments file. The c toolchain compiler
     * wrapper script doesn't handle arguments files, so we need to move the compiler arguments from
     * the file to the command line.
     *
     * @param blazeCompilerExecutableFile blaze compiler wrapper
     * @return The wrapper script that CLion can call.
     */
    @Nullable
    private static File createCompilerExecutableWrapper(File blazeCompilerExecutableFile) {
        try {
            File blazeCompilerWrapper = FileUtil.createTempFile("blaze_compiler", ".sh", true /* deleteOnExit */);
            if (!blazeCompilerWrapper.setExecutable(true)) {
                return null;
            }
            ImmutableList<String> compilerWrapperScriptLines = ImmutableList.of("#!/bin/bash", "",
                    "# The c toolchain compiler wrapper script doesn't handle arguments files, so we",
                    "# need to move the compiler arguments from the file to the command line.", "",
                    "if [ $# -ne 2 ]; then", "  echo \"Usage: $0 @arg-file compile-file\"", "  exit 2;", "fi", "",
                    "if [[ $1 != @* ]]; then", "  echo \"Usage: $0 @arg-file compile-file\"", "  exit 3;", "fi", "",
                    " # Remove the @ before the arguments file path", "ARG_FILE=${1#@}",
                    "# The actual compiler wrapper script we get from blaze",
                    "EXE=" + blazeCompilerExecutableFile.getPath(),
                    "# Read in the arguments file so we can pass the arguments on the command line.",
                    "ARGS=`cat $ARG_FILE`", "$EXE $ARGS $2");

            try (PrintWriter pw = new PrintWriter(blazeCompilerWrapper, UTF_8.name())) {
                compilerWrapperScriptLines.forEach(pw::println);
            }
            return blazeCompilerWrapper;
        } catch (IOException e) {
            return null;
        }
    }

    @Nullable
    public OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
        SourceToTargetMap sourceToTargetMap = SourceToTargetMap.getInstance(project);
        List<TargetKey> targetsForSourceFile = Lists
                .newArrayList(sourceToTargetMap.getRulesForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile)));
        if (targetsForSourceFile.isEmpty()) {
            return null;
        }

        // If a source file is in two different targets,
        // we can't possibly show how it will be interpreted in both contexts at the same time
        // in the IDE, so just pick the first target after we sort.
        targetsForSourceFile.sort((o1, o2) -> o1.toString().compareTo(o2.toString()));
        TargetKey targetKey = Iterables.getFirst(targetsForSourceFile, null);
        assert (targetKey != null);

        return resolveConfigurations.get(targetKey);
    }

    public List<? extends OCResolveConfiguration> getAllConfigurations() {
        return ImmutableList.copyOf(resolveConfigurations.values());
    }
}