Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[internal] Switch from Rust-Cpython to PyO3 #13526

Merged
merged 41 commits into from
Nov 14, 2021
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ea2332e
[WIP] Switch from Rust-cpython to PyO3
Eric-Arellano Nov 8, 2021
0c8359a
Revert back to Python::acquire_gil
Eric-Arellano Nov 9, 2021
91d90c7
Use explicit types in interface.rs
Eric-Arellano Nov 9, 2021
01bf069
Fix some problems in mod.rs
Eric-Arellano Nov 9, 2021
ee2ff66
Port Generator classes
Eric-Arellano Nov 9, 2021
3bdbab0
Port `engine_aware.rs`
Eric-Arellano Nov 10, 2021
0e9a741
Port the rest of python.rs (Failure)
Eric-Arellano Nov 10, 2021
766778d
Port fs.rs
Eric-Arellano Nov 10, 2021
1e88c1e
Port all remaining classes to PyO3
Eric-Arellano Nov 10, 2021
108c231
Port most of interface.rs
Eric-Arellano Nov 10, 2021
5a5d3f0
Port most of intrinsics.rs, which means finishing mod.rs port
Eric-Arellano Nov 11, 2021
96cfc9e
More fixes
Eric-Arellano Nov 11, 2021
830d1bb
Fix lifetime of `throw()` - use `String` instead of `&str`
Eric-Arellano Nov 11, 2021
5c15528
Fix some more issues
Eric-Arellano Nov 11, 2021
ac808c4
Fix `scheduler_execute`
Eric-Arellano Nov 11, 2021
192839d
Fix `nailgun_server_create`
Eric-Arellano Nov 11, 2021
7ed8843
Fix `call_function()` to work with `Value`
Eric-Arellano Nov 11, 2021
ed423a6
Fix `DownloadedFile` invalid move
Eric-Arellano Nov 11, 2021
615b3f3
Fix Generator code!
Eric-Arellano Nov 11, 2021
0065c6a
Some easy fixes
Eric-Arellano Nov 11, 2021
b413cc8
Fix borrow checker issues!
Eric-Arellano Nov 12, 2021
a94bcf5
Fix Clippy
Eric-Arellano Nov 12, 2021
7ee2abd
Pants can now load PyO3 extension
Eric-Arellano Nov 12, 2021
47d204e
`./pants --no-pantsd --version` works
Eric-Arellano Nov 12, 2021
56d72e2
Fix Pantsd crashing!
Eric-Arellano Nov 12, 2021
e3c4ae5
Merge branch 'main' of github.com:pantsbuild/pants into pyo3
Eric-Arellano Nov 12, 2021
0dae5ef
Merge branch 'main' of github.com:pantsbuild/pants into pyo3
Eric-Arellano Nov 12, 2021
dfc015a
Derive Debug for all types that can be to faciliate debugging
Eric-Arellano Nov 13, 2021
483c601
Add `debug-engine` goal
Eric-Arellano Nov 13, 2021
2556a9e
Fix the hangs!!! Don't clone the Scheduler
Eric-Arellano Nov 13, 2021
25f5ccc
Revert "Derive Debug for all types that can be to faciliate debugging"
Eric-Arellano Nov 13, 2021
07db0de
Merge branch 'main' of github.com:pantsbuild/pants into pyo3
Eric-Arellano Nov 13, 2021
c82b723
Clippy
Eric-Arellano Nov 13, 2021
fff36c3
Green CI run!
Eric-Arellano Nov 13, 2021
a04dc32
Don't lie in docstring for `Value`
Eric-Arellano Nov 14, 2021
ce63e69
Use `.as_ref()` when possible
Eric-Arellano Nov 14, 2021
8082056
Merge branch 'main' of github.com:pantsbuild/pants into pyo3
Eric-Arellano Nov 14, 2021
0a3d421
Fix using the GIL inside `py.allow_threads()`
Eric-Arellano Nov 14, 2021
eaf83b1
Merge branch 'main' of github.com:pantsbuild/pants into pyo3
Eric-Arellano Nov 14, 2021
38c8b6e
Use patch from PyO3 maintainer's fork to fix streaming workunits not …
Eric-Arellano Nov 14, 2021
b718f9d
Two more places we can use `.as_ref()` instead
Eric-Arellano Nov 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/python/pants/bin/daemon_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ def __call__(
command: str,
args: Tuple[str, ...],
env: Dict[str, str],
working_directory: bytes,
Copy link
Contributor Author

@Eric-Arellano Eric-Arellano Nov 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was unused and its type conversion was wrong with cpython, so it was easier to delete than port.

cancellation_latch: PySessionCancellationLatch,
stdin_fileno: int,
stdout_fileno: int,
Expand Down
83 changes: 32 additions & 51 deletions src/python/pants/engine/fs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@
Workspace,
)
from pants.engine.goal import Goal, GoalSubsystem
from pants.engine.internals.native_engine_pyo3 import PyDigest as DigestPyO3
from pants.engine.internals.native_engine_pyo3 import PySnapshot as SnapshotPyO3
from pants.engine.internals.scheduler import ExecutionError
from pants.engine.rules import Get, goal_rule, rule
from pants.testutil.rule_runner import QueryRule, RuleRunner
Expand Down Expand Up @@ -1093,60 +1091,50 @@ def is_changed_snapshot() -> bool:
# -----------------------------------------------------------------------------------------------


