1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{Amd64Variant, StringOrBool, deserialize_string_or_bool_opt};
/// PyPI publisher configuration.
///
/// Publishes the project's prebuilt binaries as native Python wheels — one
/// `py3-none-<platform>` wheel per built target, with the platform tag
/// derived by inspecting each binary (glibc floor for `manylinux`, Mach-O
/// deployment target for `macosx`) — and uploads them via PyPI's legacy
/// (twine-protocol) upload API. Optionally also builds and uploads a source
/// distribution via `maturin sdist`. Each `pypis[]` entry produces one
/// publish.
///
/// ```yaml
/// pypis:
/// - name: my-tool
/// requires_python: ">=3.7"
/// sdist: true
/// sdist_manifest: "pypi/"
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct PypiConfig {
/// Unique identifier for selecting this entry from the CLI (`--id=...`).
pub id: Option<String>,
/// Build IDs filter: only include binaries whose crate is in this list.
pub ids: Option<Vec<String>>,
/// Target-triple allowlist: restrict the wheels to a subset of the built
/// targets. When unset (the default), every built target becomes a wheel.
/// When set, only binaries whose target triple appears in this list are
/// built into wheels; the rest are silently skipped. Orthogonal to `ids:`:
/// both filters apply (a binary must pass the `ids` filter AND, when this
/// is set, be listed here). A listed triple that no selected build
/// produces is a config error, and an explicit empty list (`targets: []`)
/// is rejected — omit the field to publish every built target. A common use
/// is excluding `x86_64-pc-windows-gnu` so it does not collide with the
/// `x86_64-pc-windows-msvc` wheel on the shared `win_amd64` platform tag.
/// Example: `targets: [x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc]`.
pub targets: Option<Vec<String>>,
/// PyPI project name. May use any PEP 508 name form (`My.Tool`,
/// `my_tool`); PyPI normalizes it per PEP 503 for index lookups and the
/// wheel filename escapes it per PEP 427. Falls back to the crate name
/// when unset.
pub name: Option<String>,
/// Also build and upload a source distribution via `maturin sdist`.
/// Default `false`. Requires `sdist_manifest` to point at the directory
/// containing the project's `pyproject.toml`, and `maturin` on `PATH`.
///
/// ```yaml
/// pypis:
/// - sdist: true
/// sdist_manifest: "pypi/"
/// ```
pub sdist: bool,
/// Templated directory containing the `pyproject.toml` that `maturin
/// sdist` builds from, relative to the project root (e.g. `"pypi/"`).
/// Required when `sdist: true`; unused otherwise.
pub sdist_manifest: Option<String>,
/// Templated twine upload endpoint URL. Default
/// `https://upload.pypi.org/legacy/` (the production PyPI upload API).
/// Point it at TestPyPI to rehearse a release:
///
/// ```yaml
/// pypis:
/// - index_url: "https://test.pypi.org/legacy/"
/// ```
///
/// This is the twine *upload* target, not a `{owner, name}` source
/// repository — the name `index_url` keeps it distinct from the reserved
/// `repository` meaning every git-based publisher uses. The legacy
/// `repository:` spelling is still accepted via serde alias.
#[serde(alias = "repository")]
pub index_url: Option<String>,
/// Tolerate the index rejecting a file that already exists (the
/// twine `--skip-existing` semantics). Default `true` so a re-run of an
/// already-published tag skips previously-uploaded files instead of
/// failing the release. Set to `false` to make a duplicate upload a hard
/// error.
pub skip_existing: bool,
/// `Requires-Python` version specifier written into each wheel's
/// METADATA (e.g. `">=3.7"`). Purely declarative for a binary wheel —
/// the shipped executable does not import Python — but pip honors it
/// during resolution. Omitted when unset.
pub requires_python: Option<String>,
/// Templated one-line `Summary` for the package METADATA. Falls back to
/// the project-level `metadata.description` (and then the crate's
/// `Cargo.toml [package].description`) when unset.
pub summary: Option<String>,
/// Templated long description written as the METADATA body (rendered on
/// the PyPI project page). Falls back to the summary when unset.
pub description: Option<String>,
/// `Description-Content-Type` for the long-description body — how PyPI
/// renders it (`text/markdown`, `text/x-rst`, `text/plain`). When a
/// `description` is present and this is unset, defaults to
/// `text/markdown` (the modern norm); without the header PyPI renders the
/// body as raw plaintext. Omitted entirely when there is no description.
pub description_content_type: Option<String>,
/// Package author, emitted as the METADATA `Author` header.
pub author: Option<String>,
/// Package author email, emitted as the METADATA `Author-email` header.
pub author_email: Option<String>,
/// Arbitrary `Project-URL` label → URL map, one
/// `Project-URL: <label>, <url>` METADATA header each (the PyPI sidebar
/// links). Emitted in addition to the `Homepage` link derived from
/// `homepage`; use this for `Repository`, `Documentation`, `Changelog`,
/// `Funding`, etc. Rendered in sorted label order for a byte-stable wheel.
///
/// ```yaml
/// pypis:
/// - project_urls:
/// Repository: "https://github.com/me/my-tool"
/// Documentation: "https://docs.example.com"
/// ```
pub project_urls: Option<BTreeMap<String, String>>,
/// Templated homepage URL, emitted as `Project-URL: Homepage`. Falls
/// back to `metadata.homepage` (then `Cargo.toml [package].homepage`)
/// when unset.
pub homepage: Option<String>,
/// Templated license expression (e.g. `MIT`, `Apache-2.0`), emitted as
/// the METADATA `License` field. Falls back to `metadata.license` (then
/// `Cargo.toml [package].license`) when unset.
pub license: Option<String>,
/// Keywords list, emitted comma-separated in METADATA.
pub keywords: Option<Vec<String>>,
/// Trove classifier lines (e.g.
/// `"Programming Language :: Rust"`), one `Classifier:` METADATA header
/// each.
pub classifiers: Option<Vec<String>>,
/// Per-target-triple wheel platform-tag overrides: `<target triple>` →
/// explicit wheel platform tag. When a built target has an entry, its tag
/// is used *verbatim* — binary inspection (the glibc floor for
/// `manylinux`, the Mach-O deployment target for `macosx`) is skipped for
/// that target. Every target without an entry keeps the auto-detected tag.
///
/// The escape hatch for toolchains whose emitted glibc floor is stricter
/// than the compatibility a project wants to advertise — e.g. pinning
/// `aarch64-unknown-linux-gnu` to `manylinux_2_28` to match a
/// `maturin`/PyO3 build environment rather than shipping the higher floor
/// the binary's symbols imply.
///
/// ```yaml
/// pypis:
/// - platform_tag_overrides:
/// aarch64-unknown-linux-gnu: manylinux_2_28_aarch64
/// ```
pub platform_tag_overrides: Option<BTreeMap<String, String>>,
/// `x86_64` micro-architecture variant selector — `v1` (baseline), `v2`,
/// `v3` (AVX2), or `v4`. When set, an amd64 binary carrying
/// `amd64_variant` metadata becomes the `win_amd64`/`manylinux…x86_64`
/// wheel only when its variant matches; a binary with no variant metadata
/// still matches (the baseline build). Default: `v1`. Typed as
/// [`Amd64Variant`], so any value outside `v1`..`v4` is rejected at parse
/// time.
pub amd64_variant: Option<Amd64Variant>,
/// ARM version selector (e.g. `"6"`, `"7"`). When set, a 32-bit ARM binary
/// carrying `arm_variant` metadata becomes the wheel only when its variant
/// matches; a binary with no variant metadata still matches. Does not
/// affect `aarch64`/`arm64` (64-bit ARM has no sub-variant).
pub arm_variant: Option<String>,
/// Whether the upload authenticates with a long-lived API token or with
/// GitHub Actions OIDC (PyPI Trusted Publishing). Default [`Auto`]:
/// a token when one is available, otherwise a Trusted-Publishing exchange
/// when an OIDC context is present.
///
/// [`Auto`]: PypiAuthMode::Auto
pub auth: PypiAuthMode,
/// API token for the upload (templated). Falls back to the `PYPI_TOKEN`
/// env var, then `MATURIN_PYPI_TOKEN`, when unset. Sent as HTTP Basic
/// auth with the literal username `__token__` and NEVER logged. Unused
/// when `auth: oidc` (Trusted Publishing mints its own short-lived token).
pub token: Option<String>,
/// Skip this publisher. Accepts bool or template string.
/// Accepts the legacy `disable:` spelling via serde alias for back-compat.
#[serde(
default,
alias = "disable",
deserialize_with = "deserialize_string_or_bool_opt"
)]
pub skip: Option<StringOrBool>,
/// Override whether this publisher failing should fail the overall release.
///
/// Default: `true` — PyPI is a Manager-group publisher whose uploads are
/// one-way (a published filename can never be re-uploaded, even after
/// deletion), so a failed publish aborts by default to avoid surprising
/// the operator with a half-released version. Set to `false` to log
/// failures but continue.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
/// Template-conditional gate: when the rendered result is falsy
/// (`"false"` / `"0"` / `"no"` / empty), the PyPI publisher entry is
/// skipped. Render failure hard-errors.
#[serde(rename = "if")]
pub if_condition: Option<String>,
/// When `true`, a triggered rollback leaves this publisher's work in
/// place rather than attempting to undo it. Default `false`. (PyPI has
/// no programmatic delete path anyway — rollback is warn-only — but the
/// flag suppresses even that warning.)
pub retain_on_rollback: Option<bool>,
}
impl Default for PypiConfig {
fn default() -> Self {
Self {
id: None,
ids: None,
targets: None,
name: None,
sdist: false,
sdist_manifest: None,
index_url: None,
skip_existing: true,
requires_python: None,
summary: None,
description: None,
description_content_type: None,
author: None,
author_email: None,
project_urls: None,
homepage: None,
license: None,
keywords: None,
classifiers: None,
platform_tag_overrides: None,
amd64_variant: None,
arm_variant: None,
auth: PypiAuthMode::default(),
token: None,
skip: None,
required: None,
if_condition: None,
retain_on_rollback: None,
}
}
}
/// How a `pypis[]` entry authenticates its upload: a long-lived API token, or
/// GitHub Actions OIDC (PyPI Trusted Publishing, which mints a short-lived
/// upload token per run — no stored secret).
///
/// Unlike npm, PyPI is uploaded directly over HTTP rather than through a CLI,
/// so the Trusted-Publishing exchange (Actions id-token → PyPI mint-token) is
/// performed by anodizer itself. Trusted Publishing also creates brand-new
/// projects when a *pending* publisher is configured on PyPI, so there is no
/// per-package "must already exist" caveat as there is for npm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum PypiAuthMode {
/// Use a token when one is available (`cfg.token` / `PYPI_TOKEN` /
/// `MATURIN_PYPI_TOKEN`); otherwise, when an OIDC context is present, mint
/// a Trusted-Publishing token. Errors only when neither is available.
#[default]
Auto,
/// Always authenticate with the token; never attempt OIDC. Errors if no
/// token is available. This is anodizer's historical behaviour.
Token,
/// Always authenticate with OIDC (Trusted Publishing); never fall back to
/// the token. Errors if the GitHub Actions OIDC request env
/// (`ACTIONS_ID_TOKEN_REQUEST_URL` / `_TOKEN`) is absent, so a misconfigured
/// Trusted Publisher fails the release loudly instead of silently falling
/// back to a token.
Oidc,
}