Skip to content

Commit 763dd0c

Browse files
rbeasleycopybara-github
authored andcommitted
Add stub_shebang to py_runtime
Added a `stub_shebang` to `py_runtime` to allow users to specify a custom "shebang" expression used by the Python stub script. Motivation is to support environments where `#!/usr/bin/env python` isn't valid (ex: no `/usr/bin/python`, but there's a `/usr/bin/python3`). Closes #8685. ### Guided tour - Added field to `PyRuntimeInfoApi` inc. default value. - Added field to `PyRuntimeInfo` provider inc. tests. - Added field to `py_runtime` Starlark rule inc tests. - Replaced static `#!/usr/bin/env python` in the stub w/ `%shebang%`. - There are a few redundancies w/r/t declaring defaults and documentation. This is because there are a few separate public APIs (ex: `PyRuntimeInfo(...)`, `py_runtime(...)`), and I want to make sure defaults appear in the generated docs. Testing Done: - `bazelisk test src/test/java/com/google/devtools/build/lib/bazel/rules/python/...` `bazelisk test src/test/java/com/google/devtools/build/lib/rules/python/...` Closes #11434. PiperOrigin-RevId: 370622012
1 parent b6f87f1 commit 763dd0c

File tree

9 files changed

+144
-24
lines changed

9 files changed

+144
-24
lines changed

