/* * Copyright 2016-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.cxx; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import com.facebook.buck.cli.FakeBuckConfig; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.model.Flavor; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.BuildRuleSuccessType; import com.facebook.buck.rules.DefaultBuildTargetSourcePath; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildRuleParamsBuilder; import com.facebook.buck.rules.FakeSourcePath; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNodeToBuildRuleTransformer; import com.facebook.buck.testutil.integration.BuckBuildLog; import com.facebook.buck.testutil.integration.ProjectWorkspace; import com.facebook.buck.testutil.integration.TemporaryPaths; import com.facebook.buck.testutil.integration.TestDataHelper; import com.facebook.buck.util.environment.Platform; import com.facebook.buck.util.sha1.Sha1HashCode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSortedSet; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.TreeMap; import java.util.stream.Collectors; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @SuppressWarnings("all") public class CxxPrecompiledHeaderRuleTest { private static final CxxBuckConfig CXX_CONFIG_PCH_ENABLED = new CxxBuckConfig(FakeBuckConfig.builder().setSections("[cxx]", "pch_enabled=true").build()); private static final PreprocessorProvider PREPROCESSOR_SUPPORTING_PCH = new PreprocessorProvider(Paths.get("foopp"), Optional.of(CxxToolProvider.Type.CLANG)); private static final CxxPlatform PLATFORM_SUPPORTING_PCH = CxxPlatformUtils.build(CXX_CONFIG_PCH_ENABLED).withCpp(PREPROCESSOR_SUPPORTING_PCH); @Rule public TemporaryPaths tmp = new TemporaryPaths(); private ProjectFilesystem filesystem; private ProjectWorkspace workspace; @Before public void setUp() throws InterruptedException, IOException { filesystem = new ProjectFilesystem(tmp.getRoot()); workspace = TestDataHelper.createProjectWorkspaceForScenario(this, "cxx_precompiled_header_rule", tmp); workspace.setUp(); } public final TargetNodeToBuildRuleTransformer transformer = new DefaultTargetNodeToBuildRuleTransformer(); public final BuildRuleResolver ruleResolver = new BuildRuleResolver(TargetGraph.EMPTY, transformer); public final SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(ruleResolver); public final SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); public final Compiler compiler = CxxPlatformUtils.DEFAULT_PLATFORM.getCxx().resolve(ruleResolver); public BuildTarget newTarget(String fullyQualifiedName) { return BuildTargetFactory.newInstance(fullyQualifiedName); } public BuildRuleParams newParams(BuildTarget target) { return new FakeBuildRuleParamsBuilder(target).build(); } /** Note: creates the {@link CxxPrecompiledHeaderTemplate}, add to ruleResolver index. */ public CxxPrecompiledHeaderTemplate newPCH( BuildTarget target, SourcePath headerSourcePath, ImmutableSortedSet<BuildRule> deps) { return new CxxPrecompiledHeaderTemplate( newParams(target).copyAppendingExtraDeps(deps), ruleResolver, headerSourcePath); } public CxxSource.Builder newCxxSourceBuilder() { return CxxSource.builder().setType(CxxSource.Type.C); } public CxxSource newSource(String filename) { return newCxxSourceBuilder().setPath(new FakeSourcePath(filename)).build(); } public CxxSource newSource() { return newSource("foo.cpp"); } public CxxSourceRuleFactory.Builder newFactoryBuilder(BuildRuleParams params) { SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(ruleResolver); return CxxSourceRuleFactory.builder() .setParams(params) .setResolver(ruleResolver) .setRuleFinder(ruleFinder) .setPathResolver(new SourcePathResolver(ruleFinder)) .setCxxPlatform(PLATFORM_SUPPORTING_PCH) .setPicType(AbstractCxxSourceRuleFactory.PicType.PIC) .setCxxBuckConfig(CXX_CONFIG_PCH_ENABLED); } public CxxSourceRuleFactory.Builder newFactoryBuilder(BuildRuleParams params, String flag) { return newFactoryBuilder(params) .setCxxPreprocessorInput( ImmutableList.of( CxxPreprocessorInput.builder() .setPreprocessorFlags(ImmutableMultimap.of(CxxSource.Type.C, flag)) .build())); } private CxxPrecompiledHeaderTemplate newPCH(BuildTarget pchTarget) { return newPCH(pchTarget, new FakeSourcePath("header.h"), /* deps */ ImmutableSortedSet.of()); } /** Return the sublist, starting at {@code toFind}, or empty list if not found. */ List<String> seek(List<String> immList, String toFind) { ArrayList<String> list = new ArrayList<>(immList.size()); list.addAll(immList); int i; for (i = 0; i < list.size(); i++) { if (list.get(i).equals(toFind)) { break; } } return list.subList(i, list.size()); } private boolean platformOkForPCHTests() { return Platform.detect() != Platform.WINDOWS; } /** @return exit code from that process */ private int runBuiltBinary(String binaryTarget) throws Exception { return workspace .runCommand( workspace .resolve( BuildTargets.getGenPath( filesystem, workspace.newBuildTarget(binaryTarget), "%s")) .toString()) .getExitCode(); } /** Stolen from {@link PrecompiledHeaderIntegrationTest} */ private static Matcher<BuckBuildLog> reportedTargetSuccessType( final BuildTarget target, final BuildRuleSuccessType successType) { return new CustomTypeSafeMatcher<BuckBuildLog>( "target: " + target.toString() + " with result: " + successType) { @Override protected boolean matchesSafely(BuckBuildLog buckBuildLog) { return buckBuildLog.getLogEntry(target).getSuccessType().equals(Optional.of(successType)); } }; } /** Stolen from {@link PrecompiledHeaderIntegrationTest} */ private BuildTarget findPchTarget() throws IOException { for (BuildTarget target : workspace.getBuildLog().getAllTargets()) { for (Flavor flavor : target.getFlavors()) { if (flavor.getName().startsWith("pch-")) { return target; } } } fail("should have generated a pch target"); return null; } @Test public void samePchIffSameFlags() throws Exception { assumeTrue(platformOkForPCHTests()); BuildTarget pchTarget = newTarget("//test:pch"); CxxPrecompiledHeaderTemplate pch = newPCH(pchTarget); ruleResolver.addToIndex(pch); BuildTarget lib1Target = newTarget("//test:lib1"); BuildRuleParams lib1Params = newParams(lib1Target); CxxSourceRuleFactory factory1 = newFactoryBuilder(lib1Params, "-frtti") .setPrecompiledHeader(new DefaultBuildTargetSourcePath(pchTarget)) .build(); CxxPreprocessAndCompile lib1 = factory1.createPreprocessAndCompileBuildRule("lib1.cpp", newSource("lib1.cpp")); ruleResolver.addToIndex(lib1); ImmutableList<String> cmd1 = lib1.makeMainStep(pathResolver, Paths.get("/tmp/x"), false).getCommand(); BuildTarget lib2Target = newTarget("//test:lib2"); BuildRuleParams lib2Params = newParams(lib2Target); CxxSourceRuleFactory factory2 = newFactoryBuilder(lib2Params, "-frtti") .setPrecompiledHeader(new DefaultBuildTargetSourcePath(pchTarget)) .build(); CxxPreprocessAndCompile lib2 = factory2.createPreprocessAndCompileBuildRule("lib2.cpp", newSource("lib2.cpp")); ruleResolver.addToIndex(lib2); ImmutableList<String> cmd2 = lib2.makeMainStep(pathResolver, Paths.get("/tmp/x"), false).getCommand(); BuildTarget lib3Target = newTarget("//test:lib3"); BuildRuleParams lib3Params = newParams(lib3Target); CxxSourceRuleFactory factory3 = newFactoryBuilder(lib3Params, "-fno-rtti") .setPrecompiledHeader(new DefaultBuildTargetSourcePath(pchTarget)) .build(); CxxPreprocessAndCompile lib3 = factory3.createPreprocessAndCompileBuildRule("lib3.cpp", newSource("lib3.cpp")); ruleResolver.addToIndex(lib3); ImmutableList<String> cmd3 = lib3.makeMainStep(pathResolver, Paths.get("/tmp/x"), false).getCommand(); assertTrue(seek(cmd1, "-frtti").size() > 0); assertTrue(seek(cmd2, "-frtti").size() > 0); assertFalse(seek(cmd3, "-frtti").size() > 0); assertFalse(seek(cmd1, "-fno-rtti").size() > 0); assertFalse(seek(cmd2, "-fno-rtti").size() > 0); assertTrue(seek(cmd3, "-fno-rtti").size() > 0); List<String> pchFlag1 = seek(cmd1, "-include-pch"); assertTrue(pchFlag1.size() >= 2); pchFlag1 = pchFlag1.subList(0, 2); List<String> pchFlag2 = seek(cmd2, "-include-pch"); assertTrue(pchFlag2.size() >= 2); pchFlag2 = pchFlag2.subList(0, 2); List<String> pchFlag3 = seek(cmd3, "-include-pch"); assertTrue(pchFlag3.size() >= 2); pchFlag3 = pchFlag3.subList(0, 2); assertEquals(pchFlag1, pchFlag2); assertNotEquals(pchFlag2, pchFlag3); } @Test public void userRuleChangesDependencyPCHRuleFlags() throws Exception { assumeTrue(platformOkForPCHTests()); BuildTarget pchTarget = newTarget("//test:pch"); CxxPrecompiledHeaderTemplate pch = newPCH(pchTarget); ruleResolver.addToIndex(pch); BuildTarget libTarget = newTarget("//test:lib"); BuildRuleParams libParams = newParams(libTarget); CxxSourceRuleFactory factory1 = newFactoryBuilder(libParams, "-flag-for-factory") .setPrecompiledHeader(new DefaultBuildTargetSourcePath(pchTarget)) .build(); CxxPreprocessAndCompile lib = factory1.createPreprocessAndCompileBuildRule( "lib.cpp", newCxxSourceBuilder() .setPath(new FakeSourcePath("lib.cpp")) .setFlags(ImmutableList.of("-flag-for-source")) .build()); ruleResolver.addToIndex(lib); ImmutableList<String> libCmd = lib.makeMainStep(pathResolver, Paths.get("/tmp/x"), false).getCommand(); assertTrue(seek(libCmd, "-flag-for-source").size() > 0); assertTrue(seek(libCmd, "-flag-for-factory").size() > 0); CxxPrecompiledHeader pchInstance = null; for (BuildRule dep : lib.getBuildDeps()) { if (dep instanceof CxxPrecompiledHeader) { pchInstance = (CxxPrecompiledHeader) dep; } } assertNotNull(pchInstance); ImmutableList<String> pchCmd = pchInstance.makeMainStep(pathResolver, Paths.get("/tmp/x")).getCommand(); assertTrue(seek(pchCmd, "-flag-for-source").size() > 0); assertTrue(seek(pchCmd, "-flag-for-factory").size() > 0); } private static <T> void assertContains(ImmutableList<T> container, Iterable<T> items) { for (T item : items) { assertThat(container, Matchers.hasItem(item)); } } @Test public void userRuleIncludePathsChangedByPCH() throws Exception { assumeTrue(platformOkForPCHTests()); CxxPreprocessorInput cxxPreprocessorInput = CxxPreprocessorInput.builder() .addIncludes( CxxHeadersDir.of( CxxPreprocessables.IncludeType.SYSTEM, new FakeSourcePath("/tmp/sys"))) .build(); BuildTarget lib1Target = newTarget("//some/other/dir:lib1"); BuildRuleParams lib1Params = newParams(lib1Target); CxxSourceRuleFactory lib1Factory = newFactoryBuilder(lib1Params).addCxxPreprocessorInput(cxxPreprocessorInput).build(); CxxPreprocessAndCompile lib1 = lib1Factory.createPreprocessAndCompileBuildRule("lib1.cpp", newSource("lib1.cpp")); ruleResolver.addToIndex(lib1); ImmutableList<String> lib1Cmd = lib1.makeMainStep(pathResolver, Paths.get("/tmp/x"), false).getCommand(); BuildTarget pchTarget = newTarget("//test:pch"); CxxPrecompiledHeaderTemplate pch = newPCH(pchTarget, new FakeSourcePath("header.h"), ImmutableSortedSet.of(lib1)); ruleResolver.addToIndex(pch); BuildTarget lib2Target = newTarget("//test:lib2"); BuildRuleParams lib2Params = newParams(lib2Target); CxxSourceRuleFactory lib2Factory = newFactoryBuilder(lib2Params) .setPrecompiledHeader(new DefaultBuildTargetSourcePath(pchTarget)) .build(); CxxPreprocessAndCompile lib2 = lib2Factory.createPreprocessAndCompileBuildRule("lib2.cpp", newSource("lib2.cpp")); ruleResolver.addToIndex(lib2); ImmutableList<String> lib2Cmd = lib2.makeMainStep(pathResolver, Paths.get("/tmp/y"), false).getCommand(); CxxPrecompiledHeader pchInstance = null; for (BuildRule dep : lib2.getBuildDeps()) { if (dep instanceof CxxPrecompiledHeader) { pchInstance = (CxxPrecompiledHeader) dep; } } assertNotNull(pchInstance); ImmutableList<String> pchCmd = pchInstance.makeMainStep(pathResolver, Paths.get("/tmp/z")).getCommand(); // (pretend that) lib1 has a dep resulting in adding this dir to the include path flags assertContains(lib1Cmd, ImmutableList.of("-isystem", "/tmp/sys")); // PCH should inherit those flags assertContains(pchCmd, ImmutableList.of("-isystem", "/tmp/sys")); // and because PCH uses them, these should be used in lib2 which uses PCH; also, used *first* assertContains(lib2Cmd, ImmutableList.of("-isystem", "/tmp/sys")); Iterator<String> iter = lib2Cmd.iterator(); while (iter.hasNext()) { if (iter.next().equals("-isystem")) { break; } } assertTrue(iter.hasNext()); assertEquals("/tmp/sys", iter.next()); } @Test public void successfulBuildWithPchHavingNoDeps() throws Exception { assumeTrue(platformOkForPCHTests()); workspace.runBuckBuild("//basic_tests:main").assertSuccess(); } @Test public void successfulBuildWithPchHavingDeps() throws Exception { assumeTrue(platformOkForPCHTests()); workspace.runBuckBuild("//deps_test:bin").assertSuccess(); } @Test public void changingPrecompilableHeaderCausesRecompile() throws Exception { assumeTrue(platformOkForPCHTests()); BuckBuildLog buildLog; workspace.writeContentsToPath( "#define TESTVALUE 42\n", "recompile_after_header_changed/header.h"); workspace.runBuckBuild("//recompile_after_header_changed:main#default").assertSuccess(); buildLog = workspace.getBuildLog(); assertThat( buildLog, reportedTargetSuccessType(findPchTarget(), BuildRuleSuccessType.BUILT_LOCALLY)); assertThat( buildLog, reportedTargetSuccessType( workspace.newBuildTarget("//recompile_after_header_changed:main#binary,default"), BuildRuleSuccessType.BUILT_LOCALLY)); assertEquals(42, runBuiltBinary("//recompile_after_header_changed:main#default")); workspace.resetBuildLogFile(); workspace.writeContentsToPath( "#define TESTVALUE 43\n", "recompile_after_header_changed/header.h"); workspace.runBuckBuild("//recompile_after_header_changed:main#default").assertSuccess(); buildLog = workspace.getBuildLog(); assertThat( buildLog, reportedTargetSuccessType(findPchTarget(), BuildRuleSuccessType.BUILT_LOCALLY)); assertThat( buildLog, reportedTargetSuccessType( workspace.newBuildTarget("//recompile_after_header_changed:main#binary,default"), BuildRuleSuccessType.BUILT_LOCALLY)); assertEquals(43, runBuiltBinary("//recompile_after_header_changed:main#default")); } @Test public void changingHeaderIncludedByPCHPrefixHeaderCausesRecompile() throws Exception { assumeTrue(platformOkForPCHTests()); BuckBuildLog buildLog; workspace.writeContentsToPath( "#define TESTVALUE 50\n", "recompile_after_include_changed/included_by_pch.h"); workspace.runBuckBuild("//recompile_after_include_changed:main#default").assertSuccess(); buildLog = workspace.getBuildLog(); assertThat( buildLog, reportedTargetSuccessType(findPchTarget(), BuildRuleSuccessType.BUILT_LOCALLY)); assertThat( buildLog, reportedTargetSuccessType( workspace.newBuildTarget("//recompile_after_include_changed:main#binary,default"), BuildRuleSuccessType.BUILT_LOCALLY)); assertEquals( workspace .runCommand( workspace .resolve( BuildTargets.getGenPath( filesystem, workspace.newBuildTarget( "//recompile_after_include_changed:main#default"), "%s")) .toString()) .getExitCode(), 50); workspace.resetBuildLogFile(); workspace.writeContentsToPath( "#define TESTVALUE 51\n", "recompile_after_include_changed/included_by_pch.h"); workspace.runBuckBuild("//recompile_after_include_changed:main#default").assertSuccess(); buildLog = workspace.getBuildLog(); assertThat( buildLog, reportedTargetSuccessType(findPchTarget(), BuildRuleSuccessType.BUILT_LOCALLY)); assertThat( buildLog, reportedTargetSuccessType( workspace.newBuildTarget("//recompile_after_include_changed:main#binary,default"), BuildRuleSuccessType.BUILT_LOCALLY)); assertEquals( workspace .runCommand( workspace .resolve( BuildTargets.getGenPath( filesystem, workspace.newBuildTarget( "//recompile_after_include_changed:main#default"), "%s")) .toString()) .getExitCode(), 51); } private static void getAllFiles(TreeMap<Path, byte[]> out, Path dir) throws Exception { assertTrue(dir.toFile().isDirectory()); for (Path relativeEntry : Files.list(dir).collect(Collectors.toList())) { if (relativeEntry.toFile().isDirectory()) { getAllFiles(out, relativeEntry); } else { out.put(relativeEntry, Files.readAllBytes(relativeEntry)); } } } @Test public void deterministicHashesForSharedPCHs() throws Exception { assumeTrue(platformOkForPCHTests()); Sha1HashCode pchHashA = null; workspace.runBuckBuild("//determinism/a:main").assertSuccess(); BuckBuildLog buildLogA = workspace.getBuildLog(); for (BuildTarget target : buildLogA.getAllTargets()) { if (target.toString().startsWith("//determinism/lib:pch#default,pch-cxx-")) { pchHashA = buildLogA.getLogEntry(target).getRuleKey(); System.err.println("A: " + pchHashA); } } assertNotNull(pchHashA); Sha1HashCode pchHashB = null; workspace.runBuckBuild("//determinism/b:main").assertSuccess(); BuckBuildLog buildLogB = workspace.getBuildLog(); for (BuildTarget target : buildLogB.getAllTargets()) { if (target.toString().startsWith("//determinism/lib:pch#default,pch-cxx-")) { pchHashB = buildLogB.getLogEntry(target).getRuleKey(); System.err.println("B: " + pchHashB); } } assertNotNull(pchHashB); assertEquals(pchHashA, pchHashB); Sha1HashCode pchHashC = null; workspace.runBuckBuild("//determinism/c:main").assertSuccess(); BuckBuildLog buildLogC = workspace.getBuildLog(); for (BuildTarget target : buildLogC.getAllTargets()) { if (target.toString().startsWith("//determinism/lib:pch#default,pch-cxx-")) { pchHashC = buildLogC.getLogEntry(target).getRuleKey(); System.err.println("C: " + pchHashC); } } assertNotNull(pchHashC); assertNotEquals(pchHashA, pchHashC); } }