// 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.apple; 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.local.LocalEnvProvider; 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. * *

Admittedly, hermeticity is "best effort" in such cases; these environment values should be as * tied to configuration parameters as possible. * *

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 clientEnv; private static final ConcurrentMap sdkRootCache = new ConcurrentHashMap<>(); private static final ConcurrentMap developerDirCache = new ConcurrentHashMap<>(); /** * Creates a new {@link XcodeLocalEnvProvider}. * * @param clientEnv a map of the current Bazel command's environment */ public XcodeLocalEnvProvider(Map clientEnv) { this.clientEnv = clientEnv; } @Override public Map rewriteLocalEnv( Map env, Path execRoot, 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 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(execRoot, DottedVersion.fromString(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. * *

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 env = Strings.isNullOrEmpty(developerDir) ? ImmutableMap.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. * *

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. * *

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 execRoot the execution root 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 queryDeveloperDir(Path execRoot, DottedVersion version) throws IOException { String xcodeLocatorPath = execRoot.getRelative("_bin/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. * *

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 execRoot the execution root 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(Path execRoot, DottedVersion version) throws IOException { try { return developerDirCache.computeIfAbsent( version.toString(), (key) -> { try { String developerDir = queryDeveloperDir(execRoot, 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(); } } }