Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
^src/*\.gcda$
^src/*\.gcno$
^R/release\.R$
^R/roxygen-cdocs\.R$
^tests/testthat/test-roxygen-cdocs\.R$
^scripts$
^\.claude$
^\.series-source$
Expand Down
12 changes: 0 additions & 12 deletions .github/workflows/custom/after-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,6 @@ runs:
run: scripts/warnings.sh glue
shell: bash

# The roclet that links the C manual from the reference pages, named in
# DESCRIPTION's Roxygen field, is vendored in tools/igraph.r2cdocs
# (handbook/architecture/r-layer/documentation/). Only the smoke test
# roxygenizes, and only it installs the roclet's dependencies
# (Config/Needs/roxygen2). scripts/rcc-one.sh installs it the same way.
- name: Install the vendored C-docs roclet and run its tests
if: github.job == 'rcc-smoke'
run: |
R CMD INSTALL tools/igraph.r2cdocs
Rscript -e 'testthat::test_local("tools/igraph.r2cdocs", reporter = "summary")'
shell: bash

# Guard for the flavor rename (scripts/flavor-package-name.R): fails when
# the package name is hard-coded where scripts/flavor.patch does not rewrite
# it, when a file the patch renames still carries the mainline name, when a
Expand Down
4 changes: 0 additions & 4 deletions .handbook-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ em-dash: .claude/
src/vendor/cigraph/
tools/py-stimulus/

# The C-docs roclet subrepo is igraph/igraph.r2cdocs, with its own README;
# handbook/architecture/r-layer/documentation/ says how this repository uses it.
tools/igraph.r2cdocs/

# Generated documents cannot carry a backreference; the generator does.
# NEWS.md is fledge's, the snapshots testthat's, and revdep/ holds reports
# revdepcheck writes and the notes kept beside them for one release.
Expand Down
9 changes: 4 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,12 +232,11 @@ under `tools/migrations/`.

### Documentation

- Install roxygen2 and the roclet that links the C docs, which is vendored in `tools/igraph.r2cdocs`
- Install roxygen2 and what the roclet that links the C docs needs, which is defined in `R/roxygen-cdocs.R`
([documentation](/handbook/architecture/r-layer/documentation/README.md) in the handbook):

```r
pak::pak(dependencies = "Config/Needs/roxygen2")
pak::local_install("tools/igraph.r2cdocs", upgrade = FALSE)
```

- Please document your new functions using
Expand All @@ -255,10 +254,10 @@ can check roxygen prose for stray lowercase forms with:
Rscript tools/check-id-casing.R
```

### Maintaining igraph.r2cdocs
### Maintaining the C-docs roclet

The roclet is a git subrepo in `tools/igraph.r2cdocs`, and a fix to it belongs upstream as well.
The link table for a new release of the C library is added
The roclet is `R/roxygen-cdocs.R`, left out of the built package, with its tests in `tests/testthat/test-roxygen-cdocs.R`.
The link table for a new release of the C library is downloaded and cached on first use,
as the handbook's [documentation](/handbook/architecture/r-layer/documentation/README.md) page says.

### Test Cases
Expand Down
14 changes: 8 additions & 6 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,20 @@ Config/comment/gha/extra-packages: graph is an optional Enhances
Config/Needs/build: devtools, irlba, pkgconfig
Config/Needs/coverage: covr
Config/Needs/roxygen2: r-lib/roxygen2, moodymudskipper/devtag,
treesitter, treesitter.r
Config/comment/Needs/roxygen2: The roclet igraph.r2cdocs::docs_rd that
the Roxygen field names is vendored in tools/igraph.r2cdocs and
installed from there; treesitter and treesitter.r are the dependencies
it adds. See handbook/architecture/r-layer/documentation/.
treesitter, treesitter.r, xml2
Config/comment/Needs/roxygen2: The roclet igraph:::docs_rd that the
Roxygen field names is defined in R/roxygen-cdocs.R, which
.Rbuildignore leaves out of the package; treesitter, treesitter.r and
xml2 are what it needs, and roxygen2 from GitHub finds a roclet
defined in the package it documents. See
handbook/architecture/r-layer/documentation/.
Config/Needs/website: here, readr, tibble, xmlparsedata, xml2
Config/testthat/edition: 3
Config/testthat/parallel: true
Config/testthat/start-first: aaa-*, vs-es, scan, vs-operators, weakref,
watts.strogatz.game
Encoding: UTF-8
Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace",
"igraph.r2cdocs::docs_rd", "devtag::dev_roclet"))
"igraph:::docs_rd", "devtag::dev_roclet"))
SystemRequirements: libxml2 (optional), glpk (>= 4.57, optional)
Config/roxygen2/version: 8.1.1.9000
114 changes: 85 additions & 29 deletions tools/igraph.r2cdocs/R/roxygen.R → R/roxygen-cdocs.R
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
#' @importFrom roxygen2 roclet
#' @export
# The roclet that links the C manual from the reference pages.
# The `Roxygen` field of `DESCRIPTION` names it as `docs_rd` of this package,
# in place of roxygen2's `rd`;
# roxygen2 finds it after loading the package code, so it needs no install.
# This file is `.Rbuildignore`d: the roclet serves documenting only,
# and its dependencies are in `Config/Needs/roxygen2` rather than `Imports`.
# See handbook/architecture/r-layer/documentation/.

docs_rd <- function() {
roclet(c("docs_rd", "rd"))
}

#' @importFrom roxygen2 roclet_process
#' @export

roclet_process.roclet_docs_rd <- function(x, blocks, env, base_path) {
NextMethod()
# Registered here rather than in `NAMESPACE`,
# which the built package, without this file, could not honor.
registerS3method(
"roclet_output",
"roclet_docs_rd",
roclet_output.roclet_docs_rd,
envir = asNamespace("roxygen2")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder if we could do something simpler

)
roxygen2::roclet(c("docs_rd", "rd"))
}

#' @importFrom roxygen2 roclet_output
#' @export

roclet_output.roclet_docs_rd <- function(x, results, base_path, ...) {
# Built once per run rather than memoised across runs,
# where a second run in the same session would get the call graph
Expand Down Expand Up @@ -206,7 +209,7 @@ r_call_graph <- function(base_path) {
# as the function names (`vertices`) and the callees of each one
# (`callees`, sorted vertex indices).
# Vertices are numbered in order of first appearance, callers first,
# as igraph::graph_from_data_frame() numbers them.
# as graph_from_data_frame() numbers them.
call_graph <- function(from, to) {
vertices <- unique(c(from, to))
callees <- split(
Expand All @@ -220,7 +223,7 @@ call_graph <- function(from, to) {
}

# The functions reachable from `start` through calls, `start` included,
# in the order igraph::subcomponent(mode = "out") returns them:
# in the order subcomponent(mode = "out") returns them:
# breadth first, and the callees of each function by ascending index.
# Empty when `start` is not a vertex.
reachable_functions <- function(call_graph, start) {
Expand Down Expand Up @@ -264,17 +267,58 @@ c_docs_version <- function(version) {
sub("-[0-9]+-g[0-9a-f]+(-dirty)?$", "", version)
}

# The link tables this package carries, one per release of the C manual,
# as data-raw/clinks.R writes them, named by the release.
# The releases of the C library vendored at `base_path`,
# from the headings of its changelog, such as `## [0.10.17] - 2025-09-19`.
c_releases <- function(base_path) {
lines <- readLines(file.path(
base_path,
"src",
"vendor",
"cigraph",
"CHANGELOG.md"
))
headings <- regmatches(lines, regexec("^## \\[([0-9][^]]*)\\]", lines))
map_chr(compact(headings), \(x) x[[2]])
}

# The directory caching the link tables, one per release of the C manual.
# The tables describe the C library, so every flavor of this package shares them.
c_links_cache_dir <- function() {
file.path(tools::R_user_dir("igraph", "cache"), "clinks")
}

# The link tables in the cache, named by the release.
c_links_tables <- function() {
files <- dir(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused because I thought we had already switched to the user directory caching that this PR has.

system.file("clinks", package = "igraph.r2cdocs"),
pattern = "[.]csv$",
full.names = TRUE
)
files <- dir(c_links_cache_dir(), pattern = "[.]csv$", full.names = TRUE)
set_names(files, sub("[.]csv$", "", basename(files)))
}

# Downloads the link table for one release of the C manual into the cache,
# from the index igraph.org publishes for that release.
# The release tarball of the C library carries the same index
# as `doc/html/ix01.html`, which `index_url` can name instead.
c_links_download <- function(
version,
index_url = sprintf("https://igraph.org/c/html/%s/ix01.html", version)
) {
index <- xml2::read_html(index_url)
entries <- xml2::xml_find_all(index, ".//dt")
href <- map_chr(entries, \(dt) xml2::xml_attr(xml2::xml_child(dt), "href"))

clinks <- data.frame(
method = sub(".*#", "", href),
url = sprintf("https://igraph.org/c/html/%s/%s", version, href)
)

dir <- c_links_cache_dir()
dir.create(dir, recursive = TRUE, showWarnings = FALSE)
utils::write.csv(
clinks,
file.path(dir, paste0(version, ".csv")),
row.names = FALSE
)
}

# The release whose link table serves `version`:
# its own, else the newest release before it, else `NA`.
# Versions compare by their numeric part, so a pre-release comes after the
Expand All @@ -294,30 +338,42 @@ c_links_release <- function(version, releases) {
older[[order(core(older), is_release, decreasing = TRUE)[[1]]]]
}

# The link table for the version of the C library vendored at `base_path`,
# from the tables this package carries rather than from igraph.org,
# so that the links depend on the sources alone.
# The link table for the version of the C library vendored at `base_path`.
# The table of its release is downloaded once and cached;
# the links depend on the sources and that cache alone.
c_links <- function(base_path) {
engine <- igraph_c_version(base_path)
version <- c_docs_version(engine)

# The newest release of the C library documenting `version`,
# as the changelog vendored with it lists the releases.
target <- c_links_release(version, c_releases(base_path))
if (!is.na(target) && !target %in% names(c_links_tables())) {
tryCatch(
c_links_download(target),
error = function(e) NULL
)
}

tables <- c_links_tables()
release <- c_links_release(version, names(tables))

if (is.na(release)) {
cli::cli_abort(c(
"No link table for the C manual of igraph {version} or an earlier release.",
i = "Add one with {.file data-raw/clinks.R} in {.pkg igraph.r2cdocs}."
i = "Could not download it from igraph.org; check the network access."
))
}

if (release != version) {
# The links stay those of the newest release that has a table.
# The links stay those of the newest release in the cache.
# A development snapshot past that release is the normal state of a
# vendoring series; a release without a table is a table to add.
# vendoring series, whose manual is not published yet;
# a release whose manual cannot be downloaded is worth a warning.
notify <- if (engine != version) cli::cli_inform else cli::cli_warn
notify(c(
"No link table for the C manual of igraph {version}, linking the manual of {release}.",
i = "Add one with {.file data-raw/clinks.R} in {.pkg igraph.r2cdocs} once that manual is published."
i = "It is downloaded from igraph.org once that manual is published."
))
}

Expand Down
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ ignore:
- "src/vendor/io"
- "src/vendor/simpleraytracer"
- "src/vendor/uuid"
# The C-docs roclet serves documenting only, and does not ship.
- "R/roxygen-cdocs.R"
33 changes: 12 additions & 21 deletions handbook/architecture/r-layer/documentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,28 @@ How the reference pages are written: roxygen2 with Markdown syntax, regenerated
`Rscript tools/check-id-casing.R` finds violations.

**Links to the C manual are generated, not written.**
The roclet `igraph.r2cdocs::docs_rd` runs in place of roxygen2's `rd`, as the `Roxygen` field of `DESCRIPTION` says.
The roclet `igraph:::docs_rd` runs in place of roxygen2's `rd`, as the `Roxygen` field of `DESCRIPTION` says.
It follows each topic's aliases through the call graph of `R/` to the `*_impl()` functions they reach,
and adds a section "Related documentation in the C library" that links the C function behind each.
It reads the call graph from the sources, so documenting needs no installed igraph.
Its name does not change under a flavor ([`branches/flavors/`](/handbook/branches/flavors/README.md)).
Its name follows the flavor, which `scripts/flavor.patch` rewrites in `DESCRIPTION` ([`branches/flavors/`](/handbook/branches/flavors/README.md)).

**The roclet is vendored.**
`tools/igraph.r2cdocs/` is a git subrepo of [igraph/igraph.r2cdocs](https://github.com/igraph/igraph.r2cdocs),
as `tools/py-stimulus/` is of Stimulus ([`architecture/stimulus/`](/handbook/architecture/stimulus/README.md)).
Its `.gitrepo` records the upstream commit, and a fix made there belongs upstream as well.
It is a package of its own, installed before documenting and again whenever it changes:
**The roclet is part of the sources, not of the package.**
It is defined in `R/roxygen-cdocs.R` and tested in `tests/testthat/test-roxygen-cdocs.R`,
both left out of the built package by `.Rbuildignore`, and the first left out of coverage by `codecov.yml`.
roxygen2 from GitHub finds a roclet defined in the package it documents, after loading its code,
so documenting needs only the dependencies in `Config/Needs/roxygen2`:

```r
pak::pak(dependencies = "Config/Needs/roxygen2")
pak::local_install("tools/igraph.r2cdocs", upgrade = FALSE)
```

CI installs it and runs its tests in `.github/workflows/custom/after-install`, in the `rcc-smoke` job that roxygenizes.
The `roxygen` gate of `scripts/rcc-one.sh` installs it for every commit of a series.

**The links follow the vendored C library, without a network.**
**The links follow the vendored C library.**
The roclet reads `IGRAPH_VERSION` from `src/vendor/igraph_version.h` and links the manual of the release that documents it.
A development snapshot carries the version `git describe` gives its commit, such as `0.10.17-1-g16081ce60`, and links the manual of its tag.
The roclet carries one link table per release, under its `inst/clinks/`.
A version without one links the newest release before it that has one, with a message for a snapshot and a warning for a release.
So the links in `man/` change only with the commit that adds a table.
`data-raw/clinks.R` writes one from the index igraph.org publishes for the release, or from `doc/html/ix01.html` in its release tarball:

```sh
cd tools/igraph.r2cdocs
Rscript data-raw/clinks.R 1.0.1
```
The release is the newest that the vendored `CHANGELOG.md` lists, up to that version.
Its link table is read from the index igraph.org publishes for the release,
once, into `tools::R_user_dir("igraph", "cache")/clinks/<release>.csv`.
Where the download fails, the newest cached release before it serves, with a message for a snapshot and a warning for a release.

*To deepen: document the devtag convention for internal functions once it settles.*
1 change: 0 additions & 1 deletion handbook/meta/local/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ The status of the vendoring series lives in its refs and the per-commit verdicts
**What this repository does not author.**
The vendored C library under `src/vendor/cigraph/` ([`architecture/engine/`](/handbook/architecture/engine/README.md)),
the Stimulus subrepo under `tools/py-stimulus/`,
the C-docs roclet subrepo under `tools/igraph.r2cdocs/` ([`architecture/r-layer/documentation/`](/handbook/architecture/r-layer/documentation/README.md)),
the generated R/C interface `R/aaa-*.R` and `src/rinterface.c` ([`architecture/stimulus/`](/handbook/architecture/stimulus/README.md)),
`src/cpp11.cpp` and `R/cpp11.R`, `NEWS.md` (fledge), the `man/` pages (roxygen2), the snapshots under `tests/testthat/_snaps/`,
and the reports under `revdep/`.
Expand Down
2 changes: 1 addition & 1 deletion igraph.Rproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ LineEndingConversion: Posix
BuildType: Package
PackageUseDevtools: Yes
PackageInstallArgs: --no-multiarch --with-keep.source
PackageRoxygenize: collate,namespace,igraph.r2cdocs::docs_rd,devtag::dev_roclet
PackageRoxygenize: collate,namespace,igraph:::docs_rd,devtag::dev_roclet
5 changes: 4 additions & 1 deletion scripts/flavor-package-name.R
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ flavor_legitimate_spellings <- c(
'Rf_install[(]"igraph"[)]',
# An instruction to install the mainline package from CRAN, which is what
# the vignettes give: it names that package, not this one.
"install[.]packages[(][\"']igraph[\"'][)]"
"install[.]packages[(][\"']igraph[\"'][)]",
# The cache of the C manual's link tables (R/roxygen-cdocs.R), which
# describe the C library and so serve every flavor.
'R_user_dir[(]"igraph", "cache"[)]'
)

# The files to scan, as paths relative to `root`, each named by the pattern that
Expand Down
5 changes: 5 additions & 0 deletions scripts/flavor.patch
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ index 98169e0..8a75ced 100644
-Package: igraph
+Package: igraph.1.3
Title: Network Analysis and Visualization
@@ -99,3 +99,3 @@ Encoding: UTF-8
Roxygen: list(markdown = TRUE, roclets = c("collate", "namespace",
- "igraph:::docs_rd", "devtag::dev_roclet"))
+ "igraph.1.3:::docs_rd", "devtag::dev_roclet"))
SystemRequirements: libxml2 (optional), glpk (>= 4.57, optional)
diff --git a/NAMESPACE b/NAMESPACE
index 7831331..abf11dd 100644
--- a/NAMESPACE
Expand Down
12 changes: 3 additions & 9 deletions scripts/rcc-one.sh
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ stage_budget() {
flavor) echo 300 ;; # a base-R scan of the checkout
style) echo 600 ;;
snapshots) echo 1800 ;; # the snapshot tests: ~350 s
roxygen) echo 900 ;; # ~60 s, after installing the vendored roclet
roxygen) echo 900 ;; # ~60 s, the first run downloading the C docs index
clean) echo 900 ;;
warnings) echo 600 ;; # recompiles the ten glue files: ~10 s
check) echo 2700 ;; # rebuilds the package, then the suite again: ~400 s
Expand Down Expand Up @@ -568,15 +568,9 @@ EOF
return "${rc}"
}

# Mirrors cynkra/cynkratemplate's roxygenize action, after installing the
# C-docs roclet vendored in tools/igraph.r2cdocs as
# .github/workflows/custom/after-install does for `rcc-smoke`. Installed per
# commit, so each commit is documented by its own copy of the roclet; its
# dependencies come with Config/Needs/roxygen2.
# Mirrors cynkra/cynkratemplate's roxygenize action. The C-docs roclet is
# R/roxygen-cdocs.R, so each commit is documented by its own copy of it.
gate_roxygen() {
if [ -d tools/igraph.r2cdocs ]; then
R CMD INSTALL tools/igraph.r2cdocs || return 1
fi
rscript <<'EOF'
roxygen2::roxygenize()
EOF
Expand Down
Loading
Loading