oras-pants-plugin 0.1.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,113 @@
1
+ """The fields the plugin adds to `pex_binary` and `pex_binaries` targets."""
2
+
3
+ from collections.abc import Iterable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pants.backend.python.target_types import PexBinariesGeneratorTarget, PexBinary
7
+ from pants.engine.addresses import Address
8
+ from pants.engine.target import BoolField, Field, InvalidFieldException, StringField, StringSequenceField
9
+ from pants.util.strutil import softwrap
10
+
11
+ if TYPE_CHECKING:
12
+ from pants_plugin_developer_utils import CollectedRules
13
+
14
+
15
+ def is_untagged_reference(reference: str) -> bool:
16
+ """
17
+ Whether `reference` is a `<registry>/<repository>` OCI reference without a `:<tag>` or `@<digest>` suffix.
18
+
19
+ A registry host may carry a port (`localhost:5000/app`), so only the last path segment is inspected.
20
+ """
21
+ registry, separator, repository = reference.rpartition("/")
22
+ return bool(registry and separator and repository) and ":" not in repository and "@" not in repository
23
+
24
+
25
+ class OrasRepositoriesField(StringSequenceField):
26
+ """Where `oras` pushes the target's package output, as untagged OCI references."""
27
+
28
+ alias = "oci_repositories"
29
+ help = softwrap(
30
+ """
31
+ The OCI repositories to push this target's package output to as an OCI artifact, given as
32
+ `<registry>/<repository>` references such as `ghcr.io/my-org/my-app` or `localhost:5000/my-app`.
33
+ `pants publish` runs one `oras push` per entry, tagging the artifact with every `oci_tags` entry, so a
34
+ reference MUST NOT carry a `:<tag>` or `@<digest>` suffix. Leave unset to skip the target.
35
+ """
36
+ )
37
+
38
+ @classmethod
39
+ def compute_value(cls, raw_value: Iterable[str] | None, address: Address) -> tuple[str, ...] | None:
40
+ value = super().compute_value(raw_value, address)
41
+ invalid = [reference for reference in value or () if not is_untagged_reference(reference)]
42
+ if invalid:
43
+ raise InvalidFieldException(
44
+ softwrap(
45
+ f"""
46
+ The `{cls.alias}` field in target {address} must contain `<registry>/<repository>` references
47
+ without a tag or digest; the tags come from the `{OrasTagsField.alias}` field.
48
+ Invalid entries: {invalid}.
49
+ """
50
+ )
51
+ )
52
+ return value
53
+
54
+
55
+ class OrasTagsField(StringSequenceField):
56
+ """The tags `oras` applies to the pushed artifact in every repository."""
57
+
58
+ alias = "oci_tags"
59
+ default = ("latest",)
60
+ help = softwrap(
61
+ """
62
+ The tags to apply to the pushed OCI artifact in every `oci_repositories` entry. An empty list pushes the
63
+ artifact untagged, addressable by its digest only.
64
+ """
65
+ )
66
+
67
+
68
+ class OciMediaType(StringField):
69
+ """What OCI `artifactType` to designate the `oras`-published artifact."""
70
+
71
+ alias = "oci_media_type"
72
+ help = softwrap(
73
+ """
74
+ The media type identifying the kind of OCI artifact that wraps the package output of this target, passed
75
+ to `oras push` as `--artifact-type` and stored as the manifest's `artifactType`.
76
+ See also: https://oras.land/docs/concepts/artifact
77
+ """
78
+ )
79
+
80
+
81
+ class PexOciMediaType(OciMediaType):
82
+ """The default OCI artifact type for a PEX published via `oras` to an OCI registry."""
83
+
84
+ default = "application/vnd.python.pex.v1"
85
+
86
+
87
+ class SkipOrasPublishField(BoolField):
88
+ """A custom field to opt-out of `oras` publication to the configured OCI registry(s)."""
89
+
90
+ alias = "skip_oras"
91
+ default = False
92
+ help = "If true, don't publish this target's package output with `oras` during `pants publish`."
93
+
94
+
95
+ PEX_BINARY_PLUGIN_FIELDS: tuple[type[Field], ...] = (
96
+ OrasRepositoriesField,
97
+ OrasTagsField,
98
+ PexOciMediaType,
99
+ SkipOrasPublishField,
100
+ )
101
+ """The fields registered on `pex_binary`, and moved from `pex_binaries` onto the targets it generates."""
102
+
103
+
104
+ def rules() -> CollectedRules:
105
+ """Registers the plugin's fields on `pex_binary` and `pex_binaries`."""
106
+ return (
107
+ *(PexBinary.register_plugin_field(field) for field in PEX_BINARY_PLUGIN_FIELDS),
108
+ # `pex_binaries` moves its per-binary fields onto the generated `pex_binary` targets; so must these.
109
+ *(
110
+ PexBinariesGeneratorTarget.register_plugin_field(field, as_moved_field=True)
111
+ for field in PEX_BINARY_PLUGIN_FIELDS
112
+ ),
113
+ )
File without changes
@@ -0,0 +1,203 @@
1
+ """
2
+ Implements a publisher for wrapping `BuiltPackage` instances as OCI artifacts and publishing those to an OCI
3
+ registry by using `oras`.
4
+ See also:
5
+ https://www.pantsbuild.org/stable/docs/writing-plugins/common-plugin-tasks/add-a-publisher
6
+ https://oras.land/
7
+ """
8
+
9
+ import os
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, Final
12
+
13
+ from pants.backend.python.goals.package_pex_binary import PexBinaryFieldSet
14
+ from pants.backend.python.target_types import PexLayoutField
15
+ from pants.core.goals.package import PackageFieldSet
16
+ from pants.core.goals.publish import (
17
+ CheckSkipRequest,
18
+ CheckSkipResult,
19
+ PublishFieldSet,
20
+ PublishOutputData,
21
+ PublishPackages,
22
+ PublishProcesses,
23
+ PublishRequest,
24
+ )
25
+ from pants.core.util_rules.env_vars import environment_vars_subset
26
+ from pants.core.util_rules.external_tool import download_external_tool
27
+ from pants.engine.env_vars import EnvironmentVarsRequest
28
+ from pants.engine.fs import Digest, MergeDigests
29
+ from pants.engine.intrinsics import merge_digests
30
+ from pants.engine.platform import Platform
31
+ from pants.engine.process import InteractiveProcess, Process, ProcessCacheScope
32
+ from pants.engine.rules import collect_rules, concurrently, implicitly, rule
33
+ from pants.engine.unions import UnionRule
34
+ from pants.util.logging import LogLevel
35
+ from pants.util.strutil import pluralize
36
+
37
+ from oras_pants_plugin.fields import (
38
+ OciMediaType,
39
+ OrasRepositoriesField,
40
+ OrasTagsField,
41
+ PexOciMediaType,
42
+ SkipOrasPublishField,
43
+ )
44
+ from oras_pants_plugin.subsystem import ORAS_BASE_ENV_VAR_NAMES, Oras
45
+
46
+ if TYPE_CHECKING:
47
+ from pants_plugin_developer_utils import CollectedRules
48
+
49
+ ORAS_INPUT_DIR: Final[str] = "__oras__"
50
+ """Sandbox directory the downloaded `oras` binary is mounted in, out of the way of the pushed files."""
51
+
52
+
53
+ class OrasPublishRequest(PublishRequest["OrasPublishFieldSet"]):
54
+ """The general `PublishRequest` type which should be subtyped per `oras`-publishable target type."""
55
+
56
+
57
+ class OrasPublishPexRequest(OrasPublishRequest):
58
+ """The `PublishRequest` subtype specifically for publishing PEX artifacts to OCI registries."""
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class OrasPublishFieldSet(PublishFieldSet[OrasPublishRequest]):
63
+ """
64
+ The base `PublishFieldSet` to subtype per `oras`-publishable target type.
65
+
66
+ A subtype narrows `required_fields` to the target type's `OciMediaType` subclass, so `oci_media_type` carries
67
+ that subclass's default.
68
+ """
69
+
70
+ required_fields = (OrasRepositoriesField, OrasTagsField, OciMediaType, SkipOrasPublishField)
71
+
72
+ oci_repositories: OrasRepositoriesField
73
+ oci_tags: OrasTagsField
74
+ oci_media_type: OciMediaType
75
+ skip_oras: SkipOrasPublishField
76
+
77
+ def check_skip_request(self, package_fs: PackageFieldSet) -> OrasPublishSkipRequest | None:
78
+ return OrasPublishSkipRequest(package_fs=package_fs, publish_fs=self)
79
+
80
+ def get_output_data(self) -> PublishOutputData:
81
+ return PublishOutputData({"publisher": "oras", **super().get_output_data()})
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class OrasPublishPexFieldSet(OrasPublishFieldSet):
86
+ """The fields required for publishing a `pex_binary` target's package output artifact to an OCI registry."""
87
+
88
+ publish_request_type = OrasPublishPexRequest
89
+ required_fields = (*OrasPublishFieldSet.required_fields, PexOciMediaType, PexLayoutField)
90
+
91
+
92
+ class OrasPublishSkipRequest(CheckSkipRequest[OrasPublishFieldSet]):
93
+ """The input to determining if an `oras`-compatible target should be skipped during a publish run."""
94
+
95
+
96
+ @rule(desc="Checks whether `oras push` is skipped for a target.", level=LogLevel.DEBUG)
97
+ async def check_if_skip_oras_push(request: OrasPublishSkipRequest, oras: Oras) -> CheckSkipResult:
98
+ """Skips both packaging and publishing when nothing would be pushed, and says why."""
99
+ if oras.skip:
100
+ reason = f"(by `[{Oras.options_scope}].skip = True`)"
101
+ elif request.publish_fs.skip_oras.value:
102
+ reason = f"(by `{request.publish_fs.skip_oras.alias}` on {request.address})"
103
+ elif not request.publish_fs.oci_repositories.value:
104
+ reason = f"(no `{request.publish_fs.oci_repositories.alias}` specified for {request.address})"
105
+ else:
106
+ return CheckSkipResult.no_skip()
107
+ name = (
108
+ request.package_fs.output_pex_filename()
109
+ if isinstance(request.package_fs, PexBinaryFieldSet)
110
+ else request.address.spec
111
+ )
112
+ return CheckSkipResult.skip(names=[name], description=reason, data=request.publish_fs.get_output_data())
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class OrasPushRequest:
117
+ """One `oras push`: every `files` entry becomes a layer of the artifact at `repository`, tagged with `tags`."""
118
+
119
+ repository: str
120
+ tags: tuple[str, ...]
121
+ files: tuple[str, ...]
122
+ input_digest: Digest
123
+ artifact_type: str | None = None
124
+
125
+ @property
126
+ def reference(self) -> str:
127
+ """The `<repository>[:<tag>[,<tag>...]]` argument of `oras push`; no tags pushes the artifact untagged."""
128
+ return f"{self.repository}:{','.join(self.tags)}" if self.tags else self.repository
129
+
130
+
131
+ def construct_oras_push_argv(oras: Oras, exe: str, request: OrasPushRequest) -> tuple[str, ...]:
132
+ """Returns the full `oras push` command line for `request`, with `[oras].args` after the plugin's own flags."""
133
+ argv: list[str] = [exe, "push"]
134
+ if request.artifact_type:
135
+ argv.extend(["--artifact-type", request.artifact_type])
136
+ argv.extend(oras.args)
137
+ argv.append(request.reference)
138
+ # `oras` parses `<file>:<media type>`, so a path containing `:` needs a trailing `:` to keep the default type.
139
+ argv.extend(f"{file}:" if ":" in file else file for file in request.files)
140
+ return tuple(argv)
141
+
142
+
143
+ @rule(desc="Set up an `oras push` process", level=LogLevel.DEBUG)
144
+ async def setup_oras_push_process(request: OrasPushRequest, oras: Oras, platform: Platform) -> Process:
145
+ downloaded_oras, env = await concurrently(
146
+ download_external_tool(oras.get_request(platform)),
147
+ environment_vars_subset(
148
+ EnvironmentVarsRequest([*ORAS_BASE_ENV_VAR_NAMES, *oras.extra_env_vars]), **implicitly()
149
+ ),
150
+ )
151
+ return Process(
152
+ argv=construct_oras_push_argv(
153
+ oras, os.path.normpath(os.path.join(ORAS_INPUT_DIR, downloaded_oras.exe)), request
154
+ ),
155
+ input_digest=request.input_digest,
156
+ immutable_input_digests={ORAS_INPUT_DIR: downloaded_oras.digest},
157
+ env=env,
158
+ description=f"Push {pluralize(len(request.files), 'file')} to {request.reference} with `oras`",
159
+ level=LogLevel.DEBUG,
160
+ # Publishing must run every time: the engine asserts this scope for non-interactive publish processes.
161
+ cache_scope=ProcessCacheScope.PER_SESSION,
162
+ )
163
+
164
+
165
+ @rule(
166
+ desc="Uses `oras` to wrap PEX artifacts as an OCI artifact and push to the configured OCI registry.",
167
+ level=LogLevel.DEBUG,
168
+ )
169
+ async def publish_pex_as_oci_artifact(request: OrasPublishPexRequest) -> PublishProcesses:
170
+ """One interactive `oras push` per repository, each pushing every artifact `pants package` built."""
171
+ files = tuple(artifact.relpath for pkg in request.packages for artifact in pkg.artifacts if artifact.relpath)
172
+ if not files:
173
+ return PublishProcesses()
174
+
175
+ input_digest = await merge_digests(MergeDigests(pkg.digest for pkg in request.packages))
176
+ field_set = request.field_set
177
+ push_requests = tuple(
178
+ OrasPushRequest(
179
+ repository=repository,
180
+ tags=field_set.oci_tags.value or (),
181
+ files=files,
182
+ input_digest=input_digest,
183
+ artifact_type=field_set.oci_media_type.value,
184
+ )
185
+ for repository in field_set.oci_repositories.value or ()
186
+ )
187
+ processes = await concurrently(
188
+ setup_oras_push_process(push_request, **implicitly()) for push_request in push_requests
189
+ )
190
+ return PublishProcesses(
191
+ PublishPackages(
192
+ names=files,
193
+ process=InteractiveProcess.from_process(process),
194
+ description=push_request.reference,
195
+ data=PublishOutputData({"repository": push_request.repository, "tags": push_request.tags}),
196
+ )
197
+ for push_request, process in zip(push_requests, processes, strict=True)
198
+ )
199
+
200
+
201
+ def rules() -> CollectedRules:
202
+ """Returns the `publish`-goal-specific rules for the `oras` pants plugin."""
203
+ return (*collect_rules(), *OrasPublishPexFieldSet.rules(), UnionRule(CheckSkipRequest, OrasPublishSkipRequest))
File without changes
@@ -0,0 +1,20 @@
1
+ """Backend entry point: Pants loads this module when `oras_pants_plugin` is in `backend_packages`."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from oras_pants_plugin.fields import rules as field_rules
6
+ from oras_pants_plugin.goals.publish import rules as publish_rules
7
+ from oras_pants_plugin.subsystem import rules as subsystem_rules
8
+
9
+ if TYPE_CHECKING:
10
+ from pants_plugin_developer_utils import CollectedRules, CollectedTargetTypes
11
+
12
+
13
+ def rules() -> CollectedRules:
14
+ """Return every rule this backend registers with the Pants engine."""
15
+ return (*field_rules(), *publish_rules(), *subsystem_rules())
16
+
17
+
18
+ def target_types() -> CollectedTargetTypes:
19
+ """The plugin adds fields to the builtin `pex_binary` targets and registers no target types of its own."""
20
+ return ()
@@ -0,0 +1,84 @@
1
+ """The `oras` subsystem: where the binary comes from and how `pants publish` runs it."""
2
+
3
+ from typing import TYPE_CHECKING, Final
4
+
5
+ from pants.core.goals.resolves import ExportableTool
6
+ from pants.core.util_rules.external_tool import TemplatedExternalTool
7
+ from pants.engine.platform import Platform
8
+ from pants.engine.rules import collect_rules
9
+ from pants.engine.unions import UnionRule
10
+ from pants.option.option_types import ArgsListOption, SkipOption, StrListOption
11
+ from pants.util.strutil import softwrap
12
+
13
+ if TYPE_CHECKING:
14
+ from pants_plugin_developer_utils import CollectedRules
15
+
16
+ ORAS_BASE_ENV_VAR_NAMES: Final[tuple[str, ...]] = ("PATH", "HOME", "DOCKER_CONFIG")
17
+ """
18
+ Always passed to `oras` processes. `oras` reads the credentials `docker login` and `oras login` store in
19
+ `$DOCKER_CONFIG/config.json` or `~/.docker/config.json`, and runs the credential helpers named there from `PATH`.
20
+ """
21
+
22
+
23
+ class Oras(TemplatedExternalTool):
24
+ """
25
+ The configuration and tooling wrapper for the `oras` binary to publish arbitrary artifacts as OCI artifacts in
26
+ OCI-compliant registries. See also: https://oras.land/docs/
27
+ """
28
+
29
+ options_scope = "oras"
30
+ name = "oras"
31
+ help = "The `oras` CLI for pushing files to OCI registries as OCI artifacts (https://oras.land/)."
32
+
33
+ default_version = "1.3.4"
34
+ # Compute with: `curl -L $URL | tee >(wc -c) >(shasum -a 256) >/dev/null`
35
+ # Pants declares these class attributes without `ClassVar`, so `ty` rejects a `ClassVar` annotation here.
36
+ default_known_versions = [ # noqa: RUF012
37
+ "1.3.4|linux_arm64 |15702c6e3a4a56a8bd8ac5c17efdbcab56d9bada661ccbcf017f5b10c1d89399|4238235",
38
+ "1.3.4|linux_x86_64|f27adb935022d94df8dc77719c322dda592c78a0d57a6f7dcdd8d900b248c454|4679642",
39
+ "1.3.4|macos_arm64 |217761a9500242ff473de8656b5aca21136ff39e17e9e61fd8936bbfd902704c|4401355",
40
+ "1.3.4|macos_x86_64|5e964f3d5a36eb9499a9d3e252a86b09e7adf3e6f6447eec56fd249c6702af7e|4750901",
41
+ ]
42
+ version_constraints = ">=1.3,<2"
43
+ default_url_template = (
44
+ "https://github.com/oras-project/oras/releases/download/v{version}/oras_{version}_{platform}.tar.gz"
45
+ )
46
+ # Go binaries are statically linked, so one Linux build runs on any distribution.
47
+ default_url_platform_mapping: dict[str, str] = { # noqa: RUF012
48
+ "linux_arm64": "linux_arm64",
49
+ "linux_x86_64": "linux_amd64",
50
+ "macos_arm64": "darwin_arm64",
51
+ "macos_x86_64": "darwin_amd64",
52
+ }
53
+
54
+ skip = SkipOption("publish")
55
+ args = ArgsListOption(
56
+ example="--registry-config /path/to/auth.json --plain-http",
57
+ extra_help=softwrap(
58
+ """
59
+ Added to every `oras push` run by `pants publish`, after the plugin's own flags
60
+ (https://oras.land/docs/commands/oras_push). A repeated flag overrides the plugin's value.
61
+ """
62
+ ),
63
+ )
64
+ extra_env_vars = StrListOption(
65
+ default=[],
66
+ advanced=True,
67
+ help=softwrap(
68
+ """
69
+ Additional environment variables to make available to `oras` processes. Entries are either strings
70
+ in the form `ENV_VAR=value` to use explicitly, or just `ENV_VAR` to copy the value from Pants's own
71
+ environment. `PATH`, `HOME` and `DOCKER_CONFIG` are always passed through, so the credentials stored
72
+ by `docker login` or `oras login` and their credential helpers are found. Use this for proxy or TLS
73
+ settings such as `HTTPS_PROXY` or `SSL_CERT_FILE`.
74
+ """
75
+ ),
76
+ )
77
+
78
+ def generate_exe(self, plat: Platform) -> str:
79
+ """The release tarball extracts the binary to its root, next to the license."""
80
+ return "./oras"
81
+
82
+
83
+ def rules() -> CollectedRules:
84
+ return (*collect_rules(), UnionRule(ExportableTool, Oras))
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: oras-pants-plugin
3
+ Version: 0.1.0
4
+ Summary: A Pantsbuild plugin for publishing PEX binaries to OCI registries as OCI artifacts with oras.
5
+ Author: Zach Gottesman
6
+ Maintainer: zach-overflow
7
+ License-Expression: Apache-2.0
8
+ Project-URL: Bug Tracker, https://github.com/zach-overflow/pants-plugin-depot/issues
9
+ Project-URL: Changelog, https://github.com/zach-overflow/pants-plugin-depot/blob/main/provides/oras-pants-plugin/CHANGELOG.md
10
+ Project-URL: Repository, https://github.com/zach-overflow/pants-plugin-depot
11
+ Keywords: oci,oci artifact,oras,pants,pants plugin,pantsbuild,pex,publish,registry
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Classifier: Topic :: System :: Software Distribution
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.14
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: pants-plugin-developer-utils>=0.1.0
23
+ Dynamic: license-file
24
+
25
+ # oras-pants-plugin
26
+
27
+ A [Pantsbuild](https://www.pantsbuild.org/) plugin that publishes the output of `pex_binary` targets to OCI
28
+ registries as [OCI artifacts](https://oras.land/docs/concepts/artifact) with [`oras`](https://oras.land/). Any
29
+ registry that stores OCI artifacts works: GitHub Container Registry, Docker Hub, Artifact Registry, ECR, ACR,
30
+ Harbor, a local `registry:2`, and so on.
31
+
32
+ ## Installation
33
+
34
+ Add the plugin to `pants.toml` and enable its backend next to the Python backend:
35
+
36
+ ```toml
37
+ [GLOBAL]
38
+ plugins = ["oras-pants-plugin==<version>"]
39
+ backend_packages.add = ["pants.backend.python", "oras_pants_plugin"]
40
+ ```
41
+
42
+ Tell a `pex_binary` target where to push:
43
+
44
+ ```python
45
+ # src/app/BUILD
46
+ pex_binary(
47
+ name="app",
48
+ entry_point="main.py",
49
+ oci_repositories=["ghcr.io/my-org/app"],
50
+ oci_tags=["1.2.0", "latest"],
51
+ )
52
+ ```
53
+
54
+ Then build and push it through the `publish` goal:
55
+
56
+ ```bash
57
+ pants publish src/app:app
58
+ ```
59
+
60
+ Pull it back anywhere with `oras pull ghcr.io/my-org/app:1.2.0`, which restores `app.pex`.
61
+
62
+ ## How it works
63
+
64
+ `oras` is downloaded as a static binary; nothing is installed into a Pants resolve. For every `pex_binary`
65
+ target that lists `oci_repositories`, `pants publish`:
66
+
67
+ 1. Packages the target as `pants package` would. A target without `oci_repositories`, with `skip_oras=True`,
68
+ or run with `[oras].skip = true` is skipped before it is packaged.
69
+ 2. Runs one `oras push` per `oci_repositories` entry, in the foreground so `oras` can show progress. Every
70
+ artifact `pants package` produced for the target (the PEX, plus any `scie` executables) becomes a layer of
71
+ one OCI artifact, which is tagged with every `oci_tags` entry at once. The manifest's `artifactType` is the
72
+ target's `oci_media_type`.
73
+ 3. Reports each push as published, failed or skipped, like every other publisher. `--publish-output` writes the
74
+ same as JSON, with the `repository` and `tags` of each push.
75
+
76
+ A `packed` or `loose` layout PEX is a directory, which `oras` pushes as a gzipped tar layer and `oras pull`
77
+ unpacks again. The layer media types are left to `oras`.
78
+
79
+ ### Target fields
80
+
81
+ | Field | Target | Purpose |
82
+ | :----------------- | :--------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | `oci_repositories` | `pex_binary`, `pex_binaries` | `<registry>/<repository>` references to push to, such as `ghcr.io/my-org/app` or `localhost:5000/app`. One push per entry. A reference MUST NOT carry a tag or digest. |
84
+ | `oci_tags` | `pex_binary`, `pex_binaries` | Tags applied to the artifact in every repository. Default: `["latest"]`. `[]` pushes the artifact untagged, addressable by digest only. |
85
+ | `oci_media_type` | `pex_binary`, `pex_binaries` | The `artifactType` of the pushed manifest, passed as `--artifact-type`. Default: `application/vnd.python.pex.v1`. |
86
+ | `skip_oras` | `pex_binary`, `pex_binaries` | If true, the target is skipped by `pants publish`. |
87
+ | `overrides` | `pex_binaries` | Per-binary values for the fields above, e.g. `overrides={"tool.py": {"skip_oras": True}}`. On `pex_binaries` the fields apply to every generated `pex_binary`. |
88
+
89
+ ## Configuration
90
+
91
+ | Option | Default | Purpose |
92
+ | :------------------------- | :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `[oras].skip` | `false` | Skip `oras` during `pants publish`. |
94
+ | `[oras].args` | `[]` | Extra arguments for every `oras push`, e.g. `--plain-http` or `--registry-config /path/to/auth.json`. They follow the plugin's own flags, so a repeated flag overrides it. |
95
+ | `[oras].extra_env_vars` | `[]` | Extra environment variables for `oras`, as `NAME=value` or `NAME` to copy from Pants' environment, e.g. `HTTPS_PROXY` or `SSL_CERT_FILE`. |
96
+ | `[oras].version` | bundled default | `oras` release to download. |
97
+ | `[oras].known_versions` | bundled default | Checksums of downloadable releases; extend to use another version. |
98
+
99
+ ### Credentials
100
+
101
+ `oras` reads the credentials that `docker login` or `oras login` store in `$DOCKER_CONFIG/config.json` or
102
+ `~/.docker/config.json`, and runs the credential helpers configured there. `PATH`, `HOME` and `DOCKER_CONFIG`
103
+ are always passed to `oras`, so logging in once is enough:
104
+
105
+ ```bash
106
+ oras login ghcr.io # or: docker login ghcr.io
107
+ pants publish src/app:app
108
+ ```
109
+
110
+ In CI, pass the token to a login step before `pants publish`, or point `[oras].args` at an auth file with
111
+ `--registry-config`. Avoid `--username`/`--password` in `pants.toml`: the file is committed.
112
+
113
+ ### Pushing to a directory
114
+
115
+ `oras` can write to an [OCI image layout](https://github.com/opencontainers/image-spec/blob/main/image-layout.md)
116
+ directory instead of a registry, which is handy for inspecting what would be pushed:
117
+
118
+ ```bash
119
+ pants --oras-args="['--oci-layout-path', '/tmp/app-layout']" publish src/app:app
120
+ ```
121
+
122
+ The reference in `oci_repositories` is kept as the tag name inside the layout.
123
+
124
+ ### Using a different `oras` version
125
+
126
+ Add the release's checksums and sizes to `known_versions` and set `version`:
127
+
128
+ ```toml
129
+ [oras]
130
+ version = "1.3.5"
131
+ known_versions.add = [
132
+ "1.3.5|linux_arm64 |<sha256>|<size>",
133
+ "1.3.5|linux_x86_64|<sha256>|<size>",
134
+ "1.3.5|macos_arm64 |<sha256>|<size>",
135
+ "1.3.5|macos_x86_64|<sha256>|<size>",
136
+ ]
137
+ ```
138
+
139
+ Compute each line with `curl -L $URL | tee >(wc -c) >(shasum -a 256) >/dev/null`, where `$URL` is the
140
+ `oras_<version>_<os>_<arch>.tar.gz` asset of the
141
+ [GitHub release](https://github.com/oras-project/oras/releases) (`linux`/`darwin`, `amd64`/`arm64`). The
142
+ release's `oras_<version>_checksums.txt` lists the same checksums.
143
+
144
+ ## Supported versions
145
+
146
+ | Plugin | Pants | Python | `oras` |
147
+ | :----- | :----- | :----- | :--------- |
148
+ | `0.x` | `2.33` | `3.14` | `>=1.3,<2` |
149
+
150
+ Pants loads plugins into its own interpreter, so the plugin requires the Python version Pants runs on. The PEX
151
+ being published may target any Python version.
152
+
153
+ ## Releases
154
+
155
+ Wheels are published to [PyPI](https://pypi.org/project/oras-pants-plugin/). Release notes live in the
156
+ [changelog](https://github.com/zach-overflow/pants-plugin-depot/blob/main/provides/oras-pants-plugin/CHANGELOG.md)
157
+ and on the [GitHub releases page](https://github.com/zach-overflow/pants-plugin-depot/releases).
158
+
159
+ ## Contributing, Bug Reports and Feature Requests
160
+
161
+ This plugin lives in the [pants-plugin-depot](https://github.com/zach-overflow/pants-plugin-depot) repo.
162
+ See its [CONTRIBUTING.md](https://github.com/zach-overflow/pants-plugin-depot/blob/main/CONTRIBUTING.md), or
163
+ open an [issue](https://github.com/zach-overflow/pants-plugin-depot/issues/new/choose).
@@ -0,0 +1,13 @@
1
+ oras_pants_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ oras_pants_plugin/fields.py,sha256=2aHICEvew7-AW1PZWmIcWOXifX98aJ9TdMfV_yAKhq4,4374
3
+ oras_pants_plugin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ oras_pants_plugin/register.py,sha256=YBxqBDYmCUldPCt7TuAGgYwDop6Bf4mkjRGqoa4CxvI,772
5
+ oras_pants_plugin/subsystem.py,sha256=OjWuS7D7rrnJpOGcMtQ-TnN76QcMckJBGCroD_NI1ug,3783
6
+ oras_pants_plugin/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
7
+ oras_pants_plugin/goals/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ oras_pants_plugin/goals/publish.py,sha256=vtgzsa4xU_8NtWp-yZWF4QXUMxcxhEOlghmTtqxf-bo,8427
9
+ oras_pants_plugin-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
10
+ oras_pants_plugin-0.1.0.dist-info/METADATA,sha256=dATDjCLCDiYf8VHrJIhs8WOxfEo2P4m9oy5RcesWkM4,8855
11
+ oras_pants_plugin-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
12
+ oras_pants_plugin-0.1.0.dist-info/top_level.txt,sha256=OyVbti6SvqLNhscUwlw3jFiyXn6ZwXaShtubLVa4sFE,18
13
+ oras_pants_plugin-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1 @@
1
+ oras_pants_plugin