Project, source & scripts

These sections describe your project’s identity, control which files ubCode processes, and let you define runnable scripts. Adjust them when you need to set your project name or version, change file-discovery patterns, or add custom build / validation commands.

Minimal example

[project]
name = "My Docs"

[source]
dir = "docs"            # root directory for source files
extend_exclude = ["_build/**"]

[scripts]
build = "sphinx-build -b html docs docs/_build/html"

project

Basic metadata about your documentation project.

[project]
name = "My Project"
version = "1.0.0"
description = "My project description"
root_doc = "index"
name

Type: string (optional)

The project name, displayed in various UI elements.

In the ubc build html site it is the sidebar heading — unless [build.html].title is set, which takes precedence there.

version

Type: string (optional)

The project version.

description

Type: string (optional)

A short description of the project.

root_doc

Type: string (optional, default: "index")

The document at the root of the table-of-contents tree, given as its docname — the source path relative to the source directory, without the file extension. Toctree expansion and the document ordering start from this document. An unset or empty value means "index".

srcdir

Type: string (optional, default: config directory)

Deprecated since version 0.33.0: Use [source] dir instead. It names the same directory, beside the other source roots, and it keeps working unchanged until it is removed.

The directory the project’s own documents are discovered from, relative to this file. Unset or an empty string means the directory containing ubproject.toml.

One thing differs from [source] dir: the discovery globs stay anchored at the configuration file’s own directory rather than being matched under each source root, so a pattern such as docs/sub/skip.rst written for srcdir = "docs" keeps naming <config dir>/docs/sub/skip.rst. That is deliberate — an existing project changes nothing by upgrading — and it is what the deprecation warning’s per-glob advice is about.

When both keys are set, [source] dir wins and this one is ignored. Carrying both is the mixed-version bridge during a rollout: an older ubCode does not know dir and reads this key, a newer one reads dir and warns. The bridge is only clean for patterns that name no path, since the two versions anchor path-naming patterns differently.

Two diagnostics carry the migration, and each can be silenced on its own via [lint] ignore:

config.srcdir_deprecated

This key is set and [source] dir is not. The message lists every [source] discovery glob you wrote, and every [parse.parsers.*] include that needs rewriting, with what migrating it requires: a pattern naming no file path needs no rewrite; a pattern under the source root is given its rewritten spelling; a pattern pointing outside it is named as one that can no longer match — and, unless it also lands outside this file’s own directory or is written from the filesystem root, is flagged as one that would anchor at the source root if carried across unchanged, which is often what it meant; and a pattern this cannot place relative to the source root at all — because a wildcard sits where the source root’s own path segments are, or it uses a backslash, or a .. in it cancels an earlier wildcard — is reported as one to review by hand rather than guessed at. When the source root is this file’s own directory (srcdir = "."), nothing needs rewriting and the message says so.

config.source_dir_superseded

Both keys are set. [source] dir wins and this key is ignored entirely.

needs_json

Configuration for the Needs Index tree view in VS Code, which displays needs from a pre-built needs.json file.

[needs_json]
path = "relative/path/to/needs.json"
src = "."
path

Type: string (required)

Path to the needs.json file. Resolved relative to the directory containing ubproject.toml.

src

Type: string | null (default: null)

Path to the source directory, used to resolve docname values to file paths for click-to-source navigation. When null, source locations are not resolved.

source

The [source] section determines which files ubCode processes. Use it to include additional file types, exclude build artifacts, or control how the file walker traverses your project.

Minimal example

[source]
respect_gitignore = true
extend_exclude = ["temp/**", "*.tmp"]

Core options

dir

Type: string (optional, default: config directory)

Added in version 0.33.0.

The directory the project’s own documents are discovered from — the project’s source root, and rank 0 of the same ordered list of roots that mounts extend. Relative to the ubproject.toml file; an absolute path, or one climbing out with .., is accepted too. Unset or an empty string ("") means the directory containing the configuration file, which is the default layout and changes nothing.

Only files under a declared root are discovered. A source file elsewhere in the configuration folder produces no page and cannot be referenced by document name; it is reported as std.file_outside_roots so it does not go unnoticed.

