cibuildmp 0.3.0a1__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.
- cibuildmp/__init__.py +19 -0
- cibuildmp/__main__.py +4 -0
- cibuildmp/cli.py +322 -0
- cibuildmp/options.py +213 -0
- cibuildmp/resources/natmod.toml +140 -0
- cibuildmp/resources.py +22 -0
- cibuildmp/sources.py +275 -0
- cibuildmp/targets.py +153 -0
- cibuildmp/toolchains.py +293 -0
- cibuildmp-0.3.0a1.dist-info/METADATA +477 -0
- cibuildmp-0.3.0a1.dist-info/RECORD +15 -0
- cibuildmp-0.3.0a1.dist-info/WHEEL +5 -0
- cibuildmp-0.3.0a1.dist-info/entry_points.txt +2 -0
- cibuildmp-0.3.0a1.dist-info/licenses/LICENSE +21 -0
- cibuildmp-0.3.0a1.dist-info/top_level.txt +1 -0
cibuildmp/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""cibuildmp -- build MicroPython native C extensions on CI, and locally."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("cibuildmp")
|
|
10
|
+
except PackageNotFoundError: # running from a source tree, not installed
|
|
11
|
+
__version__ = "0.0.0.dev0"
|
|
12
|
+
|
|
13
|
+
from .cli import main as _main
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__", "main"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
sys.exit(_main())
|
cibuildmp/__main__.py
ADDED
cibuildmp/cli.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .options import BuildOptions, ConfigError, Options
|
|
14
|
+
from .sources import (
|
|
15
|
+
SourceError,
|
|
16
|
+
build_mpy_cross,
|
|
17
|
+
cache_root,
|
|
18
|
+
fetch_micropython,
|
|
19
|
+
read_mpy_abi,
|
|
20
|
+
)
|
|
21
|
+
from .targets import NATMOD_ARCHS, Target, UnknownArchError, is_abi_known
|
|
22
|
+
from .toolchains import ResolvedToolchain, ToolchainError, resolve
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
prog="cibuildmp",
|
|
28
|
+
description="Build MicroPython native C extensions across every target "
|
|
29
|
+
"a module supports, from one declarative config.",
|
|
30
|
+
epilog="Most options are supplied via cibuildmp.toml or CIBMP_* environment "
|
|
31
|
+
"variables. See docs/BACKLOG.md for the design and what is implemented.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"package_dir",
|
|
35
|
+
nargs="?",
|
|
36
|
+
default=".",
|
|
37
|
+
type=Path,
|
|
38
|
+
help='Directory containing the module and its config (default: ".")',
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--version",
|
|
42
|
+
action="version",
|
|
43
|
+
version=f"cibuildmp {__version__}",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--config-file",
|
|
47
|
+
type=Path,
|
|
48
|
+
default=None,
|
|
49
|
+
help="Config file to use instead of <package_dir>/cibuildmp.toml",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--output-dir",
|
|
53
|
+
type=Path,
|
|
54
|
+
default=None,
|
|
55
|
+
help="Where to collect built .mpy files (overrides the config)",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--only",
|
|
59
|
+
default=None,
|
|
60
|
+
metavar="IDENTIFIER",
|
|
61
|
+
help="Build exactly this one identifier, overriding the config's own "
|
|
62
|
+
"build/skip selectors. Opt into a job-per-target CI layout with this; "
|
|
63
|
+
"the default is one invocation building every selected target.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--archs",
|
|
67
|
+
default=None,
|
|
68
|
+
help="Comma-separated list of architectures to build, or 'all'. Overrides "
|
|
69
|
+
"the config's own archs. There is no 'auto': every natmod arch is a "
|
|
70
|
+
"cross-compile, so none of them depends on what this machine is.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--toolchain",
|
|
74
|
+
default="auto",
|
|
75
|
+
choices=["auto", "host", "download"],
|
|
76
|
+
help="How to obtain each target's cross toolchain. auto (default) uses "
|
|
77
|
+
"one already on PATH and downloads a pinned tarball otherwise; host "
|
|
78
|
+
"refuses to download; download ignores what is on PATH.",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--clean-cache",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Delete cibuildmp's cache (MicroPython checkouts, mpy-cross builds "
|
|
84
|
+
"and downloaded toolchains) and exit",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--dry-run",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Print the resolved build plan and exit without building",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--print-build-identifiers",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Print the identifiers this config selects, one per line, then exit",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--print-build-matrix",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Print a JSON array of {only, os} objects, then exit. Feed it to a "
|
|
100
|
+
"GitHub Actions `strategy.matrix.include` via fromJSON() to get one job "
|
|
101
|
+
"per target. Only worth it when targets need different runners.",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--json",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="With --print-build-identifiers, emit a JSON array instead of lines",
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--allow-empty",
|
|
110
|
+
action="store_true",
|
|
111
|
+
help="Do not report an error code if no target is selected",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--debug-traceback",
|
|
115
|
+
action="store_true",
|
|
116
|
+
default=os.environ.get("CIBMP_DEBUG_TRACEBACK", "") not in {"", "0"},
|
|
117
|
+
help="Print a full traceback for all errors",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--platform",
|
|
121
|
+
default="natmod",
|
|
122
|
+
choices=["natmod"],
|
|
123
|
+
help="Build mode. Only natmod is implemented; usermod is planned "
|
|
124
|
+
"(see docs/BACKLOG.md).",
|
|
125
|
+
)
|
|
126
|
+
return parser
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _plan_line(
|
|
130
|
+
index: int,
|
|
131
|
+
total: int,
|
|
132
|
+
options: BuildOptions,
|
|
133
|
+
chain: ResolvedToolchain | None = None,
|
|
134
|
+
) -> str:
|
|
135
|
+
make = ["make", "-C", options.module_dir, f"ARCH={options.target.arch}"]
|
|
136
|
+
make += options.extra_make_args
|
|
137
|
+
make.append(options.make_target)
|
|
138
|
+
# The prefix actually in play, which is not always the one dynruntime.mk
|
|
139
|
+
# hardcodes -- showing target.cross here would contradict the CROSS=
|
|
140
|
+
# override sitting in the same line's make command.
|
|
141
|
+
prefix = chain.prefix if chain is not None else options.target.cross
|
|
142
|
+
# Right-align the counter so the columns after it stay put once the
|
|
143
|
+
# index gains a digit ([10/10] is wider than [9/10]).
|
|
144
|
+
counter = f"[{index:>{len(str(total))}}/{total}]"
|
|
145
|
+
return (
|
|
146
|
+
f"{counter} {options.target.identifier:<28} "
|
|
147
|
+
f"CROSS={prefix or '(host)':<22} {' '.join(make)}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build(options: Options, targets: list[Target], *, toolchain: str = "auto") -> int:
|
|
152
|
+
"""Build every selected target in one invocation.
|
|
153
|
+
|
|
154
|
+
Sequential and in-process on purpose (D9), the same shape cibuildwheel
|
|
155
|
+
uses for the Python versions inside one runner. Fetching MicroPython and
|
|
156
|
+
building mpy-cross are identical for every natmod arch, so doing them
|
|
157
|
+
once here is strictly cheaper than paying for them in each of ten matrix
|
|
158
|
+
legs -- and unlike cibuildwheel, no natmod target needs a runner any
|
|
159
|
+
other target cannot use, so nothing forces a fan-out. Callers who want
|
|
160
|
+
one anyway (failure isolation, wall-clock) opt in with --only.
|
|
161
|
+
"""
|
|
162
|
+
resolved = [options.build_options(t) for t in targets]
|
|
163
|
+
total = len(resolved)
|
|
164
|
+
|
|
165
|
+
# Toolchains are resolved before the plan is printed, not after: where a
|
|
166
|
+
# toolchain's prefix is not the one dynruntime.mk hardcodes, resolution
|
|
167
|
+
# adds a CROSS= override, and a plan printed first would show a make
|
|
168
|
+
# command that is not the one about to run.
|
|
169
|
+
print(f"cibuildmp: resolving toolchains for {total} target(s)")
|
|
170
|
+
chains = []
|
|
171
|
+
for build_options in resolved:
|
|
172
|
+
chain = resolve(build_options.target.arch, strategy=toolchain)
|
|
173
|
+
chains.append(chain)
|
|
174
|
+
build_options.extra_make_args = [
|
|
175
|
+
*chain.make_overrides,
|
|
176
|
+
*build_options.extra_make_args,
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
print(
|
|
180
|
+
f"\ncibuildmp: {total} target(s) against MicroPython {options.micropython} "
|
|
181
|
+
f"(.mpy ABI {options.abi})"
|
|
182
|
+
)
|
|
183
|
+
for index, (build_options, chain) in enumerate(
|
|
184
|
+
zip(resolved, chains, strict=True), 1
|
|
185
|
+
):
|
|
186
|
+
print(" " + _plan_line(index, total, build_options, chain))
|
|
187
|
+
print(f" {chain.describe()}")
|
|
188
|
+
|
|
189
|
+
# Shared setup, paid once for the whole invocation rather than once per
|
|
190
|
+
# matrix leg -- see build()'s own docstring and D9.
|
|
191
|
+
print("\ncibuildmp: preparing MicroPython")
|
|
192
|
+
mpy_dir = fetch_micropython(
|
|
193
|
+
options.micropython, submodules=options.micropython_submodules
|
|
194
|
+
)
|
|
195
|
+
build_mpy_cross(mpy_dir)
|
|
196
|
+
|
|
197
|
+
# The checkout is authoritative about the ABI; targets.MPY_ABI's table
|
|
198
|
+
# is only a way to answer the question without one. A disagreement means
|
|
199
|
+
# the identifiers already printed are wrong, so it stops here rather
|
|
200
|
+
# than producing files labelled with an ABI they do not have.
|
|
201
|
+
actual_abi = read_mpy_abi(mpy_dir)
|
|
202
|
+
if actual_abi != options.abi:
|
|
203
|
+
raise SourceError(
|
|
204
|
+
f"MicroPython {options.micropython} has .mpy ABI {actual_abi}, but the "
|
|
205
|
+
f"identifiers were built assuming {options.abi}. Set `mpy-abi = "
|
|
206
|
+
f'"{actual_abi}"` in the config, or report the stale entry in '
|
|
207
|
+
f"cibuildmp.targets.MPY_ABI."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# M3 lands here: run make under that toolchain, then collect and verify
|
|
211
|
+
# the output.
|
|
212
|
+
sys.stdout.flush() # keep the output above ahead of the stderr note below
|
|
213
|
+
print(
|
|
214
|
+
"\ncibuildmp: the per-target build is not implemented yet (M2 ships "
|
|
215
|
+
"toolchain resolution). Re-run with --dry-run to get the plan as a "
|
|
216
|
+
"success.",
|
|
217
|
+
file=sys.stderr,
|
|
218
|
+
)
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main(argv: list[str] | None = None) -> int:
|
|
223
|
+
parser = build_parser()
|
|
224
|
+
args = parser.parse_args(argv)
|
|
225
|
+
|
|
226
|
+
if args.clean_cache:
|
|
227
|
+
root = cache_root()
|
|
228
|
+
if not root.exists():
|
|
229
|
+
print(f"cibuildmp: nothing to clean ({root} does not exist)")
|
|
230
|
+
return 0
|
|
231
|
+
shutil.rmtree(root)
|
|
232
|
+
print(f"cibuildmp: removed {root}")
|
|
233
|
+
return 0
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
options = Options.load(args.package_dir, args.config_file)
|
|
237
|
+
if args.output_dir is not None:
|
|
238
|
+
options.output_dir = args.output_dir
|
|
239
|
+
if args.archs is not None:
|
|
240
|
+
options.archs = (
|
|
241
|
+
list(NATMOD_ARCHS)
|
|
242
|
+
if args.archs.strip() == "all"
|
|
243
|
+
else [a.strip() for a in args.archs.split(",") if a.strip()]
|
|
244
|
+
)
|
|
245
|
+
targets = options.targets()
|
|
246
|
+
except (ConfigError, UnknownArchError, SourceError) as exc:
|
|
247
|
+
if args.debug_traceback:
|
|
248
|
+
raise
|
|
249
|
+
print(f"cibuildmp: error: {exc}", file=sys.stderr)
|
|
250
|
+
return 2
|
|
251
|
+
|
|
252
|
+
if args.only is not None:
|
|
253
|
+
# --only overrides build/skip, matching cibuildwheel's own semantics
|
|
254
|
+
# for the flag: the caller has already decided what this invocation
|
|
255
|
+
# is for, and a matrix leg that reached here was selected when the
|
|
256
|
+
# matrix was generated.
|
|
257
|
+
targets = [t for t in targets if t.identifier == args.only]
|
|
258
|
+
if not targets:
|
|
259
|
+
print(
|
|
260
|
+
f"cibuildmp: error: --only {args.only!r} matches no target this "
|
|
261
|
+
f"config can produce",
|
|
262
|
+
file=sys.stderr,
|
|
263
|
+
)
|
|
264
|
+
return 2
|
|
265
|
+
|
|
266
|
+
if args.print_build_matrix:
|
|
267
|
+
print(
|
|
268
|
+
json.dumps(
|
|
269
|
+
[
|
|
270
|
+
{"only": bo.target.identifier, "os": bo.runs_on}
|
|
271
|
+
for bo in (options.build_options(t) for t in targets)
|
|
272
|
+
]
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
if args.print_build_identifiers:
|
|
278
|
+
identifiers = [t.identifier for t in targets]
|
|
279
|
+
if args.json:
|
|
280
|
+
print(json.dumps(identifiers))
|
|
281
|
+
else:
|
|
282
|
+
for identifier in identifiers:
|
|
283
|
+
print(identifier)
|
|
284
|
+
return 0
|
|
285
|
+
|
|
286
|
+
if not targets:
|
|
287
|
+
if args.allow_empty:
|
|
288
|
+
print("cibuildmp: no targets selected")
|
|
289
|
+
return 0
|
|
290
|
+
print(
|
|
291
|
+
"cibuildmp: error: no targets selected. Pass --allow-empty if that "
|
|
292
|
+
"is expected.",
|
|
293
|
+
file=sys.stderr,
|
|
294
|
+
)
|
|
295
|
+
return 2
|
|
296
|
+
|
|
297
|
+
if not is_abi_known(options.micropython):
|
|
298
|
+
print(
|
|
299
|
+
f"cibuildmp: warning: no recorded .mpy ABI for MicroPython "
|
|
300
|
+
f"{options.micropython}; assuming {options.abi}. The ABI actually "
|
|
301
|
+
f"encoded in each built .mpy is verified against its identifier, so "
|
|
302
|
+
f"a wrong guess fails the build rather than shipping.",
|
|
303
|
+
file=sys.stderr,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if args.dry_run:
|
|
307
|
+
total = len(targets)
|
|
308
|
+
print(
|
|
309
|
+
f"cibuildmp: {total} target(s) against MicroPython "
|
|
310
|
+
f"{options.micropython} (.mpy ABI {options.abi})"
|
|
311
|
+
)
|
|
312
|
+
for index, target in enumerate(targets, 1):
|
|
313
|
+
print(" " + _plan_line(index, total, options.build_options(target)))
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
return build(options, targets, toolchain=args.toolchain)
|
|
318
|
+
except (SourceError, ToolchainError) as exc:
|
|
319
|
+
if args.debug_traceback:
|
|
320
|
+
raise
|
|
321
|
+
print(f"cibuildmp: error: {exc}", file=sys.stderr)
|
|
322
|
+
return 2
|
cibuildmp/options.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Config loading and option resolution.
|
|
2
|
+
|
|
3
|
+
Precedence, lowest to highest:
|
|
4
|
+
defaults -> config file -> matching [[overrides]] -> environment -> CLI
|
|
5
|
+
|
|
6
|
+
Config lives in cibuildmp.toml at the package root, with the same tree
|
|
7
|
+
accepted under [tool.cibuildmp] in pyproject.toml for the rare MicroPython
|
|
8
|
+
C-module repo that has one. cibuildmp.toml wins when both exist.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shlex
|
|
15
|
+
import tomllib
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .targets import (
|
|
22
|
+
NATMOD_ARCHS,
|
|
23
|
+
Target,
|
|
24
|
+
abi_for_tag,
|
|
25
|
+
matches,
|
|
26
|
+
natmod_targets,
|
|
27
|
+
parse_selector,
|
|
28
|
+
select,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
CONFIG_FILENAME = "cibuildmp.toml"
|
|
32
|
+
|
|
33
|
+
DEFAULT_MICROPYTHON = "v1.28.0"
|
|
34
|
+
DEFAULT_OUTPUT_DIR = "mpyhouse"
|
|
35
|
+
DEFAULT_MODULE_DIR = "natmod"
|
|
36
|
+
DEFAULT_MAKE_TARGET = "dist"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConfigError(Exception):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _as_list(value: Any, key: str) -> list[str]:
|
|
44
|
+
"""Accept a list, or a shell-ish string, for list-valued options.
|
|
45
|
+
|
|
46
|
+
The string form exists for the environment layer -- CIBMP_EXTRA_MAKE_ARGS
|
|
47
|
+
can only ever be a string -- and is accepted in the file too so the two
|
|
48
|
+
layers do not disagree about what a valid value looks like.
|
|
49
|
+
"""
|
|
50
|
+
if value is None:
|
|
51
|
+
return []
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
return shlex.split(value)
|
|
54
|
+
if isinstance(value, list):
|
|
55
|
+
return [str(v) for v in value]
|
|
56
|
+
raise ConfigError(f"{key}: expected a list or a string, got {type(value).__name__}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class BuildOptions:
|
|
61
|
+
"""Fully resolved options for a single target."""
|
|
62
|
+
|
|
63
|
+
target: Target
|
|
64
|
+
micropython: str
|
|
65
|
+
output_dir: Path
|
|
66
|
+
module_dir: str
|
|
67
|
+
make_target: str
|
|
68
|
+
runs_on: str = ""
|
|
69
|
+
extra_make_args: list[str] = field(default_factory=list)
|
|
70
|
+
pre_build_command: str = ""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def identifier(self) -> str:
|
|
74
|
+
return self.target.identifier
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Options:
|
|
79
|
+
"""The whole config, before it is narrowed to a single target."""
|
|
80
|
+
|
|
81
|
+
package_dir: Path
|
|
82
|
+
config_path: Path | None
|
|
83
|
+
micropython: str
|
|
84
|
+
output_dir: Path
|
|
85
|
+
build: list[str]
|
|
86
|
+
skip: list[str]
|
|
87
|
+
archs: list[str]
|
|
88
|
+
micropython_submodules: list[str]
|
|
89
|
+
mpy_abi: str | None
|
|
90
|
+
natmod: dict[str, Any]
|
|
91
|
+
overrides: list[dict[str, Any]]
|
|
92
|
+
|
|
93
|
+
# ── Loading ───────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def load(
|
|
97
|
+
cls,
|
|
98
|
+
package_dir: Path,
|
|
99
|
+
config_file: Path | None = None,
|
|
100
|
+
env: Mapping[str, str] | None = None,
|
|
101
|
+
) -> Options:
|
|
102
|
+
environ: Mapping[str, str] = os.environ if env is None else env
|
|
103
|
+
config_path, raw = _read_config(package_dir, config_file)
|
|
104
|
+
|
|
105
|
+
natmod = dict(raw.get("natmod") or {})
|
|
106
|
+
overrides = list(raw.get("overrides") or [])
|
|
107
|
+
if not isinstance(overrides, list):
|
|
108
|
+
raise ConfigError("[[overrides]] must be an array of tables")
|
|
109
|
+
|
|
110
|
+
def opt(key: str, default: Any = None) -> Any:
|
|
111
|
+
# Environment beats the file for every global option. Keys are
|
|
112
|
+
# kebab-case in TOML (matching cibuildwheel) and
|
|
113
|
+
# CIBMP_SCREAMING_SNAKE in the environment.
|
|
114
|
+
env_value = environ.get("CIBMP_" + key.replace("-", "_").upper())
|
|
115
|
+
if env_value is not None:
|
|
116
|
+
return env_value
|
|
117
|
+
return raw.get(key, default)
|
|
118
|
+
|
|
119
|
+
archs_value = opt("archs") or natmod.get("archs") or list(NATMOD_ARCHS)
|
|
120
|
+
|
|
121
|
+
return cls(
|
|
122
|
+
package_dir=package_dir,
|
|
123
|
+
config_path=config_path,
|
|
124
|
+
micropython=str(opt("micropython", DEFAULT_MICROPYTHON)),
|
|
125
|
+
output_dir=Path(str(opt("output-dir", DEFAULT_OUTPUT_DIR))),
|
|
126
|
+
build=parse_selector(opt("build", "*")),
|
|
127
|
+
skip=parse_selector(opt("skip", "")),
|
|
128
|
+
archs=_as_list(archs_value, "archs"),
|
|
129
|
+
micropython_submodules=_as_list(
|
|
130
|
+
opt("micropython-submodules"), "micropython-submodules"
|
|
131
|
+
),
|
|
132
|
+
mpy_abi=(str(opt("mpy-abi")) if opt("mpy-abi") is not None else None),
|
|
133
|
+
natmod=natmod,
|
|
134
|
+
overrides=overrides,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# ── Resolution ────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def abi(self) -> str:
|
|
141
|
+
return abi_for_tag(self.micropython, self.mpy_abi)
|
|
142
|
+
|
|
143
|
+
def targets(self) -> list[Target]:
|
|
144
|
+
"""Every target this config selects, in NATMOD_ARCHS order."""
|
|
145
|
+
return select(natmod_targets(self.archs, self.abi), self.build, self.skip)
|
|
146
|
+
|
|
147
|
+
def build_options(
|
|
148
|
+
self, target: Target, env: Mapping[str, str] | None = None
|
|
149
|
+
) -> BuildOptions:
|
|
150
|
+
"""Resolve per-target options: file -> overrides -> environment."""
|
|
151
|
+
environ: Mapping[str, str] = os.environ if env is None else env
|
|
152
|
+
|
|
153
|
+
layers: list[dict[str, Any]] = [self.natmod]
|
|
154
|
+
for override in self.overrides:
|
|
155
|
+
selector = override.get("select")
|
|
156
|
+
if selector is None:
|
|
157
|
+
raise ConfigError("every [[overrides]] table needs a `select` key")
|
|
158
|
+
if matches(target.identifier, parse_selector(selector)):
|
|
159
|
+
layers.append(override)
|
|
160
|
+
|
|
161
|
+
def opt(key: str, default: Any = None) -> Any:
|
|
162
|
+
env_value = environ.get("CIBMP_" + key.replace("-", "_").upper())
|
|
163
|
+
if env_value is not None:
|
|
164
|
+
return env_value
|
|
165
|
+
# Later layers win: a matching override beats [natmod].
|
|
166
|
+
for layer in reversed(layers):
|
|
167
|
+
if key in layer:
|
|
168
|
+
return layer[key]
|
|
169
|
+
return default
|
|
170
|
+
|
|
171
|
+
return BuildOptions(
|
|
172
|
+
target=target,
|
|
173
|
+
micropython=self.micropython,
|
|
174
|
+
output_dir=self.output_dir,
|
|
175
|
+
module_dir=str(opt("module-dir", DEFAULT_MODULE_DIR)),
|
|
176
|
+
make_target=str(opt("make-target", DEFAULT_MAKE_TARGET)),
|
|
177
|
+
runs_on=str(opt("runs-on", target.default_runner)),
|
|
178
|
+
extra_make_args=_as_list(opt("extra-make-args"), "extra-make-args"),
|
|
179
|
+
pre_build_command=str(opt("pre-build-command", "")),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _read_config(
|
|
184
|
+
package_dir: Path, config_file: Path | None
|
|
185
|
+
) -> tuple[Path | None, dict[str, Any]]:
|
|
186
|
+
if config_file is not None:
|
|
187
|
+
if not config_file.is_file():
|
|
188
|
+
raise ConfigError(f"config file not found: {config_file}")
|
|
189
|
+
return config_file, _load_toml_tree(config_file)
|
|
190
|
+
|
|
191
|
+
standalone = package_dir / CONFIG_FILENAME
|
|
192
|
+
if standalone.is_file():
|
|
193
|
+
return standalone, _load_toml_tree(standalone)
|
|
194
|
+
|
|
195
|
+
pyproject = package_dir / "pyproject.toml"
|
|
196
|
+
if pyproject.is_file():
|
|
197
|
+
with pyproject.open("rb") as f:
|
|
198
|
+
data = tomllib.load(f)
|
|
199
|
+
tool = (data.get("tool") or {}).get("cibuildmp")
|
|
200
|
+
if tool is not None:
|
|
201
|
+
return pyproject, dict(tool)
|
|
202
|
+
|
|
203
|
+
# No config at all is legitimate: every option has a default, so a repo
|
|
204
|
+
# following the conventional natmod/ layout builds with none.
|
|
205
|
+
return None, {}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _load_toml_tree(path: Path) -> dict[str, Any]:
|
|
209
|
+
with path.open("rb") as f:
|
|
210
|
+
data = tomllib.load(f)
|
|
211
|
+
if path.name == "pyproject.toml":
|
|
212
|
+
return dict((data.get("tool") or {}).get("cibuildmp") or {})
|
|
213
|
+
return data
|