// Copyright 2014 Pants project contributors (see CONTRIBUTORS.md). // Licensed under the Apache License, Version 2.0 (see LICENSE). package com.twitter.intellij.pants.service.project.modifier; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Factory; import com.intellij.openapi.util.Pair; import com.intellij.util.containers.ContainerUtil; import com.twitter.intellij.pants.service.PantsCompileOptionsExecutor; import com.twitter.intellij.pants.service.project.PantsProjectInfoModifierExtension; import com.twitter.intellij.pants.service.project.model.ContentRoot; import com.twitter.intellij.pants.service.project.model.ProjectInfo; import com.twitter.intellij.pants.service.project.model.TargetInfo; import org.jetbrains.annotations.NotNull; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; public class PantsCommonSourceRootModifier implements PantsProjectInfoModifierExtension { public static final String COMMON_SOURCES_TARGET_NAME = "common_sources"; @Override public void modify(@NotNull ProjectInfo projectInfo, @NotNull PantsCompileOptionsExecutor executor, @NotNull Logger log) { // IntelliJ doesn't support when several modules have the same source root // so, for source roots that point at multiple targets, we need to convert those so that // they have only one target that owns them. // to do that, we // - find or create a target to own the source root // - for each target that depends on that root, // we replace the root with a dependency on the new target final Map<ContentRoot, List<Pair<String, TargetInfo>>> sourceRoot2Targets = getSourceRoot2TargetMapping(projectInfo); if (sourceRoot2Targets.isEmpty()) { return; } for (Map.Entry<ContentRoot, List<Pair<String, TargetInfo>>> entry : sourceRoot2Targets.entrySet()) { final List<Pair<String, TargetInfo>> targetNameAndInfos = entry.getValue(); final ContentRoot commonContentRoot = entry.getKey(); if (targetNameAndInfos.size() <= 1) { continue; } final Pair<String, TargetInfo> commonTargetNameAndInfo = createTargetForCommonSourceRoot(executor.getBuildRoot().getPath(), targetNameAndInfos, commonContentRoot); projectInfo.addTarget(commonTargetNameAndInfo.getFirst(), commonTargetNameAndInfo.getSecond()); for (Pair<String, TargetInfo> nameAndInfo : targetNameAndInfos) { nameAndInfo.getSecond().getRoots().remove(commonContentRoot); nameAndInfo.getSecond().addDependency(commonTargetNameAndInfo.getFirst()); } } } @NotNull public Map<ContentRoot, List<Pair<String, TargetInfo>>> getSourceRoot2TargetMapping(@NotNull ProjectInfo projectInfo) { final Factory<List<Pair<String, TargetInfo>>> listFactory = ArrayList::new; final Map<ContentRoot, List<Pair<String, TargetInfo>>> result = new HashMap<>(); for (Map.Entry<String, TargetInfo> entry : projectInfo.getTargets().entrySet()) { final String targetName = entry.getKey(); final TargetInfo targetInfo = entry.getValue(); for (ContentRoot contentRoot : targetInfo.getRoots()) { ContainerUtil.getOrCreate( result, contentRoot, listFactory ).add(Pair.create(targetName, targetInfo)); } } return result; } @NotNull private Pair<String, TargetInfo> createTargetForCommonSourceRoot( @NotNull String buildRoot, @NotNull List<Pair<String, TargetInfo>> targetNameAndInfos, @NotNull ContentRoot originalContentRoot ) { final String commonTargetAddress = createTargetAddressForCommonSource(buildRoot, originalContentRoot); final TargetInfo commonInfo = createTargetForSourceRootUnioningDeps(targetNameAndInfos, originalContentRoot); return Pair.create(commonTargetAddress, commonInfo); } @NotNull private String createTargetAddressForCommonSource(@NotNull String projectPath, @NotNull ContentRoot originalContentRoot) { final String commonPath = originalContentRoot.getRawSourceRoot(); final String relativePath = Paths.get(projectPath).relativize(Paths.get(commonPath)).toString(); return relativePath + ":" + COMMON_SOURCES_TARGET_NAME; } @NotNull private TargetInfo createTargetForSourceRootUnioningDeps( @NotNull List<Pair<String, TargetInfo>> targetNameAndInfos, @NotNull ContentRoot originalContentRoot ) { final Iterator<Pair<String, TargetInfo>> iterator = targetNameAndInfos.iterator(); TargetInfo commonInfo = iterator.next().getSecond(); while (iterator.hasNext()) { commonInfo = commonInfo.union(iterator.next().getSecond()); } // make sure we won't have cyclic deps commonInfo.getTargets().removeAll(targetNameAndInfos.stream().map(s -> s.getFirst()).collect(Collectors.toSet())); final Set<ContentRoot> newRoots = ContainerUtil.newHashSet(originalContentRoot); commonInfo.setRoots(newRoots); return commonInfo; } }