src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ private static void createStubFile(
167167
stubOutput,
168168
STUB_TEMPLATE,
169169
ImmutableList.of(
170+
Substitution.of("%shebang%", getStubShebang(ruleContext, common)),
170171
Substitution.of(
171172
"%main%", common.determineMainExecutableSource(/*withWorkspaceName=*/ true)),
172173
Substitution.of("%python_binary%", pythonBinary),
@@ -451,6 +452,15 @@ private static String getPythonBinary(
451452
return pythonBinary;
452453
}
453454

455+
private static String getStubShebang(RuleContext ruleContext, PyCommon common) {
456+
PyRuntimeInfo provider = getRuntime(ruleContext, common);
457+
if (provider != null) {
458+
return provider.getStubShebang();
459+
} else {
460+
return PyRuntimeInfo.DEFAULT_STUB_SHEBANG;
461+
}
462+
}
463+
454464
@Override
455465
public CcInfo buildCcInfoProvider(Iterable<? extends TransitiveInfoCollection> deps) {
456466
ImmutableList<CcInfo> ccInfos =

src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python
1+
%shebang%
22

33
# This script must retain compatibility with a wide variety of Python versions
44
# since it is run for every py_binary target. Currently we guarantee support

src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public ConfiguredTarget create(RuleContext ruleContext)
4242
PythonVersion pythonVersion =
4343
PythonVersion.parseTargetOrSentinelValue(
4444
ruleContext.attributes().get("python_version", Type.STRING));
45+
String stubShebang = ruleContext.attributes().get("stub_shebang", Type.STRING);
4546

4647
// Determine whether we're pointing to an in-build target (hermetic) or absolute system path
4748
// (non-hermetic).
@@ -80,8 +81,8 @@ public ConfiguredTarget create(RuleContext ruleContext)
8081

8182
PyRuntimeInfo provider =
8283
hermetic
83-
? PyRuntimeInfo.createForInBuildRuntime(interpreter, files, pythonVersion)
84-
: PyRuntimeInfo.createForPlatformRuntime(interpreterPath, pythonVersion);
84+
? PyRuntimeInfo.createForInBuildRuntime(interpreter, files, pythonVersion, stubShebang)
85+
: PyRuntimeInfo.createForPlatformRuntime(interpreterPath, pythonVersion, stubShebang);
8586

8687
return new RuleConfiguredTargetBuilder(ruleContext)
8788
.setFilesToBuild(files)

src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeInfo.java

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,20 @@ public final class PyRuntimeInfo implements Info, PyRuntimeInfoApi<Artifact> {
5454
private final Location location;
5555
@Nullable private final PathFragment interpreterPath;
5656
@Nullable private final Artifact interpreter;
57-
// Validated on initalization to contain Artifact
57+
// Validated on initialization to contain Artifact
5858
@Nullable private final Depset files;
5959
/** Invariant: either PY2 or PY3. */
6060
private final PythonVersion pythonVersion;
6161

62+
private final String stubShebang;
63+
6264
private PyRuntimeInfo(
6365
@Nullable Location location,
6466
@Nullable PathFragment interpreterPath,
6567
@Nullable Artifact interpreter,
6668
@Nullable Depset files,
67-
PythonVersion pythonVersion) {
69+
PythonVersion pythonVersion,
70+
@Nullable String stubShebang) {
6871
Preconditions.checkArgument((interpreterPath == null) != (interpreter == null));
6972
Preconditions.checkArgument((interpreter == null) == (files == null));
7073
Preconditions.checkArgument(pythonVersion.isTargetValue());
@@ -73,6 +76,11 @@ private PyRuntimeInfo(
7376
this.interpreterPath = interpreterPath;
7477
this.interpreter = interpreter;
7578
this.pythonVersion = pythonVersion;
79+
if (stubShebang != null && !stubShebang.isEmpty()) {
80+
this.stubShebang = stubShebang;
81+
} else {
82+
this.stubShebang = PyRuntimeInfoApi.DEFAULT_STUB_SHEBANG;
83+
}
7684
}
7785

7886
@Override
@@ -87,20 +95,29 @@ public Location getCreationLocation() {
8795

8896
/** Constructs an instance from native rule logic (built-in location) for an in-build runtime. */
8997
public static PyRuntimeInfo createForInBuildRuntime(
90-
Artifact interpreter, NestedSet<Artifact> files, PythonVersion pythonVersion) {
98+
Artifact interpreter,
99+
NestedSet<Artifact> files,
100+
PythonVersion pythonVersion,
101+
@Nullable String stubShebang) {
91102
return new PyRuntimeInfo(
92103
/*location=*/ null,
93104
/*interpreterPath=*/ null,
94105
interpreter,
95106
Depset.of(Artifact.TYPE, files),
96-
pythonVersion);
107+
pythonVersion,
108+
stubShebang);
97109
}
98110

99111
/** Constructs an instance from native rule logic (built-in location) for a platform runtime. */
100112
public static PyRuntimeInfo createForPlatformRuntime(
101-
PathFragment interpreterPath, PythonVersion pythonVersion) {
113+
PathFragment interpreterPath, PythonVersion pythonVersion, @Nullable String stubShebang) {
102114
return new PyRuntimeInfo(
103-
/*location=*/ null, interpreterPath, /*interpreter=*/ null, /*files=*/ null, pythonVersion);
115+
/*location=*/ null,
116+
interpreterPath,
117+
/*interpreter=*/ null,
118+
/*files=*/ null,
119+
pythonVersion,
120+
stubShebang);
104121
}
105122

106123
@Override
@@ -113,12 +130,13 @@ public boolean equals(Object other) {
113130
PyRuntimeInfo otherInfo = (PyRuntimeInfo) other;
114131
return (this.interpreterPath.equals(otherInfo.interpreterPath)
115132
&& this.interpreter.equals(otherInfo.interpreter)
116-
&& this.files.equals(otherInfo.files));
133+
&& this.files.equals(otherInfo.files)
134+
&& this.stubShebang.equals(otherInfo.stubShebang));
117135
}
118136

119137
@Override
120138
public int hashCode() {
121-
return Objects.hash(PyRuntimeInfo.class, interpreterPath, interpreter, files);
139+
return Objects.hash(PyRuntimeInfo.class, interpreterPath, interpreter, files, stubShebang);
122140
}
123141

124142
/**
@@ -153,6 +171,11 @@ public Artifact getInterpreter() {
153171
return interpreter;
154172
}
155173

174+
@Override
175+
public String getStubShebang() {
176+
return stubShebang;
177+
}
178+
156179
@Nullable
157180
public NestedSet<Artifact> getFiles() {
158181
try {
@@ -191,6 +214,7 @@ public PyRuntimeInfo constructor(
191214
Object interpreterUncast,
192215
Object filesUncast,
193216
String pythonVersion,
217+
String stubShebang,
194218
StarlarkThread thread)
195219
throws EvalException {
196220
String interpreterPath =
@@ -225,14 +249,20 @@ public PyRuntimeInfo constructor(
225249
filesDepset = Depset.of(Artifact.TYPE, NestedSetBuilder.emptySet(Order.STABLE_ORDER));
226250
}
227251
return new PyRuntimeInfo(
228-
loc, /*interpreterPath=*/ null, interpreter, filesDepset, parsedPythonVersion);
252+
loc,
253+
/*interpreterPath=*/ null,
254+
interpreter,
255+
filesDepset,
256+
parsedPythonVersion,
257+
stubShebang);
229258
} else {
230259
return new PyRuntimeInfo(
231260
loc,
232261
PathFragment.create(interpreterPath),
233262
/*interpreter=*/ null,
234263
/*files=*/ null,
235-
parsedPythonVersion);
264+
parsedPythonVersion,
265+
stubShebang);
236266
}
237267
}
238268
}

