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