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

thrift: add scrooge java backend #14043

Merged
merged 5 commits into from Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/python/pants/backend/codegen/thrift/scrooge/java/BUILD
@@ -0,0 +1,6 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()

python_tests(name="tests")
Empty file.
72 changes: 72 additions & 0 deletions src/python/pants/backend/codegen/thrift/scrooge/java/rules.py
@@ -0,0 +1,72 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from pants.backend.codegen.thrift.scrooge.java.subsystem import ScroogeJavaSubsystem
from pants.backend.codegen.thrift.scrooge.rules import (
GeneratedScroogeThriftSources,
GenerateScroogeThriftSourcesRequest,
)
from pants.backend.codegen.thrift.target_types import ThriftDependenciesField, ThriftSourceField
from pants.backend.java.target_types import JavaSourceField
from pants.engine.addresses import Addresses, UnparsedAddressInputs
from pants.engine.fs import AddPrefix, Digest, Snapshot
from pants.engine.internals.selectors import Get
from pants.engine.rules import collect_rules, rule
from pants.engine.target import (
GeneratedSources,
GenerateSourcesRequest,
InjectDependenciesRequest,
InjectedDependencies,
)
from pants.engine.unions import UnionRule
from pants.source.source_root import SourceRoot, SourceRootRequest
from pants.util.logging import LogLevel


class GenerateJavaFromThriftRequest(GenerateSourcesRequest):
input = ThriftSourceField
output = JavaSourceField


class InjectScroogeJavaDependencies(InjectDependenciesRequest):
inject_for = ThriftDependenciesField


@rule(desc="Generate Java from Thrift with Scrooge", level=LogLevel.DEBUG)
async def generate_java_from_thrift_with_scrooge(
request: GenerateJavaFromThriftRequest,
) -> GeneratedSources:
result = await Get(
GeneratedScroogeThriftSources,
GenerateScroogeThriftSourcesRequest(
thrift_source_field=request.protocol_target[ThriftSourceField],
lang_id="java",
lang_name="Java",
),
)

source_root = await Get(
SourceRoot, SourceRootRequest, SourceRootRequest.for_target(request.protocol_target)
)

source_root_restored = (
await Get(Snapshot, AddPrefix(result.snapshot.digest, source_root.path))
if source_root.path != "."
else await Get(Snapshot, Digest, result.snapshot.digest)
)
return GeneratedSources(source_root_restored)


@rule
async def inject_scrooge_java_dependencies(
_: InjectScroogeJavaDependencies, scrooge: ScroogeJavaSubsystem
) -> InjectedDependencies:
addresses = await Get(Addresses, UnparsedAddressInputs, scrooge.runtime_dependencies)
return InjectedDependencies(addresses)


def rules():
return (
*collect_rules(),
UnionRule(GenerateSourcesRequest, GenerateJavaFromThriftRequest),
UnionRule(InjectDependenciesRequest, InjectScroogeJavaDependencies),
)
@@ -0,0 +1,172 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from textwrap import dedent

import pytest

from pants.backend.codegen.thrift.rules import rules as thrift_rules
from pants.backend.codegen.thrift.scrooge.java.rules import GenerateJavaFromThriftRequest
from pants.backend.codegen.thrift.scrooge.java.rules import rules as scrooge_java_rules
from pants.backend.codegen.thrift.scrooge.rules import rules as scrooge_rules
from pants.backend.codegen.thrift.target_types import (
ThriftSourceField,
ThriftSourcesGeneratorTarget,
)
from pants.backend.scala import target_types
from pants.backend.scala.compile.scalac import rules as scalac_rules
from pants.backend.scala.target_types import ScalaSourcesGeneratorTarget, ScalaSourceTarget
from pants.build_graph.address import Address
from pants.core.util_rules import config_files, source_files, stripped_source_files
from pants.core.util_rules.external_tool import rules as external_tool_rules
from pants.engine.rules import QueryRule
from pants.engine.target import GeneratedSources, HydratedSources, HydrateSourcesRequest
from pants.jvm import classpath
from pants.jvm.jdk_rules import rules as jdk_rules
from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules
from pants.jvm.resolve.coursier_setup import rules as coursier_setup_rules
from pants.jvm.util_rules import rules as util_rules
from pants.testutil.rule_runner import PYTHON_BOOTSTRAP_ENV, RuleRunner


