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 htmlsite 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 asdocs/sub/skip.rstwritten forsrcdir = "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] dirwins and this one is ignored. Carrying both is the mixed-version bridge during a rollout: an older ubCode does not knowdirand reads this key, a newer one readsdirand 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_deprecatedThis key is set and
[source] diris not. The message lists every[source]discovery glob you wrote, and every[parse.parsers.*]includethat 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_supersededBoth keys are set.
[source] dirwins 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.jsonfile. Resolved relative to the directory containingubproject.toml.- src
Type:
string | null(default:null)Path to the source directory, used to resolve
docnamevalues to file paths for click-to-source navigation. Whennull, 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.tomlfile; 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/excludeand 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
srcdirtodirtherefore rewrites its path-naming patterns, and the deprecation warning computes that rewrite per pattern.[source] dir = "docs"
A root outside the configuration folder
dirmay 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 aubproject.redirect.tomlbeside 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:
.gitignorefiles.git/info/excludeGlobal gitignore configuration
.ignorefiles (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_includeto add to it.Note
When one or more parsers are declared under
[parse.parsers.*], file discovery is derived from the parsers’ ownincludeglobs 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
excludestarts walking, routing, parsing and indexingnode_modulesand the rest. Useextend_excludeto 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.
excludeitself 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_excludedoes.includeitself 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 onlydirectory/foo.rst— matches “foo.rst” in “directory” under each rootdirectory/*.rst— matches every RST file under “directory” at any depthdocs/**/*.rst— matches every RST file under “docs” at any depth
Note
*crosses/in these patterns, sodirectory/*.rstis recursive — it is not limited to files directly inside “directory”.**is therefore not what makes a pattern recursive here, which differs from Sphinx’sexclude_patterns.A project still on the deprecated [project] srcdir is the exception, and unchanged: its relative patterns anchor at the directory containing
ubproject.tomland 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.rstwithmount_at = "api"becomes the documentapi/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.rstwithmount_at = "api"becomesapi/pageregardless of thea/in its path.There is no walk, so
includeandexcludefilter nothing here — the list is the selection. Setting them is reported asconfig.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_atthat is not a relative document name is reported (config.mount_invalid) and the mount is dropped. ubCode has no configuration-time equivalent ofsphinx-build -W, and a dropped mount is visible inubc 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:
the host source directory, ahead of every mount;
then the earliest declared mount;
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
includeis anchored at the project, so an inherited pattern that names a path —docs/**/*.rstrather 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 asgitignorebelow, from the other direction.- exclude
Type:
array(default:[])Glob patterns excluding files under the mount. The project’s own
excludelist 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 vendorednode_modulesinside the bundle.- gitignore
Type:
boolean(default:true)Whether to respect
.gitignorefiles while walking this mount.Set it to
falsefor a generated tree that a.gitignoredeliberately hides — abazel-bin/output directory is the usual case. Withtrue(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’stoctreedirectives receives the entry, counting from 0 in document order. A host document with notoctreeat 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_topoints at. For adirmount this is normally the bundle’s own index page, whosetoctreethen 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, infilesorder. 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
filesmount with anattach_to. On adirmount, withoutattach_to, or beside a non-defaultentry_docit is reported (config.mount_dead_option) and ignored — in the last caseattach_eachwins, since it attaches every listed file including the oneentry_docnamed.
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, nestedvar.*access, and the Python-spelled literalsTrueandFalse. Every field must be rooted atvar, 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_tohost, and reportsconfig.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
filesmount exactly as it gates adirmount. Gating one file of afilesmount 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_gatedthe condition is outside the grammar
gated off in every variant;
config.mount_invalid_conditionthe condition cannot be evaluated (an unknown
var.*key)gated off;
config.mount_condition_unevaluablegating it would remove
[project] root_docthe 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
filesmount (where the list is the selection, so the answer is exact), or adirmount withgitignore = falseand 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
.gitignorefiles,.git/info/exclude, your global excludes), with parsers declared it is the parser router — notinclude— 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 ordinarytoctree.missing_root_documentwarning 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 thantoctree.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, andconfig.mount_gated_contestednames the contested document and what claims it.A contest has two sources, and the remedy differs:
another mount — two bundles sharing one
mount_atwith mutually exclusive conditions, where both have anindex. Give them distinctmount_atvalues.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_atprefix 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-buildreads this key only through sphinx-mounts, at a release that supports it; without that it builds the bundle anyway. Declaring the key therefore always reportsconfig.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.