|
20 | 20 | from __future__ import annotations |
21 | 21 |
|
22 | 22 | import argparse |
| 23 | +import io |
23 | 24 | import json |
24 | 25 | import os |
25 | 26 | import re |
@@ -208,45 +209,103 @@ def run_build(cmd: list[str], cwd: Path, show=None) -> int: |
208 | 209 | def vinca_pinning(distro: str) -> str: |
209 | 210 | """shared/pinning/conda_forge.yaml + overrides.yaml, adjusted by distro.yaml. |
210 | 211 |
|
211 | | - Shared override entries that the distribution overrides itself are left out |
212 | | - together with the comment lines above them (comments carry the conda-build |
213 | | - selectors, so this works on text, not on parsed YAML). |
| 212 | + The files are read and written with ruamel.yaml's round trip, which keeps |
| 213 | + comments, and with them the conda-build selectors (`- 2.39 # [riscv64]`). |
| 214 | + A distribution's own pinning_overrides replace the shared entries of the same |
| 215 | + name, together with the comment above them. The distribution's own comments |
| 216 | + are left out: vinca-pinning-render moves its entries to their place in the |
| 217 | + conda-forge pinning, and the comments would end up above other keys. |
214 | 218 | """ |
215 | | - s = settings(distro) |
| 219 | + from ruamel.yaml.comments import CommentedMap, CommentedSeq |
| 220 | + |
| 221 | + yaml = ruamel_yaml() |
| 222 | + s = yaml.load((DISTROS / distro / "distro.yaml").read_text()) or {} |
216 | 223 | own = s.get("pinning_overrides") or {} |
217 | | - out = [f"# Generated by `pixi run rs {distro} render-pinning` from shared/pinning/ and", |
218 | | - f"# distros/{distro}/distro.yaml; edit those instead.", ""] |
| 224 | + shared = yaml.load((SHARED / "pinning" / "overrides.yaml").read_text())["pinning_overrides"] |
| 225 | + shared_comments = key_comments(shared) |
| 226 | + |
| 227 | + out = CommentedMap() |
219 | 228 | if s.get("conda_forge_pinning_version"): |
220 | | - out.append(f"conda_forge_pinning_version: {s['conda_forge_pinning_version']}") |
221 | | - migrations = s.get("conda_forge_migrations") or [] |
222 | | - out.append("migrations:" if migrations else "migrations: []") |
223 | | - out += [f" - {m}" for m in migrations] |
| 229 | + out["conda_forge_pinning_version"] = s["conda_forge_pinning_version"] |
| 230 | + out["migrations"] = s.get("conda_forge_migrations") or [] |
| 231 | + cut_trailing_comments(out["migrations"]) |
224 | 232 | else: |
225 | | - out += [l for l in (SHARED / "pinning" / "conda_forge.yaml").read_text().splitlines() |
226 | | - if not l.startswith("#")] |
227 | | - comments: list[str] = [] |
228 | | - skip = False |
229 | | - for line in (SHARED / "pinning" / "overrides.yaml").read_text().splitlines(): |
230 | | - if line.startswith("#"): |
231 | | - continue |
232 | | - if line.startswith(" #"): |
233 | | - comments.append(line) |
234 | | - elif line.startswith(" ") and not line.startswith(" "): |
235 | | - skip = line[2:].split(":")[0] in own |
236 | | - if not skip: |
237 | | - out += comments + [line] |
238 | | - comments = [] |
239 | | - elif line.startswith(" "): |
240 | | - if not skip: |
241 | | - out.append(line) |
242 | | - else: |
243 | | - out.append(line) |
| 233 | + out.update(yaml.load((SHARED / "pinning" / "conda_forge.yaml").read_text())) |
| 234 | + overrides = out["pinning_overrides"] = CommentedMap() |
| 235 | + for key, value in shared.items(): |
| 236 | + if key not in own: |
| 237 | + overrides[key] = value |
| 238 | + if key in shared_comments: |
| 239 | + overrides.yaml_set_comment_before_after_key(key, before=shared_comments[key], indent=2) |
| 240 | + for key, value in own.items(): |
| 241 | + if not isinstance(value, list): |
| 242 | + value = CommentedSeq([value]) |
| 243 | + cut_trailing_comments(value) |
| 244 | + value.fa.set_block_style() |
| 245 | + overrides[key] = value |
244 | 246 | if own: |
245 | | - out.append(f" # ros-{distro} specific (distros/{distro}/distro.yaml)") |
246 | | - for key, value in own.items(): |
247 | | - out.append(f" {key}:") |
248 | | - out += [f" - {v}" for v in (value if isinstance(value, list) else [value])] |
249 | | - return "\n".join(out) + "\n" |
| 247 | + overrides.yaml_set_comment_before_after_key( |
| 248 | + next(iter(own)), before=f"ros-{distro} specific (distros/{distro}/distro.yaml)", indent=2) |
| 249 | + |
| 250 | + out.yaml_set_start_comment(f"Generated by `pixi run rs {distro} render-pinning` from shared/pinning/ and\n" |
| 251 | + f"distros/{distro}/distro.yaml; edit those instead.\n") |
| 252 | + stream = io.StringIO() |
| 253 | + yaml.dump(out, stream) |
| 254 | + return stream.getvalue() |
| 255 | + |
| 256 | + |
| 257 | +def ruamel_yaml(): |
| 258 | + """ruamel.yaml round trip in the layout of the repository's YAML files.""" |
| 259 | + import ruamel.yaml |
| 260 | + |
| 261 | + yaml = ruamel.yaml.YAML() |
| 262 | + yaml.preserve_quotes = True |
| 263 | + yaml.width = 4096 |
| 264 | + yaml.indent(mapping=2, sequence=4, offset=2) |
| 265 | + return yaml |
| 266 | + |
| 267 | + |
| 268 | +def _comment_text(text: str) -> str: |
| 269 | + return "\n".join(line.strip().removeprefix("#").removeprefix(" ") |
| 270 | + for line in text.splitlines() if line.strip()) |
| 271 | + |
| 272 | + |
| 273 | +def cut_trailing_comments(value) -> str: |
| 274 | + """Cut the comment lines that follow a block list's last item (ruamel keeps |
| 275 | + the comment above the next key there) and return them; the item's own |
| 276 | + comment, e.g. its selector, stays.""" |
| 277 | + token = value.ca.items.get(len(value) - 1, [None])[0] if isinstance(value, list) and value else None |
| 278 | + if token is None: |
| 279 | + return "" |
| 280 | + own_line, _, rest = token.value.partition("\n") |
| 281 | + if own_line.strip(): |
| 282 | + token.value = own_line + "\n" |
| 283 | + else: |
| 284 | + value.ca.items[len(value) - 1][0] = None |
| 285 | + return rest |
| 286 | + |
| 287 | + |
| 288 | +def key_comments(mapping) -> dict[str, str]: |
| 289 | + """The comment above each key of a ruamel mapping, without the "# ". |
| 290 | +
|
| 291 | + ruamel attaches it to whatever precedes the key: the mapping (first key), the |
| 292 | + end of the previous block list (cut from there, see cut_trailing_comments), |
| 293 | + the previous key after a scalar value, or the key itself after a flow value.""" |
| 294 | + comments = {} |
| 295 | + previous = None |
| 296 | + for key in mapping: |
| 297 | + if previous is None: |
| 298 | + text = "".join(t.value for t in (mapping.ca.comment or [None, []])[1] or []) |
| 299 | + elif isinstance(mapping[previous], list) and mapping[previous]: |
| 300 | + text = cut_trailing_comments(mapping[previous]) |
| 301 | + else: |
| 302 | + token = mapping.ca.items.get(previous, [None] * 3)[2] |
| 303 | + text = token.value.partition("\n")[2] if token else "" |
| 304 | + text += "".join(t.value for t in mapping.ca.items.get(key, [None, None])[1] or []) |
| 305 | + if text := _comment_text(text): |
| 306 | + comments[key] = text |
| 307 | + previous = key |
| 308 | + return comments |
250 | 309 |
|
251 | 310 |
|
252 | 311 | def load_yaml(path: Path) -> dict: |
|
0 commit comments