-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsetup.py
More file actions
220 lines (181 loc) · 8.22 KB
/
Copy pathsetup.py
File metadata and controls
220 lines (181 loc) · 8.22 KB
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
"""Local source-install build driver for fbuild.
`pip install ~/dev/fbuild` (or any `pip install .` from the repo root) goes
through this file because `pyproject.toml` declares the setuptools build
backend. The plain backend would ship only `ci/` Python helpers — no working
`fbuild` command — because the actual CLI is a Rust crate (`fbuild-cli`)
that lives in the cargo workspace under `crates/`.
This file wires the install path through `soldr cargo build --release -p
fbuild-cli`, copies the resulting binary to `ci/bin/fbuild[.exe]`, and lets
setuptools pack it into the wheel. The `ci.bin_launcher:main` entry point
(declared in pyproject.toml) execs that binary, so `fbuild` on PATH after
install Just Works.
This is the LOCAL DEV install path. The RELEASE path lives entirely in
the Autonomous Release GitHub Action (`.github/workflows/release-auto.yml`):
the action builds per-platform binaries on its own runners, calls
`ci/publish.py::build_all_wheels` to assemble platform-tagged wheels, and
uploads to PyPI via trusted publishing (OIDC). See `docs/RELEASING.md`.
Why soldr (and not bare cargo)?
- soldr resolves the toolchain via `rustup which`, respecting
`rust-toolchain.toml` without requiring PATH to be pre-shaped.
- soldr auto-sets `RUSTC_WRAPPER` to zccache, so rebuilds across `pip
install .` invocations are incremental + dep-cached.
Why not `setuptools-rust` or `maturin`? Both are reasonable but heavier:
they introduce another tool with its own toolchain assumptions, while
soldr is already the canonical build driver across this repo's dev,
trampoline, and CI paths (see `ci/trampoline.py`). Keeping the single
soldr-cargo invocation means there's only one place to look when iteration
is slow.
Locating the built binary
-------------------------
Cargo writes the `fbuild` executable to either
<target>/release/fbuild[.exe]
(when no host triple is configured) or
<target>/<host-triple>/release/fbuild[.exe]
(when soldr / zccache sets `CARGO_BUILD_TARGET=<host-triple>` to isolate
its caches by host — which is what happens in this repo by default). The
previous version of this file only checked the first path; on Windows
where soldr was configured for a host triple, that path was empty even
on a green build, so every `pip install .` failed with
ERROR: cargo build succeeded but binary not at target\release\fbuild.exe.
To handle both layouts (and any future per-feature/per-profile target
directory) we drive cargo with `--message-format=json-render-diagnostics`
and pull the real artifact path out of cargo's structured output. That's
how `cargo install` and most Rust packaging tools find their binaries.
"""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Optional
from setuptools import setup
from setuptools.command.build_py import build_py
from setuptools.dist import Distribution
REPO_ROOT = Path(__file__).resolve().parent
TARGET_BINARY_NAME = "fbuild.exe" if sys.platform == "win32" else "fbuild"
STAGED_BIN_DIR = REPO_ROOT / "ci" / "bin"
STAGED_BINARY_PATH = STAGED_BIN_DIR / TARGET_BINARY_NAME
def _require_soldr() -> None:
if shutil.which("soldr") is None:
sys.stderr.write(
"\n"
"ERROR: `soldr` is required to build fbuild from source.\n"
"Install one of:\n"
" uv tool install soldr\n"
" curl -fsSL https://raw.githubusercontent.com/zackees/soldr/main/install.sh | bash\n"
"Then re-run `pip install .`.\n"
"\n"
"If you only want the Python helpers (no `fbuild` CLI), install\n"
"the `fbuild-dev-tools` subpackage instead: `uv sync` from this\n"
"repo root.\n"
"\n"
)
sys.exit(1)
def _find_fbuild_executable_from_json(stdout: str) -> Optional[Path]:
"""Walk cargo's structured artifact stream and return the path to the
`fbuild` binary, or `None` if no compiler-artifact line for it appeared.
cargo emits one JSON object per line; the artifact we want has
`reason == "compiler-artifact"`, `target.name == "fbuild"`, and a non-
null `executable` field. We keep the *last* match because cargo emits
one artifact per crate target kind and the bin artifact is what we
want (matches `cargo install`'s selection rule).
"""
binary_path: Optional[Path] = None
for line in stdout.splitlines():
line = line.strip()
if not line or not line.startswith("{"):
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
# Non-JSON noise (rare; cargo's renderer can inline human-
# readable progress on stdout when there's no compatible TTY).
continue
if msg.get("reason") != "compiler-artifact":
continue
target = msg.get("target") or {}
if target.get("name") != "fbuild":
continue
executable = msg.get("executable")
if executable:
binary_path = Path(executable)
return binary_path
def _find_fbuild_executable_by_search() -> Optional[Path]:
"""Fallback when cargo didn't emit a usable artifact line (e.g. a fully
cached build that reports `Fresh` and skips compiler-artifact). Probe
the canonical `target/release` path and every per-host-triple subdir.
"""
candidates = [REPO_ROOT / "target" / "release" / TARGET_BINARY_NAME]
target_root = REPO_ROOT / "target"
if target_root.is_dir():
for child in target_root.iterdir():
candidate = child / "release" / TARGET_BINARY_NAME
if candidate.is_file():
candidates.append(candidate)
for candidate in candidates:
if candidate.is_file():
return candidate
return None
def _build_fbuild_cli() -> Path:
"""Run `soldr cargo build` and return the path to the built executable."""
cmd = [
"soldr",
"cargo",
"build",
"--release",
"-p",
"fbuild-cli",
"--message-format=json-render-diagnostics",
]
sys.stderr.write(f" $ {' '.join(cmd)}\n")
# stderr passes through so soldr's session summary stays visible; stdout
# is captured because that's where cargo writes its JSON artifact stream.
proc = subprocess.run(
cmd,
cwd=str(REPO_ROOT),
stdout=subprocess.PIPE,
stderr=None,
check=False,
text=True,
encoding="utf-8",
)
if proc.returncode != 0:
sys.stderr.write(
f"ERROR: `soldr cargo build` exited with code {proc.returncode}.\n"
)
sys.exit(proc.returncode)
binary_path = _find_fbuild_executable_from_json(proc.stdout)
if binary_path is None or not binary_path.is_file():
binary_path = _find_fbuild_executable_by_search()
if binary_path is None or not binary_path.is_file():
sys.stderr.write(
"ERROR: cargo build succeeded but no `fbuild` binary was found.\n"
"Searched:\n"
" - cargo's structured JSON artifact stream\n"
" (--message-format=json-render-diagnostics)\n"
f" - {REPO_ROOT / 'target' / 'release' / TARGET_BINARY_NAME}\n"
f" - {REPO_ROOT / 'target'}/<host-triple>/release/{TARGET_BINARY_NAME}\n"
"If you suspect cargo wrote the binary somewhere else, please\n"
"file an issue at https://github.com/FastLED/fbuild/issues and\n"
"attach the output of `soldr cargo build --release -p fbuild-cli -v`.\n"
)
sys.exit(1)
return binary_path
class BuildWithCargo(build_py):
"""Run `soldr cargo build --release -p fbuild-cli` before packaging."""
def run(self) -> None: # noqa: D401 — setuptools API name
_require_soldr()
binary_path = _build_fbuild_cli()
STAGED_BIN_DIR.mkdir(parents=True, exist_ok=True)
shutil.copy2(binary_path, STAGED_BINARY_PATH)
sys.stderr.write(f" staged binary -> {STAGED_BINARY_PATH}\n")
super().run()
class BinaryDistribution(Distribution):
"""Force a platform-tagged wheel because we ship a native binary."""
def has_ext_modules(self) -> bool: # noqa: D401 — setuptools API name
return True
setup(
cmdclass={"build_py": BuildWithCargo},
distclass=BinaryDistribution,
)