@pytest.fixture
def rule_runner() -> RuleRunner:
rule_runner = RuleRunner(
rules=[
*thrift_rules(),
*scrooge_rules(),
*scrooge_java_rules(),
*config_files.rules(),
*classpath.rules(),
*coursier_fetch_rules(),
*coursier_setup_rules(),
*external_tool_rules(),
*source_files.rules(),
*scalac_rules(),
*util_rules(),
*jdk_rules(),
*target_types.rules(),
*stripped_source_files.rules(),
QueryRule(HydratedSources, [HydrateSourcesRequest]),
QueryRule(GeneratedSources, [GenerateJavaFromThriftRequest]),
],
target_types=[
ScalaSourceTarget,
ScalaSourcesGeneratorTarget,
ThriftSourcesGeneratorTarget,
],
)
rule_runner.set_options(
[],
env_inherit=PYTHON_BOOTSTRAP_ENV,
)
return rule_runner


def assert_files_generated(
rule_runner: RuleRunner,
address: Address,
*,
expected_files: list[str],
source_roots: list[str],
extra_args: list[str] | None = None,
) -> None:
args = [f"--source-root-patterns={repr(source_roots)}", *(extra_args or ())]
rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
tgt = rule_runner.get_target(address)
thrift_sources = rule_runner.request(
HydratedSources, [HydrateSourcesRequest(tgt[ThriftSourceField])]
)
generated_sources = rule_runner.request(
GeneratedSources,
[GenerateJavaFromThriftRequest(thrift_sources.snapshot, tgt)],
)
assert set(generated_sources.snapshot.files) == set(expected_files)


def test_generates_java(rule_runner: RuleRunner) -> None:
# This tests a few things:
# * We generate the correct file names.
# * Thrift files can import other thrift files, and those can import others
# (transitive dependencies). We'll only generate the requested target, though.
# * We can handle multiple source roots, which need to be preserved in the final output.
rule_runner.write_files(
{
"src/thrift/dir1/f.thrift": dedent(
"""\
namespace java org.pantsbuild.example
struct Person {
1: string name
2: i32 id
3: string email
}
"""
),
"src/thrift/dir1/f2.thrift": dedent(
"""\
namespace java org.pantsbuild.example
include "dir1/f.thrift"
struct ManagedPerson {
1: f.Person employee
2: f.Person manager
}
"""
),
"src/thrift/dir1/BUILD": "thrift_sources()",
"src/thrift/dir2/g.thrift": dedent(
"""\
namespace java org.pantsbuild.example
include "dir1/f2.thrift"
struct ManagedPersonWrapper {
1: f2.ManagedPerson managed_person
}
"""
),
"src/thrift/dir2/BUILD": "thrift_sources(dependencies=['src/thrift/dir1'])",
# Test another source root.
"tests/thrift/test_thrifts/f.thrift": dedent(
"""\
namespace java org.pantsbuild.example
include "dir2/g.thrift"
struct Executive {
1: g.ManagedPersonWrapper managed_person_wrapper
}
"""
),
"tests/thrift/test_thrifts/BUILD": "thrift_sources(dependencies=['src/thrift/dir2'])",
}
)

def assert_gen(addr: Address, expected: list[str]) -> None:
assert_files_generated(
rule_runner,
addr,
source_roots=["src/python", "/src/thrift", "/tests/thrift"],
expected_files=expected,
)

assert_gen(
Address("src/thrift/dir1", relative_file_path="f.thrift"),
[
"src/thrift/org/pantsbuild/example/Person.java",
],
)
assert_gen(
Address("src/thrift/dir1", relative_file_path="f2.thrift"),
[
"src/thrift/org/pantsbuild/example/ManagedPerson.java",
],
)
assert_gen(
Address("src/thrift/dir2", relative_file_path="g.thrift"),
[
"src/thrift/org/pantsbuild/example/ManagedPersonWrapper.java",
],
)
assert_gen(
Address("tests/thrift/test_thrifts", relative_file_path="f.thrift"),
[
"tests/thrift/org/pantsbuild/example/Executive.java",
],
)
33 changes: 33 additions & 0 deletions src/python/pants/backend/codegen/thrift/scrooge/java/subsystem.py
@@ -0,0 +1,33 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations

