// Copyright 2014 Google Inc. 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.analysis.config; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.devtools.build.lib.actions.ArtifactFactory; import com.google.devtools.build.lib.actions.PackageRootResolver; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.analysis.BlazeDirectories; import com.google.devtools.build.lib.analysis.ViewCreationFailedException; import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection.Transitions; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.Attribute.Configurator; import com.google.devtools.build.lib.packages.Attribute.SplitTransition; import com.google.devtools.build.lib.packages.Attribute.Transition; import com.google.devtools.build.lib.packages.InputFile; import com.google.devtools.build.lib.packages.PackageGroup; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.packages.RuleClass; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.rules.test.TestActionBuilder; import com.google.devtools.build.lib.syntax.Label; import com.google.devtools.build.lib.syntax.Label.SyntaxException; import com.google.devtools.build.lib.syntax.SkylarkCallable; import com.google.devtools.build.lib.syntax.SkylarkModule; import com.google.devtools.build.lib.util.Fingerprint; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.RegexFilter; import com.google.devtools.build.lib.util.StringUtilities; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunction.Environment; import com.google.devtools.common.options.Converter; import com.google.devtools.common.options.Converters; import com.google.devtools.common.options.EnumConverter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParsingException; import com.google.devtools.common.options.TriState; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import javax.annotation.Nullable; /** * Instances of BuildConfiguration represent a collection of context * information which may affect a build (for example: the target platform for * compilation, or whether or not debug tables are required). In fact, all * "environmental" information (e.g. from the tool's command-line, as opposed * to the BUILD file) that can affect the output of any build tool should be * explicitly represented in the BuildConfiguration instance. * *

A single build may require building tools to run on a variety of * platforms: when compiling a server application for production, we must build * the build tools (like compilers) to run on the host platform, but cross-compile * the application for the production environment. * *

There is always at least one BuildConfiguration instance in any build: * the one representing the host platform. Additional instances may be created, * in a cross-compilation build, for example. * *

Instances of BuildConfiguration are canonical: *

