blob: c4e60006ab7799affdd40b656d34f06d30b403c7 [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.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.FailAction;
import com.google.devtools.build.lib.actions.extra.ExtraActionInfo;
import com.google.devtools.build.lib.actions.extra.PythonInfo;
import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.OutputGroupInfo;
import com.google.devtools.build.lib.analysis.PseudoAction;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.Util;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector.LocalMetadataCollector;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.rules.cpp.CcInfo;
import com.google.devtools.build.lib.rules.cpp.CppFileTypes;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.FileType;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.protobuf.GeneratedMessage.GeneratedExtension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
/** A helper class for analyzing a Python configured target. */
public final class PyCommon {
public static final String DEFAULT_PYTHON_VERSION_ATTRIBUTE = "default_python_version";
public static final String PYTHON_VERSION_ATTRIBUTE = "python_version";
/**
* Returns the Python version based on the {@code python_version} and {@code
* default_python_version} attributes of the given {@code AttributeMap}.
*
* <p>It is expected that both attributes are defined, string-typed, and default to {@link
* PythonVersion#_INTERNAL_SENTINEL}. The returned version is the value of {@code python_version}
* if it is not the sentinel, then {@code default_python_version} if it is not the sentinel,
* otherwise null (when both attributes are sentinels). In all cases the return value is either a
* target version value ({@code PY2} or {@code PY3}) or null.
*
* @throws IllegalArgumentException if the attributes are not present, not string-typed, or not
* parsable as target {@link PythonVersion} values or as the sentinel value
*/
@Nullable
public static PythonVersion readPythonVersionFromAttributes(AttributeMap attrs) {
PythonVersion pythonVersionAttr =
PythonVersion.parseTargetOrSentinelValue(attrs.get(PYTHON_VERSION_ATTRIBUTE, Type.STRING));
PythonVersion defaultPythonVersionAttr =
PythonVersion.parseTargetOrSentinelValue(
attrs.get(DEFAULT_PYTHON_VERSION_ATTRIBUTE, Type.STRING));
if (pythonVersionAttr != PythonVersion._INTERNAL_SENTINEL) {
return pythonVersionAttr;
} else if (defaultPythonVersionAttr != PythonVersion._INTERNAL_SENTINEL) {
return defaultPythonVersionAttr;
} else {
return null;
}
}
private static final LocalMetadataCollector METADATA_COLLECTOR = new LocalMetadataCollector() {
@Override
public void collectMetadataArtifacts(Iterable<Artifact> artifacts,
AnalysisEnvironment analysisEnvironment, NestedSetBuilder<Artifact> metadataFilesBuilder) {
// Python doesn't do any compilation, so we simply return the empty set.
}
};
/** The context for the target this {@code PyCommon} is helping to analyze. */
private final RuleContext ruleContext;
/** The pluggable semantics object with hooks that customizes how analysis is done. */
private final PythonSemantics semantics;
/**
* The Python major version for which this target is being built, as per the {@code
* python_version} attribute or the configuration.
*
* <p>This is always either {@code PY2} or {@code PY3}.
*/
private final PythonVersion version;
/**
* The level of compatibility with Python major versions, as per the {@code srcs_version}
* attribute.
*/
private final PythonVersion sourcesVersion;
/**
* The Python sources belonging to this target's transitive {@code deps}, not including this
* target's own {@code srcs}.
*/
private final NestedSet<Artifact> dependencyTransitivePythonSources;
/**
* The Python sources belonging to this target's transitive {@code deps}, including the Python
* sources in this target's {@code srcs}.
*/
private final NestedSet<Artifact> transitivePythonSources;
/** Whether this target or any of its {@code deps} or {@code data} deps has a shared library. */
private final boolean usesSharedLibraries;
/** Extra Python module import paths propagated or used by this target. */
private final NestedSet<String> imports;
/**
* Whether any of this target's transitive {@code deps} have PY2-only source files, including this
* target itself.
*/
private final boolean hasPy2OnlySources;
/**
* Whether any of this target's transitive {@code deps} have PY3-only source files, including this
* target itself.
*/
private final boolean hasPy3OnlySources;
/**
* Symlink map from root-relative paths to 2to3 converted source artifacts.
*
* <p>Null if no 2to3 conversion is required.
*/
@Nullable private final Map<PathFragment, Artifact> convertedFiles;
private Artifact executable = null;
private NestedSet<Artifact> filesToBuild = null;
private static String getOrderErrorMessage(String fieldName, Order expected, Order actual) {
return String.format(
"Incompatible order for %s: expected 'default' or '%s', got '%s'",
fieldName, expected.getSkylarkName(), actual.getSkylarkName());
}
public PyCommon(RuleContext ruleContext, PythonSemantics semantics) {
this.ruleContext = ruleContext;
this.semantics = semantics;
this.sourcesVersion = initSrcsVersionAttr(ruleContext);
this.version = ruleContext.getFragment(PythonConfiguration.class).getPythonVersion();
this.dependencyTransitivePythonSources = initDependencyTransitivePythonSources(ruleContext);
this.transitivePythonSources = initTransitivePythonSources(ruleContext);
this.usesSharedLibraries = initUsesSharedLibraries(ruleContext);
this.imports = initImports(ruleContext, semantics);
this.hasPy2OnlySources = initHasPy2OnlySources(ruleContext, this.sourcesVersion);
this.hasPy3OnlySources = initHasPy3OnlySources(ruleContext, this.sourcesVersion);
this.convertedFiles = makeAndInitConvertedFiles(ruleContext, version, this.sourcesVersion);
maybeValidateVersionCompatibleWithOwnSourcesAttr();
validateTargetPythonVersionAttr(DEFAULT_PYTHON_VERSION_ATTRIBUTE);
validateTargetPythonVersionAttr(PYTHON_VERSION_ATTRIBUTE);
validateOldVersionAttrNotUsedIfDisabled();
validateLegacyProviderNotUsedIfDisabled();
}
/** Returns the parsed value of the "srcs_version" attribute. */
private static PythonVersion initSrcsVersionAttr(RuleContext ruleContext) {
String attrValue = ruleContext.attributes().get("srcs_version", Type.STRING);
try {
return PythonVersion.parseSrcsValue(attrValue);
} catch (IllegalArgumentException ex) {
// Should already have been disallowed in the rule.
ruleContext.attributeError(
"srcs_version",
String.format(
"'%s' is not a valid value. Expected one of: %s",
attrValue, Joiner.on(", ").join(PythonVersion.SRCS_STRINGS)));
return PythonVersion.DEFAULT_SRCS_VALUE;
}
}
private static NestedSet<Artifact> initDependencyTransitivePythonSources(
RuleContext ruleContext) {
NestedSetBuilder<Artifact> builder = NestedSetBuilder.compileOrder();
collectTransitivePythonSourcesFromDeps(ruleContext, builder);
return builder.build();
}
private static NestedSet<Artifact> initTransitivePythonSources(RuleContext ruleContext) {
NestedSetBuilder<Artifact> builder = NestedSetBuilder.compileOrder();
collectTransitivePythonSourcesFromDeps(ruleContext, builder);
builder.addAll(
ruleContext
.getPrerequisiteArtifacts("srcs", Mode.TARGET)
.filter(PyRuleClasses.PYTHON_SOURCE)
.list());
return builder.build();
}
/**
* Gathers transitive .py files from {@code deps} (not including this target's {@code srcs} and
* adds them to {@code builder}.
*/
private static void collectTransitivePythonSourcesFromDeps(
RuleContext ruleContext, NestedSetBuilder<Artifact> builder) {
for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
try {
builder.addTransitive(PyProviderUtils.getTransitiveSources(dep));
} catch (EvalException e) {
// Either the provider type or field type is bad.
ruleContext.attributeError(
"deps", String.format("In dep '%s': %s", dep.getLabel(), e.getMessage()));
}
}
}
/**
* Returns true if any of this target's {@code deps} or {@code data} deps has a shared library
* file (e.g. a {@code .so}) in its transitive dependency closure.
*
* <p>For targets with the py provider, we consult the {@code uses_shared_libraries} field. For
* targets without this provider, we look for {@link CppFileTypes#SHARED_LIBRARY}-type files in
* the filesToBuild.
*/
private static boolean initUsesSharedLibraries(RuleContext ruleContext) {
Iterable<? extends TransitiveInfoCollection> targets;
// The deps attribute must exist for all rule types that use PyCommon, but not necessarily the
// data attribute.
if (ruleContext.attributes().has("data")) {
targets =
Iterables.concat(
ruleContext.getPrerequisites("deps", Mode.TARGET),
ruleContext.getPrerequisites("data", Mode.DONT_CHECK));
} else {
targets = ruleContext.getPrerequisites("deps", Mode.TARGET);
}
for (TransitiveInfoCollection target : targets) {
try {
if (PyProviderUtils.getUsesSharedLibraries(target)) {
return true;
}
} catch (EvalException e) {
ruleContext.ruleError(String.format("In dep '%s': %s", target.getLabel(), e.getMessage()));
}
}
return false;
}
private static NestedSet<String> initImports(RuleContext ruleContext, PythonSemantics semantics) {
NestedSetBuilder<String> builder = NestedSetBuilder.compileOrder();
builder.addAll(semantics.getImports(ruleContext));
for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
try {
NestedSet<String> imports = PyProviderUtils.getImports(dep);
if (!builder.getOrder().isCompatible(imports.getOrder())) {
// TODO(brandjon): We should make order an invariant of the Python provider. Then once we
/// remove PythonImportsProvider we can move this check into PyProvider/PyStructUtils.
ruleContext.ruleError(
getOrderErrorMessage(PyStructUtils.IMPORTS, builder.getOrder(), imports.getOrder()));
} else {
builder.addTransitive(imports);
}
} catch (EvalException e) {
ruleContext.attributeError(
"deps", String.format("In dep '%s': %s", dep.getLabel(), e.getMessage()));
}
}
return builder.build();
}
/**
* Returns true if any of {@code deps} has a py provider with {@code has_py2_only_sources} set, or
* this target has a {@code srcs_version} of {@code PY2ONLY}.
*/
// TODO(#1393): For Bazel, deprecate 2to3 support and treat PY2 the same as PY2ONLY.
private static boolean initHasPy2OnlySources(
RuleContext ruleContext, PythonVersion sourcesVersion) {
if (sourcesVersion == PythonVersion.PY2ONLY) {
return true;
}
for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
try {
if (PyProviderUtils.getHasPy2OnlySources(dep)) {
return true;
}
} catch (EvalException e) {
ruleContext.attributeError(
"deps", String.format("In dep '%s': %s", dep.getLabel(), e.getMessage()));
}
}
return false;
}
/**
* Returns true if any of {@code deps} has a py provider with {@code has_py3_only_sources} set, or
* this target has {@code srcs_version} of {@code PY3} or {@code PY3ONLY}.
*/
private static boolean initHasPy3OnlySources(
RuleContext ruleContext, PythonVersion sourcesVersion) {
if (sourcesVersion == PythonVersion.PY3 || sourcesVersion == PythonVersion.PY3ONLY) {
return true;
}
for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
try {
if (PyProviderUtils.getHasPy3OnlySources(dep)) {
return true;
}
} catch (EvalException e) {
ruleContext.attributeError(
"deps", String.format("In dep '%s': %s", dep.getLabel(), e.getMessage()));
}
}
return false;
}
/**
* If 2to3 conversion is to be done, creates the 2to3 actions and returns the map of converted
* files; otherwise returns null.
*/
// TODO(#1393): 2to3 conversion doesn't work in Bazel and the attempt to invoke it for Bazel
// should be removed / factored away into PythonSemantics.
@Nullable
private static Map<PathFragment, Artifact> makeAndInitConvertedFiles(
RuleContext ruleContext, PythonVersion version, PythonVersion sourcesVersion) {
if (sourcesVersion == PythonVersion.PY2 && version == PythonVersion.PY3) {
Iterable<Artifact> artifacts =
ruleContext
.getPrerequisiteArtifacts("srcs", Mode.TARGET)
.filter(PyRuleClasses.PYTHON_SOURCE)
.list();
return PythonUtils.generate2to3Actions(ruleContext, artifacts);
} else {
return null;
}
}
/**
* Under the old version semantics ({@code
* --incompatible_allow_python_version_transitions=false}), checks that the {@code srcs_version}
* attribute is compatible with the Python version as determined by the configuration.
*
* <p>A failure is reported as a rule error.
*
* <p>This check is local to the current target and intended to be enforced by each {@code
* py_library} up the dependency chain.
*
* <p>No-op under the new version semantics.
*/
private void maybeValidateVersionCompatibleWithOwnSourcesAttr() {
if (ruleContext.getFragment(PythonConfiguration.class).useNewPyVersionSemantics()) {
return;
}
// Treat PY3 as PY3ONLY: we'll never implement 3to2.
if ((version == PythonVersion.PY2 || version == PythonVersion.PY2AND3)
&& (sourcesVersion == PythonVersion.PY3 || sourcesVersion == PythonVersion.PY3ONLY)) {
ruleContext.ruleError(
"Rule '"
+ ruleContext.getLabel()
+ "' can only be used with Python 3, and cannot be converted to Python 2");
}
if ((version == PythonVersion.PY3 || version == PythonVersion.PY2AND3)
&& sourcesVersion == PythonVersion.PY2ONLY) {
ruleContext.ruleError(
"Rule '"
+ ruleContext.getLabel()
+ "' can only be used with Python 2, and cannot be converted to Python 3");
}
}
/**
* Reports an attribute error if the given target Python version attribute ({@code
* default_python_version} or {@code python_version}) cannot be parsed as {@code PY2}, {@code
* PY3}, or the sentinel value.
*
* <p>This *should* be enforced by rule attribute validation ({@link
* Attribute.Builder.allowedValues}), but this check is here to fail-fast just in case.
*/
private void validateTargetPythonVersionAttr(String attr) {
AttributeMap attrs = ruleContext.attributes();
if (!attrs.has(attr, Type.STRING)) {
return;
}
String attrValue = attrs.get(attr, Type.STRING);
try {
PythonVersion.parseTargetOrSentinelValue(attrValue);
} catch (IllegalArgumentException ex) {
ruleContext.attributeError(
attr,
String.format("'%s' is not a valid value. Expected either 'PY2' or 'PY3'", attrValue));
}
}
/**
* Reports an attribute error if the {@code default_python_version} attribute is set but
* disallowed by the configuration.
*/
private void validateOldVersionAttrNotUsedIfDisabled() {
AttributeMap attrs = ruleContext.attributes();
if (!attrs.has(DEFAULT_PYTHON_VERSION_ATTRIBUTE, Type.STRING)) {
return;
}
PythonVersion value;
try {
value =
PythonVersion.parseTargetOrSentinelValue(
attrs.get(DEFAULT_PYTHON_VERSION_ATTRIBUTE, Type.STRING));
} catch (IllegalArgumentException e) {
// Should be reported by validateTargetPythonVersionAttr(); no action required here.
return;
}
PythonConfiguration config = ruleContext.getFragment(PythonConfiguration.class);
if (value != PythonVersion._INTERNAL_SENTINEL && !config.oldPyVersionApiAllowed()) {
ruleContext.attributeError(
DEFAULT_PYTHON_VERSION_ATTRIBUTE,
"the 'default_python_version' attribute is disabled by the "
+ "'--incompatible_remove_old_python_version_api' flag");
}
}
/**
* Reports an attribute error if a target in {@code deps} passes the legacy "py" provider but this
* is disallowed by the configuration.
*/
private void validateLegacyProviderNotUsedIfDisabled() {
if (!ruleContext.getFragment(PythonConfiguration.class).disallowLegacyPyProvider()) {
return;
}
for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) {
if (PyProviderUtils.hasLegacyProvider(dep)) {
ruleContext.attributeError(
"deps",
String.format(
"In dep '%s': The legacy 'py' provider is disallowed. Migrate to the PyInfo "
+ "provider instead. You can temporarily disable this failure with "
+ "--incompatible_disallow_legacy_py_provider=false.",
dep.getLabel()));
}
}
}
/**
* Under the new version semantics ({@code --incompatible_allow_python_version_transitions=true}),
* if the Python version (as determined by the configuration) is inconsistent with {@link
* #hasPy2OnlySources} or {@link #hasPy3OnlySources}, emits a {@link FailAction} that "generates"
* the executable.
*
* <p>If the version is consistent, or if we are using the old semantics, no such action is
* emitted.
*
* <p>We use a {@code FailAction} rather than a rule error because we want to defer the error
* until the execution phase. This way, we still get a configured target that the user can query
* over with an aspect to find the exact transitive dependency that introduced the offending
* version constraint.
*
* @return true if a {@link FailAction} was created
*/
private boolean maybeCreateFailActionDueToTransitiveSourcesVersion() {
if (!ruleContext.getFragment(PythonConfiguration.class).useNewPyVersionSemantics()) {
return false;
}
String errorTemplate =
"This target is being built for Python %s but (transitively) includes Python %s-only "
+ "sources. You can get diagnostic information about which dependencies introduce this "
+ "version requirement by running the `find_requirements` aspect. For more info see "
+ "the documentation for the `srcs_version` attribute.";
String error = null;
if (version == PythonVersion.PY2 && hasPy3OnlySources) {
error = String.format(errorTemplate, "2", "3");
} else if (version == PythonVersion.PY3 && hasPy2OnlySources) {
error = String.format(errorTemplate, "3", "2");
}
if (error == null) {
return false;
} else {
ruleContext.registerAction(
new FailAction(ruleContext.getActionOwner(), ImmutableList.of(executable), error));
return true;
}
}
public PythonVersion getVersion() {
return version;
}
public PythonVersion getSourcesVersion() {
return sourcesVersion;
}
/**
* Returns the transitive Python sources collected from the deps attribute, not including sources
* from the srcs attribute (unless they were separately reached via deps).
*/
public NestedSet<Artifact> getDependencyTransitivePythonSources() {
return dependencyTransitivePythonSources;
}
/** Returns the transitive Python sources collected from the deps and srcs attributes. */
public NestedSet<Artifact> getTransitivePythonSources() {
return transitivePythonSources;
}
public boolean usesSharedLibraries() {
return usesSharedLibraries;
}
public NestedSet<String> getImports() {
return imports;
}
public boolean hasPy2OnlySources() {
return hasPy2OnlySources;
}
public boolean hasPy3OnlySources() {
return hasPy3OnlySources;
}
public Map<PathFragment, Artifact> getConvertedFiles() {
return convertedFiles;
}
public void initBinary(List<Artifact> srcs) {
Preconditions.checkNotNull(version);
validatePackageName();
if (OS.getCurrent() == OS.WINDOWS) {
executable =
ruleContext.getImplicitOutputArtifact(ruleContext.getTarget().getName() + ".exe");
} else {
executable = ruleContext.createOutputArtifact();
}
NestedSetBuilder<Artifact> filesToBuildBuilder =
NestedSetBuilder.<Artifact>stableOrder().addAll(srcs).add(executable);
if (ruleContext.getFragment(PythonConfiguration.class).buildPythonZip()) {
filesToBuildBuilder.add(getPythonZipArtifact(executable));
} else if (OS.getCurrent() == OS.WINDOWS) {
// TODO(bazel-team): Here we should check target platform instead of using OS.getCurrent().
// On Windows, add the python stub launcher in the set of files to build.
filesToBuildBuilder.add(getPythonLauncherArtifact(executable));
}
filesToBuild = filesToBuildBuilder.build();
if (ruleContext.hasErrors()) {
return;
}
addPyExtraActionPseudoAction();
}
/** @return An artifact next to the executable file with ".zip" suffix */
public Artifact getPythonZipArtifact(Artifact executable) {
return ruleContext.getRelatedArtifact(executable.getRootRelativePath(), ".zip");
}
/** @return An artifact next to the executable file with no suffix, only used on Windows */
public Artifact getPythonLauncherArtifact(Artifact executable) {
return ruleContext.getRelatedArtifact(executable.getRootRelativePath(), "");
}
public void addCommonTransitiveInfoProviders(
RuleConfiguredTargetBuilder builder,
NestedSet<Artifact> filesToBuild,
NestedSet<String> imports) {
boolean createLegacyPyProvider =
!ruleContext.getFragment(PythonConfiguration.class).disallowLegacyPyProvider();
PyProviderUtils.builder(createLegacyPyProvider)
.setTransitiveSources(transitivePythonSources)
.setUsesSharedLibraries(usesSharedLibraries)
.setImports(imports)
.setHasPy2OnlySources(hasPy2OnlySources)
.setHasPy3OnlySources(hasPy3OnlySources)
.buildAndAddToTarget(builder);
builder
.addNativeDeclaredProvider(
InstrumentedFilesCollector.collect(
ruleContext,
semantics.getCoverageInstrumentationSpec(),
METADATA_COLLECTOR,
filesToBuild,
/* reportedToActualSources= */ NestedSetBuilder.create(Order.STABLE_ORDER)))
// Python targets are not really compilable. The best we can do is make sure that all
// generated source files are ready.
.addOutputGroup(OutputGroupInfo.FILES_TO_COMPILE, transitivePythonSources)
.addOutputGroup(OutputGroupInfo.COMPILATION_PREREQUISITES, transitivePythonSources);
}
/**
* Returns a mutable List of the source Artifacts.
*/
public List<Artifact> validateSrcs() {
List<Artifact> sourceFiles = new ArrayList<>();
// TODO(bazel-team): Need to get the transitive deps closure, not just the sources of the rule.
for (TransitiveInfoCollection src :
ruleContext.getPrerequisitesIf("srcs", Mode.TARGET, FileProvider.class)) {
// Make sure that none of the sources contain hyphens.
if (Util.containsHyphen(src.getLabel().getPackageFragment())) {
ruleContext.attributeError("srcs",
src.getLabel() + ": paths to Python packages may not contain '-'");
}
Iterable<Artifact> pySrcs =
FileType.filter(
src.getProvider(FileProvider.class).getFilesToBuild(), PyRuleClasses.PYTHON_SOURCE);
Iterables.addAll(sourceFiles, pySrcs);
if (Iterables.isEmpty(pySrcs)) {
ruleContext.attributeWarning("srcs",
"rule '" + src.getLabel() + "' does not produce any Python source files");
}
}
return convertedFiles != null
? ImmutableList.copyOf(convertedFiles.values())
: sourceFiles;
}
/**
* Checks that the package name of this Python rule does not contain a '-'.
*/
void validatePackageName() {
if (Util.containsHyphen(ruleContext.getLabel().getPackageFragment())) {
ruleContext.ruleError("paths to Python packages may not contain '-'");
}
}
/**
* Adds a {@link PseudoAction} to the build graph that is only used for providing information to
* the blaze extra_action feature.
*/
void addPyExtraActionPseudoAction() {
if (ruleContext.getConfiguration().getActionListeners().isEmpty()) {
return;
}
ruleContext.registerAction(
makePyExtraActionPseudoAction(
ruleContext.getActionOwner(),
// Has to be unfiltered sources as filtered will give an error for
// unsupported file types where as certain tests only expect a warning.
ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list(),
// We must not add the files declared in the srcs of this rule.;
dependencyTransitivePythonSources,
PseudoAction.getDummyOutput(ruleContext)));
}
/**
* Creates a {@link PseudoAction} that is only used for providing
* information to the blaze extra_action feature.
*/
public static Action makePyExtraActionPseudoAction(
ActionOwner owner,
Iterable<Artifact> sources,
Iterable<Artifact> dependencies,
Artifact output) {
PythonInfo info =
PythonInfo.newBuilder()
.addAllSourceFile(Artifact.toExecPaths(sources))
.addAllDepFile(Artifact.toExecPaths(dependencies))
.build();
return new PyPseudoAction(
owner,
NestedSetBuilder.wrap(Order.STABLE_ORDER, Iterables.concat(sources, dependencies)),
ImmutableList.of(output),
"Python",
PYTHON_INFO,
info);
}
@AutoCodec @AutoCodec.VisibleForSerialization
static final GeneratedExtension<ExtraActionInfo, PythonInfo> PYTHON_INFO = PythonInfo.pythonInfo;
/** @return A String that is the full path to the main python entry point. */
public String determineMainExecutableSource(boolean withWorkspaceName) {
String mainSourceName;
Rule target = ruleContext.getRule();
boolean explicitMain = target.isAttributeValueExplicitlySpecified("main");
if (explicitMain) {
mainSourceName = ruleContext.attributes().get("main", BuildType.LABEL).getName();
if (!mainSourceName.endsWith(".py")) {
ruleContext.attributeError("main", "main must end in '.py'");
}
} else {
String ruleName = target.getName();
if (ruleName.endsWith(".py")) {
ruleContext.attributeError("name", "name must not end in '.py'");
}
mainSourceName = ruleName + ".py";
}
PathFragment mainSourcePath = PathFragment.create(mainSourceName);
Artifact mainArtifact = null;
for (Artifact outItem : ruleContext.getPrerequisiteArtifacts("srcs", Mode.TARGET).list()) {
if (outItem.getRootRelativePath().endsWith(mainSourcePath)) {
if (mainArtifact == null) {
mainArtifact = outItem;
} else {
ruleContext.attributeError("srcs",
buildMultipleMainMatchesErrorText(explicitMain, mainSourceName,
mainArtifact.getRunfilesPath().toString(),
outItem.getRunfilesPath().toString()));
}
}
}
if (mainArtifact == null) {
ruleContext.attributeError("srcs", buildNoMainMatchesErrorText(explicitMain, mainSourceName));
return null;
}
if (!withWorkspaceName) {
return mainArtifact.getRunfilesPath().getPathString();
}
PathFragment workspaceName =
PathFragment.create(ruleContext.getRule().getPackage().getWorkspaceName());
return workspaceName.getRelative(mainArtifact.getRunfilesPath()).getPathString();
}
public String determineMainExecutableSource() {
return determineMainExecutableSource(true);
}
public Artifact getExecutable() {
return executable;
}
public NestedSet<Artifact> getFilesToBuild() {
return filesToBuild;
}
/**
* Creates the actual executable artifact, i.e., emits a generating action for {@link
* #getExecutable()}.
*
* <p>If there is a transitive sources version conflict, may produce a {@link FailAction} to
* trigger an execution-time failure. See {@link
* #maybeCreateFailActionDueToTransitiveSourcesVersion}.
*/
public Artifact createExecutable(
CcInfo ccInfo, NestedSet<String> givenImports, Runfiles.Builder defaultRunfilesBuilder)
throws InterruptedException, RuleErrorException {
boolean failed = maybeCreateFailActionDueToTransitiveSourcesVersion();
if (failed) {
return executable;
} else {
// TODO(#7054): We pass imports as an arg instead of taking them from the PyCommon field
// because the imports logic is a little inconsistent, and passing it explicitly may help
// avoid creating bugs that make the situation worse. We can eliminate this arg when we
// straighten up the other imports logic.
return semantics.createExecutable(
ruleContext, this, ccInfo, givenImports, defaultRunfilesBuilder);
}
}
private static String buildMultipleMainMatchesErrorText(boolean explicit, String proposedMainName,
String match1, String match2) {
String errorText;
if (explicit) {
errorText = "file name '" + proposedMainName
+ "' specified by 'main' attribute matches multiple files: e.g., '" + match1
+ "' and '" + match2 + "'";
} else {
errorText = "default main file name '" + proposedMainName
+ "' matches multiple files. Perhaps specify an explicit file with 'main' attribute? "
+ "Matches were: '" + match1 + "' and '" + match2 + "'";
}
return errorText;
}
private static String buildNoMainMatchesErrorText(boolean explicit, String proposedMainName) {
String errorText;
if (explicit) {
errorText = "could not find '" + proposedMainName
+ "' as specified by 'main' attribute";
} else {
errorText = "corresponding default '" + proposedMainName + "' does not appear in srcs. Add it"
+ " or override default file name with a 'main' attribute";
}
return errorText;
}
// Used purely to set the legacy ActionType of the ExtraActionInfo.
@Immutable
private static final class PyPseudoAction extends PseudoAction<PythonInfo> {
private static final UUID ACTION_UUID = UUID.fromString("8d720129-bc1a-481f-8c4c-dbe11dcef319");
public PyPseudoAction(ActionOwner owner,
NestedSet<Artifact> inputs, Collection<Artifact> outputs,
String mnemonic, GeneratedExtension<ExtraActionInfo, PythonInfo> infoExtension,
PythonInfo info) {
super(ACTION_UUID, owner, inputs, outputs, mnemonic, infoExtension, info);
}
}
}