blob: 6c0e0e663754c68e0434cd71083c4434534782b0 [file] [log] [blame]
// Copyright 2014 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.rules.python;
import com.google.common.base.Ascii;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.analysis.config.FragmentOptions;
import com.google.devtools.common.options.Converter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionMetadataTag;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.TriState;
import java.util.Map;
/**
* Python-related command-line options.
*
* <p>Due to the migration of the Python version API (see #6583) and the default Python version (see
* (see #6647), the Python major version mode ({@code PY2} vs {@code PY3}) is a function of multiple
* flags. See {@link #getPythonVersion} for more details.
*/
public class PythonOptions extends FragmentOptions {
/** Converter for options that take ({@code PY2} or {@code PY3}). */
// We don't use EnumConverter because we want to disallow non-target PythonVersion values.
public static class TargetPythonVersionConverter implements Converter<PythonVersion> {
@Override
public PythonVersion convert(String input) throws OptionsParsingException {
try {
// Although in rule attributes the enum values are case sensitive, the convention from
// EnumConverter is that the options parser is case insensitive.
input = Ascii.toUpperCase(input);
return PythonVersion.parseTargetValue(input);
} catch (IllegalArgumentException ex) {
throw new OptionsParsingException(
"Not a valid Python major version, should be PY2 or PY3", ex);
}
}
@Override
public String getTypeDescription() {
return "PY2 or PY3";
}
}
@Option(
name = "build_python_zip",
defaultValue = "auto",
documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
help = "Build python executable zip; on on Windows, off on other platforms")
public TriState buildPythonZip;
/**
* Deprecated machinery for setting the Python version; will be removed soon.
*
* <p>Not GraveyardOptions'd because we'll delete this alongside other soon-to-be-removed options
* in this file.
*/
@Option(
name = "incompatible_remove_old_python_version_api",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help = "No-op, will be removed soon.")
public boolean incompatibleRemoveOldPythonVersionApi;
/**
* Deprecated machinery for setting the Python version; will be removed soon.
*
* <p>Not GraveyardOptions'd because we'll delete this alongside other soon-to-be-removed options
* in this file.
*/
@Option(
name = "incompatible_allow_python_version_transitions",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help = "No-op, will be removed soon.")
public boolean incompatibleAllowPythonVersionTransitions;
/**
* Native rule logic should call {@link #getDefaultPythonVersion} instead of accessing this option
* directly.
*/
@Option(
name = "incompatible_py3_is_default",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
effectTags = {
OptionEffectTag.LOADING_AND_ANALYSIS,
OptionEffectTag.AFFECTS_OUTPUTS // because of "-py2"/"-py3" output root
},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help =
"If true, `py_binary` and `py_test` targets that do not set their `python_version` (or "
+ "`default_python_version`) attribute will default to PY3 rather than to PY2. If "
+ "you set this flag it is also recommended to set "
+ "`--incompatible_py2_outputs_are_suffixed`.")
public boolean incompatiblePy3IsDefault;
@Option(
name = "incompatible_py2_outputs_are_suffixed",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help =
"If true, targets built in the Python 2 configuration will appear under an output root "
+ "that includes the suffix '-py2', while targets built for Python 3 will appear "
+ "in a root with no Python-related suffix. This means that the `bazel-bin` "
+ "convenience symlink will point to Python 3 targets rather than Python 2. "
+ "If you enable this option it is also recommended to enable "
+ "`--incompatible_py3_is_default`.")
public boolean incompatiblePy2OutputsAreSuffixed;
/**
* This field should be either null (unset), {@code PY2}, or {@code PY3}. Other {@code
* PythonVersion} values do not represent distinct Python versions and are not allowed.
*
* <p>Native rule logic should call {@link #getPythonVersion} / {@link #setPythonVersion} instead
* of accessing this option directly. BUILD/.bzl code should {@code select()} on {@code <tools
* repo>//tools/python:python_version} rather than on this option directly.
*/
@Option(
name = "python_version",
defaultValue = "null",
converter = TargetPythonVersionConverter.class,
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
effectTags = {
OptionEffectTag.LOADING_AND_ANALYSIS,
OptionEffectTag.AFFECTS_OUTPUTS // because of "-py2"/"-py3" output root
},
help =
"The Python major version mode, either `PY2` or `PY3`. Note that this is overridden by "
+ "`py_binary` and `py_test` targets (even if they don't explicitly specify a "
+ "version) so there is usually not much reason to supply this flag.")
public PythonVersion pythonVersion;
private static final OptionDefinition PYTHON_VERSION_DEFINITION =
OptionsParser.getOptionDefinitionByName(PythonOptions.class, "python_version");
/**
* Deprecated machinery for setting the Python version; will be removed soon.
*
* <p>Not in GraveyardOptions because we still want to prohibit users from select()ing on it.
*/
@Option(
name = "force_python",
defaultValue = "null",
converter = TargetPythonVersionConverter.class,
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.AFFECTS_OUTPUTS},
help = "No-op, will be removed soon.")
public PythonVersion forcePython;
private static final OptionDefinition FORCE_PYTHON_DEFINITION =
OptionsParser.getOptionDefinitionByName(PythonOptions.class, "force_python");
/**
* This field should be either null (unset), {@code PY2}, or {@code PY3}. Other {@code
* PythonVersion} values do not represent distinct Python versions and are not allowed.
*
* <p>Null means to use the default ({@link #getDefaultPythonVersion}).
*
* <p>This option is only read by {@link #getHost}. It should not be read by other native code or
* by {@code select()}s in user code.
*/
@Option(
name = "host_force_python",
defaultValue = "null",
converter = TargetPythonVersionConverter.class,
documentationCategory = OptionDocumentationCategory.OUTPUT_PARAMETERS,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.AFFECTS_OUTPUTS},
help = "Overrides the Python version for the host configuration. Can be \"PY2\" or \"PY3\".")
public PythonVersion hostForcePython;
private static final OptionDefinition HOST_FORCE_PYTHON_DEFINITION =
OptionsParser.getOptionDefinitionByName(PythonOptions.class, "host_force_python");
@Option(
name = "incompatible_disallow_legacy_py_provider",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.STARLARK_SEMANTICS,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help =
"If set to true, native Python rules will neither produce nor consume the legacy \"py\" "
+ "provider. Use PyInfo instead. Under this flag, passing the legacy provider to a "
+ "Python target will be an error.")
public boolean incompatibleDisallowLegacyPyProvider;
@Option(
name = "incompatible_use_python_toolchains",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help =
"If set to true, executable native Python rules will use the Python runtime specified by "
+ "the Python toolchain, rather than the runtime given by legacy flags like "
+ "--python_top.")
public boolean incompatibleUsePythonToolchains;
@Option(
name = "incompatible_load_python_rules_from_bzl",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES,
},
help =
"If enabled, direct use of the native Python rules is disabled. Please use the Starlark "
+ "rules instead: https://github.com/bazelbuild/rules_python. See also "
+ "https://github.com/bazelbuild/bazel/issues/9006.")
public boolean loadPythonRulesFromBzl;
@Option(
name = "experimental_build_transitive_python_runfiles",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.AFFECTS_OUTPUTS},
help =
"Build the runfiles trees of py_binary targets that appear in the transitive "
+ "data runfiles of another binary.")
public boolean buildTransitiveRunfilesTrees;
@Option(
name = "incompatible_default_to_explicit_init_py",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.GENERIC_INPUTS,
effectTags = {OptionEffectTag.AFFECTS_OUTPUTS},
metadataTags = {
OptionMetadataTag.INCOMPATIBLE_CHANGE,
OptionMetadataTag.TRIGGERED_BY_ALL_INCOMPATIBLE_CHANGES
},
help =
"This flag changes the default behavior so that __init__.py files are no longer "
+ "automatically created in the runfiles of Python targets. Precisely, when a "
+ "py_binary or py_test target has legacy_create_init set to \"auto\" (the default), "
+ "it is treated as false if and only if this flag is set. See "
+ "https://github.com/bazelbuild/bazel/issues/10076.")
public boolean incompatibleDefaultToExplicitInitPy;
@Override
public Map<OptionDefinition, SelectRestriction> getSelectRestrictions() {
// TODO(brandjon): Instead of referencing the python_version target, whose path depends on the
// tools repo name, reference a standalone documentation page instead.
ImmutableMap.Builder<OptionDefinition, SelectRestriction> restrictions = ImmutableMap.builder();
restrictions.put(
PYTHON_VERSION_DEFINITION,
new SelectRestriction(
/*visibleWithinToolsPackage=*/ true,
"Use @bazel_tools//python/tools:python_version instead."));
restrictions.put(
FORCE_PYTHON_DEFINITION,
new SelectRestriction(
/*visibleWithinToolsPackage=*/ true,
"Use @bazel_tools//python/tools:python_version instead."));
restrictions.put(
HOST_FORCE_PYTHON_DEFINITION,
new SelectRestriction(
/*visibleWithinToolsPackage=*/ false,
"Use @bazel_tools//python/tools:python_version instead."));
return restrictions.build();
}
/**
* Returns the Python major version ({@code PY2} or {@code PY3}) that targets that do not specify
* a version should be built for.
*/
public PythonVersion getDefaultPythonVersion() {
return incompatiblePy3IsDefault ? PythonVersion.PY3 : PythonVersion.PY2;
}
/**
* Returns the Python major version ({@code PY2} or {@code PY3}) that targets should be built for.
*
* <p>The version is taken as the value of {@code --python_version} if not null, otherwise {@link
* #getDefaultPythonVersion}.
*/
public PythonVersion getPythonVersion() {
if (pythonVersion != null) {
return pythonVersion;
} else {
return getDefaultPythonVersion();
}
}
/**
* Returns whether a Python version transition to {@code version} is not a no-op.
*
* @throws IllegalArgumentException if {@code version} is not {@code PY2} or {@code PY3}
*/
public boolean canTransitionPythonVersion(PythonVersion version) {
Preconditions.checkArgument(version.isTargetValue());
return !version.equals(getPythonVersion());
}
/**
* Sets the Python version to {@code version}.
*
* <p>Since this is a mutation, it should only be called on a newly constructed instance.
*
* @throws IllegalArgumentException if {@code version} is not {@code PY2} or {@code PY3}
*/
// TODO(brandjon): Consider removing this mutator now that the various flags and semantics it
// used to consider are gone. We'd revert to just setting the public option field directly.
public void setPythonVersion(PythonVersion version) {
Preconditions.checkArgument(version.isTargetValue());
this.pythonVersion = version;
}
@Override
public FragmentOptions getHost() {
PythonOptions hostPythonOptions = (PythonOptions) getDefault();
PythonVersion hostVersion =
(hostForcePython != null) ? hostForcePython : getDefaultPythonVersion();
hostPythonOptions.setPythonVersion(hostVersion);
hostPythonOptions.incompatiblePy3IsDefault = incompatiblePy3IsDefault;
hostPythonOptions.incompatiblePy2OutputsAreSuffixed = incompatiblePy2OutputsAreSuffixed;
hostPythonOptions.buildPythonZip = buildPythonZip;
hostPythonOptions.incompatibleDisallowLegacyPyProvider = incompatibleDisallowLegacyPyProvider;
hostPythonOptions.incompatibleUsePythonToolchains = incompatibleUsePythonToolchains;
hostPythonOptions.loadPythonRulesFromBzl = loadPythonRulesFromBzl;
// Save host options in case of a further exec->host transition.
hostPythonOptions.hostForcePython = hostForcePython;
return hostPythonOptions;
}
@Override
public FragmentOptions getNormalized() {
// We want to ensure that options with "null" physical default values are normalized, to avoid
// #7808.
PythonOptions newOptions = (PythonOptions) clone();
newOptions.setPythonVersion(newOptions.getPythonVersion());
return newOptions;
}
}