@pytest.mark.parametrize("cls", [Digest, DigestPyO3])
def test_digest_properties(cls) -> None:
digest = cls("a" * 64, 1000)
def test_digest_properties() -> None:
digest = Digest("a" * 64, 1000)
assert digest.fingerprint == "a" * 64
assert digest.serialized_bytes_length == 1000


@pytest.mark.parametrize("cls", [Digest, DigestPyO3])
def test_digest_repr(cls) -> None:
assert str(cls("a" * 64, 1)) == f"Digest({repr('a' * 64)}, 1)"
def test_digest_repr() -> None:
assert str(Digest("a" * 64, 1)) == f"Digest({repr('a' * 64)}, 1)"


@pytest.mark.parametrize("cls", [Digest, DigestPyO3])
def test_digest_hash(cls) -> None:
assert hash(cls("a" * 64, 1)) == -6148914691236517206
assert hash(cls("b" * 64, 1)) == -4919131752989213765
def test_digest_hash() -> None:
assert hash(Digest("a" * 64, 1)) == -6148914691236517206
assert hash(Digest("b" * 64, 1)) == -4919131752989213765
# Note that the size bytes is not considered in the hash.
assert hash(cls("a" * 64, 1000)) == -6148914691236517206
assert hash(Digest("a" * 64, 1000)) == -6148914691236517206


@pytest.mark.parametrize("cls", [Digest, DigestPyO3])
def test_digest_equality(cls) -> None:
digest = cls("a" * 64, 1)
assert digest == cls("a" * 64, 1)
assert digest != cls("a" * 64, 1000)
assert digest != cls("0" * 64, 1)
def test_digest_equality() -> None:
digest = Digest("a" * 64, 1)
assert digest == Digest("a" * 64, 1)
assert digest != Digest("a" * 64, 1000)
assert digest != Digest("0" * 64, 1)
with pytest.raises(TypeError):
digest < digest
digest < digest # type: ignore[operator]


@pytest.mark.parametrize(
"snapshot_cls,digest_cls", [(Snapshot, Digest), (SnapshotPyO3, DigestPyO3)]
)
def test_snapshot_properties(snapshot_cls, digest_cls) -> None:
digest = digest_cls("a" * 64, 1000)
snapshot = snapshot_cls._create_for_testing(digest, ["f.ext", "dir/f.ext"], ["dir"])
def test_snapshot_properties() -> None:
digest = Digest("a" * 64, 1000)
snapshot = Snapshot._create_for_testing(digest, ["f.ext", "dir/f.ext"], ["dir"])
assert snapshot.digest == digest
assert snapshot.files == ("f.ext", "dir/f.ext")
assert snapshot.dirs == ("dir",)


