Some components, library snippets and example instruments shipped with McCode
are maintained in their own repositories. Rather than copying them into this
tree (where they drift) or hiding the fetch inside a bespoke CMake module
(where nobody finds it), each such contribution declares itself with a small
JSON manifest named <something>.ext, placed in the directory the files are
populated into.
So a developer browsing mcstas-comps/contrib/ for a component finds either
the .comp itself or an .ext file naming the upstream repository, the
release it is pinned to, and the SHA256 of every file taken from it. The
record lives where you would look for the thing it describes.
The CMake side is cmake/Modules/External.cmake. The manifests are written
and kept honest by mcext and the GitHub Action around it, both in
mccode-dev/external-registrar;
.github/workflows/external-contributions.yml runs them here.
mcstas-chopper-lib contributes files to three destinations, so it has a
manifest in each:
| Manifest | Populates |
|---|---|
mcstas-comps/share/chopper-lib.ext |
chopper-lib.c, chopper-lib.h — %include-d into generated instrument C, like the other *-lib.c snippets beside it |
mcstas-comps/contrib/chopper-lib.ext |
NXdisk_chopper.comp, Masked_ESS_butterfly.comp |
mcstas-comps/examples/Tests_optics/<Instrument>/<Instrument>.ext |
that one example instrument |
Manifests are not installed: the install( DIRECTORY ... ) rules that cover
those directories carry PATTERN "*.ext" EXCLUDE.
mcstas-comps/libs/cif2hkl/cif2hkl.ext (McXtrace: a symlink to it) pins the
SOLEIL cif2hkl
Fortran sources. Its mccode_install_externals() call has no DESTINATION,
so the files are only staged, never installed; OUTPUT_VARIABLE hands them
to add_executable( cif2hkl ... ). cif2hkl lives on GitLab, which the poll
job below does not query, so move it to a new release by hand with
mcext update mcstas-comps/libs/cif2hkl/cif2hkl.ext -v <tag> (and bump the
cif2hkl >= conda and Debian pins to match).
Short form — a JSON array of fully self-describing file entries:
[
{ "name": "chopper-lib.h",
"url": "https://raw.githubusercontent.com/<owner>/<repo>/<tag>/chopper-lib.h",
"sha256": "0965f6660850acce12d665d7a57064389ccb570e218f21c1dbad9155ee7d8ef9" }
]Long form — an object of contribution-wide defaults plus a files array. Any
key understood in a file entry may also appear at the top level, where it
applies to every entry that does not override it:
{
"name": "mcstas-chopper-lib",
"description": "Chopper opening/closing times and (inverse velocity, time) masks.",
"homepage": "https://github.com/<owner>/<repo>",
"git": "https://github.com/<owner>/<repo>.git",
"version": "<tag>",
"base": "https://raw.githubusercontent.com/<owner>/<repo>/<tag>/",
"files": [
{ "name": "chopper-lib.h", "sha256": "0965f666..." },
{ "name": "chopper-lib.c", "sha256": "d914a307..." }
]
}Prefer the long form: it is the one that records what the contribution is, not just where some bytes live.
For a real, current manifest see
mcstas-comps/share/chopper-lib.ext.
| Key | |
|---|---|
name |
required — file name upstream, and the default installed name |
sha256 |
required — SHA256 of the contents |
as |
install path relative to the manifest's own directory; may name a subdirectory, e.g. "Test_Foo/Test_Foo.instr". Default: name |
url |
explicit download URL for this one file |
from |
path of the file inside the release archive. Default: name |
base |
URL prefix; the file is fetched from <base><from> |
git |
upstream repository — informational, but see below |
version |
upstream tag/ref |
| Key | |
|---|---|
files |
required in the long form |
archive |
{ "url", "sha256", "strip" } — a release tarball or zip, downloaded and unpacked once and shared across every manifest that names the same URL. strip leading path components are dropped; the default of 1 matches GitHub's generated source archives |
license, description, homepage |
recorded in configure output |
A single file resolves through url, then base, then archive; failing all
three, a github.com git plus a version is enough to derive a raw base
URL. Mixing is fine — a manifest may take most of its files from a release
tarball and one from a direct URL.
The hash is the point. A pinned tag says what should be fetched; the SHA256 is what makes a build fail loudly when the bytes behind that tag change — a moved tag, a regenerated release archive, a compromised host. Without it, "pinned to v4.1.0" is a statement of intent rather than a fact.
This matters more than usual for GitHub's auto-generated source archives
(/archive/refs/tags/*.tar.gz), which are produced on demand and have changed
byte-for-byte across GitHub compression changes in the past. A release asset
that the maintainer uploads is stable and is the better thing to point
archive.url at where one exists.
A manifest may omit sha256 only if the build is configured with
-DMCCODE_EXTERNALS_ALLOW_UNVERIFIED=ON, which exists for bisecting an
upstream problem and should not appear in a manifest's normal life.
.github/workflows/external-contributions.yml runs once a day, and on demand
from the Actions tab:
- poll looks up the latest release of every repository a manifest's
gitnames. When it finds one newer than McCode records, it opens a pull request fromexternal/OWNER-REPOthat moves every manifest for that repository to the new tag, with each hash computed from the files as published. A repository can shape those manifests by keeping templates in a.mccode/directory laid out like this tree; see the external-registrar README. Closing such a PR unmerged declines that release; the next release is proposed as usual. - check re-verifies every recorded hash against upstream and fails on any mismatch: a moved tag, a regenerated release archive, a compromised host.
The poll job needs the registrar GitHub App (REGISTRAR_CLIENT_ID and
REGISTRAR_PRIVATE_KEY) and is skipped until it is configured; check needs no
credentials.
mcext needs only Python 3.9+. Without installing anything:
pipx run --spec https://github.com/mccode-dev/external-registrar/archive/refs/tags/v1.tar.gz mcext --helpor pip install https://github.com/mccode-dev/external-registrar/archive/refs/tags/v1.tar.gz to
put mcext on your path. The steps below assume the latter.
- Create the directory the files belong in, if it does not exist.
- Write the manifest there with
name/git/version/base(orarchive) and the list of files, leaving the hashes out. - Fill them in:
mcext update path/to/thing.ext - Add a
mccode_install_externals()call for the containing directory if one does not already exist — seemcstas-comps/CMakeLists.txt, where each call mirrors theinstall( DIRECTORY ... )rule for the same directory.
Once that is merged, the poll job keeps the contribution current. To move to a release by hand instead, e.g. one the poll would not choose:
mcext update mcstas-comps/contrib/chopper-lib.ext -v v4.2.1which repoints every recorded URL at the new tag and recomputes every hash.
mcext check [PATH ...] runs the same verification as the check job, over
the current directory by default.
| Option | Default | |
|---|---|---|
ENABLE_EXTERNALS |
ON |
populate external contributions at all |
MCCODE_EXTERNALS_CACHE |
<build>/externals-cache |
content-addressed download cache; point it somewhere persistent to survive a wiped build directory |
MCCODE_EXTERNALS_LOCAL |
(empty) | directory of pre-fetched files, searched by name before any download and still hash-verified |
MCCODE_EXTERNALS_OFFLINE |
OFF |
never download: any file not in the cache or MCCODE_EXTERNALS_LOCAL is a hard error rather than a silent fetch |
MCCODE_EXTERNALS_ALLOW_UNVERIFIED |
OFF |
accept entries with no sha256 |
MCCODE_EXTERNALS_TIMEOUT |
60 |
per-file download timeout, seconds |
Distribution packagers who may not reach the network during a build have two
routes: pre-populate MCCODE_EXTERNALS_CACHE, or drop the files into
MCCODE_EXTERNALS_LOCAL. Either way, set MCCODE_EXTERNALS_OFFLINE=ON so a
gap in the provided set fails the configure instead of quietly downloading.
The manifest machinery uses string( JSON ) and file( ARCHIVE_EXTRACT ),
which is why McCode's cmake_minimum_required is 3.19. The cmake_policy( VERSION 3.17.0 ) pins in the tools subdirectories were deliberately left
alone: raising the floor should not quietly change policy behaviour anywhere
it was explicitly fixed.