• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.09
/src/python/pants/backend/python/util_rules/faas.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
"""Function-as-a-service (FaaS) support like AWS Lambda and Google Cloud Functions."""
4

5
from __future__ import annotations
1✔
6

7
import importlib.resources
1✔
8
import json
1✔
9
import logging
1✔
10
import os.path
1✔
11
from abc import ABC, abstractmethod
1✔
12
from dataclasses import dataclass
1✔
13
from enum import Enum
1✔
14
from pathlib import Path
1✔
15
from typing import ClassVar, cast
1✔
16

17
from pants.backend.python.dependency_inference.module_mapper import (
1✔
18
    PythonModuleOwnersRequest,
19
    map_module_to_address,
20
)
21
from pants.backend.python.dependency_inference.rules import import_rules
1✔
22
from pants.backend.python.dependency_inference.subsystem import (
1✔
23
    AmbiguityResolution,
24
    PythonInferSubsystem,
25
)
26
from pants.backend.python.subsystems.setup import PythonSetup
1✔
27
from pants.backend.python.target_types import (
1✔
28
    PexCompletePlatformsField,
29
    PexLayout,
30
    PythonResolveField,
31
)
32
from pants.backend.python.util_rules.pex import (
1✔
33
    CompletePlatforms,
34
    create_pex,
35
    digest_complete_platform_addresses,
36
)
37
from pants.backend.python.util_rules.pex_from_targets import (
1✔
38
    InterpreterConstraintsRequest,
39
    PexFromTargetsRequest,
40
    interpreter_constraints_for_targets,
41
)
42
from pants.backend.python.util_rules.pex_from_targets import rules as pex_from_targets_rules
1✔
43
from pants.backend.python.util_rules.pex_venv import PexVenvLayout, PexVenvRequest
1✔
44
from pants.backend.python.util_rules.pex_venv import pex_venv as pex_venv_get
1✔
45
from pants.backend.python.util_rules.pex_venv import rules as pex_venv_rules
1✔
46
from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact, OutputPathField
1✔
47
from pants.engine.addresses import Address
1✔
48
from pants.engine.fs import (
1✔
49
    EMPTY_DIGEST,
50
    CreateDigest,
51
    FileContent,
52
    GlobMatchErrorBehavior,
53
    PathGlobs,
54
)
55
from pants.engine.internals.graph import determine_explicitly_provided_dependencies
1✔
56
from pants.engine.internals.native_engine import MergeDigests
1✔
57
from pants.engine.intrinsics import (
1✔
58
    create_digest,
59
    digest_to_snapshot,
60
    merge_digests,
61
    path_globs_to_paths,
62
)
63
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
1✔
64
from pants.engine.target import (
1✔
65
    AsyncFieldMixin,
66
    Dependencies,
67
    DependenciesRequest,
68
    FieldSet,
69
    InferDependenciesRequest,
70
    InferredDependencies,
71
    InvalidFieldException,
72
    InvalidTargetException,
73
    StringField,
74
    StringSequenceField,
75
)
76
from pants.engine.unions import UnionRule
1✔
77
from pants.source.source_root import SourceRootRequest, get_source_root
1✔
78
from pants.util.docutil import doc_url
1✔
79
from pants.util.ordered_set import FrozenOrderedSet
1✔
80
from pants.util.strutil import help_text, softwrap
1✔
81

82
logger = logging.getLogger(__name__)
1✔
83

84

85
class PythonFaaSLayoutField(StringField):
1✔
86
    alias = "layout"
1✔
87
    valid_choices = PexVenvLayout
1✔
88
    expected_type = str
1✔
89
    default = PexVenvLayout.FLAT_ZIPPED.value
1✔
90
    help = help_text(
1✔
91
        """
92
        Control the layout of the final artifact: `flat` creates a directory with the
93
        source and requirements at the top level, as recommended by cloud vendors,
94
        while `flat-zipped` (the default) wraps this up into a single zip file.
95
        """
96
    )
