From c82d3d8381070dbc7a4fcf837408e83131b210c3 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 16 Jul 2026 10:13:11 +0000 Subject: [PATCH] Add .IntLib compilation (build_intlib.py + container template): every part folder now pushes five files; make .SchLib parameter fill compulsory --- SKILL.md | 94 ++++++-- assets/CHANGELOG.xlsx | Bin 5664 -> 5664 bytes assets/templates/intlib_container.IntLib | Bin 0 -> 23040 bytes scripts/build_intlib.py | 277 +++++++++++++++++++++++ 4 files changed, 351 insertions(+), 20 deletions(-) create mode 100644 assets/templates/intlib_container.IntLib create mode 100644 scripts/build_intlib.py diff --git a/SKILL.md b/SKILL.md index 1f68148..1b9a087 100644 --- a/SKILL.md +++ b/SKILL.md @@ -2,16 +2,16 @@ name: library-manager description: >- Manage Vecmocon's component library. Extract parameters from a component datasheet PDF into the - per-typeid Excel template (125 type-IDs across 18 classes). Checks Gitea for a duplicate - MPN_make, classifies to a typeid, updates that typeid's template (versioning, changelog, - backfill), fills a per-part workbook, loops on human verification, reads the Altium - symbol/footprint refs, assembles a part folder and pushes to Gitea. Can UPDATE an existing part - instead of hard-stopping on a duplicate, and can WRITE the mandatory SOP parameters directly - into a .SchLib symbol (Value, Manufacturer, Operating Temperature, Tolerance, RoHS, Datasheet, - ...) while stripping Ultra-Librarian defaults. Every Description follows Vecmocon's strict + per-typeid Excel template, check Gitea for a duplicate MPN_make, classify to a + typeid, update that typeid's template (versioning, changelog, backfill), fill a per-part + workbook, verify with a human, read the Altium symbol/footprint refs, and push a part folder to + Gitea. Can UPDATE an existing part instead of hard-stopping on a duplicate. WRITES the mandatory + SOP parameters (from the verified Excel) directly into the .SchLib symbol and COMPILES an Altium + integrated library (.IntLib) bundling symbol + footprint, so every part folder holds five files + (workbook, datasheet, .SchLib, .PcbLib, .IntLib). Every Description follows Vecmocon's strict Altium Description Format. Use whenever the user uploads a datasheet, builds/updates a library - entry, adds a template parameter, fills/updates .SchLib parameters, or pushes a part to Gitea. - ALWAYS trigger on "\datasheet", "\library", "\library-manager", or "\schlib". + entry, adds a parameter, fills .SchLib parameters, builds an integrated library, or pushes to + Gitea. ALWAYS trigger on "\datasheet", "\library", "\library-manager", or "\schlib". --- # Library Manager @@ -75,10 +75,16 @@ library repo/ __/ e.g. BAT46WJ_Nexperia_SCH __.xlsx this part's own one-row parameter sheet _data.pdf the datasheet - .SchLib user-provided + .SchLib user-provided, with all parameters written in .PcbLib user-provided + .IntLib integrated library (symbol + footprint, compiled by the skill) ``` +**Every part folder holds five files** — the workbook, the datasheet, the `.SchLib` (with the +full parameter set written into it), the `.PcbLib`, and the compiled `.IntLib`. The `.SchLib` +parameter fill and the `.IntLib` build are **not optional** — they run on every part before the +push (see *Mandatory symbol parameters* and *Build the integrated library* below). + There is **no single master workbook** — each part carries its own sheet inside its folder. Connection + repo names live in `config/gitea.env` (`SKILL_REPO`, `LIBRARY_REPO`), so runs need no per-session token. If the host is unreachable, the git steps fail clearly and write @@ -286,16 +292,23 @@ python scripts/fill_templates.py part.json \ --template assets/template/template.xlsx --dest // --design design.json ``` -Once you have the symbol and its Library Ref, you can also produce the **mandatory symbol -parameters** the SOP requires on the `.SchLib` (Manufacturer, Manufacturer Part, Value, -Tolerance, Operating Temperature, RoHS, Datasheet, Process, Vecmocon Part Code, …) — see -*Mandatory symbol parameters* below. This is optional per run but is how the datasheet values -land in the Altium symbol's properties. +Once you have the symbol and its Library Ref, **always** write the full parameter set into the +`.SchLib` from the verified per-part Excel — see *Mandatory symbol parameters* below. **This is +compulsory on every run; do not ask the engineer whether to do it — just do it.** It is how the +verified workbook values (and the Description) land in the Altium symbol's properties, and the +`.IntLib` is then built from this enriched symbol. -### 7. Assemble the part folder +### 7. Assemble the part folder — build the `.IntLib`, then five files -The staging folder `/` should now hold the four files: the per-part `.xlsx`, the -datasheet (name it `_data.`), the symbol, and the footprint. +The staging folder `/` should hold the per-part `.xlsx`, the datasheet (name it +`_data.`), the enriched symbol (`.SchLib` with parameters written in, step 6), and the +footprint (`.PcbLib`). Now **compile the integrated library** from the enriched symbol + footprint +so the folder carries all **five** files — see *Build the integrated library* below: + +```bash +python scripts/build_intlib.py --schlib //.SchLib \ + --pcblib //.PcbLib --out //.IntLib +``` ### 8. Push to the library repo, under the part's Class @@ -313,8 +326,9 @@ user where it landed. The SOP (§5) requires every schematic symbol to carry a fixed parameter set in its Altium properties — `Manufacturer`, `Manufacturer Part`, `Value`, `Tolerance`, `Operating Temperature`, `ROHS`, `Datasheet`, `Process`, `Vecmocon Part Code`, and the two second-source -fields — with the **Comment** set to the MPN. The skill can stamp these onto the symbol from -the datasheet. +fields — with the **Comment** set to the MPN. The skill stamps these onto the symbol from the +verified workbook. **This step is compulsory on every part build — never ask whether to fill the +`.SchLib` parameters; always do it** before assembling the folder and building the `.IntLib`. This runs **as its own task too**, not only inside new-part creation: whenever the user hands over one or more `.SchLib` files and wants their parameters filled/updated (e.g. "\schlib", "add @@ -370,6 +384,40 @@ round-trip, fall back to `scripts/altium_params.py` (emits an Altium DXP script parameters from inside Altium). Always have the engineer confirm the file opens in Altium — the skill writes Altium's own binary format, so Altium is the final validator. +## Build the integrated library (.IntLib) + +Every part also gets a compiled **integrated library** — one file that bundles the schematic +symbol (with the parameters written in) and its footprint, so the component resolves in Altium +with no separate `.PcbLib` to locate. `scripts/build_intlib.py` compiles it in pure Python, no +Altium needed, and it's the **fifth file** in every part folder. + +```bash +python scripts/build_intlib.py --schlib //.SchLib \ + --pcblib //.PcbLib --out //.IntLib +``` + +How it works and what it needs: + +- Feed it the **enriched `.SchLib`** (after `schlib_write.py` has written the parameters in) so the + integrated symbol carries the full parameter set and the correct ComponentDescription. +- The symbol **must contain a footprint model link** (an Altium RECORD=45 `ModelName` / + `ModelType=PCBLIB` in its `Data` stream — Ultra-Librarian and Altium exports include this). The + builder reads that link to know which footprint to bind, and errors clearly if it's absent — in + that case the symbol has no footprint assigned, so fix the symbol (or re-export it) first. +- It builds the `.IntLib` as an OLE compound file with five streams — the embedded `.schlib` and + `.pcblib` (zlib-compressed at Altium's default level), plus `LibCrossRef.Txt`, `Parameters .bin`, + and `Version.Txt` — reusing a bundled container skeleton + (`assets/templates/intlib_container.IntLib`) for the exact directory layout Altium expects, and a + **FAT-first** compound-file writer (`build_intlib.write_cfb`). Both the outer container and the + embedded symbol are written FAT-first — this matters: a FAT-last layout re-opens fine in olefile + and even standalone in Altium, but Altium's **IntLib extractor** throws "Stream read error" on it. + The two embedded libraries are compressed at zlib's **default level** (`0x789c`); Altium's + decompressor rejects other levels (e.g. level-9 `0x78da`). +- The builder self-validates: it re-opens the output, decompresses both embedded libraries, and + confirms they round-trip and that the cross-reference names the symbol + footprint. Even so, + Altium is the final validator — have the engineer open the `.IntLib` once (or, as a guaranteed + fallback, compile a `.LibPkg` in Altium from the same `.SchLib` + `.PcbLib`). + ## Per-typeid versioning Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and @@ -544,6 +592,12 @@ plain flat push, but it does not merge the changelog or blank the token, so pref `Manufacturer_Part_Number` defaults). Primary path; see `references/schlib_parameters.md`. - `scripts/altium_params.py` — fallback: generate an Altium DelphiScript that stamps the same parameters onto a `.SchLib` from inside Altium (DXP → Run Script). +- `scripts/build_intlib.py` — compile a component's `.SchLib` + `.PcbLib` into an Altium + **integrated library** (`.IntLib`) in pure Python (FAT-first OLE writer + Altium-level zlib); + the fifth file in every part folder. Needs the enriched `.SchLib` (parameters written) with a + footprint model link. Uses `assets/templates/intlib_container.IntLib` as the container skeleton. +- `assets/templates/intlib_container.IntLib` — a known-good single-component `.IntLib` reused + purely as the OLE container skeleton by `build_intlib.py` (all its streams are overwritten). - `scripts/gitea_components.py` — `check-mpn`, `find-part` (locate an existing part to update), `checkout`, `list-type`, `place-part`, `commit-push`, `push-part` (library repo), and `push-skill` (skill repo: token-blanked push + append-only changelog merge). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index a31adbf5ec60bb10d618532c9a2e733c2f2e3126..721e44557c64d74192d91a7b1be6b8ec4140952e 100644 GIT binary patch delta 1791 zcmYk7c{Cen7sitkNsx*qmQq`gw)UoI8e3`YYTq;Vu}p-q6v2E+O>LtT>82HXM26Z` zYhRCTXe||?rIu-={ZvzmVsz;D&6&Rc{Lb^oz3(~q+~+>UYQ<`T_SQVSVgLXD0-!nM z3id#04j*w5`+2E5xij*lJ}tpiAJCr*mqHybJKP^txhAim4eeLAYFGE1GRtsiYoV)q zfs?^+B=?`QpXcF4%Q=dMgBrd{X`G;swvWo8s3Di&86WL=wNOV+irFP&= zn0E#yJw%YKrI+m~6<=|2MW<$+$t%;-1L%c^AUQ*SbYC?v>_q^{8hS;8%n0?r2(yK` zb@L)`8UBKJq0#?r4;A|XfJLRP-g92uA^MCziv$BB>~3prw&vqSIWuo|RWX)yvTAJp zfoF~0Jh+12+gYk>ly{L|BSOGqq06qu=%A*zvoV}~m=>ucmFdXO9oto`{Au^P#5>XN z1_E?w+y)x)eVfS>UVeB>qaoxYkkl4jdh_yZ1lVVeO6J}9H9RnSQFuA>d?RtgzT>9u zV6E^NegZRQng^$z2oJ~}yyG7qwW^)dHQIhjW6rZF^hr<-J~{0Si+o40q-IR`r?M#B z0U>xR3i8^{bdC4dA>H8&WIvPTqYF8C_jIpLDUk{F(^ZaN7o(|YCga;3Ngr|9)|M-# zKkAPIzJyu7ay0Rz8a-ly*=3Z?2{!dMUr)yv(L9|_dr3?i`EYR5U^S)_o|vQLXH}p4 z@akg2db-h^kh;lczs#Ds$LP)aPU`k(ATq$$kXsdTI zY)eL=@DRsZ!#Kw#Fw;qI4Nx8QfoL!P>zgLRH_yqtu9_`(P-HkwLk~0K2S2Sx1~;eA zp-@MC^0EShd1{t0d|4uE>iy(h||oqQBnr0It{MkZ1zzVf7zYSvY=zv_;f3> z;Ztlz7Q^(%;DoG@g}C!n&EQqk6oZuj^>{fQ9$V#AO@_YpA#pRV-dh5t*ptH~nNR2I zWT!$w;a-|R(aIbXz2*(rE5b(l5F+r_A4NqUuoLee!{`Ct!I66rzhO5U-{g;|k0O@b zLjo*GJ0NC8_2^RT4a6@lebb-gKR;N0$v-LBy(4{NSP$l8Quv>ZTVFxnt;3NsW{Xsj zd_p_`fX*q3wxB$zu3js}Lfxs{a^9KoS6wZiV11w#y25(zJ-ZRAK&`bx+0LmLBepvd zJ{C4H__uZK!LS6OGp9PfQY zK>}xC7kc&r_r5QuP_4ZQZI9_^cp#M|c^)mL=(FGf%LEN-koLIwvyuqdysh<}(gqeM zV@YyFGe_^1T=wG`FUu%g&B7Nq(MvrZz_V4;)n2ao8q3SNd%K50gkGD<0Q=~P5wwE9 zIZZ19$#xz_r%lET|89W08&U!>IqUHLa*u`bDSz=;f1CWm(7b-P#Kz$-DNjxpm`Oi9 ziKz*c^g*(%lDG9aoq2v#lz=o}t2gG)1KH#x)xlEr>y@S)mf@kZu|VPrNqR}mmR6JB ze|bf0%UH9Nx~i5g1QX9Hvv29}HVJsUm{_t#qLwRTAABj10r^hcXRlR-#t=}1(c5|j zF1EJo%eB{@zRnSSJ})p_7^C~NEQ(M(xg*PPRx5HZzLNIo^VMDS`4X3}F1ZUrmUG|) zd-`H+mp4}}N*t3}aCKZUIJa@gWA*>C(=kx1-Pvy8mFB{{c@=muN#v_)Qo}Nd-Q+!k zu5m~Iv0Z(pQaFLTSJS7O7nvaCvoF!4WZ))*e6Y*rN_f_FW}DYy&kVe|E=x+?I6WG2 zP)CcduP6q4miIZkvIgjKiGU?c13FE*=c0HtlI%y+0!a=w)aienN|;$oP)iNRq{j}GTWEmm?!mS3Gt+1qAZ9m#&-Vs(0pPW^}S0d zKM1i;&>R}g;C{d(QW(N3$1+>Q>YJPs)&DnSC~{)Da>tRAk1IgJ`2c_d6aXN46i5F& zGLS=w5mWs41XGH{5WsVkW--*ax?@BB&yN5A?4P0|l7SMGeKDEi|D_Om$8bbok8vAe Jb_^!M@^5PmGdlnP delta 1791 zcmYk7XHXMp5QYo$uK#QY%t}*jj>kVE_OC3~)i= zAyj@B)ViCtiDLZ@eoPLRs41M}@sWW*WgOQ3Su2gktG+?ZyxdO zfG9#VVkHKvyUKRq;cS56>25y;T@%92hBy230dM`D1@CJ~SyiaAnd7RuqWQ9i!5vrF~tIHW4?h?>YFiFT*3&_ZmaGwIT1Cn|51HNsiurG6!ArVrIad?(u6P zJT9pEP2k0&KZmj_YJBxoJp#NA#L>>%b;_+YCBRp0F2(O5_3E0}z?=&O<85~;v8YFL zj0cMimCdw8~CAmWhPSubrc0rZtM+Bt4g$C(X!Xbmf$dD+5CkCn^iA zIiSQZ{HW~_VFRjMoptamm=m^@Cm3(+oqRjPZz>FxZ_ST2G_C4Qn~|d6U`Ba!_QN~2 zl3C|3{l+}rl#}hq8d%(B5&UcnG%`x_Ya#Yt~11C_?^-x!@Eu?q4Vvl zi+qNfZWzvHZ5gKx8fo$E;$a*3ZZb8ea(t&`P(j?AN`=1H5+u!x2K}|ZME<;aAk@CD zCyg+FP;t>oHwsE5PwC)FA_vfG{+v&_&XaAnyu|_!#G3IDYf9z@>UE0>Q?)VCpIp>S zwye6V*b^2zS5T*9T?LWzxT~NKI=Ln{0s#z5B~f%Q^*vonUt;7S#s53ON^zJCZMm0G zrP#%d47k2zJ6?cQByE$eSbdUTdqV>nu_#BKhDVc2;;NqUUvW+`UU=^aq&!Sc`kl@f z-Rka8*L%z{%MRYNPY116qd|9=h6=$)Bo+T=RqWYtqXKE{e1=Qm!K2y!eYJ@4;J#qf z3*r;b!_#-EVG7H^um1Ej^%&I*Ltqi*5e@O$p!IKNo*UM*oa~Be!#N!A*MB&4gR8m5 zHz^1L0J0?saEKhPs8;j7h5F^EXr@Eew;BrcM9m+X=gTd3ms$1v@?;7|#d_)VC9$pc z=sN=}MQ{t7jvv`ewO=?HFcq5Oab`3Y60h;h&${JKNm+)OfP7#H*C7|lz+g6@7CJ(X zX{IoGqRv712zT>^BRMbSwOz`a^@P=O;Sb|=8_$ug4@tPFtVESq%mt>UPYn9Q&JfAT zvxiq=__C{j^}e!mi^?(hD#QUA6fg$nf7js?AwLlNK#QZBXH$QIRr+bLDQUG_8DlrT z!tJ`9&e7|UekauJxU8=z?HNCLMlCtVJ77$`uU%tyR(F^Cm(R^ktIF%PAw%!C zQHu`PoCzGMQQGEM8T%no%#G+*3`U)?TSoR za~Y@8H4c^%nS%P9HjKsA(|YPjqCbfEdfTGRYrKm`*#Jg?cTVH^pYKvqxv`kx-v$#5 zzQwjjzOC9 zW03mP1$`Y9d@4OG!gQ+;`;Kv-uWm6gN&}xhh}c)dy8yM;=;k~vs6Ufy3BY1EyGhLty!d_#wedB{kn_u!AlHQgzB_%^Vy z0v-l#v~LrwCC)Y+?Q+-i?5d4!sx|892nQ~o>{;svAr=-BDQLwE5TXh;hTLlvG1oj&1T^j(G~#=L$ci5cIeYxVcOjp>WB+%A z;4Z{E+?-~@*ML7rs1lhwjG4zVKjVa`(XS&zkcH{W9%@zKR;Va^6aXkx0RTjQoTL9< z9B_#c3RC#CfeBAwVn9_w15D*--47lAwGjXi{IBQ-<|nw^gmELP>%or diff --git a/assets/templates/intlib_container.IntLib b/assets/templates/intlib_container.IntLib new file mode 100644 index 0000000000000000000000000000000000000000..2cb35c5965acbaeac62f7d21e07a398d9825f380 GIT binary patch literal 23040 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 lb`G}_L`#E*) + PCBLib/0.pcblib 0x02 + zlib() + LibCrossRef.Txt symbol -> footprint cross-reference index (length-prefixed strings) + Parameters .bin the Components-panel search index (parameter string) + Version.Txt constant \x00 + uint32(2) + +We don't hand-roll a compound-file writer: we take a known-good single-component .IntLib as a +**container template** and swap in this part's five stream contents, then rebuild the OLE with the +same directory tree (reusing schlib_write.read_container / rebuild, already proven to emit +Altium-valid OLEs). The symbol->footprint linkage lives inside the embedded .SchLib itself +(RECORD=45 model link), so the compiled library resolves the footprint with no external PcbLib. +""" +import argparse, json, os, re, struct, sys, zlib +import olefile +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)))) +from schlib_write import read_container + +FREESECT = 0xFFFFFFFF; ENDOFCHAIN = 0xFFFFFFFE; FATSECT = 0xFFFFFFFD +SEC = 512; MINI = 64; CUTOFF = 4096 + + +def _pad(b, n): + return b + b"\x00" * ((-len(b)) % n) + + +def write_cfb(entries, content): + """Serialise an OLE/CFB compound file from raw 128-byte directory entries + {sid: bytes}, + reproducing Altium's on-disk SECTOR ORDER: [FAT][directory][miniFAT][mini-stream][big streams]. + (schlib_write.rebuild is spec-valid but places the FAT last; Altium's IntLib reader is strict + about big streams and expects the conventional FAT-first layout, so we match it here.) + Streams < 4096 B live in the mini-stream; >= 4096 B get their own FAT sectors.""" + minis = [sid for sid, c in content.items() if len(c) < CUTOFF] + bigs = [sid for sid, c in content.items() if len(c) >= CUTOFF] + + # mini-stream + mini-FAT + ministream = b""; mini_start = {} + for sid in minis: + mini_start[sid] = len(ministream) // MINI + ministream += _pad(content[sid], MINI) + n_mini = len(ministream) // MINI + minifat = [ENDOFCHAIN] * n_mini + for sid in minis: + st = mini_start[sid]; cnt = (len(content[sid]) + MINI - 1) // MINI or 1 + for k in range(cnt - 1): + minifat[st + k] = st + k + 1 + minifat_bytes = _pad(b"".join(struct.pack(" n_fat * SEC: + n_fat += 1 + total = n_fat + non_fat + if n_fat > 109: + sys.exit("too many FAT sectors for a header-only DIFAT") + + # sector assignment — FAT first, matching Altium + fat_secs = list(range(0, n_fat)); cur = n_fat + dir_secs = list(range(cur, cur + n_dir)); cur += n_dir + mf_secs = list(range(cur, cur + n_mf)); cur += n_mf + ms_secs = list(range(cur, cur + n_ms)); cur += n_ms + big_secs = {} + for sid in bigs: + big_secs[sid] = list(range(cur, cur + big_k[sid])); cur += big_k[sid] + + # patch directory entries (start sector + 64-bit size) + def setentry(e, start, size): + e[116:120] = struct.pack(" 254: + raise ValueError(f"string too long for single-byte length prefix: {s[:40]!r}...") + return struct.pack("