From 59f3df278bf567c17d5528793811cae15b064f7e Mon Sep 17 00:00:00 2001 From: grabowski Date: Fri, 16 Oct 2026 19:01:20 +0700 Subject: [PATCH] release/: JLCPCB order files when the repository makes them A repository with .gitea/scripts/jlcpcb-files.py (harNES) gets --jlcpcb-bom.csv and -jlcpcb-cpl.csv (rotations corrected per LCSC part) in release/: the script runs with --force on a copy of hardware/ without the committed jlcpcb/ folders, so nothing stale is taken. When it refuses a board (DRC, missing correction), release/README.md and a workflow warning say why; the generic -cpl.csv then is the only placement file. --- .gitea/scripts/release-files.py | 60 +++++++++++++++++++++++++++++++-- TEMPLATE.md | 6 ++-- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/.gitea/scripts/release-files.py b/.gitea/scripts/release-files.py index 615bf1f..8f5f7f2 100644 --- a/.gitea/scripts/release-files.py +++ b/.gitea/scripts/release-files.py @@ -13,6 +13,8 @@ Writes into --out (default release/; existing content is replaced): --pcb.pdf one page per copper, silkscreen and fab layer --top.png 3D render, top --bottom.png 3D render, bottom + --jlcpcb-bom.csv, -jlcpcb-cpl.csv JLCPCB order files with rotations + corrected, when the repository has .gitea/scripts/jlcpcb-files.py (harNES) docs/-.pdf docs/*.md (pandoc, pdflatex) and docs/tex/*.tex (pdflatex) Boards are the [[boards]] of proplan.toml, or hardware/*/*.kicad_pro when it names none. DRC and ERC @@ -190,7 +192,42 @@ def cpl(pcb, dest): return f"pick and place: {max(len(rows) - 1, 0)} parts, both sides, mm, DNP left out" -def board(board_dir, pro, version, out, tmp): +def jlcpcb_files(tmp): + """The repository's own JLCPCB order files, if it has .gitea/scripts/jlcpcb-files.py (harNES's pattern: + JLCPCB BOM, CPL with rotations corrected per LCSC part, refused unless DRC is clean). It runs with + --force on a copy of hardware/ without the committed jlcpcb/ folders, so nothing stale is taken. + Returns {board folder relative to the root: (jlcpcb folder or None, why not)}.""" + script = ROOT / ".gitea" / "scripts" / "jlcpcb-files.py" + if not script.exists(): + return {} + work = tmp / "jlcpcb-work" + shutil.copytree(ROOT / "hardware", work / "hardware", ignore=shutil.ignore_patterns("jlcpcb", "render", "*-backups")) + (work / ".gitea" / "scripts").mkdir(parents=True) + shutil.copyfile(script, work / ".gitea" / "scripts" / script.name) + r = run([sys.executable, work / ".gitea" / "scripts" / script.name, "--force"], cwd=work, check=False) + print(" " + "\n ".join((r.stdout + r.stderr).strip().splitlines())) + blocks = {b.split("\n", 1)[0].split(":")[0].strip(): b for b in re.split(r"^== ", r.stdout, flags=re.M) if b.strip()} + result = {} + for pro in sorted(work.glob("hardware/*/*.kicad_pro")): + folder = pro.parent / "jlcpcb" + rel = pro.parent.relative_to(work).as_posix() + if (folder / f"{pro.stem}-bom.csv").exists() and (folder / f"{pro.stem}-cpl.csv").exists(): + result[rel] = (folder, "") + else: + lines = [l.strip() for l in blocks.get(pro.stem, "").splitlines()[1:] if l.strip()] + i = next((k for k, l in enumerate(lines) if l.startswith("NOT written")), None) + if i is None: # no verdict: the script crashed (its last line says why) + why = (r.stderr.strip().splitlines() or ["jlcpcb-files.py failed"])[-1] + else: # "NOT written: DRC is not clean" or "NOT written:", then one line per problem + head, rest = lines[i].partition(":")[2].strip(), lines[i + 1:] + why = "; ".join(x for x in (head, rest[0] if rest else "") if x) or "NOT written" + if len(rest) > 1: + why += f" (+{len(rest) - 1} more)" + result[rel] = (None, why[:300]) + return result + + +def board(board_dir, pro, version, out, tmp, jlc): """Write one board's files; returns (name, [(file, what)], checks).""" name = pro.stem sch, pcb = board_dir / f"{name}.kicad_sch", board_dir / f"{name}.kicad_pcb" @@ -210,6 +247,17 @@ def board(board_dir, pro, version, out, tmp): f"{d['schematic_parity']} schematic parity issues") files.insert(0, (f"{base}-gerbers.zip", gerbers_zip(pcb, dest / f"{base}-gerbers.zip", tmp))) files.append((f"{base}-cpl.csv", cpl(pcb, dest / f"{base}-cpl.csv"))) + rel = board_dir.relative_to(ROOT).as_posix() + if rel in jlc: + folder, why = jlc[rel] + if folder: + for kind, what in (("bom", "JLCPCB BOM: one line per LCSC part number"), + ("cpl", "JLCPCB CPL: rotations and centres corrected for JLCPCB's footprints")): + shutil.copyfile(folder / f"{name}-{kind}.csv", dest / f"{base}-jlcpcb-{kind}.csv") + files.append((f"{base}-jlcpcb-{kind}.csv", what)) + files[-3] = (files[-3][0], files[-3][1] + "; KiCad rotations, for JLCPCB use the -jlcpcb-cpl.csv") + else: + checks.append(f"JLCPCB files not made ({why})") copper = ",".join(copper_layers(pcb)) cli("pcb", "export", "pdf", "--mode-multipage", "--include-border-title", "--layers", f"{copper},F.Silkscreen,B.Silkscreen,F.Fab,B.Fab", "--common-layers", "Edge.Cuts", @@ -358,6 +406,11 @@ def main(): kicad = cli("version").strip() clear(out) boards, skipped, ok = [], [], True + jlc_tmp = tempfile.TemporaryDirectory() + jlc = {} + if (ROOT / ".gitea" / "scripts" / "jlcpcb-files.py").exists(): + print("== JLCPCB order files (.gitea/scripts/jlcpcb-files.py)") + jlc = jlcpcb_files(Path(jlc_tmp.name)) for board_dir, pro in projects(m): rel = board_dir.relative_to(ROOT).as_posix() if pro is None: @@ -367,7 +420,7 @@ def main(): print(f"== {pro.stem}") with tempfile.TemporaryDirectory() as t: try: - name, files, checks = board(board_dir, pro, a.version, out, Path(t)) + name, files, checks = board(board_dir, pro, a.version, out, Path(t), jlc) except Failed as e: print(f"::error::{pro.stem}: {e}") ok = False @@ -375,10 +428,11 @@ def main(): boards.append((name, files, checks)) for c in checks: print(f" {c}") - if re.search(r"[1-9]\d* (errors|unconnected|schematic)", c): + if re.search(r"[1-9]\d* (errors|unconnected|schematic)", c) or c.startswith("JLCPCB"): print(f"::warning::{name} {c}") for f, _ in files: print(f" hardware/{name}/{f}") + jlc_tmp.cleanup() print("== documentation") foot = tex_escape(f"{product} {a.version}, {commit}, {when[:10]}") docs_made, docs_failed = documents(a.version, foot, out) diff --git a/TEMPLATE.md b/TEMPLATE.md index 6b96352..ca3bd52 100644 --- a/TEMPLATE.md +++ b/TEMPLATE.md @@ -121,6 +121,7 @@ stage tag is pushed; nobody edits it. | `release/hardware//--cpl.csv` | pick and place (KiCad position file, mm, both sides, DNP left out) | | `release/hardware//--schematic.pdf`, `-pcb.pdf` | the schematic; the PCB with one page per copper, silkscreen and fab layer | | `release/hardware//--top.png`, `-bottom.png` | 3D renders | +| `release/hardware//--jlcpcb-bom.csv`, `-jlcpcb-cpl.csv` | only in a repository with `.gitea/scripts/jlcpcb-files.py` (harNES): JLCPCB's BOM and a CPL with rotations corrected per LCSC part, made only from a clean DRC; use these, not `-cpl.csv`, for JLCPCB assembly | | `release/docs/-.pdf` | every `docs/*.md` except `README.md` and `input.md` (pandoc), and every `docs/tex/*.tex` with a `\documentclass` (pdflatex); the footer carries product, tag, commit and date | - Boards are the `[[boards]]` of `proplan.toml` (or `hardware/*/*.kicad_pro` @@ -146,8 +147,9 @@ stage tag is pushed; nobody edits it. writes the same files to `build/` (ignored by git). Not in it (yet): firmware binaries (firmware has its own tags), STEP models -(`mech/`), JLCPCB-specific BOM/CPL with rotation corrections (harNES's -`jlcpcb-files.py` does that per push). +(`mech/`). JLCPCB order files need `jlcpcb-files.py` and a +`jlcpcb-corrections.csv` per board (harNES has both); without them the generic +`-cpl.csv` carries KiCad's rotations, which JLCPCB's footprints may not share. ## proplan.toml