src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeRule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ public RuleClass build(RuleClass.Builder builder, RuleDefinitionEnvironment env)
6666
attr("python_version", STRING)
6767
.value(PythonVersion._INTERNAL_SENTINEL.toString())
6868
.allowedValues(PyRuleClasses.TARGET_PYTHON_ATTR_VALUE_SET))
69+
70+
/* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(stub_shebang) -->
71+
"Shebang" expression prepended to the bootstrapping Python stub script
72+
used when executing <code>py_binary</code> targets.
73+
74+
<p>See <a href="https://github.com/bazelbuild/bazel/issues/8685">issue 8685</a> for
75+
motivation.
76+
77+
<p>Does not apply to Windows.
78+
<!-- #END_BLAZE_RULE.ATTRIBUTE --> */
79+
.add(attr("stub_shebang", STRING).value(PyRuntimeInfo.DEFAULT_STUB_SHEBANG))
6980
.add(attr("output_licenses", LICENSE))
7081
.build();
7182
}

src/main/java/com/google/devtools/build/lib/starlarkbuildapi/python/PyRuntimeInfoApi.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
category = DocCategory.PROVIDER)
4646
public interface PyRuntimeInfoApi<FileT extends FileApi> extends StarlarkValue {
4747

48+
static final String DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python";
49+
4850
@StarlarkMethod(
4951
name = "interpreter_path",
5052
structField = true,
@@ -88,6 +90,15 @@ public interface PyRuntimeInfoApi<FileT extends FileApi> extends StarlarkValue {
8890
+ "(only) <code>\"PY2\"</code> and <code>\"PY3\"</code>.")
8991
String getPythonVersionForStarlark();
9092

93+
@StarlarkMethod(
94+
name = "stub_shebang",
95+
structField = true,
96+
doc =
97+
"\"Shebang\" expression prepended to the bootstrapping Python stub script "
98+
+ "used when executing <code>py_binary</code> targets. Does not apply "
99+
+ "to Windows.")
100+
String getStubShebang();
101+
91102
/** Provider type for {@link PyRuntimeInfoApi} objects. */
92103
@StarlarkBuiltin(name = "Provider", documented = false, doc = "")
93104
interface PyRuntimeInfoProviderApi extends ProviderApi {
@@ -139,6 +150,17 @@ interface PyRuntimeInfoProviderApi extends ProviderApi {
139150
positional = false,
140151
named = true,
141152
doc = "The value for the new object's <code>python_version</code> field."),
153+
@Param(
154+
name = "stub_shebang",
155+
allowedTypes = {@ParamType(type = String.class)},
156+
positional = false,
157+
named = true,
158+
defaultValue = "'" + DEFAULT_STUB_SHEBANG + "'",
159+
doc =
160+
"The value for the new object's <code>stub_shebang</code> field. "
161+
+ "Default is <code>"
162+
+ DEFAULT_STUB_SHEBANG
163+
+ "</code>."),
142164
},
143165
useStarlarkThread = true,
144166
selfCall = true)
@@ -148,6 +170,7 @@ PyRuntimeInfoApi<?> constructor(
148170
Object interpreterUncast,
149171
Object filesUncast,
150172
String pythonVersion,
173+
String stubShebang,
151174
StarlarkThread thread)
152175
throws EvalException;
153176
}

src/main/java/com/google/devtools/build/skydoc/fakebuildapi/python/FakePyRuntimeInfo.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ public String getPythonVersionForStarlark() {
4444
return "";
4545
}
4646

47+
@Override
48+
public String getStubShebang() {
49+
return "";
50+
}
51+
4752
@Override
4853
public void repr(Printer printer) {}
4954

