/* * 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; import com.dd.plist.NSDictionary; import com.dd.plist.NSObject; import com.dd.plist.NSString; import com.dd.plist.PropertyListFormatException; import com.dd.plist.PropertyListParser; import com.facebook.buck.cli.BuckConfig; import com.facebook.buck.cxx.BsdArchiver; import com.facebook.buck.cxx.CompilerProvider; import com.facebook.buck.cxx.CxxBuckConfig; import com.facebook.buck.cxx.CxxPlatform; import com.facebook.buck.cxx.CxxPlatforms; import com.facebook.buck.cxx.CxxToolProvider; import com.facebook.buck.cxx.DebugPathSanitizer; import com.facebook.buck.cxx.DefaultLinkerProvider; import com.facebook.buck.cxx.HeaderVerification; import com.facebook.buck.cxx.LinkerProvider; import com.facebook.buck.cxx.Linkers; import com.facebook.buck.cxx.MungingDebugPathSanitizer; import com.facebook.buck.cxx.PosixNmSymbolNameTool; import com.facebook.buck.cxx.PrefixMapDebugPathSanitizer; import com.facebook.buck.cxx.PreprocessorProvider; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.log.Logger; import com.facebook.buck.model.Flavor; import com.facebook.buck.model.UserFlavor; import com.facebook.buck.rules.ConstantToolProvider; import com.facebook.buck.rules.Tool; import com.facebook.buck.rules.VersionedTool; import com.facebook.buck.swift.SwiftBuckConfig; import com.facebook.buck.swift.SwiftPlatform; import com.facebook.buck.swift.SwiftPlatforms; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.Optionals; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.environment.Platform; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.ParseException; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; /** * Utility class to create Objective-C/C/C++/Objective-C++ platforms to support building iOS and Mac * OS X products with Xcode. */ public class AppleCxxPlatforms { private static final Logger LOG = Logger.get(AppleCxxPlatforms.class); // Utility class, do not instantiate. private AppleCxxPlatforms() {} private static final String USR_BIN = "usr/bin"; public static ImmutableList<AppleCxxPlatform> buildAppleCxxPlatforms( ProjectFilesystem filesystem, BuckConfig buckConfig, SwiftBuckConfig swiftBuckConfig, ProcessExecutor processExecutor) throws IOException { AppleConfig appleConfig = buckConfig.getView(AppleConfig.class); Supplier<Optional<Path>> appleDeveloperDirectorySupplier = appleConfig.getAppleDeveloperDirectorySupplier(processExecutor); Optional<Path> appleDeveloperDirectory = appleDeveloperDirectorySupplier.get(); if (appleDeveloperDirectory.isPresent() && !Files.isDirectory(appleDeveloperDirectory.get())) { LOG.error( "Developer directory is set to %s, but is not a directory", appleDeveloperDirectory.get()); return ImmutableList.of(); } ImmutableList.Builder<AppleCxxPlatform> appleCxxPlatformsBuilder = ImmutableList.builder(); ImmutableMap<String, AppleToolchain> toolchains = AppleToolchainDiscovery.discoverAppleToolchains( appleDeveloperDirectory, appleConfig.getExtraToolchainPaths()); ImmutableMap<AppleSdk, AppleSdkPaths> sdkPaths = AppleSdkDiscovery.discoverAppleSdkPaths( appleDeveloperDirectory, appleConfig.getExtraPlatformPaths(), toolchains, appleConfig); Optional<String> swiftVersion = swiftBuckConfig.getVersion(); Optional<AppleToolchain> swiftToolChain; if (swiftVersion.isPresent()) { Optional<String> swiftToolChainName = swiftVersion.map(AppleCxxPlatform.SWIFT_VERSION_TO_TOOLCHAIN_IDENTIFIER); swiftToolChain = toolchains .values() .stream() .filter(input -> input.getIdentifier().equals(swiftToolChainName.get())) .findFirst(); } else { swiftToolChain = Optional.empty(); } XcodeToolFinder xcodeToolFinder = new XcodeToolFinder(); XcodeBuildVersionCache xcodeBuildVersionCache = new XcodeBuildVersionCache(); sdkPaths.forEach( (sdk, appleSdkPaths) -> { String targetSdkVersion = appleConfig.getTargetSdkVersion(sdk.getApplePlatform()).orElse(sdk.getVersion()); LOG.debug("SDK %s using default version %s", sdk, targetSdkVersion); for (String architecture : sdk.getArchitectures()) { appleCxxPlatformsBuilder.add( buildWithExecutableChecker( filesystem, sdk, targetSdkVersion, architecture, appleSdkPaths, buckConfig, xcodeToolFinder, xcodeBuildVersionCache, swiftToolChain)); } }); return appleCxxPlatformsBuilder.build(); } @VisibleForTesting static AppleCxxPlatform buildWithExecutableChecker( ProjectFilesystem filesystem, AppleSdk targetSdk, String minVersion, String targetArchitecture, final AppleSdkPaths sdkPaths, BuckConfig buckConfig, XcodeToolFinder xcodeToolFinder, XcodeBuildVersionCache xcodeBuildVersionCache, Optional<AppleToolchain> swiftToolChain) { AppleCxxPlatform.Builder platformBuilder = AppleCxxPlatform.builder(); ImmutableList.Builder<Path> toolSearchPathsBuilder = ImmutableList.builder(); // Search for tools from most specific to least specific. toolSearchPathsBuilder .add(sdkPaths.getSdkPath().resolve(USR_BIN)) .add(sdkPaths.getSdkPath().resolve("Developer").resolve(USR_BIN)) .add(sdkPaths.getPlatformPath().resolve("Developer").resolve(USR_BIN)); for (Path toolchainPath : sdkPaths.getToolchainPaths()) { toolSearchPathsBuilder.add(toolchainPath.resolve(USR_BIN)); } if (sdkPaths.getDeveloperPath().isPresent()) { toolSearchPathsBuilder.add(sdkPaths.getDeveloperPath().get().resolve(USR_BIN)); toolSearchPathsBuilder.add(sdkPaths.getDeveloperPath().get().resolve("Tools")); } // TODO(beng): Add more and better cflags. ImmutableList.Builder<String> cflagsBuilder = ImmutableList.builder(); cflagsBuilder.add("-isysroot", sdkPaths.getSdkPath().toString()); cflagsBuilder.add("-iquote", filesystem.getRootPath().toString()); cflagsBuilder.add("-arch", targetArchitecture); cflagsBuilder.add(targetSdk.getApplePlatform().getMinVersionFlagPrefix() + minVersion); if (targetSdk.getApplePlatform().equals(ApplePlatform.WATCHOS)) { cflagsBuilder.add("-fembed-bitcode"); } ImmutableList.Builder<String> ldflagsBuilder = ImmutableList.builder(); ldflagsBuilder.addAll(Linkers.iXlinker("-sdk_version", targetSdk.getVersion(), "-ObjC")); if (targetSdk.getApplePlatform().equals(ApplePlatform.WATCHOS)) { ldflagsBuilder.addAll( Linkers.iXlinker("-bitcode_verify", "-bitcode_hide_symbols", "-bitcode_symbol_map")); } // Populate Xcode version keys from Xcode's own Info.plist if available. Optional<String> xcodeBuildVersion = Optional.empty(); Optional<Path> developerPath = sdkPaths.getDeveloperPath(); if (developerPath.isPresent()) { Path xcodeBundlePath = developerPath.get().getParent(); if (xcodeBundlePath != null) { Path xcodeInfoPlistPath = xcodeBundlePath.resolve("Info.plist"); try (InputStream stream = Files.newInputStream(xcodeInfoPlistPath)) { NSDictionary parsedXcodeInfoPlist = (NSDictionary) PropertyListParser.parse(stream); NSObject xcodeVersionObject = parsedXcodeInfoPlist.objectForKey("DTXcode"); if (xcodeVersionObject != null) { Optional<String> xcodeVersion = Optional.of(xcodeVersionObject.toString()); platformBuilder.setXcodeVersion(xcodeVersion); } } catch (IOException e) { LOG.warn( "Error reading Xcode's info plist %s; ignoring Xcode versions", xcodeInfoPlistPath); } catch (PropertyListFormatException | ParseException | ParserConfigurationException | SAXException e) { LOG.warn("Error in parsing %s; ignoring Xcode versions", xcodeInfoPlistPath); } } xcodeBuildVersion = xcodeBuildVersionCache.lookup(developerPath.get()); platformBuilder.setXcodeBuildVersion(xcodeBuildVersion); LOG.debug("Xcode build version is: " + xcodeBuildVersion.orElse("<absent>")); } ImmutableList.Builder<String> versions = ImmutableList.builder(); versions.add(targetSdk.getVersion()); ImmutableList<String> toolchainVersions = targetSdk .getToolchains() .stream() .map(AppleToolchain::getVersion) .flatMap(Optionals::toStream) .collect(MoreCollectors.toImmutableList()); if (toolchainVersions.isEmpty()) { if (!xcodeBuildVersion.isPresent()) { throw new HumanReadableException("Failed to read toolchain versions and Xcode version."); } versions.add(xcodeBuildVersion.get()); } else { versions.addAll(toolchainVersions); } String version = Joiner.on(':').join(versions.build()); ImmutableList<Path> toolSearchPaths = toolSearchPathsBuilder.build(); Tool clangPath = VersionedTool.of( getToolPath("clang", toolSearchPaths, xcodeToolFinder), "apple-clang", version); Tool clangXxPath = VersionedTool.of( getToolPath("clang++", toolSearchPaths, xcodeToolFinder), "apple-clang++", version); Tool ar = VersionedTool.of(getToolPath("ar", toolSearchPaths, xcodeToolFinder), "apple-ar", version); Tool ranlib = VersionedTool.builder() .setPath(getToolPath("ranlib", toolSearchPaths, xcodeToolFinder)) .setName("apple-ranlib") .setVersion(version) .build(); Tool strip = VersionedTool.of( getToolPath("strip", toolSearchPaths, xcodeToolFinder), "apple-strip", version); Tool nm = VersionedTool.of(getToolPath("nm", toolSearchPaths, xcodeToolFinder), "apple-nm", version); Tool actool = VersionedTool.of( getToolPath("actool", toolSearchPaths, xcodeToolFinder), "apple-actool", version); Tool ibtool = VersionedTool.of( getToolPath("ibtool", toolSearchPaths, xcodeToolFinder), "apple-ibtool", version); Tool momc = VersionedTool.of( getToolPath("momc", toolSearchPaths, xcodeToolFinder), "apple-momc", version); Tool xctest = VersionedTool.of( getToolPath("xctest", toolSearchPaths, xcodeToolFinder), "apple-xctest", version); Tool dsymutil = VersionedTool.of( getToolPath("dsymutil", toolSearchPaths, xcodeToolFinder), "apple-dsymutil", version); Tool lipo = VersionedTool.of( getToolPath("lipo", toolSearchPaths, xcodeToolFinder), "apple-lipo", version); Tool lldb = VersionedTool.of(getToolPath("lldb", toolSearchPaths, xcodeToolFinder), "lldb", version); Optional<Path> stubBinaryPath = targetSdk .getApplePlatform() .getStubBinaryPath() .map(input -> sdkPaths.getSdkPath().resolve(input)); CxxBuckConfig config = new CxxBuckConfig(buckConfig); UserFlavor targetFlavor = UserFlavor.of( Flavor.replaceInvalidCharacters(targetSdk.getName() + "-" + targetArchitecture), String.format("SDK: %s, architecture: %s", targetSdk.getName(), targetArchitecture)); ImmutableBiMap.Builder<Path, Path> sanitizerPaths = ImmutableBiMap.builder(); sanitizerPaths.put(sdkPaths.getSdkPath(), Paths.get("APPLE_SDKROOT")); sanitizerPaths.put(sdkPaths.getPlatformPath(), Paths.get("APPLE_PLATFORM_DIR")); if (sdkPaths.getDeveloperPath().isPresent()) { sanitizerPaths.put(sdkPaths.getDeveloperPath().get(), Paths.get("APPLE_DEVELOPER_DIR")); } DebugPathSanitizer compilerDebugPathSanitizer = new PrefixMapDebugPathSanitizer( config.getDebugPathSanitizerLimit(), File.separatorChar, Paths.get("."), sanitizerPaths.build(), filesystem.getRootPath().toAbsolutePath(), CxxToolProvider.Type.CLANG, filesystem); DebugPathSanitizer assemblerDebugPathSanitizer = new MungingDebugPathSanitizer( config.getDebugPathSanitizerLimit(), File.separatorChar, Paths.get("."), sanitizerPaths.build()); ImmutableList<String> cflags = cflagsBuilder.build(); ImmutableMap.Builder<String, String> macrosBuilder = ImmutableMap.builder(); macrosBuilder.put("SDKROOT", sdkPaths.getSdkPath().toString()); macrosBuilder.put("PLATFORM_DIR", sdkPaths.getPlatformPath().toString()); macrosBuilder.put("CURRENT_ARCH", targetArchitecture); if (sdkPaths.getDeveloperPath().isPresent()) { macrosBuilder.put("DEVELOPER_DIR", sdkPaths.getDeveloperPath().get().toString()); } ImmutableMap<String, String> macros = macrosBuilder.build(); Optional<String> buildVersion = Optional.empty(); Path platformVersionPlistPath = sdkPaths.getPlatformPath().resolve("version.plist"); try (InputStream versionPlist = Files.newInputStream(platformVersionPlistPath)) { NSDictionary versionInfo = (NSDictionary) PropertyListParser.parse(versionPlist); if (versionInfo != null) { NSObject productBuildVersion = versionInfo.objectForKey("ProductBuildVersion"); if (productBuildVersion != null) { buildVersion = Optional.of(productBuildVersion.toString()); } else { LOG.warn( "In %s, missing ProductBuildVersion. Build version will be unset for this platform.", platformVersionPlistPath); } } else { LOG.warn( "Empty version plist in %s. Build version will be unset for this platform.", platformVersionPlistPath); } } catch (NoSuchFileException e) { LOG.warn( "%s does not exist. Build version will be unset for this platform.", platformVersionPlistPath); } catch (PropertyListFormatException | SAXException | ParserConfigurationException | ParseException | IOException e) { // Some other error occurred, print the exception since it may contain error details. LOG.warn( e, "Failed to parse %s. Build version will be unset for this platform.", platformVersionPlistPath); } PreprocessorProvider aspp = new PreprocessorProvider(new ConstantToolProvider(clangPath), CxxToolProvider.Type.CLANG); CompilerProvider as = new CompilerProvider(new ConstantToolProvider(clangPath), CxxToolProvider.Type.CLANG); PreprocessorProvider cpp = new PreprocessorProvider(new ConstantToolProvider(clangPath), CxxToolProvider.Type.CLANG); CompilerProvider cc = new CompilerProvider(new ConstantToolProvider(clangPath), CxxToolProvider.Type.CLANG); PreprocessorProvider cxxpp = new PreprocessorProvider(new ConstantToolProvider(clangXxPath), CxxToolProvider.Type.CLANG); CompilerProvider cxx = new CompilerProvider(new ConstantToolProvider(clangXxPath), CxxToolProvider.Type.CLANG); ImmutableList.Builder<String> whitelistBuilder = ImmutableList.builder(); whitelistBuilder.add("^" + Pattern.quote(sdkPaths.getSdkPath().toString()) + "\\/.*"); whitelistBuilder.add( "^" + Pattern.quote(sdkPaths.getPlatformPath().toString() + "/Developer/Library/Frameworks") + "\\/.*"); for (Path toolchainPath : sdkPaths.getToolchainPaths()) { LOG.debug("Apple toolchain path: %s", toolchainPath); try { whitelistBuilder.add("^" + Pattern.quote(toolchainPath.toRealPath().toString()) + "\\/.*"); } catch (IOException e) { LOG.warn(e, "Apple toolchain path could not be resolved: %s", toolchainPath); } } HeaderVerification headerVerification = config.getHeaderVerification().withPlatformWhitelist(whitelistBuilder.build()); LOG.debug( "Headers verification platform whitelist: %s", headerVerification.getPlatformWhitelist()); CxxPlatform cxxPlatform = CxxPlatforms.build( targetFlavor, Platform.MACOS, config, as, aspp, cc, cxx, cpp, cxxpp, new DefaultLinkerProvider( LinkerProvider.Type.DARWIN, new ConstantToolProvider(clangXxPath)), ImmutableList.<String>builder().addAll(cflags).addAll(ldflagsBuilder.build()).build(), strip, new BsdArchiver(ar), ranlib, new PosixNmSymbolNameTool(nm), cflagsBuilder.build(), ImmutableList.of(), cflags, ImmutableList.of(), "dylib", "%s.dylib", "a", "o", compilerDebugPathSanitizer, assemblerDebugPathSanitizer, macros, Optional.empty(), headerVerification); ApplePlatform applePlatform = targetSdk.getApplePlatform(); ImmutableList.Builder<Path> swiftOverrideSearchPathBuilder = ImmutableList.builder(); AppleSdkPaths.Builder swiftSdkPathsBuilder = AppleSdkPaths.builder().from(sdkPaths); if (swiftToolChain.isPresent()) { swiftOverrideSearchPathBuilder.add(swiftToolChain.get().getPath().resolve(USR_BIN)); swiftSdkPathsBuilder.setToolchainPaths(ImmutableList.of(swiftToolChain.get().getPath())); } Optional<SwiftPlatform> swiftPlatform = getSwiftPlatform( applePlatform.getName(), targetArchitecture + "-apple-" + applePlatform.getSwiftName().orElse(applePlatform.getName()) + minVersion, version, swiftSdkPathsBuilder.build(), swiftOverrideSearchPathBuilder.addAll(toolSearchPaths).build(), xcodeToolFinder); AppleConfig appleConfig = buckConfig.getView(AppleConfig.class); platformBuilder .setCxxPlatform(cxxPlatform) .setSwiftPlatform(swiftPlatform) .setAppleSdk(targetSdk) .setAppleSdkPaths(sdkPaths) .setMinVersion(minVersion) .setBuildVersion(buildVersion) .setActool(actool) .setIbtool(ibtool) .setMomc(momc) .setCopySceneKitAssets( getOptionalTool("copySceneKitAssets", toolSearchPaths, xcodeToolFinder, version)) .setXctest(xctest) .setDsymutil(dsymutil) .setLipo(lipo) .setStubBinary(stubBinaryPath) .setLldb(lldb) .setCodesignAllocate( getOptionalTool("codesign_allocate", toolSearchPaths, xcodeToolFinder, version)) .setCodesignProvider(appleConfig.getCodesignProvider()); return platformBuilder.build(); } private static Optional<SwiftPlatform> getSwiftPlatform( String platformName, String targetArchitectureName, String version, AbstractAppleSdkPaths sdkPaths, ImmutableList<Path> toolSearchPaths, XcodeToolFinder xcodeToolFinder) { ImmutableList<String> swiftParams = ImmutableList.of( "-frontend", "-sdk", sdkPaths.getSdkPath().toString(), "-target", targetArchitectureName); ImmutableList.Builder<String> swiftStdlibToolParamsBuilder = ImmutableList.builder(); swiftStdlibToolParamsBuilder .add("--copy") .add("--verbose") .add("--strip-bitcode") .add("--platform") .add(platformName); for (Path toolchainPath : sdkPaths.getToolchainPaths()) { swiftStdlibToolParamsBuilder.add("--toolchain").add(toolchainPath.toString()); } Optional<Tool> swiftc = getOptionalToolWithParams("swiftc", toolSearchPaths, xcodeToolFinder, version, swiftParams); Optional<Tool> swiftStdLibTool = getOptionalToolWithParams( "swift-stdlib-tool", toolSearchPaths, xcodeToolFinder, version, swiftStdlibToolParamsBuilder.build()); if (swiftc.isPresent() && swiftStdLibTool.isPresent()) { return Optional.of( SwiftPlatforms.build( platformName, sdkPaths.getToolchainPaths(), swiftc.get(), swiftStdLibTool.get())); } else { return Optional.empty(); } } private static Optional<Tool> getOptionalTool( String tool, ImmutableList<Path> toolSearchPaths, XcodeToolFinder xcodeToolFinder, String version) { return getOptionalToolWithParams( tool, toolSearchPaths, xcodeToolFinder, version, ImmutableList.of()); } private static Optional<Tool> getOptionalToolWithParams( final String tool, ImmutableList<Path> toolSearchPaths, XcodeToolFinder xcodeToolFinder, final String version, final ImmutableList<String> params) { return xcodeToolFinder .getToolPath(toolSearchPaths, tool) .map( input -> VersionedTool.builder() .setPath(input) .setName(tool) .setVersion(version) .setExtraArgs(params) .build()); } private static Path getToolPath( String tool, ImmutableList<Path> toolSearchPaths, XcodeToolFinder xcodeToolFinder) { Optional<Path> result = xcodeToolFinder.getToolPath(toolSearchPaths, tool); if (!result.isPresent()) { throw new HumanReadableException("Cannot find tool %s in paths %s", tool, toolSearchPaths); } return result.get(); } @VisibleForTesting static class XcodeBuildVersionCache { private final Map<Path, Optional<String>> cache = new HashMap<>(); /** * Returns the Xcode build version. This is an alphanumeric string as output by {@code * xcodebuild -version} and shown in the About Xcode window. This value is embedded into the * plist of app bundles built by Xcode, under the field named {@code DTXcodeBuild} * * @param developerDir Path to developer dir, i.e. /Applications/Xcode.app/Contents/Developer * @return the xcode build version if found, nothing if it fails to be found, or the version * plist file cannot be read. */ Optional<String> lookup(Path developerDir) { return cache.computeIfAbsent( developerDir, ignored -> { Path versionPlist = developerDir.getParent().resolve("version.plist"); NSString result; try { NSDictionary dict = (NSDictionary) PropertyListParser.parse(Files.readAllBytes(versionPlist)); result = (NSString) dict.get("ProductBuildVersion"); } catch (IOException | ClassCastException | SAXException | PropertyListFormatException | ParseException e) { LOG.warn( e, "%s: Cannot find xcode build version, file is in an invalid format.", versionPlist); return Optional.empty(); } catch (ParserConfigurationException e) { throw new IllegalStateException("plist parser threw unexpected exception", e); } if (result != null) { return Optional.of(result.toString()); } else { LOG.warn( "%s: Cannot find xcode build version, file is in an invalid format.", versionPlist); return Optional.empty(); } }); } } }