Skip to content

Commit bc46347

Browse files
authored
Support running pexes from a working directory. (#12347)
Previously we only supported running them from the sandbox root. Now we can run them from any subdir. [ci skip-rust] [ci skip-build-wheels]
1 parent a427ea8 commit bc46347

File tree

2 files changed

+98
-6
lines changed

2 files changed

+98
-6
lines changed

src/python/pants/backend/python/util_rules/pex.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import dataclasses
77
import json
88
import logging
9+
import os
910
import shlex
1011
from dataclasses import dataclass
1112
from pathlib import PurePath
@@ -523,11 +524,18 @@ def _create_venv_script(
523524
f"{name}={shlex.quote(value)}"
524525
for name, value in pex_environment.environment_dict(python_configured=True).items()
525526
)
527+
# Ensure that the pex is executed from a path relative to the shim script, so
528+
# that running from within a working directory works properly.
529+
# NB: No method for determining the path of the current script is entirely foolproof,
530+
# but BASH_SOURCE works fine in our specific case (and has been available since bash-3.0,
531+
# released in 2004, so it seems safe to assume it's available).
532+
local_pex_bin = '"${BASH_SOURCE%/*}/"' + shlex.quote(self.pex.name)
526533
target_venv_executable = shlex.quote(str(venv_executable))
527534
venv_dir = shlex.quote(str(self.venv_dir))
528535
execute_pex_args = " ".join(
529-
shlex.quote(arg)
530-
for arg in pex_environment.create_argv(self.pex.name, python=self.pex.python)
536+
# Don't quote the BASH_SOURCE dereference in local_pex_bin.
537+
arg if arg == local_pex_bin else shlex.quote(arg)
538+
for arg in pex_environment.create_argv(local_pex_bin, python=self.pex.python)[1:]
531539
)
532540

533541
script = dedent(
@@ -693,6 +701,7 @@ class PexProcess:
693701
description: str = dataclasses.field(compare=False)
694702
level: LogLevel
695703
input_digest: Digest | None
704+
working_directory: str | None
696705
extra_env: FrozenDict[str, str] | None
697706
output_files: tuple[str, ...] | None
698707
output_directories: tuple[str, ...] | None
@@ -708,6 +717,7 @@ def __init__(
708717
argv: Iterable[str] = (),
709718
level: LogLevel = LogLevel.INFO,
710719
input_digest: Digest | None = None,
720+
working_directory: str | None = None,
711721
extra_env: Mapping[str, str] | None = None,
712722
output_files: Iterable[str] | None = None,
713723
output_directories: Iterable[str] | None = None,
@@ -720,6 +730,7 @@ def __init__(
720730
self.description = description
721731
self.level = level
722732
self.input_digest = input_digest
733+
self.working_directory = working_directory
723734
self.extra_env = FrozenDict(extra_env) if extra_env else None
724735
self.output_files = tuple(output_files) if output_files else None
725736
self.output_directories = tuple(output_directories) if output_directories else None
@@ -731,7 +742,12 @@ def __init__(
731742
@rule
732743
async def setup_pex_process(request: PexProcess, pex_environment: SandboxPexEnvironment) -> Process:
733744
pex = request.pex
734-
argv = pex_environment.create_argv(pex.name, *request.argv, python=pex.python)
745+
pex_bin = (
746+
os.path.relpath(pex.name, request.working_directory)
747+
if request.working_directory
748+
else pex.name
749+
)
750+
argv = pex_environment.create_argv(pex_bin, *request.argv, python=pex.python)
735751
env = {
736752
**pex_environment.environment_dict(python_configured=pex.python is not None),
737753
**(request.extra_env or {}),
@@ -746,6 +762,7 @@ async def setup_pex_process(request: PexProcess, pex_environment: SandboxPexEnvi
746762
description=request.description,
747763
level=request.level,
748764
input_digest=input_digest,
765+
working_directory=request.working_directory,
749766
env=env,
750767
output_files=request.output_files,
751768
output_directories=request.output_directories,
@@ -764,6 +781,7 @@ class VenvPexProcess:
764781
description: str = dataclasses.field(compare=False)
765782
level: LogLevel
766783
input_digest: Digest | None
784+
working_directory: str | None
767785
extra_env: FrozenDict[str, str] | None
768786
output_files: tuple[str, ...] | None
769787
output_directories: tuple[str, ...] | None
@@ -779,6 +797,7 @@ def __init__(
779797
argv: Iterable[str] = (),
780798
level: LogLevel = LogLevel.INFO,
781799
input_digest: Digest | None = None,
800+
working_directory: str | None = None,
782801
extra_env: Mapping[str, str] | None = None,
783802
output_files: Iterable[str] | None = None,
784803
output_directories: Iterable[str] | None = None,
@@ -791,6 +810,7 @@ def __init__(
791810
self.description = description
792811
self.level = level
793812
self.input_digest = input_digest
813+
self.working_directory = working_directory
794814
self.extra_env = FrozenDict(extra_env) if extra_env else None
795815
self.output_files = tuple(output_files) if output_files else None
796816
self.output_directories = tuple(output_directories) if output_directories else None
@@ -804,7 +824,12 @@ async def setup_venv_pex_process(
804824
request: VenvPexProcess, pex_environment: SandboxPexEnvironment
805825
) -> Process:
806826
venv_pex = request.venv_pex
807-
argv = (venv_pex.pex.argv0, *request.argv)
827+
pex_bin = (
828+
os.path.relpath(venv_pex.pex.argv0, request.working_directory)
829+
if request.working_directory
830+
else venv_pex.pex.argv0
831+
)
832+
argv = (pex_bin, *request.argv)
808833
input_digest = (
809834
await Get(Digest, MergeDigests((venv_pex.digest, request.input_digest)))
810835
if request.input_digest
@@ -815,6 +840,7 @@ async def setup_venv_pex_process(
815840
description=request.description,
816841
level=request.level,
817842
input_digest=input_digest,
843+
working_directory=request.working_directory,
818844
env=request.extra_env,
819845
output_files=request.output_files,
820846
output_directories=request.output_directories,

src/python/pants/backend/python/util_rules/pex_test.py

+68-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import json
77
import os.path
8+
import re
89
import textwrap
910
import zipfile
1011
from dataclasses import dataclass
@@ -30,7 +31,7 @@
3031
)
3132
from pants.backend.python.util_rules.pex import rules as pex_rules
3233
from pants.backend.python.util_rules.pex_cli import PexPEX
33-
from pants.engine.fs import CreateDigest, Digest, FileContent
34+
from pants.engine.fs import CreateDigest, Digest, Directory, FileContent
3435
from pants.engine.process import Process, ProcessResult
3536
from pants.testutil.rule_runner import QueryRule, RuleRunner
3637

@@ -73,7 +74,7 @@ def rule_runner() -> RuleRunner:
7374
QueryRule(PexResolveInfo, (Pex,)),
7475
QueryRule(PexResolveInfo, (VenvPex,)),
7576
QueryRule(PexPEX, ()),
76-
]
77+
],
7778
)
7879

7980

@@ -268,6 +269,71 @@ def test_pex_environment(rule_runner: RuleRunner, pex_type: type[Pex | VenvPex])
268269
assert b"ftp_proxy=dummyproxy" in result.stdout
269270

270271

272+
@pytest.mark.parametrize("pex_type", [Pex, VenvPex])
273+
def test_pex_working_directory(rule_runner: RuleRunner, pex_type: type[Pex | VenvPex]) -> None:
274+
sources = rule_runner.request(
275+
Digest,
276+
[
277+
CreateDigest(
278+
(
279+
FileContent(
280+
path="main.py",
281+
content=textwrap.dedent(
282+
"""
283+
import os
284+
cwd = os.getcwd()
285+
print(f"CWD: {cwd}")
286+
for path, dirs, _ in os.walk(cwd):
287+
for name in dirs:
288+
print(f"DIR: {os.path.relpath(os.path.join(path, name), cwd)}")
289+
"""
290+
).encode(),
291+
),
292+
)
293+
),
294+
],
295+
)
296+
297+
pex_output = create_pex_and_get_all_data(
298+
rule_runner,
299+
pex_type=pex_type,
300+
main=EntryPoint("main"),
301+
sources=sources,
302+
interpreter_constraints=InterpreterConstraints(["CPython>=3.6"]),
303+
)
304+
305+
pex = pex_output["pex"]
306+
pex_process_type = PexProcess if isinstance(pex, Pex) else VenvPexProcess
307+
308+
dirpath = "foo/bar/baz"
309+
runtime_files = rule_runner.request(Digest, [CreateDigest([Directory(path=dirpath)])])
310+
311+
dirpath_parts = os.path.split(dirpath)
312+
for i in range(0, len(dirpath_parts)):
313+
working_dir = os.path.join(*dirpath_parts[:i]) if i > 0 else None
314+
expected_subdir = os.path.join(*dirpath_parts[i:]) if i < len(dirpath_parts) else None
315+
process = rule_runner.request(
316+
Process,
317+
[
318+
pex_process_type(
319+
pex,
320+
description="Run the pex and check its cwd",
321+
working_directory=working_dir,
322+
input_digest=runtime_files,
323+
)
324+
],
325+
)
326+
result = rule_runner.request(ProcessResult, [process])
327+
output_str = result.stdout.decode()
328+
mo = re.search(r"CWD: (.*)\n", output_str)
329+
assert mo is not None
330+
reported_cwd = mo.group(1)
331+
if working_dir:
332+
assert reported_cwd.endswith(working_dir)
333+
if expected_subdir:
334+
assert f"DIR: {expected_subdir}" in output_str
335+
336+
271337
def test_resolves_dependencies(rule_runner: RuleRunner) -> None:
272338
requirements = PexRequirements(["six==1.12.0", "jsonschema==2.6.0", "requests==2.23.0"])
273339
pex_info = create_pex_and_get_pex_info(rule_runner, requirements=requirements)

0 commit comments

Comments
 (0)