@@ -56,6 +61,7 @@ public PyRuntimeInfoApi<?> constructor(
5661
Object interpreterUncast,
5762
Object filesUncast,
5863
String pythonVersion,
64+
String stubShebang,
5965
StarlarkThread thread)
6066
throws EvalException {
6167
return new FakePyRuntimeInfo();

src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyBinaryConfiguredTargetTest.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ private static String join(String... lines) {
4545
}
4646

4747
/**
48-
* Given a {@code py_binary} or {@code py_test} target, returns the path of the Python interpreter
49-
* used by the generated stub script.
48+
* Given a {@code py_binary} or {@code py_test} target and substitution key, returns the
49+
* corresponding substitution value used by the generated stub script.
5050
*
5151
* <p>This works by casting the stub script's generating action to a template expansion action and
52-
* looking for the expansion key for the Python interpreter. It's therefore linked to the
53-
* implementation of the rule, but that's the cost we pay for avoiding an execution-time test.
52+
* looking for the requested substitution key. It's therefore linked to the implementation of the
53+
* rule, but that's the cost we pay for avoiding an execution-time test.
5454
*/
55-
private String getInterpreterPathFromStub(ConfiguredTarget pyExecutableTarget) {
55+
private String getSubstitutionValueFromStub(
56+
ConfiguredTarget pyExecutableTarget, String substitutionKey) {
5657
// First find the stub script. Normally this is just the executable associated with the target.
5758
// But for Windows the executable is a separate launcher with an ".exe" extension, and the stub
5859
// script artifact has the same base name with the extension ".temp" instead. (At least, when
@@ -75,12 +76,23 @@ private String getInterpreterPathFromStub(ConfiguredTarget pyExecutableTarget) {
7576
assertThat(generatingAction).isInstanceOf(TemplateExpansionAction.class);
7677
TemplateExpansionAction templateAction = (TemplateExpansionAction) generatingAction;
7778
for (Substitution sub : templateAction.getSubstitutions()) {
78-
if (sub.getKey().equals("%python_binary%")) {
79+
if (sub.getKey().equals(substitutionKey)) {
7980
return sub.getValue();
8081
}
8182
}
8283
throw new AssertionError(
83-
"Failed to find the '%python_binary%' key in the stub script's template expansion action");
84+
"Failed to find the '"
85+
+ substitutionKey
86+
+ "' key in the stub script's template "
87+
+ "expansion action");
88+
}
89+
90+
private String getInterpreterPathFromStub(ConfiguredTarget pyExecutableTarget) {
91+
return getSubstitutionValueFromStub(pyExecutableTarget, "%python_binary%");
92+
}
93+
94+
private String getShebangFromStub(ConfiguredTarget pyExecutableTarget) {
95+
return getSubstitutionValueFromStub(pyExecutableTarget, "%shebang%");
8496
}
8597

8698
// TODO(#8169): Delete tests of the legacy --python_top / --python_path behavior.
@@ -168,11 +180,13 @@ private void defineToolchains() throws Exception {
168180
" name = 'py2_runtime',",
169181
" interpreter_path = '/system/python2',",
170182
" python_version = 'PY2',",
183+
" stub_shebang = '#!/usr/bin/env python',",
171184
")",
172185
"py_runtime(",
173186
" name = 'py3_runtime',",
174187
" interpreter_path = '/system/python3',",
175188
" python_version = 'PY3',",
189+
" stub_shebang = '#!/usr/bin/env python3',",
176190
")",
177191
"py_runtime_pair(",
178192
" name = 'py_runtime_pair',",
@@ -214,10 +228,18 @@ public void runtimeObtainedFromToolchain() throws Exception {
214228
"--incompatible_use_python_toolchains=true",
215229
"--extra_toolchains=//toolchains:py_toolchain");
216230

217-
String py2Path = getInterpreterPathFromStub(getConfiguredTarget("//pkg:py2_bin"));
218-
String py3Path = getInterpreterPathFromStub(getConfiguredTarget("//pkg:py3_bin"));
231+
ConfiguredTarget py2 = getConfiguredTarget("//pkg:py2_bin");
232+
ConfiguredTarget py3 = getConfiguredTarget("//pkg:py3_bin");
233+
234+
String py2Path = getInterpreterPathFromStub(py2);
235+
String py3Path = getInterpreterPathFromStub(py3);
219236
assertThat(py2Path).isEqualTo("/system/python2");
220237
assertThat(py3Path).isEqualTo("/system/python3");
238+
239+
String py2Shebang = getShebangFromStub(py2);
240+
String py3Shebang = getShebangFromStub(py3);
241+
assertThat(py2Shebang).isEqualTo("#!/usr/bin/env python");
242+
assertThat(py3Shebang).isEqualTo("#!/usr/bin/env python3");
221243
}
222244

223245
@Test

0 commit comments

Comments
 (0)