aswb/querysync/java/com/google/idea/blaze/qsync/cc/ConfigureCcCompilation.kt (172 lines of code) (raw):

/* * Copyright 2023 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.qsync.cc import com.google.common.base.Preconditions import com.google.common.collect.ImmutableListMultimap import com.google.common.collect.Multimap import com.google.idea.blaze.common.Context import com.google.idea.blaze.common.PrintOutput import com.google.idea.blaze.exception.BuildException import com.google.idea.blaze.qsync.deps.ArtifactDirectories import com.google.idea.blaze.qsync.deps.ArtifactTracker.State import com.google.idea.blaze.qsync.deps.CcCompilationInfo import com.google.idea.blaze.qsync.deps.CcToolchain import com.google.idea.blaze.qsync.deps.DependencyBuildContext import com.google.idea.blaze.qsync.deps.ProjectProtoUpdate import com.google.idea.blaze.qsync.deps.ProjectProtoUpdateOperation import com.google.idea.blaze.qsync.project.LanguageClassProto.LanguageClass import com.google.idea.blaze.qsync.project.ProjectPath import com.google.idea.blaze.qsync.project.ProjectProto.CcCompilationContext import com.google.idea.blaze.qsync.project.ProjectProto.CcCompilerFlag import com.google.idea.blaze.qsync.project.ProjectProto.CcCompilerFlagSet import com.google.idea.blaze.qsync.project.ProjectProto.CcCompilerSettings import com.google.idea.blaze.qsync.project.ProjectProto.CcLanguage import com.google.idea.blaze.qsync.project.ProjectProto.CcSourceFile import com.google.idea.blaze.qsync.project.ProjectTarget.SourceType import com.intellij.util.containers.orNull import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger /** Adds C/C++ compilation information and headers to the project proto. */ class ConfigureCcCompilation(private val artifactState: State, private val update: ProjectProtoUpdate) { /** An update operation to configure CC compilation. */ class UpdateOperation : ProjectProtoUpdateOperation { override fun update( update: ProjectProtoUpdate, artifactState: State, context: Context<*>, ) { ConfigureCcCompilation(artifactState, update).update(); } } /* Map from toolchain ID -> language -> flags for that toolchain & language. */ private val toolchainLanguageFlags: MutableMap<String, Multimap<CcLanguage, CcCompilerFlag>> = hashMapOf() /* Map of unique sets of compiler flags to an ID to identify them. * We do this as the downstream code turns each set of flags into a CidrCompilerSwitches instance * which can have a large memory footprint. */ private val uniqueFlagSetIds: MutableMap<Set<CcCompilerFlag>, String> = hashMapOf() @Throws(BuildException::class) fun update() { visitToolchainMap(artifactState.ccToolchainMap()) for (target in artifactState.targets()) { val ccInfo = target.ccInfo().orNull() ?: continue visitTarget(ccInfo, target.buildContext()) } if (update.project().getCcWorkspaceBuilder().getContextsCount() > 0) { update.project().addActiveLanguages(LanguageClass.LANGUAGE_CLASS_CC) } } private fun visitToolchainMap(toolchainInfoMap: Map<String, CcToolchain>) { toolchainInfoMap.values.forEach(this::visitToolchain) } private fun visitToolchain(toolchain: CcToolchain) { val commonFlags = toolchain.builtInIncludeDirectories().map { makePathFlag("-I", it) } toolchainLanguageFlags.put( toolchain.id(), ImmutableListMultimap.builder<CcLanguage, CcCompilerFlag>() .putAll( CcLanguage.C, commonFlags + toolchain.cOptions().map { makeStringFlag(it, "") } ) .putAll( CcLanguage.CPP, commonFlags + toolchain.cppOptions().map { makeStringFlag(it, "") } ) .build() ) } private fun visitTarget(ccInfo: CcCompilationInfo, buildContext: DependencyBuildContext) { val projectTarget = update.buildGraph().getProjectTarget(ccInfo.target()) // This target is no longer present in the project. Ignore it. // We should really clean up the dependency cache itself to remove any artifacts relating to // no-longer-present targets, but that will be a lot more work. For now, just ensure we // don't crash. ?: return val toolchain = Preconditions.checkNotNull( artifactState.ccToolchainMap().get(ccInfo.toolchainId()), ccInfo.toolchainId())!! val targetFlags = buildList { addAll(projectTarget.copts().map { makeStringFlag(it, "") }) addAll(ccInfo.defines().map { makeStringFlag("-D", it) }) addAll(ccInfo.includeDirectories().map { makePathFlag("-I", it) }) addAll(ccInfo.quoteIncludeDirectories().map { p -> makePathFlag("-iquote", p) }) addAll(ccInfo.systemIncludeDirectories().map { p -> makePathFlag("-isystem", p) }) addAll(ccInfo.frameworkIncludeDirectories().map { p -> makePathFlag("-F", p) }) } // TODO(mathewi): The handling of flag sets here is not optimal, since we recalculate an // identical flag set for each source of the same language, then immediately de-dupe them in // the addFlagSet call. For large flag sets this may be slow. val srcs = update.buildGraph().getTargetSources(ccInfo.target(), *SourceType.all()) .mapNotNull { srcPath -> val lang = getLanguage(srcPath) ?: return@mapNotNull null CcSourceFile.newBuilder() .setLanguage(lang) .setWorkspacePath(srcPath.toString()) .setCompilerSettings( CcCompilerSettings.newBuilder() .setCompilerExecutablePath(toolchain.compilerExecutable().toProto()) .setFlagSetId(addFlagSet(targetFlags + toolchainLanguageFlags[toolchain.id()]?.get(lang).orEmpty())) ) .build() } val targetContext = CcCompilationContext.newBuilder() .setId(ccInfo.target().toString() + "%" + toolchain.targetGnuSystemName()) .setHumanReadableName(ccInfo.target().toString() + " - " + toolchain.targetGnuSystemName()) .addAllSources(srcs) .putAllLanguageToCompilerSettings( toolchainLanguageFlags[toolchain.id()]?.asMap()?.entries?.associate { it.key.getValueDescriptor().name to CcCompilerSettings.newBuilder() .setCompilerExecutablePath( toolchain.compilerExecutable().toProto()) .setFlagSetId(addFlagSet(it.value)) .build() } .orEmpty() ) .build() update.project().getCcWorkspaceBuilder().addContexts(targetContext) val headersDir = update.artifactDirectory(ArtifactDirectories.GEN_CC_HEADERS) for (artifact in ccInfo.genHeaders()) { headersDir.addIfNewer(artifact.artifactPath(), artifact, buildContext) } } /** Ensure that the given flagset exists, adding it if necessary, and return its unique ID. */ private fun addFlagSet(flags: Collection<CcCompilerFlag>): String { // Create a set so that two flags sets are considered equivalent if their flag order differs. val canonicalFlagSet: kotlin.collections.Set<CcCompilerFlag> = flags.toSet() return uniqueFlagSetIds[canonicalFlagSet] ?: nextFlagSetId.incrementAndGet().toString().also { flagSetId -> uniqueFlagSetIds[canonicalFlagSet] = flagSetId update .project() .getCcWorkspaceBuilder() .putFlagSets(flagSetId, CcCompilerFlagSet.newBuilder().addAllFlags(flags).build()) } } private fun makeStringFlag(flag: String, value: String): CcCompilerFlag { return CcCompilerFlag.newBuilder().setFlag(flag).setPlainValue(value).build() } private fun makePathFlag(flag: String, path: ProjectPath): CcCompilerFlag { return CcCompilerFlag.newBuilder().setFlag(flag).setPath(path.toProto()).build() } companion object { private val nextFlagSetId = AtomicInteger(0); private val EXTENSION_TO_LANGUAGE_MAP = mapOf( "c" to CcLanguage.C, "cc" to CcLanguage.CPP, "cpp" to CcLanguage.CPP, "cxx" to CcLanguage.CPP, "c++" to CcLanguage.CPP, "C" to CcLanguage.C) /* Files we ignore because they are not top level source files: */ private val IGNORE_SRC_FILE_EXTENSIONS = setOf("h", "hh", "hpp", "hxx", "inc", "inl", "H", "S", "a", "lo", "so", "o") } private fun getLanguage(srcPath: Path): CcLanguage? { // logic in here based on https://bazel.build/reference/be/c-cpp#cc_library.srcs val lastDot = srcPath.fileName.toString().lastIndexOf('.'); if (lastDot < 0) { // default to cpp update .context() .output(PrintOutput.log("No extension for c/c++ source file %s; assuming cpp", srcPath)) return CcLanguage.CPP } val ext = srcPath.fileName.toString().substring(lastDot + 1); if (IGNORE_SRC_FILE_EXTENSIONS.contains(ext)) { return null } if (EXTENSION_TO_LANGUAGE_MAP.containsKey(ext)) { return EXTENSION_TO_LANGUAGE_MAP[ext] } update .context() .output( PrintOutput.log( "Unrecognized extension %s for c/c++ source file %s; assuming cpp", ext, srcPath)) return CcLanguage.CPP } }