Auditing the vendored Lexilla source
Note
Paths on this page are relative to packages/lexilla/, except
repo-level ones (docs/, tools/, .github/, examples/).
Note
src/lexilla_vendor/ is vendored unmodified from Lexilla's official
release. If you'd like to confirm that for yourself, here's one way --
use whatever approach you're comfortable with. Mirrors
Verifying vendored Scintilla
for its vendored src/scintilla/.
Lexilla 5.5.5 is extracted as-is in src/lexilla_vendor/.
| Field | Value |
|---|---|
| Upstream project | https://www.scintilla.org/Lexilla.html |
| Upstream version | 5.5.5 |
| Upstream tag | rel-5-5-5, commit e67537693bfb (per version.txt: 555) |
| Tarball/zip URL | https://www.scintilla.org/lexilla555.tgz |
| Tarball/zip SHA-256 | fa4093ad5c48acf49a6f3eca1b75143a5c824ef1a6f5b0ee59e8c3d6a970f229 |
| Vendored path | src/lexilla_vendor |
After verifying the checksum and extracting the tarball, a tree diff against
src/lexilla_vendor/ should come back empty:
tar -xzf lexilla555.tgz --strip-components=1 -C <some-dir>
diff -rq <some-dir> src/lexilla_vendor/
src/scintilla_interface/ — the two headers Lexilla needs but doesn't ship
Lexilla.h says "Must have already included ILexer.h to have
Scintilla::ILexer5 defined" — but ILexer.h is a Scintilla header, not a
Lexilla one, so it isn't part of the Lexilla tarball above. ILexer5
(the interface this binding wraps), ILexer4, IDocument, and the
Sci_Position/Sci_PositionU typedefs it uses are all declared there and in
the Sci_Position.h it includes. Separately, the individual lexer
implementations under src/lexilla_vendor/lexers/ #include "Scintilla.h"
for a handful of fold-level flag constants and INVALID_POSITION
(SC_FOLDLEVELBASE, SC_FOLDLEVELWHITEFLAG, SC_FOLDLEVELHEADERFLAG,
SC_FOLDLEVELNUMBERMASK, SC_FOLDLEVELBOXFOOTERFLAG) — not, as it might
seem, the per-language SCE_* style constants (those already live in
Lexilla's own vendored include/SciLexer.h, no extra vendoring needed).
Rather than depend on the whole Scintilla tree (which would reintroduce the
"this package has no Scintilla/Qt dependency" coupling the project
deliberately avoids -- see lexilla's mission),
only these three headers are copied, unmodified, into
src/scintilla_interface/include/ (with Scintilla's License.txt and
version.txt). They are pure interface/constant declarations (no
implementation, no Qt).
Where the copy comes from
Since the move into the monorepo, the copy is the Scintilla release the
repo vendors for pyside6-scintilla (packages/pyside6-scintilla/src/scintilla/,
currently 5.6.9 -- see Verifying vendored Scintilla for its
tarball and checksum), byte for byte. tools/sync_scintilla_headers.py
copies it, and its --check mode (in make lint and CI) fails if the copy
drifts. lexilla keeps a copy at all, rather than including the vendored tree,
only because its sdist must build on its own.
(Before the monorepo, lexilla-py vendored the headers from the Scintilla
release matching its Lexilla version's major.minor, 5.5.0. ILexer5,
Sci_Position and the fold-level constants are stable across those
releases; the 5.6 Scintilla.h only adds messages.)
| Field | Value |
|---|---|
| Source | packages/pyside6-scintilla/src/scintilla/ (the vendored Scintilla) |
| Version | per src/scintilla_interface/version.txt (569 = 5.6.9) |
| Files copied | include/ILexer.h, include/Sci_Position.h, include/Scintilla.h, License.txt, version.txt |
| Copy path | src/scintilla_interface |
To verify, from the repo root:
uv run python tools/sync_scintilla_headers.py --check
When either vendored release changes
- New Scintilla release vendored (for pyside6-scintilla): run
make sync-headersto refresh the copy. IfILexer5/ILexer4/IDocumentgained new virtual methods (rare, and typically called out in Scintilla's release notes),src/lexilla/bindings/_binding.cppwill need updating to match -- a binding that's missing newly-added pure-virtual methods won't link. - New Lexilla release vendored: check its release notes for a newer minimum Scintilla interface; if it needs one, that's a Scintilla upgrade first (step 1).
- Either way, re-run
uv sync --reinstall-package lexilla-- a mismatch between the interface and what_binding.cppassumes surfaces as a compile error, not a silent runtime bug, sinceILexer5is an abstract C++ interface. - Lexilla's lexers are also built against these headers; their test suite
(
uv run pytest packages/lexilla/tests) exercises lexing end to end. - Separately, whenever
src/lexilla_vendor/itself is re-vendored (a new Lexilla release, not just a Scintilla interface-header refresh), re-runmake lexilla-enums(see docs/bindings.md) and review/commit the diff toLanguageIdentifier(in_binding.cpp) andsrc/lexilla/_languages.py-- new Lexilla releases can add lexers (newLanguage/LanguageIdentifiermembers) or, in principle, newSCLEX_*values. Unlike step 2 above, a missed regeneration here does not fail at compile time (static_castto an enum always succeeds) -- it surfaces as a nanobind runtime error the first time.identifieris read on a lexer whoseSCLEX_*value isn't yet in the enum.
No constant values actually cross between lexilla and pyside6-scintilla
Scintilla.h also #defines ~1300 protocol constants (SCI_* messages,
SC_*/SCN_* flags and notification codes — e.g. SCI_SETILEXER 4033,
SC_FOLDLEVELBASE). It's tempting to assume these are a point of coupling
between lexilla and pyside6-scintilla, but neither package actually
passes any of these constants to the other:
- lexilla's
_binding.cppnever registers any of them as a Python attribute -- they're consumed only while compiling the vendored lexer.cxxfiles (forINVALID_POSITIONand theSC_FOLDLEVEL*flags) and never appear in thelexillapackage's Python surface. - pyside6-scintilla doesn't expose the legacy
#definemacros either. It binds Scintilla's modernenum class Message/FoldLevel/etc. (frominclude/ScintillaMessages.h) via shiboken instead, so Python seesScintilla.Message.SetILexer, not a bareSCI_SETILEXERint.
So there's no "did this constant come from lexilla or pyside6-scintilla"
ambiguity for a caller, because no constant is ever obtained from
lexilla in the first place -- a value common to both vendored copies of
the legacy Scintilla.h macros (byte-identical copies since the monorepo;
before it, verified identical for all 1277 names shared by the 5.5.0/5.6.3
versions each project vendored) is
purely an internal implementation detail of each package's own build, not
something either package's API surfaces or relies on the other to agree on.
What is shared: the ILexer5 interface shape and Sci_Position's type
The one thing that does cross the boundary between the two packages is the
ILexer5* pointer itself: lexilla's create_lexer() returns the
address of a C++ object, and pyside6-scintilla's engine later calls virtual
methods on that same address (Lex, Fold, PropertySet, ...) once you
hand it to editor.setILexer(lexer.pointer). For that call to be safe, both
sides must agree on:
- The exact shape of the
ILexer4/ILexer5vtable -- same virtual methods, in the same order, with the same signatures. This comes from each side's own vendored copy ofILexer.h. - The definition of
Sci_Position/Sci_PositionU/Sci_PositionCRused in those signatures (ptrdiff_t/size_t/longrespectively) -- not any particular value of that type, but the type itself, so aSci_Positionparameter is the same size and signedness on both sides of the call.
This is a structural/ABI agreement, not a value-sharing one -- and unlike
the Scintilla.h constants above, nothing here is exposed to Python on
either side; it only matters to the C++ compilers that built each package's
extension module.
Within one commit of this repo, this is enforced: both packages compile
against the same Scintilla headers -- pyside6-scintilla against its vendored
tree, lexilla against a byte-identical copy that tools/sync_scintilla_headers.py
--check guards in CI -- and lexilla's tests hand a lexer to the
pyside6-scintilla built from the same commit.
Across releases it still isn't: the two packages are published
separately, so a user can combine a lexilla built against one Scintilla's
headers with a pyside6-scintilla built against another's. If a future
Scintilla release adds a virtual method to ILexer4/ILexer5, handing
lexilla's pointer to pyside6-scintilla's setILexer() across those versions
would be undefined behavior (mismatched vtable layout), not a clean error.
So when a Scintilla upgrade changes ILexer.h's interfaces, release both
packages together, and state the matching pyside6-scintilla version in
lexilla's changelog (lexilla has no hard dependency on it -- its glue module
just imports it).