Publishing
The package is called sel-lang on every registry. sel was already taken
in most of them: an npm CSS-selector library, a projects/sel entry in
quicklisp-projects (GrammaTech's Software Evolution Library), and a sel project
on PyPI.
| Registry | Name | Manifest |
|---|---|---|
| PyPI | sel-lang |
pyproject.toml |
| npm | sel-lang |
package.json |
| Packagist | nathanjel/sel-lang |
composer.json |
| Quicklisp / Ultralisp | sel-lang |
lisp/sel-lang.asd |
| Conan | sel-lang |
cpp/conanfile.py |
| vcpkg | sel-lang |
cpp/vcpkg.json |
Packagist requires a vendor prefix, so nathanjel/ is unavoidable there. The
repository itself is nathanjel/sel; only the published package is sel-lang.
Nothing about publishing changes how the project is used without a package manager: copying a directory still works, and that remains the primary story in the README.
Before any release
tools/check-generated.sh # artifacts freshly generated
tools/check.sh # ALL GREEN, full roster
SEL_IMPLS="$SEL_IMPLS python-wheel" tools/check.sh # and through the built wheel
tools/oracle-db.sh # the map, against real servers
tools/oracle-db.sh run python3 tools/mutate-sql.py # every mutation, none skipped
tools/check-version.sh 0.8.1 # every manifest agrees
tools/check-package-docs.sh # user docs only, in every package
tools/check-usage.sh # the SQL examples, every host, real serversOnly the user documentation ships. The pages a reader of the docs is sent
to — docs/README.md, the language pages (overview, parity, syntax,
operators, functions), sql.md, extending.md, everything in
docs/usage/, and the generated docs/reference/ — go out with every package;
the contributor guide (docs/contributing.md), the design documents in
docs/internals/, the site's assets and build files, this file and CLAUDE.md
do not. Three configurations carry that list — files in package.json, the sdist
include in pyproject.toml, and .gitattributes' export-ignore, which is
what git archive and so Packagist's dist and GitHub's tag tarballs honour — and
each lists the user documents file by file, so a new note in docs/ stays out
until someone lets it in. tools/check-package-docs.sh (a gate layer) holds all
three to one list; a new user document is added to all four places. Quicklisp and
Ultralisp clone the repository, so they alone carry everything.
The first command above is what makes the rest of this document possible. The SEL→SQL map is
authored once, in sql/dialects/*.json, and rendered by tools/gen-sql-map.mjs
into each host's own source language — MapData.php, _map.py, _map.mjs,
sel_sql_map_data.cpp. Those renderings are committed and published, so a C++,
PHP or Python user never installs Node to get a working library; the price is
that a release cut from a tree where one of them is stale ships hosts that
quietly disagree about the map. check-generated.sh refuses that, names the
artifact, and prints the command to fix it. Unlike check-sql-map.sh it does
not skip when Node is absent — it falls back to timestamps, because a release
machine without Node is precisely where the mistake would otherwise pass.
tools/check.sh must print ALL GREEN with every implementation present — a partial
roster is refused rather than quietly passing, because every differential layer
degrades to a no-op when there is nothing to compare against.
Then tag. Every registry below either reads the tag or is told the version by hand, and they must agree:
git tag -a v0.8.1 -m "SEL 0.8.1"
git push origin v0.8.1Never re-tag or move an existing tag. Upstream registries forbid republishing under an existing version: Packagist blocks re-tagged releases with Upstream re-tag blocked — Packagist may no longer match the VCS repo for this version, while npm and PyPI permanently refuse file uploads for already-published versions. If a defect or correction is needed after pushing a tag, always bump to the next patch version.
Versions live in six manifests. Keep them in step:
package.json "version": "0.8.1"
pyproject.toml version = "0.8.1"
cpp/conanfile.py version = "0.8.1"
cpp/vcpkg.json "version-semver": "0.8.1"
cpp/CMakeLists.txt project(... VERSION 0.8.1 ...)
lisp/sel-lang.asd :version "0.8.1"python/sel/__init__.py carries __version__, CHANGELOG.md's top heading
carries the version being released, and composer.json carries
extra.branch-alias.dev-main (0.8.x-dev). All are checked against the release
version by tools/check-version.sh, so they are places fewer to remember rather
than more — and a release whose notes or branch alias were never updated fails
the check before the tag is cut.
composer.json deliberately carries no version field — Packagist infers
release versions from git tags, and hard-coding it there is a known way to
publish a lie.
PyPI
rm -rf dist/python/*
python3 -m build --outdir dist/python
python3 -m twine check dist/python/*
python3 -m twine upload dist/python/*Build into dist/python, not dist. The repository root's dist/ already
holds the JavaScript bundle; letting build write beside it would mix two
languages' artefacts in one directory and eventually upload the wrong thing.
The wheel ships python/sel/ and nothing else — the package, its py.typed
marker and the licence, about 50 kB. The sdist adds the user documents,
spec/, conformance/ and the Python examples, mirroring what npm's files
whitelists.
The package has no runtime dependencies, and that is a property worth
keeping: the regex subset is small enough that re covers it after the anchor
rewrite, and the decimal core is deliberately hand-written (see below). Python
and JavaScript are the only two hosts with neither a vendored engine nor an
external one.
Verify the built package rather than the source tree, which is what the
python-wheel implementation in tools/impls.sh is for:
python3 -m venv python/.venv-wheel
python/.venv-wheel/bin/pip install dist/python/*.whl
SEL_IMPLS="python-wheel" tools/check.shThat runs the whole conformance suite, the API probes and the fuzzer through the
installed package. It is the only layer that catches a packaging mistake — a
sub-package left out of the wheel, a missing py.typed, an entry point that
names a module the wheel does not contain — because every other layer imports
from python/.
Two things to know
The sel console script collides with npm's. Both packages install a command
called sel. There is no good way around it and no attempt is made to hide it:
python -m sel always works and is the spelling to prefer on a machine that has
both. It is the same trade as the SEL package name in Common Lisp — an unlikely
collision, made loud rather than silent.
python/sel/decimal.py does not use the decimal module, and must not start
to. tools/decimal-oracle.py generates this project's decimal test cases from
decimal, as an independent third opinion on cores that were all written from
one spec by one hand. A host built on decimal would turn tools/check-decimal.sh
into a comparison of the standard library with itself — still printing
"0 mismatches", while verifying nothing at all for that host.
For automated releases, PyPI's Trusted Publishing (OIDC from a CI workflow)
removes the need for a long-lived token; a manual twine upload with an API
token is equally fine and is what the commands above assume.
npm
npm pack --dry-run # inspect the file list first
npm publish --access publicfiles in package.json whitelists what ships: js/, the user documents,
spec/, the JS examples, the licence and the README. The PHP, C++ and Lisp
trees are excluded, so the tarball is ~515 kB rather than the whole repository.
The package is ESM-only ("type": "module") and exposes one entry point plus the
sel CLI:
import { compile, evaluate, Value, SelError } from 'sel-lang';sideEffects lists js/src/builtins/*.mjs, because those modules register
themselves in the function table and a bundler that tree-shook them would leave
you with a language that has no functions in it.
Packagist
Submit the GitHub URL once at https://packagist.org/packages/submit, then add the GitHub webhook so subsequent tags publish themselves.
Autoloading is a single files entry pointing at php/src/bootstrap.php, not
PSR-4. That is deliberate: the function table must be complete before any source
is parsed, because an unknown function name is a compile-time error, and PSR-4
would only load a class at the moment it is first mentioned. bootstrap.php uses
require_once throughout, so loading it twice is harmless.
Verify before publishing:
composer validateBranch alias (dev-main)
Packagist infers release versions from git tags, but it reads extra.branch-alias.dev-main in composer.json to determine what dev-main represents. On every minor release series bump, update this alias to match:
"extra": {
"branch-alias": {
"dev-main": "0.8.x-dev"
}
}tools/check-version.sh enforces this against the release series, so an outdated branch alias fails the version check before tagging.
Tag immutability
Never delete, move, or re-tag an existing release tag. Packagist explicitly tracks tag commit hashes and flags moved tags with:
Upstream re-tag blocked — Packagist may no longer match the VCS repo for this version
Once a tag is pushed, it must be treated as immutable. If any fix or correction is needed post-release, cut a new patch release (e.g. 0.8.2) rather than moving v0.8.1.
Quicklisp and Ultralisp
The ASDF system is sel-lang, defined in lisp/sel-lang.asd. ASDF requires the
file name to match the primary system name, which is why the file was renamed.
Ultralisp is the quicker of the two: add the repository at
https://ultralisp.org/, and it scans for .asd files itself — including in
subdirectories, so lisp/sel-lang.asd is found without moving anything.
Quicklisp needs a pull request against
https://github.com/quicklisp/quicklisp-projects adding projects/sel-lang/source.txt:
git https://github.com/nathanjel/sel.gitReleases are cut monthly, so expect a wait.
One thing to know
The ASDF system is sel-lang but the Common Lisp package is still SEL, so
the API reads sel:evaluate. If you ever load this alongside GrammaTech's
Software Evolution Library in one image, the package names may collide. Renaming
the package would change every call site in the public API; it has not been done
because the collision is unlikely and loud rather than silent.
A note for C++ consumers upgrading to 0.3.0
sel::Value became a handle: copying one now aliases, and clone() is the deep
copy. Every signature in sel.hpp is unchanged, so this compiles silently — the
break is behavioural, not a build error, which is the awkward kind.
Value b = a; // 0.2.0: an independent deep copy
// 0.3.0: the same value as a
Value b = a.clone(); // an independent deep copy, both versionsThe interpreter needed this to agree with the other four hosts (spec/SPEC.md
§3.4), and it makes copies cheap. Code that builds each value fresh and moves it
into place — the idiom cpp/bin/e2e.cpp already uses — needs no change at all.
Conan
Conan Center does not have SRELL, so the vendored copy is what makes the recipe build at all. See the note below.
Conan needs a profile before its first use, or it refuses with "The default build profile doesn't exist" — that is Conan asking to be initialised, not a problem with the recipe:
conan profile detect # once, per machine
conan create cpp/ --build=missingconan create also builds and runs cpp/test_package/, which links the
packaged library through the exported CMake target and nothing else, so a
recipe that produces an unusable package fails there rather than downstream.
The recipe does not gate on the consumer's compiler.cppstd. A stock
conan profile detect yields gnu20, and the library reaches C++23 through
target_compile_features in CMakeLists.txt, so refusing to build on the default
profile would only make the package unusable out of the box.
To publish, either upload to your own remote:
conan upload sel-lang/0.8.1 -r <remote> --confirmor open a pull request against
https://github.com/conan-io/conan-center-index adding recipes/sel-lang/.
Conan Center requires the recipe to fetch sources from a release URL rather than
carry them, so a Center submission needs the recipe reworked around
conan.tools.files.get() pointing at the GitHub tarball for the tag.
vcpkg
cpp/vcpkg.json is a manifest, usable immediately in overlay-port form. For the
public registry, open a pull request against
https://github.com/microsoft/vcpkg adding ports/sel-lang/ with a
portfile.cmake that calls vcpkg_from_github, vcpkg_cmake_configure,
vcpkg_cmake_install and vcpkg_cmake_config_fixup(PACKAGE_NAME sel-lang).
SRELL: vendored, with an opt-out
The C++ implementation needs an ECMAScript-conformant regex engine, because that
is what makes it agree with the JavaScript host. It vendors SRELL, pinned to
release 2026.05 at commit 7bf06e58…, under cpp/third_party/srell/.
Where each package manager stands:
| SRELL available? | what SEL does | |
|---|---|---|
| vcpkg | yes, srell at exactly 2026.05 |
vendored by default; system-srell feature links vcpkg's |
| Conan | no such package | vendored, no alternative |
| plain CMake / copy the files | n/a | vendored |
The vendored copy is the default everywhere, on purpose. It is what keeps "copy
sel.hpp, sel_ast.hpp, sel.cpp and third_party/srell/ and compile" true
— and, with sel_sql*.{hpp,cpp} added, the same for the SQL layer — it is the only
option for Conan, and it removes any chance of a resolver quietly selecting a
different engine version — which would not be a build difference, it would be a
language difference, since the regex engine decides what a rule matches.
To link an external SRELL instead:
cmake -S cpp -B build -DSEL_USE_SYSTEM_SRELL=ON # needs find_package(srell)
vcpkg install sel-lang[system-srell]cpp/vcpkg.json pins srell to 2026.05 in overrides so the feature cannot
silently drift to another release. Either way, run tools/check.sh: the
regex cases in conformance/09-regex.selt are what actually decide whether a
given SRELL still agrees with the other three implementations.