pants-backend-distroless 0.1.0rc0__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,254 @@
1
+ Metadata-Version: 2.1
2
+ Name: pants_backend_distroless
3
+ Version: 0.1.0rc0
4
+ Summary: A distroless image plugin for the Pants build system
5
+ Keywords: pantsbuild,pants,backend,distroless,debian,apt,oci
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 :: 4 - Beta
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/distroless
34
+ Project-URL: Changelog, https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/distroless/CHANGELOG.md
35
+ Description-Content-Type: text/markdown
36
+
37
+ # Distroless backend for Pants
38
+
39
+ This release targets Pants **2.32.x**. Install it with
40
+ `pants-backend-distroless==0.1.0rc0` in `[GLOBAL].plugins` in `pants.toml`, and enable
41
+ `pants_backend_distroless` in `[GLOBAL].backend_packages`.
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/pants-backend-distroless?label=Latest%20release)](https://pypi.org/project/pants-backend-distroless)
44
+
45
+ > **Warning**
46
+ > This plugin is in development. No stability is guaranteed! Contributions welcome.
47
+
48
+ This backend builds the parts of distroless-style images that come from Debian or Ubuntu packages, in the
49
+ spirit of [rules_distroless](https://github.com/GoogleContainerTools/rules_distroless): declare the
50
+ top-level packages against an apt repository snapshot, and `pants generate-lockfiles` resolves their
51
+ dependencies into a committed lockfile. The locked packages feed `oci_deb_layer` from
52
+ [`pants-backend-oci`](https://pypi.org/project/pants-backend-oci), which builds a layer from them
53
+ without running `apt`, `dpkg` or maintainer scripts. `pants-backend-oci` also has the helper layers such
54
+ an image needs: `oci_passwd_layer`, `oci_group_layer`, `oci_cacerts_layer` and `oci_locale_layer`.
55
+
56
+ ## Targets
57
+
58
+ ### `deb_packages`
59
+
60
+ A set of Debian packages resolved into a lockfile. Each resolve is named in `[apt].resolves`, which
61
+ maps it to its lockfile, and is declared by exactly one `deb_packages` target.
62
+
63
+ ``` toml
64
+ [apt.resolves]
65
+ bookworm = "3rdparty/debian/bookworm.lock.json"
66
+ ```
67
+
68
+ ``` python
69
+ deb_packages(
70
+ name="bookworm",
71
+ resolve="bookworm",
72
+ sources=["https://snapshot.debian.org/archive/debian/20241001T000000Z bookworm main"],
73
+ architectures=["amd64", "arm64"],
74
+ packages=["jq"],
75
+ # Already in the base image.
76
+ exclude=["libc6"],
77
+ # From the `debian-archive-keyring` package.
78
+ keyrings=["debian-archive-keyring.gpg"],
79
+ )
80
+
81
+ oci_deb_layer(
82
+ name="jq",
83
+ debs=[":bookworm#amd64/jq"],
84
+ )
85
+ ```
86
+
87
+ Run `pants generate-lockfiles --resolve=bookworm` to write the lockfile, and again whenever the
88
+ target changes. Until then, and while the lockfile is out of date, Pants warns and generates no
89
+ packages, so anything using them fails with an error that `:bookworm#amd64/jq` was not generated.
90
+ The warning is only printed when the targets are generated, so with `pantsd` a later run may show
91
+ just that error; regenerating the lockfile fixes it.
92
+
93
+ The target generates:
94
+
95
+ * a `deb_package` target named `<arch>/<package>` for every locked package, which depends on the
96
+ pinned `.deb` of the package and of every locked package in its closure, so `:bookworm#amd64/jq`
97
+ brings `jq` and its whole closure;
98
+ * a `deb_package_file` target named `<arch>/<package>/deb` for every locked package, which downloads
99
+ only its pinned `.deb`;
100
+ * a `deb_package_group` target named `<arch>`, depending on all the requested packages, so
101
+ `:bookworm#amd64` is everything.
102
+
103
+ Packages don't depend on each other's targets, only on `.deb` files, and the lockfile records each
104
+ package's closure. Apt dependency cycles are therefore fine: `libc6` and `libgcc-s1` depend on each
105
+ other in Debian, and `:bookworm#amd64/libc6` brings both.
106
+
107
+ A layer is for one architecture, so always pick one: `debs=[":bookworm"]` would bring the packages of
108
+ every architecture, which `oci_deb_layer` rejects.
109
+
110
+ | Argument | Meaning | Default value |
111
+ |------------------|-----------------------------------------------------------------------------------------------|-------------------------------------------------------|
112
+ | `name` | The target name | Same as any other target, which is the directory name |
113
+ | `resolve` | The resolve name, a key of `[apt].resolves`. | Required |
114
+ | `sources` | The repositories, each `<uri> <suite> <component>...` like a sources.list line without `deb`. | Required |
115
+ | `architectures` | The Debian architectures to lock, e.g. `amd64` and `arm64`. | Required |
116
+ | `packages` | The top-level packages. | Required |
117
+ | `exclude` | Packages treated as already installed, typically those the base image ships. | `[]` |
118
+ | `keyrings` | OpenPGP keyrings (`.gpg` or `.asc`) to verify the repositories' `Release` signatures with. | Required unless `allow_unsigned=True` |
119
+ | `allow_unsigned` | Resolve without verifying signatures. Can't be combined with `keyrings`. | `False` |
120
+ | `description` | A description of the target | |
121
+ | `tags` | List of tags | `[]` |
122
+
123
+ #### Sources
124
+
125
+ Pin each URI to a snapshot, such as `https://snapshot.debian.org/archive/debian/<timestamp>`, so
126
+ the lockfile can be regenerated and the packages stay downloadable. Several sources can be combined,
127
+ for example to add `bookworm-updates`, or `bookworm-security` from
128
+ `https://snapshot.debian.org/archive/debian-security/<timestamp>`. Every `Packages` index is checked
129
+ against the SHA256 in its suite's signed `Release` file, see [Signature verification](#signature-verification).
130
+
131
+ Ubuntu works the same way, from [snapshot.ubuntu.com](https://snapshot.ubuntu.com). Its updates
132
+ and security fixes are separate suites of the same archive, and unlike the live archive, where
133
+ arm64 lives on `ports.ubuntu.com`, the snapshot serves arm64 packages under `/ubuntu` too, so one
134
+ set of sources covers both architectures. `Phased-Update-Percentage` is ignored: a snapshot lock
135
+ takes a phased update like any other version. Ubuntu's packages are zstd-compressed, which
136
+ `oci_deb_layer` reads with its `zstd` tool.
137
+
138
+ ``` python
139
+ deb_packages(
140
+ name="noble",
141
+ resolve="noble",
142
+ sources=[
143
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble main",
144
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble-updates main",
145
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble-security main",
146
+ ],
147
+ architectures=["amd64", "arm64"],
148
+ packages=["jq", "ca-certificates"],
149
+ # Already in the ubuntu:noble base image.
150
+ exclude=["libc6", "debconf", "libssl3t64"],
151
+ # From the `ubuntu-keyring` package.
152
+ keyrings=["ubuntu-archive-keyring.gpg"],
153
+ )
154
+ ```
155
+
156
+ #### Signature verification
157
+
158
+ Like rules_distroless's `gpg_keys`, `generate-lockfiles` verifies every suite's `InRelease`, or its
159
+ `Release` and `Release.gpg` when there's no `InRelease`, against `keyrings`, and fails unless at
160
+ least one signature is good and made by a key in them. A bad signature from one of their keys fails
161
+ too. The hashes of the `Packages` indices are only taken from the verified file. It also fails
162
+ unless the signed file's `Suite` or `Codename` is the suite asked for, so another suite's validly
163
+ signed `Release` can't stand in for it. This is apt's rule, though apt only warns: a path like
164
+ debian-security's old `buster/updates` matches by what comes before its last `/`. Either `keyrings` or `allow_unsigned=True` must be set.
165
+
166
+ Use the archive's own keyring, from the snapshot you resolve against so it's pinned too:
167
+ `/usr/share/keyrings/debian-archive-keyring.gpg` from the `debian-archive-keyring` package, or
168
+ `/usr/share/keyrings/ubuntu-archive-keyring.gpg` from `ubuntu-keyring`. Copy it next to the
169
+ `deb_packages` target, since sources can't come from another directory.
170
+
171
+ Verification needs no `gpgv` or `sqv`: it's built into the resolver, and supports what Debian and
172
+ Ubuntu sign with, v4 RSA and Ed25519 keys with SHA-2 hashes. Older snapshots signed only with SHA-1
173
+ need `allow_unsigned=True`, which turns signature verification off entirely; the suite is still
174
+ checked. Every key in the keyrings is trusted as given: expiry and revocation aren't checked, since a
175
+ snapshot's `Release` files are historical. For the same reason, freshness (`Valid-Until`) isn't
176
+ checked either: the snapshot URL is what pins it. The keys aren't recorded in the lockfile, so
177
+ changing `keyrings` doesn't make it out of date.
178
+
179
+ #### Resolution
180
+
181
+ The `Pre-Depends` and `Depends` of the requested packages are resolved transitively, for each
182
+ architecture separately, much like rules_distroless does:
183
+
184
+ * A package resolves to its highest version, across all sources, that satisfies the constraint.
185
+ When sources have the same version, the first source wins.
186
+ * For alternatives (`a | b`), a package already in the closure is preferred; otherwise the first
187
+ alternative that exists wins.
188
+ * A virtual package (`Provides`) resolves to a provider already in the closure, else the only
189
+ provider, else a `Priority: required` one, else the highest version, else the first by name.
190
+ * The result doesn't depend on the order of `packages`.
191
+
192
+ `Recommends` and `Suggests` are not installed, and `Conflicts` and `Breaks` are not checked.
193
+
194
+ The resolver runs in a sandboxed process from `apt-resolver`, a [scie](https://github.com/a-scie/jump)
195
+ the plugin builds with pex from its own resolver code and a pinned python-build-standalone Python,
196
+ CPython `3.11.17` from release `20261009` (set `[apt].resolver_python_version` and
197
+ `[apt].resolver_pbs_release` to use another). Since it names nothing on the host, a resolve's
198
+ cached result can be shared across checkouts, and across machines through a remote cache. It is built once per machine, downloading
199
+ that Python and pex's `science` tool from GitHub the first time, and unpacks into the
200
+ `apt_resolver` named cache. The Python and `science` are checked only against the checksum files
201
+ published in the same GitHub releases, through pex.
202
+
203
+ #### Packages already in the base image
204
+
205
+ A layer replaces any file the base image has at the same path, and nothing warns about it. If a
206
+ locked package is also in the base, its files from the snapshot overwrite the base's copies, which
207
+ can mix versions when the base image was built from a different archive state than the snapshot.
208
+ List such packages in `exclude`: they count as installed, satisfy any dependency on them, and are not
209
+ traversed, so whatever only they need is left out as well. Images built on an empty base leave
210
+ `exclude` empty.
211
+
212
+ ## Moving from a Python base image
213
+
214
+ A few things change when a Python app moves from a pulled base such as `python:3.11-slim` to one
215
+ built from Debian or Ubuntu packages.
216
+
217
+ **The interpreter is `/usr/bin/python3.X`.** The official Python images install it under
218
+ `/usr/local/bin` and have `/usr/bin/env`. A package's interpreter is `/usr/bin/python3.X`, where
219
+ `3.X` is the suite's Python: 3.11 in Debian bookworm, 3.13 in trixie, 3.12 in Ubuntu noble. Build
220
+ the pex for that version, since its interpreter constraints and wheels must match. An image built
221
+ from packages has no `env` unless it locks `coreutils`, and a pex's default shebang is
222
+ `#!/usr/bin/env python3.X`, so the pex fails to start. Point the shebang at the interpreter, and
223
+ make the pex the entrypoint with `oci_image_build`, whose first package is the entrypoint:
224
+
225
+ ``` python
226
+ pex_binary(
227
+ name="app",
228
+ entry_point="app.py:main",
229
+ shebang="/usr/bin/python3.11",
230
+ )
231
+
232
+ oci_image_build(
233
+ name="image",
234
+ base=[...],
235
+ layers=[":python"],
236
+ packages=[":app"],
237
+ )
238
+ ```
239
+
240
+ `oci_python_image` doesn't use the shebang: it runs the pex as `python <pex>`, and no Debian or
241
+ Ubuntu package but `python-is-python3` provides `/usr/bin/python`. Lock that package to keep using
242
+ `oci_python_image`. `python3-minimal`, which `python3` pre-depends on, adds `/usr/bin/python3`.
243
+
244
+ **pex warns that `ldd` is missing.** On startup a pex may print `Failed to detect if this
245
+ interpreter is running under musl libc: [Errno 2] No such file or directory: 'ldd'`. pex runs `ldd`
246
+ to tell musl from glibc and, when it can't, assumes glibc, which is right for Debian and Ubuntu.
247
+ The warning is harmless. To hide it, set `emit_warnings=False` on the `pex_binary`, or
248
+ `PEX_EMIT_WARNINGS=false` in the image's environment; either hides all of pex's warnings. Locking
249
+ `libc-bin` doesn't help in an image without a shell, since Debian's `ldd` is a bash script.
250
+
251
+ **The Pants store grows quickly.** Rebuilding multi-GB images adds new layers to the Pants store on
252
+ every change. See
253
+ [Pants store size](https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/oci#pants-store-size)
254
+ in the OCI README for a setting that keeps it in check.
@@ -0,0 +1,218 @@
1
+ # Distroless backend for Pants
2
+
3
+ This release targets Pants **2.32.x**. Install it with
4
+ `pants-backend-distroless==0.1.0rc0` in `[GLOBAL].plugins` in `pants.toml`, and enable
5
+ `pants_backend_distroless` in `[GLOBAL].backend_packages`.
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/pants-backend-distroless?label=Latest%20release)](https://pypi.org/project/pants-backend-distroless)
8
+
9
+ > **Warning**
10
+ > This plugin is in development. No stability is guaranteed! Contributions welcome.
11
+
12
+ This backend builds the parts of distroless-style images that come from Debian or Ubuntu packages, in the
13
+ spirit of [rules_distroless](https://github.com/GoogleContainerTools/rules_distroless): declare the
14
+ top-level packages against an apt repository snapshot, and `pants generate-lockfiles` resolves their
15
+ dependencies into a committed lockfile. The locked packages feed `oci_deb_layer` from
16
+ [`pants-backend-oci`](https://pypi.org/project/pants-backend-oci), which builds a layer from them
17
+ without running `apt`, `dpkg` or maintainer scripts. `pants-backend-oci` also has the helper layers such
18
+ an image needs: `oci_passwd_layer`, `oci_group_layer`, `oci_cacerts_layer` and `oci_locale_layer`.
19
+
20
+ ## Targets
21
+
22
+ ### `deb_packages`
23
+
24
+ A set of Debian packages resolved into a lockfile. Each resolve is named in `[apt].resolves`, which
25
+ maps it to its lockfile, and is declared by exactly one `deb_packages` target.
26
+
27
+ ``` toml
28
+ [apt.resolves]
29
+ bookworm = "3rdparty/debian/bookworm.lock.json"
30
+ ```
31
+
32
+ ``` python
33
+ deb_packages(
34
+ name="bookworm",
35
+ resolve="bookworm",
36
+ sources=["https://snapshot.debian.org/archive/debian/20241001T000000Z bookworm main"],
37
+ architectures=["amd64", "arm64"],
38
+ packages=["jq"],
39
+ # Already in the base image.
40
+ exclude=["libc6"],
41
+ # From the `debian-archive-keyring` package.
42
+ keyrings=["debian-archive-keyring.gpg"],
43
+ )
44
+
45
+ oci_deb_layer(
46
+ name="jq",
47
+ debs=[":bookworm#amd64/jq"],
48
+ )
49
+ ```
50
+
51
+ Run `pants generate-lockfiles --resolve=bookworm` to write the lockfile, and again whenever the
52
+ target changes. Until then, and while the lockfile is out of date, Pants warns and generates no
53
+ packages, so anything using them fails with an error that `:bookworm#amd64/jq` was not generated.
54
+ The warning is only printed when the targets are generated, so with `pantsd` a later run may show
55
+ just that error; regenerating the lockfile fixes it.
56
+
57
+ The target generates:
58
+
59
+ * a `deb_package` target named `<arch>/<package>` for every locked package, which depends on the
60
+ pinned `.deb` of the package and of every locked package in its closure, so `:bookworm#amd64/jq`
61
+ brings `jq` and its whole closure;
62
+ * a `deb_package_file` target named `<arch>/<package>/deb` for every locked package, which downloads
63
+ only its pinned `.deb`;
64
+ * a `deb_package_group` target named `<arch>`, depending on all the requested packages, so
65
+ `:bookworm#amd64` is everything.
66
+
67
+ Packages don't depend on each other's targets, only on `.deb` files, and the lockfile records each
68
+ package's closure. Apt dependency cycles are therefore fine: `libc6` and `libgcc-s1` depend on each
69
+ other in Debian, and `:bookworm#amd64/libc6` brings both.
70
+
71
+ A layer is for one architecture, so always pick one: `debs=[":bookworm"]` would bring the packages of
72
+ every architecture, which `oci_deb_layer` rejects.
73
+
74
+ | Argument | Meaning | Default value |
75
+ |------------------|-----------------------------------------------------------------------------------------------|-------------------------------------------------------|
76
+ | `name` | The target name | Same as any other target, which is the directory name |
77
+ | `resolve` | The resolve name, a key of `[apt].resolves`. | Required |
78
+ | `sources` | The repositories, each `<uri> <suite> <component>...` like a sources.list line without `deb`. | Required |
79
+ | `architectures` | The Debian architectures to lock, e.g. `amd64` and `arm64`. | Required |
80
+ | `packages` | The top-level packages. | Required |
81
+ | `exclude` | Packages treated as already installed, typically those the base image ships. | `[]` |
82
+ | `keyrings` | OpenPGP keyrings (`.gpg` or `.asc`) to verify the repositories' `Release` signatures with. | Required unless `allow_unsigned=True` |
83
+ | `allow_unsigned` | Resolve without verifying signatures. Can't be combined with `keyrings`. | `False` |
84
+ | `description` | A description of the target | |
85
+ | `tags` | List of tags | `[]` |
86
+
87
+ #### Sources
88
+
89
+ Pin each URI to a snapshot, such as `https://snapshot.debian.org/archive/debian/<timestamp>`, so
90
+ the lockfile can be regenerated and the packages stay downloadable. Several sources can be combined,
91
+ for example to add `bookworm-updates`, or `bookworm-security` from
92
+ `https://snapshot.debian.org/archive/debian-security/<timestamp>`. Every `Packages` index is checked
93
+ against the SHA256 in its suite's signed `Release` file, see [Signature verification](#signature-verification).
94
+
95
+ Ubuntu works the same way, from [snapshot.ubuntu.com](https://snapshot.ubuntu.com). Its updates
96
+ and security fixes are separate suites of the same archive, and unlike the live archive, where
97
+ arm64 lives on `ports.ubuntu.com`, the snapshot serves arm64 packages under `/ubuntu` too, so one
98
+ set of sources covers both architectures. `Phased-Update-Percentage` is ignored: a snapshot lock
99
+ takes a phased update like any other version. Ubuntu's packages are zstd-compressed, which
100
+ `oci_deb_layer` reads with its `zstd` tool.
101
+
102
+ ``` python
103
+ deb_packages(
104
+ name="noble",
105
+ resolve="noble",
106
+ sources=[
107
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble main",
108
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble-updates main",
109
+ "https://snapshot.ubuntu.com/ubuntu/20241001T000000Z noble-security main",
110
+ ],
111
+ architectures=["amd64", "arm64"],
112
+ packages=["jq", "ca-certificates"],
113
+ # Already in the ubuntu:noble base image.
114
+ exclude=["libc6", "debconf", "libssl3t64"],
115
+ # From the `ubuntu-keyring` package.
116
+ keyrings=["ubuntu-archive-keyring.gpg"],
117
+ )
118
+ ```
119
+
120
+ #### Signature verification
121
+
122
+ Like rules_distroless's `gpg_keys`, `generate-lockfiles` verifies every suite's `InRelease`, or its
123
+ `Release` and `Release.gpg` when there's no `InRelease`, against `keyrings`, and fails unless at
124
+ least one signature is good and made by a key in them. A bad signature from one of their keys fails
125
+ too. The hashes of the `Packages` indices are only taken from the verified file. It also fails
126
+ unless the signed file's `Suite` or `Codename` is the suite asked for, so another suite's validly
127
+ signed `Release` can't stand in for it. This is apt's rule, though apt only warns: a path like
128
+ debian-security's old `buster/updates` matches by what comes before its last `/`. Either `keyrings` or `allow_unsigned=True` must be set.
129
+
130
+ Use the archive's own keyring, from the snapshot you resolve against so it's pinned too:
131
+ `/usr/share/keyrings/debian-archive-keyring.gpg` from the `debian-archive-keyring` package, or
132
+ `/usr/share/keyrings/ubuntu-archive-keyring.gpg` from `ubuntu-keyring`. Copy it next to the
133
+ `deb_packages` target, since sources can't come from another directory.
134
+
135
+ Verification needs no `gpgv` or `sqv`: it's built into the resolver, and supports what Debian and
136
+ Ubuntu sign with, v4 RSA and Ed25519 keys with SHA-2 hashes. Older snapshots signed only with SHA-1
137
+ need `allow_unsigned=True`, which turns signature verification off entirely; the suite is still
138
+ checked. Every key in the keyrings is trusted as given: expiry and revocation aren't checked, since a
139
+ snapshot's `Release` files are historical. For the same reason, freshness (`Valid-Until`) isn't
140
+ checked either: the snapshot URL is what pins it. The keys aren't recorded in the lockfile, so
141
+ changing `keyrings` doesn't make it out of date.
142
+
143
+ #### Resolution
144
+
145
+ The `Pre-Depends` and `Depends` of the requested packages are resolved transitively, for each
146
+ architecture separately, much like rules_distroless does:
147
+
148
+ * A package resolves to its highest version, across all sources, that satisfies the constraint.
149
+ When sources have the same version, the first source wins.
150
+ * For alternatives (`a | b`), a package already in the closure is preferred; otherwise the first
151
+ alternative that exists wins.
152
+ * A virtual package (`Provides`) resolves to a provider already in the closure, else the only
153
+ provider, else a `Priority: required` one, else the highest version, else the first by name.
154
+ * The result doesn't depend on the order of `packages`.
155
+
156
+ `Recommends` and `Suggests` are not installed, and `Conflicts` and `Breaks` are not checked.
157
+
158
+ The resolver runs in a sandboxed process from `apt-resolver`, a [scie](https://github.com/a-scie/jump)
159
+ the plugin builds with pex from its own resolver code and a pinned python-build-standalone Python,
160
+ CPython `3.11.17` from release `20261009` (set `[apt].resolver_python_version` and
161
+ `[apt].resolver_pbs_release` to use another). Since it names nothing on the host, a resolve's
162
+ cached result can be shared across checkouts, and across machines through a remote cache. It is built once per machine, downloading
163
+ that Python and pex's `science` tool from GitHub the first time, and unpacks into the
164
+ `apt_resolver` named cache. The Python and `science` are checked only against the checksum files
165
+ published in the same GitHub releases, through pex.
166
+
167
+ #### Packages already in the base image
168
+
169
+ A layer replaces any file the base image has at the same path, and nothing warns about it. If a
170
+ locked package is also in the base, its files from the snapshot overwrite the base's copies, which
171
+ can mix versions when the base image was built from a different archive state than the snapshot.
172
+ List such packages in `exclude`: they count as installed, satisfy any dependency on them, and are not
173
+ traversed, so whatever only they need is left out as well. Images built on an empty base leave
174
+ `exclude` empty.
175
+
176
+ ## Moving from a Python base image
177
+
178
+ A few things change when a Python app moves from a pulled base such as `python:3.11-slim` to one
179
+ built from Debian or Ubuntu packages.
180
+
181
+ **The interpreter is `/usr/bin/python3.X`.** The official Python images install it under
182
+ `/usr/local/bin` and have `/usr/bin/env`. A package's interpreter is `/usr/bin/python3.X`, where
183
+ `3.X` is the suite's Python: 3.11 in Debian bookworm, 3.13 in trixie, 3.12 in Ubuntu noble. Build
184
+ the pex for that version, since its interpreter constraints and wheels must match. An image built
185
+ from packages has no `env` unless it locks `coreutils`, and a pex's default shebang is
186
+ `#!/usr/bin/env python3.X`, so the pex fails to start. Point the shebang at the interpreter, and
187
+ make the pex the entrypoint with `oci_image_build`, whose first package is the entrypoint:
188
+
189
+ ``` python
190
+ pex_binary(
191
+ name="app",
192
+ entry_point="app.py:main",
193
+ shebang="/usr/bin/python3.11",
194
+ )
195
+
196
+ oci_image_build(
197
+ name="image",
198
+ base=[...],
199
+ layers=[":python"],
200
+ packages=[":app"],
201
+ )
202
+ ```
203
+
204
+ `oci_python_image` doesn't use the shebang: it runs the pex as `python <pex>`, and no Debian or
205
+ Ubuntu package but `python-is-python3` provides `/usr/bin/python`. Lock that package to keep using
206
+ `oci_python_image`. `python3-minimal`, which `python3` pre-depends on, adds `/usr/bin/python3`.
207
+
208
+ **pex warns that `ldd` is missing.** On startup a pex may print `Failed to detect if this
209
+ interpreter is running under musl libc: [Errno 2] No such file or directory: 'ldd'`. pex runs `ldd`
210
+ to tell musl from glibc and, when it can't, assumes glibc, which is right for Debian and Ubuntu.
211
+ The warning is harmless. To hide it, set `emit_warnings=False` on the `pex_binary`, or
212
+ `PEX_EMIT_WARNINGS=false` in the image's environment; either hides all of pex's warnings. Locking
213
+ `libc-bin` doesn't help in an image without a shell, since Debian's `ldd` is a bash script.
214
+
215
+ **The Pants store grows quickly.** Rebuilding multi-GB images adds new layers to the Pants store on
216
+ every change. See
217
+ [Pants store size](https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/oci#pants-store-size)
218
+ in the OCI README for a setting that keeps it in check.