Struggling to implement dependency inference for s...
# plugins
w
Struggling to implement dependency inference for some QT project files.
The rule:
Copy code
# dependencies.py
from __future__ import annotations

import itertools
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable

from pants.backend.python.target_types import PythonSourceField
from pants.build_graph.address import Address
from pants.engine.addresses import Addresses
from pants.engine.fs import Digest
from pants.engine.fs import DigestContents
from pants.engine.internals.graph import Owners
from pants.engine.internals.graph import OwnersRequest
from pants.engine.internals.selectors import MultiGet
from pants.engine.rules import collect_rules
from pants.engine.rules import Get
from pants.engine.rules import rule
from pants.engine.rules import Rule
from pants.engine.rules import rule_helper
from pants.engine.target import FieldSet
from pants.engine.target import HydratedSources
from pants.engine.target import HydrateSourcesRequest
from pants.engine.target import InferDependenciesRequest
from pants.engine.target import InferredDependencies
from pants.engine.target import Targets
from pants.engine.unions import UnionRule

from .project_file import QtProjectFileDependenciesField
from .project_file import QtProjectSourceField
from .translation_file import QtTranslationSourceField
import logging

_logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class QtProjectFileInferenceFieldSet(FieldSet):
    required_fields = (QtProjectSourceField, QtProjectFileDependenciesField,)


class InferQtProjectFileDependencies(InferDependenciesRequest):
    infer_from = QtProjectFileInferenceFieldSet


@dataclass(frozen=True)
class QtProjectContents:
    path: str
    sources: Iterable[Address] = ()
    translations: Iterable[Address] = ()


@dataclass(frozen=True)
class ParseQtProjectFileRequest:
    address: Address


@rule_helper
async def _parse_qt_project_file(request: ParseQtProjectFileRequest) -> QtProjectContents:
    target = await Get(Targets, Addresses([request.address]))
    source = await Get(
        HydratedSources,
        HydrateSourcesRequest(
            target.expect_single()[QtProjectSourceField], for_sources_types=[QtProjectSourceField]
        ),
    )
    digest_contents = await Get(DigestContents, Digest, source.snapshot.digest)
    file_content = digest_contents[0].content.decode()
    continuation_lines_removed = file_content.replace('\\', '')

    files = []
    source_path = Path(source.snapshot.files[0])
    for maybe_address in continuation_lines_removed.split():
        if maybe_address.endswith('.py') or maybe_address.endswith('.ts'):
            files.append(source_path.parent / Path(maybe_address))
    owners = await MultiGet(Get(Owners, OwnersRequest((str(f),))) for f in files)
    owner_tgts = await Get(Targets, Addresses(itertools.chain.from_iterable(owners)))

    return QtProjectContents(
        path=digest_contents[0].path,
        sources=Addresses([tgt.address for tgt in owner_tgts if tgt.has_field(PythonSourceField)]),
        translations=Addresses(
            [tgt.address for tgt in owner_tgts if tgt.has_field(QtTranslationSourceField)]
        ),
    )


@rule
async def infer_qt_project_dependencies(
    request: InferQtProjectFileDependencies
) -> InferredDependencies:
    project_contents = await _parse_qt_project_file(
        ParseQtProjectFileRequest(request.field_set.address)
    )

    return InferredDependencies([*project_contents.sources, *project_contents.translations])


def dependency_rules() -> Iterable[Rule]:
    return [
        *collect_rules(),
        UnionRule(InferDependenciesRequest, InferQtProjectFileDependencies),
    ]
Copy code
# project_file.py
from __future__ import annotations

from pants.engine.target import COMMON_TARGET_FIELDS
from pants.engine.target import Dependencies
from pants.engine.target import SingleSourceField
from pants.engine.target import Target


class QtProjectFileDependenciesField(Dependencies):
    supports_transitive_excludes = True


class QtProjectSourceField(SingleSourceField):
    expected_file_extensions = ('.pro',)
    uses_source_roots = True


class QtProjectFileTarget(Target):
    alias = "qt_project_file"
    core_fields = (QtProjectSourceField, QtProjectFileDependenciesField, *COMMON_TARGET_FIELDS)
    help = "A qt project file."
Copy code
# register.py
from __future__ import annotations

from typing import Iterable

from pants.engine.rules import Rule
from pants.engine.target import Target

from .dependencies import dependency_rules
from .project_file import QtProjectFileTarget
from .translation_file import QtTranslationFileTarget


def rules() -> Iterable[Rule]:
    return [
        *dependency_rules(),
    ]


def target_types() -> Iterable[type(Target)]:
    return [QtProjectFileTarget, QtTranslationFileTarget]
Copy code
# translation_file.py

from __future__ import annotations

from pants.engine.target import COMMON_TARGET_FIELDS
from pants.engine.target import SingleSourceField
from pants.engine.target import Target


class QtTranslationSourceField(SingleSourceField):
    expected_file_extensions = ('.ts',)

    uses_source_roots = True


class QtTranslationFileTarget(Target):
    alias = "qt_translation_file"
    core_fields = (QtTranslationSourceField, *COMMON_TARGET_FIELDS)
    help = "A qt translation file."
My understanding was that the magic should happen as soon as I register the union rule
Ìt isn't broken, pants runs fine, but I also do not see the rule being executed when I make a target like so
Copy code
# BUILD

qt_project_file(name="qt_project", source="<http://file.pro|file.pro>")`
Pants recognizes the target type and fields, it just wont run
infer_qt_project_dependencies
when I do
./pants dependencies <http://file.pro|file.pro>
I've essentially tried to mirror the docker backend, fwiw
Hmm, managed to get it running in a test.
Copy code
def test_dependencies_goal(self, rule_runner: RuleRunner):
        rule_runner.write_files(
            {
                "project/project.pro": "SOURCES += file.py",
                "project/file.py": "print('hi')",
                "project/BUILD": dedent(
                    """\
                    python_sources()
                    qt_project_file(name="qt_project", source="<http://project.pro|project.pro>")
                    """
                ),
            }
        )
        result = rule_runner.run_goal_rule(Dependencies, args=["project:qt_project"])
        assert result.stdout == "project/file.py\n"
passes so the test setup is somehow different from real world in a not obvious way to me...
This turned out to solve itself by upgrading from 2.13.1 to 2.14.0
🎉 1