from pants.engine.addresses import UnparsedAddressInputs
from pants.option.custom_types import target_option
from pants.option.subsystem import Subsystem


class ScroogeJavaSubsystem(Subsystem):
options_scope = "scrooge-java"
help = "Java-specific options for the Scrooge Thrift IDL compiler (https://twitter.github.io/scrooge/)."

@classmethod
def register_options(cls, register):
super().register_options(register)
register(
"--runtime-dependencies",
type=list,
member_type=target_option,
help=(
"A list of addresses to `jvm_artifact` targets for the runtime "
"dependencies needed for generated Java code to work. For example, "
"`['3rdparty/jvm:libthrift']`. These dependencies will "
"be automatically added to every `thrift_source` target. At the very least, "
"this option must be set to a `jvm_artifact` for the "
"`org.apache.thrift:libthrift` runtime library."
),
)

@property
def runtime_dependencies(self) -> UnparsedAddressInputs:
return UnparsedAddressInputs(self.options.runtime_dependencies, owning_address=None)
Expand Up @@ -96,7 +96,7 @@ def test_generates_python(rule_runner: RuleRunner) -> None:
{
"src/thrift/dir1/f.thrift": dedent(
"""\
namespace py dir1
#@namespace scala org.pantsbuild.example
struct Person {
1: string name
2: i32 id
Expand All @@ -106,7 +106,7 @@ def test_generates_python(rule_runner: RuleRunner) -> None:
),
"src/thrift/dir1/f2.thrift": dedent(
"""\
namespace py dir1
#@namespace scala org.pantsbuild.example
include "dir1/f.thrift"
struct ManagedPerson {
1: f.Person employee
Expand All @@ -117,6 +117,7 @@ def test_generates_python(rule_runner: RuleRunner) -> None:
"src/thrift/dir1/BUILD": "thrift_sources()",
"src/thrift/dir2/g.thrift": dedent(
"""\
#@namespace scala org.pantsbuild.example
include "dir1/f2.thrift"
struct ManagedPersonWrapper {
1: f2.ManagedPerson managed_person
Expand All @@ -127,6 +128,7 @@ def test_generates_python(rule_runner: RuleRunner) -> None:
# Test another source root.
"tests/thrift/test_thrifts/f.thrift": dedent(
"""\
#@namespace scala org.pantsbuild.example
include "dir2/g.thrift"
struct Executive {
1: g.ManagedPersonWrapper managed_person_wrapper
Expand All @@ -145,30 +147,27 @@ def assert_gen(addr: Address, expected: list[str]) -> None:
expected_files=expected,
)

# TODO: Why is generated path `src/thrift/thrift`?
assert_gen(
Address("src/thrift/dir1", relative_file_path="f.thrift"),
[
"src/thrift/thrift/Person.scala",
"src/thrift/org/pantsbuild/example/Person.scala",
],
)
assert_gen(
Address("src/thrift/dir1", relative_file_path="f2.thrift"),
[
"src/thrift/thrift/ManagedPerson.scala",
"src/thrift/org/pantsbuild/example/ManagedPerson.scala",
],
)
# TODO: Fix package namespacing?
assert_gen(
Address("src/thrift/dir2", relative_file_path="g.thrift"),
[
"src/thrift/thrift/ManagedPersonWrapper.scala",
"src/thrift/org/pantsbuild/example/ManagedPersonWrapper.scala",
],
)
# TODO: Fix namespacing.
assert_gen(
Address("tests/thrift/test_thrifts", relative_file_path="f.thrift"),
[
"tests/thrift/thrift/Executive.scala",
"tests/thrift/org/pantsbuild/example/Executive.scala",
],
)
@@ -0,0 +1,4 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()