arXiv and Zenodo
The main goal of this workflow is to automate submission tasks and make the resulting packages reproducible, that is, something that compiles in another system (e.g., arXiv) or that has identical checksums for identical content (Zenodo).
arXiv
--dry-run runs the inventory and the checks and writes nothing, so it is safe in CI, and it
exits with 1 if anything is wrong.
$ research-helpers arxiv --dry-run
paper.tex 125.3 KB
paper.bbl 341.2 KB
tables/annotation-effort-efficiency.tex 1.2 KB
tables/consistency.tex 1.1 KB
...
figures/pos-distribution.png 83.3 KB
18 files, 1.21 MB
bbl format 3.3 (TeX Live 2025)
ready to upload: select xelatex and TeX Live 2025
(dry run, nothing written)
Omit --dry-run to write to the submission directory. Include --tar to pack it, and --preview
to compile it with the engine arXiv will use, so the output can be verified.
manifest() keys every file by the path it takes inside the
submission, relative to the manuscript’s directory. So a table installed at tex/tables/scores.tex
and read as \input{tables/scores} is uploaded as tables/scores.tex and resolves identically on
arXiv.
From the tables and figures directories, only the files read by the manuscript are uploaded, i.e., those
used by \input or \includegraphics, outside a comment. Files that the manuscript does
not read, such as a table left behind when its \input was removed, are listed in the report:
$ research-helpers arxiv --dry-run
paper.tex 0.3 KB
paper.bbl 0.1 KB
tables/scores.tex 0.0 KB
figures/learning-curve.png 16.3 KB
4 files, 0.02 MB
bbl format 3.3 (TeX Live 2025)
not uploaded, since paper.tex does not read them: tables/ablation.tex
ready to upload: select xelatex and TeX Live 2025
(dry run, nothing written)
A float read in some other way, e.g., through a macro, is also omitted, and --preview
shows that the submission cannot compile without it. To include them, write the reference out,
or pass the file to manifest() in extra. A package that the manuscript
loads from its own directory, such as the rhtextmath.sty that research-helpers sty copies there, is
uploaded with it.
A second document
A second document is submitted with --document and its name. Its submission is
built in its own build directory, so the paper’s is left as is:
$ research-helpers arxiv --document note --tar
note.tex 0.1 KB
note.bbl 0.1 KB
tables/rates.tex 0.0 KB
3 files, 0.00 MB
bbl format 3.3 (TeX Live 2025)
ready to upload: select xelatex and TeX Live 2025
wrote build/note/arxiv/
packed build/note/arxiv.tar.gz (0.00 MB)
What the check finds
check() returns every reason why the submission would be rejected
or would fail to build:
A filename arXiv will not take. Only [A-Za-z0-9_+,=.-] are permitted.
A missing or stale .bbl. arXiv does not run biber, so an uploaded .bbl is the
bibliography.
microtype font expansion under XeTeX. expansion=true is a hard error under xelatex and
xetex, which have no font expansion.
A marker left in the text. A project can list text that must not reach a submission, such as the placeholders it writes for a page number or a citation still to be found:
[tool.research-helpers.arxiv]
markers = ["[PAGE]", "[CITATION NEEDED]"]
Each is looked for in every TeX file uploaded, the .bbl included, outside comments:
$ research-helpers arxiv --dry-run
paper.tex 0.3 KB
paper.bbl 0.1 KB
tables/scores.tex 0.0 KB
figures/learning-curve.png 16.3 KB
4 files, 0.02 MB
bbl format 3.3 (TeX Live 2025)
not uploaded, since paper.tex does not read them: tables/ablation.tex
1 problem(s):
- marker [PAGE] left in paper.tex: line 4
No marker is looked for unless one is configured.
Reproducible archives
research_helpers.archive writes tarballs that are byte-identical across runs and
machines. A tarball by default records mtimes, uids, gids, and the compressing machine’s filename
in the gzip header, and uses the order the filesystem returned. Two archives of identical content
thus get different checksums.
write_archive() pins each of those: mtime to a fixed epoch, uid
and gid to zero. It also normalizes modes, sorts members, sets GNU_FORMAT, and empties the gzip
header’s filename field.
from research_helpers.archive import archive_directory, digest, write_checksums
archive = archive_directory('build/deposit.tar.gz', 'build/deposit')
write_checksums([archive], 'build/SHA256SUMS')
digest() always gives the same answer for the same content.