/* * 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.macho; import static com.facebook.buck.cxx.CxxFlavorSanitizer.sanitize; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.Assume.assumeTrue; import com.facebook.buck.apple.AppleDescriptions; import com.facebook.buck.apple.CodeSigning; import com.facebook.buck.cxx.DebugPathSanitizer; import com.facebook.buck.cxx.MungingDebugPathSanitizer; 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.BuildTargets; import com.facebook.buck.model.Flavor; import com.facebook.buck.model.InternalFlavor; import com.facebook.buck.testutil.TestConsole; import com.facebook.buck.testutil.integration.FakeAppleDeveloperEnvironment; 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.DefaultProcessExecutor; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.facebook.buck.util.environment.Platform; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; import java.util.Optional; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; public class ObjectPathsAbsolutifierIntegrationTest { @Rule public TemporaryPaths tmp = new TemporaryPaths(); private ProjectFilesystem filesystem; @Before public void setUp() throws InterruptedException { assumeTrue(Platform.detect() == Platform.MACOS || Platform.detect() == Platform.LINUX); filesystem = new ProjectFilesystem(tmp.getRoot()); } private DebugPathSanitizer getDebugPathSanitizer() { // this was stolen from the implementation detail of AppleCxxPlatforms return new MungingDebugPathSanitizer( 250, File.separatorChar, Paths.get("."), ImmutableBiMap.of()); } @Test public void testAbsolutifyingPathsForIntel64Bit() throws IOException, InterruptedException { Flavor platformFlavor = InternalFlavor.of("iphonesimulator-x86_64"); runAndCheckAbsolutificationWithPlatformFlavor(platformFlavor); } @Test public void testAbsolutifyingPathsForIntel32Bit() throws IOException, InterruptedException { Flavor platformFlavor = InternalFlavor.of("iphonesimulator-i386"); runAndCheckAbsolutificationWithPlatformFlavor(platformFlavor); } @Test public void testAbsolutifyingPathsForArm64Bit() throws IOException, InterruptedException { Flavor platformFlavor = InternalFlavor.of("iphoneos-arm64"); runAndCheckAbsolutificationWithPlatformFlavor(platformFlavor); } @Test public void testAbsolutifyingPathsForArm32Bit() throws IOException, InterruptedException { Flavor platformFlavor = InternalFlavor.of("iphoneos-armv7"); runAndCheckAbsolutificationWithPlatformFlavor(platformFlavor); } private void runAndCheckAbsolutificationWithPlatformFlavor(Flavor platformFlavor) throws IOException, InterruptedException { assumeTrue(Platform.detect() == Platform.MACOS); ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(this, "apple_binary_with_platform", tmp); workspace.setUp(); BuildTarget target = BuildTargetFactory.newInstance("//Apps/TestApp:TestApp") .withAppendedFlavors(platformFlavor); ProjectWorkspace.ProcessResult result = workspace.runBuckCommand( "build", "--config", "cxx.cflags=-g", target.getFullyQualifiedName()); result.assertSuccess(); Path relativeSanitizedObjectFilePath = BuildTargets.getGenPath( filesystem, target.withFlavors( platformFlavor, InternalFlavor.of("compile-" + sanitize("main.c.o"))), "%s") .resolve("main.c.o"); Path relativeSourceFilePath = Paths.get("Apps/TestApp/main.c"); Path sanitizedBinaryPath = workspace.getPath( BuildTargets.getGenPath(filesystem, target.withFlavors(platformFlavor), "%s")); Path unsanizitedBinaryPath = workspace.getPath( filesystem.getBuckPaths().getScratchDir().resolve(sanitizedBinaryPath.getFileName())); // this was stolen from the implementation detail of AppleCxxPlatforms DebugPathSanitizer sanitizer = getDebugPathSanitizer(); String oldCompDirValue = sanitizer.getCompilationDirectory(); String newCompDirValue = workspace.getDestPath().toString(); result = workspace.runBuckCommand( "machoutils", "absolutify_object_paths", "--binary", sanitizedBinaryPath.toString(), "--output", unsanizitedBinaryPath.toString(), "--old_compdir", oldCompDirValue, "--new_compdir", newCompDirValue); result.assertSuccess(); ProcessExecutor.Result sanitizedResult = workspace.runCommand("nm", "-a", sanitizedBinaryPath.toString()); ProcessExecutor.Result unsanitizedResult = workspace.runCommand("nm", "-a", unsanizitedBinaryPath.toString()); String sanitizedOutput = sanitizedResult.getStdout().orElse(""); String unsanitizedOutput = unsanitizedResult.getStdout().orElse(""); // check that weird buck comp dir is not present anymore assertThat( sanitizedOutput, containsString("SO .///////////////////////////////////////////////////////////////")); assertThat( unsanitizedOutput, not(containsString("SO .///////////////////////////////////////////////////////////////"))); // check that relative path to object file is not present anymore assertThat(sanitizedOutput, containsString("OSO ./buck-out")); assertThat(unsanitizedOutput, not(containsString("OSO ./buck-out"))); // check that relative path to source file is not present anymore assertThat(sanitizedOutput, containsString("SOL Apps/TestApp/main.c")); assertThat(unsanitizedOutput, not(containsString("SOL Apps/TestApp/main.c"))); // check that absolute path to source file is correct assertThat( unsanitizedOutput, containsString("SOL " + newCompDirValue + "/Apps/TestApp/main.c")); // check that absolute path to object file is correct assertThat( unsanitizedOutput, containsString( "OSO " + newCompDirValue + "/buck-out/bin/" + relativeSanitizedObjectFilePath.toString())); assertThat( unsanitizedOutput, containsString( "SO " + newCompDirValue + "/" + relativeSourceFilePath.getParent().toString())); assertThat( unsanitizedOutput, containsString("SOL " + newCompDirValue + "/" + relativeSourceFilePath.toString())); } private boolean checkCodeSigning(Path absoluteBundlePath) throws IOException, InterruptedException { if (!Files.exists(absoluteBundlePath)) { throw new NoSuchFileException(absoluteBundlePath.toString()); } return CodeSigning.hasValidSignature( new DefaultProcessExecutor(new TestConsole()), absoluteBundlePath); } private boolean checkCodeSignatureMatchesBetweenFiles(Path file1, Path file2) throws IOException, InterruptedException { if (!Files.exists(file1)) { throw new NoSuchFileException(file1.toString()); } if (!Files.exists(file2)) { throw new NoSuchFileException(file2.toString()); } ProcessExecutor processExecutor = new DefaultProcessExecutor(new TestConsole()); ProcessExecutor.Result result1 = processExecutor.launchAndExecute( ProcessExecutorParams.builder() .setCommand(ImmutableList.of("codesign", "-vvvv", "-d", file1.toString())) .build(), EnumSet.of(ProcessExecutor.Option.EXPECTING_STD_OUT, ProcessExecutor.Option.IS_SILENT), /* stdin */ Optional.empty(), /* timeOutMs */ Optional.empty(), /* timeOutHandler */ Optional.empty()); ProcessExecutor.Result result2 = processExecutor.launchAndExecute( ProcessExecutorParams.builder() .setCommand(ImmutableList.of("codesign", "-vvvv", "-d", file1.toString())) .build(), EnumSet.of(ProcessExecutor.Option.EXPECTING_STD_OUT, ProcessExecutor.Option.IS_SILENT), /* stdin */ Optional.empty(), /* timeOutMs */ Optional.empty(), /* timeOutHandler */ Optional.empty()); String stderr1 = result1.getStderr().orElse(""); String stderr2 = result2.getStderr().orElse(""); // skip first line as it has a path to the binary Assert.assertThat(stderr1, startsWith("Executable=")); Assert.assertThat(stderr2, startsWith("Executable=")); stderr1 = stderr1.substring(stderr1.indexOf("\n")); stderr2 = stderr2.substring(stderr2.indexOf("\n")); return stderr1.equals(stderr2); } @Test public void testAbsolutifyingPathsPreservesCodeSignature() throws Exception { assumeTrue(Platform.detect() == Platform.MACOS); assumeTrue(FakeAppleDeveloperEnvironment.supportsCodeSigning()); ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario( this, "simple_application_bundle_with_codesigning", tmp); workspace.setUp(); BuildTarget target = workspace.newBuildTarget("//:DemoApp#iphoneos-arm64,dwarf"); workspace .runBuckCommand("build", "--config", "cxx.cflags=-g", target.getFullyQualifiedName()) .assertSuccess(); workspace.verify( Paths.get("DemoApp_output.expected"), BuildTargets.getGenPath( filesystem, BuildTarget.builder(target) .addFlavors(AppleDescriptions.NO_INCLUDE_FRAMEWORKS_FLAVOR) .build(), "%s")); Path appPath = workspace.getPath( BuildTargets.getGenPath( filesystem, BuildTarget.builder(target) .addFlavors(AppleDescriptions.NO_INCLUDE_FRAMEWORKS_FLAVOR) .build(), "%s") .resolve(target.getShortName() + ".app")); Path sanitizedBinaryPath = appPath.resolve(target.getShortName()); assertThat(Files.exists(sanitizedBinaryPath), equalTo(true)); assertThat(checkCodeSigning(sanitizedBinaryPath), equalTo(true)); assertThat(checkCodeSigning(appPath), equalTo(true)); Path unsanizitedBinaryPath = workspace.getPath( filesystem .getBuckPaths() .getTmpDir() .resolve(sanitizedBinaryPath.getParent().getFileName()) .resolve(sanitizedBinaryPath.getFileName())); filesystem.mkdirs(unsanizitedBinaryPath.getParent()); // copy bundle MoreFiles.copyRecursively(sanitizedBinaryPath.getParent(), unsanizitedBinaryPath.getParent()); DebugPathSanitizer sanitizer = getDebugPathSanitizer(); String oldCompDirValue = sanitizer.getCompilationDirectory(); String newCompDirValue = workspace.getDestPath().toString(); ProjectWorkspace.ProcessResult result = workspace.runBuckCommand( "machoutils", "absolutify_object_paths", "--binary", sanitizedBinaryPath.toString(), "--output", unsanizitedBinaryPath.toString(), "--old_compdir", oldCompDirValue, "--new_compdir", newCompDirValue); result.assertSuccess(); assertThat(Files.exists(unsanizitedBinaryPath), equalTo(true)); // of course signature should be broken assertThat(checkCodeSigning(unsanizitedBinaryPath), equalTo(false)); // but it should stay unchanged from what is used to be assertThat( checkCodeSignatureMatchesBetweenFiles(unsanizitedBinaryPath, sanitizedBinaryPath), equalTo(true)); } }