| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // 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.google.devtools.build.lib.exec.local; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Maps; |
| import com.google.devtools.build.lib.exec.BinTools; |
| import com.google.devtools.build.lib.rules.apple.AppleConfiguration; |
| import com.google.devtools.build.lib.rules.apple.DottedVersion; |
| import com.google.devtools.build.lib.shell.AbnormalTerminationException; |
| import com.google.devtools.build.lib.shell.Command; |
| import com.google.devtools.build.lib.shell.CommandException; |
| import com.google.devtools.build.lib.shell.CommandResult; |
| import com.google.devtools.build.lib.shell.TerminationStatus; |
| import com.google.devtools.build.lib.vfs.Path; |
| import java.io.IOException; |
| import java.io.UncheckedIOException; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.logging.Logger; |
| |
| /** |
| * Adds to the given environment all variables that are dependent on system state of the host |
| * machine. |
| * |
| * <p>Admittedly, hermeticity is "best effort" in such cases; these environment values should be as |
| * tied to configuration parameters as possible. |
| * |
| * <p>For example, underlying iOS toolchains require that SDKROOT resolve to an absolute system |
| * path, but, when selecting which SDK to resolve, the version number comes from build |
| * configuration. |
| */ |
| public final class XcodeLocalEnvProvider implements LocalEnvProvider { |
| |
| private static final Logger log = Logger.getLogger(XcodeLocalEnvProvider.class.getName()); |
| |
| private final Map<String, String> clientEnv; |
| |
| private static final ConcurrentMap<String, String> sdkRootCache = new ConcurrentHashMap<>(); |
| private static final ConcurrentMap<String, String> developerDirCache = new ConcurrentHashMap<>(); |
| |
| /** |
| * Creates a new {@link XcodeLocalEnvProvider}. |
| * |
| * <p>Use {@link LocalEnvProvider#forCurrentOs(Map)} to instantiate this. |
| * |
| * @param clientEnv a map of the current Bazel command's environment |
| */ |
| XcodeLocalEnvProvider(Map<String, String> clientEnv) { |
| this.clientEnv = clientEnv; |
| } |
| |
| @Override |
| public Map<String, String> rewriteLocalEnv( |
| Map<String, String> env, BinTools binTools, String fallbackTmpDir) throws IOException { |
| boolean containsXcodeVersion = env.containsKey(AppleConfiguration.XCODE_VERSION_ENV_NAME); |
| boolean containsAppleSdkVersion = |
| env.containsKey(AppleConfiguration.APPLE_SDK_VERSION_ENV_NAME); |
| |
| ImmutableMap.Builder<String, String> newEnvBuilder = ImmutableMap.builder(); |
| newEnvBuilder.putAll(Maps.filterKeys(env, k -> !k.equals("TMPDIR"))); |
| String p = clientEnv.get("TMPDIR"); |
| if (Strings.isNullOrEmpty(p)) { |
| // Do not use `fallbackTmpDir`, use `/tmp` instead. This way if the user didn't export TMPDIR |
| // in their environment, Bazel will still set a TMPDIR that's Posixy enough and plays well |
| // with heavily path-length-limited scenarios, such as the socket creation scenario that |
| // motivated https://github.com/bazelbuild/bazel/issues/4376. |
| p = "/tmp"; |
| } |
| newEnvBuilder.put("TMPDIR", p); |
| |
| if (!containsXcodeVersion && !containsAppleSdkVersion) { |
| return newEnvBuilder.build(); |
| } |
| |
| // Empty developer dir indicates to use the system default. |
| // TODO(bazel-team): Bazel's view of the xcode version and developer dir should be explicitly |
| // set for build hermeticity. |
| String developerDir = ""; |
| if (containsXcodeVersion) { |
| String version = env.get(AppleConfiguration.XCODE_VERSION_ENV_NAME); |
| developerDir = getDeveloperDir(binTools, DottedVersion.fromStringUnchecked(version)); |
| newEnvBuilder.put("DEVELOPER_DIR", developerDir); |
| } |
| if (containsAppleSdkVersion) { |
| // The Apple platform is needed to select the appropriate SDK. |
| if (!env.containsKey(AppleConfiguration.APPLE_SDK_PLATFORM_ENV_NAME)) { |
| throw new IOException("Could not resolve apple platform for determining SDK"); |
| } |
| String iosSdkVersion = env.get(AppleConfiguration.APPLE_SDK_VERSION_ENV_NAME); |
| String appleSdkPlatform = env.get(AppleConfiguration.APPLE_SDK_PLATFORM_ENV_NAME); |
| newEnvBuilder.put("SDKROOT", getSdkRoot(developerDir, iosSdkVersion, appleSdkPlatform)); |
| } |
| |
| return newEnvBuilder.build(); |
| } |
| |
| /** |
| * Queries the path to the target Apple SDK on the host system for a given version of Xcode. |
| * |
| * <p>This spawns a subprocess to run the {@code /usr/bin/xcrun} binary to locate the target SDK. |
| * As this is a costly operation, always call {@link #getSdkRoot(String, String, String)} instead, |
| * which does caching. |
| * |
| * @param developerDir the value of {@code DEVELOPER_DIR} for the target version of xcode |
| * @param sdkVersion the sdk version; for example, {@code 9.1} |
| * @param appleSdkPlatform the sdk platform; for example, {@code iPhoneOS} |
| * @return an absolute path to the root of the target Apple SDK |
| * @throws IOException if there is an issue with obtaining the root from the spawned process, |
| * either because the SDK platform/version pair doesn't exist, or there was an unexpected |
| * issue finding or running the tool |
| */ |
| private static String querySdkRoot( |
| String developerDir, |
| String sdkVersion, |
| String appleSdkPlatform) |
| throws IOException { |
| try { |
| String sdkString = appleSdkPlatform.toLowerCase() + sdkVersion; |
| Map<String, String> env = |
| Strings.isNullOrEmpty(developerDir) |
| ? ImmutableMap.<String, String>of() |
| : ImmutableMap.of("DEVELOPER_DIR", developerDir); |
| CommandResult xcrunResult = |
| new Command( |
| new String[] {"/usr/bin/xcrun", "--sdk", sdkString, "--show-sdk-path"}, |
| env, |
| null) |
| .execute(); |
| |
| return new String(xcrunResult.getStdout(), StandardCharsets.UTF_8).trim(); |
| } catch (AbnormalTerminationException e) { |
| TerminationStatus terminationStatus = e.getResult().getTerminationStatus(); |
| |
| if (terminationStatus.exited()) { |
| throw new IOException( |
| String.format( |
| "xcrun failed with code %s.\n" |
| + "This most likely indicates that SDK version [%s] for platform [%s] is " |
| + "unsupported for the target version of xcode.\n" |
| + "%s\n" |
| + "stdout: %s" |
| + "stderr: %s", |
| terminationStatus.getExitCode(), |
| sdkVersion, |
| appleSdkPlatform, |
| terminationStatus.toString(), |
| new String(e.getResult().getStdout(), StandardCharsets.UTF_8), |
| new String(e.getResult().getStderr(), StandardCharsets.UTF_8))); |
| } |
| String message = |
| String.format( |
| "xcrun failed.\n" + "%s\n" + "stdout: %s\n" + "stderr: %s", |
| e.getResult().getTerminationStatus(), |
| new String(e.getResult().getStdout(), StandardCharsets.UTF_8), |
| new String(e.getResult().getStderr(), StandardCharsets.UTF_8)); |
| throw new IOException(message, e); |
| } catch (CommandException e) { |
| throw new IOException(e); |
| } |
| } |
| |
| /** |
| * Returns the path to the target Apple SDK on the host system for a given version of Xcode. |
| * |
| * <p>This may delegate to {@link #querySdkRoot(String, String, String)} to obtain the path from |
| * external sources in the system. Values are cached in-memory throughout the lifetime of the |
| * Bazel server. |
| * |
| * @param developerDir the value of {@code DEVELOPER_DIR} for the target version of xcode |
| * @param sdkVersion the sdk version; for example, {@code 9.1} |
| * @param appleSdkPlatform the sdk platform; for example, {@code iPhoneOS} |
| * @return an absolute path to the root of the target Apple SDK |
| * @throws IOException if there is an issue with obtaining the root from the spawned process, |
| * either because the SDK platform/version pair doesn't exist, or there was an unexpected |
| * issue finding or running the tool |
| */ |
| private static String getSdkRoot(String developerDir, String sdkVersion, String appleSdkPlatform) |
| throws IOException { |
| try { |
| return sdkRootCache.computeIfAbsent( |
| developerDir + ":" + appleSdkPlatform.toLowerCase() + ":" + sdkVersion, |
| (key) -> { |
| try { |
| String sdkRoot = querySdkRoot(developerDir, sdkVersion, appleSdkPlatform); |
| log.info("Queried Xcode SDK root with key " + key + " and got " + sdkRoot); |
| return sdkRoot; |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| }); |
| } catch (UncheckedIOException e) { |
| throw e.getCause(); |
| } |
| } |
| |
| /** |
| * Queries the path to the Xcode developer directory on the host system for the given Xcode |
| * version. |
| * |
| * <p>This spawns a subprocess to run the {@code xcode-locator} binary. As this is a costly |
| * operation, always call {@link #getDeveloperDir(Path, DottedVersion)} instead, which does |
| * caching. |
| * |
| * @param binTools the {@link BinTools}, used to locate the cache file |
| * @param version the xcode version number to look up |
| * @return an absolute path to the root of the Xcode developer directory |
| * @throws IOException if there is an issue with obtaining the path from the spawned process, |
| * either because there is no installed xcode with the given version, or there was an |
| * unexpected issue finding or running the tool |
| */ |
| private static String queryDeveloperDir(BinTools binTools, DottedVersion version) |
| throws IOException { |
| String xcodeLocatorPath = binTools.getEmbeddedPath("xcode-locator").getPathString(); |
| try { |
| CommandResult xcodeLocatorResult = |
| new Command(new String[] {xcodeLocatorPath, version.toString()}).execute(); |
| |
| return new String(xcodeLocatorResult.getStdout(), StandardCharsets.UTF_8).trim(); |
| } catch (AbnormalTerminationException e) { |
| TerminationStatus terminationStatus = e.getResult().getTerminationStatus(); |
| |
| String message; |
| if (e.getResult().getTerminationStatus().exited()) { |
| message = |
| String.format( |
| "Running '%s %s' failed with code %s.\n" |
| + "This most likely indicates that xcode version %s is not available on the " |
| + "host machine.\n" |
| + "%s\n" |
| + "stdout: %s\n" |
| + "stderr: %s", |
| xcodeLocatorPath, |
| version, |
| terminationStatus.getExitCode(), |
| version, |
| terminationStatus.toString(), |
| new String(e.getResult().getStdout(), StandardCharsets.UTF_8), |
| new String(e.getResult().getStderr(), StandardCharsets.UTF_8)); |
| } else { |
| message = |
| String.format( |
| "Running '%s %s' failed.\n" + "%s\n" + "stdout: %s\n" + "stderr: %s", |
| xcodeLocatorPath, |
| version, |
| e.getResult().getTerminationStatus(), |
| new String(e.getResult().getStdout(), StandardCharsets.UTF_8), |
| new String(e.getResult().getStderr(), StandardCharsets.UTF_8)); |
| } |
| throw new IOException(message, e); |
| } catch (CommandException e) { |
| throw new IOException(e); |
| } |
| } |
| |
| /** |
| * Returns the absolute root path of the xcode developer directory on the host system for the |
| * given Xcode version. |
| * |
| * <p>This may delegate to {@link #queryDeveloperDir(Path, DottedVersion)} to obtain the path from |
| * external sources in the system. Values are cached in-memory throughout the lifetime of the |
| * Bazel server. |
| * |
| * @param binTools the {@link BinTools} path, used to locate the cache file |
| * @param version the xcode version number to look up |
| * @return an absolute path to the root of the Xcode developer directory |
| * @throws IOException if there is an issue with obtaining the path from the spawned process, |
| * either because there is no installed xcode with the given version, or there was an |
| * unexpected issue finding or running the tool |
| */ |
| private static String getDeveloperDir(BinTools binTools, DottedVersion version) |
| throws IOException { |
| try { |
| return developerDirCache.computeIfAbsent( |
| version.toString(), |
| (key) -> { |
| try { |
| String developerDir = queryDeveloperDir(binTools, version); |
| log.info("Queried Xcode developer dir with key " + key + " and got " + developerDir); |
| return developerDir; |
| } catch (IOException e) { |
| throw new UncheckedIOException(e); |
| } |
| }); |
| } catch (UncheckedIOException e) { |
| throw e.getCause(); |
| } |
| } |
| } |