Setting it also re-anchors every discovery glob at each source root[source] include/exclude and each [parse.parsers.*] include. One written pattern then means the same thing under this directory and under every mount root: extend_exclude = ["internal/**"] excludes <dir>/internal/ and <mount>/internal/ alike.

It supersedes the deprecated [project] srcdir, which anchors those globs at the configuration file’s own directory instead. A project migrating from srcdir to dir therefore rewrites its path-naming patterns, and the deprecation warning computes that rewrite per pattern.

[source]
dir = "docs"

A root outside the configuration folder

dir may point anywhere, including out of the configuration folder (dir = "../sources"), which is what makes it a real root rather than a subdirectory selector — mount roots may already live anywhere.

One editor limitation applies to such a layout, and it applies to mounted trees for the same reason. The language server finds a file’s project by walking up from the file’s own directory looking for ubproject.toml, so a file under a root outside the configuration folder is never matched with the project that claims it, and project-wide editor features do not see it (opening it still gives full syntax, formatting and parse diagnostics). The escape hatch is a ubproject.redirect.toml beside the file, or in one of its ancestors, pointing at the real configuration.

respect_gitignore

Type: boolean (default: true)

When enabled, automatically excludes files that are ignored by:

  • .gitignore files

  • .git/info/exclude

  • Global gitignore configuration

  • .ignore files (used by tools like ripgrep)

follow_links

Type: boolean (default: false)

When enabled, symbolic links will be followed when traversing source directories. This allows you to include files or directories that are symlinked from other locations in your filesystem.

Warning

Enabling this option may lead to infinite recursion if there are circular symbolic links. Use with caution and ensure your directory structure does not contain symlink loops.

[source]
follow_links = true
include

Type: array (default: ["*.rst"])

Base list of glob patterns for files to include. By default, only reStructuredText files are processed.

Setting it replaces the default; use extend_include to add to it.

Note

When one or more parsers are declared under [parse.parsers.*], file discovery is derived from the parsers’ own include globs and this option is ignored. See Parsers and file routing.

exclude

Type: array (default: comprehensive list)

Base list of glob patterns for files and directories to exclude. The default list includes common directories that typically don’t contain source documentation:

exclude = [
    ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite",
    ".hg", ".svn", ".venv", ".vscode", "_build", "build",
    "dist", "node_modules", "site-packages"
]

Setting it replaces that hygiene list rather than adding to it, so a project that sets exclude starts walking, routing, parsing and indexing node_modules and the rest. Use extend_exclude to add a pattern and keep the defaults.

extend_exclude

Type: array (default: [])

Additional patterns to add to the exclude list without replacing the defaults.

Across an extend chain these accumulate: a child configuration’s entries are added to its parent’s rather than replacing them, so a shared base configuration’s additions survive a project that adds one of its own. exclude itself replaces.

extend_include

Type: array (default: [])

Additional patterns to add to the include list without replacing the defaults.

Across an extend chain these accumulate, exactly as extend_exclude does. include itself replaces.

Glob pattern types

Single-path patterns

Match anywhere in the directory tree:

  • directory — matches any directory named “directory”

  • foo.rst — matches any file named “foo.rst”

  • foo_*.rst — matches files like “foo_test.rst”, “foo_doc.rst”

Relative patterns

Match specific paths relative to a source root[source] dir and every mount root, each in turn. That is the whole model in one sentence: globs anchor at each root, so one written pattern means one thing.

  • ./foo.rst — matches “foo.rst” at the root of each source tree only

  • directory/foo.rst — matches “foo.rst” in “directory” under each root

  • directory/*.rst — matches every RST file under “directory” at any depth

  • docs/**/*.rst — matches every RST file under “docs” at any depth

Note

