async def pylint_lint(request: PylintRequest, pylint: Pylint, python_setup: PythonSetup) -> LintResults: if pylint.skip: return LintResults([], linter_name="Pylint") plugin_target_addresses = await Get(Addresses, UnparsedAddressInputs, pylint.source_plugins) plugin_targets_request = Get( TransitiveTargets, TransitiveTargetsRequest(plugin_target_addresses)) linted_targets_request = Get( Targets, Addresses(field_set.address for field_set in request.field_sets)) plugin_targets, linted_targets = await MultiGet(plugin_targets_request, linted_targets_request) plugin_targets_compatibility_fields = tuple( plugin_tgt[InterpreterConstraintsField] for plugin_tgt in plugin_targets.closure if plugin_tgt.has_field(InterpreterConstraintsField)) # Pylint needs direct dependencies in the chroot to ensure that imports are valid. However, it # doesn't lint those direct dependencies nor does it care about transitive dependencies. per_target_dependencies = await MultiGet( Get(Targets, DependenciesRequest(field_set.dependencies)) for field_set in request.field_sets) # We batch targets by their interpreter constraints to ensure, for example, that all Python 2 # targets run together and all Python 3 targets run together. # Note that Pylint uses the AST of the interpreter that runs it. So, we include any plugin # targets in this interpreter constraints calculation. interpreter_constraints_to_target_setup = defaultdict(set) for field_set, tgt, dependencies in zip(request.field_sets, linted_targets, per_target_dependencies): target_setup = PylintTargetSetup(field_set, Targets([tgt, *dependencies])) interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( ( *(tgt[InterpreterConstraintsField] for tgt in [tgt, *dependencies] if tgt.has_field(InterpreterConstraintsField)), *plugin_targets_compatibility_fields, ), python_setup, ) interpreter_constraints_to_target_setup[interpreter_constraints].add( target_setup) partitions = (PylintPartition( tuple( sorted(target_setups, key=lambda tgt_setup: tgt_setup.field_set.address)), interpreter_constraints, Targets(plugin_targets.closure), ) for interpreter_constraints, target_setups in sorted( interpreter_constraints_to_target_setup.items())) partitioned_results = await MultiGet( Get(LintResult, PylintPartition, partition) for partition in partitions) return LintResults(partitioned_results, linter_name="Pylint")
async def mypy_typecheck(request: MyPyRequest, mypy: MyPy, python_setup: PythonSetup) -> TypecheckResults: if mypy.skip: return TypecheckResults([], typechecker_name="MyPy") # We batch targets by their interpreter constraints to ensure, for example, that all Python 2 # targets run together and all Python 3 targets run together. We can only do this by setting # the `--python-version` option, but we allow the user to set it as a safety valve. We warn if # they've set the option. config_content = await Get(DigestContents, PathGlobs, config_path_globs(mypy)) python_version_configured = check_and_warn_if_python_version_configured( config=next(iter(config_content), None), args=mypy.args) # When determining how to batch by interpreter constraints, we must consider the entire # transitive closure to get the final resulting constraints. # TODO(#10863): Improve the performance of this. transitive_targets_per_field_set = await MultiGet( Get(TransitiveTargets, TransitiveTargetsRequest([field_set.address])) for field_set in request.field_sets) interpreter_constraints_to_transitive_targets = defaultdict(set) for transitive_targets in transitive_targets_per_field_set: interpreter_constraints = ( PexInterpreterConstraints.create_from_compatibility_fields( (tgt[PythonInterpreterCompatibility] for tgt in transitive_targets.closure if tgt.has_field(PythonInterpreterCompatibility)), python_setup, ) or PexInterpreterConstraints(mypy.interpreter_constraints)) interpreter_constraints_to_transitive_targets[ interpreter_constraints].add(transitive_targets) partitions = [] for interpreter_constraints, all_transitive_targets in sorted( interpreter_constraints_to_transitive_targets.items()): combined_roots: OrderedSet[Address] = OrderedSet() combined_closure: OrderedSet[Target] = OrderedSet() for transitive_targets in all_transitive_targets: combined_roots.update(tgt.address for tgt in transitive_targets.roots) combined_closure.update(transitive_targets.closure) partitions.append( MyPyPartition( FrozenOrderedSet(combined_roots), FrozenOrderedSet(combined_closure), interpreter_constraints, python_version_already_configured=python_version_configured, )) partitioned_results = await MultiGet( Get(TypecheckResult, MyPyPartition, partition) for partition in partitions) return TypecheckResults(partitioned_results, typechecker_name="MyPy")
async def package_python_dist( field_set: PythonDistributionFieldSet, python_setup: PythonSetup, ) -> BuiltPackage: transitive_targets = await Get( TransitiveTargets, TransitiveTargetsRequest([field_set.address])) exported_target = ExportedTarget(transitive_targets.roots[0]) interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( (tgt[PythonInterpreterCompatibility] for tgt in transitive_targets.dependencies if tgt.has_field(PythonInterpreterCompatibility)), python_setup, ) chroot = await Get( SetupPyChroot, SetupPyChrootRequest(exported_target, py2=interpreter_constraints.includes_python2()), ) # If commands were provided, run setup.py with them; Otherwise just dump chroots. commands = exported_target.target.get(SetupPyCommandsField).value or () if commands: validate_commands(commands) setup_py_result = await Get( RunSetupPyResult, RunSetupPyRequest(exported_target, interpreter_constraints, chroot, commands), ) dist_snapshot = await Get(Snapshot, Digest, setup_py_result.output) return BuiltPackage( setup_py_result.output, tuple(BuiltPackageArtifact(path) for path in dist_snapshot.files), ) else: dirname = f"{chroot.setup_kwargs.name}-{chroot.setup_kwargs.version}" rel_chroot = await Get(Digest, AddPrefix(chroot.digest, dirname)) return BuiltPackage(rel_chroot, (BuiltPackageArtifact(dirname), ))
async def pex_from_targets(request: PexFromTargetsRequest, python_setup: PythonSetup) -> PexRequest: if request.direct_deps_only: targets = await Get(Targets, Addresses(request.addresses)) direct_deps = await MultiGet( Get(Targets, DependenciesRequest(tgt.get(Dependencies))) for tgt in targets) all_targets = FrozenOrderedSet(itertools.chain(*direct_deps, targets)) else: transitive_targets = await Get( TransitiveTargets, TransitiveTargetsRequest(request.addresses)) all_targets = transitive_targets.closure input_digests = [] if request.additional_sources: input_digests.append(request.additional_sources) if request.include_source_files: prepared_sources = await Get(StrippedPythonSourceFiles, PythonSourceFilesRequest(all_targets)) input_digests.append( prepared_sources.stripped_source_files.snapshot.digest) merged_input_digest = await Get(Digest, MergeDigests(input_digests)) if request.hardcoded_interpreter_constraints: interpreter_constraints = request.hardcoded_interpreter_constraints else: calculated_constraints = PexInterpreterConstraints.create_from_compatibility_fields( (tgt[PythonInterpreterCompatibility] for tgt in all_targets if tgt.has_field(PythonInterpreterCompatibility)), python_setup, ) # If there are no targets, we fall back to the global constraints. This is relevant, # for example, when running `./pants repl` with no specs. interpreter_constraints = calculated_constraints or PexInterpreterConstraints( python_setup.interpreter_constraints) exact_reqs = PexRequirements.create_from_requirement_fields( (tgt[PythonRequirementsField] for tgt in all_targets if tgt.has_field(PythonRequirementsField)), additional_requirements=request.additional_requirements, ) requirements = exact_reqs description = request.description if python_setup.requirement_constraints: # In requirement strings Foo_-Bar.BAZ and foo-bar-baz refer to the same project. We let # packaging canonicalize for us. # See: https://www.python.org/dev/peps/pep-0503/#normalized-names exact_req_projects = { canonicalize_project_name(Requirement.parse(req).project_name) for req in exact_reqs } constraints_file_contents = await Get( DigestContents, PathGlobs( [python_setup.requirement_constraints], glob_match_error_behavior=GlobMatchErrorBehavior.error, conjunction=GlobExpansionConjunction.all_match, description_of_origin= "the option `--python-setup-requirement-constraints`", ), ) constraints_file_reqs = set( parse_requirements( next(iter(constraints_file_contents)).content.decode())) constraint_file_projects = { canonicalize_project_name(req.project_name) for req in constraints_file_reqs } unconstrained_projects = exact_req_projects - constraint_file_projects if unconstrained_projects: logger.warning( f"The constraints file {python_setup.requirement_constraints} does not contain " f"entries for the following requirements: {', '.join(unconstrained_projects)}" ) if python_setup.resolve_all_constraints == ResolveAllConstraintsOption.ALWAYS or ( python_setup.resolve_all_constraints == ResolveAllConstraintsOption.NONDEPLOYABLES and request.internal_only): if unconstrained_projects: logger.warning( "Ignoring resolve_all_constraints setting in [python_setup] scope " "because constraints file does not cover all requirements." ) else: requirements = PexRequirements( str(req) for req in constraints_file_reqs) description = description or f"Resolving {python_setup.requirement_constraints}" elif (python_setup.resolve_all_constraints != ResolveAllConstraintsOption.NEVER and python_setup.resolve_all_constraints_was_set_explicitly()): raise ValueError( f"[python-setup].resolve_all_constraints is set to " f"{python_setup.resolve_all_constraints.value}, so " f"[python-setup].requirement_constraints must also be provided.") return PexRequest( output_filename=request.output_filename, internal_only=request.internal_only, requirements=requirements, interpreter_constraints=interpreter_constraints, platforms=request.platforms, entry_point=request.entry_point, sources=merged_input_digest, additional_inputs=request.additional_inputs, additional_args=request.additional_args, description=description, )
async def run_setup_pys( targets_with_origins: TargetsWithOrigins, setup_py_subsystem: SetupPySubsystem, python_setup: PythonSetup, distdir: DistDir, workspace: Workspace, union_membership: UnionMembership, ) -> SetupPy: logger.warning( "The `setup_py` goal is deprecated in favor of the `package` goal, which behaves " "similarly, except that you specify setup.py commands using the `setup_py_commands` " "field on your `python_distribution` targets, instead of on the command line. " "`setup_py` will be removed in 2.1.0.dev0.") """Run setup.py commands on all exported targets addressed.""" validate_commands(setup_py_subsystem.args) # Get all exported targets, ignoring any non-exported targets that happened to be # globbed over, but erroring on any explicitly-requested non-exported targets. exported_targets: List[ExportedTarget] = [] explicit_nonexported_targets: List[Target] = [] for target_with_origin in targets_with_origins: tgt = target_with_origin.target if tgt.has_field(PythonProvidesField): exported_targets.append(ExportedTarget(tgt)) elif isinstance(target_with_origin.origin, (AddressLiteralSpec, FilesystemLiteralSpec)): explicit_nonexported_targets.append(tgt) if explicit_nonexported_targets: raise TargetNotExported( "Cannot run setup.py on these targets, because they have no `provides=` clause: " f'{", ".join(so.address.spec for so in explicit_nonexported_targets)}' ) transitive_targets_per_exported_target = await MultiGet( Get(TransitiveTargets, TransitiveTargetsRequest([et.target.address])) for et in exported_targets) if setup_py_subsystem.transitive: closure = FrozenOrderedSet( itertools.chain.from_iterable( tt.closure for tt in transitive_targets_per_exported_target)) owners = await MultiGet( Get(ExportedTarget, OwnedDependency(tgt)) for tgt in closure if is_ownable_target(tgt, union_membership)) exported_targets = list(FrozenOrderedSet(owners)) # We must recalculate the transitive targets because it's possible the exported_targets # have changed. Any prior results will be memoized. transitive_targets_per_exported_target = await MultiGet( Get(TransitiveTargets, TransitiveTargetsRequest( [et.target.address])) for et in exported_targets) interpreter_constraints_per_exported_target = tuple( PexInterpreterConstraints.create_from_compatibility_fields( (tgt[PythonInterpreterCompatibility] for tgt in transitive_targets.dependencies if tgt.has_field(PythonInterpreterCompatibility)), python_setup, ) for transitive_targets in transitive_targets_per_exported_target) chroots = await MultiGet( Get( SetupPyChroot, SetupPyChrootRequest( exported_target, py2=interpreter_constraints.includes_python2()), ) for exported_target, interpreter_constraints in zip( exported_targets, interpreter_constraints_per_exported_target)) # If args were provided, run setup.py with them; Otherwise just dump chroots. if setup_py_subsystem.args: setup_py_results = await MultiGet( Get( RunSetupPyResult, RunSetupPyRequest(exported_target, interpreter_constraints, chroot, setup_py_subsystem.args), ) for exported_target, interpreter_constraints, chroot in zip( exported_targets, interpreter_constraints_per_exported_target, chroots)) for exported_target, setup_py_result in zip(exported_targets, setup_py_results): addr = exported_target.target.address.spec logger.info(f"Writing dist for {addr} under {distdir.relpath}/.") workspace.write_digest(setup_py_result.output, path_prefix=str(distdir.relpath)) else: # Just dump the chroot. for exported_target, chroot in zip(exported_targets, chroots): addr = exported_target.target.address.spec setup_py_dir = ( distdir.relpath / f"{chroot.setup_kwargs.name}-{chroot.setup_kwargs.version}") logger.info( f"Writing setup.py chroot for {addr} to {setup_py_dir}") workspace.write_digest(chroot.digest, path_prefix=str(setup_py_dir)) return SetupPy(0)
async def setup_black( setup_request: SetupRequest, black: Black, python_setup: PythonSetup ) -> Setup: # Black requires 3.6+ but uses the typed-ast library to work with 2.7, 3.4, 3.5, 3.6, and 3.7. # However, typed-ast does not understand 3.8+, so instead we must run Black with Python 3.8+ # when relevant. We only do this if if <3.8 can't be used, as we don't want a loose requirement # like `>=3.6` to result in requiring Python 3.8, which would error if 3.8 is not installed on # the machine. all_interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( (field_set.interpreter_constraints for field_set in setup_request.request.field_sets), python_setup, ) tool_interpreter_constraints = ( all_interpreter_constraints if ( all_interpreter_constraints.requires_python38_or_newer() and black.options.is_default("interpreter_constraints") ) else PexInterpreterConstraints(black.interpreter_constraints) ) black_pex_get = Get( VenvPex, PexRequest( output_filename="black.pex", internal_only=True, requirements=PexRequirements(black.all_requirements), interpreter_constraints=tool_interpreter_constraints, main=black.main, ), ) config_files_get = Get(ConfigFiles, ConfigFilesRequest, black.config_request) source_files_get = Get( SourceFiles, SourceFilesRequest(field_set.sources for field_set in setup_request.request.field_sets), ) source_files, black_pex, config_files = await MultiGet( source_files_get, black_pex_get, config_files_get ) source_files_snapshot = ( source_files.snapshot if setup_request.request.prior_formatter_result is None else setup_request.request.prior_formatter_result ) input_digest = await Get( Digest, MergeDigests((source_files_snapshot.digest, config_files.snapshot.digest)) ) process = await Get( Process, VenvPexProcess( black_pex, argv=generate_args( source_files=source_files, black=black, check_only=setup_request.check_only ), input_digest=input_digest, output_files=source_files_snapshot.files, description=f"Run Black on {pluralize(len(setup_request.request.field_sets), 'file')}.", level=LogLevel.DEBUG, ), ) return Setup(process, original_digest=source_files_snapshot.digest)
async def setup_black(setup_request: SetupRequest, black: Black, python_setup: PythonSetup) -> Setup: # Black requires 3.6+ but uses the typed-ast library to work with 2.7, 3.4, 3.5, 3.6, and 3.7. # However, typed-ast does not understand 3.8, so instead we must run Black with Python 3.8 when # relevant. We only do this if if <3.8 can't be used, as we don't want a loose requirement like # `>=3.6` to result in requiring Python 3.8, which would error if 3.8 is not installed on the # machine. all_interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( (field_set.interpreter_constraints for field_set in setup_request.request.field_sets), python_setup, ) tool_interpreter_constraints = PexInterpreterConstraints(( "CPython>=3.8", ) if ( all_interpreter_constraints.requires_python38_or_newer() and black.options.is_default("interpreter_constraints") ) else black.interpreter_constraints) black_pex_request = Get( Pex, PexRequest( output_filename="black.pex", internal_only=True, requirements=PexRequirements(black.all_requirements), interpreter_constraints=tool_interpreter_constraints, entry_point=black.entry_point, ), ) config_digest_request = Get( Digest, PathGlobs( globs=[black.config] if black.config else [], glob_match_error_behavior=GlobMatchErrorBehavior.error, description_of_origin="the option `--black-config`", ), ) source_files_request = Get( SourceFiles, SourceFilesRequest(field_set.sources for field_set in setup_request.request.field_sets), ) source_files, black_pex, config_digest = await MultiGet( source_files_request, black_pex_request, config_digest_request) source_files_snapshot = ( source_files.snapshot if setup_request.request.prior_formatter_result is None else setup_request.request.prior_formatter_result) input_digest = await Get( Digest, MergeDigests( (source_files_snapshot.digest, black_pex.digest, config_digest)), ) process = await Get( Process, PexProcess( black_pex, argv=generate_args(source_files=source_files, black=black, check_only=setup_request.check_only), input_digest=input_digest, output_files=source_files_snapshot.files, description= f"Run Black on {pluralize(len(setup_request.request.field_sets), 'file')}.", level=LogLevel.DEBUG, ), ) return Setup(process, original_digest=source_files_snapshot.digest)
async def setup_pytest_for_target( request: TestSetupRequest, pytest: PyTest, test_subsystem: TestSubsystem, python_setup: PythonSetup, coverage_config: CoverageConfig, coverage_subsystem: CoverageSubsystem, test_extra_env: TestExtraEnv, global_options: GlobalOptions, ) -> TestSetup: transitive_targets = await Get( TransitiveTargets, TransitiveTargetsRequest([request.field_set.address])) all_targets = transitive_targets.closure interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( (tgt[PythonInterpreterCompatibility] for tgt in all_targets if tgt.has_field(PythonInterpreterCompatibility)), python_setup, ) # Defaults to zip_safe=False. requirements_pex_request = Get( Pex, PexFromTargetsRequest, PexFromTargetsRequest.for_requirements([request.field_set.address], internal_only=True), ) pytest_pex_request = Get( Pex, PexRequest( output_filename="pytest.pex", requirements=PexRequirements(pytest.get_requirement_strings()), interpreter_constraints=interpreter_constraints, entry_point="pytest:main", internal_only=True, additional_args=( # NB: We set `--not-zip-safe` because Pytest plugin discovery, which uses # `importlib_metadata` and thus `zipp`, does not play nicely when doing import # magic directly from zip files. `zipp` has pathologically bad behavior with large # zipfiles. # TODO: this does have a performance cost as the pex must now be expanded to disk. # Long term, it would be better to fix Zipp (whose fix would then need to be used # by importlib_metadata and then by Pytest). See # https://github.com/jaraco/zipp/pull/26. "--not-zip-safe", # TODO(John Sirois): Support shading python binaries: # https://github.com/pantsbuild/pants/issues/9206 "--pex-path", requirements_pex_request.input.output_filename, ), ), ) prepared_sources_request = Get( PythonSourceFiles, PythonSourceFilesRequest(all_targets, include_files=True)) # Create any assets that the test depends on through the `runtime_package_dependencies` field. assets: Tuple[BuiltPackage, ...] = () unparsed_runtime_packages = (request.field_set.runtime_package_dependencies .to_unparsed_address_inputs()) unparsed_runtime_binaries = (request.field_set.runtime_binary_dependencies. to_unparsed_address_inputs()) if unparsed_runtime_packages.values or unparsed_runtime_binaries.values: runtime_package_targets, runtime_binary_dependencies = await MultiGet( Get(Targets, UnparsedAddressInputs, unparsed_runtime_packages), Get(Targets, UnparsedAddressInputs, unparsed_runtime_binaries), ) field_sets_per_target = await Get( FieldSetsPerTarget, FieldSetsPerTargetRequest( PackageFieldSet, itertools.chain(runtime_package_targets, runtime_binary_dependencies), ), ) assets = await MultiGet( Get(BuiltPackage, PackageFieldSet, field_set) for field_set in field_sets_per_target.field_sets) # Get the file names for the test_target so that we can specify to Pytest precisely which files # to test, rather than using auto-discovery. field_set_source_files_request = Get( SourceFiles, SourceFilesRequest([request.field_set.sources])) pytest_pex, requirements_pex, prepared_sources, field_set_source_files = await MultiGet( pytest_pex_request, requirements_pex_request, prepared_sources_request, field_set_source_files_request, ) input_digest = await Get( Digest, MergeDigests(( coverage_config.digest, prepared_sources.source_files.snapshot.digest, requirements_pex.digest, pytest_pex.digest, *(binary.digest for binary in assets), )), ) add_opts = [f"--color={'yes' if global_options.options.colors else 'no'}"] output_files = [] results_file_name = None if pytest.options.junit_xml_dir and not request.is_debug: results_file_name = f"{request.field_set.address.path_safe_spec}.xml" add_opts.extend((f"--junitxml={results_file_name}", "-o", f"junit_family={pytest.options.junit_family}")) output_files.append(results_file_name) coverage_args = [] if test_subsystem.use_coverage and not request.is_debug: output_files.append(".coverage") cov_paths = coverage_subsystem.filter if coverage_subsystem.filter else ( ".", ) coverage_args = [ "--cov-report=", # Turn off output. *itertools.chain.from_iterable(["--cov", cov_path] for cov_path in cov_paths), ] extra_env = { "PYTEST_ADDOPTS": " ".join(add_opts), "PEX_EXTRA_SYS_PATH": ":".join(prepared_sources.source_roots), } extra_env.update(test_extra_env.env) process = await Get( Process, PexProcess( pytest_pex, argv=(*pytest.options.args, *coverage_args, *field_set_source_files.files), extra_env=extra_env, input_digest=input_digest, output_files=output_files, timeout_seconds=request.field_set.timeout. calculate_from_global_options(pytest), execution_slot_variable=pytest.options.execution_slot_var, description=f"Run Pytest for {request.field_set.address}", level=LogLevel.DEBUG, uncacheable=test_subsystem.force and not request.is_debug, ), ) return TestSetup(process, results_file_name=results_file_name)
async def run_setup_pys( targets_with_origins: TargetsWithOrigins, setup_py_subsystem: SetupPySubsystem, python_setup: PythonSetup, distdir: DistDir, workspace: Workspace, union_membership: UnionMembership, ) -> SetupPy: """Run setup.py commands on all exported targets addressed.""" validate_args(setup_py_subsystem.args) # Get all exported targets, ignoring any non-exported targets that happened to be # globbed over, but erroring on any explicitly-requested non-exported targets. exported_targets: List[ExportedTarget] = [] explicit_nonexported_targets: List[Target] = [] for target_with_origin in targets_with_origins: tgt = target_with_origin.target if tgt.has_field(PythonProvidesField): exported_targets.append(ExportedTarget(tgt)) elif isinstance(target_with_origin.origin, (AddressLiteralSpec, FilesystemLiteralSpec)): explicit_nonexported_targets.append(tgt) if explicit_nonexported_targets: raise TargetNotExported( "Cannot run setup.py on these targets, because they have no `provides=` clause: " f'{", ".join(so.address.spec for so in explicit_nonexported_targets)}' ) if setup_py_subsystem.transitive: # Expand out to all owners of the entire dep closure. transitive_targets = await Get( TransitiveTargets, Addresses(et.target.address for et in exported_targets) ) owners = await MultiGet( Get(ExportedTarget, OwnedDependency(tgt)) for tgt in transitive_targets.closure if is_ownable_target(tgt, union_membership) ) exported_targets = list(FrozenOrderedSet(owners)) interpreter_constraints = PexInterpreterConstraints.create_from_compatibility_fields( ( target_with_origin.target[PythonInterpreterCompatibility] for target_with_origin in targets_with_origins if target_with_origin.target.has_field(PythonInterpreterCompatibility) ), python_setup, ) chroots = await MultiGet( Get( SetupPyChroot, SetupPyChrootRequest(exported_target, py2=interpreter_constraints.includes_python2()), ) for exported_target in exported_targets ) # If args were provided, run setup.py with them; Otherwise just dump chroots. if setup_py_subsystem.args: setup_py_results = await MultiGet( Get( RunSetupPyResult, RunSetupPyRequest(exported_target, chroot, setup_py_subsystem.args), ) for exported_target, chroot in zip(exported_targets, chroots) ) for exported_target, setup_py_result in zip(exported_targets, setup_py_results): addr = exported_target.target.address.spec logger.info(f"Writing dist for {addr} under {distdir.relpath}/.") workspace.write_digest(setup_py_result.output, path_prefix=str(distdir.relpath)) else: # Just dump the chroot. for exported_target, chroot in zip(exported_targets, chroots): addr = exported_target.target.address.spec setup_py_dir = ( distdir.relpath / f"{chroot.setup_kwargs.name}-{chroot.setup_kwargs.version}" ) logger.info(f"Writing setup.py chroot for {addr} to {setup_py_dir}") workspace.write_digest(chroot.digest, path_prefix=str(setup_py_dir)) return SetupPy(0)