97

98

99
class PythonFaaSPex3VenvCreateExtraArgsField(StringSequenceField):
1✔
100
    alias = "pex3_venv_create_extra_args"
1✔
101
    default = ()
1✔
102
    help = help_text(
1✔
103
        """
104
        Any extra arguments to pass to the `pex3 venv create` invocation that is used to create the
105
        final zip file or directory.
106

107
        For example, `pex3_venv_create_extra_args=["--collisions-ok"]`, if using packages that have
108
        colliding files that aren't required at runtime (errors like "Encountered collisions
109
        populating ...").
110
        """
111
    )
112

113

114
class PythonFaaSPexBuildExtraArgs(StringSequenceField):
1✔
115
    alias = "pex_build_extra_args"
1✔
116
    default = ()
1✔
117
    help = help_text(
1✔
118
        """
119
        Additional arguments to pass to the `pex` invocation that is used to collect the requirements
120
        and sources for packaging.
121

122
        For example, `pex_build_extra_args=["--exclude=pypi-package-name"]` to force a package called
123
        `pypi-package-name` isn't included in the artifact.
124

125
        Note: Excluding dependencies currently causes Pex to throw an error. You can additionally pass
126
        the `--ignore-errors` flag.
127
        """
128
    )
129

130

131
class PythonFaaSHandlerField(StringField, AsyncFieldMixin):
1✔
132
    alias = "handler"
1✔
133
    required = True
1✔
134
    value: str
1✔
135
    help = help_text(
1✔
136
        """
137
        You can specify a full module like `'path.to.module:handler_func'` or use a shorthand to
138
        specify a file name, using the same syntax as the `sources` field, e.g.
139
        `'cloud_function.py:handler_func'`.
140
        """
141
    )
142

143
    @classmethod
1✔
144
    def compute_value(cls, raw_value: str | None, address: Address) -> str:
1✔
145
        value = cast(str, super().compute_value(raw_value, address))
×
146
        if ":" not in value:
×
147
            raise InvalidFieldException(
×
148
                f"The `{cls.alias}` field in target at {address} must end in the "
149
                f"format `:my_handler_func`, but was {value}."
150
            )
151
        return value
×
152

153

154
@dataclass(frozen=True)
1✔
155
class ResolvedPythonFaaSHandler:
1✔
156
    module: str
1✔
157
    func: str
1✔
158
    file_name_used: bool
1✔
159

160

161
@dataclass(frozen=True)
1✔
162
class ResolvePythonFaaSHandlerRequest:
1✔
163
    field: PythonFaaSHandlerField
1✔
164

165

166
@rule(desc="Determining the handler for a python FaaS target")
1✔
167
async def resolve_python_faas_handler(
1✔
168
    request: ResolvePythonFaaSHandlerRequest,
169
) -> ResolvedPythonFaaSHandler:
170
    handler_val = request.field.value
×
171
    field_alias = request.field.alias
×
172
    address = request.field.address
×
173
    path, _, func = handler_val.partition(":")
×
174

175
    # If it's already a module, simply use that. Otherwise, convert the file name into a module
176
    # path.
177
    if not path.endswith(".py"):
×
178
        return ResolvedPythonFaaSHandler(module=path, func=func, file_name_used=False)
×
179

180
    # Use the engine to validate that the file exists and that it resolves to only one file.
181
    full_glob = os.path.join(address.spec_path, path)
×
182
    handler_paths = await path_globs_to_paths(
×
183
        PathGlobs(
184
            [full_glob],
185
            glob_match_error_behavior=GlobMatchErrorBehavior.error,
186
            description_of_origin=f"{address}'s `{field_alias}` field",
187
        )
188
    )
189

190
    # We will have already raised if the glob did not match, i.e. if there were no files. But
191
    # we need to check if they used a file glob (`*` or `**`) that resolved to >1 file.
