/* * 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.facebook.buck.cli.BuckConfig; import com.facebook.buck.config.ConfigView; import com.facebook.buck.io.ExecutableFinder; import com.facebook.buck.log.Logger; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.rules.BinaryBuildRuleToolProvider; import com.facebook.buck.rules.ConstantToolProvider; import com.facebook.buck.rules.HashedFileTool; import com.facebook.buck.rules.ToolProvider; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessExecutorParams; import com.facebook.buck.util.immutables.BuckStyleTuple; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.EnumSet; import java.util.Optional; import java.util.Set; import org.immutables.value.Value; public class AppleConfig implements ConfigView<BuckConfig> { private static final String DEFAULT_TEST_LOG_DIRECTORY_ENVIRONMENT_VARIABLE = "FB_LOG_DIRECTORY"; private static final String DEFAULT_TEST_LOG_LEVEL_ENVIRONMENT_VARIABLE = "FB_LOG_LEVEL"; private static final String DEFAULT_TEST_LOG_LEVEL = "debug"; private static final Logger LOG = Logger.get(AppleConfig.class); public static final String APPLE_SECTION = "apple"; private final BuckConfig delegate; // Reflection-based factory for ConfigView public static AppleConfig of(BuckConfig delegate) { return new AppleConfig(delegate); } @Override public BuckConfig getDelegate() { return delegate; } private AppleConfig(BuckConfig delegate) { this.delegate = delegate; } /** * If specified, the value of {@code [apple] xcode_developer_dir} wrapped in a {@link Supplier}. * Otherwise, this returns a {@link Supplier} that lazily runs {@code xcode-select --print-path} * and caches the result. */ public Supplier<Optional<Path>> getAppleDeveloperDirectorySupplier( ProcessExecutor processExecutor) { Optional<String> xcodeDeveloperDirectory = delegate.getValue(APPLE_SECTION, "xcode_developer_dir"); if (xcodeDeveloperDirectory.isPresent()) { Path developerDirectory = delegate.resolvePathThatMayBeOutsideTheProjectFilesystem( Paths.get(xcodeDeveloperDirectory.get())); return Suppliers.ofInstance(Optional.of(normalizePath(developerDirectory))); } else { return createAppleDeveloperDirectorySupplier(processExecutor); } } /** * If specified, the value of {@code [apple] xcode_developer_dir_for_tests} wrapped in a {@link * Supplier}. Otherwise, this falls back to {@code [apple] xcode_developer_dir} and finally {@code * xcode-select --print-path}. */ public Supplier<Optional<Path>> getAppleDeveloperDirectorySupplierForTests( ProcessExecutor processExecutor) { Optional<String> xcodeDeveloperDirectory = delegate.getValue(APPLE_SECTION, "xcode_developer_dir_for_tests"); if (xcodeDeveloperDirectory.isPresent()) { Path developerDirectory = delegate.resolvePathThatMayBeOutsideTheProjectFilesystem( Paths.get(xcodeDeveloperDirectory.get())); return Suppliers.ofInstance(Optional.of(normalizePath(developerDirectory))); } else { return getAppleDeveloperDirectorySupplier(processExecutor); } } public ImmutableList<Path> getExtraToolchainPaths() { ImmutableList<String> extraPathsStrings = delegate.getListWithoutComments(APPLE_SECTION, "extra_toolchain_paths"); return ImmutableList.copyOf( Lists.transform( extraPathsStrings, string -> normalizePath( delegate.resolveNonNullPathOutsideTheProjectFilesystem(Paths.get(string))))); } public ImmutableList<Path> getExtraPlatformPaths() { ImmutableList<String> extraPathsStrings = delegate.getListWithoutComments(APPLE_SECTION, "extra_platform_paths"); return ImmutableList.copyOf( Lists.transform( extraPathsStrings, string -> normalizePath( delegate.resolveNonNullPathOutsideTheProjectFilesystem(Paths.get(string))))); } private static Path normalizePath(Path path) { try { return path.toRealPath(); } catch (IOException e) { return path; } } public ImmutableMap<AppleSdk, AppleSdkPaths> getAppleSdkPaths(ProcessExecutor processExecutor) { Optional<Path> appleDeveloperDirectory = getAppleDeveloperDirectorySupplier(processExecutor).get(); try { ImmutableMap<String, AppleToolchain> toolchains = AppleToolchainDiscovery.discoverAppleToolchains( appleDeveloperDirectory, getExtraToolchainPaths()); return AppleSdkDiscovery.discoverAppleSdkPaths( appleDeveloperDirectory, getExtraPlatformPaths(), toolchains, this); } catch (IOException e) { throw new RuntimeException(e); } } /** * @return a memoizing {@link Supplier} that caches the output of {@code xcode-select * --print-path}. */ private static Supplier<Optional<Path>> createAppleDeveloperDirectorySupplier( final ProcessExecutor processExecutor) { return Suppliers.memoize( new Supplier<Optional<Path>>() { @Override public Optional<Path> get() { ProcessExecutorParams processExecutorParams = ProcessExecutorParams.builder() .setCommand(ImmutableList.of("xcode-select", "--print-path")) .build(); // Must specify that stdout is expected or else output may be wrapped in Ansi escape chars. Set<ProcessExecutor.Option> options = EnumSet.of(ProcessExecutor.Option.EXPECTING_STD_OUT); ProcessExecutor.Result result; try { result = processExecutor.launchAndExecute( processExecutorParams, options, /* stdin */ Optional.empty(), /* timeOutMs */ Optional.empty(), /* timeOutHandler */ Optional.empty()); } catch (InterruptedException | IOException e) { LOG.warn("Could not execute xcode-select, continuing without developer dir."); return Optional.empty(); } if (result.getExitCode() != 0) { throw new RuntimeException( result.getMessageForUnexpectedResult("xcode-select --print-path")); } return Optional.of(normalizePath(Paths.get(result.getStdout().get().trim()))); } }); } public Optional<String> getTargetSdkVersion(ApplePlatform platform) { return delegate.getValue(APPLE_SECTION, platform.getName() + "_target_sdk_version"); } public ImmutableList<String> getXctestPlatformNames() { return delegate.getListWithoutComments(APPLE_SECTION, "xctest_platforms"); } public Optional<Path> getXctoolPath() { Path xctool = getOptionalPath(APPLE_SECTION, "xctool_path").orElse(Paths.get("xctool")); return new ExecutableFinder().getOptionalExecutable(xctool, delegate.getEnvironment()); } public Optional<BuildTarget> getXctoolZipTarget() { return delegate.getBuildTarget(APPLE_SECTION, "xctool_zip_target"); } public ToolProvider getCodesignProvider() { final String codesignField = "codesign"; Optional<BuildTarget> target = delegate.getMaybeBuildTarget(APPLE_SECTION, codesignField); String source = String.format("[%s] %s", APPLE_SECTION, codesignField); if (target.isPresent()) { return new BinaryBuildRuleToolProvider(target.get(), source); } else { Optional<Path> codesignPath = delegate.getPath(APPLE_SECTION, codesignField); Path defaultCodesignPath = Paths.get("/usr/bin/codesign"); HashedFileTool codesign = new HashedFileTool(codesignPath.orElse(defaultCodesignPath)); return new ConstantToolProvider(codesign); } } public Optional<String> getXctoolDefaultDestinationSpecifier() { return delegate.getValue(APPLE_SECTION, "xctool_default_destination_specifier"); } public Optional<Long> getXctoolStutterTimeoutMs() { return delegate.getLong(APPLE_SECTION, "xctool_stutter_timeout"); } public boolean getXcodeDisableParallelizeBuild() { return delegate.getBooleanValue(APPLE_SECTION, "xcode_disable_parallelize_build", false); } public boolean useDryRunCodeSigning() { return delegate.getBooleanValue(APPLE_SECTION, "dry_run_code_signing", false); } public boolean cacheBundlesAndPackages() { return delegate.getBooleanValue(APPLE_SECTION, "cache_bundles_and_packages", true); } public Optional<Path> getAppleDeviceHelperAbsolutePath() { return getOptionalPath(APPLE_SECTION, "device_helper_path"); } public Optional<BuildTarget> getAppleDeviceHelperTarget() { return delegate.getBuildTarget(APPLE_SECTION, "device_helper_target"); } public Path getProvisioningProfileSearchPath() { return getOptionalPath(APPLE_SECTION, "provisioning_profile_search_path") .orElse( Paths.get( System.getProperty("user.home") + "/Library/MobileDevice/Provisioning Profiles")); } private Optional<Path> getOptionalPath(String sectionName, String propertyName) { Optional<String> pathString = delegate.getValue(sectionName, propertyName); if (pathString.isPresent()) { return Optional.of( delegate.resolvePathThatMayBeOutsideTheProjectFilesystem(Paths.get(pathString.get()))); } else { return Optional.empty(); } } public boolean shouldUseHeaderMapsInXcodeProject() { return delegate.getBooleanValue(APPLE_SECTION, "use_header_maps_in_xcode", true); } public boolean shouldMergeHeaderMapsInXcodeProject() { return delegate.getBooleanValue(APPLE_SECTION, "merge_header_maps_in_xcode", false); } public boolean shouldGenerateHeaderSymlinkTreesOnly() { return delegate.getBooleanValue(APPLE_SECTION, "generate_header_symlink_tree_only", false); } public String getTestLogDirectoryEnvironmentVariable() { return delegate .getValue(APPLE_SECTION, "test_log_directory_environment_variable") .orElse(DEFAULT_TEST_LOG_DIRECTORY_ENVIRONMENT_VARIABLE); } public String getTestLogLevelEnvironmentVariable() { return delegate .getValue(APPLE_SECTION, "test_log_level_environment_variable") .orElse(DEFAULT_TEST_LOG_LEVEL_ENVIRONMENT_VARIABLE); } public String getTestLogLevel() { return delegate.getValue(APPLE_SECTION, "test_log_level").orElse(DEFAULT_TEST_LOG_LEVEL); } public AppleDebugFormat getDefaultDebugInfoFormatForBinaries() { return delegate .getEnum(APPLE_SECTION, "default_debug_info_format_for_binaries", AppleDebugFormat.class) .orElse(AppleDebugFormat.DWARF_AND_DSYM); } public AppleDebugFormat getDefaultDebugInfoFormatForTests() { return delegate .getEnum(APPLE_SECTION, "default_debug_info_format_for_tests", AppleDebugFormat.class) .orElse(AppleDebugFormat.DWARF); } public AppleDebugFormat getDefaultDebugInfoFormatForLibraries() { return delegate .getEnum(APPLE_SECTION, "default_debug_info_format_for_libraries", AppleDebugFormat.class) .orElse(AppleDebugFormat.DWARF); } public ImmutableList<String> getProvisioningProfileReadCommand() { Optional<String> value = delegate.getValue(APPLE_SECTION, "provisioning_profile_read_command"); if (!value.isPresent()) { return ProvisioningProfileStore.DEFAULT_READ_COMMAND; } return ImmutableList.copyOf(Splitter.on(' ').splitToList(value.get())); } public ImmutableList<String> getCodeSignIdentitiesCommand() { Optional<String> value = delegate.getValue(APPLE_SECTION, "code_sign_identities_command"); if (!value.isPresent()) { return CodeSignIdentityStore.DEFAULT_IDENTITIES_COMMAND; } return ImmutableList.copyOf(Splitter.on(' ').splitToList(value.get())); } /** * Returns the custom packager command specified in the config, if defined. * * <p>This is translated into the config value of {@code apple.PLATFORMNAME_packager_command}. * * @param platform the platform to query. * @return the custom packager command specified in the config, if defined. */ public Optional<ApplePackageConfig> getPackageConfigForPlatform(ApplePlatform platform) { String command = delegate.getValue(APPLE_SECTION, platform.getName() + "_package_command").orElse(""); String extension = delegate.getValue(APPLE_SECTION, platform.getName() + "_package_extension").orElse(""); if (command.isEmpty() ^ extension.isEmpty()) { throw new HumanReadableException( "Config option %s and %s should be both specified, or be both omitted.", "apple." + platform.getName() + "_package_command", "apple." + platform.getName() + "_package_extension"); } else if (command.isEmpty() && extension.isEmpty()) { return Optional.empty(); } else { return Optional.of(ApplePackageConfig.of(command, extension)); } } public Optional<ImmutableList<String>> getToolchainsOverrideForSDKName(String name) { return delegate.getOptionalListWithoutComments(APPLE_SECTION, name + "_toolchains_override"); } @Value.Immutable @BuckStyleTuple interface AbstractApplePackageConfig { String getCommand(); String getExtension(); } }