From 096010beccfdf635bab87a3dff6516b3d4f5bdbc Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 10 Jun 2022 23:04:17 +0200 Subject: [PATCH] Add new `visibility` field to targets to limit target dependencies. Also introduces a new `defaults` target which allows to dynamically provide "scoped" default field values. Closes #13393. [ci skip-rust] [ci skip-build-wheels] --- src/python/pants/core/register.py | 8 +- src/python/pants/core/target_types.py | 44 ++++++++++- .../pants/engine/internals/visibility.py | 74 +++++++++++++++++++ src/python/pants/engine/target.py | 33 ++++++++- src/python/pants/init/engine_initializer.py | 3 +- 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/python/pants/engine/internals/visibility.py diff --git a/src/python/pants/core/register.py b/src/python/pants/core/register.py index 53425a48a01..80d1c9d59d8 100644 --- a/src/python/pants/core/register.py +++ b/src/python/pants/core/register.py @@ -23,6 +23,7 @@ ) from pants.core.target_types import ( ArchiveTarget, + DefaultsTarget, FilesGeneratorTarget, FileTarget, GenericTarget, @@ -84,12 +85,13 @@ def rules(): def target_types(): return [ ArchiveTarget, - FileTarget, + DefaultsTarget, FilesGeneratorTarget, + FileTarget, GenericTarget, - ResourceTarget, - ResourcesGeneratorTarget, RelocatedFiles, + ResourcesGeneratorTarget, + ResourceTarget, ] diff --git a/src/python/pants/core/target_types.py b/src/python/pants/core/target_types.py index 33f380c174c..a802d6e8437 100644 --- a/src/python/pants/core/target_types.py +++ b/src/python/pants/core/target_types.py @@ -187,8 +187,10 @@ async def _hydrate_asset_source(request: GenerateSourcesRequest) -> GeneratedSou # ----------------------------------------------------------------------------------------------- -# `file` and`files` targets +# `file` and `files` targets # ----------------------------------------------------------------------------------------------- + + class FileSourceField(AssetSourceField): uses_source_roots = False @@ -708,6 +710,46 @@ async def package_archive_target(field_set: ArchiveFieldSet) -> BuiltPackage: return BuiltPackage(archive, (BuiltPackageArtifact(output_filename),)) +# ----------------------------------------------------------------------------------------------- +# `defaults` target +# ----------------------------------------------------------------------------------------------- + + +class DefaultsTarget(Target): + alias = "defaults" + core_fields = COMMON_TARGET_FIELDS + help = softwrap( + """ + A meta target providing scoped default values for all closest targets from current directory + and below. + """ + ) + + +@dataclass(frozen=True) +class DefaultsRequest: + path: str + target_name: str | None = None + + @classmethod + def for_target(cls, target: Target, defaults_name: str | None = None) -> DefaultsRequest: + return cls(target.address.spec_path, defaults_name) + + +@rule +async def get_defaults(request: DefaultsRequest, all_targets: AllTargets) -> Targets: + """Fetch all applicable `defaults` targets, sorted on specificity (closest first).""" + defaults = [] + for tgt in all_targets: + if not isinstance(tgt, DefaultsTarget): + continue + if request.target_name in (None, tgt.address.target_name) and request.path.startswith( + tgt.address.spec_path + ): + defaults.append(tgt) + return Targets(sorted(defaults, key=lambda d: len(d.address.spec_path), reverse=True)) + + def rules(): return ( *collect_rules(), diff --git a/src/python/pants/engine/internals/visibility.py b/src/python/pants/engine/internals/visibility.py new file mode 100644 index 00000000000..336d0029f6f --- /dev/null +++ b/src/python/pants/engine/internals/visibility.py @@ -0,0 +1,74 @@ +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +from pants.core.target_types import DefaultsRequest +from pants.engine.rules import Get, MultiGet, collect_rules, rule +from pants.engine.target import ( + FieldSet, + Targets, + ValidatedDependencies, + ValidateDependenciesRequest, + VisibilityField, + VisibilityViolationError, + WrappedTarget, + WrappedTargetRequest, +) +from pants.engine.unions import UnionRule +from pants.util.strutil import softwrap + + +class ValidateVisibilityRulesFieldSet(FieldSet): + required_fields = (VisibilityField,) + + +class ValidateVisibilityRulesRequest(ValidateDependenciesRequest): + field_set_type = ValidateVisibilityRulesFieldSet + + +@rule +async def validate_visibility_rules( + request: ValidateVisibilityRulesRequest, +) -> ValidatedDependencies: + wrapped_dependency_targets = await MultiGet( + Get(WrappedTarget, WrappedTargetRequest(address, description_of_origin="")) + for address in request.dependencies + ) + dependency_targets = [wrapped.target for wrapped in wrapped_dependency_targets] + targets_defaults = await MultiGet( + Get(Targets, DefaultsRequest, DefaultsRequest.for_target(tgt, "visibility")) + for tgt in dependency_targets + ) + subject = request.field_set.address + for target, defaults in zip(dependency_targets, targets_defaults): + if defaults: + default_visibility = defaults[0].get(VisibilityField).value + default_origin = defaults[0] + else: + default_visibility = (VisibilityField.PUBLIC_VISIBILITY,) + default_origin = target + visibility_field = target.get(VisibilityField) + if visibility_field.value is not None: + origin = target + else: + origin = default_origin + if not visibility_field.visible(origin.address, subject, default=default_visibility): + raise VisibilityViolationError( + softwrap( + f""" + {target.address} is not visible to {subject}. + + Visibility from {origin.alias} {origin.address} : \ + {", ".join(visibility_field.get_visibility(default_visibility) or ("",))}. + """ + ) + ) + return ValidatedDependencies() + + +def rules(): + return ( + *collect_rules(), + UnionRule(ValidateDependenciesRequest, ValidateVisibilityRulesRequest), + ) diff --git a/src/python/pants/engine/target.py b/src/python/pants/engine/target.py index 07eb2e6d855..0edf9857150 100644 --- a/src/python/pants/engine/target.py +++ b/src/python/pants/engine/target.py @@ -1524,6 +1524,10 @@ def __init__( ) +class VisibilityViolationError(Exception): + pass + + # ----------------------------------------------------------------------------------------------- # Field templates # ----------------------------------------------------------------------------------------------- @@ -2658,7 +2662,34 @@ class DescriptionField(StringField): ) -COMMON_TARGET_FIELDS = (Tags, DescriptionField) +class VisibilityField(StringSequenceField): + PUBLIC_VISIBILITY = "public" + PRIVATE_VISIBILITY = "private" + + alias = "visibility" + help = softwrap( + """ + TBW. + """ + ) + + def get_visibility(self, default: tuple[str, ...] | None = None) -> tuple[str, ...]: + return next(value for value in (self.value, default, ()) if value is not None) + + def visible( + self, origin: Address, destination: Address, default: tuple[str, ...] | None = None + ) -> bool: + for rule in self.get_visibility(default): + if rule == self.PUBLIC_VISIBILITY: + return True + if rule == self.PRIVATE_VISIBILITY: + return destination.spec_path.startswith(origin.spec_path) + if destination.spec_path.startswith(rule): + return True + return False + + +COMMON_TARGET_FIELDS = (Tags, DescriptionField, VisibilityField) class OverridesField(AsyncFieldMixin, Field): diff --git a/src/python/pants/init/engine_initializer.py b/src/python/pants/init/engine_initializer.py index d696d33a73e..a4ca05a7899 100644 --- a/src/python/pants/init/engine_initializer.py +++ b/src/python/pants/init/engine_initializer.py @@ -19,7 +19,7 @@ from pants.engine.console import Console from pants.engine.fs import PathGlobs, Snapshot, Workspace from pants.engine.goal import Goal -from pants.engine.internals import build_files, graph, options_parsing, specs_rules +from pants.engine.internals import build_files, graph, options_parsing, specs_rules, visibility from pants.engine.internals.native_engine import PyExecutor, PySessionCancellationLatch from pants.engine.internals.parser import Parser from pants.engine.internals.scheduler import Scheduler, SchedulerSession @@ -266,6 +266,7 @@ def build_root_singleton() -> BuildRoot: *changed_rules(), *streaming_workunit_handler_rules(), *specs_calculator.rules(), + *visibility.rules(), *rules, ) )