192
    if len(handler_paths.files) != 1:
×
193
        raise InvalidFieldException(
×
194
            f"Multiple files matched for the `{field_alias}` {repr(handler_val)} for the target "
195
            f"{address}, but only one file expected. Are you using a glob, rather than a file "
196
            f"name?\n\nAll matching files: {list(handler_paths.files)}."
197
        )
198
    handler_path = handler_paths.files[0]
×
199
    source_root = await get_source_root(SourceRootRequest.for_file(handler_path))
×
200
    stripped_source_path = os.path.relpath(handler_path, source_root.path)
×
201
    module_base, _ = os.path.splitext(stripped_source_path)
×
202
    normalized_path = module_base.replace(os.path.sep, ".")
×
203
    return ResolvedPythonFaaSHandler(module=normalized_path, func=func, file_name_used=True)
×
204

205

206
class PythonFaaSDependencies(Dependencies):
1✔
207
    supports_transitive_excludes = True
1✔
208

209

210
@dataclass(frozen=True)
1✔
211
class PythonFaaSHandlerInferenceFieldSet(FieldSet):
1✔
212
    required_fields = (
1✔
213
        PythonFaaSDependencies,
214
        PythonFaaSHandlerField,
215
        PythonResolveField,
216
    )
217

218
    dependencies: PythonFaaSDependencies
1✔
219
    handler: PythonFaaSHandlerField
1✔
220
    resolve: PythonResolveField
1✔
221

222

223
class InferPythonFaaSHandlerDependency(InferDependenciesRequest):
1✔
224
    infer_from = PythonFaaSHandlerInferenceFieldSet
1✔
225

226

227
@rule(desc="Inferring dependency from the python FaaS `handler` field")
1✔
228
async def infer_faas_handler_dependency(
1✔
229
    request: InferPythonFaaSHandlerDependency,
230
    python_infer_subsystem: PythonInferSubsystem,
231
    python_setup: PythonSetup,
232
) -> InferredDependencies:
233
    if not python_infer_subsystem.entry_points:
×
234
        return InferredDependencies([])
×
235

236
    explicitly_provided_deps, handler = await concurrently(
×
237
        determine_explicitly_provided_dependencies(
238
            **implicitly(DependenciesRequest(request.field_set.dependencies))
239
        ),
240
        resolve_python_faas_handler(ResolvePythonFaaSHandlerRequest(request.field_set.handler)),
241
    )
242

243
    # Only set locality if needed, to avoid unnecessary rule graph memoization misses.
244
    # When set, use the source root, which is useful in practice, but incurs fewer memoization
245
    # misses than using the full spec_path.
246
    locality = None
×
247
    if python_infer_subsystem.ambiguity_resolution == AmbiguityResolution.by_source_root:
×
248
        source_root = await get_source_root(
×
249
            SourceRootRequest.for_address(request.field_set.address)
250
        )
251
        locality = source_root.path
×
252

253
    owners = await map_module_to_address(
×
254
        PythonModuleOwnersRequest(
255
            handler.module,
256
            resolve=request.field_set.resolve.normalized_value(python_setup),
257
            locality=locality,
258
        ),
259
        **implicitly(),
260
    )
261
    address = request.field_set.address
×
262
    explicitly_provided_deps.maybe_warn_of_ambiguous_dependency_inference(
×
263
        owners.ambiguous,
264
        address,
265
        # If the handler was specified as a file, like `app.py`, we know the module must
266
        # live in the python_google_cloud_function's directory or subdirectory, so the owners must be ancestors.
267
        owners_must_be_ancestors=handler.file_name_used,
268
        import_reference="module",
269
        context=(
270
            f"The target {address} has the field "
271
            f"`handler={repr(request.field_set.handler.value)}`, which maps "
272
            f"to the Python module `{handler.module}`"
273
        ),
274
    )
