/* * Copyright 2015-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.android; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import com.facebook.buck.cli.BuckConfig; import com.facebook.buck.cli.FakeBuckConfig; import com.facebook.buck.cxx.CxxBuckConfig; import com.facebook.buck.cxx.CxxLinkableEnhancer; import com.facebook.buck.cxx.CxxPlatform; import com.facebook.buck.cxx.CxxPlatformUtils; import com.facebook.buck.cxx.CxxPreprocessAndCompile; import com.facebook.buck.cxx.CxxSource; import com.facebook.buck.cxx.CxxSourceRuleFactory; import com.facebook.buck.cxx.DebugPathSanitizer; import com.facebook.buck.cxx.Linker; import com.facebook.buck.cxx.NativeLinkableInput; import com.facebook.buck.io.AlwaysFoundExecutableFinder; import com.facebook.buck.io.MoreFiles; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.model.InternalFlavor; import com.facebook.buck.model.Pair; import com.facebook.buck.parser.NoSuchBuildTargetException; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildRuleParamsBuilder; import com.facebook.buck.rules.FakeSourcePath; import com.facebook.buck.rules.RuleKey; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.args.SourcePathArg; import com.facebook.buck.rules.keys.DefaultRuleKeyFactory; import com.facebook.buck.testutil.FakeFileHashCache; import com.facebook.buck.testutil.integration.TemporaryPaths; import com.facebook.buck.testutil.integration.TestDataHelper; import com.facebook.buck.util.environment.Platform; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.common.io.Files; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.function.Function; import org.hamcrest.Matchers; import org.junit.Assume; import org.junit.Rule; import org.junit.Test; public class NdkCxxPlatformTest { @Rule public TemporaryPaths tmp = new TemporaryPaths(); enum Operation { COMPILE, PREPROCESS_AND_COMPILE, } // Create and return some rule keys from a dummy source for the given platforms. private ImmutableMap<NdkCxxPlatforms.TargetCpuType, RuleKey> constructCompileRuleKeys( Operation operation, ImmutableMap<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> cxxPlatforms) { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); String source = "source.cpp"; DefaultRuleKeyFactory ruleKeyFactory = new DefaultRuleKeyFactory( 0, FakeFileHashCache.createFromStrings( ImmutableMap.<String, String>builder() .put("source.cpp", Strings.repeat("a", 40)) .build()), pathResolver, ruleFinder); BuildTarget target = BuildTargetFactory.newInstance("//:target"); ImmutableMap.Builder<NdkCxxPlatforms.TargetCpuType, RuleKey> ruleKeys = ImmutableMap.builder(); for (Map.Entry<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> entry : cxxPlatforms.entrySet()) { CxxSourceRuleFactory cxxSourceRuleFactory = CxxSourceRuleFactory.builder() .setParams(new FakeBuildRuleParamsBuilder(target).build()) .setResolver(resolver) .setPathResolver(pathResolver) .setRuleFinder(ruleFinder) .setCxxBuckConfig(CxxPlatformUtils.DEFAULT_CONFIG) .setCxxPlatform(entry.getValue().getCxxPlatform()) .setPicType(CxxSourceRuleFactory.PicType.PIC) .build(); CxxPreprocessAndCompile rule; switch (operation) { case PREPROCESS_AND_COMPILE: rule = cxxSourceRuleFactory.createPreprocessAndCompileBuildRule( source, CxxSource.of(CxxSource.Type.CXX, new FakeSourcePath(source), ImmutableList.of())); break; case COMPILE: rule = cxxSourceRuleFactory.createCompileBuildRule( source, CxxSource.of( CxxSource.Type.CXX_CPP_OUTPUT, new FakeSourcePath(source), ImmutableList.of())); break; default: throw new IllegalStateException(); } ruleKeys.put(entry.getKey(), ruleKeyFactory.build(rule)); } return ruleKeys.build(); } // Create and return some rule keys from a dummy source for the given platforms. private ImmutableMap<NdkCxxPlatforms.TargetCpuType, RuleKey> constructLinkRuleKeys( ImmutableMap<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> cxxPlatforms) throws NoSuchBuildTargetException { BuildRuleResolver resolver = new BuildRuleResolver(TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()); SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); SourcePathResolver pathResolver = new SourcePathResolver(ruleFinder); DefaultRuleKeyFactory ruleKeyFactory = new DefaultRuleKeyFactory( 0, FakeFileHashCache.createFromStrings( ImmutableMap.<String, String>builder() .put("input.o", Strings.repeat("a", 40)) .build()), pathResolver, ruleFinder); BuildTarget target = BuildTargetFactory.newInstance("//:target"); ImmutableMap.Builder<NdkCxxPlatforms.TargetCpuType, RuleKey> ruleKeys = ImmutableMap.builder(); for (Map.Entry<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> entry : cxxPlatforms.entrySet()) { BuildRule rule = CxxLinkableEnhancer.createCxxLinkableBuildRule( CxxPlatformUtils.DEFAULT_CONFIG, entry.getValue().getCxxPlatform(), new FakeBuildRuleParamsBuilder(target).build(), resolver, pathResolver, ruleFinder, target, Linker.LinkType.EXECUTABLE, Optional.empty(), Paths.get("output"), Linker.LinkableDepType.SHARED, /* thinLto */ false, ImmutableList.of(), Optional.empty(), Optional.empty(), ImmutableSet.of(), NativeLinkableInput.builder() .setArgs(SourcePathArg.from(new FakeSourcePath("input.o"))) .build(), Optional.empty()); ruleKeys.put(entry.getKey(), ruleKeyFactory.build(rule)); } return ruleKeys.build(); } @Test public void testNdkMajorVersion() { assertEquals(9, NdkCxxPlatforms.getNdkMajorVersion("r9")); assertEquals(9, NdkCxxPlatforms.getNdkMajorVersion("r9b")); assertEquals(10, NdkCxxPlatforms.getNdkMajorVersion("r10c")); assertEquals(10, NdkCxxPlatforms.getNdkMajorVersion("r10e")); assertEquals(11, NdkCxxPlatforms.getNdkMajorVersion("11.0.1234")); assertEquals(11, NdkCxxPlatforms.getNdkMajorVersion("11.2.2725575")); assertEquals(12, NdkCxxPlatforms.getNdkMajorVersion("12.0.1234")); assertEquals(12, NdkCxxPlatforms.getNdkMajorVersion("12.1.2977051")); } @Test public void testNdkFlags() throws IOException, InterruptedException { ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot()); Path ndkRoot = tmp.newFolder("android-ndk-r10b"); NdkCxxPlatformTargetConfiguration targetConfiguration = NdkCxxPlatforms.getTargetConfiguration( NdkCxxPlatforms.TargetCpuType.X86, NdkCxxPlatformCompiler.builder() .setType(NdkCxxPlatformCompiler.Type.GCC) .setVersion("gcc-version") .setGccVersion("clang-version") .build(), "target-app-platform"); MoreFiles.writeLinesToFile(ImmutableList.of("r9c"), ndkRoot.resolve("RELEASE.TXT")); NdkCxxPlatform platform = NdkCxxPlatforms.build( CxxPlatformUtils.DEFAULT_CONFIG, new AndroidBuckConfig(FakeBuckConfig.builder().build(), Platform.detect()), filesystem, InternalFlavor.of("android-x86"), Platform.detect(), ndkRoot, targetConfiguration, NdkCxxRuntime.GNUSTL, new AlwaysFoundExecutableFinder(), false /* strictToolchainPaths */); assertThat(platform.getCxxPlatform().getCflags(), hasItems("-std=gnu11", "-O2")); assertThat( platform.getCxxPlatform().getCxxflags(), hasItems("-std=gnu++11", "-O2", "-fno-exceptions", "-fno-rtti")); assertThat(platform.getCxxPlatform().getCppflags(), hasItems("-std=gnu11", "-O2")); } @Test public void testExtraNdkFlags() throws IOException, InterruptedException { ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot()); Path ndkRoot = tmp.newFolder("android-ndk-r10b"); NdkCxxPlatformTargetConfiguration targetConfiguration = NdkCxxPlatforms.getTargetConfiguration( NdkCxxPlatforms.TargetCpuType.X86, NdkCxxPlatformCompiler.builder() .setType(NdkCxxPlatformCompiler.Type.GCC) .setVersion("gcc-version") .setGccVersion("clang-version") .build(), "target-app-platform"); MoreFiles.writeLinesToFile(ImmutableList.of("r9c"), ndkRoot.resolve("RELEASE.TXT")); BuckConfig buckConfig = FakeBuckConfig.builder() .setSections( ImmutableMap.of( "ndk", ImmutableMap.of( "extra_cflags", "-DSOME_CFLAG -DBUCK -std=buck -Og -Wno-buck", "extra_cxxflags", "-DSOME_CXXFLAG -DBUCK -std=buck++ -Og -Wno-buck -frtti -fexceptions"), "cxx", ImmutableMap.of( "cxxflags", "-Wignored-cxx-flag", "cflags", "-Wignored-c-flag", "cppflags", "-Wignored-cpp-flag"))) .build(); NdkCxxPlatform platform = NdkCxxPlatforms.build( new CxxBuckConfig(buckConfig), new AndroidBuckConfig(buckConfig, Platform.detect()), filesystem, InternalFlavor.of("android-x86"), Platform.detect(), ndkRoot, targetConfiguration, NdkCxxRuntime.GNUSTL, new AlwaysFoundExecutableFinder(), false /* strictToolchainPaths */); // Check that we can add new flags and that we can actually override things like // warning/optimazation/etc. ImmutableList<String> cppflags = platform.getCxxPlatform().getCppflags(); assertThat( cppflags, hasItems("-std=buck", "-O2", "-Og", "-DSOME_CFLAG", "-DBUCK", "-Wno-buck")); assertThat(cppflags, not(hasItems("-DSOME_CXXFLAG"))); assertLastMatchingFlagIs(cppflags, f -> f.startsWith("-O"), "-Og"); assertLastMatchingFlagIs(cppflags, f -> f.startsWith("-std="), "-std=buck"); ImmutableList<String> cflags = platform.getCxxPlatform().getCflags(); assertThat(cflags, hasItems("-Og", "-O2", "-std=buck", "-DSOME_CFLAG", "-DBUCK", "-Wno-buck")); assertThat(cflags, not(hasItem("-DSOME_CXXFLAG"))); assertLastMatchingFlagIs(cflags, f -> f.startsWith("-O"), "-Og"); assertLastMatchingFlagIs(cflags, f -> f.startsWith("-W"), "-Wno-buck"); assertLastMatchingFlagIs(cflags, f -> f.startsWith("-std="), "-std=buck"); ImmutableList<String> cxxppflags = platform.getCxxPlatform().getCxxppflags(); assertThat(cxxppflags, hasItems("-O2", "-DSOME_CXXFLAG", "-DBUCK", "-Og", "-Wno-buck")); assertThat(cxxppflags, not(hasItem("-DSOME_CFLAG"))); assertLastMatchingFlagIs(cxxppflags, f -> f.startsWith("-O"), "-Og"); assertLastMatchingFlagIs(cxxppflags, f -> f.startsWith("-std="), "-std=buck++"); ImmutableList<String> cxxflags = platform.getCxxPlatform().getCxxflags(); assertThat( cxxflags, hasItems( "-std=gnu++11", "-O2", "-DSOME_CXXFLAG", "-DBUCK", "-Og", "-fno-exceptions", "-fno-rtti", "-Wno-buck")); assertThat(cxxflags, not(hasItem("-DSOME_CFLAG"))); assertLastMatchingFlagIs(cxxflags, f -> f.startsWith("-O"), "-Og"); assertLastMatchingFlagIs(cxxflags, f -> f.startsWith("-std="), "-std=buck++"); assertLastMatchingFlagIs(cxxflags, f -> f.equals("-frtti") || f.equals("-fno-rtti"), "-frtti"); assertLastMatchingFlagIs( cxxflags, f -> f.equals("-fexceptions") || f.equals("-fno-exceptions"), "-fexceptions"); } private void assertLastMatchingFlagIs( ImmutableList<String> flags, Function<String, Boolean> filter, String expected) { assertThat(flags, hasItems(expected)); for (String flag : flags.reverse()) { if (filter.apply(flag)) { assertThat(flag, is(expected)); return; } } } // This test is mostly just so that changes are forced to update this string if they change the // ndk platform flags so that such changes can actually be reviewed. @Test public void testExtraNdkFlagsLiterally() throws IOException, InterruptedException { Assume.assumeTrue(Platform.detect() != Platform.WINDOWS); ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot()); Path ndkRoot = tmp.newFolder("android-ndk-r10b"); NdkCxxPlatformTargetConfiguration targetConfiguration = NdkCxxPlatforms.getTargetConfiguration( NdkCxxPlatforms.TargetCpuType.X86, NdkCxxPlatformCompiler.builder() .setType(NdkCxxPlatformCompiler.Type.GCC) .setVersion("gcc-version") .setGccVersion("clang-version") .build(), "target-app-platform"); MoreFiles.writeLinesToFile(ImmutableList.of("r9c"), ndkRoot.resolve("RELEASE.TXT")); BuckConfig buckConfig = FakeBuckConfig.builder() .setSections( ImmutableMap.of( "ndk", ImmutableMap.of( "extra_cflags", "--start-extra-cflags-- -DSOME_CFLAG --end-extra-cflags--", "extra_cxxflags", "--start-extra-cxxflags-- -DSOME_CXXFLAG --end-extra-cxxflags--"), "cxx", ImmutableMap.of( "cxxflags", "-Wignored-cxx-flag", "cflags", "-Wignored-c-flag", "cppflags", "-Wignored-cpp-flag"))) .build(); NdkCxxPlatform platform = NdkCxxPlatforms.build( new CxxBuckConfig(buckConfig), new AndroidBuckConfig(buckConfig, Platform.detect()), filesystem, InternalFlavor.of("android-x86"), Platform.detect(), ndkRoot, targetConfiguration, NdkCxxRuntime.GNUSTL, new AlwaysFoundExecutableFinder(), false /* strictToolchainPaths */); Joiner joiner = Joiner.on("\n"); CxxPlatform cxxPlatform = platform.getCxxPlatform(); DebugPathSanitizer sanitizer = cxxPlatform.getCompilerDebugPathSanitizer(); Path expectedFlags = TestDataHelper.getTestDataDirectory(this).resolve("ndkcxxplatforms.flags"); String expected = Files.toString(expectedFlags.toFile(), Charsets.UTF_8); // This string is constructed to try to get intellij/phabricator to not detect moves across the // boundaries of different flag types. We could split this into multiple files, but the tradeoff // there is then that changes need to update all those files and reviewing the changes is across // multiple files. String actual = String.format( "---BEGIN CFLAGS-------\n" + "%s\n" + "---END CFLAGS---------\n" + "----------------------\n" + "======================\n" + "----------------------\n" + "---BEGIN CPPFLAGS-----\n" + "%s\n" + "---END CPPFLAGS-------\n" + "----------------------\n" + "======================\n" + "----------------------\n" + "---BEGIN CXXFLAGS-----\n" + "%s\n" + "---END CXXFLAGS-------\n" + "----------------------\n" + "======================\n" + "----------------------\n" + "---BEGIN CXXPPFLAGS---\n" + "%s\n" + "---END CXXPPFLAGS-----\n", joiner.join(sanitizer.sanitizeFlags(cxxPlatform.getCflags())), joiner.join(sanitizer.sanitizeFlags(cxxPlatform.getCppflags())), joiner.join(sanitizer.sanitizeFlags(cxxPlatform.getCxxflags())), joiner.join(sanitizer.sanitizeFlags(cxxPlatform.getCxxppflags()))); // Use assertEquals instead of assertThat because Intellij's handling of failures of // assertEquals is more user-friendly than for assertThat. assertEquals(expected, actual); } // The important aspects we check for in rule keys is that the host platform and the path // to the NDK don't cause changes. @Test public void checkRootAndPlatformDoNotAffectRuleKeys() throws Exception { ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot()); // Test all major compiler and runtime combinations. ImmutableList<Pair<NdkCxxPlatformCompiler.Type, NdkCxxRuntime>> configs = ImmutableList.of( new Pair<>(NdkCxxPlatformCompiler.Type.GCC, NdkCxxRuntime.GNUSTL), new Pair<>(NdkCxxPlatformCompiler.Type.CLANG, NdkCxxRuntime.GNUSTL), new Pair<>(NdkCxxPlatformCompiler.Type.CLANG, NdkCxxRuntime.LIBCXX)); for (Pair<NdkCxxPlatformCompiler.Type, NdkCxxRuntime> config : configs) { Map<String, ImmutableMap<NdkCxxPlatforms.TargetCpuType, RuleKey>> preprocessAndCompileRukeKeys = new HashMap<>(); Map<String, ImmutableMap<NdkCxxPlatforms.TargetCpuType, RuleKey>> compileRukeKeys = new HashMap<>(); Map<String, ImmutableMap<NdkCxxPlatforms.TargetCpuType, RuleKey>> linkRukeKeys = new HashMap<>(); // Iterate building up rule keys for combinations of different platforms and NDK root // directories. for (String dir : ImmutableList.of("android-ndk-r9c", "android-ndk-r10b")) { for (Platform platform : ImmutableList.of(Platform.LINUX, Platform.MACOS, Platform.WINDOWS)) { Path root = tmp.newFolder(dir); MoreFiles.writeLinesToFile(ImmutableList.of("r9c"), root.resolve("RELEASE.TXT")); ImmutableMap<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> platforms = NdkCxxPlatforms.getPlatforms( CxxPlatformUtils.DEFAULT_CONFIG, new AndroidBuckConfig(FakeBuckConfig.builder().build(), platform), filesystem, root, NdkCxxPlatformCompiler.builder() .setType(config.getFirst()) .setVersion("gcc-version") .setGccVersion("clang-version") .build(), NdkCxxRuntime.GNUSTL, "target-app-platform", ImmutableSet.of("x86"), platform, new AlwaysFoundExecutableFinder(), /* strictToolchainPaths */ false); preprocessAndCompileRukeKeys.put( String.format("NdkCxxPlatform(%s, %s)", dir, platform), constructCompileRuleKeys(Operation.PREPROCESS_AND_COMPILE, platforms)); compileRukeKeys.put( String.format("NdkCxxPlatform(%s, %s)", dir, platform), constructCompileRuleKeys(Operation.COMPILE, platforms)); linkRukeKeys.put( String.format("NdkCxxPlatform(%s, %s)", dir, platform), constructLinkRuleKeys(platforms)); MoreFiles.deleteRecursively(root); } } // If everything worked, we should be able to collapse all the generated rule keys down // to a singleton set. assertThat( Arrays.toString(preprocessAndCompileRukeKeys.entrySet().toArray()), Sets.newHashSet(preprocessAndCompileRukeKeys.values()), Matchers.hasSize(1)); assertThat( Arrays.toString(compileRukeKeys.entrySet().toArray()), Sets.newHashSet(compileRukeKeys.values()), Matchers.hasSize(1)); assertThat( Arrays.toString(linkRukeKeys.entrySet().toArray()), Sets.newHashSet(linkRukeKeys.values()), Matchers.hasSize(1)); } } @Test public void headerVerificationWhitelistsNdkRoot() throws InterruptedException, IOException { ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot()); String dir = "android-ndk-r9c"; Path root = tmp.newFolder(dir); MoreFiles.writeLinesToFile(ImmutableList.of("r9c"), root.resolve("RELEASE.TXT")); ImmutableMap<NdkCxxPlatforms.TargetCpuType, NdkCxxPlatform> platforms = NdkCxxPlatforms.getPlatforms( CxxPlatformUtils.DEFAULT_CONFIG, new AndroidBuckConfig(FakeBuckConfig.builder().build(), Platform.detect()), filesystem, root, NdkCxxPlatformCompiler.builder() .setType(NdkCxxPlatformCompiler.Type.GCC) .setVersion("gcc-version") .setGccVersion("clang-version") .build(), NdkCxxRuntime.GNUSTL, "target-app-platform", ImmutableSet.of("x86"), Platform.LINUX, new AlwaysFoundExecutableFinder(), /* strictToolchainPaths */ false); for (NdkCxxPlatform ndkCxxPlatform : platforms.values()) { assertTrue( ndkCxxPlatform .getCxxPlatform() .getHeaderVerification() .isWhitelisted(root.resolve("test.h").toString())); } } }