* crosses / in these patterns, so directory/*.rst is recursive — it is not limited to files directly inside “directory”. ** is therefore not what makes a pattern recursive here, which differs from Sphinx’s exclude_patterns.

A project still on the deprecated [project] srcdir is the exception, and unchanged: its relative patterns anchor at the directory containing ubproject.toml and nowhere else.

Pattern precedence

Exclude patterns take precedence over include patterns. If a file matches both an include and exclude pattern, it will be excluded.

Tip

To see which patterns are active: ubc config

To see all discovered files: ubc build list-documents

Common configurations

Multi-format documentation:

[source]
extend_include = ["*.md", "*.txt"]
extend_exclude = ["README.md", "CHANGELOG.md"]

Note

In classic mode the included *.md / *.txt files are parsed as reStructuredText. To parse Markdown as Markdown (MyST), configure a Markdown parser under [parse.parsers.md] instead — see Parsers and file routing.

Excluding build artifacts:

[source]
extend_exclude = [
    "build/**",
    "_build/**",
    "*.tmp",
    "temp/**"
]

Including only specific directories:

[source]
include = ["docs/**/*.rst", "specifications/**/*.rst"]
exclude = ["**"]  # Exclude everything else

See also

For detailed glob syntax documentation, see the globset documentation.

Variant sources: per-variant file selection

Added in version 0.33.0.

[[source.variant_sources]] decides which files are part of the build for the current build variant. Each rule pairs a condition (if) with a set of globs (files); every rule whose condition is false removes its files from the build, and a file that no false rule matches is unaffected.

[needs.variant_data]
edition = "basic"

[[source.variant_sources]]
if = "var.edition == 'pro'"
files = ["reference/pro/**/*.rst"]

The files globs use the same dialect and the same base as exclude above — including its “a pattern with no path separator matches by file name” behaviour, so files = ["internal.rst"] gates every internal.rst in the project and in every mounted tree. {a,b} alternation and patterns that climb out of the project with .. are refused.

Rules only ever narrow the discovered set, and their order does not matter. A removed file is not read at all: no page, no document name, no needs.

5. Selecting whole files with variant_sources is the full guide, including the condition grammar, the toctree consequences, and the current sphinx-build limitation.

Mounts: extra source trees

Added in version 0.33.0.

[[source.mounts]] folds an extra source tree into the project’s document set under a document-name prefix. A generated or vendored tree can then be part of the site without being copied into the project’s own source directory:

[source]
dir = "docs"

[[source.mounts]]
dir = "../bazel-bin/docs/api"
mount_at = "_generated/api"
gitignore = false
attach_to = "index"

Each entry is walked separately from the project’s own tree, with its own include / exclude / gitignore settings, and its files become documents named <mount_at>/<path below the root>.

The table is shared with sphinx-mounts, so one ubproject.toml can drive both ubCode and a sphinx-build run. Keys ubCode does not model are reported as configuration diagnostics naming the ubCode equivalent, and otherwise ignored, rather than failing the load.

Two modes

Exactly one of dir and files is required. An entry naming neither, or both, fails the load — it does not describe a tree at all, so there is no defensible way to carry on.

dir

Type: string (relative to the configuration file)

A directory to walk. Structure below the root is preserved: <dir>/sub/page.rst with mount_at = "api" becomes the document api/sub/page.

files

Type: array (relative to the configuration file)

An explicit list of files. This is a flat namespace: each file is named by its BASENAME and the directory it lives in is discarded, so ../pool/a/page.rst with mount_at = "api" becomes api/page regardless of the a/ in its path.

There is no walk, so include and exclude filter nothing here — the list is the selection. Setting them is reported as config.mount_dead_option.

mount_at — the document-name prefix

mount_at

Type: string (default: unset)

A relative document name: no leading /, no . or .. segment, no empty segment, no backslash, and no leading or trailing whitespace. A trailing / is ignored.

Unset (or "") mounts at the document root, where a mounted file can take the name of a host document. That is supported — it is how sphinx-mounts spells a root mount, by omitting the key — but it is reported (config.mount_at_root), because every name the mount contributes then competes with the host’s own.

A mount_at that is not a relative document name is reported (config.mount_invalid) and the mount is dropped. ubCode has no configuration-time equivalent of sphinx-build -W, and a dropped mount is visible in ubc check — the same posture [intersphinx] takes for an entry it cannot use.