275
    maybe_disambiguated = explicitly_provided_deps.disambiguated(
×
276
        owners.ambiguous, owners_must_be_ancestors=handler.file_name_used
277
    )
278
    unambiguous_owners = owners.unambiguous or (
×
279
        (maybe_disambiguated,) if maybe_disambiguated else ()
280
    )
281
    return InferredDependencies(unambiguous_owners)
×
282

283

284
class PythonFaaSCompletePlatforms(PexCompletePlatformsField):
1✔
285
    help = help_text(
1✔
286
        f"""
287
        {PexCompletePlatformsField.help}
288

289
        N.B.: only one of this and `runtime` can be set. If `runtime` is set, a default complete
290
        platform is chosen, if one is known for that runtime. Explicitly set this to `[]` to use the
291
        platform's ambient interpreter, such as when running in an docker environment.
292
        """
293
    )
294

295

296
class FaaSArchitecture(str, Enum):
1✔
297
    X86_64 = "x86_64"
1✔
298
    ARM64 = "arm64"
1✔
299

300

301
@dataclass(frozen=True)
1✔
302
class PythonFaaSKnownRuntime:
1✔
303
    name: str
1✔
304
    major: int
1✔
305
    minor: int
1✔
306
    docker_repo: str
1✔
307
    tag: str
1✔
308
    architecture: FaaSArchitecture
1✔
309

310
    def file_name(self) -> str:
1✔
311
        return f"complete_platform_{self.tag}.json"
×
312

313

314
class PythonFaaSRuntimeField(StringField, ABC):
1✔
315
    alias = "runtime"
1✔
316
    default = None
1✔
317

318
    known_runtimes: ClassVar[tuple[PythonFaaSKnownRuntime, ...]] = ()
1✔
319

320
    @classmethod
1✔
321
    def known_runtimes_complete_platforms_module(cls) -> str:
1✔
322
        # the runtime field subclasses are conventionally in a `target_types.py` file, and we want
323
        # to put the JSONs in a sibling file
324
        return cls.__module__.rsplit(".", 1)[0]
×
325

326
    @abstractmethod
1✔
327
    def to_interpreter_version(self) -> None | tuple[int, int]:
1✔
328
        """Returns the Python version implied by the runtime, as (major, minor)."""
329

330
    @classmethod
1✔
331
    @abstractmethod
1✔
332
    def from_interpreter_version(cls, py_major: int, py_minor: int) -> str:
1✔
333
        """Returns an appropriately-formatted runtime argument."""
334

335
    def to_platform_string(self) -> None | str:
1✔
336
        # We hardcode the platform value to the appropriate one for each FaaS runtime.
337
        # (Running the "hello world" cloud function in the example code will report the platform, and can be
338
        # used to verify correctness of these platform strings.)
339
        interpreter_version = self.to_interpreter_version()
×
340
        if interpreter_version is None:
×
341
            return None
×
342

343
        return _format_platform_from_major_minor(*interpreter_version)
×
344

345

346
def _format_platform_from_major_minor(py_major: int, py_minor: int) -> str:
1✔
347
    platform_str = f"linux_x86_64-cp-{py_major}{py_minor}-cp{py_major}{py_minor}"
×
348
    # set pymalloc ABI flag - this was removed in python 3.8 https://bugs.python.org/issue36707
349
    if py_major <= 3 and py_minor < 8:
×
350
        platform_str += "m"
×
351
    return platform_str
×
352

353

354
@rule
1✔
355
async def digest_complete_platforms(
1✔
356
    complete_platforms: PythonFaaSCompletePlatforms,
357
) -> CompletePlatforms:
358
    return await digest_complete_platform_addresses(complete_platforms.to_unparsed_address_inputs())
×
359

360

361
@dataclass(frozen=True)
1✔
362
class RuntimePlatformsRequest:
1✔
363
    address: Address
1✔
364
    target_name: str
1✔
365

