aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/exec
diff options
context:
space:
mode:
authorGravatar ulfjack <ulfjack@google.com>2017-06-13 12:56:07 +0200
committerGravatar Yun Peng <pcloudy@google.com>2017-06-13 17:13:17 +0200
commit1d62c67a2d6d6eccf415ad5647d860d08f4c5966 (patch)
tree23eeec1aa45038fc5693b09c8d2556df5f92aa59 /src/main/java/com/google/devtools/build/lib/exec
parent3e87c626ed76536420aa06e4c258209b32bb76e0 (diff)
Extract the MacOS/XCode env rewrite logic into lib.exec.apple
Also add an interface to allow injecting that logic into LocalSpawnRunner; this is in preparation for rewriting StandaloneSpawnStrategy to use LocalSpawnRunner. At the same time, this reduces the dependencies from exec / standalone to rules.apple, which is a prerequisite for micro-Bazel. There's a small semantic change hidden here - we now only set the new XCodeLocalEnvProvider if we're actually running on Darwin, so we no longer fail execution on non-Darwin platforms if XCODE_VERSION_OVERRIDE or APPLE_SDK_VERSION_OVERRIDE is set. As a result, I moved the corresponding test from StandaloneSpawnStrategyTest to the new XCodeLocalEnvProviderTest. While I'm at it, also open source DottedVersionTest and CacheManagerTest. PiperOrigin-RevId: 158829077
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/exec')
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/apple/BUILD24
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/apple/CacheManager.java108
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/apple/XCodeLocalEnvProvider.java215
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/local/LocalEnvProvider.java36
-rw-r--r--src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java33
5 files changed, 393 insertions, 23 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/exec/apple/BUILD b/src/main/java/com/google/devtools/build/lib/exec/apple/BUILD
new file mode 100644
index 0000000000..c7c19dc84e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/apple/BUILD
@@ -0,0 +1,24 @@
+package(
+ default_visibility = ["//src:__subpackages__"],
+)
+
+java_library(
+ name = "apple",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib:build-base",
+ "//src/main/java/com/google/devtools/build/lib:packages-internal",
+ "//src/main/java/com/google/devtools/build/lib:shell",
+ "//src/main/java/com/google/devtools/build/lib:util",
+ "//src/main/java/com/google/devtools/build/lib:vfs",
+ "//src/main/java/com/google/devtools/build/lib/exec/local",
+ "//src/main/java/com/google/devtools/build/lib/rules/apple",
+ "//third_party:guava",
+ ],
+)
+
+filegroup(
+ name = "srcs",
+ testonly = 0, # All srcs should be not test only, overwrite package default.
+ srcs = glob(["**"]),
+)
diff --git a/src/main/java/com/google/devtools/build/lib/exec/apple/CacheManager.java b/src/main/java/com/google/devtools/build/lib/exec/apple/CacheManager.java
new file mode 100644
index 0000000000..0c7e52a328
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/apple/CacheManager.java
@@ -0,0 +1,108 @@
+// Copyright 2016 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.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * General cache file manager for mapping one or more keys to host-related path information.
+ *
+ * <p> Cache management has some notable restrictions:
+ * <ul>
+ * <li>Each cache entry must have the same number of (string) keys, and one value.</li>
+ * <li>An entry, once written to the cache, must be stable between builds. Clearing the cache
+ * requires a full clean of the bazel output directory.</li>
+ * </ul>
+ *
+ * <p> Note that a single cache manager instance is not thread-safe, though multiple threads may
+ * hold cache manager instances for the same cache file. As a result, it is possible multiple
+ * threads may write the same entry to cache. This is fine, as retrieval from the cache will simply
+ * return the first found entry.
+ */
+final class CacheManager {
+
+ private final Path cacheFilePath;
+ private boolean cacheFileTouched;
+
+ /**
+ * @param outputRoot path to the bazel's output root
+ * @param cacheFilename name of the cache file
+ */
+ CacheManager(Path outputRoot, String cacheFilename) {
+ cacheFilePath = outputRoot.getRelative(cacheFilename);
+ }
+
+ private void touchCacheFile() throws IOException {
+ if (!cacheFileTouched) {
+ FileSystemUtils.touchFile(cacheFilePath);
+ cacheFileTouched = true;
+ }
+ }
+
+ /**
+ * Returns the value associated with the given list of string keys from the cache,
+ * or null if the entry is not present in the cache. If there is more than one value for the
+ * given key, the first value is returned.
+ */
+ @Nullable
+ public String getValue(String... keys) throws IOException {
+ Preconditions.checkArgument(keys.length > 0);
+ touchCacheFile();
+
+ List<String> keyList = ImmutableList.copyOf(keys);
+ Iterable<String> cacheContents =
+ FileSystemUtils.readLines(cacheFilePath, StandardCharsets.UTF_8);
+ for (String cacheLine : cacheContents) {
+ if (cacheLine.isEmpty()) {
+ continue;
+ }
+ List<String> cacheEntry = Splitter.on(':').splitToList(cacheLine);
+ List<String> cacheKeys = cacheEntry.subList(0, cacheEntry.size() - 1);
+ String cacheValue = cacheEntry.get(cacheEntry.size() - 1);
+ if (keyList.size() != cacheKeys.size()) {
+ throw new IllegalStateException(
+ String.format("cache file %s is malformed. Expected %s keys. line: '%s'",
+ cacheFilePath, keyList.size(), cacheLine));
+ }
+ if (keyList.equals(cacheKeys)) {
+ return cacheValue;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Write an entry to the cache. An entry consists of one or more keys and a single value.
+ * No validation is made regarding whether there are redundant or conflicting entries in the
+ * cache; it is thus the responsibility of the caller to ensure that any redundant entries
+ * (entries which have the same keys) also have the same value.
+ */
+ public void writeEntry(List<String> keys, String value) throws IOException {
+ Preconditions.checkArgument(!keys.isEmpty());
+
+ touchCacheFile();
+ FileSystemUtils.appendLinesAs(cacheFilePath, StandardCharsets.UTF_8,
+ Joiner.on(":").join(ImmutableList.builder().addAll(keys).add(value).build()));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/apple/XCodeLocalEnvProvider.java b/src/main/java/com/google/devtools/build/lib/exec/apple/XCodeLocalEnvProvider.java
new file mode 100644
index 0000000000..8744ef0186
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/apple/XCodeLocalEnvProvider.java
@@ -0,0 +1,215 @@
+// 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.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+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.util.OS;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * 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 String XCRUN_CACHE_FILENAME = "__xcruncache";
+ private static final String XCODE_LOCATOR_CACHE_FILENAME = "__xcodelocatorcache";
+
+ @Override
+ public Map<String, String> rewriteLocalEnv(
+ Map<String, String> env, Path execRoot, String productName) throws IOException {
+ boolean containsXcodeVersion = env.containsKey(AppleConfiguration.XCODE_VERSION_ENV_NAME);
+ boolean containsAppleSdkVersion =
+ env.containsKey(AppleConfiguration.APPLE_SDK_VERSION_ENV_NAME);
+ if (!containsXcodeVersion && !containsAppleSdkVersion) {
+ return env;
+ }
+
+ ImmutableMap.Builder<String, String> newEnvBuilder = ImmutableMap.builder();
+ newEnvBuilder.putAll(env);
+ // 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), productName);
+ 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(execRoot, developerDir, iosSdkVersion, appleSdkPlatform, productName));
+ }
+ return newEnvBuilder.build();
+ }
+
+ /**
+ * Returns the absolute root path of the target Apple SDK on the host system for a given
+ * version of xcode (as defined by the given {@code developerDir}). This may spawn a
+ * process and use the {@code /usr/bin/xcrun} binary to locate the target SDK. This uses a local
+ * cache file under {@code bazel-out}, and will only spawn a new {@code xcrun} process in the case
+ * of a cache miss.
+ *
+ * @param execRoot the execution root path, used to locate the cache file
+ * @param developerDir the value of {@code DEVELOPER_DIR} for the target version of xcode
+ * @param sdkVersion the sdk version, for example, "9.1"
+ * @param appleSdkPlatform the sdk platform, for example, "iPhoneOS"
+ * @param productName the product name
+ * @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(Path execRoot, String developerDir,
+ String sdkVersion, String appleSdkPlatform, String productName) throws IOException {
+ if (OS.getCurrent() != OS.DARWIN) {
+ throw new IOException("Cannot locate iOS SDK on non-darwin operating system");
+ }
+ try {
+ CacheManager cacheManager =
+ new CacheManager(execRoot.getRelative(
+ BlazeDirectories.getRelativeOutputPath(productName)),
+ XCRUN_CACHE_FILENAME);
+
+ String sdkString = appleSdkPlatform.toLowerCase() + sdkVersion;
+ String cacheResult = cacheManager.getValue(developerDir, sdkString);
+ if (cacheResult != null) {
+ return cacheResult;
+ } else {
+ 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();
+
+ // calling xcrun via Command returns a value with a newline on the end.
+ String sdkRoot = new String(xcrunResult.getStdout(), StandardCharsets.UTF_8).trim();
+
+ cacheManager.writeEntry(ImmutableList.of(developerDir, sdkString), sdkRoot);
+ return sdkRoot;
+ }
+ } 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"
+ + "Stderr: %s",
+ terminationStatus.getExitCode(),
+ sdkVersion, appleSdkPlatform,
+ terminationStatus.toString(),
+ new String(e.getResult().getStderr(), StandardCharsets.UTF_8)));
+ }
+ String message = String.format("xcrun failed.\n%s\n%s",
+ e.getResult().getTerminationStatus(),
+ 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 spawn a process and use the {@code xcode-locator} binary.
+ * This uses a local cache file under {@code bazel-out}, and will only spawn a new process in the
+ * case of a cache miss.
+ *
+ * @param execRoot the execution root path, used to locate the cache file
+ * @param version the xcode version number to look up
+ * @param productName the product name
+ * @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, String productName)
+ throws IOException {
+ if (OS.getCurrent() != OS.DARWIN) {
+ throw new IOException(
+ "Cannot locate xcode developer directory on non-darwin operating system");
+ }
+ try {
+ CacheManager cacheManager =
+ new CacheManager(
+ execRoot.getRelative(BlazeDirectories.getRelativeOutputPath(productName)),
+ XCODE_LOCATOR_CACHE_FILENAME);
+
+ String cacheResult = cacheManager.getValue(version.toString());
+ if (cacheResult != null) {
+ return cacheResult;
+ } else {
+ CommandResult xcodeLocatorResult = new Command(new String[] {
+ execRoot.getRelative("_bin/xcode-locator").getPathString(), version.toString()})
+ .execute();
+
+ String developerDir =
+ new String(xcodeLocatorResult.getStdout(), StandardCharsets.UTF_8).trim();
+
+ cacheManager.writeEntry(ImmutableList.of(version.toString()), developerDir);
+ return developerDir;
+ }
+ } catch (AbnormalTerminationException e) {
+ TerminationStatus terminationStatus = e.getResult().getTerminationStatus();
+
+ String message;
+ if (e.getResult().getTerminationStatus().exited()) {
+ message = String.format("xcode-locator failed with code %s.\n"
+ + "This most likely indicates that xcode version %s is not available on the host "
+ + "machine.\n"
+ + "%s\n"
+ + "stderr: %s",
+ terminationStatus.getExitCode(),
+ version,
+ terminationStatus.toString(),
+ new String(e.getResult().getStderr(), StandardCharsets.UTF_8));
+ } else {
+ message = String.format("xcode-locator failed. %s\nstderr: %s",
+ e.getResult().getTerminationStatus(),
+ new String(e.getResult().getStderr(), StandardCharsets.UTF_8));
+ }
+ throw new IOException(message, e);
+ } catch (CommandException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/local/LocalEnvProvider.java b/src/main/java/com/google/devtools/build/lib/exec/local/LocalEnvProvider.java
new file mode 100644
index 0000000000..52610761c5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/exec/local/LocalEnvProvider.java
@@ -0,0 +1,36 @@
+// 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.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Allows just-in-time rewriting of the environment used for local actions. Do not use! This class
+ * probably should not exist, but is currently necessary for our local MacOS support.
+ */
+public interface LocalEnvProvider {
+ public static final LocalEnvProvider UNMODIFIED = new LocalEnvProvider() {
+ @Override
+ public Map<String, String> rewriteLocalEnv(
+ Map<String, String> env, Path execRoot, String productName) throws IOException {
+ return env;
+ }
+ };
+
+ /** Rewrites the environment if necessary. */
+ Map<String, String> rewriteLocalEnv(Map<String, String> env, Path execRoot, String productName)
+ throws IOException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
index 215ae2ef14..418646b1cd 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/local/LocalSpawnRunner.java
@@ -34,7 +34,6 @@ 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.util.NetUtil;
-import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.io.FileOutErr;
import com.google.devtools.build.lib.vfs.Path;
@@ -75,6 +74,9 @@ public final class LocalSpawnRunner implements SpawnRunner {
private final boolean useProcessWrapper;
private final String processWrapper;
+ private final String productName;
+ private final LocalEnvProvider localEnvProvider;
+
public LocalSpawnRunner(
Logger logger,
AtomicInteger execCount,
@@ -82,7 +84,9 @@ public final class LocalSpawnRunner implements SpawnRunner {
ActionInputPrefetcher actionInputPrefetcher,
LocalExecutionOptions localExecutionOptions,
ResourceManager resourceManager,
- boolean useProcessWrapper) {
+ boolean useProcessWrapper,
+ String productName,
+ LocalEnvProvider localEnvProvider) {
this.logger = logger;
this.execRoot = execRoot;
this.actionInputPrefetcher = Preconditions.checkNotNull(actionInputPrefetcher);
@@ -92,25 +96,8 @@ public final class LocalSpawnRunner implements SpawnRunner {
this.execCount = execCount;
this.resourceManager = resourceManager;
this.useProcessWrapper = useProcessWrapper;
- }
-
- public LocalSpawnRunner(
- Path execRoot,
- ActionInputPrefetcher actionInputPrefetcher,
- LocalExecutionOptions localExecutionOptions,
- ResourceManager resourceManager) {
- this(
- null,
- new AtomicInteger(),
- execRoot,
- actionInputPrefetcher,
- localExecutionOptions,
- resourceManager,
- // TODO(bazel-team): process-wrapper seems to work on Windows, but requires additional setup
- // as it is an msys2 binary, so it needs msys2 DLLs on %PATH%. Disable it for now to make
- // the setup easier and to avoid further PATH hacks. Ideally we should have a native
- // implementation of process-wrapper for Windows.
- OS.getCurrent() != OS.WINDOWS);
+ this.productName = productName;
+ this.localEnvProvider = localEnvProvider;
}
@Override
@@ -247,14 +234,14 @@ public final class LocalSpawnRunner implements SpawnRunner {
cmdLine.addAll(spawn.getArguments());
cmd = new Command(
cmdLine.toArray(new String[0]),
- spawn.getEnvironment(),
+ localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName),
execRoot.getPathFile());
} else {
stdOut = outErr.getOutputStream();
stdErr = outErr.getErrorStream();
cmd = new Command(
spawn.getArguments().toArray(new String[0]),
- spawn.getEnvironment(),
+ localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), execRoot, productName),
execRoot.getPathFile(),
policy.getTimeoutMillis());
}