In the shell:
scripts/vendor.sh <path-to-cigraph-working-copy>By default, cigraph is assumed to be in ../../../igraph.
There is a CI/CD workflow that runs this very frequently.
Currently, there are only two files generated by Stimulus.
They are version-controlled and depend on various input files.
The logic is governed by the Makefile-cigraph file.
To update them, do the following steps:
make -f Makefile-cigraph src/rinterface.c R/aaa-auto.RStimulus uses type definition files to generate R-to-C interface code:
tools/stimulus/types-RR.yaml: Defines R-level type conversions and argument validation for the R wrapper layer. All arguments in autogenerated*_impl()functions are validated here before being passed to C.tools/stimulus/types-RC.yaml: Defines C-level type conversions between R's SEXP objects and igraph C types.
When adding support for new types (e.g., GRAPH_PTR_LIST, MATRIX_LIST):
- Add the type definition to
types-RR.yamlwithINCONVfor R-level validation - Add the type definition to
types-RC.yamlwithINCONV/OUTCONVfor C conversion - Regenerate the interface files with the make command above
The Stimulus code generator is vendored into this repository using git subrepo, a tool that allows you to include an external git repository as a subdirectory within your main repository.
Unlike git submodules, subrepos are easier to work with because they don't require special checkout commands; the code is directly present in the repository.
This approach gives us the flexibility to test local modifications to Stimulus while still being able to pull upstream updates cleanly.
If you don't have git subrepo installed yet:
# On macOS with Homebrew
brew install git-subrepo
# Or install from source
git clone https://github.com/ingydotnet/git-subrepo /path/to/git-subrepo
echo 'source /path/to/git-subrepo/.rc' >> ~/.zshrcTo pull the latest changes from the upstream Stimulus repository:
# Pull the latest from the main branch
git subrepo pull tools/py-stimulus
# Or pull a specific branch
git subrepo pull tools/py-stimulus -b branch-name
# Or pull a specific commit or tag
git subrepo pull tools/py-stimulus -b v0.21.5This command will:
- Fetch changes from the upstream Stimulus repository
- Merge them into the
tools/py-stimulus/directory - Create a merge commit with metadata in
.gitrepo - Update the commit hash in
tools/py-stimulus/.gitrepo
After pulling, test the changes:
# Clean the virtualenv to ensure fresh installation
rm -rf .venv
# Regenerate the interface files
make -f Makefile-cigraph src/rinterface.c R/aaa-auto.RIf you need to test local changes to Stimulus:
-
Make your changes directly in
tools/py-stimulus/:cd tools/py-stimulus # Edit files as needed vim src/stimulus/some_file.py cd ../..
-
Test your changes:
# Remove the virtualenv to force reinstallation rm -rf .venv # Regenerate with your modified Stimulus make -f Makefile-cigraph src/rinterface.c R/aaa-auto.R
-
Commit everything together:
git add tools/py-stimulus/ src/rinterface.c R/aaa-auto.R git commit -m "feat: update Stimulus and regenerated interface files"
The modified Stimulus code and generated files will be committed together, making it easy to see what Stimulus changes caused which interface changes.
If your Stimulus modifications should be contributed back to the upstream repository:
# Push your changes to the upstream Stimulus repository
git subrepo push tools/py-stimulus
# Or push to a specific branch (useful for PRs)
git subrepo push tools/py-stimulus -b feature-branch-nameThis will create commits in the upstream Stimulus repository based on the commits you made in tools/py-stimulus/.
To see information about the subrepo:
git subrepo status tools/py-stimulusThis shows the remote URL, branch, and commit hashes.
When a public function's signature is migrated by inserting ... and renaming
arguments, pre-existing calls would otherwise break: the old positional (or
partially-named) values land in ... and hit check_dots_empty(). To soften
that, the migrated function carries a small generated block in its body that
recovers the legacy call — by position or by (partial) name — and emits one
soft-deprecation.
The block is generated in place, not written by hand:
tools/migrations.Ris the single source of truth. Each entry declares the function'soldandnewsignatures as literal R functions; renames and defaults are read straight off their formals (a bare-symbol default inold, e.g.c = c_renamed, means a rename). The schema is documented at the top of that file.tools/generate-migrations.Rreads the registry and rewrites the code between the# BEGIN GENERATED ARG_HANDLE: <fn>/# END GENERATED ARG_HANDLEmarkers inside each function. Output is idempotent (running twice produces no diff) and laid out exactly asairformats it, so the host files stay clean.
Regenerate after editing the registry:
Rscript tools/generate-migrations.RThis usually happens for you: a testthat helper
(tests/testthat/helper-migrations.R)
regenerates the blocks whenever the registry is newer, and CI re-runs the
generator (in custom/after-install) and fails if the committed code has
drifted. Do not edit between the markers by hand.
A migrated function gets the marker pair at the top of its body; the generator fills it in:
as_biadjacency_matrix <- function(graph, types, ..., weights = NULL,
attr = lifecycle::deprecated(),
names = TRUE, sparse = FALSE) {
# BEGIN GENERATED ARG_HANDLE: as_biadjacency_matrix
# ... generated: recover dots, reassign weights/attr/..., deprecate_soft() ...
# END GENERATED ARG_HANDLE
# ... rest of the body, using the (possibly reassigned) locals
}The recovery runs inline, so there is no handler function and no .user_env
plumbing. The generated lifecycle::deprecate_soft() sits directly in the
function body, so its default user_env (caller_env(2)) already resolves to
the user's frame: real user calls warn, while genuinely internal igraph callers
stay correctly silent — the point of a soft deprecation. The matching itself is
delegated to a small hand-written helper, migrate_recover_args()
(R/migrate-args.R) — a plain, debuggable function that
takes the per-function maps and returns the recovered values plus message parts
(or NULL); the generated block just supplies the configuration and assigns the
results.
Positional recovery only covers old slots whose order is preserved. To reorder
an argument, or to drop a positional slot without breaking old calls, place
the affected argument after ... in the new signature. Arguments past the
ellipsis are keyword-only and are recovered by (partial) name rather than by
position, so they side-step the position math entirely.
The one case the generator cannot rescue is a positional slot that is removed and whose old position is reused by a different surviving argument — a legacy positional call would then rebind to the wrong slot. No migration planned for 3.0.0 does this (they are pure renames).
For detailed information on implementing R wrappers for callback functions, see AGENTS.md in this directory.