366
    runtime: PythonFaaSRuntimeField
1✔
367
    complete_platforms: PythonFaaSCompletePlatforms
1✔
368
    architecture: FaaSArchitecture
1✔
369

370

371
@dataclass(frozen=True)
1✔
372
class RuntimePlatforms:
1✔
373
    interpreter_version: None | tuple[int, int]
1✔
374
    complete_platforms: CompletePlatforms = CompletePlatforms()
1✔
375

376

377
async def _infer_from_ics(request: RuntimePlatformsRequest) -> tuple[int, int]:
1✔
378
    ics = await interpreter_constraints_for_targets(
×
379
        InterpreterConstraintsRequest([request.address]), **implicitly()
380
    )
381

382
    # Future proofing: use naive non-universe-based IC requirement matching to determine if the
383
    # requirements cover exactly (and all patch versions of) one major.minor interpreter
384
    # version.
385
    #
386
    # Either reasonable option for a universe (`PythonSetup.interpreter_universe` or the FaaS's
387
    # known runtimes) can and will be expanded during a Pants upgrade: for instance, at the time of
388
    # writing, Pants only supports up to 3.11 but might soon add support for 3.12, or AWS Lambda
389
    # (and pants.backend.awslambda.python's known runtimes) only supports up to 3.10 but might soon
390
    # add support for 3.11.
391
    #
392
    # When this happens, some ranges (like `>=3.11`, if using `PythonSetup.interpreter_universe`)
393
    # will go from covering one major.minor interpreter version to covering more than one, and thus
394
    # inference starts breaking during the upgrade, requiring the user to do distracting changes
395
    # without deprecations/warnings to help.
396
    major_minor = ics.major_minor_version_when_single_and_entire()
×
397
    if major_minor is not None:
×
398
        return major_minor
×
399

400
    raise InvalidTargetException(
×
401
        softwrap(
402
            f"""
403
            The {request.target_name!r} target {request.address} cannot have its runtime platform
404
            inferred, because inference requires simple interpreter constraints covering exactly one
405
            minor release of Python, and all its patch version. The constraints for this target
406
            ({ics}) aren't understood.
407

408
            To fix, provide one of the following:
409

410
            - a value for the `{request.runtime.alias}` field, or
411

412
            - a value for the `{request.complete_platforms.alias}` field, or
413

414
            - simple and narrow interpreter constraints (for example, `==3.10.*` or `>=3.10,<3.11` are simple enough to imply Python 3.10)
415
            """
416
        )
417
    )
418

419

420
@rule
1✔
421
async def infer_runtime_platforms(request: RuntimePlatformsRequest) -> RuntimePlatforms:
1✔
422
    if request.complete_platforms.value is not None:
×
423
        # explicit complete platforms wins:
424

425
        complete_platforms = await digest_complete_platforms(request.complete_platforms)
×
426
        # Don't bother trying to infer the runtime version if the user has provided their own
427
        # complete platform; they probably know what they're doing.
428
        return RuntimePlatforms(interpreter_version=None, complete_platforms=complete_platforms)
×
429

430
    version = request.runtime.to_interpreter_version()
×
431
    inferred_from_ics = False
×
432
    if version is None:
×
433
        # if there's not a specified version, let's try to infer it from the interpreter constraints
434
        version = await _infer_from_ics(request)
×
435
        inferred_from_ics = True
×
436

437
    try:
×
438
        file_name = next(
×
439
            rt.file_name()
440
            for rt in request.runtime.known_runtimes
441
            if version == (rt.major, rt.minor) and request.architecture.value == rt.architecture
442
        )
443
    except StopIteration:
×
444
        # No known runtime, so prompt the user to specify
445
        version_modifier = "[inferred from interpreter constraints]" if inferred_from_ics else ""
×
446
        version_adjective = "inferred" if inferred_from_ics else "specified"
