/* * Copyright 2014-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.apple.project_generator; import static com.facebook.buck.apple.project_generator.ProjectGeneratorTestUtils.assertHasSingletonFrameworksPhaseWithFrameworkEntries; import static com.facebook.buck.apple.project_generator.ProjectGeneratorTestUtils.assertHasSingletonPhaseWithEntries; import static com.facebook.buck.apple.project_generator.ProjectGeneratorTestUtils.assertTargetExistsAndReturnTarget; import static com.facebook.buck.apple.project_generator.ProjectGeneratorTestUtils.getSingletonPhaseByType; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsEmptyIterable.emptyIterable; import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import com.facebook.buck.apple.AppleAssetCatalogDescriptionArg; import com.facebook.buck.apple.AppleResourceDescriptionArg; import com.facebook.buck.apple.XcodePostbuildScriptBuilder; import com.facebook.buck.apple.XcodePrebuildScriptBuilder; import com.facebook.buck.apple.xcode.xcodeproj.CopyFilePhaseDestinationSpec; import com.facebook.buck.apple.xcode.xcodeproj.PBXBuildPhase; import com.facebook.buck.apple.xcode.xcodeproj.PBXCopyFilesBuildPhase; import com.facebook.buck.apple.xcode.xcodeproj.PBXFileReference; import com.facebook.buck.apple.xcode.xcodeproj.PBXGroup; import com.facebook.buck.apple.xcode.xcodeproj.PBXNativeTarget; import com.facebook.buck.apple.xcode.xcodeproj.PBXProject; import com.facebook.buck.apple.xcode.xcodeproj.PBXReference; import com.facebook.buck.apple.xcode.xcodeproj.PBXResourcesBuildPhase; import com.facebook.buck.apple.xcode.xcodeproj.PBXShellScriptBuildPhase; import com.facebook.buck.apple.xcode.xcodeproj.ProductType; import com.facebook.buck.apple.xcode.xcodeproj.SourceTreePath; import com.facebook.buck.cli.FakeBuckConfig; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.js.IosReactNativeLibraryBuilder; import com.facebook.buck.js.ReactNativeBuckConfig; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.parser.NoSuchBuildTargetException; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeSourcePath; import com.facebook.buck.rules.PathSourcePath; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.SourceWithFlags; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.rules.coercer.FrameworkPath; import com.facebook.buck.testutil.AllExistingProjectFilesystem; import com.facebook.buck.util.environment.Platform; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import java.nio.file.Paths; import java.util.List; import java.util.Optional; import org.junit.Before; import org.junit.Test; public class NewNativeTargetProjectMutatorTest { private PBXProject generatedProject; private PathRelativizer pathRelativizer; private SourcePathResolver sourcePathResolver; @Before public void setUp() { assumeTrue(Platform.detect() == Platform.MACOS || Platform.detect() == Platform.LINUX); generatedProject = new PBXProject("TestProject"); sourcePathResolver = new SourcePathResolver( new SourcePathRuleFinder( new BuildRuleResolver( TargetGraph.EMPTY, new DefaultTargetNodeToBuildRuleTransformer()))); pathRelativizer = new PathRelativizer(Paths.get("_output"), sourcePathResolver::getRelativePath); } @Test public void shouldCreateTargetAndTargetGroup() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = new NewNativeTargetProjectMutator(pathRelativizer, sourcePathResolver::getRelativePath); mutator .setTargetName("TestTarget") .setProduct(ProductType.BUNDLE, "TestTargetProduct", Paths.get("TestTargetProduct.bundle")) .buildTargetAndAddToProject(generatedProject, true); assertTargetExistsAndReturnTarget(generatedProject, "TestTarget"); assertHasTargetGroupWithName(generatedProject, "TestTarget"); } @Test public void shouldCreateTargetAndCustomTargetGroup() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = new NewNativeTargetProjectMutator(pathRelativizer, sourcePathResolver::getRelativePath); mutator .setTargetName("TestTarget") .setTargetGroupPath(ImmutableList.of("Grandparent", "Parent")) .setProduct(ProductType.BUNDLE, "TestTargetProduct", Paths.get("TestTargetProduct.bundle")) .buildTargetAndAddToProject(generatedProject, true); assertTargetExistsAndReturnTarget(generatedProject, "TestTarget"); PBXGroup grandparentGroup = assertHasSubgroupAndReturnIt(generatedProject.getMainGroup(), "Grandparent"); assertHasSubgroupAndReturnIt(grandparentGroup, "Parent"); } @Test public void shouldCreateTargetAndNoGroup() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = new NewNativeTargetProjectMutator(pathRelativizer, sourcePathResolver::getRelativePath); NewNativeTargetProjectMutator.Result result = mutator .setTargetName("TestTarget") .setTargetGroupPath(ImmutableList.of("Grandparent", "Parent")) .setProduct( ProductType.BUNDLE, "TestTargetProduct", Paths.get("TestTargetProduct.bundle")) .buildTargetAndAddToProject(generatedProject, false); assertFalse(result.targetGroup.isPresent()); } @Test public void testSourceGroups() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); SourcePath foo = new FakeSourcePath("Group1/foo.m"); SourcePath bar = new FakeSourcePath("Group1/bar.m"); SourcePath baz = new FakeSourcePath("Group2/baz.m"); mutator.setSourcesWithFlags( ImmutableSet.of( SourceWithFlags.of(foo), SourceWithFlags.of(bar, ImmutableList.of("-Wall")), SourceWithFlags.of(baz))); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXGroup sourcesGroup = result.targetGroup.get().getOrCreateChildGroupByName("Sources"); PBXGroup group1 = (PBXGroup) Iterables.get(sourcesGroup.getChildren(), 0); assertEquals("Group1", group1.getName()); assertThat(group1.getChildren(), hasSize(2)); PBXFileReference fileRefBar = (PBXFileReference) Iterables.get(group1.getChildren(), 0); assertEquals("bar.m", fileRefBar.getName()); PBXFileReference fileRefFoo = (PBXFileReference) Iterables.get(group1.getChildren(), 1); assertEquals("foo.m", fileRefFoo.getName()); PBXGroup group2 = (PBXGroup) Iterables.get(sourcesGroup.getChildren(), 1); assertEquals("Group2", group2.getName()); assertThat(group2.getChildren(), hasSize(1)); PBXFileReference fileRefBaz = (PBXFileReference) Iterables.get(group2.getChildren(), 0); assertEquals("baz.m", fileRefBaz.getName()); } @Test public void testLibraryHeaderGroups() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); SourcePath foo = new FakeSourcePath("HeaderGroup1/foo.h"); SourcePath bar = new FakeSourcePath("HeaderGroup1/bar.h"); SourcePath baz = new FakeSourcePath("HeaderGroup2/baz.h"); mutator.setPublicHeaders(ImmutableSet.of(bar, baz)); mutator.setPrivateHeaders(ImmutableSet.of(foo)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXGroup sourcesGroup = result.targetGroup.get().getOrCreateChildGroupByName("Sources"); assertThat(sourcesGroup.getChildren(), hasSize(2)); PBXGroup group1 = (PBXGroup) Iterables.get(sourcesGroup.getChildren(), 0); assertEquals("HeaderGroup1", group1.getName()); assertThat(group1.getChildren(), hasSize(2)); PBXFileReference fileRefBar = (PBXFileReference) Iterables.get(group1.getChildren(), 0); assertEquals("bar.h", fileRefBar.getName()); PBXFileReference fileRefFoo = (PBXFileReference) Iterables.get(group1.getChildren(), 1); assertEquals("foo.h", fileRefFoo.getName()); PBXGroup group2 = (PBXGroup) Iterables.get(sourcesGroup.getChildren(), 1); assertEquals("HeaderGroup2", group2.getName()); assertThat(group2.getChildren(), hasSize(1)); PBXFileReference fileRefBaz = (PBXFileReference) Iterables.get(group2.getChildren(), 0); assertEquals("baz.h", fileRefBaz.getName()); } @Test public void testPrefixHeaderInSourceGroup() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); SourcePath prefixHeader = new FakeSourcePath("Group1/prefix.pch"); mutator.setPrefixHeader(Optional.of(prefixHeader)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); // No matter where the prefixHeader file is it should always be directly inside Sources PBXGroup sourcesGroup = result.targetGroup.get().getOrCreateChildGroupByName("Sources"); assertThat(sourcesGroup.getChildren(), hasSize(1)); PBXFileReference fileRef = (PBXFileReference) Iterables.get(sourcesGroup.getChildren(), 0); assertEquals("prefix.pch", fileRef.getName()); } @Test public void testFrameworkBuildPhase() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); mutator.setFrameworks( ImmutableSet.of( FrameworkPath.ofSourceTreePath( new SourceTreePath( PBXReference.SourceTree.SDKROOT, Paths.get("Foo.framework"), Optional.empty())))); mutator.setArchives( ImmutableSet.of( new PBXFileReference( "libdep.a", "libdep.a", PBXReference.SourceTree.BUILT_PRODUCTS_DIR, Optional.empty()))); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); assertHasSingletonFrameworksPhaseWithFrameworkEntries( result.target, ImmutableList.of("$SDKROOT/Foo.framework", "$BUILT_PRODUCTS_DIR/libdep.a")); } @Test public void testResourcesBuildPhase() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); AppleResourceDescriptionArg arg = AppleResourceDescriptionArg.builder() .setName("resources") .setFiles(ImmutableSet.of(new FakeSourcePath("foo.png"))) .build(); mutator.setRecursiveResources(ImmutableSet.of(arg)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); assertHasSingletonPhaseWithEntries( result.target, PBXResourcesBuildPhase.class, ImmutableList.of("$SOURCE_ROOT/../foo.png")); } @Test public void testCopyFilesBuildPhase() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); CopyFilePhaseDestinationSpec.Builder specBuilder = CopyFilePhaseDestinationSpec.builder(); specBuilder.setDestination(PBXCopyFilesBuildPhase.Destination.FRAMEWORKS); specBuilder.setPath("foo.png"); PBXBuildPhase copyPhase = new PBXCopyFilesBuildPhase(specBuilder.build()); mutator.setCopyFilesPhases(ImmutableList.of(copyPhase)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXBuildPhase buildPhaseToTest = getSingletonPhaseByType(result.target, PBXCopyFilesBuildPhase.class); assertThat(copyPhase, equalTo(buildPhaseToTest)); } @Test public void testCopyFilesBuildPhaseIsBeforePostBuildScriptBuildPhase() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); CopyFilePhaseDestinationSpec.Builder specBuilder = CopyFilePhaseDestinationSpec.builder(); specBuilder.setDestination(PBXCopyFilesBuildPhase.Destination.FRAMEWORKS); specBuilder.setPath("script/input.png"); PBXBuildPhase copyFilesPhase = new PBXCopyFilesBuildPhase(specBuilder.build()); mutator.setCopyFilesPhases(ImmutableList.of(copyFilesPhase)); TargetNode<?, ?> postbuildNode = XcodePostbuildScriptBuilder.createBuilder(BuildTargetFactory.newInstance("//foo:script")) .setCmd("echo \"hello world!\"") .build(); mutator.setPostBuildRunScriptPhasesFromTargetNodes(ImmutableList.of(postbuildNode)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXNativeTarget target = result.target; List<PBXBuildPhase> buildPhases = target.getBuildPhases(); PBXBuildPhase copyBuildPhaseToTest = getSingletonPhaseByType(target, PBXCopyFilesBuildPhase.class); PBXBuildPhase postBuildScriptPhase = getSingletonPhaseByType(target, PBXShellScriptBuildPhase.class); assertThat( buildPhases.indexOf(copyBuildPhaseToTest), lessThan(buildPhases.indexOf(postBuildScriptPhase))); } @Test public void assetCatalogsBuildPhaseBuildsAssetCatalogs() throws NoSuchBuildTargetException { AppleAssetCatalogDescriptionArg arg = AppleAssetCatalogDescriptionArg.builder() .setName("some_rule") .setDirs(ImmutableSortedSet.of(new FakeSourcePath("AssetCatalog1.xcassets"))) .build(); NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); mutator.setRecursiveAssetCatalogs(ImmutableSet.of(arg)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); assertHasSingletonPhaseWithEntries( result.target, PBXResourcesBuildPhase.class, ImmutableList.of("$SOURCE_ROOT/../AssetCatalog1.xcassets")); } @Test public void testScriptBuildPhase() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); TargetNode<?, ?> prebuildNode = XcodePrebuildScriptBuilder.createBuilder(BuildTargetFactory.newInstance("//foo:script")) .setSrcs(ImmutableSortedSet.of(new FakeSourcePath("script/input.png"))) .setOutputs(ImmutableSortedSet.of("helloworld.txt")) .setCmd("echo \"hello world!\"") .build(); mutator.setPostBuildRunScriptPhasesFromTargetNodes(ImmutableList.of(prebuildNode)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXShellScriptBuildPhase phase = getSingletonPhaseByType(result.target, PBXShellScriptBuildPhase.class); assertThat( "Should set input paths correctly", "../script/input.png", is(equalTo(Iterables.getOnlyElement(phase.getInputPaths())))); assertThat( "Should set output paths correctly", "helloworld.txt", is(equalTo(Iterables.getOnlyElement(phase.getOutputPaths())))); assertEquals("should set script correctly", "echo \"hello world!\"", phase.getShellScript()); } @Test public void testScriptBuildPhaseWithReactNative() throws NoSuchBuildTargetException { NewNativeTargetProjectMutator mutator = mutatorWithCommonDefaults(); BuildTarget depBuildTarget = BuildTargetFactory.newInstance("//foo:dep"); ProjectFilesystem filesystem = new AllExistingProjectFilesystem(); ReactNativeBuckConfig buckConfig = new ReactNativeBuckConfig( FakeBuckConfig.builder() .setSections( ImmutableMap.of( "react-native", ImmutableMap.of("packager_worker", "react-native/packager.sh"))) .setFilesystem(filesystem) .build()); TargetNode<?, ?> reactNativeNode = IosReactNativeLibraryBuilder.builder(depBuildTarget, buckConfig) .setBundleName("Apps/Foo/FooBundle.js") .setEntryPath(new PathSourcePath(filesystem, Paths.get("js/FooApp.js"))) .build(); mutator.setPostBuildRunScriptPhasesFromTargetNodes(ImmutableList.of(reactNativeNode)); NewNativeTargetProjectMutator.Result result = mutator.buildTargetAndAddToProject(generatedProject, true); PBXShellScriptBuildPhase phase = getSingletonPhaseByType(result.target, PBXShellScriptBuildPhase.class); String shellScript = phase.getShellScript(); assertThat( shellScript, startsWith( "BASE_DIR=${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\n" + "JS_OUT=${BASE_DIR}/Apps/Foo/FooBundle.js\n" + "SOURCE_MAP=${TEMP_DIR}/rn_source_map/Apps/Foo/FooBundle.js.map\n")); } private NewNativeTargetProjectMutator mutatorWithCommonDefaults() { NewNativeTargetProjectMutator mutator = new NewNativeTargetProjectMutator(pathRelativizer, sourcePathResolver::getRelativePath); mutator .setTargetName("TestTarget") .setProduct(ProductType.BUNDLE, "TestTargetProduct", Paths.get("TestTargetProduct.bundle")); return mutator; } private static void assertHasTargetGroupWithName(PBXProject project, final String name) { assertThat( "Should contain a target group named: " + name, Iterables.filter( project.getMainGroup().getChildren(), input -> input.getName().equals(name)), not(emptyIterable())); } private static PBXGroup assertHasSubgroupAndReturnIt(PBXGroup group, final String subgroupName) { ImmutableList<PBXGroup> candidates = FluentIterable.from(group.getChildren()) .filter(input -> input.getName().equals(subgroupName)) .filter(PBXGroup.class) .toList(); if (candidates.size() != 1) { fail("Could not find a unique subgroup by its name"); } return candidates.get(0); } }