Name collisions: the host wins

Two files can claim one document name — most often a mounted file and a host file, or two mounts. Exactly one of them becomes the document, chosen by:

  1. the host source directory, ahead of every mount;

  2. then the earliest declared mount;

  3. then the lowest path within one root.

The others are reported (std.duplicate_docname) and contribute nothing: no page, no targets, no needs, no table-of-contents entries. The message names the mount each losing file came from, as (mounts[0]), so the configuration block to change is identifiable. Every claimant is shown relative to the folder holding ubproject.toml, climbing out of it with .. where the file lives outside — never as an absolute path, and never stripped down to a bare file name that two listed files could share. The one exception is a claimant on a different Windows drive from ubproject.toml: no relative path reaches across drives, so such a file is shown by its file name alone.

Note

ubCode drops the one FILE, where sphinx-mounts skips the whole mount. Dropping one file with a named report beats removing a 200-file bundle over one name — but be aware of the trade: the mount’s own references to the dropped document then report as missing, so one line of configuration can produce several unrelated-looking reports. The collision message says so, which is what makes the cause findable from any one of them.

Walk settings

include

Type: array (default: the project’s own [source] include)

Glob patterns selecting which files under the mount are used. As for the host tree, this is only read in “classic” mode: when parsers are declared under [parse.parsers.*] they own file inclusion for mounted trees too.

Give a mount its own patterns. The project’s include is anchored at the project, so an inherited pattern that names a path — docs/**/*.rst rather than *.rst — can never match a file under a mount root outside the project, and the mount then yields no documents at all. This is the same trap as gitignore below, from the other direction.

exclude

Type: array (default: [])

Glob patterns excluding files under the mount. The project’s own exclude list still applies as well — that is where the hygiene entries live (.git, node_modules, _build, …), and a mount walk without them would descend into a vendored node_modules inside the bundle.

gitignore

Type: boolean (default: true)

Whether to respect .gitignore files while walking this mount.

Set it to false for a generated tree that a .gitignore deliberately hides — a bazel-bin/ output directory is the usual case. With true (the default) such a tree is walked under the host’s ignore rules and yields nothing at all, which is a mount that silently contributes no documents.

An absent mount root

A declared root that is not on disk is reported and skipped (config.mount_missing), not fatal: the point of mounting a generated tree is that ubc build still works before the generator has run. A files mount reports per entry, so a half-present list mounts the files that are there. A root that is there but is the wrong kind — dir naming a file, or a directory listed under files — is reported as such rather than as missing, since the fix is to repoint the key rather than to wait for a generator.

The pages an absent root contributed on an earlier run are kept rather than reported as deleted, so a build that cannot see the bundle does not empty the indexes of it. They are replaced as soon as the root returns, and disappear entirely when the mount is removed from the configuration. Only the pages that root produced are kept: a file the host source directory or an earlier mount claims belongs to that root, and its deletion is reported and swept as any other would be. Where two roots overlap, that makes retention declaration-order sensitive — the same two roots and the same deleted file are kept or swept depending on which mount is declared first, because the earlier declaration is the one that claims the file.

Because the report is a warning, [build.html] deny = "warning" turns a missing mount into a failed build — which is usually what continuous integration wants, while a developer keeps a working local build.

Wiring a mount into a table of contents

A mount’s documents are reachable only through references written by hand unless attach_to names a host document to wire them into.

attach_to

Type: string (default: unset)

A host document name whose table of contents gains an entry for this mount. Unset attaches nothing.

If the document does not exist, or the mount produced no document to attach, that is reported against the configuration (config.mount_attach) — never as a missing-document warning against the host page, which would point at a line nobody wrote.

An entry is injected only for a document this mount produced. A document of the requested name that came from another root — the host’s own tree, or an earlier mount that won the name — is reported the same way and not attached, because wiring it in would present another root’s page as this mount’s entry point.

toctree_index

Type: integer (default: 0)

Which of attach_to’s toctree directives receives the entry, counting from 0 in document order. A host document with no toctree at all gains one. An index past the end of the ones it has is reported and nothing is wired, rather than quietly falling back to the first.

