pants-backend-platforms 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Tom Solberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: pants_backend_platforms
3
+ Version: 0.1.0
4
+ Summary: Multi-platform builds for Pants backends: platform targets and a contract for building dependencies per platform.
5
+ Keywords: pantsbuild,pants,backend,cross-compilation,platforms
6
+ Author-Email: Tom Solberg <me@sbg.dev>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2022 Tom Solberg
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Programming Language :: Python
31
+ Classifier: Topic :: Software Development :: Build Tools
32
+ Project-URL: Repository, https://github.com/tgolsson/pants-backends
33
+ Project-URL: Code, https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/platforms
34
+ Project-URL: Changelog, https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/platforms/CHANGELOG.md
35
+ Requires-Python: >=3.11
36
+ Description-Content-Type: text/markdown
37
+
38
+ # pants-backend-platforms
39
+
40
+ A [Pants](https://www.pantsbuild.org) backend for building targets for more than one platform.
41
+ It has no build rules of its own: language backends (such as pants-backend-cmake) use it to
42
+ know which platforms to build and how to ask another backend for a dependency built for the
43
+ same platform. The C and C++ toolchain for each platform is in [pants-backend-cc](../cc).
44
+
45
+ This release targets Pants **2.32.x**. Install it with `pants-backend-platforms==0.1.0` in
46
+ `[GLOBAL].plugins` in `pants.toml`, and enable `pants_backend_platforms` in
47
+ `[GLOBAL].backend_packages`.
48
+
49
+ **Status: alpha.**
50
+
51
+ ## Targets
52
+
53
+ ```python
54
+ platform(name="linux-x86_64", os="linux", arch="x86_64")
55
+ platform(name="windows-x86_64", os="windows", arch="x86_64")
56
+ ```
57
+
58
+ - `platform`: `os` (`linux`, `windows`, `macos`) and `arch` (`x86_64`, `aarch64`). Everything
59
+ refers to a platform by its target name, so names are unique.
60
+ - The host is the `platform` matching the machine Pants runs on. When several match, choose
61
+ with `[platforms].host`; when none does, there is an implicit `<os>-<arch>` platform.
62
+
63
+ ## Choosing what to build
64
+
65
+ A target of a backend that supports platforms has a `platforms` field: the platforms it can be
66
+ built for, the host alone when unset.
67
+
68
+ ```python
69
+ cmake_target(name="app", project=":project", platforms=["linux-x86_64", "windows-x86_64"])
70
+ ```
71
+
72
+ `pants package` builds the platforms in that list that are the host or in
73
+ `[platforms].enabled`. The host's output keeps its usual path in `dist/`, and every other
74
+ platform's goes under `dist/<platform>/`:
75
+
76
+ ```
77
+ pants --platforms-enabled="['windows-x86_64']" package ::
78
+ ```
79
+
80
+ A target built for a platform has its dependencies built for the same platform, enabled or not.
81
+ A dependency whose `platforms` does not list it is an error naming both targets.
82
+
83
+ Enable extra platforms where the packages are published rather than in `pants.toml`: whatever
84
+ packages a target, such as a test's `runtime_package_dependencies` or an `archive`, gets every
85
+ enabled platform too.
86
+
87
+ ## For backend authors
88
+
89
+ - Make the field set a `PlatformBuildFieldSet` member, which brings the `platforms` field, and
90
+ register `UnionRule(PlatformBuildFieldSet, MyFieldSet)`. Its rule takes the field set and a
91
+ `TargetPlatform` and returns a `PlatformBuild`: the files at the paths `pants package` writes
92
+ them to for the host.
93
+ - `build_platform_dependencies(PlatformDependenciesRequest(address, targets, platform))` builds
94
+ the members among `targets` for the platform and checks their `platforms`. A tool that runs
95
+ during the build is built with `build_for_platform` for `KnownPlatforms.host`.
96
+ - The `package` rule returns `package_for_platforms(PackageForPlatformsRequest(field_set))`.
97
+ Goals that run what they build, such as `run` and `test`, build for the host.
@@ -0,0 +1,60 @@
1
+ # pants-backend-platforms
2
+
3
+ A [Pants](https://www.pantsbuild.org) backend for building targets for more than one platform.
4
+ It has no build rules of its own: language backends (such as pants-backend-cmake) use it to
5
+ know which platforms to build and how to ask another backend for a dependency built for the
6
+ same platform. The C and C++ toolchain for each platform is in [pants-backend-cc](../cc).
7
+
8
+ This release targets Pants **2.32.x**. Install it with `pants-backend-platforms==0.1.0` in
9
+ `[GLOBAL].plugins` in `pants.toml`, and enable `pants_backend_platforms` in
10
+ `[GLOBAL].backend_packages`.
11
+
12
+ **Status: alpha.**
13
+
14
+ ## Targets
15
+
16
+ ```python
17
+ platform(name="linux-x86_64", os="linux", arch="x86_64")
18
+ platform(name="windows-x86_64", os="windows", arch="x86_64")
19
+ ```
20
+
21
+ - `platform`: `os` (`linux`, `windows`, `macos`) and `arch` (`x86_64`, `aarch64`). Everything
22
+ refers to a platform by its target name, so names are unique.
23
+ - The host is the `platform` matching the machine Pants runs on. When several match, choose
24
+ with `[platforms].host`; when none does, there is an implicit `<os>-<arch>` platform.
25
+
26
+ ## Choosing what to build
27
+
28
+ A target of a backend that supports platforms has a `platforms` field: the platforms it can be
29
+ built for, the host alone when unset.
30
+
31
+ ```python
32
+ cmake_target(name="app", project=":project", platforms=["linux-x86_64", "windows-x86_64"])
33
+ ```
34
+
35
+ `pants package` builds the platforms in that list that are the host or in
36
+ `[platforms].enabled`. The host's output keeps its usual path in `dist/`, and every other
37
+ platform's goes under `dist/<platform>/`:
38
+
39
+ ```
40
+ pants --platforms-enabled="['windows-x86_64']" package ::
41
+ ```
42
+
43
+ A target built for a platform has its dependencies built for the same platform, enabled or not.
44
+ A dependency whose `platforms` does not list it is an error naming both targets.
45
+
46
+ Enable extra platforms where the packages are published rather than in `pants.toml`: whatever
47
+ packages a target, such as a test's `runtime_package_dependencies` or an `archive`, gets every
48
+ enabled platform too.
49
+
50
+ ## For backend authors
51
+
52
+ - Make the field set a `PlatformBuildFieldSet` member, which brings the `platforms` field, and
53
+ register `UnionRule(PlatformBuildFieldSet, MyFieldSet)`. Its rule takes the field set and a
54
+ `TargetPlatform` and returns a `PlatformBuild`: the files at the paths `pants package` writes
55
+ them to for the host.
56
+ - `build_platform_dependencies(PlatformDependenciesRequest(address, targets, platform))` builds
57
+ the members among `targets` for the platform and checks their `platforms`. A tool that runs
58
+ during the build is built with `build_for_platform` for `KnownPlatforms.host`.
59
+ - The `package` rule returns `package_for_platforms(PackageForPlatformsRequest(field_set))`.
60
+ Goals that run what they build, such as `run` and `test`, build for the host.
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from pants_backend_platforms.target_types import PlatformTarget
4
+ from pants_backend_platforms.util_rules import contract, lookup, platforms
5
+
6
+
7
+ def target_types():
8
+ return [PlatformTarget]
9
+
10
+
11
+ def rules():
12
+ return [*lookup.rules(), *platforms.rules(), *contract.rules()]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from pants.option.option_types import StrListOption, StrOption
4
+ from pants.option.subsystem import Subsystem
5
+ from pants.util.strutil import softwrap
6
+
7
+
8
+ class PlatformsSubsystem(Subsystem):
9
+ options_scope = "platforms"
10
+ help = "Which platforms a run builds."
11
+
12
+ enabled = StrListOption(
13
+ default=[],
14
+ help="Platforms `pants package` builds besides the host, for targets whose `platforms` list them.",
15
+ )
16
+ host = StrOption(
17
+ default=None,
18
+ help=softwrap(
19
+ """
20
+ The `platform` this machine is, when several match its os and arch. Unset, the only
21
+ match, or an implicit `<os>-<arch>` platform when none matches.
22
+ """
23
+ ),
24
+ )
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from pants.engine.target import (
4
+ COMMON_TARGET_FIELDS,
5
+ AsyncFieldMixin,
6
+ StringField,
7
+ StringSequenceField,
8
+ Target,
9
+ )
10
+ from pants.util.strutil import help_text
11
+
12
+
13
+ class PlatformOsField(StringField):
14
+ alias = "os"
15
+ required = True
16
+ valid_choices = ("linux", "windows", "macos")
17
+ help = "The operating system."
18
+
19
+
20
+ class PlatformArchField(StringField):
21
+ alias = "arch"
22
+ required = True
23
+ valid_choices = ("x86_64", "aarch64")
24
+ help = "The CPU architecture."
25
+
26
+
27
+ class PlatformTarget(Target):
28
+ alias = "platform"
29
+ core_fields = (*COMMON_TARGET_FIELDS, PlatformOsField, PlatformArchField)
30
+ help = "A platform to build for, referred to by its target name, which is unique."
31
+
32
+
33
+ class PlatformsField(StringSequenceField, AsyncFieldMixin):
34
+ alias = "platforms"
35
+ default = None
36
+ help = help_text(
37
+ """
38
+ The platforms this target can be built for, by name; unset, the host only. `pants
39
+ package` builds those that are the host or in `[platforms].enabled`.
40
+ """
41
+ )
@@ -0,0 +1,156 @@
1
+ """How a backend builds another backend's targets for the platform it is building for."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from abc import ABCMeta
7
+ from dataclasses import dataclass
8
+
9
+ from pants.core.goals.package import BuiltPackage, BuiltPackageArtifact
10
+ from pants.engine.addresses import Address
11
+ from pants.engine.environment import EnvironmentName
12
+ from pants.engine.fs import (
13
+ EMPTY_DIGEST,
14
+ AddPrefix,
15
+ Digest,
16
+ DigestSubset,
17
+ GlobExpansionConjunction,
18
+ GlobMatchErrorBehavior,
19
+ MergeDigests,
20
+ PathGlobs,
21
+ )
22
+ from pants.engine.internals.graph import find_valid_field_sets
23
+ from pants.engine.intrinsics import add_prefix, digest_subset_to_digest, merge_digests
24
+ from pants.engine.rules import collect_rules, concurrently, implicitly, rule
25
+ from pants.engine.target import FieldSet, FieldSetsPerTargetRequest, Target
26
+ from pants.engine.unions import union
27
+ from pants.util.frozendict import FrozenDict
28
+ from pants.util.logging import LogLevel
29
+
30
+ from pants_backend_platforms.target_types import PlatformsField
31
+ from pants_backend_platforms.util_rules.platforms import (
32
+ KnownPlatforms,
33
+ SelectPlatformsRequest,
34
+ TargetPlatform,
35
+ select_platforms,
36
+ )
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @union(in_scope_types=[EnvironmentName, TargetPlatform])
42
+ @dataclass(frozen=True)
43
+ class PlatformBuildFieldSet(FieldSet, metaclass=ABCMeta):
44
+ """A member's rule takes the field set and a `TargetPlatform` and returns a `PlatformBuild`
45
+ holding its files at the paths `pants package` writes them to for the host."""
46
+
47
+ platforms: PlatformsField
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class PlatformBuild:
52
+ digest: Digest
53
+ files: tuple[str, ...]
54
+
55
+
56
+ def platform_build_files(build: PlatformBuild, description_of_origin: str) -> DigestSubset:
57
+ """The build's `files` alone; any file it does not hold is an error naming its origin."""
58
+ return DigestSubset(
59
+ build.digest,
60
+ PathGlobs(
61
+ build.files,
62
+ glob_match_error_behavior=GlobMatchErrorBehavior.error,
63
+ conjunction=GlobExpansionConjunction.all_match,
64
+ description_of_origin=description_of_origin,
65
+ ),
66
+ )
67
+
68
+
69
+ @rule(polymorphic=True)
70
+ async def build_for_platform(
71
+ field_set: PlatformBuildFieldSet, platform: TargetPlatform, environment_name: EnvironmentName
72
+ ) -> PlatformBuild:
73
+ raise NotImplementedError()
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class PlatformDependenciesRequest:
78
+ dependent: Address
79
+ targets: tuple[Target, ...]
80
+ """Targets that are not `PlatformBuildFieldSet` members are ignored."""
81
+ platform: TargetPlatform
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class PlatformDependencies:
86
+ builds: FrozenDict[Address, PlatformBuild]
87
+
88
+
89
+ @rule(desc="Build dependencies for a platform", level=LogLevel.DEBUG)
90
+ async def build_platform_dependencies(
91
+ request: PlatformDependenciesRequest, known: KnownPlatforms
92
+ ) -> PlatformDependencies:
93
+ per_target = await find_valid_field_sets(
94
+ FieldSetsPerTargetRequest(PlatformBuildFieldSet, request.targets), **implicitly()
95
+ )
96
+ members = []
97
+ for tgt, field_sets in zip(request.targets, per_target.collection):
98
+ if len(field_sets) > 1:
99
+ kinds = sorted(type(fs).__name__ for fs in field_sets)
100
+ raise ValueError(f"{tgt.address} matches several platform build field sets: {kinds}")
101
+ if not field_sets:
102
+ continue
103
+ listed = known.listed(field_sets[0].platforms)
104
+ if request.platform not in listed:
105
+ raise ValueError(
106
+ f"{request.dependent} is being built for {request.platform.name}, but its "
107
+ f"dependency {tgt.address} lists only {[p.name for p in listed]} in `platforms`."
108
+ )
109
+ members.append(field_sets[0])
110
+ builds = await concurrently(
111
+ build_for_platform(**implicitly({fs: PlatformBuildFieldSet, request.platform: TargetPlatform}))
112
+ for fs in members
113
+ )
114
+ return PlatformDependencies(FrozenDict({fs.address: b for fs, b in zip(members, builds)}))
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class PackageForPlatformsRequest:
119
+ field_set: PlatformBuildFieldSet
120
+
121
+
122
+ @rule(desc="Package for each platform", level=LogLevel.DEBUG)
123
+ async def package_for_platforms(request: PackageForPlatformsRequest, known: KnownPlatforms) -> BuiltPackage:
124
+ """The host build at its own paths, every other selected platform under `<platform>/`."""
125
+ field_set = request.field_set
126
+ selected = await select_platforms(SelectPlatformsRequest(field_set.platforms), **implicitly())
127
+ if not selected.platforms:
128
+ logger.info(
129
+ f"{field_set.address}: none of its `platforms` is the host ({known.host.name}) or "
130
+ "enabled, so nothing is packaged."
131
+ )
132
+ return BuiltPackage(EMPTY_DIGEST, ())
133
+ builds = await concurrently(
134
+ build_for_platform(**implicitly({field_set: PlatformBuildFieldSet, p: TargetPlatform}))
135
+ for p in selected.platforms
136
+ )
137
+ files = await concurrently(
138
+ digest_subset_to_digest(platform_build_files(b, f"the build of {field_set.address} for {p.name}"))
139
+ for p, b in zip(selected.platforms, builds)
140
+ )
141
+ digests, relpaths = [], []
142
+ for platform, build, digest in zip(selected.platforms, builds, files):
143
+ if platform == known.host:
144
+ digests.append(digest)
145
+ relpaths.extend(build.files)
146
+ else:
147
+ digests.append(await add_prefix(AddPrefix(digest, platform.name)))
148
+ relpaths.extend(f"{platform.name}/{f}" for f in build.files)
149
+ return BuiltPackage(
150
+ await merge_digests(MergeDigests(digests)),
151
+ tuple(BuiltPackageArtifact(relpath=r) for r in relpaths),
152
+ )
153
+
154
+
155
+ def rules():
156
+ return collect_rules()
@@ -0,0 +1,57 @@
1
+ """The targets of one type in the repository, without `AllTargets`.
2
+
3
+ `AllTargets` generates every target, so a rule that awaits it cannot run during target
4
+ generation (it would wait on itself). Reading BUILD files and constructing only the targets of
5
+ one alias avoids that, and is cheaper. It matches the alias as written in BUILD files, so targets
6
+ of a subclass registered under another alias are not found.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from pants.base.specs import RawSpecsWithoutFileOwners, RecursiveGlobSpec
14
+ from pants.engine.collection import Collection
15
+ from pants.engine.internals.graph import (
16
+ address_families_from_raw_specs_without_file_owners,
17
+ resolve_target_parametrizations,
18
+ )
19
+ from pants.engine.internals.parametrize import _TargetParametrizationsRequest
20
+ from pants.engine.rules import collect_rules, concurrently, implicitly, rule
21
+ from pants.engine.target import Target
22
+ from pants.util.logging import LogLevel
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class TargetsOfAliasRequest:
27
+ alias: str
28
+
29
+
30
+ class TargetsOfAlias(Collection[Target]):
31
+ pass
32
+
33
+
34
+ @rule(desc="Find targets of one type", level=LogLevel.DEBUG)
35
+ async def targets_of_alias(request: TargetsOfAliasRequest) -> TargetsOfAlias:
36
+ families = await address_families_from_raw_specs_without_file_owners(
37
+ RawSpecsWithoutFileOwners(
38
+ recursive_globs=(RecursiveGlobSpec(""),),
39
+ description_of_origin=f"the `{request.alias}` targets",
40
+ ),
41
+ **implicitly(),
42
+ )
43
+ # Private, but the only input `resolve_target_parametrizations` takes; Pants' own
44
+ # `resolve_all_generator_target_requests` builds it the same way.
45
+ parametrizations = await concurrently(
46
+ resolve_target_parametrizations(
47
+ _TargetParametrizationsRequest(address, f"the `{request.alias}` targets"), **implicitly()
48
+ )
49
+ for family in families
50
+ for address, adaptor in family.addresses_to_target_adaptors.items()
51
+ if adaptor.type_alias == request.alias
52
+ )
53
+ return TargetsOfAlias(t for p in parametrizations for t in p.all if not t.address.is_generated_target)
54
+
55
+
56
+ def rules():
57
+ return collect_rules()
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pants.engine.platform import Platform
6
+ from pants.engine.rules import collect_rules, implicitly, rule
7
+ from pants.util.frozendict import FrozenDict
8
+ from pants.util.logging import LogLevel
9
+
10
+ from pants_backend_platforms.subsystems import PlatformsSubsystem
11
+ from pants_backend_platforms.target_types import (
12
+ PlatformArchField,
13
+ PlatformOsField,
14
+ PlatformsField,
15
+ PlatformTarget,
16
+ )
17
+ from pants_backend_platforms.util_rules.lookup import TargetsOfAliasRequest, targets_of_alias
18
+
19
+ _MACHINES = {
20
+ Platform.linux_x86_64: ("linux", "x86_64"),
21
+ Platform.linux_arm64: ("linux", "aarch64"),
22
+ Platform.macos_x86_64: ("macos", "x86_64"),
23
+ Platform.macos_arm64: ("macos", "aarch64"),
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True, order=True)
28
+ class TargetPlatform:
29
+ name: str
30
+ os: str
31
+ arch: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class KnownPlatforms:
36
+ by_name: FrozenDict[str, TargetPlatform]
37
+ host: TargetPlatform
38
+
39
+ def unknown(self, name: str, origin: str) -> str:
40
+ return f"{origin}: unknown platform `{name}`; known: {sorted(self.by_name)}"
41
+
42
+ def get(self, name: str, origin: str) -> TargetPlatform:
43
+ platform = self.by_name.get(name)
44
+ if platform is None:
45
+ raise ValueError(self.unknown(name, origin))
46
+ return platform
47
+
48
+ def listed(self, field: PlatformsField) -> tuple[TargetPlatform, ...]:
49
+ """The platforms a `platforms` field names, the host when unset."""
50
+ origin = f"the `platforms` field of {field.address}"
51
+ names = dict.fromkeys(field.value or (self.host.name,))
52
+ return tuple(self.get(name, origin) for name in names)
53
+
54
+
55
+ @rule(desc="Find the platforms", level=LogLevel.DEBUG)
56
+ async def known_platforms(platform: Platform, subsystem: PlatformsSubsystem) -> KnownPlatforms:
57
+ if platform not in _MACHINES:
58
+ raise ValueError(f"pants_backend_platforms does not support Pants on {platform.value}.")
59
+ os_, arch = _MACHINES[platform]
60
+ platforms = await targets_of_alias(TargetsOfAliasRequest(PlatformTarget.alias), **implicitly())
61
+ by_name: dict[str, TargetPlatform] = {}
62
+ for tgt in platforms:
63
+ name = tgt.address.target_name
64
+ if name in by_name:
65
+ raise ValueError(f"Two `platform` targets are named `{name}`.")
66
+ by_name[name] = TargetPlatform(name, tgt[PlatformOsField].value, tgt[PlatformArchField].value)
67
+
68
+ if subsystem.host:
69
+ host = by_name.get(subsystem.host)
70
+ if host is None:
71
+ raise ValueError(
72
+ f"[platforms].host: unknown platform `{subsystem.host}`; known: {sorted(by_name)}"
73
+ )
74
+ if (host.os, host.arch) != (os_, arch):
75
+ raise ValueError(
76
+ f"[platforms].host is `{host.name}` ({host.os}, {host.arch}), but this machine is "
77
+ f"({os_}, {arch})."
78
+ )
79
+ return KnownPlatforms(FrozenDict(by_name), host)
80
+
81
+ matches = sorted(name for name, p in by_name.items() if (p.os, p.arch) == (os_, arch))
82
+ if len(matches) > 1:
83
+ raise ValueError(
84
+ f"Several `platform` targets match this machine ({os_}, {arch}): {matches}. Choose "
85
+ "one with `[platforms].host`."
86
+ )
87
+ if matches:
88
+ return KnownPlatforms(FrozenDict(by_name), by_name[matches[0]])
89
+ host = TargetPlatform(f"{os_}-{arch}", os_, arch)
90
+ if host.name in by_name:
91
+ raise ValueError(
92
+ f"No `platform` matches this machine ({os_}, {arch}), and the implicit host's name "
93
+ f"`{host.name}` is taken by another platform."
94
+ )
95
+ return KnownPlatforms(FrozenDict({**by_name, host.name: host}), host)
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class SelectPlatformsRequest:
100
+ platforms: PlatformsField
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class SelectedPlatforms:
105
+ platforms: tuple[TargetPlatform, ...]
106
+
107
+
108
+ @rule
109
+ async def select_platforms(
110
+ request: SelectPlatformsRequest, known: KnownPlatforms, subsystem: PlatformsSubsystem
111
+ ) -> SelectedPlatforms:
112
+ enabled = {known.host}
113
+ enabled.update(known.get(name, "[platforms].enabled") for name in subsystem.enabled)
114
+ return SelectedPlatforms(tuple(p for p in known.listed(request.platforms) if p in enabled))
115
+
116
+
117
+ def rules():
118
+ return collect_rules()
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "pants_backend_platforms"
3
+ description = "Multi-platform builds for Pants backends: platform targets and a contract for building dependencies per platform."
4
+ version = "0.1.0"
5
+ requires-python = ">=3.11"
6
+ authors = [
7
+ { name = "Tom Solberg", email = "me@sbg.dev" },
8
+ ]
9
+ dependencies = []
10
+ readme = "README.md"
11
+ keywords = [
12
+ "pantsbuild",
13
+ "pants",
14
+ "backend",
15
+ "cross-compilation",
16
+ "platforms",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Programming Language :: Python",
21
+ "Topic :: Software Development :: Build Tools",
22
+ ]
23
+
24
+ [project.license]
25
+ file = "LICENSE-MIT.txt"
26
+
27
+ [project.urls]
28
+ Repository = "https://github.com/tgolsson/pants-backends"
29
+ Code = "https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/platforms"
30
+ Changelog = "https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/platforms/CHANGELOG.md"
31
+
32
+ [tool.pdm.build]
33
+ includes = [
34
+ "pants_backend_platforms",
35
+ ]
36
+
37
+ [tool.isort]
38
+ profile = "black"
39
+ known_first_party = [
40
+ "pants_backend_platforms",
41
+ ]
42
+ line_length = 100
43
+ include_trailing_comma = true
44
+
45
+ [tool.black]
46
+ line-length = 100
47
+
48
+ [build-system]
49
+ requires = [
50
+ "pdm-backend>2.0",
51
+ ]
52
+ build-backend = "pdm.backend"