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.
- pants_backend_platforms-0.1.0/LICENSE-MIT.txt +21 -0
- pants_backend_platforms-0.1.0/PKG-INFO +97 -0
- pants_backend_platforms-0.1.0/README.md +60 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/__init__.py +0 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/register.py +12 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/subsystems.py +24 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/target_types.py +41 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/util_rules/__init__.py +0 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/util_rules/contract.py +156 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/util_rules/lookup.py +57 -0
- pants_backend_platforms-0.1.0/pants_backend_platforms/util_rules/platforms.py +118 -0
- pants_backend_platforms-0.1.0/pyproject.toml +52 -0
|
@@ -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.
|
|
File without changes
|
|
@@ -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
|
+
)
|
|
File without changes
|
|
@@ -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"
|