entry_doc

Type: string (default: "index")

The mount-relative document attach_to points at. For a dir mount this is normally the bundle’s own index page, whose toctree then carries the rest of the tree — only this one document is attached automatically.

attach_each

Type: boolean (default: false)

Attach every listed file instead of just entry_doc, in files order. For loose files with no index page to stitch them together.

Only the listed files this mount produced are attached. If a listed file is claimed by a higher-precedence root it keeps that root’s document name and is left where it is; if that is true of every listed file, the mount attaches nothing and says so (config.mount_attach).

It applies to a files mount with an attach_to. On a dir mount, without attach_to, or beside a non-default entry_doc it is reported (config.mount_dead_option) and ignored — in the last case attach_each wins, since it attaches every listed file including the one entry_doc named.

if — gate a whole bundle per variant

if

Type: string (default: unset)

Added in version 0.33.0.

A condition deciding whether this mount is part of the build. Unset, the mount always is.

The grammar is the one [[source.variant_sources]] uses for its own if, enforced by the same validator — comparisons, in / not in, is None / is not None, .startswith(…) / .endswith(…), and / or / not, parentheses, nested var.* access, and the Python-spelled literals True and False. Every field must be rooted at var, and the condition must evaluate to a boolean.

Where a rule narrows a file set by glob, this removes a whole bundle:

[needs.variant_data]
edition = "basic"

[[source.mounts]]
dir = "../bundles/reference-pro"
mount_at = "reference/pro"
attach_to = "index"
if = "var.edition == 'pro'"      # gated off for edition = "basic"

A gated-off mount contributes no documents, wires nothing into its attach_to host, and reports config.mount_gated — once, and even when nothing in the project references the bundle. That last part is the point: a gated bundle may live in another repository, and without the report a build that lost four hundred pages would say nothing at all.

The whole mount is the unit, in both modes: this gates a files mount exactly as it gates a dir mount. Gating one file of a files mount is a different thing, and is supported by neither key.

Every way the condition can fail to hold gates the mount off:

What happened

What you get

the condition is false

gated off; config.mount_gated

the condition is outside the grammar

gated off in every variant; config.mount_invalid_condition

the condition cannot be evaluated (an unknown var.* key)

gated off; config.mount_condition_unevaluable

gating it would remove [project] root_doc

the whole configuration is refused; config.mount_excludes_root (see the note below on when this can be decided)

Failing towards fewer documents is deliberate. The key exists to keep content out of a build, so publishing a gated bundle because a condition could not be read is the one outcome it must not have.

Note

The root-document refusal only fires where ubCode can prove, from the configuration alone, that the gated mount would have supplied the document: a files mount (where the list is the selection, so the answer is exact), or a dir mount with gitignore = false and no parser declared under [parse.parsers.*].

Everywhere else it stands down, because the answer belongs to the file walk rather than to the configuration: ignore files are the walker’s business (nested .gitignore files, .git/info/exclude, your global excludes), with parsers declared it is the parser router — not include — that decides which files are read, and a false [[source.variant_sources]] rule can remove the file in every variant, which is settled after this check runs. In those cases a gated mount that really was the only source of your root document produces the ordinary toctree.missing_root_document warning instead of a refusal, and a project with no toctree at all gets no report.

That is deliberate: a refusal stops the build outright and cannot be suppressed, so it is reserved for the cases ubCode is sure about.

A toctree entry naming a document a gated mount would have supplied reports toctree.variant_excluded (informational) and names the mount, rather than toctree.nonexisting_document. There is one case where it does not, and it is worth knowing before writing two mounts at one prefix:

Warning

If a gated mount would have supplied a document name the build already uses, that mount is not credited with any of the documents it removed — including the ones nothing else claims. References to those report toctree.nonexisting_document, and config.mount_gated_contested names the contested document and what claims it.

A contest has two sources, and the remedy differs:

  • another mount — two bundles sharing one mount_at with mutually exclusive conditions, where both have an index. Give them distinct mount_at values.

  • the host’s own tree — the mount’s prefix leads to a document name the project’s source directory already provides. Give the mount a mount_at prefix the host tree does not use, or rename the host document.

