Corpus Research: required_files Mechanism (AI Generated)#
This is a deep-dive companion to the Remote Execution Checklist.
It explains what the tool-XML <required_files> block actually does once Galaxy and
Pulsar get hold of it — useful if you want to understand
why declaring your tool-directory files makes remote jobs reliable, and what happens when
you leave the block out.
The feature was introduced in galaxyproject/galaxy#12250 — “Systematic handling of remotely required tool files” — merged into the 21.09 release. It closed pulsar#113 and superseded an earlier approach in pulsar#260. The change touched the tool parser, the XSD schema, the Galaxy → Pulsar job runner, and unit tests (+353 / -3), and required a matching bump of the Pulsar client library Galaxy depends on so it would understand the new job-description field.
The reference documentation for the element lives in the Galaxy tool-XML schema docs:
required_files
and its include
/ exclude
children.
What the feature is#
<required_files> is a top-level tool-XML block that lets you explicitly declare which files in
the tool’s directory must be shipped to a remote host for the job to run. It replaces a fragile
heuristic (the “Pulsar hack”) that guessed at required files by scanning the rendered command line
for path-like strings.
XML syntax#
<tool id="my_tool" name="My Tool" version="1.0">
<requirements><!-- ... --></requirements>
<required_files>
<include path="my_script.R" />
<include path="scripts/" type="prefix" />
<exclude path="scripts/experimental.R" />
</required_files>
<!-- command, inputs, outputs ... -->
</tool>
<required_files>sits between<requirements>and<entry_points>in tool document order.It contains an ordered sequence of
<include>elements followed by<exclude>elements — all includes should be listed before excludes.Each
<include>/<exclude>has:path— a path relative to the tool’s directory (the directory containing the tool’s.xmlfile, which is not necessarily the repository root).type— howpathis matched: one ofliteral(default),prefix,glob, orregex.
<required_files>carries one attribute,extend_default_excludes(boolean, defaulttrue), which controls whether the built-in excludes (tool-data,test-data,.hg) are appended.
type (path-match) semantics#
|
Match rule (relative path vs. |
Implementation |
|---|---|---|
|
exact string equality (default) |
|
|
path starts with the string |
|
|
shell-style wildcard match |
|
|
Python |
|
The schema itself spells out the intent:
This declaration is used to define files that must be shipped from the tool directory for the tool to function properly in remote environments where the tool directory is not available to the job. … Pulsar hacks to implicitly find referenced files from the tool directory will be disabled when this block is used. A future Galaxy tool profile version may disable these hacks altogether and specifying this block for all referenced files should be considered a best practice.
How it’s implemented in Galaxy#
The RequiredFiles domain object#
The PR introduced a RequiredFiles class in
tool_util/parser/interface.py.
It holds includes, excludes, and extend_default_excludes, and its core method resolves the
declaration against a real directory:
def find_required_files(self, tool_directory: str) -> List[str]:
def matches(ie_list, rel_path):
for ie_item in ie_list:
ie_item_path = ie_item["path"]
ie_item_type = ie_item.get("path_type", "literal")
if ie_item_type == "literal":
if rel_path == ie_item_path: return True
elif ie_item_type == "prefix":
if rel_path.startswith(ie_item_path): return True
elif ie_item_type == "glob":
if fnmatch.fnmatch(rel_path, ie_item_path): return True
else: # regex
if re.match(ie_item_path, rel_path) is not None: return True
return False
excludes = self.excludes
if self.extend_default_excludes:
excludes.append({"path": "tool-data", "path_type": "prefix"})
excludes.append({"path": "test-data", "path_type": "prefix"})
excludes.append({"path": ".hg", "path_type": "prefix"})
files = []
for (dirpath, _, filenames) in safe_walk(tool_directory):
for filename in filenames:
rel_path = join(dirpath, filename).replace(tool_directory + os.path.sep, '')
if matches(self.includes, rel_path) and not matches(self.excludes, rel_path):
files.append(rel_path)
return files
A few things worth knowing as an author:
It walks the whole tool directory (using a symlink-safe walk that stays inside the directory) and returns every relative path that matches an include and does not match an exclude.
When
extend_default_excludesis true, the default excludestool-data/*,test-data/*, and.hg/*are always appended — reference data, test data, and Mercurial internals should never be shipped to a compute node.Because the class lives in
galaxy.tool_util.parser, it ships in the standalonegalaxy-tool-utilpackage and can be imported by Pulsar without pulling in the full Galaxy application. Galaxy core and Pulsar run the same resolution code.
Parsing the XML#
An abstract hook parse_required_files() -> Optional[RequiredFiles] was added to the ToolSource
interface, returning None by default so tools without the block fall back to the implicit logic.
The XML implementation lives in
tool_util/parser/xml.py.
One gotcha to note: the XML attribute type is normalized to the internal dict key path_type.
Wiring into the Tool object and implicit defaults#
During Tool.parse, in
galaxy/tools/__init__.py,
Galaxy resolves the tool’s required_files, falling back to a curated table when the tool declares
nothing:
required_files = tool_source.parse_required_files()
if required_files is None:
old_id = self.old_id
if old_id in IMPLICITLY_REQUIRED_TOOL_FILES:
lineage_requirement = IMPLICITLY_REQUIRED_TOOL_FILES[old_id]
lineage_requirement_until = lineage_requirement.get("version")
if lineage_requirement_until is None or self.version_object < lineage_requirement_until:
required_files = RequiredFiles.from_dict(lineage_requirement["required"])
self.required_files = required_files
IMPLICITLY_REQUIRED_TOOL_FILES is a hardcoded registry of legacy tools that reference files at
runtime but predate the <required_files> block — so they keep working under Pulsar without editing
their (often shed-installed, version-frozen) XML. Each entry maps a tool’s old_id to a
RequiredFiles-shaped dict, optionally gated by a maximum version so the implicit default only
applies to versions older than the one that added an explicit block.
The registry shipped with a single entry —
deseq2 below
2.11.40.6, requiring *.R — and had grown to 16 entries as of 2026-07. It uses two convenient
shapes: REQUIRE_FULL_DIRECTORY (an ** glob that ships everything, used by e.g.
circos) and
per-tool globs like {"path": "utils/*", "path_type": "glob"}. Adding an entry here — rather than
patching frozen shed XML — is the intended maintenance path when a legacy tool is found to break
under Pulsar.
Handoff to the Pulsar runner#
The Pulsar job runner
reads job_wrapper.tool.required_files and passes it straight into the client job description
submitted to Pulsar:
tool_directory_required_files = job_wrapper.tool.required_files
client_job_description = ClientJobDescription(
...
tool_directory_required_files=tool_directory_required_files,
)
So the resolved RequiredFiles object itself (not a pre-computed file list) crosses from Galaxy
into the Pulsar client, which runs find_required_files against the real on-disk tool directory
during staging.
Why it matters for remote execution#
The staging problem#
Pulsar runs Galaxy jobs on a remote host that does not share Galaxy’s filesystem. Before a job
runs, Pulsar must copy (“stage up”) everything it needs: input datasets, config files, and —
critically — files from the tool’s own installation directory that the command line invokes
(wrapper scripts, R/Python helpers, config templates, utils/ modules, and so on). Galaxy exposes
the tool directory to the command template as $__tool_directory__. On a shared filesystem that
path just works; on a remote node those files are absent unless Pulsar ships them.
The legacy “hack” this replaces#
Before this PR, Pulsar guessed the tool files by scanning job inputs (the rendered command line and
config files) for substrings that looked like paths under the tool directory. That logic still
exists as the fallback branch in Pulsar’s
client/staging/up.py:
JobInputs.find_referenced_subfiles builds a regex and returns every path-like token it finds in
the command line and config contents.
This is brittle:
It only finds files whose full path literally appears in the command string. A script that is invoked but whose path is assembled at runtime, or that is referenced only from another script, is missed — the job then fails on the remote host with a missing-file error (the class of bug behind pulsar#113).
It can over-match (grabbing unrelated path-like tokens) or mis-tokenize paths with spaces or quotes.
It gives you no control — no way to add a file the heuristic can’t see, or exclude a large reference-data tree it wrongly grabs.
How required_files plugs into staging#
ClientJobDescription gained a tool_directory_required_files field (defined in Pulsar’s
client/staging/__init__.py)
carrying the RequiredFiles object. During stage-up, FileStager.__initialize_referenced_tool_files
branches on it:
def __initialize_referenced_tool_files(self):
if self.tool_directory_required_files:
self.referenced_tool_files = [
(join(self.tool_dir, x), x)
for x in self.tool_directory_required_files.find_required_files(self.tool_dir)
]
else:
# legacy heuristic fallback: scan command line / configs for path-like tokens
...
When an explicit (or implicit-registry) RequiredFiles is present, Pulsar entirely bypasses the
command-line-scanning heuristic and instead walks the tool directory selecting exactly the
declared files. Those files are then transferred with their relative layout preserved, landing in
the tool directory on the remote node. The net effect is deterministic, author-controlled,
correct staging instead of a best-effort guess — which is exactly what “systematic handling of
remotely required tool files” means.
Because RequiredFiles lives in galaxy-tool-util, the code Galaxy uses to parse the XML is the
same code Pulsar uses to resolve files against the directory — no duplicated logic and no serialized
file list to keep in sync.
How the feature has evolved since merge#
The parser and
RequiredFilesclass are essentially unchanged.find_required_files, the fourtypesemantics, and the default-exclude set (tool-data,test-data,.hg) work as they did in the original PR; only cosmetic formatting differs.The implicit-defaults registry has grown.
IMPLICITLY_REQUIRED_TOOL_FILESshipped with one entry (deseq2) and now carries roughly 17 legacy Galaxy and IUC tools — for examplecircos(ship the whole directory), thequery_tabularfamily (*.py), thegops_*/fasta_*legacy tools (utils/*), andshasta(configs/*).The
typeset is stable. No new match types were added; globs already cover the**,*.ext, anddir/*cases the registry uses.The Pulsar side is stable. The
tool_directory_required_filesfield and theFileStagerbranch remain as introduced, and a Pulsar integration test (test_integration_explicit_tool_directory_includes) exercises the explicit-declaration path.
No profile-version enforcement has been switched on yet: the schema still only warns that a future
tool profile may disable the implicit heuristic. As of 2026-07 the legacy scan is still the default when no
<required_files> block or registry entry applies — which is exactly why declaring the block
yourself is the reliable choice.
Concrete XML examples#
Drawn from the schema and the feature’s unit tests:
Single literal file (default type="literal"):
<required_files>
<include path="my_script.R" />
</required_files>
Glob include with a literal exclude — ship every .R except one:
<required_files>
<include path="*.R" type="glob" />
<exclude path="other_script.R" />
</required_files>
Regex include + glob exclude:
<required_files>
<include path=".*R" type="regex" />
<exclude path="other_script*" type="glob" />
</required_files>
Disable the default excludes — needed only if you genuinely must ship something under .hg/,
test-data/, or tool-data/:
<required_files extend_default_excludes="false">
<include path="*.R" type="glob" />
</required_files>
Prefix (subdirectory) include — ship a whole helper directory:
<required_files>
<include path="scripts/" type="prefix" />
</required_files>
Ship the entire tool directory (the equivalent of the registry’s REQUIRE_FULL_DIRECTORY):
<required_files>
<include path="**" type="glob" />
</required_files>
References#
Galaxy core:
tool_util/parser/interface.py— theRequiredFilesclass,find_required_files, and theparse_required_fileshooktool_util/parser/xml.py— XML parsing of the blocktool_util/xsd/galaxy.xsd— schema andtypeenumgalaxy/tools/__init__.py— theIMPLICITLY_REQUIRED_TOOL_FILESregistry and resolutiongalaxy/jobs/runners/pulsar.py— passes the object intoClientJobDescriptiontest/unit/tool_util/test_required_files.py— unit tests
Pulsar:
client/staging/__init__.py—ClientJobDescription.tool_directory_required_filesclient/staging/up.py— the explicit-vs-legacyFileStagerbranch and the heuristic being replacedtest/integration_test.py—test_integration_explicit_tool_directory_includes
Documentation:
galaxyproject/galaxy#12250 — the PR that introduced the feature