c1.equals(c2) <=> c1==c2.
*/ @SkylarkModule(name = "configuration", doc = "Data required for the analysis of a target that comes from targets that " + "depend on it and not targets that it depends on.") public final class BuildConfiguration implements Serializable { /** * An interface for language-specific configurations. */ public abstract static class Fragment implements Serializable { /** * Returns a human-readable name of the configuration fragment. */ public abstract String getName(); /** * Validates the options for this Fragment. Issues warnings for the * use of deprecated options, and warnings or errors for any option settings * that conflict. */ @SuppressWarnings("unused") public void reportInvalidOptions(EventHandler reporter, BuildOptions buildOptions) { } /** * Adds mapping of names to values of "Make" variables defined by this configuration. */ @SuppressWarnings("unused") public void addGlobalMakeVariables(ImmutableMap.Builder globalMakeEnvBuilder) { } /** * Collects all labels that should be implicitly loaded from labels that were specified as * options, keyed by the name to be displayed to the user if something goes wrong. * The resulting set only contains labels that were derived from command-line options; the * intention is that it can be used to sanity-check that the command-line options actually * contain these in their transitive closure. */ @SuppressWarnings("unused") public void addImplicitLabels(Multimap implicitLabels) { } /** * Returns a string that identifies the configuration fragment. */ public abstract String cacheKey(); /** * The fragment may use this hook to perform I/O and read data into memory that is used during * analysis. During the analysis phase disk I/O operations are disallowed. * *

This hook is only called for the top-level configuration after the loading phase is * complete. */ @SuppressWarnings("unused") public void prepareHook(Path execPath, ArtifactFactory artifactFactory, PathFragment genfilesPath, PackageRootResolver resolver) throws ViewCreationFailedException { } /** * Adds all the roots from this fragment. */ @SuppressWarnings("unused") public void addRoots(List roots) { } /** * Returns a (key, value) mapping to insert into the subcommand environment for coverage. */ public Map getCoverageEnvironment() { return ImmutableMap.of(); } /* * Returns the command-line "Make" variable overrides. */ public ImmutableMap getCommandLineDefines() { return ImmutableMap.of(); } /** * Returns all the coverage labels for the fragment. */ public ImmutableList

This detects the host cpu of the Blaze's server but if the compilation happens in a * compilation cluster then the host cpu of the compilation cluster might be different than * the auto-detected one and the --host_cpu option must then be set explicitly. */ public static class HostCpuConverter implements Converter { @Override public String convert(String input) throws OptionsParsingException { if (input.isEmpty()) { switch (OS.getCurrent()) { case DARWIN: return "darwin"; default: return "k8"; } } return input; } @Override public String getTypeDescription() { return "a string"; } } /** * Options that affect the value of a BuildConfiguration instance. * *

(Note: any client that creates a view will also need to declare * BuildView.Options, which affect the mechanism of view construction, * even if they don't affect the value of the BuildConfiguration instances.) * *

IMPORTANT: when adding new options, be sure to consider whether those * values should be propagated to the host configuration or not (see * {@link ConfigurationFactory#getConfiguration}. * *

ALSO IMPORTANT: all option types MUST define a toString method that * gives identical results for semantically identical option values. The * simplest way to ensure that is to return the input string. */ public static class Options extends FragmentOptions implements Cloneable { public String getCpu() { return cpu; } @Option(name = "cpu", defaultValue = "null", category = "semantics", help = "The target CPU.") public String cpu; @Option(name = "min_param_file_size", defaultValue = "32768", category = "undocumented", help = "Minimum command line length before creating a parameter file.") public int minParamFileSize; @Option(name = "experimental_extended_sanity_checks", defaultValue = "false", category = "undocumented", help = "Enables internal validation checks to make sure that configured target " + "implementations only access things they should. Causes a performance hit.") public boolean extendedSanityChecks; @Option(name = "experimental_allow_runtime_deps_on_neverlink", defaultValue = "true", category = "undocumented", help = "Flag to help transition from allowing to disallowing runtime_deps on neverlink" + " Java archives. The depot needs to be cleaned up to roll this out by default.") public boolean allowRuntimeDepsOnNeverLink; @Option(name = "strict_filesets", defaultValue = "false", category = "semantics", help = "If this option is enabled, filesets crossing package boundaries are reported " + "as errors. It does not work when check_fileset_dependencies_recursively is " + "disabled.") public boolean strictFilesets; // Plugins are build using the host config. To avoid cycles we just don't propagate // this option to the host config. If one day we decide to use plugins when building // host tools, we can improve this by (for example) creating a compiler configuration that is // used only for building plugins. @Option(name = "plugin", converter = LabelConverter.class, allowMultiple = true, defaultValue = "", category = "flags", help = "Plugins to use in the build. Currently works with java_plugin.") public List

This can be used to: *

    *
  1. Find an option's (parsed) value given its command-line name
  2. *
  3. Parse alternative values for the option.
  4. *
* *

This map is "transitive" in that it includes *all* options recognizable by this * configuration, including those defined in child fragments. */ private final Map transitiveOptionsMap; /** * Validates the options for this BuildConfiguration. Issues warnings for the * use of deprecated options, and warnings or errors for any option settings * that conflict. */ public void reportInvalidOptions(EventHandler reporter) { for (Fragment fragment : fragments.values()) { fragment.reportInvalidOptions(reporter, this.buildOptions); } Set plugins = new HashSet<>(); for (Label plugin : options.pluginList) { String name = plugin.getName(); if (plugins.contains(name)) { reporter.handle(Event.error("A build cannot have two plugins with the same name")); } plugins.add(name); } for (Map.Entry opt : options.pluginCoptList) { if (!plugins.contains(opt.getKey())) { reporter.handle(Event.error("A plugin_copt must refer to an existing plugin")); } } if (options.shortName != null) { reporter.handle(Event.error( "The internal '--configuration short name' option cannot be used on the command line")); } if (options.testShardingStrategy == TestActionBuilder.TestShardingStrategy.EXPERIMENTAL_HEURISTIC) { reporter.handle(Event.warn( "Heuristic sharding is intended as a one-off experimentation tool for determing the " + "benefit from sharding certain tests. Please don't keep this option in your " + ".blazerc or continuous build")); } } private ImmutableMap setupShellEnvironment() { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); for (Fragment fragment : fragments.values()) { fragment.setupShellEnvironment(builder); } return builder.build(); } BuildConfiguration(BlazeDirectories directories, Map, Fragment> fragmentsMap, BuildOptions buildOptions, Map clientEnv, boolean actionsDisabled) { this.actionsEnabled = !actionsDisabled; fragments = ImmutableMap.copyOf(fragmentsMap); // This is a view that will be updated upon each client command. this.clientEnvironment = ImmutableMap.copyOf(clientEnv); this.buildOptions = buildOptions; this.options = buildOptions.get(Options.class); this.mnemonic = buildMnemonic(); String outputDirName = (options.shortName != null) ? options.shortName : mnemonic; this.shortName = buildShortName(outputDirName); this.platformName = buildPlatformName(); this.shExecutable = collectExecutables().get("sh"); this.outputRoots = new OutputRoots(directories, outputDirName); ImmutableSet.Builder

optionName is the name of the option as it appears on the command line * e.g. {@link Option#name}). */ Class getOptionClass(String optionName) { OptionDetails optionData = transitiveOptionsMap.get(optionName); return optionData == null ? null : optionData.optionsClass; } /** * Returns the value of the specified option for this configuration or null if the * option isn't recognized. Since an option's legitimate value could be null, use * {@link #getOptionClass} to distinguish between that and an unknown option. * *

optionName is the name of the option as it appears on the command line * e.g. {@link Option#name}). */ Object getOptionValue(String optionName) { OptionDetails optionData = transitiveOptionsMap.get(optionName); return (optionData == null) ? null : optionData.value; } /** * Returns whether or not the given option supports multiple values at the command line (e.g. * "--myoption value1 --myOption value2 ..."). Returns false for unrecognized options. Use * {@link #getOptionClass} to distinguish between those and legitimate single-value options. * *

As declared in {@link Option#allowMultiple}, multi-value options are expected to be * of type {@code List}. */ boolean allowsMultipleValues(String optionName) { OptionDetails optionData = transitiveOptionsMap.get(optionName); return (optionData == null) ? false : optionData.allowsMultiple; } /** * The platform string, suitable for use as a key into a MakeEnvironment. */ public String getPlatformName() { return platformName; } /** * Returns the output directory for this build configuration. */ public Root getOutputDirectory() { return outputRoots.outputDirectory; } /** * Returns the bin directory for this build configuration. */ @SkylarkCallable(name = "bin_dir", structField = true, doc = "The root corresponding to bin directory.") public Root getBinDirectory() { return outputRoots.binDirectory; } /** * Returns a relative path to the bin directory at execution time. */ public PathFragment getBinFragment() { return getBinDirectory().getExecPath(); } /** * Returns the include directory for this build configuration. */ public Root getIncludeDirectory() { return outputRoots.includeDirectory; } /** * Returns the genfiles directory for this build configuration. */ @SkylarkCallable(name = "genfiles_dir", structField = true, doc = "The root corresponding to genfiles directory.") public Root getGenfilesDirectory() { return outputRoots.genfilesDirectory; } /** * Returns the directory where coverage-related artifacts and metadata files * should be stored. This includes for example uninstrumented class files * needed for Jacoco's coverage reporting tools. */ public Root getCoverageMetadataDirectory() { return outputRoots.coverageMetadataDirectory; } /** * Returns the testlogs directory for this build configuration. */ public Root getTestLogsDirectory() { return outputRoots.testLogsDirectory; } /** * Returns a relative path to the genfiles directory at execution time. */ public PathFragment getGenfilesFragment() { return getGenfilesDirectory().getExecPath(); } /** * Returns the path separator for the host platform. This is basically the same as {@link * java.io.File#pathSeparator}, except that that returns the value for this JVM, which may or may * not match the host platform. You should only use this when invoking tools that are known to use * the native path separator, i.e., the path separator for the machine that they run on. */ @SkylarkCallable(name = "host_path_separator", structField = true, doc = "Returns the separator for PATH environment variable, which is ':' on Unix.") public String getHostPathSeparator() { // TODO(bazel-team): This needs to change when we support Windows. return ":"; } /** * Returns the internal directory (used for middlemen) for this build configuration. */ public Root getMiddlemanDirectory() { return outputRoots.middlemanDirectory; } public boolean getAllowRuntimeDepsOnNeverLink() { return options.allowRuntimeDepsOnNeverLink; } public boolean isStrictFilesets() { return options.strictFilesets; } public List

The intention here is that we use this string as the directory name * for artifacts of this build. * *

For configuration settings which are NOT part of the short name, * rebuilding with a different value of such a setting will build in * the same output directory. This means that any actions whose * keys (see Action.getKey()) have changed will be rerun. That * may result in a lot of recompilation. * *

For configuration settings which ARE part of the short name, * rebuilding with a different value of such a setting will rebuild * in a different output directory; this will result in higher disk * usage and more work the _first_ time you rebuild with a different * setting, but will result in less work if you regularly switch * back and forth between different settings. * *

With one important exception, it's sound to choose any subset of the * config's components for this string, it just alters the dimensionality * of the cache. In other words, it's a trade-off on the "injectiveness" * scale: at one extreme (shortName is in fact a complete fingerprint, and * thus injective) you get extremely precise caching (no competition for the * same output-file locations) but you have to rebuild for even the * slightest change in configuration. At the other extreme * (PartialFingerprint is a constant) you have very high competition for * output-file locations, but if a slight change in configuration doesn't * affect a particular build step, you're guaranteed not to have to * rebuild it. The important exception has to do with cross-compilation: * the host and target configurations must not map to the same output * directory, because then files would need to get built for the host * and then rebuilt for the target even within a single build, and that * wouldn't work. * *

Just to re-iterate: cross-compilation builds (i.e. hostConfig != * targetConfig) will not work if the two configurations' short names are * equal. This is an important practical case: the mere addition of * a compile flag to the target configuration would cause the build to * fail. In other words, it would break if the host and target * configurations are not identical but are "too close". The current * solution is to set the host configuration equal to the target * configuration if they are "too close"; this may cause the tools to get * rebuild for the new host configuration though. */ public String getShortName() { return shortName; } /** * Like getShortName(), but always returns a configuration-dependent string even for * the host configuration. */ public String getMnemonic() { return mnemonic; } @Override public String toString() { return getShortName(); } /** * Returns the default shell environment */ @SkylarkCallable(name = "default_shell_env", structField = true, doc = "A dictionary representing the default environment. It maps variables " + "to their values (strings).") public ImmutableMap getDefaultShellEnvironment() { return defaultShellEnvironment; } /** * Returns the path to sh. */ public PathFragment getShExecutable() { return shExecutable; } /** * Returns a regex-based instrumentation filter instance that used to match label * names to identify targets to be instrumented in the coverage mode. */ public RegexFilter getInstrumentationFilter() { return options.instrumentationFilter; } /** * Returns the set of labels for coverage. */ public Set

This does *not* include package-defined overrides (e.g. vardef) * and so should not be used by the build logic. This is used only for * the 'info' command. * *

Command-line definitions of make enviroments override variables defined by * {@code Fragment.addGlobalMakeVariables()}. */ public Map getMakeEnvironment() { Map makeEnvironment = new HashMap<>(); makeEnvironment.putAll(globalMakeEnv); for (Fragment fragment : fragments.values()) { makeEnvironment.putAll(fragment.getCommandLineDefines()); } return ImmutableMap.copyOf(makeEnvironment); } /** * Returns a new, unordered mapping of names that are set through the command lines. * (Fragments, in particular the Google C++ support, can set variables through the * command line.) */ public Map getCommandLineDefines() { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Fragment fragment : fragments.values()) { builder.putAll(fragment.getCommandLineDefines()); } return builder.build(); } /** * Returns the global defaults for this configuration for the Make environment. */ public Map getGlobalMakeEnvironment() { return globalMakeEnv; } /** * Returns a (key, value) mapping to insert into the subcommand environment for coverage * actions. */ public Map getCoverageEnvironment() { Map env = new HashMap<>(); for (Fragment fragment : fragments.values()) { env.putAll(fragment.getCoverageEnvironment()); } return env; } /** * Returns the default value for the specified "Make" variable for this * configuration. Returns null if no value was found. */ public String getMakeVariableDefault(String var) { return globalMakeEnv.get(var); } /** * Returns a configuration fragment instances of the given class. */ @SkylarkCallable(name = "fragment", hidden = true, doc = "Returns a configuration fragment using the key.") public T getFragment(Class clazz) { return clazz.cast(fragments.get(clazz)); } /** * Returns true if the requested configuration fragment is present. */ public boolean hasFragment(Class clazz) { return getFragment(clazz) != null; } /** * Returns true if all requested configuration fragment are present (this may be slow). */ public boolean hasAllFragments(Set> fragmentClasses) { for (Class fragmentClass : fragmentClasses) { if (!hasFragment(fragmentClass.asSubclass(Fragment.class))) { return false; } } return true; } /** * Returns true if non-functional build stamps are enabled. */ public boolean stampBinaries() { return options.stampBinaries; } /** * Returns true if extended sanity checks should be enabled. */ public boolean extendedSanityChecks() { return options.extendedSanityChecks; } /** * Returns true if we are building runfiles symlinks for this configuration. */ public boolean buildRunfiles() { return options.buildRunfiles; } public boolean getCheckFilesetDependenciesRecursively() { return options.checkFilesetDependenciesRecursively; } public List getTestArguments() { return options.testArguments; } public String getTestFilter() { return options.testFilter; } /** * Returns user-specified test environment variables and their values, as * set by the --test_env options. */ public Map getTestEnv() { return getTestEnv(options.testEnvironment, clientEnvironment); } /** * Returns user-specified test environment variables and their values, as * set by the --test_env options. * * @param envOverrides The --test_env flag values. * @param clientEnvironment The full client environment. */ public static Map getTestEnv(List> envOverrides, Map clientEnvironment) { Map testEnv = new HashMap<>(); for (Map.Entry var : envOverrides) { if (var.getValue() != null) { testEnv.put(var.getKey(), var.getValue()); } else { String value = clientEnvironment.get(var.getKey()); if (value != null) { testEnv.put(var.getKey(), value); } } } return testEnv; } public TriState cacheTestResults() { return options.cacheTestResults; } public int getMinParamFileSize() { return options.minParamFileSize; } @SkylarkCallable(name = "coverage_enabled", structField = true, doc = "A boolean that tells whether code coverage is enabled.") public boolean isCodeCoverageEnabled() { return options.collectCodeCoverage; } public boolean isMicroCoverageEnabled() { return options.collectMicroCoverage; } public boolean isActionsEnabled() { return actionsEnabled; } public TestActionBuilder.TestShardingStrategy testShardingStrategy() { return options.testShardingStrategy; } /** * @return number of times the given test should run. * If the test doesn't match any of the filters, runs it once. */ public int getRunsPerTestForLabel(Label label) { for (PerLabelOptions perLabelRuns : options.runsPerTest) { if (perLabelRuns.isIncluded(label)) { return Integer.parseInt(Iterables.getOnlyElement(perLabelRuns.getOptions())); } } return 1; } public RunUnder getRunUnder() { return options.runUnder; } /** * Returns true if this is a host configuration. */ public boolean isHostConfiguration() { return options.isHost; } public boolean checkVisibility() { return options.checkVisibility; } public boolean checkLicenses() { return options.checkLicenses; } public boolean enforceConstraints() { return options.enforceConstraints; } public List

The string uniquely identifies the configuration. As a result, it can be rather long and * include spaces and other non-alphanumeric characters. If you need a shorter key, use * {@link #shortCacheKey()}. * * @see #computeCacheKey */ public final String cacheKey() { return cacheKey; } /** * Returns a (relatively) short key that identifies the configuration. * *

The short key is the short name of the configuration concatenated with a hash of the * {@link #cacheKey()}. */ public final String shortCacheKey() { return shortCacheKey; } /** Returns a copy of the build configuration options for this configuration. */ public BuildOptions cloneOptions() { return buildOptions.clone(); } /** * Prepare the fdo support. It reads data into memory that is used during analysis. The analysis * phase is generally not allowed to perform disk I/O. This code is here because it is * conceptually part of the analysis phase, and it needs to happen when the loading phase is * complete. */ public void prepareToBuild(Path execRoot, ArtifactFactory artifactFactory, PackageRootResolver resolver) throws ViewCreationFailedException { for (Fragment fragment : fragments.values()) { fragment.prepareHook(execRoot, artifactFactory, getGenfilesFragment(), resolver); } } /** * Declares dependencies on any relevant Skyframe values (for example, relevant FileValues). */ public void declareSkyframeDependencies(SkyFunction.Environment env) { for (Fragment fragment : fragments.values()) { fragment.declareSkyframeDependencies(env); } } /** * Returns all the roots for this configuration. */ public List getRoots() { List roots = new ArrayList<>(); // Configuration-specific roots. roots.add(getBinDirectory()); roots.add(getGenfilesDirectory()); roots.add(getIncludeDirectory()); roots.add(getMiddlemanDirectory()); roots.add(getTestLogsDirectory()); // Fragment-defined roots for (Fragment fragment : fragments.values()) { fragment.addRoots(roots); } return ImmutableList.copyOf(roots); } public ListMultimap getAllLabels() { return buildOptions.getAllLabels(); } public String getCpu() { return options.cpu; } /** * Returns true is incremental builds are supported with this configuration. */ public boolean supportsIncrementalBuild() { for (Fragment fragment : fragments.values()) { if (!fragment.supportsIncrementalBuild()) { return false; } } return true; } /** * Returns true if the configuration performs static linking. */ public boolean performsStaticLink() { for (Fragment fragment : fragments.values()) { if (fragment.performsStaticLink()) { return true; } } return false; } /** * Deletes temporary directories before execution phase. This is only called for * target configuration. */ public void prepareForExecutionPhase() throws IOException { for (Fragment fragment : fragments.values()) { fragment.prepareForExecutionPhase(); } } /** * Collects executables defined by fragments. */ private ImmutableMap collectExecutables() { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); for (Fragment fragment : fragments.values()) { fragment.defineExecutables(builder); } return builder.build(); } /** * See {@code BuildConfigurationCollection.Transitions.getArtifactOwnerConfiguration()}. */ public BuildConfiguration getArtifactOwnerConfiguration() { return transitions.getArtifactOwnerConfiguration(); } /** * @return whether proto header modules should be built. */ public boolean getProtoHeaderModules() { return options.protoHeaderModules; } /** * @return the list of default features used for all packages. */ public List getDefaultFeatures() { return options.defaultFeatures; } /** * Returns the "top-level" environment space, i.e. the set of environments all top-level * targets must be compatible with. An empty value implies no restrictions. */ public List