Attribution is all-or-nothing per mount on purpose: crediting the gate with a name that is built would turn a genuine broken reference into an informational note.

sphinx-build reads this key only through sphinx-mounts, at a release that supports it; without that it builds the bundle anyway. Declaring the key therefore always reports config.variant_sources_sphinx_unsupported. See the variants guide.

Mounted trees should be self-contained

A reference from a mounted document to a file outside its own mount is reported as build.mount_path_escape: it pulls a file this project does not own into the build, and — for an image or a download — copies it into the output, where it can collide with a file of the same name.

The boundary is the mount dir, or for a files mount the set of the listed files’ own directories — each listed file’s whole directory, siblings included, and no wider. A leading / in a reference means “relative to the source directory”, which is normally outside the mount, since a mounted tree normally lives outside the source directory.

To relax the check for one tree, suppress the code for it by path:

[lint.per-file-ignores]
"generated/**" = ["build.mount_path_escape"]

See the linting reference for the whole suppression model.

What mounts do not do yet

Six limitations are worth knowing before adopting mounts.

A mount is gated as a whole, never file by file. if removes the entire bundle, and a [[source.variant_sources]] rule glob reaches a dir mount’s walk but never a files mount’s list — which has no walk to filter. So there is no way to gate one file of a files mount for one variant, in ubCode or in sphinx-mounts. Split the bundle, or give the file a mount of its own.

Only the project build reads them. ubc check and ubc format discover files by walking for the nearest configuration file, and that walk reads [source] alone — so mounted trees are invisible to those two commands.

Editor features are degraded for a file opened from a mounted tree. The language server finds a file’s project by walking UP from the file, so a file opened directly out of a mounted tree finds no ubproject.toml and gets no document name, no hover, no references and no needs context. The zero-configuration workaround is a ubproject.redirect.toml inside the mounted tree pointing at the host project. That is right for a generated tree you own and wrong for a read-only sibling repository.

A mounted tree regenerated in the background is not noticed. The editor watches the workspace folders, so a mount outside them fires no change event. An in-editor save or a window reload picks the new state up.

Mount roots are resolved lexically, never canonicalised. . and .. are resolved and the path is made absolute, but symbolic links are deliberately left alone — the flagship mount is a generated tree reached through a symlink, so resolving them would break the case the feature exists for.

A preview of a host page renders an injected entry without its text. The preview matches navigation groups to the directive they came from by source line, and an entry this feature injected has no line in the host document. The built site is unaffected.

scripts

Register custom scripts to run from VS Code or the command line. In VS Code they can be accessed via the command palette (Ctrl+Shift+P) by selecting “ubCode: Run Script in Terminal”.

Each key is a script, and each value is the configuration for that script. Normally the value is an object with different keys — the most important being cmd which holds the command to execute. If only cmd is set, the object notation is optional:

[scripts]
sphinx1 = "sphinx-build -b html . _build/html {{filepath}}"
sphinx2 = { cmd = "sphinx-build -b html . _build/html {{filepath}}", env = { SPHINXOPTS = "-W" }, terminal = "name", jinja = true }

Additionally, the chain key can be used in place of cmd to run multiple commands in sequence:

[scripts]
rm_build = "rm -rf _build"
sphinx = "sphinx-build -b html . _build/html {{filepath}}"
"sphinx:clean" = { chain = ["rm_build", "sphinx"] }

The following keys are possible for a script:

  • cmd: The command to execute.

  • chain: A list of scripts to execute in sequence.

  • env: A map of environment variables to set.

  • terminal: What to name the terminal in VS Code. If not set, the terminal will be named after the script key. Terminal names are unique, so if a terminal with the same name already exists, it will be reused.

  • jinja: If True (the default), the command is treated as a jinja template, which can take the following variables:

    • {{ confdir }}: The directory containing the configuration file.

    • {{ filepath }}: The currently active file in VS Code

The current working directory for the script is set as the directory containing the configuration file.