×
447
        known_runtimes_str = ", ".join(
×
448
            FrozenOrderedSet(r.name for r in request.runtime.known_runtimes)
449
        )
450
        raise InvalidTargetException(
×
451
            softwrap(
452
                f"""
453
                Could not find a known runtime for the {version_adjective} Python version and machine architecture!
454

455
                * Python version: {version} {version_modifier}
456
                * Machine architecture: {request.architecture.value}
457
                * Known runtime values: {known_runtimes_str}
458

459
                To fix, please generate a `complete_platforms` file for the given Python version and
460
                machine architecture, or specify a runtime that is known to Pants.
461

462
                You can follow the instructions at {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")}
463
                to generate a `complete_platforms` file for your Python version and machine
464
                architecture.
465
                """
466
            ),
467
            description_of_origin=f"In the {request.target_name!r} target",
468
        ) from None
469

470
    module = request.runtime.known_runtimes_complete_platforms_module()
×
471

472
    content = (importlib.resources.files(module) / file_name).read_bytes()
×
473
    snapshot = await digest_to_snapshot(
×
474
        **implicitly(CreateDigest([FileContent(file_name, content)]))
475
    )
476

477
    return RuntimePlatforms(
×
478
        interpreter_version=version, complete_platforms=CompletePlatforms.from_snapshot(snapshot)
479
    )
480

481

482
@dataclass(frozen=True)
1✔
483
class BuildPythonFaaSRequest:
1✔
484
    address: Address
1✔
485
    target_name: str
1✔
486

487
    complete_platforms: PythonFaaSCompletePlatforms
1✔
488
    handler: None | PythonFaaSHandlerField
1✔
489
    output_path: OutputPathField
1✔
490
    runtime: PythonFaaSRuntimeField
1✔
491
    architecture: FaaSArchitecture
1✔
492
    pex3_venv_create_extra_args: PythonFaaSPex3VenvCreateExtraArgsField
1✔
493
    pex_build_extra_args: PythonFaaSPexBuildExtraArgs
1✔
494
    layout: PythonFaaSLayoutField
1✔
495

496
    include_requirements: bool
1✔
497
    include_sources: bool
1✔
498

499
    reexported_handler_module: None | str
1✔
500
    log_only_reexported_handler_func: bool = False
1✔
501

502
    prefix_in_artifact: None | str = None
1✔
503

504

505
@rule
1✔
506
async def build_python_faas(
1✔
507
    request: BuildPythonFaaSRequest,
508
) -> BuiltPackage:
509
    additional_pex_args = (
×
510
        # Ensure we can resolve manylinux wheels in addition to any AMI-specific wheels.
511
        "--manylinux=manylinux2014",
512
        # When we're executing Pex on Linux, allow a local interpreter to be resolved if
513
        # available and matching the AMI platform.
514
        "--resolve-local-platforms",
515
        # Additional args from request
516
        *(request.pex_build_extra_args.value or ()),
517
    )
518

519
    platforms_get = infer_runtime_platforms(
×
520
        RuntimePlatformsRequest(
521
            address=request.address,
522
            target_name=request.target_name,
523
            runtime=request.runtime,
524
            architecture=request.architecture,
525
            complete_platforms=request.complete_platforms,
526
        ),
527
    )
528

529
    if request.handler:
×
530
        platforms, handler = await concurrently(
×
531
            platforms_get,
532
            resolve_python_faas_handler(ResolvePythonFaaSHandlerRequest(request.handler)),
533
        )
534
    else:
535
        platforms = await platforms_get
×
536
        handler = None
×
537

538
    # TODO: improve diagnostics if there's more than one platform/complete_platform
539

540
    if request.reexported_handler_module and handler:
×
541
        # synthesise a source file that gives a fixed handler path, no matter what the entry point is:
542
        # some platforms require a certain name (e.g. GCF), and even on others, giving a fixed name
543
        # means users don't need to duplicate the entry_point config in both the pants BUILD file and
