/* * 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.NSArray; import com.dd.plist.NSDictionary; import com.dd.plist.NSString; import com.dd.plist.PropertyListFormatException; import com.dd.plist.PropertyListParser; import com.facebook.buck.log.Logger; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.VersionStringComparator; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Ordering; import com.google.common.collect.TreeMultimap; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.text.ParseException; import java.util.Arrays; import java.util.HashSet; import java.util.Optional; import java.util.Set; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; /** Utility class to discover the location of SDKs contained inside an Xcode installation. */ public class AppleSdkDiscovery { private static final Logger LOG = Logger.get(AppleSdkDiscovery.class); private static final Ordering<AppleSdk> APPLE_SDK_VERSION_ORDERING = new Ordering<AppleSdk>() { VersionStringComparator versionComparator = new VersionStringComparator(); @Override public int compare(AppleSdk thisSdk, AppleSdk thatSdk) { int result = versionComparator.compare(thisSdk.getVersion(), thatSdk.getVersion()); return result == 0 ? thisSdk.getName().compareTo(thatSdk.getName()) : result; } }; private static final String DEFAULT_TOOLCHAIN_ID = "com.apple.dt.toolchain.XcodeDefault"; // Utility class; do not instantiate. private AppleSdkDiscovery() {} /** * Given a path to an Xcode developer directory and a map of (xctoolchain ID: path) pairs as * returned by {@link AppleToolchainDiscovery}, walks through the platforms and builds a map of * ({@link AppleSdk}: {@link AppleSdkPaths}) objects describing the paths to the SDKs inside. * * <p>The {@link AppleSdk#getName()} strings match the ones displayed by {@code xcodebuild * -showsdks} and look like {@code macosx10.9}, {@code iphoneos8.0}, {@code iphonesimulator8.0}, * etc. */ public static ImmutableMap<AppleSdk, AppleSdkPaths> discoverAppleSdkPaths( Optional<Path> developerDir, ImmutableList<Path> extraDirs, ImmutableMap<String, AppleToolchain> xcodeToolchains, AppleConfig appleConfig) throws IOException { Optional<AppleToolchain> defaultToolchain = Optional.ofNullable(xcodeToolchains.get(DEFAULT_TOOLCHAIN_ID)); ImmutableMap.Builder<AppleSdk, AppleSdkPaths> appleSdkPathsBuilder = ImmutableMap.builder(); HashSet<Path> platformPaths = new HashSet<Path>(extraDirs); if (developerDir.isPresent()) { Path platformsDir = developerDir.get().resolve("Platforms"); LOG.debug("Searching for Xcode platforms under %s", platformsDir); platformPaths.add(platformsDir); } // We need to find the most recent SDK for each platform so we can // make the fall-back SDKs with no version number in their name // ("macosx", "iphonesimulator", "iphoneos"). // // To do this, we store a map of (platform: [sdk1, sdk2, ...]) // pairs where the SDKs for each platform are ordered by version. TreeMultimap<ApplePlatform, AppleSdk> orderedSdksForPlatform = TreeMultimap.create(Ordering.natural(), APPLE_SDK_VERSION_ORDERING); for (Path platforms : platformPaths) { if (!Files.exists(platforms)) { LOG.debug("Skipping platform search path %s that does not exist", platforms); continue; } LOG.debug("Searching for Xcode SDKs in %s", platforms); try (DirectoryStream<Path> platformStream = Files.newDirectoryStream(platforms, "*.platform")) { for (Path platformDir : platformStream) { Path developerSdksPath = platformDir.resolve("Developer/SDKs"); try (DirectoryStream<Path> sdkStream = Files.newDirectoryStream(developerSdksPath, "*.sdk")) { Set<Path> scannedSdkDirs = new HashSet<>(); for (Path sdkDir : sdkStream) { LOG.debug("Fetching SDK name for %s", sdkDir); sdkDir = sdkDir.toRealPath(); if (scannedSdkDirs.contains(sdkDir)) { LOG.debug("Skipping already scanned SDK directory %s", sdkDir); continue; } AppleSdk.Builder sdkBuilder = AppleSdk.builder(); if (buildSdkFromPath( sdkDir, sdkBuilder, xcodeToolchains, defaultToolchain, appleConfig)) { AppleSdk sdk = sdkBuilder.build(); LOG.debug("Found SDK %s", sdk); AppleSdkPaths.Builder xcodePathsBuilder = AppleSdkPaths.builder(); for (AppleToolchain toolchain : sdk.getToolchains()) { xcodePathsBuilder.addToolchainPaths(toolchain.getPath()); } AppleSdkPaths xcodePaths = xcodePathsBuilder .setDeveloperPath(developerDir) .setPlatformPath(platformDir) .setSdkPath(sdkDir) .build(); appleSdkPathsBuilder.put(sdk, xcodePaths); orderedSdksForPlatform.put(sdk.getApplePlatform(), sdk); } scannedSdkDirs.add(sdkDir); } } catch (NoSuchFileException e) { LOG.warn( e, "Couldn't discover SDKs at path %s, ignoring platform %s", developerSdksPath, platformDir); } } } } // Get a snapshot of what's in appleSdkPathsBuilder, then for each // ApplePlatform, add to appleSdkPathsBuilder the most recent // SDK with an unversioned name. ImmutableMap<AppleSdk, AppleSdkPaths> discoveredSdkPaths = appleSdkPathsBuilder.build(); for (ApplePlatform platform : orderedSdksForPlatform.keySet()) { Set<AppleSdk> platformSdks = orderedSdksForPlatform.get(platform); boolean shouldCreateUnversionedSdk = true; for (AppleSdk sdk : platformSdks) { shouldCreateUnversionedSdk &= !sdk.getName().equals(platform.getName()); } if (shouldCreateUnversionedSdk) { AppleSdk mostRecentSdkForPlatform = orderedSdksForPlatform.get(platform).last(); appleSdkPathsBuilder.put( mostRecentSdkForPlatform.withName(platform.getName()), discoveredSdkPaths.get(mostRecentSdkForPlatform)); } } // This includes both the discovered SDKs with versions in their names, as well as // the unversioned aliases added just above. return appleSdkPathsBuilder.build(); } private static boolean buildSdkFromPath( Path sdkDir, AppleSdk.Builder sdkBuilder, ImmutableMap<String, AppleToolchain> xcodeToolchains, Optional<AppleToolchain> defaultToolchain, AppleConfig appleConfig) throws IOException { try (InputStream sdkSettingsPlist = Files.newInputStream(sdkDir.resolve("SDKSettings.plist")); BufferedInputStream bufferedSdkSettingsPlist = new BufferedInputStream(sdkSettingsPlist)) { NSDictionary sdkSettings; try { sdkSettings = (NSDictionary) PropertyListParser.parse(bufferedSdkSettingsPlist); } catch (PropertyListFormatException | ParseException | SAXException e) { LOG.error(e, "Malformatted SDKSettings.plist. Skipping SDK path %s.", sdkDir); return false; } catch (ParserConfigurationException e) { throw new IOException(e); } String name = sdkSettings.objectForKey("CanonicalName").toString(); String version = sdkSettings.objectForKey("Version").toString(); NSDictionary defaultProperties = (NSDictionary) sdkSettings.objectForKey("DefaultProperties"); NSString platformName = (NSString) defaultProperties.objectForKey("PLATFORM_NAME"); Optional<ImmutableList<String>> toolchains = appleConfig.getToolchainsOverrideForSDKName(platformName.toString()); boolean foundToolchain = false; if (!toolchains.isPresent()) { NSArray settingsToolchains = (NSArray) sdkSettings.objectForKey("Toolchains"); if (settingsToolchains != null) { toolchains = Optional.of( Arrays.stream(settingsToolchains.getArray()) .map(Object::toString) .collect(MoreCollectors.toImmutableList())); } } if (toolchains.isPresent()) { for (String toolchainId : toolchains.get()) { AppleToolchain toolchain = xcodeToolchains.get(toolchainId); if (toolchain != null) { foundToolchain = true; sdkBuilder.addToolchains(toolchain); } else { LOG.debug("Specified toolchain %s not found for SDK path %s", toolchainId, sdkDir); } } } if (!foundToolchain && defaultToolchain.isPresent()) { foundToolchain = true; sdkBuilder.addToolchains(defaultToolchain.get()); } if (!foundToolchain) { LOG.warn("No toolchains found and no default toolchain. Skipping SDK path %s.", sdkDir); return false; } else { ApplePlatform applePlatform = ApplePlatform.of(platformName.toString()); sdkBuilder.setName(name).setVersion(version).setApplePlatform(applePlatform); ImmutableList<String> architectures = validArchitecturesForPlatform(applePlatform, sdkDir); sdkBuilder.addAllArchitectures(architectures); return true; } } catch (NoSuchFileException e) { LOG.warn(e, "Skipping SDK at path %s, no SDKSettings.plist found", sdkDir); return false; } } private static ImmutableList<String> validArchitecturesForPlatform( ApplePlatform platform, Path sdkDir) throws IOException { ImmutableList<String> architectures = platform.getArchitectures(); try (DirectoryStream<Path> sdkFiles = Files.newDirectoryStream(sdkDir)) { ImmutableList.Builder<String> architectureSubdirsBuilder = ImmutableList.builder(); for (Path path : sdkFiles) { if (Files.isDirectory(path)) { String directoryName = path.getFileName().toString(); // Default Apple SDKs contain fat binaries and have no architecture subdirectories, // but custom SDKs might. if (architectures.contains(directoryName)) { architectureSubdirsBuilder.add(directoryName); } } } ImmutableList<String> architectureSubdirs = architectureSubdirsBuilder.build(); if (!architectureSubdirs.isEmpty()) { architectures = architectureSubdirs; } } return architectures; } }