@pytest.mark.parametrize(
"snapshot_cls,digest_cls", [(Snapshot, Digest), (SnapshotPyO3, DigestPyO3)]
)
def test_snapshot_hash(snapshot_cls, digest_cls) -> None:
def test_snapshot_hash() -> None:
def assert_hash(
expected: int,
*,
digest_char: str = "a",
files: Optional[List[str]] = None,
dirs: Optional[List[str]] = None,
) -> None:
digest = digest_cls(digest_char * 64, 1000)
snapshot = snapshot_cls._create_for_testing(
digest = Digest(digest_char * 64, 1000)
snapshot = Snapshot._create_for_testing(
digest, files or ["f.ext", "dir/f.ext"], dirs or ["dir"]
)
assert hash(snapshot) == expected
Expand All @@ -1159,28 +1147,21 @@ def assert_hash(
assert_hash(-4919131752989213765, digest_char="b")


@pytest.mark.parametrize(
"snapshot_cls,digest_cls", [(Snapshot, Digest), (SnapshotPyO3, DigestPyO3)]
)
def test_snapshot_equality(snapshot_cls, digest_cls) -> None:
def test_snapshot_equality() -> None:
# Only the digest is used for equality.
snapshot = snapshot_cls._create_for_testing(
digest_cls("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"]
)
assert snapshot == snapshot_cls._create_for_testing(
digest_cls("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"]
)
assert snapshot == snapshot_cls._create_for_testing(
digest_cls("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["foo"]
snapshot = Snapshot._create_for_testing(Digest("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"])
assert snapshot == Snapshot._create_for_testing(
Digest("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"]
)
assert snapshot == snapshot_cls._create_for_testing(
digest_cls("a" * 64, 1000), ["f.ext"], ["dir"]
assert snapshot == Snapshot._create_for_testing(
Digest("a" * 64, 1000), ["f.ext", "dir/f.ext"], ["foo"]
)
assert snapshot != snapshot_cls._create_for_testing(
digest_cls("a" * 64, 0), ["f.ext", "dir/f.ext"], ["dir"]
assert snapshot == Snapshot._create_for_testing(Digest("a" * 64, 1000), ["f.ext"], ["dir"])
assert snapshot != Snapshot._create_for_testing(
Digest("a" * 64, 0), ["f.ext", "dir/f.ext"], ["dir"]
)
assert snapshot != snapshot_cls._create_for_testing(
digest_cls("b" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"]
assert snapshot != Snapshot._create_for_testing(
Digest("b" * 64, 1000), ["f.ext", "dir/f.ext"], ["dir"]
)
with pytest.raises(TypeError):
snapshot < snapshot
snapshot < snapshot # type: ignore[operator]
9 changes: 7 additions & 2 deletions src/python/pants/engine/internals/native_engine.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class RawFdRunner(Protocol):
command: str,
args: tuple[str, ...],
env: dict[str, str],
working_directory: bytes,
cancellation_latch: PySessionCancellationLatch,
stdin_fileno: int,
stdout_fileno: int,
Expand Down Expand Up @@ -149,6 +148,9 @@ class PyDigest:
def fingerprint(self) -> str: ...
@property
def serialized_bytes_length(self) -> int: ...
def __eq__(self, other: PyDigest | Any) -> bool: ...
def __hash__(self) -> int: ...
def __repr__(self) -> str: ...

class PySnapshot:
def __init__(self) -> None: ...
Expand All @@ -162,6 +164,9 @@ class PySnapshot:
def dirs(self) -> tuple[str, ...]: ...
@property
def files(self) -> tuple[str, ...]: ...
def __eq__(self, other: PySnapshot | Any) -> bool: ...
def __hash__(self) -> int: ...
def __repr__(self) -> str: ...
Comment on lines +167 to +169
Copy link
Member

@stuhood stuhood Nov 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is it necessary to declare these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary. MyPy was not complaining about anything, I was being proactive so users of this API know what is expected to work without having to look at Rust code.

I believe __repr__ and __eq__ are always meant to be implemented, just with defaults though that is the object instance.

Wdyt? Too noisy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not necessary, would leave them off probably. But doesn't really matter.


class PyExecutionRequest:
def __init__(
Expand All @@ -184,7 +189,7 @@ class PyGeneratorResponseGetMulti:
def __init__(self, gets: tuple[PyGeneratorResponseGet, ...]) -> None: ...

class PyNailgunServer:
pass
def port(self) -> int: ...

class PyRemotingOptions:
def __init__(self, **kwargs: Any) -> None: ...
Expand Down
26 changes: 0 additions & 26 deletions src/python/pants/engine/internals/native_engine_pyo3.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

from __future__ import annotations

from typing import Any, Sequence

from pants.engine.fs import PathGlobs

# TODO: black and flake8 disagree about the content of this file:
Expand All @@ -28,30 +26,6 @@ def default_cache_path() -> str: ...
# cast to `tuple()` when not necessary.
def match_path_globs(path_globs: PathGlobs, paths: tuple[str, ...]) -> str: ...

class PyDigest:
def __init__(self, fingerprint: str, serialized_bytes_length: int) -> None: ...
@property
def fingerprint(self) -> str: ...
@property
def serialized_bytes_length(self) -> int: ...
def __eq__(self, other: PyDigest | Any) -> bool: ...
def __hash__(self) -> int: ...

class PySnapshot:
def __init__(self) -> None: ...
@classmethod
def _create_for_testing(
cls, digest: PyDigest, files: Sequence[str], dirs: Sequence[str]
) -> PySnapshot: ...
@property
def digest(self) -> PyDigest: ...
@property
def dirs(self) -> tuple[str, ...]: ...
@property
def files(self) -> tuple[str, ...]: ...
def __eq__(self, other: PySnapshot | Any) -> bool: ...
def __hash__(self) -> int: ...

# ------------------------------------------------------------------------------
# Workunits
# ------------------------------------------------------------------------------
Expand Down
10 changes: 5 additions & 5 deletions src/python/pants/engine/internals/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,12 +469,12 @@ def execute(

states = [
Throw(
raw_root.result(),
python_traceback=raw_root.python_traceback(),
engine_traceback=raw_root.engine_traceback(),
raw_root.result,
python_traceback=raw_root.python_traceback,
engine_traceback=raw_root.engine_traceback,
Comment on lines -472 to +474
Copy link
Contributor Author

@Eric-Arellano Eric-Arellano Nov 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to avoid a lot of boilerplate by making these properties. PyO3 lets you do this:

#[pyclass]
struct Foo {
   #[pyo3(get)]
   v: String,
}

)
if raw_root.is_throw()
else Return(raw_root.result())
if raw_root.is_throw
else Return(raw_root.result)
for raw_root in raw_roots
]

Expand Down
35 changes: 3 additions & 32 deletions src/rust/engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions src/rust/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ crate-type = ["cdylib"]

[features]
# NB: To actually load this crate from python, the `extension-module` feature must be enabled. But
# unfortunately, enabling `extension-module` causes tests linked against `cpython` to fail. We
# unfortunately, enabling `extension-module` causes tests linked against `pyo3` to fail. We
# define a feature to enable that, but we do not enable it by default: someone building this module
# in order to extract `libengine.so` should pass `cargo build .. --features=extension-module`.
# see https://github.com/PyO3/pyo3/issues/340
extension-module = ["cpython/extension-module"]
extension-module = ["pyo3/extension-module"]
default = []

[dependencies]
Expand All @@ -114,8 +114,6 @@ protos = { path = "protos" }
bytes = "1.0"
cache = { path = "cache" }
concrete_time = { path = "concrete_time" }
# TODO: Go back to upstream once https://github.com/dgrunwald/rust-cpython/pull/261 lands.
cpython = { git = "https://github.com/pantsbuild/rust-cpython", rev = "46d7eff26a705384e41eb6f7b870cd3f5f14b3bc" }
crossbeam-channel = "0.4"
derivative = "2.2"
double-checked-cell-async = "2.0"
Expand All @@ -138,6 +136,7 @@ num_enum = "0.4"
parking_lot = "0.11"
petgraph = "0.5"
process_execution = { path = "process_execution" }
pyo3 = "0.15"
rand = "0.8"
regex = "1"
reqwest = { version = "0.11", default_features = false, features = ["stream", "rustls-tls"] }
Expand All @@ -163,6 +162,9 @@ testutil = { path = "./testutil" }
fs = { path = "./fs" }
env_logger = "0.5.4"

[build-dependencies]
pyo3-build-config = "0.15"

[patch.crates-io]
# TODO: Waiting for release after https://github.com/mitsuhiko/console/pull/93.
console = { git = "https://github.com/mitsuhiko/console", rev = "3232775295149e6f0aace26d4cfe61013ed3e2d8" }
13 changes: 2 additions & 11 deletions src/rust/engine/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,9 @@
#![allow(clippy::mutex_atomic)]

fn main() {
pyo3_build_config::add_extension_module_link_args();

// NB: The native extension only works with the Python interpreter version it was built with
// (e.g. Python 3.7 vs 3.8).
println!("cargo:rerun-if-env-changed=PY");

if cfg!(target_os = "macos") {
// N.B. On OSX, we force weak linking by passing the param `-undefined dynamic_lookup` to
// the underlying linker. This avoids "missing symbol" errors for Python symbols
// (e.g. `_PyImport_ImportModule`) at build time when bundling the cpython sources.
// The missing symbols will instead by dynamically resolved in the address space of the parent
// binary (e.g. `python`) at runtime. We do this to avoid needing to link to libpython
// (which would constrain us to specific versions of Python).
println!("cargo:rustc-cdylib-link-arg=-undefined");
println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
}
}
Loading