544
        # infrastructure definitions (the latter can always use the same names, for every lambda).
545
        reexported_handler_file = f"{request.reexported_handler_module}.py"
×
546
        reexported_handler_func = "handler"
×
547
        reexported_handler_content = (
×
548
            f"from {handler.module} import {handler.func} as {reexported_handler_func}"
549
        )
550
        additional_sources = await create_digest(
×
551
            CreateDigest(
552
                [FileContent(reexported_handler_file, reexported_handler_content.encode())]
553
            )
554
        )
555
    else:
556
        additional_sources = EMPTY_DIGEST
×
557
        reexported_handler_func = None
×
558

559
    repository_filename = "faas_repository.pex"
×
560
    pex_request = PexFromTargetsRequest(
×
561
        addresses=[request.address],
562
        internal_only=False,
563
        include_requirements=request.include_requirements,
564
        include_source_files=request.include_sources,
565
        output_filename=repository_filename,
566
        complete_platforms=platforms.complete_platforms,
567
        layout=PexLayout.PACKED,
568
        additional_args=additional_pex_args,
569
        additional_lockfile_args=additional_pex_args,
570
        additional_sources=additional_sources,
571
        warn_for_transitive_files_targets=True,
572
    )
573

574
    pex_result = await create_pex(**implicitly({pex_request: PexFromTargetsRequest}))
×
575

576
    layout = PexVenvLayout(request.layout.value)
×
577

578
    output_filename = request.output_path.value_or_default(
×
579
        file_ending="zip" if layout is PexVenvLayout.FLAT_ZIPPED else None
580
    )
581
    metadata_filename = f"{output_filename}.metadata.json"
×
582

583
    result = await pex_venv_get(
×
584
        PexVenvRequest(
585
            pex=pex_result,
586
            layout=layout,
587
            complete_platforms=platforms.complete_platforms,
588
            extra_args=request.pex3_venv_create_extra_args.value or (),
589
            prefix=request.prefix_in_artifact,
590
            output_path=Path(output_filename),
591
            description=f"Build {request.target_name} artifact for {request.address}",
592
        ),
593
    )
594

595
    metadata = {}
×
596

597
    if platforms.interpreter_version is not None:
×
598
        runtime = request.runtime.from_interpreter_version(*platforms.interpreter_version)
×
599
        metadata["runtime"] = runtime
×
600

601
    if request.architecture is not None:
×
602
        metadata["architecture"] = request.architecture.value
×
603

604
    if reexported_handler_func is not None:
×
605
        if request.log_only_reexported_handler_func:
×
606
            handler_text = reexported_handler_func
×
607
        else:
608
            handler_text = f"{request.reexported_handler_module}.{reexported_handler_func}"
×
609
        metadata["handler"] = handler_text
×
610

611
    metadata_digest = await create_digest(
×
612
        CreateDigest(
613
            [
614
                FileContent(
615
                    metadata_filename, json.dumps(metadata, indent=2, sort_keys=True).encode()
616
                )
617
            ]
618
        )
619
    )
620
    digest = await merge_digests(MergeDigests([result.digest, metadata_digest]))
×
621

622
    extra_log_lines = [f"    {key.capitalize()}: {val}" for key, val in metadata.items()]
×
623
    artifact = BuiltPackageArtifact(
×
624
        output_filename,
625
        extra_log_lines=tuple(extra_log_lines),
626
    )
627
    metadata_artifact = BuiltPackageArtifact(metadata_filename)
×
628

629
    return BuiltPackage(digest=digest, artifacts=(artifact, metadata_artifact))
×
630

631

632
def rules():
1✔
633
    return (
×
634
        *collect_rules(),
635
        *import_rules(),
636
        *pex_venv_rules(),
637
        *pex_from_targets_rules(),
638
        UnionRule(InferDependenciesRequest, InferPythonFaaSHandlerDependency),
639
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc